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

atinc / ngx-tethys / e62d3b10-1466-49c3-aabd-707148681fc8

14 Jun 2024 08:24AM UTC coverage: 90.422%. Remained the same
e62d3b10-1466-49c3-aabd-707148681fc8

push

circleci

minlovehua
feat: use the ngx-tethys/util's coerceBooleanProperty instead of booleanAttribute #INFR-12648

5467 of 6692 branches covered (81.69%)

Branch coverage included in aggregate %.

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

183 existing lines in 46 files now uncovered.

13216 of 13970 relevant lines covered (94.6%)

985.91 hits per line

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

87.61
/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
    AfterViewInit,
7
    ChangeDetectionStrategy,
8
    ChangeDetectorRef,
9
    Component,
10
    ContentChild,
11
    EventEmitter,
12
    forwardRef,
13
    HostBinding,
14
    Inject,
15
    Input,
16
    numberAttribute,
17
    OnChanges,
18
    OnDestroy,
19
    OnInit,
1✔
20
    Output,
21
    QueryList,
22
    SimpleChanges,
23
    TemplateRef,
1✔
24
    ViewChild,
25
    ViewChildren,
26
    ViewEncapsulation
27
} from '@angular/core';
28
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
29
import { THY_TREE_ABSTRACT_TOKEN } from './tree-abstract';
30
import { ThyTreeNode } from './tree-node.class';
31
import {
1✔
32
    ThyTreeBeforeDragDropContext,
33
    ThyTreeBeforeDragStartContext,
23✔
34
    ThyClickBehavior,
35
    ThyTreeDragDropEvent,
36
    ThyTreeDropPosition,
50✔
37
    ThyTreeEmitEvent,
38
    ThyTreeIcons,
39
    ThyTreeNodeCheckState,
2,340✔
40
    ThyTreeNodeData
41
} from './tree.class';
42
import { ThyTreeService } from './tree.service';
41✔
43
import { ThyTreeNodeComponent } from './tree-node.component';
2✔
44
import { NgIf, NgFor, DOCUMENT } from '@angular/common';
45
import { CdkDrag, CdkDragDrop, CdkDragEnd, CdkDragMove, CdkDragStart, CdkDropList } from '@angular/cdk/drag-drop';
46
import { auditTime, filter, startWith, takeUntil } from 'rxjs/operators';
47
import { Subject } from 'rxjs';
40✔
48
import { ThyTreeNodeDraggablePipe } from './tree.pipe';
40✔
49

39✔
50
type ThyTreeSize = 'sm' | 'default';
51

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

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

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

64
/**
65
 * 树形控件组件
114✔
66
 * @name thy-tree
67
 */
