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

atinc / ngx-tethys / 13fcf11d-0958-4626-8dcf-f200b8133961

14 Jun 2024 10:13AM UTC coverage: 90.422%. Remained the same
13fcf11d-0958-4626-8dcf-f200b8133961

push

circleci

web-flow
feat: use the ngx-tethys/util's coerceBooleanProperty instead of booleanAttribute #INFR-12648 (#3106)

5467 of 6692 branches covered (81.69%)

Branch coverage included in aggregate %.

117 of 120 new or added lines in 66 files covered. (97.5%)

183 existing lines in 46 files now uncovered.

13216 of 13970 relevant lines covered (94.6%)

985.9 hits per line

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

87.47
/src/tree-select/tree-select.component.ts
1
import {
2
    TabIndexDisabledControlValueAccessorMixin,
3
    getFlexiblePositions,
4
    ThyClickDispatcher,
5
    EXPANDED_DROPDOWN_POSITIONS
6
} from 'ngx-tethys/core';
7
import { ThyTreeNode } from 'ngx-tethys/tree';
8
import { coerceBooleanProperty, elementMatchClosest, isArray, isObject, produce, warnDeprecation } from 'ngx-tethys/util';
9
import { Observable, of, Subject } from 'rxjs';
10
import { take, takeUntil } from 'rxjs/operators';
11

12
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectionPositionPair, ViewportRuler } from '@angular/cdk/overlay';
13
import { isPlatformBrowser, NgClass, NgFor, NgIf, 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
    Inject,
2✔
23
    Input,
24
    NgZone,
15!
25
    OnDestroy,
15✔
26
    OnInit,
15✔
27
    Output,
2✔
28
    PLATFORM_ID,
2✔
29
    TemplateRef,
30
    ViewChild
31
} from '@angular/core';
15✔
32
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
33

4✔
34
import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
2✔
35
import { ThyEmpty } from 'ngx-tethys/empty';
36
import { ThyIcon } from 'ngx-tethys/icon';
37
import { ThyFlexibleText } from 'ngx-tethys/flexible-text';
38
import { ThySelectControl, ThyStopPropagationDirective } from 'ngx-tethys/shared';
39
import { ThyTreeSelectNode, ThyTreeSelectType } from './tree-select.class';
40
import { scaleYMotion } from 'ngx-tethys/core';
41

1✔
42
type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '';
43

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

63
/**
64
 * 树选择组件
65
 * @name thy-tree-select
66
 * @order 10
67
 */
