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

atinc / ngx-tethys / #41

14 Jul 2025 08:03AM UTC coverage: 90.295% (+0.002%) from 90.293%
#41

push

web-flow
ci: update environment to env

5521 of 6794 branches covered (81.26%)

Branch coverage included in aggregate %.

13747 of 14545 relevant lines covered (94.51%)

903.28 hits per line

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

88.07
/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,
23✔
36
    ThyTreeNodeCheckState,
37
    ThyTreeNodeData
38
} from './tree.class';
92✔
39
import { ThyTreeService } from './tree.service';
92✔
40
import { ThyTreeNodeComponent } from './tree-node.component';
92✔
41
import { DOCUMENT } from '@angular/common';
92✔
42
import { CdkDrag, CdkDragDrop, CdkDragEnd, CdkDragMove, CdkDragStart, CdkDropList } from '@angular/cdk/drag-drop';
92✔
43
import { filter, startWith, takeUntil } from 'rxjs/operators';
92✔
44
import { Subject } from 'rxjs';
45
import { ThyTreeNodeDraggablePipe } from './tree.pipe';
92✔
46
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
92✔
47

92✔
48
type ThyTreeSize = 'sm' | 'default';
5!
49

50
type ThyTreeType = 'default' | 'especial';
92✔
51

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

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

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

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

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

57✔
109
    private hostRenderer = useHostRenderer();
57✔
110

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

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

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

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

92✔
120
    private startDragNodeClone: ThyTreeNode;
56✔
121

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

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

788✔
135
    public selectionModel: SelectionModel<ThyTreeNode>;
136

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

265
    /**
266
     * 是否展开所有树节点
267
     */
268
    readonly thyExpandAll = input(false, { transform: coerceBooleanProperty });
5✔
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

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

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

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

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

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

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

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

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

1✔
321
    dragging = signal(false);
1✔
322

323
    readonly cdkDrags = viewChildren(CdkDrag);
3✔
324

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

1✔
333
        effect(() => {
334
            this.initThyNodes();
4✔
335
        });
1✔
336

1✔
337
        effect(() => {
338
            this.setTreeSize();
339
        });
3!
340

3✔
341
        effect(() => {
342
            this.setTreeType();
5✔
343
        });
344

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

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

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

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

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

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

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

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

2✔
410
    private setTreeType() {
411
        const type = this.thyType();
412
        if (type && treeTypeClassMap[type]) {
1✔
413
            treeTypeClassMap[type].forEach(className => {
2✔
414
                this.hostRenderer.addClass(className);
415
            });
416
        }
1✔
417
    }
2✔
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 {
38✔
469
        this._onChange = fn;
470
    }
471

472
    registerOnTouched(fn: any): void {
473
        this._onTouched = fn;
55✔
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

© 2026 Coveralls, Inc