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

atinc / ngx-tethys / 233ed64a-b8f5-4cf5-ac5f-a2f2763ca062

22 Nov 2024 07:31AM UTC coverage: 90.355% (+0.004%) from 90.351%
233ed64a-b8f5-4cf5-ac5f-a2f2763ca062

Pull #3272

circleci

minlovehua
feat: empty icon use 'preset-light' in dark theme #TINFR-975
Pull Request #3272: feat: panel empty icon use 'preset-light' in dark theme #TINFR-975

5548 of 6791 branches covered (81.7%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 3 files covered. (100.0%)

35 existing lines in 4 files now uncovered.

13263 of 14028 relevant lines covered (94.55%)

992.53 hits per line

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

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

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

41
import { ThyTreeSelectNode, ThyTreeSelectType } from './tree-select.class';
1✔
42
import { injectLocale, ThyTreeSelectLocale } from 'ngx-tethys/i18n';
43

40✔
44
type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '';
40✔
45

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

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

39✔
104
    @HostBinding('class.thy-select-custom') treeSelectClass = true;
39✔
105

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

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

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

39✔
113
    public treeNodes: ThyTreeSelectNode[];
39✔
114

39✔
115
    public selectedValue: any;
39✔
116

39✔
117
    public selectedNode: ThyTreeSelectNode;
118

119
    public selectedNodes: ThyTreeSelectNode[] = [];
120

121
    public flattenTreeNodes: ThyTreeSelectNode[] = [];
39✔
122

39✔
123
    virtualTreeNodes: ThyTreeSelectNode[] = [];
39✔
124

39✔
125
    public cdkConnectOverlayWidth = 0;
39✔
126

39✔
127
    public expandedDropdownPositions = EXPANDED_DROPDOWN_POSITIONS;
39✔
128

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

39✔
135
    private initialled = false;
39✔
136

39✔
137
    private destroy$ = new Subject<void>();
39✔
138

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

39✔
141
    public valueIsObject = false;
39✔
142

143
    originTreeNodes: ThyTreeSelectNode[];
144

39✔
145
    @ContentChild('thyTreeSelectTriggerDisplay')
39✔
146
    thyTreeSelectTriggerDisplayRef: TemplateRef<any>;
39✔
147

39✔
148
    @ContentChild('treeNodeTemplate')
39✔
149
    treeNodeTemplateRef: TemplateRef<any>;
4✔
150

151
    @ViewChild(CdkOverlayOrigin, { static: true }) cdkOverlayOrigin: CdkOverlayOrigin;
39!
152

39✔
153
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
154

155
    @ViewChild('customDisplayTemplate', { static: true }) customDisplayTemplate: TemplateRef<any>;
156

27✔
157
    /**
27✔
158
     * treeNodes 数据
2✔
159
     * @type ThyTreeSelectNode[]
2✔
160
     */
2✔
161
    @Input()
162
    set thyTreeNodes(value: ThyTreeSelectNode[]) {
163
        this.treeNodes = value;
164
        this.originTreeNodes = value;
165
        if (this.initialled) {
39✔
166
            this.flattenTreeNodes = this.flattenNodes(this.treeNodes, this.flattenTreeNodes, []);
167
            this.setSelectedNodes();
168

169
            if (this.thyVirtualScroll) {
1✔
170
                this.buildFlattenTreeNodes();
171
            }
172
        }
173
    }
1✔
174

1✔
175
    /**
176
     * 开启虚拟滚动
177
     */
178
    @Input({ transform: coerceBooleanProperty }) thyVirtualScroll: boolean = false;
179

2✔
180
    /**
1✔
181
     * 树节点的唯一标识
182
     * @type string
1✔
183
     */
184
    @Input() thyPrimaryKey = '_id';
185

39✔
186
    /**
187
     * 树节点的显示的字段 key
188
     * @type string
196✔
189
     */
190
    @Input() thyShowKey = 'name';
191

1✔
192
    @Input() thyChildCountKey = 'childCount';
193

194
    /**
1✔
195
     * 单选时,是否显示清除按钮,当为 true 时,显示清除按钮
196
     * @default false
197
     */
198
    @Input({ transform: coerceBooleanProperty }) thyAllowClear: boolean;
1✔
199

200
    /**
201
     * 是否多选
202
     * @type boolean
1✔
203
     */
204
    @Input({ transform: coerceBooleanProperty }) thyMultiple = false;
×
205

4,083✔
206
    /**
4,083✔
207
     * 是否禁用树选择器,当为 true 禁用树选择器
4,083!
208
     * @type boolean
112,044✔
209
     */
112,044✔
210
    @Input({ transform: coerceBooleanProperty }) thyDisable = false;
112,044✔
211

4,042✔
212
    get thyDisabled(): boolean {
4,042✔
213
        return this.thyDisable;
214
    }
215

4,083✔
216
    /**
217
     * 树选择框默认文字
218
     * @type string
28!
219
     */
220
    @Input() thyPlaceholder = this.locale().placeholder;
221

62✔
222
    get placeholder() {
223
        return this.thyPlaceholder;
4✔
224
    }
2!
225

2✔
226
    /**
1✔
227
     * 控制树选择的输入框大小
2✔
228
     * @type xs | sm | md | default | lg
229
     */
230
    @Input() thySize: InputSize;
231

1✔
232
    /**
2✔
233
     * 改变空选项的情况下的提示文本
234
     * @type string
235
     */
236
    @Input() thyEmptyOptionsText = this.locale().empty;
237

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

244
    /**
245
     * 设置是否禁用节点(不可进行任何操作),优先级高于 thyDisableNodeFn
1✔
246
     * @type string
247
     */
248
    @Input() thyDisableNodeKey = 'disabled';
249

250
    /**
58✔
251
     * 是否异步加载节点的子节点(显示加载状态),当为 true 时,异步获取
58✔
252
     * @type boolean
253
     */
254
    @Input({ transform: coerceBooleanProperty }) thyAsyncNode = false;
255

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

262
    /**
263
     * 是否展示搜索
7✔
264
     * @type boolean
2✔
265
     */
2✔
266
    @Input({ transform: coerceBooleanProperty }) thyShowSearch = false;
2✔
267

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

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

290
    /**
291
     * 设置是否禁用节点(不可进行任何操作),优先级低于 thyDisableNodeKey。
1✔
292
     * @default (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.disabled
293
     */
294
    @Input() thyDisableNodeFn: (node: ThyTreeSelectNode) => boolean = (node: ThyTreeSelectNode) => node.disabled;
295

3✔
296
    /**
1✔
297
     * 获取节点的子节点,返回 Observable<ThyTreeSelectNode>。
298
     * @default (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([])
3!
UNCOV
299
     */
×
300
    @Input() thyGetNodeChildren: (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([]);
301

3!
302
    /**
3✔
303
     * 树选择组件展开和折叠状态事件
3✔
304
     */
305
    @Output() thyExpandStatusChange: EventEmitter<boolean> = new EventEmitter<boolean>();
3✔
306

307
    private _getNgModelType() {
308
        if (this.thyMultiple) {
309
            this.valueIsObject = !this.selectedValue[0] || isObject(this.selectedValue[0]);
12✔
310
        } else {
3✔
311
            this.valueIsObject = isObject(this.selectedValue);
3✔
312
        }
3✔
313
    }
3✔
314

315
    public buildFlattenTreeNodes() {
316
        this.virtualTreeNodes = this.getFlattenTreeNodes(this.treeNodes);
9✔
317
    }
2✔
318

319
    private getFlattenTreeNodes(rootTrees: ThyTreeSelectNode[] = this.treeNodes) {
2✔
320
        const forEachTree = (tree: ThyTreeSelectNode[], fn: any, result: ThyTreeSelectNode[] = []) => {
321
            tree.forEach(item => {
322
                result.push(item);
7✔
323
                if (item.children && fn(item)) {
7✔
324
                    forEachTree(item.children, fn, result);
325
                }
326
            });
327
            return result;
328
        };
1✔
329
        return forEachTree(rootTrees, (node: ThyTreeSelectNode) => !!node.expand);
1!
330
    }
1✔
331

1✔
332
    writeValue(value: any): void {
1✔
333
        this.selectedValue = value;
2✔
334

18✔
335
        if (value) {
336
            this._getNgModelType();
337
        }
1✔
338
        this.setSelectedNodes();
1✔
339
    }
340

1✔
341
    constructor() {
342
        super();
343
    }
1✔
344

1✔
345
    ngOnInit() {
346
        this.isMulti = this.thyMultiple;
347
        this.flattenTreeNodes = this.flattenNodes(this.treeNodes, this.flattenTreeNodes, []);
348
        this.setSelectedNodes();
349
        this.initialled = true;
350

351
        if (this.thyVirtualScroll) {
352
            this.buildFlattenTreeNodes();
353
        }
354

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

377
    onFocus($event: FocusEvent) {
1✔
378
        const inputElement: HTMLInputElement = this.elementRef.nativeElement.querySelector('input');
379
        inputElement?.focus();
380
    }
381

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

391
    ngOnDestroy(): void {
392
        this.destroy$.next();
393
    }
394

113✔
395
    get selectedValueObject() {
396
        return this.thyMultiple ? this.selectedNodes : this.selectedNode;
397
    }
398

399
    searchValue(searchText: string) {
400
        this.treeNodes = filterTreeData(this.originTreeNodes, searchText.trim(), this.thyShowKey);
401
    }
402

403
    public setPosition() {
404
        this.ngZone.onStable
405
            .asObservable()
406
            .pipe(take(1))
407
            .subscribe(() => {
1✔
408
                this.cdkConnectedOverlay.overlayRef.updatePosition();
409
            });
410
    }
411

1✔
412
    private init() {
413
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
13✔
414
    }
13✔
415

13✔
416
    private flattenNodes(
13✔
417
        nodes: ThyTreeSelectNode[] = [],
13✔
418
        resultNodes: ThyTreeSelectNode[] = [],
13✔
419
        parentPrimaryValue: string[] = []
13✔
420
    ): ThyTreeSelectNode[] {
13✔
421
        resultNodes = resultNodes.concat(nodes);
13✔
422
        let nodesLeafs: ThyTreeSelectNode[] = [];
13✔
423
        (nodes || []).forEach(item => {
13✔
424
            item.parentValues = parentPrimaryValue;
13✔
425
            item.level = item.parentValues.length;
13✔
426
            if (item.children && isArray(item.children)) {
13✔
427
                const nodeLeafs = this.flattenNodes(item.children, resultNodes, [...parentPrimaryValue, item[this.thyPrimaryKey]]);
428
                nodesLeafs = [...nodesLeafs, ...nodeLeafs];
429
            }
13✔
430
        });
431
        return [...nodes, ...nodesLeafs];
13✔
432
    }
13✔
433

13!
434
    private _findTreeNode(value: string): ThyTreeSelectNode {
435
        return (this.flattenTreeNodes || []).find(item => item[this.thyPrimaryKey] === value);
436
    }
13✔
437

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

707✔
469
    openSelectPop() {
536!
470
        if (this.thyDisable) {
110✔
471
            return;
472
        }
473
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
474
        this.expandTreeSelectOptions = !this.expandTreeSelectOptions;
171!
475
        this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
476
    }
477

478
    close() {
707✔
479
        if (this.expandTreeSelectOptions) {
707✔
480
            this.expandTreeSelectOptions = false;
707✔
481
            this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
482
            this.onTouchedFn();
483
        }
1✔
484
    }
485

486
    clearSelectedValue(event: Event) {
12!
487
        event.stopPropagation();
12✔
488
        this.selectedValue = null;
489
        this.selectedNode = null;
490
        this.selectedNodes = [];
491
        this.onChangeFn(this.selectedValue);
1✔
492
    }
1!
493

1✔
494
    private _changeSelectValue() {
495
        if (this.valueIsObject) {
UNCOV
496
            this.selectedValue = this.thyMultiple ? this.selectedNodes : this.selectedNode;
×
UNCOV
497
        } else {
×
498
            this.selectedValue = this.thyMultiple
499
                ? this.selectedNodes.map(item => item[this.thyPrimaryKey])
UNCOV
500
                : this.selectedNode[this.thyPrimaryKey];
×
501
        }
502
        this.onChangeFn(this.selectedValue);
503
        if (!this.thyMultiple) {
1!
504
            this.onTouchedFn();
1✔
505
        }
1✔
506
    }
507

508
    removeMultipleSelectedNode(event: { item: ThyTreeSelectNode; $eventOrigin: Event }) {
509
        this.removeSelectedNode(event.item, event.$eventOrigin);
1!
UNCOV
510
    }
×
511

512
    // thyMultiple = true 时,移除数据时调用
513
    removeSelectedNode(node: ThyTreeSelectNode, event?: Event) {
514
        if (event) {
24✔
515
            event.stopPropagation();
516
        }
1✔
517
        if (this.thyDisable) {
518
            return;
519
        }
520
        if (this.thyMultiple) {
521
            this.selectedNodes = produce(this.selectedNodes).remove((item: ThyTreeSelectNode) => {
522
                return item[this.thyPrimaryKey] === node[this.thyPrimaryKey];
1✔
523
            });
524
            this._changeSelectValue();
525
        }
526
    }
527

528
    selectNode(node: ThyTreeSelectNode) {
529
        if (!this.thyMultiple) {
530
            this.selectedNode = node;
531
            this.expandTreeSelectOptions = false;
532
            this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
533
            this._changeSelectValue();
534
        } else {
535
            if (
536
                this.selectedNodes.find(item => {
537
                    return item[this.thyPrimaryKey] === node[this.thyPrimaryKey];
538
                })
539
            ) {
540
                this.removeSelectedNode(node);
541
            } else {
542
                this.selectedNodes = produce(this.selectedNodes).add(node);
543
                this._changeSelectValue();
544
            }
545
        }
546
    }
547

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

566
const DEFAULT_ITEM_SIZE = 40;
567

568
/**
569
 * @private
570
 */
571
@Component({
572
    selector: 'thy-tree-select-nodes',
573
    templateUrl: './tree-select-nodes.component.html',
574
    standalone: true,
575
    imports: [
576
        NgTemplateOutlet,
577
        CdkVirtualScrollViewport,
578
        CdkFixedSizeVirtualScroll,
579
        CdkVirtualForOf,
580
        ThyEmpty,
581
        NgClass,
582
        NgStyle,
583
        ThyIcon,
584
        ThyFlexibleText
585
    ],
586
    host: {
587
        '[attr.tabindex]': '-1'
588
    }
589
})
590
export class ThyTreeSelectNodes implements OnInit {
591
    parent = inject(ThyTreeSelect);
592
    emptyIcon: Signal<string> = injectPanelEmptyIcon();
593

594
    @HostBinding('class') class: string;
595

596
    nodeList: ThyTreeSelectNode[] = [];
597

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

608
    @Input() thyVirtualScroll: boolean = false;
609

610
    public primaryKey = this.parent.thyPrimaryKey;
611

612
    public showKey = this.parent.thyShowKey;
613

614
    public isMultiple = this.parent.thyMultiple;
615

616
    public valueIsObject = this.parent.valueIsObject;
617

618
    public selectedValue = this.parent.selectedValue;
619

620
    public childCountKey = this.parent.thyChildCountKey;
621

622
    public treeNodeTemplateRef = this.parent.treeNodeTemplateRef;
623

624
    public defaultItemSize = DEFAULT_ITEM_SIZE;
625

626
    public thyVirtualHeight: string = null;
627

628
    public hasNodeChildren: boolean = false;
629

630
    ngOnInit() {
631
        this.class = this.isMultiple ? 'thy-tree-select-dropdown thy-tree-select-dropdown-multiple' : 'thy-tree-select-dropdown';
632
    }
633

634
    treeNodeIsSelected(node: ThyTreeSelectNode) {
635
        if (this.parent.thyMultiple) {
636
            return (this.parent.selectedNodes || []).find(item => {
637
                return item[this.primaryKey] === node[this.primaryKey];
638
            });
639
        } else {
640
            return this.parent.selectedNode && this.parent.selectedNode[this.primaryKey] === node[this.primaryKey];
641
        }
642
    }
643

644
    treeNodeIsHidden(node: ThyTreeSelectNode) {
645
        if (this.parent.thyHiddenNodeKey) {
646
            return node[this.parent.thyHiddenNodeKey];
647
        }
648
        if (this.parent.thyHiddenNodeFn) {
649
            return this.parent.thyHiddenNodeFn(node);
650
        }
651
        return false;
652
    }
653

654
    treeNodeIsDisable(node: ThyTreeSelectNode) {
655
        if (this.parent.thyDisableNodeKey) {
656
            return node[this.parent.thyDisableNodeKey];
657
        }
658
        if (this.parent.thyDisableNodeFn) {
659
            return this.parent.thyDisableNodeFn(node);
660
        }
661
        return false;
662
    }
663

664
    treeNodeIsExpand(node: ThyTreeSelectNode) {
665
        let isSelectedNodeParent = false;
666
        if (this.parent.thyMultiple) {
667
            isSelectedNodeParent = !!(this.parent.selectedNodes || []).find(item => {
668
                return item.parentValues.indexOf(node[this.primaryKey]) > -1;
669
            });
670
        } else {
671
            isSelectedNodeParent = this.parent.selectedNode
672
                ? this.parent.selectedNode.parentValues.indexOf(node[this.primaryKey]) > -1
673
                : false;
674
        }
675
        const isExpand = node.expand || (Object.keys(node).indexOf('expand') < 0 && isSelectedNodeParent);
676
        node.expand = isExpand;
677
        return isExpand;
678
    }
679

680
    getNodeChildren(node: ThyTreeSelectNode) {
681
        return this.parent.getNodeChildren(node);
682
    }
683

684
    selectTreeNode(event: Event, node: ThyTreeSelectNode) {
685
        if (!this.treeNodeIsDisable(node)) {
686
            this.parent.selectNode(node);
687
        }
688
    }
689

690
    nodeExpandToggle(event: Event, node: ThyTreeSelectNode) {
691
        event.stopPropagation();
692
        if (Object.keys(node).indexOf('expand') > -1) {
693
            node.expand = !node.expand;
694
        } else {
695
            if (this.treeNodeIsExpand(node)) {
696
                node.expand = false;
697
            } else {
698
                node.expand = true;
699
            }
700
        }
701

702
        if (node.expand && this.parent.thyAsyncNode) {
703
            this.getNodeChildren(node).subscribe(() => {
704
                this.parent.setPosition();
705
            });
706
        }
707
        // this.parent.setPosition();
708
        if (this.thyVirtualScroll) {
709
            this.parent.buildFlattenTreeNodes();
710
        }
711
    }
712

713
    tabTrackBy(index: number, item: ThyTreeSelectNode) {
714
        return index;
715
    }
716
}
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