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

atinc / ngx-tethys / #93

12 Aug 2025 05:47AM UTC coverage: 90.387% (+0.06%) from 90.324%
#93

push

web-flow
Merge 7b9fca624 into aa9fa8ee2

5533 of 6801 branches covered (81.36%)

Branch coverage included in aggregate %.

82 of 84 new or added lines in 1 file covered. (97.62%)

5 existing lines in 1 file now uncovered.

13874 of 14670 relevant lines covered (94.57%)

910.02 hits per line

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

91.4
/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
                            })
13✔
414
                        );
415
                    } else {
13✔
416
                        this.selectedNodes.set(
417
                            selectedValue.map((item: any) => {
13✔
418
                                return this._findTreeNode(item);
13!
419
                            })
420
                        );
421
                    }
422
                }
361✔
423
            } else {
361✔
424
                // 单选数据初始化
361✔
425
                if (valueIsObject) {
244!
426
                    if (Object.keys(selectedValue).indexOf(primaryKey) >= 0) {
50✔
427
                        this.selectedNode.set(this._findTreeNode(selectedValue[primaryKey]));
428
                    }
429
                } else {
430
                    this.selectedNode.set(this._findTreeNode(selectedValue));
117!
431
                }
432
            }
433
        } else {
434
            this.selectedNodes.set([]);
418✔
435
            this.selectedNode.set(null);
418✔
436
        }
394✔
437
    }
438

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

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

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

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

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

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

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

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

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

580
    public defaultItemSize = DEFAULT_ITEM_SIZE;
581

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

588
    public readonly hasNodeChildren = computed(() => {
589
        return this.treeNodes().every(item => !item.hasOwnProperty('children') || (!item?.children?.length && !item?.childCount));
590
    });
591

592
    treeNodeIsSelected(node: ThyTreeSelectNode) {
593
        const primaryKey = this.parent.thyPrimaryKey();
594
        const isMultiple = this.isMultiple();
595
        if (isMultiple) {
596
            return (this.parent.selectedNodes() || []).find(item => {
597
                return item[primaryKey] === node[primaryKey];
598
            });
599
        } else {
600
            return this.parent.selectedNode() && this.parent.selectedNode()[primaryKey] === node[primaryKey];
601
        }
602
    }
603

604
    treeNodeIsHidden(node: ThyTreeSelectNode) {
605
        const thyHiddenNodeKey = this.parent.thyHiddenNodeKey();
606
        if (thyHiddenNodeKey) {
607
            return node[thyHiddenNodeKey];
608
        }
609
        const thyHiddenNodeFn = this.parent.thyHiddenNodeFn;
610
        if (thyHiddenNodeFn) {
611
            return thyHiddenNodeFn(node);
612
        }
613
        return false;
614
    }
615

616
    treeNodeIsDisable(node: ThyTreeSelectNode) {
617
        const thyDisableNodeKey = this.parent.thyDisableNodeKey();
618
        if (thyDisableNodeKey) {
619
            return node[thyDisableNodeKey];
620
        }
621
        const thyDisableNodeFn = this.parent.thyDisableNodeFn;
622
        if (thyDisableNodeFn) {
623
            return thyDisableNodeFn(node);
624
        }
625
        return false;
626
    }
627

628
    treeNodeIsExpand(node: ThyTreeSelectNode) {
629
        const isMultiple = this.isMultiple();
630
        const primaryKey = this.parent.thyPrimaryKey();
631
        let isSelectedNodeParent = false;
632
        if (isMultiple) {
633
            isSelectedNodeParent = !!(this.parent.selectedNodes() || []).find(item => {
634
                return item.parentValues.indexOf(node[primaryKey]) > -1;
635
            });
636
        } else {
637
            isSelectedNodeParent = this.parent.selectedNode()
638
                ? this.parent.selectedNode().parentValues.indexOf(node[primaryKey]) > -1
639
                : false;
640
        }
641
        const isExpand = node.expand || (Object.keys(node).indexOf('expand') < 0 && isSelectedNodeParent);
642
        node.expand = isExpand;
643
        return isExpand;
644
    }
645

646
    getNodeChildren(node: ThyTreeSelectNode) {
647
        return this.parent.getNodeChildren(node);
648
    }
649

650
    selectTreeNode(event: Event, node: ThyTreeSelectNode) {
651
        if (!this.treeNodeIsDisable(node)) {
652
            this.parent.selectNode(node);
653
        }
654
    }
655

656
    nodeExpandToggle(event: Event, node: ThyTreeSelectNode) {
657
        event.stopPropagation();
658
        if (Object.keys(node).indexOf('expand') > -1) {
659
            node.expand = !node.expand;
660
        } else {
661
            if (this.treeNodeIsExpand(node)) {
662
                node.expand = false;
663
            } else {
664
                node.expand = true;
665
            }
666
        }
667

668
        if (node.expand && this.parent.thyAsyncNode()) {
669
            this.getNodeChildren(node).subscribe(() => {
670
                this.parent.setPosition();
671
            });
672
        }
673
        // this.parent.setPosition();
674
        if (this.thyVirtualScroll()) {
675
            this.parent.buildFlattenTreeNodes();
676
        }
677
    }
678

679
    tabTrackBy(index: number, item: ThyTreeSelectNode) {
680
        return index;
681
    }
682
}
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