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

atinc / ngx-tethys / #102

26 May 2026 08:11AM UTC coverage: 91.111% (+0.7%) from 90.407%
#102

push

web-flow
build: bump docgeni to 2.8.0-next.5 (#3809)

4571 of 5491 branches covered (83.25%)

Branch coverage included in aggregate %.

13141 of 13949 relevant lines covered (94.21%)

966.75 hits per line

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

93.12
/src/tree-select/tree-select.component.ts
1
import {
2
    EXPANDED_DROPDOWN_POSITIONS,
3
    injectPanelEmptyIcon,
4
    thyAnimationZoom,
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 { from, 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,
22
    Component,
23
    ElementRef,
24
    forwardRef,
25
    Input,
26
    NgZone,
27
    PLATFORM_ID,
28
    TemplateRef,
29
    inject,
30
    Signal,
31
    output,
32
    input,
33
    effect,
34
    computed,
35
    signal,
36
    DestroyRef,
37
    contentChild,
38
    viewChild
39
} from '@angular/core';
40
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
41

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

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

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

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

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

111
    public selectedValue = signal<any>(undefined);
39✔
112

113
    public selectedNode = signal<ThyTreeSelectNode | null | undefined>(null);
39✔
114

115
    public selectedNodes = signal<ThyTreeSelectNode[]>([]);
39✔
116

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

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

121
    animateEnterClass = thyAnimationZoom.yEnter;
39✔
122

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

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

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

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

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

144
    public searchText = signal('');
39✔
145

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

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

150
    readonly cdkOverlayOrigin = viewChild.required(CdkOverlayOrigin);
39✔
151

152
    readonly cdkConnectedOverlay = viewChild.required(CdkConnectedOverlay);
39✔
153

154
    readonly customDisplayTemplate = viewChild.required<TemplateRef<any>>('customDisplayTemplate');
39✔
155

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

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

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

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

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

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

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

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

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

203
    get thyDisabled(): boolean {
204
        return this.thyDisable();
206✔
205
    }
206

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

308
        this.bindClickEvent();
39✔
309
        this.bindResizeEvent();
39✔
310

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

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

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

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

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

351
    onFocus($event: FocusEvent) {
352
        const inputElement: HTMLInputElement = this.elementRef.nativeElement.querySelector('input');
1✔
353
        inputElement?.focus();
1✔
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'])) {
2✔
360
            return;
1✔
361
        }
362
        this.onTouchedFn();
1✔
363
    }
364

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

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

373
    public setPosition() {
374
        from(Promise.resolve())
1✔
375
            .pipe(take(1))
376
            .subscribe(() => {
377
                this.cdkConnectedOverlay().overlayRef.updatePosition();
378
            });
1✔
379
    }
380

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

395
    private _findTreeNode(value: string): ThyTreeSelectNode | undefined {
396
        return (this.flattenTreeNodes() || []).find(item => item[this.thyPrimaryKey()] === value);
397
    }
98!
398

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

438
    openSelectPop() {
439
        if (this.thyDisable()) {
440
            return;
14✔
441
        }
1✔
442
        this.cdkConnectOverlayWidth.set(this.cdkOverlayOrigin().elementRef.nativeElement.getBoundingClientRect().width);
443
        this.expandTreeSelectOptions.set(!this.expandTreeSelectOptions());
13✔
444
        this.thyExpandStatusChange.emit(this.expandTreeSelectOptions());
13✔
445
    }
13✔
446

447
    close() {
448
        if (this.expandTreeSelectOptions()) {
449
            this.expandTreeSelectOptions.set(false);
7✔
450
            this.thyExpandStatusChange.emit(false);
2✔
451
            this.onTouchedFn();
2✔
452
        }
2✔
453
    }
454

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

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

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

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

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

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

539
const DEFAULT_ITEM_SIZE = 40;
540

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

13✔
567
    emptyIcon: Signal<string> = injectPanelEmptyIcon();
568

13✔
569
    readonly treeNodes = input<ThyTreeSelectNode[]>([]);
570

13✔
571
    readonly thyVirtualScroll = input<boolean>(false);
572

13✔
573
    public readonly isMultiple = computed(() => this.parent.thyMultiple());
574

13✔
575
    public readonly childCountKey = computed(() => this.parent.thyChildCountKey());
576

13✔
577
    public readonly treeNodeTemplateRef = computed(() => this.parent.treeNodeTemplateRef());
578

13✔
579
    public defaultItemSize = DEFAULT_ITEM_SIZE;
580

13✔
581
    public readonly thyPrimaryKey = computed(() => this.parent.thyPrimaryKey());
582

13✔
583
    public readonly selectedNodes = computed(() => this.parent.selectedNodes());
584

17✔
585
    public readonly selectedNode = computed(() => this.parent.selectedNode());
586

13✔
587
    public readonly hiddenNodeKey = computed(() => this.parent.thyHiddenNodeKey());
588

14✔
589
    public readonly disableNodeKey = computed(() => this.parent.thyDisableNodeKey());
590

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

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

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

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

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

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

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

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

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

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

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

© 2026 Coveralls, Inc