68
@Component({
6✔
69
    selector: 'thy-tree',
1✔
70
    templateUrl: './tree.component.html',
71
    encapsulation: ViewEncapsulation.None,
5✔
72
    changeDetection: ChangeDetectionStrategy.OnPush,
73
    providers: [
74
        {
1,050✔
75
            provide: NG_VALUE_ACCESSOR,
76
            useExisting: forwardRef(() => ThyTree),
77
            multi: true
89✔
78
        },
75✔
79
        {
80
            provide: THY_TREE_ABSTRACT_TOKEN,
81
            useExisting: forwardRef(() => ThyTree)
82
        },
1,034✔
83
        ThyTreeService
84
    ],
85
    standalone: true,
×
UNCOV
86
    imports: [
×
87
        NgIf,
88
        CdkDrag,
89
        CdkDropList,
UNCOV
90
        CdkVirtualScrollViewport,
×
91
        CdkFixedSizeVirtualScroll,
92
        CdkVirtualForOf,
93
        ThyTreeNodeComponent,
89✔
94
        NgFor,
89✔
95
        ThyTreeNodeDraggablePipe
89✔
96
    ]
89✔
97
})
89✔
98
export class ThyTree implements ControlValueAccessor, OnInit, OnChanges, AfterViewInit, OnDestroy {
89✔
99
    private _templateRef: TemplateRef<any>;
89✔
100

89✔
101
    private _emptyChildrenTemplateRef: TemplateRef<any>;
102

89✔
103
    private _draggable = false;
89✔
104

89✔
105
    private _expandedKeys: (string | number)[];
5!
106

107
    private _selectedKeys: (string | number)[];
89✔
108

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

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

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

89✔
115
    private destroy$ = new Subject();
89✔
116

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

89✔
120
    private nodeDragMoved = new Subject<CdkDragMove>();
89✔
121

89✔
122
    // private startDragNodeExpanded: boolean;
89✔
123

89✔
124
    private startDragNodeClone: ThyTreeNode;
89✔
125

89✔
126
    // Node 拖动经过目标时临时记录目标id以及相对应目标的位置
127
    private nodeDropTarget: {
128
        position?: ThyTreeDropPosition;
54✔
129
        key?: number | string;
54✔
130
    };
54✔
131

54✔
132
    private dropEnterPredicate?: (context: { source: ThyTreeNode; target: ThyTreeNode; dropPosition: ThyTreeDropPosition }) => boolean =
54✔
133
        context => {
54✔
134
            return (
1,218✔
135
                this.isShowExpand(context.target) || (!this.isShowExpand(context.target) && context.dropPosition !== ThyTreeDropPosition.in)
1,218✔
136
            );
137
        };
138

139
    public _selectionModel: SelectionModel<ThyTreeNode>;
54✔
140

141
    public get treeNodes() {
142
        return this.thyTreeService.treeNodes;
74✔
143
    }
74✔
144

730!
145
    public flattenTreeNodes: ThyTreeNode[] = [];
146

730✔
147
    /**
148
     * 虚拟化滚动的视口
149
     */
150
    @Output() @ViewChild('viewport', { static: false }) viewport: CdkVirtualScrollViewport;
54✔
151

152
    /**
153
     * TreeNode 展现所需的数据
154
     * @type ThyTreeNodeData[]
5✔
155
     */
156
    @Input() thyNodes: ThyTreeNodeData[];
5✔
157

158
    /**
159
     * 设置 TreeNode 是否支持展开
160
     * @type boolean | Function
63✔
161
     */
2✔
162
    @Input() thyShowExpand: boolean | ((_: ThyTreeNodeData) => boolean) = true;
163

63✔
164
    /**
1✔
165
     * 设置是否支持多选节点
166
     */
63✔
167
    @HostBinding(`class.thy-multiple-selection-list`)
1✔
168
    @Input({ transform: coerceBooleanProperty })
169
    thyMultiple = false;
63✔
170

1✔
171
    /**
1✔
172
     * 设置 TreeNode 是否支持拖拽排序
173
     * @default false
174
     */
175
    @HostBinding('class.thy-tree-draggable')
6!
176
    @Input({ transform: coerceBooleanProperty })
177
    set thyDraggable(value: boolean) {
6✔
178
        this._draggable = value;
6✔
179
    }
180

×
UNCOV
181
    get thyDraggable() {
×
182
        return this._draggable;
183
    }
184

185
    /**
1,111✔
186
     * 设置 TreeNode 是否支持 Checkbox 选择
57✔
187
     * @default false
57✔
188
     */
57✔
189
    @Input({ transform: coerceBooleanProperty }) thyCheckable: boolean;
57✔
190

57✔
191
    /**
192
     * 点击节点的行为,`default` 为选中当前节点,`selectCheckbox` 为选中节点的 Checkbox, `thyCheckable` 为 true 时生效。
193
     */
54!
194
    @Input() thyClickBehavior: ThyClickBehavior = 'default';
54✔
195

54✔
196
    /**
197
     * 设置 check 状态的计算策略
198
     */
199
    @Input() set thyCheckStateResolve(resolve: (node: ThyTreeNode) => ThyTreeNodeCheckState) {
200
        if (resolve) {
54!
201
            this.thyTreeService.setCheckStateResolve(resolve);
54✔
202
        }
203
    }
204

205
    /**
54✔
206
     * 设置 TreeNode 是否支持异步加载
207
     */
208
    @Input({ transform: coerceBooleanProperty }) thyAsync = false;
111✔
209

40✔
210
    private _thyType: ThyTreeType = 'default';
40!
211

40✔
212
    /**
213
     * 设置不同展示类型的 Tree,`default` 为小箭头展示, `especial` 为 加减号图标展示
214
     * @type ThyTreeType
215
     * @default default
216
     */
1,036✔
217
    @Input()
218
    set thyType(type: ThyTreeType) {
219
        this._thyType = type;
1!
220
        if (type === 'especial') {
1✔
221
            this.thyIcons = { expand: 'minus-square', collapse: 'plus-square' };
222
        }
223
    }
224

1,034!
225
    get thyType() {
226
        return this._thyType;
227
    }
1,044✔
228

20✔
229
    /**
230
     * 设置不同 Tree 展开折叠的图标,`expand` 为展开状态的图标,`collapse` 为折叠状态的图标
231
     * @type { expand: string, collapse: string }
1,024✔
232
     */
233
    @Input() thyIcons: ThyTreeIcons = {};
234

235
    private _thySize: ThyTreeSize = 'default';
2✔
236
    /**
1✔
237
     * 支持 `sm` | `default` 两种大小,默认值为 `default`
1✔
238
     * @type ThyTreeSize
239
     * @default default
240
     */
241
    @Input()
1✔
242
    set thySize(size: ThyTreeSize) {
243
        this._thySize = size;
244
        if (this._thySize) {
1✔
245
            this._thyItemSize = treeItemSizeMap[this._thySize];
246
        } else {
247
            this._thyItemSize = treeItemSizeMap.default;
5✔
248
        }
5✔
249
    }
5✔
250

2✔
251
    get thySize() {
252
        return this._thySize;
253
    }
254

5✔
255
    /**
256
     * 设置是否开启虚拟滚动
257
     */
258
    @HostBinding('class.thy-virtual-scrolling-tree')
5✔
259
    @Input({ transform: coerceBooleanProperty })
5!
260
    thyVirtualScroll = false;
×
UNCOV
261

×
262
    private _thyItemSize = 44;
263

5!
264
    /**
265
     * 开启虚拟滚动时,单行节点的高度,当`thySize`为`default`时,该参数才生效
266
     * @default 44
5!
267
     */
×
UNCOV
268
    @Input({ transform: numberAttribute })
×
269
    set thyItemSize(itemSize: number) {
270
        if (this.thySize !== 'default') {
271
            throw new Error('setting thySize and thyItemSize at the same time is not allowed');
5✔
272
        }
273
        this._thyItemSize = itemSize;
274
    }
275

276
    get thyItemSize() {
5!
277
        return this._thyItemSize;
5✔
278
    }
5!
279

280
    /**
281
     * 设置节点名称是否支持超出截取
282
     * @type boolean
283
     */
5✔
284
    @Input({ transform: coerceBooleanProperty }) thyTitleTruncate = true;
285

286
    /**
×
UNCOV
287
     * 已选中的 node 节点集合
×
288
     * @default []
289
     */
290
    @Input() thySelectedKeys: string[];
UNCOV
291

×
292
    /**
293
     * 设置缩进距离,缩进距离 = thyIndent * node.level
294
     * @type number
295
     */
5✔
296
    @Input({ transform: numberAttribute }) thyIndent = 25;
297

5✔
298
    /**
5✔
299
     * 拖拽之前的回调,函数返回 false 则阻止拖拽
5✔
300
     */
301
    @Input() thyBeforeDragStart: (context: ThyTreeBeforeDragStartContext) => boolean;
302

303
    /**
5!
UNCOV
304
     * 拖放到元素时回调,函数返回 false 则阻止拖放到当前元素
×
305
     */
306
    @Input() thyBeforeDragDrop: (context: ThyTreeBeforeDragDropContext) => boolean;
5!
307

×
UNCOV
308
    /**
×
309
     * 设置子 TreeNode 点击事件
310
     */
5✔
311
    @Output() thyOnClick: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
5✔
312

19✔
313
    /**
5✔
314
     * 设置 check 选择事件
5✔
315
     */
5✔
316
    @Output() thyOnCheckboxChange: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
317

318
    /**
319
     * 设置点击展开触发事件
320
     */
321
    @Output() thyOnExpandChange: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
322

5!
323
    /**
×
UNCOV
324
     * 设置 TreeNode 拖拽事件
×
325
     */
326
    @Output() thyOnDragDrop: EventEmitter<ThyTreeDragDropEvent> = new EventEmitter<ThyTreeDragDropEvent>();
5✔
327

5✔
328
    /**
329
     * 双击 TreeNode 事件
1✔
330
     */
1✔
331
    @Output() thyDblClick: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
1✔
332

333
    /**
1!
334
     * 设置 TreeNode 的渲染模板
1✔
335
     */
1✔
336
    @ContentChild('treeNodeTemplate', { static: true })
337
    set templateRef(template: TemplateRef<any>) {
3✔
338
        if (template) {
3✔
339
            this._templateRef = template;
340
        }
5✔
341
    }
5✔
342

5✔
343
    get templateRef() {
5✔
344
        return this._templateRef;
1✔
345
    }
1✔
346

1✔
347
    /**
348
     * 设置子的空数据渲染模板
4✔
349
     */
1✔
350
    @ContentChild('emptyChildrenTemplate', { static: true }) emptyChildrenTemplate: TemplateRef<any>;
1✔
351
    set emptyChildrenTemplateRef(template: TemplateRef<any>) {
352
        if (template) {
353
            this._emptyChildrenTemplateRef = template;
3!
354
        }
3✔
355
    }
356

5✔
357
    get emptyChildrenTemplateRef() {
358
        return this._emptyChildrenTemplateRef;
359
    }
360

361
    @HostBinding('class.thy-tree')
5✔
362
    thyTreeClass = true;
363

364
    @HostBinding('class.thy-tree-dragging')
5✔
365
    dragging: boolean;
5✔
366

367
    @ViewChildren(CdkDrag) cdkDrags: QueryList<CdkDrag<ThyTreeNode>>;
5✔
368

1✔
369
    constructor(public thyTreeService: ThyTreeService, private cdr: ChangeDetectorRef, @Inject(DOCUMENT) private document: Document) {}
370

4✔
371
    ngOnInit(): void {
1✔
372
        this._initThyNodes();
373
        this._setTreeType();
374
        this._setTreeSize();
3✔
375
        this._instanceSelectionModel();
376
        this._selectTreeNodes(this.thySelectedKeys);
377

378
        this.thyTreeService.flattenNodes$.subscribe(flattenTreeNodes => {
5✔
379
            this.flattenTreeNodes = flattenTreeNodes;
5!
380
            this.cdr.markForCheck();
5✔
381
        });
382
    }
383

384
    ngAfterViewInit(): void {
10✔
385
        this.cdkDrags.changes
10✔
386
            .pipe(startWith(this.cdkDrags), takeUntil(this.destroy$))
10✔
387
            .subscribe((drags: QueryList<CdkDrag<ThyTreeNode>>) => {
388
                this.nodeDragsMap.clear();
389
                drags.forEach(drag => {
5✔
390
                    if (drag.data) {
5✔
391
                        // cdkDrag 变化时,缓存 Element 与 DragRef 的关系,方便 Drag Move 时查找
392
                        this.nodeDragsMap.set(drag.element.nativeElement, drag);
393
                    }
394
                });
44✔
395
            });
41✔
396

41✔
397
        this.nodeDragMoved
398
            .pipe(
399
                // auditTime(30),
400
                //  auditTime 可能会导致拖动结束后仍然执行 moved ,所以通过判断 dragging 状态来过滤无效 moved
17✔
401
                filter((event: CdkDragMove) => event.source._dragRef.isDragging()),
402
                takeUntil(this.destroy$)
403
            )
10✔
404
            .subscribe(event => {
405
                this.onDragMoved(event);
406
            });
2!
407
    }
408

409
    ngOnChanges(changes: SimpleChanges): void {
62✔
410
        if (changes.thyNodes && !changes.thyNodes.isFirstChange()) {
411
            this._initThyNodes();
412
        }
64✔
413
        if (changes.thyType && !changes.thyType.isFirstChange()) {
414
            this._setTreeType();
415
        }
11✔
416
        if (changes.thyMultiple && !changes.thyMultiple.isFirstChange()) {
417
            this._instanceSelectionModel();
3✔
418
        }
4✔
419

4✔
420
        if (changes.thySelectedKeys && !changes.thySelectedKeys.isFirstChange()) {
421
            this._selectedKeys = changes.thySelectedKeys.currentValue;
422
            this._selectTreeNodes(changes.thySelectedKeys.currentValue);
2✔
423
        }
1✔
424
    }
425

2✔
426
    renderView = () => {};
2✔
427

428
    eventTriggerChanged(event: ThyTreeEmitEvent): void {
429
        switch (event.eventName) {
1✔
430
            case 'expand':
2✔
431
                this.thyOnExpandChange.emit(event);
432
                break;
433

1✔
434
            case 'checkboxChange':
1✔
435
                this.thyOnCheckboxChange.emit(event);
436
                break;
437
        }
438
    }
89✔
439

89✔
440
    private _initThyNodes() {
441
        this._expandedKeys = this.getExpandedNodes().map(node => node.key);
1✔
442
        this._selectedKeys = this.getSelectedNodes().map(node => node.key);
443
        this.thyTreeService.initializeTreeNodes(this.thyNodes);
444
        this.flattenTreeNodes = this.thyTreeService.flattenTreeNodes;
445
        this._selectTreeNodes(this._selectedKeys);
446
        this.thyTreeService.expandTreeNodes(this._expandedKeys);
1✔
447
    }
448

449
    private _setTreeType() {
450
        if (this.thyType && treeTypeClassMap[this.thyType]) {
451
            treeTypeClassMap[this.thyType].forEach(className => {
452
                this.hostRenderer.addClass(className);
453
            });
454
        }
455
    }
456

457
    private _setTreeSize() {
458
        if (this.thySize) {
459
            this.hostRenderer.addClass(`thy-tree-${this.thySize}`);
460
        }
461
    }
462

463
    private _instanceSelectionModel() {
464
        this._selectionModel = new SelectionModel<any>(this.thyMultiple);
465
    }
466

467
    private _selectTreeNodes(keys: (string | number)[]) {
468
        (keys || []).forEach(key => {
469
            const node = this.thyTreeService.getTreeNode(key);
470
            if (node) {
471
                this.selectTreeNode(this.thyTreeService.getTreeNode(key));
472
            }
473
        });
474
    }
475

476
    isSelected(node: ThyTreeNode) {
477
        return this._selectionModel.isSelected(node);
478
    }
1✔
479

480
    toggleTreeNode(node: ThyTreeNode) {
481
        if (node && !node.isDisabled) {
482
            this._selectionModel.toggle(node);
483
        }
484
    }
485

486
    trackByFn(index: number, item: any) {
487
        return item.key || index;
36✔
488
    }
489

490
    isShowExpand(node: ThyTreeNode) {
491
        if (helpers.isFunction(this.thyShowExpand)) {
492
            return (this.thyShowExpand as Function)(node);
54✔
493
        } else {
494
            return this.thyShowExpand;
495
        }
496
    }
497

498
    writeValue(value: ThyTreeNodeData[]): void {
499
        if (value) {
500
            this.thyNodes = value;
501
            this._initThyNodes();
502
        }
503
    }
504

505
    registerOnChange(fn: any): void {
506
        this._onChange = fn;
507
    }
508

509
    registerOnTouched(fn: any): void {
510
        this._onTouched = fn;
511
    }
512

513
    onDragStarted(event: CdkDragStart<ThyTreeNode>) {
514
        this.dragging = true;
515
        this.startDragNodeClone = Object.assign({}, event.source.data);
516
        if (event.source.data.isExpanded) {
517
            event.source.data.setExpanded(false);
518
        }
519
    }
520

521
    emitDragMoved(event: CdkDragMove) {
522
        this.nodeDragMoved.next(event);
523
    }
524

525
    onDragMoved(event: CdkDragMove<ThyTreeNode>) {
526
        // 通过鼠标位置查找对应的目标 Item 元素
527
        let currentPointElement = this.document.elementFromPoint(event.pointerPosition.x, event.pointerPosition.y) as HTMLElement;
528
        if (!currentPointElement) {
529
            this.cleanupDragArtifacts();
530
            return;
531
        }
532
        let targetElement = currentPointElement.classList.contains('thy-tree-node')
533
            ? currentPointElement
534
            : (currentPointElement.closest('.thy-tree-node') as HTMLElement);
535
        if (!targetElement) {
536
            this.cleanupDragArtifacts();
537
            return;
538
        }
539
        // 缓存放置目标Id 并计算鼠标相对应的位置
540
        this.nodeDropTarget = {
541
            key: this.nodeDragsMap.get(targetElement)?.data.key,
542
            position: this.getTargetPosition(targetElement, event)
543
        };
544
        // 执行外部传入的 dropEnterPredicate 判断是否允许拖入目标项
545
        if (this.dropEnterPredicate) {
546
            const targetDragRef = this.nodeDragsMap.get(targetElement);
547
            if (
548
                this.dropEnterPredicate({
549
                    source: event.source.data,
550
                    target: targetDragRef.data,
551
                    dropPosition: this.nodeDropTarget.position
552
                })
553
            ) {
554
                this.showDropPositionPlaceholder(targetElement);
555
            } else {
556
                this.nodeDropTarget = null;
557
                this.cleanupDragArtifacts();
558
            }
559
        } else {
560
            this.showDropPositionPlaceholder(targetElement);
561
        }
562
    }
563

564
    onDragEnded(event: CdkDragEnd<ThyTreeNode>) {
565
        this.dragging = false;
566
        // 拖拽结束后恢复原始的展开状态
567
        event.source.data.setExpanded(this.startDragNodeClone.isExpanded);
568
        setTimeout(() => {
569
            this.startDragNodeClone = null;
570
        });
571
    }
572

573
    onListDropped(event: CdkDragDrop<ThyTreeNode[], ThyTreeNode[], ThyTreeNode>) {
574
        if (!this.nodeDropTarget) {
575
            return;
576
        }
577
        if (!this.isShowExpand(this.startDragNodeClone) && this.nodeDropTarget.position === ThyTreeDropPosition.in) {
578
            this.cleanupDragArtifacts();
579
            return;
580
        }
581

582
        const sourceNode = this.startDragNodeClone;
583
        const sourceNodeParent = sourceNode.parentNode;
584
        const targetDragRef = this.cdkDrags.find(item => item.data?.key === this.nodeDropTarget.key);
585
        const targetNode = targetDragRef?.data;
586
        const targetNodeParent = targetNode.parentNode;
587

588
        const beforeDragDropContext: ThyTreeBeforeDragDropContext = {
589
            previousItem: sourceNode,
590
            previousContainerItems: sourceNodeParent?.children,
591
            item: targetNode,
592
            containerItems: targetNodeParent?.children,
593
            position: this.nodeDropTarget.position
594
        };
595

596
        if (this.thyBeforeDragDrop && !this.thyBeforeDragDrop(beforeDragDropContext)) {
597
            this.cleanupDragArtifacts();
598
            return;
599
        }
600

601
        this.thyTreeService.deleteTreeNode(sourceNode);
602

603
        switch (this.nodeDropTarget.position) {
604
            case 'before':
605
                const beforeInsertIndex = (targetNodeParent?.children || this.treeNodes).indexOf(targetNode);
606
                this.thyTreeService.addTreeNode(sourceNode, targetNodeParent, beforeInsertIndex);
607
                break;
608
            case 'after':
609
                const afterInsertIndex = (targetNodeParent?.children || this.treeNodes).indexOf(targetNode) + 1;
610
                this.thyTreeService.addTreeNode(sourceNode, targetNodeParent, afterInsertIndex);
611
                break;
612
            case 'in':
613
                this.thyTreeService.addTreeNode(sourceNode, targetNode);
614
                break;
615
        }
616

617
        this.thyTreeService.syncFlattenTreeNodes();
618

619
        let after: ThyTreeNode = null;
620
        let targe: ThyTreeNode = null;
621
        if (beforeDragDropContext.position === ThyTreeDropPosition.before) {
622
            const targetContainerNodes = targetNodeParent?.children || this.treeNodes;
623
            after = targetContainerNodes[targetContainerNodes.indexOf(targetNode) - 2];
624
            targe = targetNodeParent;
625
        } else if (beforeDragDropContext.position === ThyTreeDropPosition.after) {
626
            after = targetNode;
627
            targe = targetNodeParent;
628
        } else {
629
            after = targetNode.children?.length > 0 ? targetNode.children[targetNode.children.length - 2] : null;
630
            targe = targetNode;
631
        }
632

633
        this.thyOnDragDrop.emit({
634
            dragNode: this.thyTreeService.getTreeNode(sourceNode.key),
635
            targetNode: targe,
636
            afterNode: after
637
        });
638

639
        this.cleanupDragArtifacts();
640
    }
641

642
    private getTargetPosition(target: HTMLElement, event: CdkDragMove) {
643
        const targetRect = target.getBoundingClientRect();
644
        const beforeOrAfterGap = targetRect.height * 0.3;
645
        // 将 Node 高度分为上中下三段,其中上下的 Gap 为 height 的 30%,通过判断鼠标位置在哪一段 gap 来计算对应的位置
646
        if (event.pointerPosition.y - targetRect.top < beforeOrAfterGap) {
647
            return ThyTreeDropPosition.before;
648
        } else if (event.pointerPosition.y >= targetRect.bottom - beforeOrAfterGap) {
649
            return ThyTreeDropPosition.after;
650
        } else {
651
            return ThyTreeDropPosition.in;
652
        }
653
    }
654

655
    private showDropPositionPlaceholder(targetElement: HTMLElement) {
656
        this.cleanupDropPositionPlaceholder();
657
        if (this.nodeDropTarget && targetElement) {
658
            targetElement.classList.add(`drop-position-${this.nodeDropTarget.position}`);
659
        }
660
    }
661

662
    private cleanupDropPositionPlaceholder() {
663
        this.document.querySelectorAll('.drop-position-before').forEach(element => element.classList.remove('drop-position-before'));
664
        this.document.querySelectorAll('.drop-position-after').forEach(element => element.classList.remove('drop-position-after'));
665
        this.document.querySelectorAll('.drop-position-in').forEach(element => element.classList.remove('drop-position-in'));
666
    }
667

668
    private cleanupDragArtifacts() {
669
        this.nodeDropTarget = null;
670
        this.cleanupDropPositionPlaceholder();
671
    }
672

673
    // region Public Functions
674

675
    selectTreeNode(node: ThyTreeNode) {
676
        if (node && !node.isDisabled) {
677
            this._selectionModel.select(node);
678
            this.thyTreeService.syncFlattenTreeNodes();
679
        }
680
    }
681

682
    getRootNodes(): ThyTreeNode[] {
683
        return this.treeNodes;
684
    }
685

686
    getTreeNode(key: string) {
687
        return this.thyTreeService.getTreeNode(key);
688
    }
689

690
    getSelectedNode(): ThyTreeNode {
691
        return this._selectionModel ? this._selectionModel.selected[0] : null;
692
    }
693

694
    getSelectedNodes(): ThyTreeNode[] {
695
        return this._selectionModel ? this._selectionModel.selected : [];
696
    }
697

698
    getExpandedNodes(): ThyTreeNode[] {
699
        return this.thyTreeService.getExpandedNodes();
700
    }
701

702
    getCheckedNodes(): ThyTreeNode[] {
703
        return this.thyTreeService.getCheckedNodes();
704
    }
705

706
    addTreeNode(node: ThyTreeNodeData, parent?: ThyTreeNode, index = -1) {
707
        this.thyTreeService.addTreeNode(new ThyTreeNode(node, null, this.thyTreeService), parent, index);
708
        this.thyTreeService.syncFlattenTreeNodes();
709
    }
710

711
    deleteTreeNode(node: ThyTreeNode) {
712
        if (this.isSelected(node)) {
713
            this._selectionModel.toggle(node);
714
        }
715
        this.thyTreeService.deleteTreeNode(node);
716
        this.thyTreeService.syncFlattenTreeNodes();
717
    }
718

719
    expandAllNodes() {
720
        const nodes = this.getRootNodes();
721
        nodes.forEach(n => n.setExpanded(true, true));
722
    }
723

724
    collapsedAllNodes() {
725
        const nodes = this.getRootNodes();
726
        nodes.forEach(n => n.setExpanded(false, true));
727
    }
728

729
    // endregion
730

731
    ngOnDestroy(): void {
732
        this.destroy$.next();
733
        this.destroy$.complete();
734
    }
735
}
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