• 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

1.83
/src/tree/tree.component.ts
1
import { coerceBooleanProperty, helpers } from 'ngx-tethys/util';
2
import { useHostRenderer } from '@tethys/cdk/dom';
3
import { SelectionModel } from '@angular/cdk/collections';
4
import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf } from '@angular/cdk/scrolling';
5
import {
6
    ChangeDetectionStrategy,
7
    Component,
8
    forwardRef,
9
    numberAttribute,
10
    TemplateRef,
11
    ViewEncapsulation,
12
    inject,
13
    input,
14
    signal,
15
    effect,
16
    computed,
17
    model,
18
    DestroyRef,
19
    viewChild,
20
    contentChild,
21
    viewChildren,
1✔
22
    afterNextRender,
23
    output
24
} from '@angular/core';
25
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
1✔
26
import { THY_TREE_ABSTRACT_TOKEN } from './tree-abstract';
27
import { ThyTreeNode } from './tree.class';
28
import {
29
    ThyTreeBeforeDragDropContext,
30
    ThyTreeBeforeDragStartContext,
31
    ThyClickBehavior,
32
    ThyTreeDragDropEvent,
33
    ThyTreeDropPosition,
1✔
34
    ThyTreeEmitEvent,
35
    ThyTreeIcons,
×
36
    ThyTreeNodeCheckState,
37
    ThyTreeNodeData
38
} from './tree.class';
×
39
import { ThyTreeService } from './tree.service';
×
40
import { ThyTreeNodeComponent } from './tree-node.component';
×
41
import { DOCUMENT } from '@angular/common';
×
42
import { CdkDrag, CdkDragDrop, CdkDragEnd, CdkDragMove, CdkDragStart, CdkDropList } from '@angular/cdk/drag-drop';
×
43
import { filter, startWith, takeUntil } from 'rxjs/operators';
×
44
import { Subject } from 'rxjs';
45
import { ThyTreeNodeDraggablePipe } from './tree.pipe';
×
46
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
×
47

×
48
type ThyTreeSize = 'sm' | 'default';
×
49

50
type ThyTreeType = 'default' | 'especial';
×
51

×
52
const treeTypeClassMap = {
×
53
    default: ['thy-tree-default'],
×
54
    especial: ['thy-tree-especial']
×
55
};
×
56

×
57
const treeItemSizeMap = {
×
58
    default: 44,
×
59
    sm: 42
×
60
};
×
61

×
62
/**
×
63
 * 树形控件组件
×
64
 * @name thy-tree
×
65
 */
