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

atinc / ngx-tethys / c3bf0801-2444-43c6-9870-af168c876f54

18 Nov 2024 08:31AM UTC coverage: 90.35% (-0.005%) from 90.355%
c3bf0801-2444-43c6-9870-af168c876f54

push

circleci

web-flow
feat(tree-select): support i18n #TINFR-964 (#3249)

5522 of 6760 branches covered (81.69%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 1 file covered. (100.0%)

10 existing lines in 2 files now uncovered.

13204 of 13966 relevant lines covered (94.54%)

996.08 hits per line

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

88.0
/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,
×
19
    EventEmitter,
2✔
20
    forwardRef,
17✔
21
    HostBinding,
2✔
22
    Input,
2✔
23
    NgZone,
24
    OnDestroy,
15!
25
    OnInit,
15✔
26
    Output,
15✔
27
    PLATFORM_ID,
2✔
28
    TemplateRef,
2✔
29
    ViewChild,
30
    inject,
31
    Signal
15✔
32
} from '@angular/core';
33
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
4✔
34

2✔
35
import { ThyTreeSelectNode, ThyTreeSelectType } from './tree-select.class';
36
import { injectLocale, ThyTreeSelectLocale } from 'ngx-tethys/i18n';
37

38
type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '';
39

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

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

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

100
    @HostBinding('class.thy-select') isTreeSelect = true;
39✔
101

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

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

39✔
107
    public treeNodes: ThyTreeSelectNode[];
39✔
108

39✔
109
    public selectedValue: any;
39✔
110

39✔
111
    public selectedNode: ThyTreeSelectNode;
39✔
112

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

39✔
115
    public flattenTreeNodes: ThyTreeSelectNode[] = [];
39✔
116

39✔
117
    virtualTreeNodes: ThyTreeSelectNode[] = [];
118

119
    public cdkConnectOverlayWidth = 0;
120

121
    public expandedDropdownPositions = EXPANDED_DROPDOWN_POSITIONS;
39✔
122

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

39✔
129
    private initialled = false;
39✔
130

39✔
131
    private destroy$ = new Subject<void>();
39✔
132

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

39✔
135
    public valueIsObject = false;
39✔
136

39✔
137
    originTreeNodes: ThyTreeSelectNode[];
39✔
138

39✔
139
    @ContentChild('thyTreeSelectTriggerDisplay')
39✔
140
    thyTreeSelectTriggerDisplayRef: TemplateRef<any>;
39✔
141

39✔
142
    @ContentChild('treeNodeTemplate')
143
    treeNodeTemplateRef: TemplateRef<any>;
144

39✔
145
    @ViewChild(CdkOverlayOrigin, { static: true }) cdkOverlayOrigin: CdkOverlayOrigin;
39✔
146

39✔
147
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
39✔
148

39✔
149
    @ViewChild('customDisplayTemplate', { static: true }) customDisplayTemplate: TemplateRef<any>;
4✔
150

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

163
            if (this.thyVirtualScroll) {
164
                this.buildFlattenTreeNodes();
165
            }
39✔
166
        }
167
    }
168

169
    /**
1✔
170
     * 开启虚拟滚动
171
     */
172
    @Input({ transform: coerceBooleanProperty }) thyVirtualScroll: boolean = false;
173

1✔
174
    /**
1✔
175
     * 树节点的唯一标识
176
     * @type string
177
     */
178
    @Input() thyPrimaryKey = '_id';
179

2✔
180
    /**
1✔
181
     * 树节点的显示的字段 key
182
     * @type string
1✔
183
     */
184
    @Input() thyShowKey = 'name';
185

39✔
186
    @Input() thyChildCountKey = 'childCount';
187

188
    /**
196✔
189
     * 单选时,是否显示清除按钮,当为 true 时,显示清除按钮
190
     * @default false
191
     */
1✔
192
    @Input({ transform: coerceBooleanProperty }) thyAllowClear: boolean;
193

194
    /**
1✔
195
     * 是否多选
196
     * @type boolean
197
     */
198
    @Input({ transform: coerceBooleanProperty }) thyMultiple = false;
1✔
199

200
    /**
201
     * 是否禁用树选择器,当为 true 禁用树选择器
202
     * @type boolean
1✔
203
     */
204
    @Input({ transform: coerceBooleanProperty }) thyDisable = false;
×
205

4,083✔
206
    get thyDisabled(): boolean {
4,083✔
207
        return this.thyDisable;
4,083!
208
    }
112,044✔
209

112,044✔
210
    /**
112,044✔
211
     * 树选择框默认文字
4,042✔
212
     * @type string
4,042✔
213
     */
214
    @Input() thyPlaceholder = this.locale().placeholder;
215

4,083✔
216
    get placeholder() {
217
        return this.thyPlaceholder;
218
    }
28!
219

220
    /**
221
     * 控制树选择的输入框大小
62✔
222
     * @type xs | sm | md | default | lg
223
     */
4✔
224
    @Input() thySize: InputSize;
2!
225

2✔
226
    /**
1✔
227
     * 改变空选项的情况下的提示文本
2✔
228
     * @type string
229
     */
230
    @Input() thyEmptyOptionsText = this.locale().empty;
231

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

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

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

250
    /**
58✔
251
     * 是否展示全名
58✔
252
     * @type boolean
253
     */
254
    @Input({ transform: coerceBooleanProperty }) thyShowWholeName = false;
255

14✔
256
    /**
1✔
257
     * 是否展示搜索
258
     * @type boolean
13✔
259
     */
13✔
260
    @Input({ transform: coerceBooleanProperty }) thyShowSearch = false;
13✔
261

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

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

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

290
    /**
291
     * 获取节点的子节点,返回 Observable<ThyTreeSelectNode>。
1✔
292
     * @default (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([])
293
     */
