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

atinc / ngx-tethys / #99

12 Aug 2025 07:22AM UTC coverage: 90.407% (+0.07%) from 90.341%
#99

push

web-flow
refactor(tree-select): migration signal #TINFR-1785 (#3520)

5529 of 6801 branches covered (81.3%)

Branch coverage included in aggregate %.

77 of 81 new or added lines in 1 file covered. (95.06%)

3 existing lines in 1 file now uncovered.

13988 of 14787 relevant lines covered (94.6%)

910.89 hits per line

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

91.56
/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 } from 'ngx-tethys/util';
14
import { Observable, of } from 'rxjs';
15
import { take } from 'rxjs/operators';
16

17
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectionPositionPair, ViewportRuler } from '@angular/cdk/overlay';
18
import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
19
import { isPlatformBrowser, NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
20
import {
×
21
    ChangeDetectorRef,
42✔
22
    Component,
345✔
23
    ElementRef,
326✔
24
    forwardRef,
326✔
25
    Input,
26
    NgZone,
19✔
27
    PLATFORM_ID,
17✔
28
    TemplateRef,
17✔
29
    inject,
4✔
30
    Signal,
4✔
31
    output,
32
    input,
33
    effect,
19✔
34
    computed,
35
    signal,
328✔
36
    afterNextRender,
42✔
37
    DestroyRef,
38
    contentChild,
39
    viewChild
40
} from '@angular/core';
41
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
42

43
import { ThyTreeSelectNode, ThyTreeSelectType } from './tree-select.class';
1✔
44
import { injectLocale, ThyTreeSelectLocale } from 'ngx-tethys/i18n';
45
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
198✔
46

47
type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '';
48

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

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

39✔
110
    // 菜单是否展开
39✔
111
    public expandTreeSelectOptions = signal(false);
39✔
112

39✔
113
    public selectedValue = signal<any>(undefined);
39✔
114

39✔
115
    public selectedNode = signal<ThyTreeSelectNode>(null);
39✔
116

39✔
117
    public selectedNodes = signal<ThyTreeSelectNode[]>([]);
39✔
118

39✔
119
    public flattenTreeNodes = signal<ThyTreeSelectNode[]>([]);
39✔
120

39✔
121
    virtualTreeNodes = signal<ThyTreeSelectNode[]>([]);
39✔
122

39✔
123
    public cdkConnectOverlayWidth = signal(0);
39✔
124

39✔
125
    public expandedDropdownPositions: ConnectionPositionPair[] = EXPANDED_DROPDOWN_POSITIONS;
39✔
126

39✔
127
    public icons: { expand: string; collapse: string; gap?: number } = {
39✔
128
        expand: 'angle-down',
39✔
129
        collapse: 'angle-right',
61✔
130
        gap: 15
131
    };
39✔
132

39✔
133
    private locale: Signal<ThyTreeSelectLocale> = injectLocale('treeSelect');
39✔
134

66✔
135
    public valueIsObject = computed(() => {
136
        const selectedValue = this.selectedValue();
39✔
137
        if (this.thyMultiple()) {
40✔
138
            return selectedValue && (!selectedValue[0] || isObject(selectedValue[0]));
5✔
139
        } else {
140
            return isObject(selectedValue);
141
        }
39✔
142
    });
41✔
143

144
    public searchText = signal('');
145

146
    readonly thyTreeSelectTriggerDisplayRef = contentChild<TemplateRef<any>>('thyTreeSelectTriggerDisplay');
39!
147

39✔
148
    readonly treeNodeTemplateRef = contentChild<TemplateRef<any>>('treeNodeTemplate');
149

150
    readonly cdkOverlayOrigin = viewChild(CdkOverlayOrigin);
151

27✔
152
    readonly cdkConnectedOverlay = viewChild(CdkConnectedOverlay);
27✔
153

2✔
154
    readonly customDisplayTemplate = viewChild<TemplateRef<any>>('customDisplayTemplate');
2✔
155

156
    /**
157
     * treeNodes 数据
158
     * @type ThyTreeSelectNode[]
159
     */
160
    readonly thyTreeNodes = input<ThyTreeSelectNode[]>([]);
161

39✔
162
    treeNodes = computed(() => {
163
        return filterTreeData(this.thyTreeNodes(), this.searchText(), this.thyShowKey());
164
    });
165

2✔
166
    /**
167
     * 开启虚拟滚动
168
     */
169
    readonly thyVirtualScroll = input(false, { transform: coerceBooleanProperty });
1✔
170

1✔
171
    /**
172
     * 树节点的唯一标识
173
     * @type string
174
     */
175
    readonly thyPrimaryKey = input('_id');
2✔
176

1✔
177
    /**
178
     * 树节点的显示的字段 key
1✔
179
     * @type string
180
     */
181
    readonly thyShowKey = input('name');
1✔
182

183
    readonly thyChildCountKey = input('childCount');
184

1✔
185
    /**
186
     * 单选时,是否显示清除按钮,当为 true 时,显示清除按钮
187
     * @default false
188
     */
1✔
189
    readonly thyAllowClear = input(false, { transform: coerceBooleanProperty });
190

191
    /**
×
192
     * 是否多选
4,084✔
193
     * @type boolean
4,084!
194
     */
112,042✔
195
    readonly thyMultiple = input(false, { transform: coerceBooleanProperty });
112,042✔
196

112,042✔
197
    /**
112,042✔
198
     * 是否禁用树选择器,当为 true 禁用树选择器
4,042✔
199
     * @type boolean
4,042✔
200
     */
201
    readonly thyDisable = input(false, { transform: coerceBooleanProperty });
202

4,084✔
203
    get thyDisabled(): boolean {
204
        return this.thyDisable();
205
    }
98!
206

207
    /**
208
     * 树选择框默认文字
66✔
209
     * @type string
66✔
210
     */
66✔
211
    readonly thyPlaceholder = input(this.locale().placeholder);
66✔
212

66✔
213
    /**
214
     * 控制树选择的输入框大小
17✔
215
     * @type xs | sm | md | default | lg
12✔
216
     */
9✔
217
    readonly thySize = input<InputSize>(undefined);
2✔
218

3✔
219
    /**
220
     * 改变空选项的情况下的提示文本
221
     * @type string
222
     */
7✔
223
    readonly thyEmptyOptionsText = input(this.locale().empty);
8✔
224

225
    /**
226
     * 设置是否隐藏节点(不可进行任何操作),优先级高于 thyHiddenNodeFn
227
     * @type string
228
     */
229
    readonly thyHiddenNodeKey = input('hidden');
230

5✔
231
    /**
1!
232
     * 设置是否禁用节点(不可进行任何操作),优先级高于 thyDisableNodeFn
1✔
233
     * @type string
234
     */
235
    readonly thyDisableNodeKey = input('disabled');
236

4✔
237
    /**
238
     * 是否异步加载节点的子节点(显示加载状态),当为 true 时,异步获取
239
     * @type boolean
240
     */
241
    readonly thyAsyncNode = input(false, { transform: coerceBooleanProperty });
49✔
242

49✔
243
    /**
244
     * 是否展示全名
245
     * @type boolean
246
     */
14✔
247
    readonly thyShowWholeName = input(false, { transform: coerceBooleanProperty });
1✔
248

249
    /**
13✔
250
     * 是否展示搜索
13✔
251
     * @type boolean
13✔
252
     */
253
    readonly thyShowSearch = input(false, { transform: coerceBooleanProperty });
254

7✔
255
    /**
2✔
256
     * 图标类型,支持 default | especial,已废弃
2✔
257
     * @deprecated
2✔
258
     */
259
    readonly thyIconType = input<ThyTreeSelectType>(undefined);
260

261
    /**
2✔
262
     * 设置是否隐藏节点(不可进行任何操作),优先级低于 thyHiddenNodeKey。
2✔
263
     * @default (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.hidden
2✔
264
     */
2✔
265
    @Input() thyHiddenNodeFn: (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.hidden;
2✔
266

267
    /**
268
     * 设置是否禁用节点(不可进行任何操作),优先级低于 thyDisableNodeKey。
13✔
269
     * @default (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.disabled
13✔
270
     */
13✔
271
    @Input() thyDisableNodeFn: (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.disabled;
2!
272

273
    /**
274
     * 获取节点的子节点,返回 Observable<ThyTreeSelectNode>。
11✔
275
     * @default (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([])
11✔
276
     */
277
    @Input() thyGetNodeChildren: (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([]);
13✔
278

13✔
279
    /**
3✔
280
     * 树选择组件展开和折叠状态事件
281
     */
282
    readonly thyExpandStatusChange = output<boolean>();
283

1✔
284
    public buildFlattenTreeNodes() {
285
        this.virtualTreeNodes.set(this.getFlattenTreeNodes(this.treeNodes()));
286
    }
287

3✔
288
    private getFlattenTreeNodes(rootTrees: ThyTreeSelectNode[] = this.treeNodes()) {
1✔
289
        const forEachTree = (tree: ThyTreeSelectNode[], fn: any, result: ThyTreeSelectNode[] = []) => {
290
            tree.forEach(item => {
3!
UNCOV
291
                result.push(item);
×
292
                if (item.children && fn(item)) {
293
                    forEachTree(item.children, fn, result);
3!
294
                }
3✔
295
            });
3✔
296
            return result;
297
        };
3✔
298
        return forEachTree(rootTrees, (node: ThyTreeSelectNode) => !!node.expand);
299
    }
300

301
    writeValue(value: any): void {
12✔
302
        this.selectedValue.set(value);
3✔
303
    }
3✔
304

3✔
305
    constructor() {
3✔
306
        super();
307

308
        this.bindClickEvent();
9✔
309
        this.bindResizeEvent();
2✔
310

311
        effect(() => {
2✔
312
            this.setSelectedNodes();
313
        });
314

7✔
315
        effect(() => {
7✔
316
            if (this.thyVirtualScroll()) {
317
                this.buildFlattenTreeNodes();
318
            }
319
        });
320

1✔
321
        effect(() => {
1!
322
            this.flattenTreeNodes.set(this.flattenNodes(this.treeNodes(), []));
1✔
323
        });
1✔
324
    }
1✔
325

1✔
326
    private bindClickEvent() {
2✔
327
        if (isPlatformBrowser(this.platformId)) {
18✔
328
            this.thyClickDispatcher
329
                .clicked(0)
330
                .pipe(takeUntilDestroyed(this.destroyRef))
1✔
331
                .subscribe(event => {
1✔
332
                    event.stopPropagation();
333
                    if (!this.elementRef.nativeElement.contains(event.target) && this.expandTreeSelectOptions()) {
1✔
334
                        this.ngZone.run(() => {
335
                            this.close();
336
                        });
1✔
337
                    }
1✔
338
                });
339
        }
340
    }
341

342
    private bindResizeEvent() {
343
        this.viewportRuler
344
            .change()
345
            .pipe(takeUntilDestroyed(this.destroyRef))
346
            .subscribe(() => {
347
                this.cdkConnectOverlayWidth.set(this.cdkOverlayOrigin().elementRef.nativeElement.getBoundingClientRect().width);
348
            });
349
    }
350

351
    onFocus($event: FocusEvent) {
352
        const inputElement: HTMLInputElement = this.elementRef.nativeElement.querySelector('input');
353
        inputElement?.focus();
354
    }
355

356
    onBlur($event: FocusEvent) {
357
        // 1. Tab 聚焦后自动聚焦到 input 输入框,此分支下直接返回,无需触发 onTouchedFn
358
        // 2. 打开选择框后如果点击弹框内导致 input 失焦,无需触发 onTouchedFn
359
        if (elementMatchClosest($event?.relatedTarget as HTMLElement, ['thy-tree-select', 'thy-tree-select-nodes'])) {
360
            return;
361
        }
362
        this.onTouchedFn();
363
    }
364

365
    public readonly selectedValueObject = computed(() => {
366
        return this.thyMultiple() ? this.selectedNodes() : this.selectedNode();
1✔
367
    });
368

369
    searchValue(searchText: string) {
370
        this.searchText.set(searchText.trim());
371
    }
372

373
    public setPosition() {
9✔
374
        this.ngZone.onStable
375
            .asObservable()
376
            .pipe(take(1))
377
            .subscribe(() => {
378
                this.cdkConnectedOverlay().overlayRef.updatePosition();
379
            });
380
    }
381

382
    private flattenNodes(nodes: ThyTreeSelectNode[] = [], parentPrimaryValue: string[] = []): ThyTreeSelectNode[] {
31✔
383
        let flattenedNodes: ThyTreeSelectNode[] = [];
384
        (nodes || []).forEach(item => {
385
            item.parentValues = parentPrimaryValue;
386
            item.level = item.parentValues.length;
387
            flattenedNodes.push(item);
388
            if (item.children && isArray(item.children)) {
389
                const childNodes = this.flattenNodes(item.children, [...parentPrimaryValue, item[this.thyPrimaryKey()]]);
390
                flattenedNodes = flattenedNodes.concat(childNodes);
391
            }
392
        });
393
        return flattenedNodes;
394
    }
395

396
    private _findTreeNode(value: string): ThyTreeSelectNode {
397
        return (this.flattenTreeNodes() || []).find(item => item[this.thyPrimaryKey()] === value);
398
    }
1✔
399

400
    private setSelectedNodes() {
401
        const isMultiple = this.thyMultiple();
402
        const primaryKey = this.thyPrimaryKey();
1✔
403
        const selectedValue = this.selectedValue();
404
        const valueIsObject = this.valueIsObject();
13✔
405
        if (selectedValue) {
13✔
406
            // 多选数据初始化
13✔
407
            if (isMultiple) {
13✔
408
                if (selectedValue.length > 0) {
13✔
409
                    if (valueIsObject && Object.keys(selectedValue[0]).indexOf(primaryKey) >= 0) {
13✔
410
                        this.selectedNodes.set(
13✔
411
                            selectedValue.map((item: any) => {
13✔
412
                                return this._findTreeNode(item[primaryKey]);
13✔
413
                            })
17✔
414
                        );
13✔
415
                    } else {
14✔
416
                        this.selectedNodes.set(
14✔
417
                            selectedValue.map((item: any) => {
13✔
418
                                return this._findTreeNode(item);
13✔
419
                            })
420
                        );
13✔
421
                    }
422
                }
13✔
423
            } else {
13!
424
                // 单选数据初始化
425
                if (valueIsObject) {
426
                    if (Object.keys(selectedValue).indexOf(primaryKey) >= 0) {
427
                        this.selectedNode.set(this._findTreeNode(selectedValue[primaryKey]));
361✔
428
                    }
361✔
429
                } else {
361✔
430
                    this.selectedNode.set(this._findTreeNode(selectedValue));
244!
431
                }
244✔
432
            }
50✔
433
        } else {
434
            this.selectedNodes.set([]);
435
            this.selectedNode.set(null);
436
        }
117!
437
    }
438

439
    openSelectPop() {
440
        if (this.thyDisable()) {
418✔
441
            return;
418✔
442
        }
394✔
443
        this.cdkConnectOverlayWidth.set(this.cdkOverlayOrigin().elementRef.nativeElement.getBoundingClientRect().width);
444
        this.expandTreeSelectOptions.set(!this.expandTreeSelectOptions());
24✔
445
        this.thyExpandStatusChange.emit(this.expandTreeSelectOptions());
24!
446
    }
24✔
447

UNCOV
448
    close() {
×
449
        if (this.expandTreeSelectOptions()) {
450
            this.expandTreeSelectOptions.set(false);
451
            this.thyExpandStatusChange.emit(false);
373✔
452
            this.onTouchedFn();
373✔
453
        }
363✔
454
    }
455

10✔
456
    clearSelectedValue(event: Event) {
10!
457
        event.stopPropagation();
10✔
458
        this.selectedValue.set(null);
NEW
459
        this.selectedNode.set(null);
×
460
        this.selectedNodes.set([]);
461
        this.onChangeFn(null);
462
    }
707✔
463

707✔
464
    private _changeSelectValue() {
707✔
465
        const selectedNodes = this.selectedNodes();
707✔
466
        const selectedNode = this.selectedNode();
536!
467
        if (this.valueIsObject()) {
536✔
468
            this.selectedValue.set(this.thyMultiple() ? selectedNodes : selectedNode);
110✔
469
        } else {
470
            const value = this.thyMultiple() ? selectedNodes.map(item => item[this.thyPrimaryKey()]) : selectedNode[this.thyPrimaryKey()];
471
            this.selectedValue.set(value);
472
        }
171!
473
        this.onChangeFn(this.selectedValue());
474
        if (!this.thyMultiple()) {
707✔
475
            this.onTouchedFn();
707✔
476
        }
707✔
477
    }
478

479
    removeMultipleSelectedNode(event: { item: ThyTreeSelectNode; $eventOrigin: Event }) {
1✔
480
        this.removeSelectedNode(event.item, event.$eventOrigin);
481
    }
482

12!
483
    // thyMultiple = true 时,移除数据时调用
12✔
484
    removeSelectedNode(node: ThyTreeSelectNode, event?: Event) {
485
        if (event) {
486
            event.stopPropagation();
487
        }
1✔
488
        if (this.thyDisable()) {
1!
489
            return;
1✔
490
        }
491
        if (this.thyMultiple()) {
NEW
492
            this.selectedNodes.set(
×
NEW
493
                produce(this.selectedNodes()).remove((item: ThyTreeSelectNode) => {
×
494
                    return item[this.thyPrimaryKey()] === node[this.thyPrimaryKey()];
495
                })
NEW
496
            );
×
497
            this._changeSelectValue();
498
        }
499
    }
1!
500

1✔
501
    selectNode(node: ThyTreeSelectNode) {
1✔
502
        if (!this.thyMultiple()) {
503
            this.selectedNode.set(node);
504
            this.expandTreeSelectOptions.set(false);
505
            this.thyExpandStatusChange.emit(false);
1!
UNCOV
506
            this._changeSelectValue();
×
507
        } else {
508
            if (
509
                this.selectedNodes().find(item => {
510
                    return item[this.thyPrimaryKey()] === node[this.thyPrimaryKey()];
24✔
511
                })
512
            ) {
1✔
513
                this.removeSelectedNode(node);
514
            } else {
515
                this.selectedNodes.set(produce(this.selectedNodes()).add(node));
516
                this._changeSelectValue();
517
            }
1✔
518
        }
519
    }
520

521
    getNodeChildren(node: ThyTreeSelectNode) {
522
        const result = this.thyGetNodeChildren(node);
523
        if (result && result.subscribe) {
524
            result.subscribe((data: ThyTreeSelectNode[]) => {
525
                const flattenTreeNodes = this.flattenTreeNodes();
526
                const nodes = this.flattenNodes(data, [...node.parentValues, node[this.thyPrimaryKey()]]);
527
                const otherNodes = nodes.filter((item: ThyTreeNode) => {
528
                    return !flattenTreeNodes.find(hasItem => {
529
                        return hasItem[this.thyPrimaryKey()] === item[this.thyPrimaryKey() as keyof ThyTreeNode];
530
                    });
531
                });
532
                this.flattenTreeNodes.set(flattenTreeNodes.concat(otherNodes));
533
                node.children = data;
534
            });
535
            return result;
536
        }
537
    }
538
}
539

540
const DEFAULT_ITEM_SIZE = 40;
541

542
/**
543
 * @private
544
 */
545
@Component({
546
    selector: 'thy-tree-select-nodes',
547
    templateUrl: './tree-select-nodes.component.html',
548
    imports: [
549
        NgTemplateOutlet,
550
        CdkVirtualScrollViewport,
551
        CdkFixedSizeVirtualScroll,
552
        CdkVirtualForOf,
553
        ThyEmpty,
554
        NgClass,
555
        NgStyle,
556
        ThyIcon,
557
        ThyFlexibleText
558
    ],
559
    host: {
560
        '[attr.tabindex]': '-1',
561
        class: 'thy-tree-select-dropdown',
562
        '[class.thy-tree-select-dropdown-multiple]': 'isMultiple()'
563
    }
564
})
565
export class ThyTreeSelectNodes {
566
    parent = inject(ThyTreeSelect);
567

568
    emptyIcon: Signal<string> = injectPanelEmptyIcon();
569

570
    readonly treeNodes = input<ThyTreeSelectNode[]>([]);
571

572
    readonly thyVirtualScroll = input<boolean>(false);
573

574
    public readonly isMultiple = computed(() => this.parent.thyMultiple());
575

576
    public readonly childCountKey = computed(() => this.parent.thyChildCountKey());
577

578
    public readonly treeNodeTemplateRef = computed(() => this.parent.treeNodeTemplateRef());
579

580
    public defaultItemSize = DEFAULT_ITEM_SIZE;
581

582
    public readonly thyPrimaryKey = computed(() => this.parent.thyPrimaryKey());
583

584
    public readonly selectedNodes = computed(() => this.parent.selectedNodes());
585

586
    public readonly selectedNode = computed(() => this.parent.selectedNode());
587

588
    public readonly hiddenNodeKey = computed(() => this.parent.thyHiddenNodeKey());
589

590
    public readonly disableNodeKey = computed(() => this.parent.thyDisableNodeKey());
591

592
    public readonly thyVirtualHeight = computed(() => {
593
        const treeSelectHeight = this.defaultItemSize * this.treeNodes().length;
594
        // 父级设置了max-height:300 & padding:10 0; 故此处最多设置280,否则将出现滚动条
595
        return treeSelectHeight > 300 ? '280px' : `${treeSelectHeight}px`;
596
    });
597

598
    public readonly hasNodeChildren = computed(() => {
599
        return this.treeNodes().every(item => !item.hasOwnProperty('children') || (!item?.children?.length && !item?.childCount));
600
    });
601

602
    treeNodeIsSelected(node: ThyTreeSelectNode) {
603
        const primaryKey = this.thyPrimaryKey();
604
        const isMultiple = this.isMultiple();
605
        if (isMultiple) {
606
            const selectedNodes = this.selectedNodes() || [];
607
            return selectedNodes.find(item => {
608
                return item[primaryKey] === node[primaryKey];
609
            });
610
        } else {
611
            return this.selectedNode() && this.selectedNode()[primaryKey] === node[primaryKey];
612
        }
613
    }
614

615
    treeNodeIsHidden(node: ThyTreeSelectNode) {
616
        const hiddenNodeKey = this.hiddenNodeKey();
617
        if (hiddenNodeKey) {
618
            return node[hiddenNodeKey];
619
        }
620
        const thyHiddenNodeFn = this.parent.thyHiddenNodeFn;
621
        if (thyHiddenNodeFn) {
622
            return thyHiddenNodeFn(node);
623
        }
624
        return false;
625
    }
626

627
    treeNodeIsDisable(node: ThyTreeSelectNode) {
628
        const disableNodeKey = this.disableNodeKey();
629
        if (disableNodeKey) {
630
            return node[disableNodeKey];
631
        }
632
        const thyDisableNodeFn = this.parent.thyDisableNodeFn;
633
        if (thyDisableNodeFn) {
634
            return thyDisableNodeFn(node);
635
        }
636
        return false;
637
    }
638

639
    treeNodeIsExpand(node: ThyTreeSelectNode) {
640
        const isMultiple = this.isMultiple();
641
        const primaryKey = this.thyPrimaryKey();
642
        let isSelectedNodeParent = false;
643
        if (isMultiple) {
644
            const selectedNodes = this.selectedNodes() || [];
645
            isSelectedNodeParent = !!selectedNodes.find(item => {
646
                return item.parentValues.indexOf(node[primaryKey]) > -1;
647
            });
648
        } else {
649
            isSelectedNodeParent = this.selectedNode() ? this.selectedNode().parentValues.indexOf(node[primaryKey]) > -1 : false;
650
        }
651
        const isExpand = node.expand || (Object.keys(node).indexOf('expand') < 0 && isSelectedNodeParent);
652
        node.expand = isExpand;
653
        return isExpand;
654
    }
655

656
    getNodeChildren(node: ThyTreeSelectNode) {
657
        return this.parent.getNodeChildren(node);
658
    }
659

660
    selectTreeNode(event: Event, node: ThyTreeSelectNode) {
661
        if (!this.treeNodeIsDisable(node)) {
662
            this.parent.selectNode(node);
663
        }
664
    }
665

666
    nodeExpandToggle(event: Event, node: ThyTreeSelectNode) {
667
        event.stopPropagation();
668
        if (Object.keys(node).indexOf('expand') > -1) {
669
            node.expand = !node.expand;
670
        } else {
671
            if (this.treeNodeIsExpand(node)) {
672
                node.expand = false;
673
            } else {
674
                node.expand = true;
675
            }
676
        }
677

678
        if (node.expand && this.parent.thyAsyncNode()) {
679
            this.getNodeChildren(node).subscribe(() => {
680
                this.parent.setPosition();
681
            });
682
        }
683
        // this.parent.setPosition();
684
        if (this.thyVirtualScroll()) {
685
            this.parent.buildFlattenTreeNodes();
686
        }
687
    }
688

689
    tabTrackBy(index: number, item: ThyTreeSelectNode) {
690
        return index;
691
    }
692
}
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