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

atinc / ngx-tethys / #55

30 Jul 2025 07:08AM UTC coverage: 9.866% (-80.4%) from 90.297%
#55

push

why520crazy
feat(empty): add setMessage for update display text #TINFR-2616

92 of 6794 branches covered (1.35%)

Branch coverage included in aggregate %.

2014 of 14552 relevant lines covered (13.84%)

6.15 hits per line

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

2.13
/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, ConnectionPositionPair, ViewportRuler } from '@angular/cdk/overlay';
18
import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
19
import { isPlatformBrowser, NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
×
20
import {
×
21
    ChangeDetectorRef,
×
22
    Component,
×
23
    ContentChild,
×
24
    ElementRef,
25
    forwardRef,
×
26
    HostBinding,
×
27
    Input,
×
28
    NgZone,
×
29
    OnDestroy,
×
30
    OnInit,
31
    PLATFORM_ID,
32
    TemplateRef,
×
33
    ViewChild,
34
    inject,
×
35
    Signal,
×
36
    output
37
} from '@angular/core';
38
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
39

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

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

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

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

×
102
    @HostBinding('class.thy-select-custom') treeSelectClass = true;
×
103

×
104
    @HostBinding('class.thy-select') isTreeSelect = true;
×
105

×
106
    // 菜单是否展开
×
107
    @HostBinding('class.menu-is-opened') expandTreeSelectOptions = false;
×
108

×
109
    @HostBinding('class.thy-select-custom--multiple') isMulti = false;
×
110

×
111
    public treeNodes: ThyTreeSelectNode[];
×
112

×
113
    public selectedValue: any;
×
114

×
115
    public selectedNode: ThyTreeSelectNode;
×
116

×
117
    public selectedNodes: ThyTreeSelectNode[] = [];
×
118

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

121
    virtualTreeNodes: ThyTreeSelectNode[] = [];
122

×
123
    public cdkConnectOverlayWidth = 0;
×
124

×
125
    public expandedDropdownPositions: ConnectionPositionPair[] = EXPANDED_DROPDOWN_POSITIONS;
×
126

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

×
133
    private initialled = false;
×
134

×
135
    private destroy$ = new Subject<void>();
×
136

×
137
    private locale: Signal<ThyTreeSelectLocale> = injectLocale('treeSelect');
×
138

×
139
    public valueIsObject = false;
×
140

×
141
    originTreeNodes: ThyTreeSelectNode[];
×
142

×
143
    @ContentChild('thyTreeSelectTriggerDisplay')
144
    thyTreeSelectTriggerDisplayRef: TemplateRef<any>;
145

×
146
    @ContentChild('treeNodeTemplate')
×
147
    treeNodeTemplateRef: TemplateRef<any>;
×
148

×
149
    @ViewChild(CdkOverlayOrigin, { static: true }) cdkOverlayOrigin: CdkOverlayOrigin;
×
150

×
151
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
152

×
153
    @ViewChild('customDisplayTemplate', { static: true }) customDisplayTemplate: TemplateRef<any>;
×
154

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

×
167
            if (this.thyVirtualScroll) {
168
                this.buildFlattenTreeNodes();
169
            }
170
        }
×
171
    }
172

173
    /**
174
     * 开启虚拟滚动
×
175
     */
×
176
    @Input({ transform: coerceBooleanProperty }) thyVirtualScroll: boolean = false;
177

178
    /**
179
     * 树节点的唯一标识
180
     * @type string
×
181
     */
×
182
    @Input() thyPrimaryKey = '_id';
183

×
184
    /**
185
     * 树节点的显示的字段 key
186
     * @type string
×
187
     */
188
    @Input() thyShowKey = 'name';
189

×
190
    @Input() thyChildCountKey = 'childCount';
191

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

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

×
204
    /**
205
     * 是否禁用树选择器,当为 true 禁用树选择器
×
206
     * @type boolean
×
207
     */
×
208
    @Input({ transform: coerceBooleanProperty }) thyDisable = false;
×
209

×
210
    get thyDisabled(): boolean {
×
211
        return this.thyDisable;
×
212
    }
×
213

×
214
    /**
215
     * 树选择框默认文字
216
     * @type string
×
217
     */
218
    @Input() thyPlaceholder = this.locale().placeholder;
219

×
220
    get placeholder() {
221
        return this.thyPlaceholder;
222
    }
×
223

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

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

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

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

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

254
    /**
255
     * 是否展示全名
256
     * @type boolean
×
257
     */
×
258
    @Input({ transform: coerceBooleanProperty }) thyShowWholeName = false;
259

×
260
    /**
×
261
     * 是否展示搜索
×
262
     * @type boolean
263
     */
264
    @Input({ transform: coerceBooleanProperty }) thyShowSearch = false;
×
265

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

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

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

294
    /**
295
     * 获取节点的子节点,返回 Observable<ThyTreeSelectNode>。
296
     * @default (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([])
×
297
     */
×
298
    @Input() thyGetNodeChildren: (node: ThyTreeSelectNode) => Observable<ThyTreeSelectNode> = (node: ThyTreeSelectNode) => of([]);
299

×
300
    /**
×
301
     * 树选择组件展开和折叠状态事件
302
     */
×
303
    readonly thyExpandStatusChange = output<boolean>();
×
304

×
305
    private _getNgModelType() {
306
        if (this.thyMultiple) {
×
307
            this.valueIsObject = !this.selectedValue[0] || isObject(this.selectedValue[0]);
308
        } else {
309
            this.valueIsObject = isObject(this.selectedValue);
310
        }
×
311
    }
×
312

×
313
    public buildFlattenTreeNodes() {
×
314
        this.virtualTreeNodes = this.getFlattenTreeNodes(this.treeNodes);
×
315
    }
316

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

×
330
    writeValue(value: any): void {
×
331
        this.selectedValue = value;
×
332

×
333
        if (value) {
×
334
            this._getNgModelType();
×
335
        }
×
336
        this.setSelectedNodes();
337
    }
338

×
339
    constructor() {
×
340
        super();
341
    }
×
342

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

349
        if (this.thyVirtualScroll) {
350
            this.buildFlattenTreeNodes();
351
        }
352

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

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

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

389
    ngOnDestroy(): void {
390
        this.destroy$.next();
391
    }
392

393
    get selectedValueObject() {
394
        return this.thyMultiple ? this.selectedNodes : this.selectedNode;
×
395
    }
396

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

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

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

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

×
432
    private _findTreeNode(value: string): ThyTreeSelectNode {
×
433
        return (this.flattenTreeNodes || []).find(item => item[this.thyPrimaryKey] === value);
×
434
    }
435

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

467
    openSelectPop() {
×
468
        if (this.thyDisable) {
×
469
            return;
×
470
        }
×
471
        this.cdkConnectOverlayWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width;
472
        this.expandTreeSelectOptions = !this.expandTreeSelectOptions;
473
        this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
474
    }
×
475

476
    close() {
477
        if (this.expandTreeSelectOptions) {
478
            this.expandTreeSelectOptions = false;
×
479
            this.thyExpandStatusChange.emit(this.expandTreeSelectOptions);
×
480
            this.onTouchedFn();
×
481
        }
482
    }
483

×
484
    clearSelectedValue(event: Event) {
485
        event.stopPropagation();
486
        this.selectedValue = null;
×
487
        this.selectedNode = null;
×
488
        this.selectedNodes = [];
489
        this.onChangeFn(this.selectedValue);
490
    }
491

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

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

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

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

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

564
const DEFAULT_ITEM_SIZE = 40;
565

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

591
    @HostBinding('class') class: string;
592

593
    nodeList: ThyTreeSelectNode[] = [];
594

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

605
    @Input() thyVirtualScroll: boolean = false;
606

607
    public primaryKey = this.parent.thyPrimaryKey;
608

609
    public showKey = this.parent.thyShowKey;
610

611
    public isMultiple = this.parent.thyMultiple;
612

613
    public valueIsObject = this.parent.valueIsObject;
614

615
    public selectedValue = this.parent.selectedValue;
616

617
    public childCountKey = this.parent.thyChildCountKey;
618

619
    public treeNodeTemplateRef = this.parent.treeNodeTemplateRef;
620

621
    public defaultItemSize = DEFAULT_ITEM_SIZE;
622

623
    public thyVirtualHeight: string = null;
624

625
    public hasNodeChildren: boolean = false;
626

627
    ngOnInit() {
628
        this.class = this.isMultiple ? 'thy-tree-select-dropdown thy-tree-select-dropdown-multiple' : 'thy-tree-select-dropdown';
629
    }
630

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

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

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

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

677
    getNodeChildren(node: ThyTreeSelectNode) {
678
        return this.parent.getNodeChildren(node);
679
    }
680

681
    selectTreeNode(event: Event, node: ThyTreeSelectNode) {
682
        if (!this.treeNodeIsDisable(node)) {
683
            this.parent.selectNode(node);
684
        }
685
    }
686

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

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

710
    tabTrackBy(index: number, item: ThyTreeSelectNode) {
711
        return index;
712
    }
713
}
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