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

atinc / ngx-tethys / cd64db52-e563-41a3-85f3-a0adb87ce135

30 Oct 2024 08:03AM UTC coverage: 90.402% (-0.04%) from 90.438%
cd64db52-e563-41a3-85f3-a0adb87ce135

push

circleci

web-flow
refactor: refactor constructor to the inject function (#3222)

5503 of 6730 branches covered (81.77%)

Branch coverage included in aggregate %.

422 of 429 new or added lines in 170 files covered. (98.37%)

344 existing lines in 81 files now uncovered.

13184 of 13941 relevant lines covered (94.57%)

997.19 hits per line

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

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

11
import { CdkConnectedOverlay, CdkOverlayOrigin, ViewportRuler } from '@angular/cdk/overlay';
12
import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
13
import { isPlatformBrowser, NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
14
import {
15
    ChangeDetectorRef,
16
    Component,
17
    ContentChild,
×
18
    ElementRef,
2✔
19
    EventEmitter,
17✔
20
    forwardRef,
2✔
21
    HostBinding,
2✔
22
    Input,
23
    NgZone,
15!
24
    OnDestroy,
15✔
25
    OnInit,
15✔
26
    Output,
2✔
27
    PLATFORM_ID,
2✔
28
    TemplateRef,
29
    ViewChild,
30
    inject
15✔
31
} from '@angular/core';
32
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
4✔
33

2✔
34
import { ThyTreeSelectNode, ThyTreeSelectType } from './tree-select.class';
35

36
type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '';
37

38
export function filterTreeData(treeNodes: ThyTreeSelectNode[], searchText: string, searchKey: string = 'name') {
39
    const filterNodes = (node: ThyTreeSelectNode, result: ThyTreeSelectNode[]) => {
40
        if (node[searchKey] && node[searchKey].indexOf(searchText) !== -1) {
1✔
41
            result.push(node);
42
            return result;
40✔
43
        }
40✔
44
        if (Array.isArray(node.children)) {
40✔
45
            const nodes = node.children.reduce((previous, current) => filterNodes(current, previous), [] as ThyTreeSelectNode[]);
1✔
46
            if (nodes.length) {
1✔
47
                const parentNode = { ...node, children: nodes, expand: true };
1!
48
                result.push(parentNode);
1✔
49
            }
50
        }
51
        return result;
52
    };
53
    const treeData = treeNodes.reduce((previous, current) => filterNodes(current, previous), [] as ThyTreeSelectNode[]);
196✔
54
    return treeData;
55
}
56

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

96
    @HostBinding('class.thy-select-custom') treeSelectClass = true;
22✔
97

98
    @HostBinding('class.thy-select') isTreeSelect = true;
99

39✔
100
    // 菜单是否展开
39✔
101
    @HostBinding('class.menu-is-opened') expandTreeSelectOptions = false;
39✔
102

39✔
103
    @HostBinding('class.thy-select-custom--multiple') isMulti = false;
39✔
104

39✔
105
    public treeNodes: ThyTreeSelectNode[];
39✔
106

39✔
107
    public selectedValue: any;
39✔
108

39✔
109
    public selectedNode: ThyTreeSelectNode;
39✔
110

39✔
111
    public selectedNodes: ThyTreeSelectNode[] = [];
39✔
112

39✔
113
    public flattenTreeNodes: ThyTreeSelectNode[] = [];
39✔
114

39✔
115
    virtualTreeNodes: ThyTreeSelectNode[] = [];
39✔
116

117
    public cdkConnectOverlayWidth = 0;
118

119
    public expandedDropdownPositions = EXPANDED_DROPDOWN_POSITIONS;
120

39✔
121
    public icons: { expand: string; collapse: string; gap?: number } = {
39✔
122
        expand: 'angle-down',
39✔
123
        collapse: 'angle-right',
39✔
124
        gap: 15
39✔
125
    };
39✔
126

39✔
127
    private initialled = false;
39✔
128

39✔
129
    private destroy$ = new Subject<void>();
39✔
130

39✔
131
    public valueIsObject = false;
39✔
132

39✔
133
    originTreeNodes: ThyTreeSelectNode[];
39✔
134

39✔
135
    @ContentChild('thyTreeSelectTriggerDisplay')
39✔
136
    thyTreeSelectTriggerDisplayRef: TemplateRef<any>;
39✔
137

39✔
138
    @ContentChild('treeNodeTemplate')
39✔
139
    treeNodeTemplateRef: TemplateRef<any>;
39✔
140

141
    @ViewChild(CdkOverlayOrigin, { static: true }) cdkOverlayOrigin: CdkOverlayOrigin;
142

39✔
143
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
39✔
144

39✔
145
    @ViewChild('customDisplayTemplate', { static: true }) customDisplayTemplate: TemplateRef<any>;
39✔
146

39✔
147
    /**
4✔
148
     * treeNodes 数据
149
     * @type ThyTreeSelectNode[]
39!
150
     */
39✔
151
    @Input()
152
    set thyTreeNodes(value: ThyTreeSelectNode[]) {
153
        this.treeNodes = value;
154
        this.originTreeNodes = value;
27✔
155
        if (this.initialled) {
27✔
156
            this.flattenTreeNodes = this.flattenNodes(this.treeNodes, this.flattenTreeNodes, []);
2✔
157
            this.setSelectedNodes();
2✔
158

2✔
159
            if (this.thyVirtualScroll) {
160
                this.buildFlattenTreeNodes();
161
            }
162
        }
163
    }
39✔
164

165
    /**
166
     * 开启虚拟滚动
167
     */
1✔
168
    @Input({ transform: coerceBooleanProperty }) thyVirtualScroll: boolean = false;
169

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

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

182
    @Input() thyChildCountKey = 'childCount';
183

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

1✔
190
    /**
191
     * 是否多选
192
     * @type boolean
1✔
193
     */
194
    @Input({ transform: coerceBooleanProperty }) thyMultiple = false;
195

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

202
    get thyDisabled(): boolean {
×
203
        return this.thyDisable;
4,083✔
204
    }
4,083✔
205

4,083!
206
    /**
112,044✔
207
     * 树选择框默认文字
112,044✔
208
     * @type string
112,044✔
209
     */
4,042✔
210
    @Input() thyPlaceholder = '请选择节点';
4,042✔
211

212
    get placeholder() {
213
        return this.thyPlaceholder;
4,083✔
214
    }
215

216
    /**
28!
217
     * 控制树选择的输入框大小
218
     * @type xs | sm | md | default | lg
219
     */
62✔
220
    @Input() thySize: InputSize;
221

4✔
222
    /**
2!
223
     * 改变空选项的情况下的提示文本
2✔
224
     * @type string
1✔
225
     */
2✔
226
    @Input() thyEmptyOptionsText = '暂时没有数据可选';
227

228
    /**
229
     * 设置是否隐藏节点(不可进行任何操作),优先级高于 thyHiddenNodeFn
1✔
230
     * @type string
2✔
231
     */
232
    @Input() thyHiddenNodeKey = 'hidden';
233

234
    /**
235
     * 设置是否禁用节点(不可进行任何操作),优先级高于 thyDisableNodeFn
236
     * @type string
237
     */
2✔
238
    @Input() thyDisableNodeKey = 'disabled';
1!
239

1✔
240
    /**
241
     * 是否异步加载节点的子节点(显示加载状态),当为 true 时,异步获取
242
     * @type boolean
243
     */
1✔
244
    @Input({ transform: coerceBooleanProperty }) thyAsyncNode = false;
245

246
    /**
247
     * 是否展示全名
248
     * @type boolean
58✔
249
     */
58✔
250
    @Input({ transform: coerceBooleanProperty }) thyShowWholeName = false;
251

252
    /**
253
     * 是否展示搜索
14✔
254
     * @type boolean
1✔
255
     */
256
    @Input({ transform: coerceBooleanProperty }) thyShowSearch = false;
13✔
257

13✔
258
    /**
13✔
259
     * 图标类型,支持 default | especial,已废弃
260
     * @deprecated
261
     */
7✔
262
    @Input()
2✔
263
    set thyIconType(type: ThyTreeSelectType) {
2✔
264
        if (typeof ngDevMode === 'undefined' || ngDevMode) {
2✔
265
            warnDeprecation('This parameter has been deprecation');
266
        }
267
        // if (type === 'especial') {
268
        //     this.icons = { expand: 'minus-square', collapse: 'plus-square', gap: 20 };
2✔
269
        // } else {
2✔
270
        //     this.icons = { expand: 'caret-right-down', collapse: 'caret-right', gap: 15 };
2✔
271
        // }
2✔
272
    }
2✔
273

274
    /**
275
     * 设置是否隐藏节点(不可进行任何操作),优先级低于 thyHiddenNodeKey。
13!
UNCOV
276
     * @default (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.hidden
×
277
     */
278
    @Input() thyHiddenNodeFn: (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.hidden;
279

13✔
280
    /**
7✔
281
     * 设置是否禁用节点(不可进行任何操作),优先级低于 thyDisableNodeKey。
282
     * @default (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.disabled
283
     */
13✔
284
    @Input() thyDisableNodeFn: (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.disabled;
13✔
285

3✔
286
    /**
287
     * 获取节点的子节点,返回 Observable<ThyTreeSelectNode>。
288
     * @default (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([])
289
     */
1✔
290
    @Input() thyGetNodeChildren: (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([]);
291

292
    /**
293
     * 树选择组件展开和折叠状态事件
3✔
294
     */
1✔
295
    @Output() thyExpandStatusChange: EventEmitter<boolean> = new EventEmitter<boolean>();
296

3!
UNCOV
297
    private _getNgModelType() {
×
298
        if (this.thyMultiple) {
299
            this.valueIsObject = !this.selectedValue[0] || isObject(this.selectedValue[0]);
3!
300
        } else {
3✔
301
            this.valueIsObject = isObject(this.selectedValue);
3✔
302
        }
303
    }
3✔
304

305
    public buildFlattenTreeNodes() {
306
        this.virtualTreeNodes = this.getFlattenTreeNodes(this.treeNodes);
307
    }
12✔
308

3✔
309
    private getFlattenTreeNodes(rootTrees: ThyTreeSelectNode[] = this.treeNodes) {
3✔
310
        const forEachTree = (tree: ThyTreeSelectNode[], fn: any, result: ThyTreeSelectNode[] = []) => {
3✔
311
            tree.forEach(item => {
3✔
312
                result.push(item);
313
                if (item.children && fn(item)) {
314
                    forEachTree(item.children, fn, result);
9✔
315
                }
2✔
316
            });
317
            return result;
2✔
318
        };
319
        return forEachTree(rootTrees, (node: ThyTreeSelectNode) => !!node.expand);
320
    }
7✔
321

7✔
322
    writeValue(value: any): void {
323
        this.selectedValue = value;
324

325
        if (value) {
326
            this._getNgModelType();
1✔
327
        }
1!
328
        this.setSelectedNodes();
1✔
329
    }
1✔
330

1✔
331
    constructor() {
2✔
332
        super();
18✔
333
    }
334

335
    ngOnInit() {
1✔
336
        this.isMulti = this.thyMultiple;
1✔
337
        this.flattenTreeNodes = this.flattenNodes(this.treeNodes, this.flattenTreeNodes, []);
338
        this.setSelectedNodes();
1✔
339
        this.initialled = true;
340

341
        if (this.thyVirtualScroll) {
1✔
342
            this.buildFlattenTreeNodes();
1✔
343
        }
344

345
        if (isPlatformBrowser(this.platformId)) {
346
            this.thyClickDispatcher
347
                .clicked(0)
348
                .pipe(takeUntil(this.destroy$))
349
                .subscribe(event => {
350
                    event.stopPropagation();
351
                    if (!this.elementRef.nativeElement.contains(event.target) && this.expandTreeSelectOptions) {
352
                        this.ngZone.run(() => {
353
                            this.close();
354
                            this.ref.markForCheck();
355
                        });
356
                    }
357
                });
358
        }
359
        this.viewportRuler
360
            .change()
361
            .pipe(takeUntil(this.destroy$))
362
            .subscribe(() => {
363
                this.init();
364
            });
365
    }
366

367
    onFocus($event: FocusEvent) {
368
        const inputElement: HTMLInputElement = this.elementRef.nativeElement.querySelector('input');
369
        inputElement?.focus();
370
    }
371

372
    onBlur($event: FocusEvent) {
373
        // 1. Tab 聚焦后自动聚焦到 input 输入框,此分支下直接返回,无需触发 onTouchedFn
374
        // 2. 打开选择框后如果点击弹框内导致 input 失焦,无需触发 onTouchedFn
375
        if (elementMatchClosest($event?.relatedTarget as HTMLElement, ['thy-tree-select', 'thy-tree-select-nodes'])) {
1✔
376
            return;
377
        }
378
        this.onTouchedFn();
379
    }
380

381
    ngOnDestroy(): void {
382
        this.destroy$.next();
9✔
383
    }
384

385
    get selectedValueObject() {
386
        return this.thyMultiple ? this.selectedNodes : this.selectedNode;
387
    }
388

389
    searchValue(searchText: string) {
390
        this.treeNodes = filterTreeData(this.originTreeNodes, searchText.trim(), this.thyShowKey);
391
    }
392

113✔
393
    public setPosition() {
394
        this.ngZone.onStable
395
            .asObservable()
396
            .pipe(take(1))
397
            .subscribe(() => {
398
                this.cdkConnectedOverlay.overlayRef.updatePosition();
399
            });
400
    }
401

402
    private init() {
403
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
404
    }
405

1✔
406
    private flattenNodes(
407
        nodes: ThyTreeSelectNode[] = [],
408
        resultNodes: ThyTreeSelectNode[] = [],
409
        parentPrimaryValue: string[] = []
1✔
410
    ): ThyTreeSelectNode[] {
411
        resultNodes = resultNodes.concat(nodes);
13✔
412
        let nodesLeafs: ThyTreeSelectNode[] = [];
13✔
413
        (nodes || []).forEach(item => {
13✔
414
            item.parentValues = parentPrimaryValue;
13✔
415
            item.level = item.parentValues.length;
13✔
416
            if (item.children && isArray(item.children)) {
13✔
417
                const nodeLeafs = this.flattenNodes(item.children, resultNodes, [...parentPrimaryValue, item[this.thyPrimaryKey]]);
13✔
418
                nodesLeafs = [...nodesLeafs, ...nodeLeafs];
13✔
419
            }
13✔
420
        });
13✔
421
        return [...nodes, ...nodesLeafs];
13✔
422
    }
13✔
423

13✔
424
    private _findTreeNode(value: string): ThyTreeSelectNode {
425
        return (this.flattenTreeNodes || []).find(item => item[this.thyPrimaryKey] === value);
426
    }
13✔
427

428
    private setSelectedNodes() {
13✔
429
        if (this.selectedValue) {
13✔
430
            // 多选数据初始化
13!
431
            if (this.thyMultiple) {
432
                if (this.selectedValue.length > 0) {
433
                    if (this.valueIsObject && Object.keys(this.selectedValue[0]).indexOf(this.thyPrimaryKey) >= 0) {
13✔
434
                        this.selectedNodes = this.selectedValue.map((item: any) => {
435
                            return this._findTreeNode(item[this.thyPrimaryKey]);
436
                        });
361✔
437
                    } else {
244!
438
                        this.selectedNodes = this.selectedValue.map((item: any) => {
50✔
439
                            return this._findTreeNode(item);
440
                        });
441
                    }
442
                }
117!
443
            } else {
444
                // 单选数据初始化
445
                if (this.valueIsObject) {
446
                    if (Object.keys(this.selectedValue).indexOf(this.thyPrimaryKey) >= 0) {
418✔
447
                        this.selectedNode = this._findTreeNode(this.selectedValue[this.thyPrimaryKey]);
394✔
448
                    }
449
                } else {
24!
450
                    this.selectedNode = this._findTreeNode(this.selectedValue);
24✔
451
                }
UNCOV
452
            }
×
453
        } else {
454
            this.selectedNodes = [];
455
            this.selectedNode = null;
373✔
456
        }
363✔
457
    }
458

10!
459
    openSelectPop() {
10✔
460
        if (this.thyDisable) {
UNCOV
461
            return;
×
462
        }
463
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
464
        this.expandTreeSelectOptions = !this.expandTreeSelectOptions;
707✔
465
        this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
707✔
466
    }
536!
467

110✔
468
    close() {
469
        if (this.expandTreeSelectOptions) {
470
            this.expandTreeSelectOptions = false;
471
            this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
171!
472
            this.onTouchedFn();
473
        }
474
    }
475

707✔
476
    clearSelectedValue(event: Event) {
707✔
477
        event.stopPropagation();
707✔
478
        this.selectedValue = null;
479
        this.selectedNode = null;
480
        this.selectedNodes = [];
1✔
481
        this.onChangeFn(this.selectedValue);
482
    }
483

12!
484
    private _changeSelectValue() {
12✔
485
        if (this.valueIsObject) {
486
            this.selectedValue = this.thyMultiple ? this.selectedNodes : this.selectedNode;
487
        } else {
488
            this.selectedValue = this.thyMultiple
1✔
489
                ? this.selectedNodes.map(item => item[this.thyPrimaryKey])
1!
490
                : this.selectedNode[this.thyPrimaryKey];
1✔
491
        }
492
        this.onChangeFn(this.selectedValue);
UNCOV
493
        if (!this.thyMultiple) {
×
UNCOV
494
            this.onTouchedFn();
×
495
        }
496
    }
UNCOV
497

×
498
    removeMultipleSelectedNode(event: { item: ThyTreeSelectNode; $eventOrigin: Event }) {
499
        this.removeSelectedNode(event.item, event.$eventOrigin);
500
    }
1!
501

1✔
502
    // thyMultiple = true 时,移除数据时调用
1✔
503
    removeSelectedNode(node: ThyTreeSelectNode, event?: Event) {
504
        if (event) {
505
            event.stopPropagation();
506
        }
1!
507
        if (this.thyDisable) {
×
508
            return;
509
        }
510
        if (this.thyMultiple) {
511
            this.selectedNodes = produce(this.selectedNodes).remove((item: ThyTreeSelectNode) => {
24✔
512
                return item[this.thyPrimaryKey] === node[this.thyPrimaryKey];
513
            });
1✔
514
            this._changeSelectValue();
515
        }
516
    }
517

518
    selectNode(node: ThyTreeSelectNode) {
519
        if (!this.thyMultiple) {
1✔
520
            this.selectedNode = node;
521
            this.expandTreeSelectOptions = false;
522
            this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
523
            this._changeSelectValue();
524
        } else {
525
            if (
526
                this.selectedNodes.find(item => {
527
                    return item[this.thyPrimaryKey] === node[this.thyPrimaryKey];
528
                })
529
            ) {
530
                this.removeSelectedNode(node);
531
            } else {
532
                this.selectedNodes = produce(this.selectedNodes).add(node);
533
                this._changeSelectValue();
534
            }
535
        }
536
    }
537

538
    getNodeChildren(node: ThyTreeSelectNode) {
539
        const result = this.thyGetNodeChildren(node);
540
        if (result && result.subscribe) {
541
            result.pipe().subscribe((data: ThyTreeSelectNode[]) => {
542
                const nodes = this.flattenNodes(data, this.flattenTreeNodes, [...node.parentValues, node[this.thyPrimaryKey]]);
543
                const otherNodes = nodes.filter((item: ThyTreeNode) => {
544
                    return !this.flattenTreeNodes.find(hasItem => {
545
                        return hasItem[this.thyPrimaryKey] === item[this.thyPrimaryKey];
546
                    });
547
                });
548
                this.flattenTreeNodes = [...this.flattenTreeNodes, ...otherNodes];
549
                node.children = data;
550
            });
551
            return result;
552
        }
553
    }
554
}
555

556
const DEFAULT_ITEM_SIZE = 40;
557

558
/**
559
 * @private
560
 */
561
@Component({
562
    selector: 'thy-tree-select-nodes',
563
    templateUrl: './tree-select-nodes.component.html',
564
    standalone: true,
565
    imports: [
566
        NgTemplateOutlet,
567
        CdkVirtualScrollViewport,
568
        CdkFixedSizeVirtualScroll,
569
        CdkVirtualForOf,
570
        ThyEmpty,
571
        NgClass,
572
        NgStyle,
573
        ThyIcon,
574
        ThyFlexibleText
575
    ],
576
    host: {
577
        '[attr.tabindex]': '-1'
578
    }
579
})
580
export class ThyTreeSelectNodes implements OnInit {
581
    parent = inject(ThyTreeSelect);
582

583
    @HostBinding('class') class: string;
584

585
    nodeList: ThyTreeSelectNode[] = [];
586

587
    @Input() set treeNodes(value: ThyTreeSelectNode[]) {
588
        const treeSelectHeight = this.defaultItemSize * value.length;
589
        // 父级设置了max-height:300 & padding:10 0; 故此处最多设置280,否则将出现滚动条
590
        this.thyVirtualHeight = treeSelectHeight > 300 ? '280px' : `${treeSelectHeight}px`;
591
        this.nodeList = value;
592
        this.hasNodeChildren = this.nodeList.every(
593
            item => !item.hasOwnProperty('children') || (!item?.children?.length && !item?.childCount)
594
        );
595
    }
596

597
    @Input() thyVirtualScroll: boolean = false;
598

599
    public primaryKey = this.parent.thyPrimaryKey;
600

601
    public showKey = this.parent.thyShowKey;
602

603
    public isMultiple = this.parent.thyMultiple;
604

605
    public valueIsObject = this.parent.valueIsObject;
606

607
    public selectedValue = this.parent.selectedValue;
608

609
    public childCountKey = this.parent.thyChildCountKey;
610

611
    public treeNodeTemplateRef = this.parent.treeNodeTemplateRef;
612

613
    public defaultItemSize = DEFAULT_ITEM_SIZE;
614

615
    public thyVirtualHeight: string = null;
616

617
    public hasNodeChildren: boolean = false;
618

619
    ngOnInit() {
620
        this.class = this.isMultiple ? 'thy-tree-select-dropdown thy-tree-select-dropdown-multiple' : 'thy-tree-select-dropdown';
621
    }
622

623
    treeNodeIsSelected(node: ThyTreeSelectNode) {
624
        if (this.parent.thyMultiple) {
625
            return (this.parent.selectedNodes || []).find(item => {
626
                return item[this.primaryKey] === node[this.primaryKey];
627
            });
628
        } else {
629
            return this.parent.selectedNode && this.parent.selectedNode[this.primaryKey] === node[this.primaryKey];
630
        }
631
    }
632

633
    treeNodeIsHidden(node: ThyTreeSelectNode) {
634
        if (this.parent.thyHiddenNodeKey) {
635
            return node[this.parent.thyHiddenNodeKey];
636
        }
637
        if (this.parent.thyHiddenNodeFn) {
638
            return this.parent.thyHiddenNodeFn(node);
639
        }
640
        return false;
641
    }
642

643
    treeNodeIsDisable(node: ThyTreeSelectNode) {
644
        if (this.parent.thyDisableNodeKey) {
645
            return node[this.parent.thyDisableNodeKey];
646
        }
647
        if (this.parent.thyDisableNodeFn) {
648
            return this.parent.thyDisableNodeFn(node);
649
        }
650
        return false;
651
    }
652

653
    treeNodeIsExpand(node: ThyTreeSelectNode) {
654
        let isSelectedNodeParent = false;
655
        if (this.parent.thyMultiple) {
656
            isSelectedNodeParent = !!(this.parent.selectedNodes || []).find(item => {
657
                return item.parentValues.indexOf(node[this.primaryKey]) > -1;
658
            });
659
        } else {
660
            isSelectedNodeParent = this.parent.selectedNode
661
                ? this.parent.selectedNode.parentValues.indexOf(node[this.primaryKey]) > -1
662
                : false;
663
        }
664
        const isExpand = node.expand || (Object.keys(node).indexOf('expand') < 0 && isSelectedNodeParent);
665
        node.expand = isExpand;
666
        return isExpand;
667
    }
668

669
    getNodeChildren(node: ThyTreeSelectNode) {
670
        return this.parent.getNodeChildren(node);
671
    }
672

673
    selectTreeNode(event: Event, node: ThyTreeSelectNode) {
674
        if (!this.treeNodeIsDisable(node)) {
675
            this.parent.selectNode(node);
676
        }
677
    }
678

679
    nodeExpandToggle(event: Event, node: ThyTreeSelectNode) {
680
        event.stopPropagation();
681
        if (Object.keys(node).indexOf('expand') > -1) {
682
            node.expand = !node.expand;
683
        } else {
684
            if (this.treeNodeIsExpand(node)) {
685
                node.expand = false;
686
            } else {
687
                node.expand = true;
688
            }
689
        }
690

691
        if (node.expand && this.parent.thyAsyncNode) {
692
            this.getNodeChildren(node).subscribe(() => {
693
                this.parent.setPosition();
694
            });
695
        }
696
        // this.parent.setPosition();
697
        if (this.thyVirtualScroll) {
698
            this.parent.buildFlattenTreeNodes();
699
        }
700
    }
701

702
    tabTrackBy(index: number, item: ThyTreeSelectNode) {
703
        return index;
704
    }
705
}
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