• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / 68ef226c-f83e-44c1-b8ed-e420a83c5d84

28 May 2025 10:31AM UTC coverage: 10.352% (-80.0%) from 90.316%
68ef226c-f83e-44c1-b8ed-e420a83c5d84

Pull #3460

circleci

pubuzhixing8
chore: xxx
Pull Request #3460: refactor(icon): migrate signal input #TINFR-1476

132 of 6823 branches covered (1.93%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 1 file covered. (71.43%)

11648 existing lines in 344 files now uncovered.

2078 of 14525 relevant lines covered (14.31%)

6.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.13
/src/tree-select/tree-select.component.ts
1
import {
2
    EXPANDED_DROPDOWN_POSITIONS,
3
    injectPanelEmptyIcon,
4
    scaleYMotion,
5
    TabIndexDisabledControlValueAccessorMixin,
6
    ThyClickDispatcher
7
} from 'ngx-tethys/core';
8
import { ThyEmpty } from 'ngx-tethys/empty';
9
import { ThyFlexibleText } from 'ngx-tethys/flexible-text';
10
import { ThyIcon } from 'ngx-tethys/icon';
11
import { ThySelectControl, ThyStopPropagationDirective } from 'ngx-tethys/shared';
12
import { ThyTreeNode } from 'ngx-tethys/tree';
13
import { coerceBooleanProperty, elementMatchClosest, isArray, isObject, produce, warnDeprecation } from 'ngx-tethys/util';
14
import { Observable, of, Subject } from 'rxjs';
15
import { take, takeUntil } from 'rxjs/operators';
16

17
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectionPositionPair, ViewportRuler } from '@angular/cdk/overlay';
18
import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
×
UNCOV
19
import { isPlatformBrowser, NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
×
UNCOV
20
import {
×
UNCOV
21
    ChangeDetectorRef,
×
UNCOV
22
    Component,
×
23
    ContentChild,
UNCOV
24
    ElementRef,
×
UNCOV
25
    EventEmitter,
×
UNCOV
26
    forwardRef,
×
UNCOV
27
    HostBinding,
×
UNCOV
28
    Input,
×
29
    NgZone,
30
    OnDestroy,
UNCOV
31
    OnInit,
×
32
    Output,
UNCOV
33
    PLATFORM_ID,
×
UNCOV
34
    TemplateRef,
×
35
    ViewChild,
36
    inject,
37
    Signal
38
} from '@angular/core';
39
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
40

41
import { ThyTreeSelectNode, ThyTreeSelectType } from './tree-select.class';
1✔
42
import { injectLocale, ThyTreeSelectLocale } from 'ngx-tethys/i18n';
UNCOV
43

×
UNCOV
44
type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '';
×
UNCOV
45

×
UNCOV
46
export function filterTreeData(treeNodes: ThyTreeSelectNode[], searchText: string, searchKey: string = 'name') {
×
UNCOV
47
    const filterNodes = (node: ThyTreeSelectNode, result: ThyTreeSelectNode[]) => {
×
UNCOV
48
        if (node[searchKey] && node[searchKey].indexOf(searchText) !== -1) {
×
UNCOV
49
            result.push(node);
×
50
            return result;
51
        }
52
        if (Array.isArray(node.children)) {
53
            const nodes = node.children.reduce((previous, current) => filterNodes(current, previous), [] as ThyTreeSelectNode[]);
UNCOV
54
            if (nodes.length) {
×
55
                const parentNode = { ...node, children: nodes, expand: true };
56
                result.push(parentNode);
UNCOV
57
            }
×
58
        }
59
        return result;
60
    };
×
61
    const treeData = treeNodes.reduce((previous, current) => filterNodes(current, previous), [] as ThyTreeSelectNode[]);
×
62
    return treeData;
63
}
64

65
/**
66
 * 树选择组件
67
 * @name thy-tree-select
68
 * @order 10
69
 */
UNCOV
70
@Component({
×
UNCOV
71
    selector: 'thy-tree-select',
×
72
    templateUrl: './tree-select.component.html',
73
    providers: [
UNCOV
74
        {
×
75
            provide: NG_VALUE_ACCESSOR,
76
            useExisting: forwardRef(() => ThyTreeSelect),
77
            multi: true
UNCOV
78
        }
×
79
    ],
80
    imports: [
×
UNCOV
81
        CdkOverlayOrigin,
×
UNCOV
82
        ThySelectControl,
×
UNCOV
83
        NgTemplateOutlet,
×
UNCOV
84
        CdkConnectedOverlay,
×
UNCOV
85
        forwardRef(() => ThyTreeSelectNodes),
×
86
        ThyStopPropagationDirective
87
    ],
UNCOV
88
    host: {
×
89
        '[attr.tabindex]': 'tabIndex',
UNCOV
90
        '(focus)': 'onFocus($event)',
×
91
        '(blur)': 'onBlur($event)'
92
    },
UNCOV
93
    animations: [scaleYMotion]
×
UNCOV
94
})
×
UNCOV
95
export class ThyTreeSelect extends TabIndexDisabledControlValueAccessorMixin implements OnInit, OnDestroy, ControlValueAccessor {
×
96
    elementRef = inject(ElementRef);
UNCOV
97
    private ngZone = inject(NgZone);
×
98
    private ref = inject(ChangeDetectorRef);
99
    private platformId = inject(PLATFORM_ID);
UNCOV
100
    private thyClickDispatcher = inject(ThyClickDispatcher);
×
UNCOV
101
    private viewportRuler = inject(ViewportRuler);
×
UNCOV
102

×
UNCOV
103
    @HostBinding('class.thy-select-custom') treeSelectClass = true;
×
UNCOV
104

×
UNCOV
105
    @HostBinding('class.thy-select') isTreeSelect = true;
×
UNCOV
106

×
UNCOV
107
    // 菜单是否展开
×
UNCOV
108
    @HostBinding('class.menu-is-opened') expandTreeSelectOptions = false;
×
UNCOV
109

×
UNCOV
110
    @HostBinding('class.thy-select-custom--multiple') isMulti = false;
×
UNCOV
111

×
UNCOV
112
    public treeNodes: ThyTreeSelectNode[];
×
UNCOV
113

×
UNCOV
114
    public selectedValue: any;
×
UNCOV
115

×
UNCOV
116
    public selectedNode: ThyTreeSelectNode;
×
117

118
    public selectedNodes: ThyTreeSelectNode[] = [];
119

120
    public flattenTreeNodes: ThyTreeSelectNode[] = [];
UNCOV
121

×
UNCOV
122
    virtualTreeNodes: ThyTreeSelectNode[] = [];
×
UNCOV
123

×
UNCOV
124
    public cdkConnectOverlayWidth = 0;
×
UNCOV
125

×
UNCOV
126
    public expandedDropdownPositions: ConnectionPositionPair[] = EXPANDED_DROPDOWN_POSITIONS;
×
UNCOV
127

×
UNCOV
128
    public icons: { expand: string; collapse: string; gap?: number } = {
×
UNCOV
129
        expand: 'angle-down',
×
UNCOV
130
        collapse: 'angle-right',
×
UNCOV
131
        gap: 15
×
UNCOV
132
    };
×
UNCOV
133

×
UNCOV
134
    private initialled = false;
×
UNCOV
135

×
UNCOV
136
    private destroy$ = new Subject<void>();
×
UNCOV
137

×
UNCOV
138
    private locale: Signal<ThyTreeSelectLocale> = injectLocale('treeSelect');
×
UNCOV
139

×
UNCOV
140
    public valueIsObject = false;
×
UNCOV
141

×
142
    originTreeNodes: ThyTreeSelectNode[];
143

UNCOV
144
    @ContentChild('thyTreeSelectTriggerDisplay')
×
UNCOV
145
    thyTreeSelectTriggerDisplayRef: TemplateRef<any>;
×
UNCOV
146

×
UNCOV
147
    @ContentChild('treeNodeTemplate')
×
UNCOV
148
    treeNodeTemplateRef: TemplateRef<any>;
×
UNCOV
149

×
150
    @ViewChild(CdkOverlayOrigin, { static: true }) cdkOverlayOrigin: CdkOverlayOrigin;
UNCOV
151

×
UNCOV
152
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
×
153

154
    @ViewChild('customDisplayTemplate', { static: true }) customDisplayTemplate: TemplateRef<any>;
155

UNCOV
156
    /**
×
UNCOV
157
     * treeNodes 数据
×
UNCOV
158
     * @type ThyTreeSelectNode[]
×
UNCOV
159
     */
×
UNCOV
160
    @Input()
×
161
    set thyTreeNodes(value: ThyTreeSelectNode[]) {
162
        this.treeNodes = value;
163
        this.originTreeNodes = value;
164
        if (this.initialled) {
UNCOV
165
            this.flattenTreeNodes = this.flattenNodes(this.treeNodes, this.flattenTreeNodes, []);
×
166
            this.setSelectedNodes();
167

168
            if (this.thyVirtualScroll) {
UNCOV
169
                this.buildFlattenTreeNodes();
×
170
            }
171
        }
172
    }
UNCOV
173

×
UNCOV
174
    /**
×
175
     * 开启虚拟滚动
176
     */
177
    @Input({ transform: coerceBooleanProperty }) thyVirtualScroll: boolean = false;
178

UNCOV
179
    /**
×
UNCOV
180
     * 树节点的唯一标识
×
181
     * @type string
UNCOV
182
     */
×
183
    @Input() thyPrimaryKey = '_id';
184

UNCOV
185
    /**
×
186
     * 树节点的显示的字段 key
187
     * @type string
UNCOV
188
     */
×
189
    @Input() thyShowKey = 'name';
190

UNCOV
191
    @Input() thyChildCountKey = 'childCount';
×
192

193
    /**
UNCOV
194
     * 单选时,是否显示清除按钮,当为 true 时,显示清除按钮
×
195
     * @default false
196
     */
197
    @Input({ transform: coerceBooleanProperty }) thyAllowClear: boolean;
UNCOV
198

×
199
    /**
200
     * 是否多选
201
     * @type boolean
UNCOV
202
     */
×
203
    @Input({ transform: coerceBooleanProperty }) thyMultiple = false;
204

×
UNCOV
205
    /**
×
UNCOV
206
     * 是否禁用树选择器,当为 true 禁用树选择器
×
UNCOV
207
     * @type boolean
×
UNCOV
208
     */
×
UNCOV
209
    @Input({ transform: coerceBooleanProperty }) thyDisable = false;
×
UNCOV
210

×
UNCOV
211
    get thyDisabled(): boolean {
×
UNCOV
212
        return this.thyDisable;
×
213
    }
214

UNCOV
215
    /**
×
216
     * 树选择框默认文字
217
     * @type string
UNCOV
218
     */
×
219
    @Input() thyPlaceholder = this.locale().placeholder;
220

UNCOV
221
    get placeholder() {
×
222
        return this.thyPlaceholder;
UNCOV
223
    }
×
UNCOV
224

×
UNCOV
225
    /**
×
UNCOV
226
     * 控制树选择的输入框大小
×
UNCOV
227
     * @type xs | sm | md | default | lg
×
228
     */
229
    @Input() thySize: InputSize;
230

UNCOV
231
    /**
×
UNCOV
232
     * 改变空选项的情况下的提示文本
×
233
     * @type string
234
     */
235
    @Input() thyEmptyOptionsText = this.locale().empty;
236

237
    /**
238
     * 设置是否隐藏节点(不可进行任何操作),优先级高于 thyHiddenNodeFn
UNCOV
239
     * @type string
×
UNCOV
240
     */
×
UNCOV
241
    @Input() thyHiddenNodeKey = 'hidden';
×
242

243
    /**
244
     * 设置是否禁用节点(不可进行任何操作),优先级高于 thyDisableNodeFn
UNCOV
245
     * @type string
×
246
     */
247
    @Input() thyDisableNodeKey = 'disabled';
248

249
    /**
UNCOV
250
     * 是否异步加载节点的子节点(显示加载状态),当为 true 时,异步获取
×
UNCOV
251
     * @type boolean
×
252
     */
253
    @Input({ transform: coerceBooleanProperty }) thyAsyncNode = false;
254

UNCOV
255
    /**
×
UNCOV
256
     * 是否展示全名
×
257
     * @type boolean
UNCOV
258
     */
×
UNCOV
259
    @Input({ transform: coerceBooleanProperty }) thyShowWholeName = false;
×
UNCOV
260

×
261
    /**
262
     * 是否展示搜索
UNCOV
263
     * @type boolean
×
UNCOV
264
     */
×
UNCOV
265
    @Input({ transform: coerceBooleanProperty }) thyShowSearch = false;
×
UNCOV
266

×
267
    /**
268
     * 图标类型,支持 default | especial,已废弃
269
     * @deprecated
UNCOV
270
     */
×
UNCOV
271
    @Input()
×
UNCOV
272
    set thyIconType(type: ThyTreeSelectType) {
×
UNCOV
273
        if (typeof ngDevMode === 'undefined' || ngDevMode) {
×
UNCOV
274
            warnDeprecation('This parameter has been deprecation');
×
275
        }
276
        // if (type === 'especial') {
UNCOV
277
        //     this.icons = { expand: 'minus-square', collapse: 'plus-square', gap: 20 };
×
278
        // } else {
×
279
        //     this.icons = { expand: 'caret-right-down', collapse: 'caret-right', gap: 15 };
280
        // }
UNCOV
281
    }
×
UNCOV
282

×
283
    /**
284
     * 设置是否隐藏节点(不可进行任何操作),优先级低于 thyHiddenNodeKey。
UNCOV
285
     * @default (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.hidden
×
UNCOV
286
     */
×
UNCOV
287
    @Input() thyHiddenNodeFn: (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.hidden;
×
288

289
    /**
290
     * 设置是否禁用节点(不可进行任何操作),优先级低于 thyDisableNodeKey。
UNCOV
291
     * @default (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.disabled
×
292
     */
293
    @Input() thyDisableNodeFn: (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.disabled;
294

UNCOV
295
    /**
×
UNCOV
296
     * 获取节点的子节点,返回 Observable<ThyTreeSelectNode>。
×
297
     * @default (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([])
UNCOV
298
     */
×
299
    @Input() thyGetNodeChildren: (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([]);
×
300

UNCOV
301
    /**
×
UNCOV
302
     * 树选择组件展开和折叠状态事件
×
UNCOV
303
     */
×
304
    @Output() thyExpandStatusChange: EventEmitter<boolean> = new EventEmitter<boolean>();
UNCOV
305

×
306
    private _getNgModelType() {
307
        if (this.thyMultiple) {
308
            this.valueIsObject = !this.selectedValue[0] || isObject(this.selectedValue[0]);
UNCOV
309
        } else {
×
UNCOV
310
            this.valueIsObject = isObject(this.selectedValue);
×
UNCOV
311
        }
×
UNCOV
312
    }
×
UNCOV
313

×
314
    public buildFlattenTreeNodes() {
315
        this.virtualTreeNodes = this.getFlattenTreeNodes(this.treeNodes);
UNCOV
316
    }
×
UNCOV
317

×
318
    private getFlattenTreeNodes(rootTrees: ThyTreeSelectNode[] = this.treeNodes) {
UNCOV
319
        const forEachTree = (tree: ThyTreeSelectNode[], fn: any, result: ThyTreeSelectNode[] = []) => {
×
320
            tree.forEach(item => {
321
                result.push(item);
UNCOV
322
                if (item.children && fn(item)) {
×
UNCOV
323
                    forEachTree(item.children, fn, result);
×
324
                }
325
            });
326
            return result;
327
        };
UNCOV
328
        return forEachTree(rootTrees, (node: ThyTreeSelectNode) => !!node.expand);
×
UNCOV
329
    }
×
UNCOV
330

×
UNCOV
331
    writeValue(value: any): void {
×
UNCOV
332
        this.selectedValue = value;
×
UNCOV
333

×
UNCOV
334
        if (value) {
×
335
            this._getNgModelType();
336
        }
UNCOV
337
        this.setSelectedNodes();
×
UNCOV
338
    }
×
339

UNCOV
340
    constructor() {
×
341
        super();
342
    }
343

1✔
344
    ngOnInit() {
1✔
345
        this.isMulti = this.thyMultiple;
346
        this.flattenTreeNodes = this.flattenNodes(this.treeNodes, this.flattenTreeNodes, []);
347
        this.setSelectedNodes();
348
        this.initialled = true;
349

350
        if (this.thyVirtualScroll) {
351
            this.buildFlattenTreeNodes();
352
        }
353

354
        if (isPlatformBrowser(this.platformId)) {
355
            this.thyClickDispatcher
356
                .clicked(0)
357
                .pipe(takeUntil(this.destroy$))
358
                .subscribe(event => {
359
                    event.stopPropagation();
360
                    if (!this.elementRef.nativeElement.contains(event.target) && this.expandTreeSelectOptions) {
361
                        this.ngZone.run(() => {
362
                            this.close();
363
                            this.ref.markForCheck();
364
                        });
365
                    }
366
                });
367
        }
368
        this.viewportRuler
369
            .change()
370
            .pipe(takeUntil(this.destroy$))
371
            .subscribe(() => {
372
                this.init();
373
            });
374
    }
375

376
    onFocus($event: FocusEvent) {
377
        const inputElement: HTMLInputElement = this.elementRef.nativeElement.querySelector('input');
1✔
378
        inputElement?.focus();
379
    }
380

381
    onBlur($event: FocusEvent) {
382
        // 1. Tab 聚焦后自动聚焦到 input 输入框,此分支下直接返回,无需触发 onTouchedFn
383
        // 2. 打开选择框后如果点击弹框内导致 input 失焦,无需触发 onTouchedFn
UNCOV
384
        if (elementMatchClosest($event?.relatedTarget as HTMLElement, ['thy-tree-select', 'thy-tree-select-nodes'])) {
×
385
            return;
386
        }
387
        this.onTouchedFn();
388
    }
389

390
    ngOnDestroy(): void {
391
        this.destroy$.next();
392
    }
UNCOV
393

×
394
    get selectedValueObject() {
395
        return this.thyMultiple ? this.selectedNodes : this.selectedNode;
396
    }
397

398
    searchValue(searchText: string) {
399
        this.treeNodes = filterTreeData(this.originTreeNodes, searchText.trim(), this.thyShowKey);
400
    }
401

402
    public setPosition() {
403
        this.ngZone.onStable
404
            .asObservable()
405
            .pipe(take(1))
406
            .subscribe(() => {
1✔
407
                this.cdkConnectedOverlay.overlayRef.updatePosition();
408
            });
409
    }
410

1✔
411
    private init() {
UNCOV
412
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
×
UNCOV
413
    }
×
UNCOV
414

×
UNCOV
415
    private flattenNodes(
×
UNCOV
416
        nodes: ThyTreeSelectNode[] = [],
×
UNCOV
417
        resultNodes: ThyTreeSelectNode[] = [],
×
UNCOV
418
        parentPrimaryValue: string[] = []
×
UNCOV
419
    ): ThyTreeSelectNode[] {
×
UNCOV
420
        resultNodes = resultNodes.concat(nodes);
×
UNCOV
421
        let nodesLeafs: ThyTreeSelectNode[] = [];
×
UNCOV
422
        (nodes || []).forEach(item => {
×
UNCOV
423
            item.parentValues = parentPrimaryValue;
×
UNCOV
424
            item.level = item.parentValues.length;
×
UNCOV
425
            if (item.children && isArray(item.children)) {
×
426
                const nodeLeafs = this.flattenNodes(item.children, resultNodes, [...parentPrimaryValue, item[this.thyPrimaryKey]]);
427
                nodesLeafs = [...nodesLeafs, ...nodeLeafs];
UNCOV
428
            }
×
429
        });
UNCOV
430
        return [...nodes, ...nodesLeafs];
×
UNCOV
431
    }
×
UNCOV
432

×
433
    private _findTreeNode(value: string): ThyTreeSelectNode {
434
        return (this.flattenTreeNodes || []).find(item => item[this.thyPrimaryKey] === value);
UNCOV
435
    }
×
436

437
    private setSelectedNodes() {
UNCOV
438
        if (this.selectedValue) {
×
UNCOV
439
            // 多选数据初始化
×
UNCOV
440
            if (this.thyMultiple) {
×
441
                if (this.selectedValue.length > 0) {
442
                    if (this.valueIsObject && Object.keys(this.selectedValue[0]).indexOf(this.thyPrimaryKey) >= 0) {
443
                        this.selectedNodes = this.selectedValue.map((item: any) => {
UNCOV
444
                            return this._findTreeNode(item[this.thyPrimaryKey]);
×
445
                        });
446
                    } else {
447
                        this.selectedNodes = this.selectedValue.map((item: any) => {
UNCOV
448
                            return this._findTreeNode(item);
×
UNCOV
449
                        });
×
450
                    }
UNCOV
451
                }
×
UNCOV
452
            } else {
×
453
                // 单选数据初始化
454
                if (this.valueIsObject) {
×
455
                    if (Object.keys(this.selectedValue).indexOf(this.thyPrimaryKey) >= 0) {
456
                        this.selectedNode = this._findTreeNode(this.selectedValue[this.thyPrimaryKey]);
UNCOV
457
                    }
×
UNCOV
458
                } else {
×
459
                    this.selectedNode = this._findTreeNode(this.selectedValue);
UNCOV
460
                }
×
UNCOV
461
            }
×
462
        } else {
463
            this.selectedNodes = [];
×
464
            this.selectedNode = null;
465
        }
UNCOV
466
    }
×
UNCOV
467

×
UNCOV
468
    openSelectPop() {
×
UNCOV
469
        if (this.thyDisable) {
×
470
            return;
471
        }
472
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
UNCOV
473
        this.expandTreeSelectOptions = !this.expandTreeSelectOptions;
×
474
        this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
475
    }
476

UNCOV
477
    close() {
×
UNCOV
478
        if (this.expandTreeSelectOptions) {
×
UNCOV
479
            this.expandTreeSelectOptions = false;
×
480
            this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
481
            this.onTouchedFn();
UNCOV
482
        }
×
483
    }
484

UNCOV
485
    clearSelectedValue(event: Event) {
×
UNCOV
486
        event.stopPropagation();
×
487
        this.selectedValue = null;
488
        this.selectedNode = null;
489
        this.selectedNodes = [];
UNCOV
490
        this.onChangeFn(this.selectedValue);
×
UNCOV
491
    }
×
UNCOV
492

×
493
    private _changeSelectValue() {
494
        if (this.valueIsObject) {
495
            this.selectedValue = this.thyMultiple ? this.selectedNodes : this.selectedNode;
×
496
        } else {
×
497
            this.selectedValue = this.thyMultiple
498
                ? this.selectedNodes.map(item => item[this.thyPrimaryKey])
499
                : this.selectedNode[this.thyPrimaryKey];
×
500
        }
501
        this.onChangeFn(this.selectedValue);
UNCOV
502
        if (!this.thyMultiple) {
×
UNCOV
503
            this.onTouchedFn();
×
UNCOV
504
        }
×
505
    }
506

507
    removeMultipleSelectedNode(event: { item: ThyTreeSelectNode; $eventOrigin: Event }) {
UNCOV
508
        this.removeSelectedNode(event.item, event.$eventOrigin);
×
509
    }
×
510

511
    // thyMultiple = true 时,移除数据时调用
512
    removeSelectedNode(node: ThyTreeSelectNode, event?: Event) {
UNCOV
513
        if (event) {
×
514
            event.stopPropagation();
515
        }
1✔
516
        if (this.thyDisable) {
517
            return;
518
        }
519
        if (this.thyMultiple) {
520
            this.selectedNodes = produce(this.selectedNodes).remove((item: ThyTreeSelectNode) => {
521
                return item[this.thyPrimaryKey] === node[this.thyPrimaryKey];
1✔
522
            });
523
            this._changeSelectValue();
524
        }
525
    }
526

527
    selectNode(node: ThyTreeSelectNode) {
528
        if (!this.thyMultiple) {
529
            this.selectedNode = node;
530
            this.expandTreeSelectOptions = false;
531
            this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
532
            this._changeSelectValue();
533
        } else {
534
            if (
535
                this.selectedNodes.find(item => {
536
                    return item[this.thyPrimaryKey] === node[this.thyPrimaryKey];
537
                })
538
            ) {
539
                this.removeSelectedNode(node);
540
            } else {
541
                this.selectedNodes = produce(this.selectedNodes).add(node);
542
                this._changeSelectValue();
543
            }
544
        }
545
    }
546

547
    getNodeChildren(node: ThyTreeSelectNode) {
548
        const result = this.thyGetNodeChildren(node);
549
        if (result && result.subscribe) {
550
            result.pipe().subscribe((data: ThyTreeSelectNode[]) => {
551
                const nodes = this.flattenNodes(data, this.flattenTreeNodes, [...node.parentValues, node[this.thyPrimaryKey]]);
552
                const otherNodes = nodes.filter((item: ThyTreeNode) => {
553
                    return !this.flattenTreeNodes.find(hasItem => {
554
                        return hasItem[this.thyPrimaryKey] === item[this.thyPrimaryKey];
555
                    });
556
                });
557
                this.flattenTreeNodes = [...this.flattenTreeNodes, ...otherNodes];
558
                node.children = data;
559
            });
560
            return result;
561
        }
562
    }
563
}
564

565
const DEFAULT_ITEM_SIZE = 40;
566

567
/**
568
 * @private
569
 */
570
@Component({
571
    selector: 'thy-tree-select-nodes',
572
    templateUrl: './tree-select-nodes.component.html',
573
    imports: [
574
        NgTemplateOutlet,
575
        CdkVirtualScrollViewport,
576
        CdkFixedSizeVirtualScroll,
577
        CdkVirtualForOf,
578
        ThyEmpty,
579
        NgClass,
580
        NgStyle,
581
        ThyIcon,
582
        ThyFlexibleText
583
    ],
584
    host: {
585
        '[attr.tabindex]': '-1'
586
    }
587
})
588
export class ThyTreeSelectNodes implements OnInit {
589
    parent = inject(ThyTreeSelect);
590
    emptyIcon: Signal<string> = injectPanelEmptyIcon();
591

592
    @HostBinding('class') class: string;
593

594
    nodeList: ThyTreeSelectNode[] = [];
595

596
    @Input() set treeNodes(value: ThyTreeSelectNode[]) {
597
        const treeSelectHeight = this.defaultItemSize * value.length;
598
        // 父级设置了max-height:300 & padding:10 0; 故此处最多设置280,否则将出现滚动条
599
        this.thyVirtualHeight = treeSelectHeight > 300 ? '280px' : `${treeSelectHeight}px`;
600
        this.nodeList = value;
601
        this.hasNodeChildren = this.nodeList.every(
602
            item => !item.hasOwnProperty('children') || (!item?.children?.length && !item?.childCount)
603
        );
604
    }
605

606
    @Input() thyVirtualScroll: boolean = false;
607

608
    public primaryKey = this.parent.thyPrimaryKey;
609

610
    public showKey = this.parent.thyShowKey;
611

612
    public isMultiple = this.parent.thyMultiple;
613

614
    public valueIsObject = this.parent.valueIsObject;
615

616
    public selectedValue = this.parent.selectedValue;
617

618
    public childCountKey = this.parent.thyChildCountKey;
619

620
    public treeNodeTemplateRef = this.parent.treeNodeTemplateRef;
621

622
    public defaultItemSize = DEFAULT_ITEM_SIZE;
623

624
    public thyVirtualHeight: string = null;
625

626
    public hasNodeChildren: boolean = false;
627

628
    ngOnInit() {
629
        this.class = this.isMultiple ? 'thy-tree-select-dropdown thy-tree-select-dropdown-multiple' : 'thy-tree-select-dropdown';
630
    }
631

632
    treeNodeIsSelected(node: ThyTreeSelectNode) {
633
        if (this.parent.thyMultiple) {
634
            return (this.parent.selectedNodes || []).find(item => {
635
                return item[this.primaryKey] === node[this.primaryKey];
636
            });
637
        } else {
638
            return this.parent.selectedNode && this.parent.selectedNode[this.primaryKey] === node[this.primaryKey];
639
        }
640
    }
641

642
    treeNodeIsHidden(node: ThyTreeSelectNode) {
643
        if (this.parent.thyHiddenNodeKey) {
644
            return node[this.parent.thyHiddenNodeKey];
645
        }
646
        if (this.parent.thyHiddenNodeFn) {
647
            return this.parent.thyHiddenNodeFn(node);
648
        }
649
        return false;
650
    }
651

652
    treeNodeIsDisable(node: ThyTreeSelectNode) {
653
        if (this.parent.thyDisableNodeKey) {
654
            return node[this.parent.thyDisableNodeKey];
655
        }
656
        if (this.parent.thyDisableNodeFn) {
657
            return this.parent.thyDisableNodeFn(node);
658
        }
659
        return false;
660
    }
661

662
    treeNodeIsExpand(node: ThyTreeSelectNode) {
663
        let isSelectedNodeParent = false;
664
        if (this.parent.thyMultiple) {
665
            isSelectedNodeParent = !!(this.parent.selectedNodes || []).find(item => {
666
                return item.parentValues.indexOf(node[this.primaryKey]) > -1;
667
            });
668
        } else {
669
            isSelectedNodeParent = this.parent.selectedNode
670
                ? this.parent.selectedNode.parentValues.indexOf(node[this.primaryKey]) > -1
671
                : false;
672
        }
673
        const isExpand = node.expand || (Object.keys(node).indexOf('expand') < 0 && isSelectedNodeParent);
674
        node.expand = isExpand;
675
        return isExpand;
676
    }
677

678
    getNodeChildren(node: ThyTreeSelectNode) {
679
        return this.parent.getNodeChildren(node);
680
    }
681

682
    selectTreeNode(event: Event, node: ThyTreeSelectNode) {
683
        if (!this.treeNodeIsDisable(node)) {
684
            this.parent.selectNode(node);
685
        }
686
    }
687

688
    nodeExpandToggle(event: Event, node: ThyTreeSelectNode) {
689
        event.stopPropagation();
690
        if (Object.keys(node).indexOf('expand') > -1) {
691
            node.expand = !node.expand;
692
        } else {
693
            if (this.treeNodeIsExpand(node)) {
694
                node.expand = false;
695
            } else {
696
                node.expand = true;
697
            }
698
        }
699

700
        if (node.expand && this.parent.thyAsyncNode) {
701
            this.getNodeChildren(node).subscribe(() => {
702
                this.parent.setPosition();
703
            });
704
        }
705
        // this.parent.setPosition();
706
        if (this.thyVirtualScroll) {
707
            this.parent.buildFlattenTreeNodes();
708
        }
709
    }
710

711
    tabTrackBy(index: number, item: ThyTreeSelectNode) {
712
        return index;
713
    }
714
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc