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

atinc / ngx-tethys / #102

26 May 2026 08:11AM UTC coverage: 91.111% (+0.7%) from 90.407%
#102

push

web-flow
build: bump docgeni to 2.8.0-next.5 (#3809)

4571 of 5491 branches covered (83.25%)

Branch coverage included in aggregate %.

13141 of 13949 relevant lines covered (94.21%)

966.75 hits per line

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

88.93
/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,
22
    afterNextRender,
23
    output,
24
    DOCUMENT
25
} from '@angular/core';
26
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
27
import { THY_TREE_ABSTRACT_TOKEN } from './tree-abstract';
28
import {
29
    ThyTreeNode,
30
    ThyTreeBeforeDragDropContext,
31
    ThyTreeBeforeDragStartContext,
32
    ThyClickBehavior,
33
    ThyTreeDragDropEvent,
34
    ThyTreeDropPosition,
35
    ThyTreeEmitEvent,
36
    ThyTreeIcons,
37
    ThyTreeNodeCheckState,
38
    ThyTreeNodeData
39
} from './tree.class';
40
import { ThyTreeService } from './tree.service';
41
import { ThyTreeNodeComponent } from './tree-node.component';
42

43
import { CdkDrag, CdkDragDrop, CdkDragEnd, CdkDragMove, CdkDragStart, CdkDropList } from '@angular/cdk/drag-drop';
44
import { filter } from 'rxjs/operators';
45
import { Subject } from 'rxjs';
46
import { ThyTreeNodeDraggablePipe } from './tree.pipe';
47
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
48

49
type ThyTreeSize = 'sm' | 'default';
50

51
type ThyTreeType = 'default' | 'especial';
52

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

1✔
58
const treeItemSizeMap = {
59
    default: 44,
60
    sm: 42
61
};
62

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

106
    private expandedKeys!: (string | number)[];
107

108
    private selectedKeys!: (string | number)[];
109

92✔
110
    private hostRenderer = useHostRenderer();
111

92✔
112
    private _onTouched: () => void = () => {};
113

92✔
114
    private _onChange: (value: any) => void = (_: any) => {};
115

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

92✔
119
    private nodeDragMoved = new Subject<CdkDragMove>();
120

121
    private startDragNodeClone: ThyTreeNode | null = null;
122

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

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

136
    public selectionModel!: SelectionModel<ThyTreeNode>;
137

138
    public get treeNodes() {
23✔
139
        return this.thyTreeService.treeNodes;
140
    }
141

92✔
142
    public readonly flattenTreeNodes = computed(() => this.thyTreeService.flattenTreeNodes());
143

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

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

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

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

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

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

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

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

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

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

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

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

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

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

92✔
237
    public readonly itemSize = computed(() => {
56✔
238
        const itemSize = this.thyItemSize();
56✔
239
        const size = this.thySize();
56✔
240
        if (size === 'default') {
14!
241
            return itemSize || treeItemSizeMap.default;
42✔
242
        } else if (size) {
41!
243
            return treeItemSizeMap[size] || treeItemSizeMap.default;
244
        } else {
1✔
245
            return treeItemSizeMap.default;
246
        }
247
    });
248

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

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

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

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

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

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

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

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

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

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

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

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

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

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

92✔
322
    dragging = signal(false);
323

92✔
324
    readonly cdkDrags = viewChildren(CdkDrag);
325

326
    constructor() {
92✔
327
        effect(() => {
57✔
328
            const resolve = this.thyCheckStateResolve();
57✔
329
            if (resolve) {
2✔
330
                this.thyTreeService.setCheckStateResolve(resolve);
331
            }
332
        });
333

92✔
334
        effect(() => {
60✔
335
            this.initThyNodes();
336
        });
337

92✔
338
        effect(() => {
56✔
339
            this.setTreeSize();
340
        });
341

92✔
342
        effect(() => {
56✔
343
            this.setTreeType();
344
        });
345

92✔
346
        effect(() => {
56✔
347
            this.instanceSelectionModel();
348
        });
349

92✔
350
        effect(() => {
56✔
351
            this.selectTreeNodes(this.thySelectedKeys()!);
352
        });
353

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

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

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

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

391
    private initThyNodes() {
1,113✔
392
        this.expandedKeys = this.getExpandedNodes().map(node => node.key!);
60✔
393
        this.selectedKeys = this.getSelectedNodes().map(node => node.key!);
60✔
394
        this.thyTreeService.initializeTreeNodes(this.thyNodes()!);
60✔
395
        this.selectTreeNodes(this.selectedKeys);
60✔
396
        this.handleExpandedKeys();
397
    }
398

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

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

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

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

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

440
    isSelected(node: ThyTreeNode) {
1,136✔
441
        return this.selectionModel.isSelected(node);
442
    }
443

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

450
    trackByFn(index: number, item: any) {
1,505!
451
        return item.key || index;
452
    }
453

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

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

469
    registerOnChange(fn: any): void {
1✔
470
        this._onChange = fn;
471
    }
472

473
    registerOnTouched(fn: any): void {
1✔
474
        this._onTouched = fn;
475
    }
476

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

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

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

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

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

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

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

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

5✔
566
        this.thyTreeService.deleteTreeNode(sourceNode);
567

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

5✔
584
        this.thyTreeService.syncFlattenTreeNodes();
5✔
585

5✔
586
        let after: ThyTreeNode | undefined = undefined;
1✔
587
        let targe: ThyTreeNode | undefined = undefined;
1✔
588
        if (beforeDragDropContext.position === ThyTreeDropPosition.before) {
1✔
589
            const targetContainerNodes = targetNodeParent?.children || this.treeNodes;
4✔
590
            after = targetContainerNodes[targetContainerNodes.indexOf(targetNode) - 2];
1✔
591
            targe = targetNodeParent;
1✔
592
        } else if (beforeDragDropContext.position === ThyTreeDropPosition.after) {
593
            after = targetNode;
3!
594
            targe = targetNodeParent;
3✔
595
        } else {
596
            after = targetNode.children?.length > 0 ? targetNode.children[targetNode.children.length - 2] : null;
597
            targe = targetNode;
5✔
598
        }
599

600
        this.thyOnDragDrop.emit({
601
            dragNode: this.thyTreeService.getTreeNode(sourceNode.key!),
602
            targetNode: targe,
603
            afterNode: after
5✔
604
        });
605

606
        this.cleanupDragArtifacts();
607
    }
5✔
608

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

5✔
622
    private showDropPositionPlaceholder(targetElement: HTMLElement) {
5✔
623
        this.cleanupDropPositionPlaceholder();
624
        if (this.nodeDropTarget && targetElement) {
625
            targetElement.classList.add(`drop-position-${this.nodeDropTarget.position}`);
626
        }
627
    }
10✔
628

10✔
629
    private cleanupDropPositionPlaceholder() {
10✔
630
        this.document.querySelectorAll('.drop-position-before').forEach(element => element.classList.remove('drop-position-before'));
631
        this.document.querySelectorAll('.drop-position-after').forEach(element => element.classList.remove('drop-position-after'));
632
        this.document.querySelectorAll('.drop-position-in').forEach(element => element.classList.remove('drop-position-in'));
633
    }
5✔
634

5✔
635
    private cleanupDragArtifacts() {
636
        this.nodeDropTarget = null;
637
        this.cleanupDropPositionPlaceholder();
638
    }
639

48✔
640
    // region Public Functions
45✔
641
    selectTreeNode(node: ThyTreeNode) {
45✔
642
        if (node && !node.isDisabled) {
643
            this.selectionModel.select(node);
644
            this.thyTreeService.syncFlattenTreeNodes();
645
        }
646
    }
10✔
647

648
    getTreeNode(key: string) {
649
        return this.thyTreeService.getTreeNode(key);
650
    }
2!
651

652
    getSelectedNode(): ThyTreeNode | null {
653
        return this.selectionModel ? this.selectionModel.selected[0] : null;
654
    }
65✔
655

656
    getSelectedNodes(): ThyTreeNode[] {
657
        return this.selectionModel ? this.selectionModel.selected : [];
658
    }
67✔
659

660
    getExpandedNodes(): ThyTreeNode[] {
661
        return this.thyTreeService.getExpandedNodes();
662
    }
11✔
663

664
    getCheckedNodes(): ThyTreeNode[] {
665
        return this.thyTreeService.getCheckedNodes();
3✔
666
    }
4✔
667

4✔
668
    addTreeNode(node: ThyTreeNodeData, parent?: ThyTreeNode, index = -1) {
669
        this.thyTreeService.addTreeNode(new ThyTreeNode(node, undefined, this.thyTreeService), parent, index);
670
        this.thyTreeService.syncFlattenTreeNodes();
671
    }
2✔
672

1✔
673
    deleteTreeNode(node: ThyTreeNode) {
674
        if (this.isSelected(node)) {
2✔
675
            this.selectionModel.toggle(node);
2✔
676
        }
677
        this.thyTreeService.deleteTreeNode(node);
678
        this.thyTreeService.syncFlattenTreeNodes();
679
    }
1✔
680

2✔
681
    expandAllNodes() {
682
        const nodes = this.treeNodes;
683
        nodes.forEach(n => n.setExpanded(true, true));
684
    }
1✔
685

2✔
686
    collapsedAllNodes() {
687
        const nodes = this.treeNodes;
688
        nodes.forEach(n => n.setExpanded(false, true));
689
    }
690
}
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