66
@Component({
×
67
    selector: 'thy-tree',
×
68
    templateUrl: './tree.component.html',
69
    encapsulation: ViewEncapsulation.None,
×
70
    changeDetection: ChangeDetectionStrategy.OnPush,
71
    host: {
72
        class: 'thy-tree',
×
73
        '[class.thy-multiple-selection-list]': 'thyMultiple()',
×
74
        '[class.thy-virtual-scrolling-tree]': 'thyVirtualScroll()',
×
75
        '[class.thy-tree-draggable]': 'thyDraggable()',
76
        '[class.thy-tree-dragging]': 'dragging()'
×
77
    },
78
    providers: [
×
79
        {
×
80
            provide: NG_VALUE_ACCESSOR,
×
81
            useExisting: forwardRef(() => ThyTree),
×
82
            multi: true
×
83
        },
84
        {
×
85
            provide: THY_TREE_ABSTRACT_TOKEN,
×
86
            useExisting: forwardRef(() => ThyTree)
87
        },
88
        ThyTreeService
×
89
    ],
90
    imports: [
91
        CdkDrag,
×
92
        CdkDropList,
×
93
        CdkVirtualScrollViewport,
×
94
        CdkFixedSizeVirtualScroll,
×
95
        CdkVirtualForOf,
×
96
        ThyTreeNodeComponent,
×
97
        ThyTreeNodeDraggablePipe
×
98
    ]
×
99
})
×
100
export class ThyTree implements ControlValueAccessor {
×
101
    thyTreeService = inject(ThyTreeService);
×
102
    private document = inject(DOCUMENT);
×
103
    private destroyRef = inject(DestroyRef);
×
104

×
105
    private expandedKeys: (string | number)[];
×
106

×
107
    private selectedKeys: (string | number)[];
×
108

×
109
    private hostRenderer = useHostRenderer();
×
110

×
111
    private _onTouched: () => void = () => {};
112

113
    private _onChange: (value: any) => void = (_: any) => {};
×
114

×
115
    // 缓存 Element 和 DragRef 的关系,方便在 Item 拖动时查找
116
    private nodeDragsMap = new Map<HTMLElement, CdkDrag<ThyTreeNode>>();
×
117

×
118
    private nodeDragMoved = new Subject<CdkDragMove>();
119

×
120
    private startDragNodeClone: ThyTreeNode;
×
121

122
    // Node 拖动经过目标时临时记录目标id以及相对应目标的位置
×
123
    private nodeDropTarget: {
×
124
        position?: ThyTreeDropPosition;
125
        key?: number | string;
×
126
    };
×
127

128
    private dropEnterPredicate?: (context: { source: ThyTreeNode; target: ThyTreeNode; dropPosition: ThyTreeDropPosition }) => boolean =
×
129
        context => {
×
130
            return (
×
131
                this.isShowExpand(context.target) || (!this.isShowExpand(context.target) && context.dropPosition !== ThyTreeDropPosition.in)
×
132
            );
×
133
        };
134

×
135
    public selectionModel: SelectionModel<ThyTreeNode>;
136

137
    public get treeNodes() {
138
        return this.thyTreeService.treeNodes;
×
139
    }
×
140

141
    public readonly flattenTreeNodes = computed(() => this.thyTreeService.flattenTreeNodes());
142

143
    /**
×
144
     * 虚拟化滚动的视口
145
     */
×
146
    readonly viewport = viewChild<CdkVirtualScrollViewport>('viewport');
147

148
    /**
149
     * TreeNode 展现所需的数据
150
     * @type ThyTreeNodeData[]
×
151
     */
152
    readonly thyNodes = model<ThyTreeNodeData[]>(undefined);
×
153

×
154
    /**
155
     * 设置 TreeNode 是否支持展开
×
156
     * @type boolean | Function
×
157
     */
158
    readonly thyShowExpand = input<boolean | ((_: ThyTreeNodeData) => boolean)>(true);
159

160
    /**
×
161
     * 设置是否支持多选节点
×
162
     */
×
163
    readonly thyMultiple = input(false, { transform: coerceBooleanProperty });
×
164

×
165
    /**
166
     * 设置 TreeNode 是否支持拖拽排序
167
     * @default false
×
168
     */
×
169
    readonly thyDraggable = input(false, { transform: coerceBooleanProperty });
170

171
    /**
×
172
     * 设置 TreeNode 是否支持 Checkbox 选择
×
173
     * @default false
174
     */
175
    readonly thyCheckable = input(false, { transform: coerceBooleanProperty });
176

×
177
    /**
×
178
     * 点击节点的行为,`default` 为选中当前节点,`selectCheckbox` 为选中节点的 Checkbox, `thyCheckable` 为 true 时生效。
×
179
     */
×
180
    readonly thyClickBehavior = input<ThyClickBehavior>('default');
181

182
    /**
183
     * 设置 check 状态的计算策略
184
     */
×
185
    readonly thyCheckStateResolve = input<(node: ThyTreeNode) => ThyTreeNodeCheckState>();
×
186

×
187
    /**
188
     * 设置 TreeNode 是否支持异步加载
189
     */
190
    readonly thyAsync = input(false, { transform: coerceBooleanProperty });
×
191

192
    /**
193
     * 设置不同展示类型的 Tree,`default` 为小箭头展示, `especial` 为 加减号图标展示
×
194
     * @type ThyTreeType
×
195
     * @default default
×
196
     */
×
197
    readonly thyType = input<ThyTreeType>('default');
198

199
    /**
200
     * 设置不同 Tree 展开折叠的图标,`expand` 为展开状态的图标,`collapse` 为折叠状态的图标
201
     * @type { expand: string, collapse: string }
×
202
     */
203
    readonly thyIcons = input<ThyTreeIcons>({});
204
    /**
×
205
     * 支持 `sm` | `default` 两种大小,默认值为 `default`
×
206
     * @type ThyTreeSize
207
     * @default default
208
     */
209
    readonly thySize = input<ThyTreeSize>('default');
×
210

211
    /**
212
     * 设置是否开启虚拟滚动
×
213
     */
×
214
    readonly thyVirtualScroll = input(false, { transform: coerceBooleanProperty });
×
215

216
    /**
217
     * 开启虚拟滚动时,单行节点的高度,当`thySize`为`default`时,该参数才生效
×
218
     * @default 44
219
     */
220
    readonly thyItemSize = input(44, {
221
        transform: value => {
×
222
            if (value && this.thySize() !== 'default') {
×
223
                throw new Error('setting thySize and thyItemSize at the same time is not allowed');
224
            }
225
            return numberAttribute(value);
226
        }
×
227
    });
228

229
    protected readonly icons = computed(() => {
×
230
        if (this.thyType() === 'especial') {
231
            return { expand: 'minus-square', collapse: 'plus-square' };
232
        }
×
233
        return this.thyIcons();
×
234
    });
×
235

×
236
    public readonly itemSize = computed(() => {
237
        const itemSize = this.thyItemSize();
238
        const size = this.thySize();
239
        if (size === 'default') {
×
240
            return itemSize || treeItemSizeMap.default;
241
        } else if (size) {
242
            return treeItemSizeMap[size] || treeItemSizeMap.default;
243
        } else {
×
244
            return treeItemSizeMap.default;
×
245
        }
×
246
    });
×
247

248
    /**
×
249
     * 设置节点名称是否支持超出截取
250
     * @type boolean
251
     */
×
252
    readonly thyTitleTruncate = input(true, { transform: coerceBooleanProperty });
×
253

×
254
    /**
255
     * 已选中的 node 节点集合
256
     * @default []
×
257
     */
258
    readonly thySelectedKeys = input<string[]>(undefined);
259

260
    /**
261
     * 展开指定的树节点
×
262
     */
×
263
    readonly thyExpandedKeys = input<(string | number)[]>(undefined);
×
264

265
    /**
266
     * 是否展开所有树节点
267
     */
268
    readonly thyExpandAll = input(false, { transform: coerceBooleanProperty });
×
269

270
    /**
271
     * 设置缩进距离,缩进距离 = thyIndent * node.level
×
272
     * @type number
×
273
     */
274
    readonly thyIndent = input(25, { transform: numberAttribute });
275

276
    /**
×
277
     * 拖拽之前的回调,函数返回 false 则阻止拖拽
278
     */
279
    readonly thyBeforeDragStart = input<(context: ThyTreeBeforeDragStartContext) => boolean>(undefined);
280

×
281
    /**
282
     * 拖放到元素时回调,函数返回 false 则阻止拖放到当前元素
×
283
     */
×
284
    readonly thyBeforeDragDrop = input<(context: ThyTreeBeforeDragDropContext) => boolean>(undefined);
×
285

286
    /**
287
     * 设置子 TreeNode 点击事件
288
     */
×
289
    readonly thyOnClick = output<ThyTreeEmitEvent>();
×
290

291
    /**
×
292
     * 设置 check 选择事件
×
293
     */
×
294
    readonly thyOnCheckboxChange = output<ThyTreeEmitEvent>();
295

×
296
    /**
×
297
     * 设置点击展开触发事件
×
298
     */
×
299
    readonly thyOnExpandChange = output<ThyTreeEmitEvent>();
×
300

×
301
    /**
302
     * 设置 TreeNode 拖拽事件
303
     */
304
    readonly thyOnDragDrop = output<ThyTreeDragDropEvent>();
305

306
    /**
307
     * 双击 TreeNode 事件
×
308
     */
×
309
    readonly thyDblClick = output<ThyTreeEmitEvent>();
×
310

×
311
    /**
312
     * 设置 TreeNode 的渲染模板
×
313
     */
×
314
    readonly templateRef = contentChild<TemplateRef<any>>('treeNodeTemplate');
315

×
316
    /**
×
317
     * 设置子的空数据渲染模板
×
318
     */
319
    readonly emptyChildrenTemplate = contentChild<TemplateRef<any>>('emptyChildrenTemplate');
×
320

×
321
    dragging = signal(false);
×
322

323
    readonly cdkDrags = viewChildren(CdkDrag);
×
324

×
325
    constructor() {
326
        effect(() => {
×
327
            const resolve = this.thyCheckStateResolve();
×
328
            if (resolve) {
×
329
                this.thyTreeService.setCheckStateResolve(resolve);
×
330
            }
×
331
        });
×
332

×
333
        effect(() => {
334
            this.initThyNodes();
×
335
        });
×
336

×
337
        effect(() => {
338
            this.setTreeSize();
339
        });
×
340

×
341
        effect(() => {
342
            this.setTreeType();
×
343
        });
344

345
        effect(() => {
346
            this.instanceSelectionModel();
347
        });
×
348

349
        effect(() => {
350
            this.selectTreeNodes(this.thySelectedKeys());
×
351
        });
×
352

353
        effect(() => {
×
354
            const drags = this.cdkDrags();
×
355
            this.nodeDragsMap.clear();
356
            drags.forEach(drag => {
×
357
                if (drag.data) {
×
358
                    // cdkDrag 变化时,缓存 Element 与 DragRef 的关系,方便 Drag Move 时查找
359
                    this.nodeDragsMap.set(drag.element.nativeElement, drag);
360
                }
×
361
            });
362
        });
363

364
        afterNextRender(() => {
×
365
            this.nodeDragMoved
×
366
                .pipe(
×
367
                    // auditTime(30),
368
                    //  auditTime 可能会导致拖动结束后仍然执行 moved ,所以通过判断 dragging 状态来过滤无效 moved
369
                    filter((event: CdkDragMove) => event.source._dragRef.isDragging()),
370
                    takeUntilDestroyed(this.destroyRef)
×
371
                )
×
372
                .subscribe(event => {
×
373
                    this.onDragMoved(event);
374
                });
375
        });
×
376
    }
×
377

378
    eventTriggerChanged(event: ThyTreeEmitEvent): void {
379
        switch (event.eventName) {
380
            case 'expand':
×
381
                this.thyOnExpandChange.emit(event);
×
382
                break;
×
383

384
            case 'checkboxChange':
385
                this.thyOnCheckboxChange.emit(event);
386
                break;
×
387
        }
388
    }
389

×
390
    private initThyNodes() {
391
        this.expandedKeys = this.getExpandedNodes().map(node => node.key);
392
        this.selectedKeys = this.getSelectedNodes().map(node => node.key);
×
393
        this.thyTreeService.initializeTreeNodes(this.thyNodes());
394
        this.selectTreeNodes(this.selectedKeys);
395
        this.handleExpandedKeys();
×
396
    }
397

398
    private handleExpandedKeys() {
×
399
        if (this.thyExpandAll()) {
400
            this.thyTreeService.expandTreeNodes(true);
×
401
        } else {
×
402
            this.expandedKeys = helpers.concatArray(
×
403
                (this.thyExpandedKeys() || []).filter(key => !this.expandedKeys.includes(key)),
404
                this.expandedKeys
405
            );
×
406
            this.thyTreeService.expandTreeNodes(this.expandedKeys);
×
407
        }
408
    }
×
409

×
410
    private setTreeType() {
411
        const type = this.thyType();
412
        if (type && treeTypeClassMap[type]) {
×
413
            treeTypeClassMap[type].forEach(className => {
×
414
                this.hostRenderer.addClass(className);
415
            });
416
        }
×
417
    }
×
418

419
    private setTreeSize() {
1✔
420
        const size = this.thySize();
1✔
421
        if (size) {
422
            this.hostRenderer.addClass(`thy-tree-${size}`);
423
        }
424
    }
425

426
    private instanceSelectionModel() {
427
        this.selectionModel = new SelectionModel<any>(this.thyMultiple());
428
    }
429

430
    private selectTreeNodes(keys: (string | number)[]) {
431
        (keys || []).forEach(key => {
432
            const node = this.thyTreeService.getTreeNode(key);
433
            if (node) {
434
                this.selectTreeNode(this.thyTreeService.getTreeNode(key));
435
            }
436
        });
437
    }
438

439
    isSelected(node: ThyTreeNode) {
440
        return this.selectionModel.isSelected(node);
441
    }
442

443
    toggleTreeNode(node: ThyTreeNode) {
444
        if (node && !node.isDisabled) {
445
            this.selectionModel.toggle(node);
446
        }
447
    }
448

449
    trackByFn(index: number, item: any) {
450
        return item.key || index;
451
    }
452

1✔
453
    isShowExpand(node: ThyTreeNode) {
454
        const thyShowExpand = this.thyShowExpand();
455
        if (helpers.isFunction(thyShowExpand)) {
456
            return (thyShowExpand as Function)(node);
457
        } else {
458
            return thyShowExpand;
459
        }
460
    }
461

462
    writeValue(value: ThyTreeNodeData[]): void {
463
        if (value) {
464
            this.thyNodes.set(value);
465
        }
466
    }
467

468
    registerOnChange(fn: any): void {
×
469
        this._onChange = fn;
470
    }
471

472
    registerOnTouched(fn: any): void {
473
        this._onTouched = fn;
×
474
    }
475

476
    onDragStarted(event: CdkDragStart<ThyTreeNode>) {
477
        this.dragging.set(true);
478
        this.startDragNodeClone = Object.assign({}, event.source.data);
479
        if (event.source.data.isExpanded) {
480
            event.source.data.setExpanded(false);
481
        }
482
    }
483

484
    emitDragMoved(event: CdkDragMove) {
485
        this.nodeDragMoved.next(event);
486
    }
487

488
    onDragMoved(event: CdkDragMove<ThyTreeNode>) {
489
        // 通过鼠标位置查找对应的目标 Item 元素
490
        let currentPointElement = this.document.elementFromPoint(event.pointerPosition.x, event.pointerPosition.y) as HTMLElement;
491
        if (!currentPointElement) {
492
            this.cleanupDragArtifacts();
493
            return;
494
        }
495
        let targetElement = currentPointElement.classList.contains('thy-tree-node')
496
            ? currentPointElement
497
            : (currentPointElement.closest('.thy-tree-node') as HTMLElement);
498
        if (!targetElement) {
499
            this.cleanupDragArtifacts();
500
            return;
501
        }
502
        // 缓存放置目标Id 并计算鼠标相对应的位置
503
        this.nodeDropTarget = {
504
            key: this.nodeDragsMap.get(targetElement)?.data.key,
505
            position: this.getTargetPosition(targetElement, event)
506
        };
507
        // 执行外部传入的 dropEnterPredicate 判断是否允许拖入目标项
508
        if (this.dropEnterPredicate) {
509
            const targetDragRef = this.nodeDragsMap.get(targetElement);
510
            if (
511
                this.dropEnterPredicate({
512
                    source: event.source.data,
513
                    target: targetDragRef.data,
514
                    dropPosition: this.nodeDropTarget.position
515
                })
516
            ) {
517
                this.showDropPositionPlaceholder(targetElement);
518
            } else {
519
                this.nodeDropTarget = null;
520
                this.cleanupDragArtifacts();
521
            }
522
        } else {
523
            this.showDropPositionPlaceholder(targetElement);
524
        }
525
    }
526

527
    onDragEnded(event: CdkDragEnd<ThyTreeNode>) {
528
        this.dragging.set(false);
529
        // 拖拽结束后恢复原始的展开状态
530
        event.source.data.setExpanded(this.startDragNodeClone.isExpanded);
531
        setTimeout(() => {
532
            this.startDragNodeClone = null;
533
        });
534
    }
535

536
    onListDropped(event: CdkDragDrop<ThyTreeNode[], ThyTreeNode[], ThyTreeNode>) {
537
        if (!this.nodeDropTarget) {
538
            return;
539
        }
540
        if (!this.isShowExpand(this.startDragNodeClone) && this.nodeDropTarget.position === ThyTreeDropPosition.in) {
541
            this.cleanupDragArtifacts();
542
            return;
543
        }
544

545
        const sourceNode = this.startDragNodeClone;
546
        const sourceNodeParent = sourceNode.parentNode;
547
        const targetDragRef = this.cdkDrags().find(item => item.data?.key === this.nodeDropTarget.key);
548
        const targetNode = targetDragRef?.data;
549
        const targetNodeParent = targetNode.parentNode;
550

551
        const beforeDragDropContext: ThyTreeBeforeDragDropContext = {
552
            previousItem: sourceNode,
553
            previousContainerItems: sourceNodeParent?.children,
554
            item: targetNode,
555
            containerItems: targetNodeParent?.children,
556
            position: this.nodeDropTarget.position
557
        };
558

559
        const thyBeforeDragDrop = this.thyBeforeDragDrop();
560
        if (thyBeforeDragDrop && !thyBeforeDragDrop(beforeDragDropContext)) {
561
            this.cleanupDragArtifacts();
562
            return;
563
        }
564

565
        this.thyTreeService.deleteTreeNode(sourceNode);
566

567
        switch (this.nodeDropTarget.position) {
568
            case 'before':
569
                const beforeInsertIndex = (targetNodeParent?.children || this.treeNodes).indexOf(targetNode);
570
                this.thyTreeService.addTreeNode(sourceNode, targetNodeParent, beforeInsertIndex);
571
                break;
572
            case 'after':
573
                const afterInsertIndex = (targetNodeParent?.children || this.treeNodes).indexOf(targetNode) + 1;
574
                this.thyTreeService.addTreeNode(sourceNode, targetNodeParent, afterInsertIndex);
575
                break;
576
            case 'in':
577
                this.thyTreeService.addTreeNode(sourceNode, targetNode);
578
                break;
579
        }
580

581
        this.thyTreeService.syncFlattenTreeNodes();
582

583
        let after: ThyTreeNode = null;
584
        let targe: ThyTreeNode = null;
585
        if (beforeDragDropContext.position === ThyTreeDropPosition.before) {
586
            const targetContainerNodes = targetNodeParent?.children || this.treeNodes;
587
            after = targetContainerNodes[targetContainerNodes.indexOf(targetNode) - 2];
588
            targe = targetNodeParent;
589
        } else if (beforeDragDropContext.position === ThyTreeDropPosition.after) {
590
            after = targetNode;
591
            targe = targetNodeParent;
592
        } else {
593
            after = targetNode.children?.length > 0 ? targetNode.children[targetNode.children.length - 2] : null;
594
            targe = targetNode;
595
        }
596

597
        this.thyOnDragDrop.emit({
598
            dragNode: this.thyTreeService.getTreeNode(sourceNode.key),
599
            targetNode: targe,
600
            afterNode: after
601
        });
602

603
        this.cleanupDragArtifacts();
604
    }
605

606
    private getTargetPosition(target: HTMLElement, event: CdkDragMove) {
607
        const targetRect = target.getBoundingClientRect();
608
        const beforeOrAfterGap = targetRect.height * 0.3;
609
        // 将 Node 高度分为上中下三段,其中上下的 Gap 为 height 的 30%,通过判断鼠标位置在哪一段 gap 来计算对应的位置
610
        if (event.pointerPosition.y - targetRect.top < beforeOrAfterGap) {
611
            return ThyTreeDropPosition.before;
612
        } else if (event.pointerPosition.y >= targetRect.bottom - beforeOrAfterGap) {
613
            return ThyTreeDropPosition.after;
614
        } else {
615
            return ThyTreeDropPosition.in;
616
        }
617
    }
618

619
    private showDropPositionPlaceholder(targetElement: HTMLElement) {
620
        this.cleanupDropPositionPlaceholder();
621
        if (this.nodeDropTarget && targetElement) {
622
            targetElement.classList.add(`drop-position-${this.nodeDropTarget.position}`);
623
        }
624
    }
625

626
    private cleanupDropPositionPlaceholder() {
627
        this.document.querySelectorAll('.drop-position-before').forEach(element => element.classList.remove('drop-position-before'));
628
        this.document.querySelectorAll('.drop-position-after').forEach(element => element.classList.remove('drop-position-after'));
629
        this.document.querySelectorAll('.drop-position-in').forEach(element => element.classList.remove('drop-position-in'));
630
    }
631

632
    private cleanupDragArtifacts() {
633
        this.nodeDropTarget = null;
634
        this.cleanupDropPositionPlaceholder();
635
    }
636

637
    // region Public Functions
638
    selectTreeNode(node: ThyTreeNode) {
639
        if (node && !node.isDisabled) {
640
            this.selectionModel.select(node);
641
            this.thyTreeService.syncFlattenTreeNodes();
642
        }
643
    }
644

645
    getTreeNode(key: string) {
646
        return this.thyTreeService.getTreeNode(key);
647
    }
648

649
    getSelectedNode(): ThyTreeNode {
650
        return this.selectionModel ? this.selectionModel.selected[0] : null;
651
    }
652

653
    getSelectedNodes(): ThyTreeNode[] {
654
        return this.selectionModel ? this.selectionModel.selected : [];
655
    }
656

657
    getExpandedNodes(): ThyTreeNode[] {
658
        return this.thyTreeService.getExpandedNodes();
659
    }
660

661
    getCheckedNodes(): ThyTreeNode[] {
662
        return this.thyTreeService.getCheckedNodes();
663
    }
664

665
    addTreeNode(node: ThyTreeNodeData, parent?: ThyTreeNode, index = -1) {
666
        this.thyTreeService.addTreeNode(new ThyTreeNode(node, null, this.thyTreeService), parent, index);
667
        this.thyTreeService.syncFlattenTreeNodes();
668
    }
669

670
    deleteTreeNode(node: ThyTreeNode) {
671
        if (this.isSelected(node)) {
672
            this.selectionModel.toggle(node);
673
        }
674
        this.thyTreeService.deleteTreeNode(node);
675
        this.thyTreeService.syncFlattenTreeNodes();
676
    }
677

678
    expandAllNodes() {
679
        const nodes = this.treeNodes;
680
        nodes.forEach(n => n.setExpanded(true, true));
681
    }
682

683
    collapsedAllNodes() {
684
        const nodes = this.treeNodes;
685
        nodes.forEach(n => n.setExpanded(false, true));
686
    }
687
}
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