294
    @Input() thyGetNodeChildren: (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([]);
295

3✔
296
    /**
1✔
297
     * 树选择组件展开和折叠状态事件
298
     */
3!
UNCOV
299
    @Output() thyExpandStatusChange: EventEmitter<boolean> = new EventEmitter<boolean>();
×
300

301
    private _getNgModelType() {
3!
302
        if (this.thyMultiple) {
3✔
303
            this.valueIsObject = !this.selectedValue[0] || isObject(this.selectedValue[0]);
3✔
304
        } else {
305
            this.valueIsObject = isObject(this.selectedValue);
3✔
306
        }
307
    }
308

309
    public buildFlattenTreeNodes() {
12✔
310
        this.virtualTreeNodes = this.getFlattenTreeNodes(this.treeNodes);
3✔
311
    }
3✔
312

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

326
    writeValue(value: any): void {
327
        this.selectedValue = value;
328

1✔
329
        if (value) {
1!
330
            this._getNgModelType();
1✔
331
        }
1✔
332
        this.setSelectedNodes();
1✔
333
    }
2✔
334

18✔
335
    constructor() {
336
        super();
337
    }
1✔
338

1✔
339
    ngOnInit() {
340
        this.isMulti = this.thyMultiple;
1✔
341
        this.flattenTreeNodes = this.flattenNodes(this.treeNodes, this.flattenTreeNodes, []);
342
        this.setSelectedNodes();
343
        this.initialled = true;
1✔
344

1✔
345
        if (this.thyVirtualScroll) {
346
            this.buildFlattenTreeNodes();
347
        }
348

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

371
    onFocus($event: FocusEvent) {
372
        const inputElement: HTMLInputElement = this.elementRef.nativeElement.querySelector('input');
373
        inputElement?.focus();
374
    }
375

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

9✔
385
    ngOnDestroy(): void {
386
        this.destroy$.next();
387
    }
388

389
    get selectedValueObject() {
390
        return this.thyMultiple ? this.selectedNodes : this.selectedNode;
391
    }
392

393
    searchValue(searchText: string) {
394
        this.treeNodes = filterTreeData(this.originTreeNodes, searchText.trim(), this.thyShowKey);
113✔
395
    }
396

397
    public setPosition() {
398
        this.ngZone.onStable
399
            .asObservable()
400
            .pipe(take(1))
401
            .subscribe(() => {
402
                this.cdkConnectedOverlay.overlayRef.updatePosition();
403
            });
404
    }
405

406
    private init() {
407
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
1✔
408
    }
409

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

428
    private _findTreeNode(value: string): ThyTreeSelectNode {
13✔
429
        return (this.flattenTreeNodes || []).find(item => item[this.thyPrimaryKey] === value);
430
    }
13✔
431

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

UNCOV
463
    openSelectPop() {
×
464
        if (this.thyDisable) {
465
            return;
466
        }
707✔
467
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
707✔
468
        this.expandTreeSelectOptions = !this.expandTreeSelectOptions;
536!
469
        this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
110✔
470
    }
471

472
    close() {
473
        if (this.expandTreeSelectOptions) {
171!
474
            this.expandTreeSelectOptions = false;
475
            this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
476
            this.onTouchedFn();
477
        }
707✔
478
    }
707✔
479

707✔
480
    clearSelectedValue(event: Event) {
481
        event.stopPropagation();
482
        this.selectedValue = null;
1✔
483
        this.selectedNode = null;
484
        this.selectedNodes = [];
485
        this.onChangeFn(this.selectedValue);
12!
486
    }
12✔
487

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

502
    removeMultipleSelectedNode(event: { item: ThyTreeSelectNode; $eventOrigin: Event }) {
1!
503
        this.removeSelectedNode(event.item, event.$eventOrigin);
1✔
504
    }
1✔
505

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

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

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

560
const DEFAULT_ITEM_SIZE = 40;
561

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

587
    @HostBinding('class') class: string;
588

589
    nodeList: ThyTreeSelectNode[] = [];
590

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

601
    @Input() thyVirtualScroll: boolean = false;
602

603
    public primaryKey = this.parent.thyPrimaryKey;
604

605
    public showKey = this.parent.thyShowKey;
606

607
    public isMultiple = this.parent.thyMultiple;
608

609
    public valueIsObject = this.parent.valueIsObject;
610

611
    public selectedValue = this.parent.selectedValue;
612

613
    public childCountKey = this.parent.thyChildCountKey;
614

615
    public treeNodeTemplateRef = this.parent.treeNodeTemplateRef;
616

617
    public defaultItemSize = DEFAULT_ITEM_SIZE;
618

619
    public thyVirtualHeight: string = null;
620

621
    public hasNodeChildren: boolean = false;
622

623
    ngOnInit() {
624
        this.class = this.isMultiple ? 'thy-tree-select-dropdown thy-tree-select-dropdown-multiple' : 'thy-tree-select-dropdown';
625
    }
626

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

637
    treeNodeIsHidden(node: ThyTreeSelectNode) {
638
        if (this.parent.thyHiddenNodeKey) {
639
            return node[this.parent.thyHiddenNodeKey];
640
        }
641
        if (this.parent.thyHiddenNodeFn) {
642
            return this.parent.thyHiddenNodeFn(node);
643
        }
644
        return false;
645
    }
646

647
    treeNodeIsDisable(node: ThyTreeSelectNode) {
648
        if (this.parent.thyDisableNodeKey) {
649
            return node[this.parent.thyDisableNodeKey];
650
        }
651
        if (this.parent.thyDisableNodeFn) {
652
            return this.parent.thyDisableNodeFn(node);
653
        }
654
        return false;
655
    }
656

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

673
    getNodeChildren(node: ThyTreeSelectNode) {
674
        return this.parent.getNodeChildren(node);
675
    }
676

677
    selectTreeNode(event: Event, node: ThyTreeSelectNode) {
678
        if (!this.treeNodeIsDisable(node)) {
679
            this.parent.selectNode(node);
680
        }
681
    }
682

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

695
        if (node.expand && this.parent.thyAsyncNode) {
696
            this.getNodeChildren(node).subscribe(() => {
697
                this.parent.setPosition();
698
            });
699
        }
700
        // this.parent.setPosition();
701
        if (this.thyVirtualScroll) {
702
            this.parent.buildFlattenTreeNodes();
703
        }
704
    }
705

706
    tabTrackBy(index: number, item: ThyTreeSelectNode) {
707
        return index;
708
    }
709
}
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