4✔
68
@Component({
2✔
69
    selector: 'thy-tree-select',
70
    templateUrl: './tree-select.component.html',
71
    providers: [
2✔
72
        {
73
            provide: NG_VALUE_ACCESSOR,
74
            useExisting: forwardRef(() => ThyTreeSelect),
75
            multi: true
3✔
76
        }
77
    ],
×
78
    standalone: true,
3✔
79
    imports: [
2,793✔
80
        CdkOverlayOrigin,
83,790✔
81
        ThySelectControl,
83,790✔
82
        NgIf,
2,790✔
83
        NgTemplateOutlet,
84
        CdkConnectedOverlay,
85
        forwardRef(() => ThyTreeSelectNodes),
2,793✔
86
        ThyStopPropagationDirective
87
    ],
2,790✔
88
    host: {
89
        '[attr.tabindex]': 'tabIndex',
90
        '(focus)': 'onFocus($event)',
20✔
91
        '(blur)': 'onBlur($event)'
20✔
92
    },
4✔
93
    animations: [scaleYMotion]
94
})
20✔
95
export class ThyTreeSelect extends TabIndexDisabledControlValueAccessorMixin implements OnInit, OnDestroy, ControlValueAccessor {
96
    @HostBinding('class.thy-select-custom') treeSelectClass = true;
97

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

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

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

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

38✔
107
    public selectedValue: any;
38✔
108

38✔
109
    public selectedNode: ThyTreeSelectNode;
38✔
110

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

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

115
    virtualTreeNodes: ThyTreeSelectNode[] = [];
116

117
    public cdkConnectOverlayWidth = 0;
118

38✔
119
    public positions: ConnectionPositionPair[];
38✔
120

38✔
121
    public expandedDropdownPositions = EXPANDED_DROPDOWN_POSITIONS;
38✔
122

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

38✔
129
    private initialled = false;
38✔
130

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

38✔
133
    public valueIsObject = false;
38✔
134

38✔
135
    originTreeNodes: ThyTreeSelectNode[];
38✔
136

38✔
137
    @ContentChild('thyTreeSelectTriggerDisplay')
38✔
138
    thyTreeSelectTriggerDisplayRef: TemplateRef<any>;
139

140
    @ContentChild('treeNodeTemplate')
38✔
141
    treeNodeTemplateRef: TemplateRef<any>;
38✔
142

38✔
143
    @ViewChild(CdkOverlayOrigin, { static: true }) cdkOverlayOrigin: CdkOverlayOrigin;
38✔
144

38✔
145
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
38✔
146

3✔
147
    @ViewChild('customDisplayTemplate', { static: true }) customDisplayTemplate: TemplateRef<any>;
148

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

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

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

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

1✔
180
    @Input() thyChildCountKey = 'childCount';
181

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

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

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

1✔
200
    get thyDisabled(): boolean {
201
        return this.thyDisable;
×
202
    }
3,146✔
203

3,146✔
204
    /**
3,146!
205
     * 树选择框默认文字
84,107✔
206
     * @type string
84,107✔
207
     */
84,107✔
208
    @Input() thyPlaceholder = '请选择节点';
3,107✔
209

3,107✔
210
    get placeholder() {
211
        return this.thyPlaceholder;
212
    }
3,146✔
213

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

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

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

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

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

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

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

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

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

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

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

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

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

3✔
303
    public buildFlattenTreeNodes() {
304
        this.virtualTreeNodes = this.getFlattenTreeNodes(this.treeNodes);
305
    }
306

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

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

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

1✔
329
    constructor(
1✔
330
        public elementRef: ElementRef,
2✔
331
        private ngZone: NgZone,
18✔
332
        private ref: ChangeDetectorRef,
333
        @Inject(PLATFORM_ID) private platformId: string,
334
        private thyClickDispatcher: ThyClickDispatcher,
1✔
335
        private viewportRuler: ViewportRuler
1✔
336
    ) {
337
        super();
1✔
338
    }
339

340
    ngOnInit() {
1✔
341
        this.positions = getFlexiblePositions('bottom', 4);
342
        this.isMulti = this.thyMultiple;
343
        this.flattenTreeNodes = this.flattenNodes(this.treeNodes, this.flattenTreeNodes, []);
344
        this.setSelectedNodes();
345
        this.initialled = true;
346

347
        if (this.thyVirtualScroll) {
348
            this.buildFlattenTreeNodes();
1✔
349
        }
350

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

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

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

387
    ngOnDestroy(): void {
388
        this.destroy$.next();
8✔
389
    }
390

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

395
    searchValue(searchText: string) {
396
        this.treeNodes = filterTreeData(this.originTreeNodes, searchText.trim(), this.thyShowKey);
397
    }
398

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

408
    private init() {
409
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
410
    }
411

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

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

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

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

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

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

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

504
    removeMultipleSelectedNode(event: { item: ThyTreeSelectNode; $eventOrigin: Event }) {
UNCOV
505
        this.removeSelectedNode(event.item, event.$eventOrigin);
×
506
    }
507

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

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

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

562
const DEFAULT_ITEM_SIZE = 40;
563

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

591
    nodeList: ThyTreeSelectNode[] = [];
592

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

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

602
    public primaryKey = this.parent.thyPrimaryKey;
603

604
    public showKey = this.parent.thyShowKey;
605

606
    public isMultiple = this.parent.thyMultiple;
607

608
    public valueIsObject = this.parent.valueIsObject;
609

610
    public selectedValue = this.parent.selectedValue;
611

612
    public childCountKey = this.parent.thyChildCountKey;
613

614
    public treeNodeTemplateRef = this.parent.treeNodeTemplateRef;
615

616
    public defaultItemSize = DEFAULT_ITEM_SIZE;
617

618
    public thyVirtualHeight: string = null;
619

620
    constructor(public parent: ThyTreeSelect) {}
621

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

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

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

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

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

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

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

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

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

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