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

atinc / ngx-tethys / 5fa9630c-19a1-4ee7-af3d-6a0c3535952a

08 Oct 2024 08:24AM UTC coverage: 90.438% (+0.007%) from 90.431%
5fa9630c-19a1-4ee7-af3d-6a0c3535952a

push

circleci

minlovehua
refactor: refactor all control-flow directives to new control-flow #TINFR-381

5511 of 6738 branches covered (81.79%)

Branch coverage included in aggregate %.

98 of 104 new or added lines in 58 files covered. (94.23%)

113 existing lines in 17 files now uncovered.

13253 of 14010 relevant lines covered (94.6%)

991.73 hits per line

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

88.32
/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,
51✔
37
    ThyTreeEmitEvent,
38
    ThyTreeIcons,
39
    ThyTreeNodeCheckState,
2,496✔
40
    ThyTreeNodeData
41
} from './tree.class';
42
import { ThyTreeService } from './tree.service';
43✔
43
import { ThyTreeNodeComponent } from './tree-node.component';
2✔
44
import { 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';
42✔
48
import { ThyTreeNodeDraggablePipe } from './tree.pipe';
42✔
49

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

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

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

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

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

92✔
99
    private _emptyChildrenTemplateRef: TemplateRef<any>;
92✔
100

92✔
101
    private _draggable = false;
102

92✔
103
    private _expandedKeys: (string | number)[];
92✔
104

92✔
105
    private _selectedKeys: (string | number)[];
5!
106

107
    private hostRenderer = useHostRenderer();
92✔
108

92✔
109
    private _onTouched: () => void = () => {};
92✔
110

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

92✔
113
    private destroy$ = new Subject<void>();
92✔
114

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

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

92✔
120
    // private startDragNodeExpanded: boolean;
92✔
121

92✔
122
    private startDragNodeClone: ThyTreeNode;
92✔
123

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

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

1,254✔
137
    public _selectionModel: SelectionModel<ThyTreeNode>;
138

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

143
    public flattenTreeNodes: ThyTreeNode[] = [];
76✔
144

76✔
145
    /**
788!
146
     * 虚拟化滚动的视口
147
     */
788✔
148
    @Output() @ViewChild('viewport', { static: false }) viewport: CdkVirtualScrollViewport;
149

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

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

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

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

1✔
179
    get thyDraggable() {
180
        return this._draggable;
181
    }
182

6!
183
    /**
184
     * 设置 TreeNode 是否支持 Checkbox 选择
6✔
185
     * @default false
6✔
186
     */
UNCOV
187
    @Input({ transform: coerceBooleanProperty }) thyCheckable: boolean;
×
UNCOV
188

×
189
    /**
190
     * 点击节点的行为,`default` 为选中当前节点,`selectCheckbox` 为选中节点的 Checkbox, `thyCheckable` 为 true 时生效。
191
     */
192
    @Input() thyClickBehavior: ThyClickBehavior = 'default';
1,111✔
193

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

203
    /**
60✔
204
     * 设置 TreeNode 是否支持异步加载
205
     */
206
    @Input({ transform: coerceBooleanProperty }) thyAsync = false;
55!
207

55✔
208
    private _thyType: ThyTreeType = 'default';
55✔
209

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

42✔
223
    get thyType() {
42!
224
        return this._thyType;
42✔
225
    }
226

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

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

2✔
249
    get thySize() {
1✔
250
        return this._thySize;
1✔
251
    }
252

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

260
    private _thyItemSize = 44;
5✔
261

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

×
UNCOV
274
    get thyItemSize() {
×
275
        return this._thyItemSize;
276
    }
5!
277

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

284
    /**
5✔
285
     * 已选中的 node 节点集合
286
     * @default []
287
     */
288
    @Input() thySelectedKeys: string[];
289

5!
290
    /**
5✔
291
     * 展开指定的树节点
5!
292
     */
293
    @Input() thyExpandedKeys: (string | number)[];
294

295
    /**
296
     * 是否展开所有树节点
5✔
297
     */
298
    @Input({ transform: coerceBooleanProperty }) thyExpandAll: boolean = false;
UNCOV
299

×
UNCOV
300
    /**
×
301
     * 设置缩进距离,缩进距离 = thyIndent * node.level
302
     * @type number
303
     */
UNCOV
304
    @Input({ transform: numberAttribute }) thyIndent = 25;
×
305

306
    /**
307
     * 拖拽之前的回调,函数返回 false 则阻止拖拽
308
     */
5✔
309
    @Input() thyBeforeDragStart: (context: ThyTreeBeforeDragStartContext) => boolean;
310

5✔
311
    /**
5✔
312
     * 拖放到元素时回调,函数返回 false 则阻止拖放到当前元素
5✔
313
     */
314
    @Input() thyBeforeDragDrop: (context: ThyTreeBeforeDragDropContext) => boolean;
315

316
    /**
5!
UNCOV
317
     * 设置子 TreeNode 点击事件
×
318
     */
319
    @Output() thyOnClick: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
5!
UNCOV
320

×
UNCOV
321
    /**
×
322
     * 设置 check 选择事件
323
     */
5✔
324
    @Output() thyOnCheckboxChange: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
5✔
325

19✔
326
    /**
5✔
327
     * 设置点击展开触发事件
5✔
328
     */
5✔
329
    @Output() thyOnExpandChange: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
330

331
    /**
332
     * 设置 TreeNode 拖拽事件
333
     */
334
    @Output() thyOnDragDrop: EventEmitter<ThyTreeDragDropEvent> = new EventEmitter<ThyTreeDragDropEvent>();
335

5!
UNCOV
336
    /**
×
UNCOV
337
     * 双击 TreeNode 事件
×
338
     */
339
    @Output() thyDblClick: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
5✔
340

5✔
341
    /**
342
     * 设置 TreeNode 的渲染模板
1✔
343
     */
1✔
344
    @ContentChild('treeNodeTemplate', { static: true })
1✔
345
    set templateRef(template: TemplateRef<any>) {
346
        if (template) {
1!
347
            this._templateRef = template;
1✔
348
        }
1✔
349
    }
350

3✔
351
    get templateRef() {
3✔
352
        return this._templateRef;
353
    }
5✔
354

5✔
355
    /**
5✔
356
     * 设置子的空数据渲染模板
5✔
357
     */
1✔
358
    @ContentChild('emptyChildrenTemplate', { static: true }) emptyChildrenTemplate: TemplateRef<any>;
1✔
359
    set emptyChildrenTemplateRef(template: TemplateRef<any>) {
1✔
360
        if (template) {
361
            this._emptyChildrenTemplateRef = template;
4✔
362
        }
1✔
363
    }
1✔
364

365
    get emptyChildrenTemplateRef() {
366
        return this._emptyChildrenTemplateRef;
3!
367
    }
3✔
368

369
    @HostBinding('class.thy-tree')
5✔
370
    thyTreeClass = true;
371

372
    @HostBinding('class.thy-tree-dragging')
373
    dragging: boolean;
374

5✔
375
    @ViewChildren(CdkDrag) cdkDrags: QueryList<CdkDrag<ThyTreeNode>>;
376

377
    constructor(
5✔
378
        public thyTreeService: ThyTreeService,
5✔
379
        private cdr: ChangeDetectorRef,
380
        @Inject(DOCUMENT) private document: Document
5✔
381
    ) {}
1✔
382

383
    ngOnInit(): void {
4✔
384
        this._initThyNodes();
1✔
385
        this._setTreeType();
386
        this._setTreeSize();
387
        this._instanceSelectionModel();
3✔
388
        this._selectTreeNodes(this.thySelectedKeys);
389

390
        this.thyTreeService.flattenNodes$.subscribe(flattenTreeNodes => {
391
            this.flattenTreeNodes = flattenTreeNodes;
5✔
392
            this.cdr.markForCheck();
5!
393
        });
5✔
394
    }
395

396
    ngAfterViewInit(): void {
397
        this.cdkDrags.changes
10✔
398
            .pipe(startWith(this.cdkDrags), takeUntil(this.destroy$))
10✔
399
            .subscribe((drags: QueryList<CdkDrag<ThyTreeNode>>) => {
10✔
400
                this.nodeDragsMap.clear();
401
                drags.forEach(drag => {
402
                    if (drag.data) {
5✔
403
                        // cdkDrag 变化时,缓存 Element 与 DragRef 的关系,方便 Drag Move 时查找
5✔
404
                        this.nodeDragsMap.set(drag.element.nativeElement, drag);
405
                    }
406
                });
407
            });
46✔
408

43✔
409
        this.nodeDragMoved
43✔
410
            .pipe(
411
                // auditTime(30),
412
                //  auditTime 可能会导致拖动结束后仍然执行 moved ,所以通过判断 dragging 状态来过滤无效 moved
413
                filter((event: CdkDragMove) => event.source._dragRef.isDragging()),
17✔
414
                takeUntil(this.destroy$)
415
            )
416
            .subscribe(event => {
10✔
417
                this.onDragMoved(event);
418
            });
419
    }
2!
420

421
    ngOnChanges(changes: SimpleChanges): void {
422
        if (changes.thyNodes && !changes.thyNodes.isFirstChange()) {
63✔
423
            this._initThyNodes();
424
        }
425
        if (changes.thyType && !changes.thyType.isFirstChange()) {
65✔
426
            this._setTreeType();
427
        }
428
        if (changes.thyMultiple && !changes.thyMultiple.isFirstChange()) {
11✔
429
            this._instanceSelectionModel();
430
        }
3✔
431

4✔
432
        if (changes.thySelectedKeys && !changes.thySelectedKeys.isFirstChange()) {
4✔
433
            this._selectedKeys = changes.thySelectedKeys.currentValue;
434
            this._selectTreeNodes(changes.thySelectedKeys.currentValue);
435
        }
2✔
436

1✔
437
        if (changes.thyExpandedKeys && !changes.thyExpandedKeys.isFirstChange()) {
438
            this._handleExpandedKeys();
2✔
439
        }
2✔
440
        if (changes.thyExpandAll && !changes.thyExpandAll.isFirstChange()) {
441
            this._handleExpandedKeys();
442
        }
1✔
443
    }
2✔
444

445
    renderView = () => {};
446

1✔
447
    eventTriggerChanged(event: ThyTreeEmitEvent): void {
1✔
448
        switch (event.eventName) {
449
            case 'expand':
450
                this.thyOnExpandChange.emit(event);
451
                break;
92✔
452

92✔
453
            case 'checkboxChange':
454
                this.thyOnCheckboxChange.emit(event);
1✔
455
                break;
456
        }
457
    }
458

459
    private _initThyNodes() {
1✔
460
        this._expandedKeys = this.getExpandedNodes().map(node => node.key);
461
        this._selectedKeys = this.getSelectedNodes().map(node => node.key);
462
        this.thyTreeService.initializeTreeNodes(this.thyNodes);
463
        this.flattenTreeNodes = this.thyTreeService.flattenTreeNodes;
464
        this._selectTreeNodes(this._selectedKeys);
465
        this._handleExpandedKeys();
466
    }
467

468
    private _handleExpandedKeys() {
469
        if (this.thyExpandedKeys?.length) {
470
            this._expandedKeys = helpers.concatArray(
471
                this.thyExpandedKeys.filter(key => !this._expandedKeys.includes(key)),
472
                this._expandedKeys
473
            );
474
        }
475
        this.thyTreeService.expandTreeNodes(this.thyExpandAll || this._expandedKeys);
476
    }
477

478
    private _setTreeType() {
479
        if (this.thyType && treeTypeClassMap[this.thyType]) {
480
            treeTypeClassMap[this.thyType].forEach(className => {
481
                this.hostRenderer.addClass(className);
482
            });
483
        }
484
    }
485

486
    private _setTreeSize() {
487
        if (this.thySize) {
488
            this.hostRenderer.addClass(`thy-tree-${this.thySize}`);
489
        }
490
    }
491

492
    private _instanceSelectionModel() {
493
        this._selectionModel = new SelectionModel<any>(this.thyMultiple);
1✔
494
    }
495

496
    private _selectTreeNodes(keys: (string | number)[]) {
497
        (keys || []).forEach(key => {
498
            const node = this.thyTreeService.getTreeNode(key);
499
            if (node) {
500
                this.selectTreeNode(this.thyTreeService.getTreeNode(key));
501
            }
502
        });
38✔
503
    }
504

505
    isSelected(node: ThyTreeNode) {
506
        return this._selectionModel.isSelected(node);
507
    }
55✔
508

509
    toggleTreeNode(node: ThyTreeNode) {
510
        if (node && !node.isDisabled) {
511
            this._selectionModel.toggle(node);
512
        }
513
    }
514

515
    trackByFn(index: number, item: any) {
516
        return item.key || index;
517
    }
518

519
    isShowExpand(node: ThyTreeNode) {
520
        if (helpers.isFunction(this.thyShowExpand)) {
521
            return (this.thyShowExpand as Function)(node);
522
        } else {
523
            return this.thyShowExpand;
524
        }
525
    }
526

527
    writeValue(value: ThyTreeNodeData[]): void {
528
        if (value) {
529
            this.thyNodes = value;
530
            this._initThyNodes();
531
        }
532
    }
533

534
    registerOnChange(fn: any): void {
535
        this._onChange = fn;
536
    }
537

538
    registerOnTouched(fn: any): void {
539
        this._onTouched = fn;
540
    }
541

542
    onDragStarted(event: CdkDragStart<ThyTreeNode>) {
543
        this.dragging = true;
544
        this.startDragNodeClone = Object.assign({}, event.source.data);
545
        if (event.source.data.isExpanded) {
546
            event.source.data.setExpanded(false);
547
        }
548
    }
549

550
    emitDragMoved(event: CdkDragMove) {
551
        this.nodeDragMoved.next(event);
552
    }
553

554
    onDragMoved(event: CdkDragMove<ThyTreeNode>) {
555
        // 通过鼠标位置查找对应的目标 Item 元素
556
        let currentPointElement = this.document.elementFromPoint(event.pointerPosition.x, event.pointerPosition.y) as HTMLElement;
557
        if (!currentPointElement) {
558
            this.cleanupDragArtifacts();
559
            return;
560
        }
561
        let targetElement = currentPointElement.classList.contains('thy-tree-node')
562
            ? currentPointElement
563
            : (currentPointElement.closest('.thy-tree-node') as HTMLElement);
564
        if (!targetElement) {
565
            this.cleanupDragArtifacts();
566
            return;
567
        }
568
        // 缓存放置目标Id 并计算鼠标相对应的位置
569
        this.nodeDropTarget = {
570
            key: this.nodeDragsMap.get(targetElement)?.data.key,
571
            position: this.getTargetPosition(targetElement, event)
572
        };
573
        // 执行外部传入的 dropEnterPredicate 判断是否允许拖入目标项
574
        if (this.dropEnterPredicate) {
575
            const targetDragRef = this.nodeDragsMap.get(targetElement);
576
            if (
577
                this.dropEnterPredicate({
578
                    source: event.source.data,
579
                    target: targetDragRef.data,
580
                    dropPosition: this.nodeDropTarget.position
581
                })
582
            ) {
583
                this.showDropPositionPlaceholder(targetElement);
584
            } else {
585
                this.nodeDropTarget = null;
586
                this.cleanupDragArtifacts();
587
            }
588
        } else {
589
            this.showDropPositionPlaceholder(targetElement);
590
        }
591
    }
592

593
    onDragEnded(event: CdkDragEnd<ThyTreeNode>) {
594
        this.dragging = false;
595
        // 拖拽结束后恢复原始的展开状态
596
        event.source.data.setExpanded(this.startDragNodeClone.isExpanded);
597
        setTimeout(() => {
598
            this.startDragNodeClone = null;
599
        });
600
    }
601

602
    onListDropped(event: CdkDragDrop<ThyTreeNode[], ThyTreeNode[], ThyTreeNode>) {
603
        if (!this.nodeDropTarget) {
604
            return;
605
        }
606
        if (!this.isShowExpand(this.startDragNodeClone) && this.nodeDropTarget.position === ThyTreeDropPosition.in) {
607
            this.cleanupDragArtifacts();
608
            return;
609
        }
610

611
        const sourceNode = this.startDragNodeClone;
612
        const sourceNodeParent = sourceNode.parentNode;
613
        const targetDragRef = this.cdkDrags.find(item => item.data?.key === this.nodeDropTarget.key);
614
        const targetNode = targetDragRef?.data;
615
        const targetNodeParent = targetNode.parentNode;
616

617
        const beforeDragDropContext: ThyTreeBeforeDragDropContext = {
618
            previousItem: sourceNode,
619
            previousContainerItems: sourceNodeParent?.children,
620
            item: targetNode,
621
            containerItems: targetNodeParent?.children,
622
            position: this.nodeDropTarget.position
623
        };
624

625
        if (this.thyBeforeDragDrop && !this.thyBeforeDragDrop(beforeDragDropContext)) {
626
            this.cleanupDragArtifacts();
627
            return;
628
        }
629

630
        this.thyTreeService.deleteTreeNode(sourceNode);
631

632
        switch (this.nodeDropTarget.position) {
633
            case 'before':
634
                const beforeInsertIndex = (targetNodeParent?.children || this.treeNodes).indexOf(targetNode);
635
                this.thyTreeService.addTreeNode(sourceNode, targetNodeParent, beforeInsertIndex);
636
                break;
637
            case 'after':
638
                const afterInsertIndex = (targetNodeParent?.children || this.treeNodes).indexOf(targetNode) + 1;
639
                this.thyTreeService.addTreeNode(sourceNode, targetNodeParent, afterInsertIndex);
640
                break;
641
            case 'in':
642
                this.thyTreeService.addTreeNode(sourceNode, targetNode);
643
                break;
644
        }
645

646
        this.thyTreeService.syncFlattenTreeNodes();
647

648
        let after: ThyTreeNode = null;
649
        let targe: ThyTreeNode = null;
650
        if (beforeDragDropContext.position === ThyTreeDropPosition.before) {
651
            const targetContainerNodes = targetNodeParent?.children || this.treeNodes;
652
            after = targetContainerNodes[targetContainerNodes.indexOf(targetNode) - 2];
653
            targe = targetNodeParent;
654
        } else if (beforeDragDropContext.position === ThyTreeDropPosition.after) {
655
            after = targetNode;
656
            targe = targetNodeParent;
657
        } else {
658
            after = targetNode.children?.length > 0 ? targetNode.children[targetNode.children.length - 2] : null;
659
            targe = targetNode;
660
        }
661

662
        this.thyOnDragDrop.emit({
663
            dragNode: this.thyTreeService.getTreeNode(sourceNode.key),
664
            targetNode: targe,
665
            afterNode: after
666
        });
667

668
        this.cleanupDragArtifacts();
669
    }
670

671
    private getTargetPosition(target: HTMLElement, event: CdkDragMove) {
672
        const targetRect = target.getBoundingClientRect();
673
        const beforeOrAfterGap = targetRect.height * 0.3;
674
        // 将 Node 高度分为上中下三段,其中上下的 Gap 为 height 的 30%,通过判断鼠标位置在哪一段 gap 来计算对应的位置
675
        if (event.pointerPosition.y - targetRect.top < beforeOrAfterGap) {
676
            return ThyTreeDropPosition.before;
677
        } else if (event.pointerPosition.y >= targetRect.bottom - beforeOrAfterGap) {
678
            return ThyTreeDropPosition.after;
679
        } else {
680
            return ThyTreeDropPosition.in;
681
        }
682
    }
683

684
    private showDropPositionPlaceholder(targetElement: HTMLElement) {
685
        this.cleanupDropPositionPlaceholder();
686
        if (this.nodeDropTarget && targetElement) {
687
            targetElement.classList.add(`drop-position-${this.nodeDropTarget.position}`);
688
        }
689
    }
690

691
    private cleanupDropPositionPlaceholder() {
692
        this.document.querySelectorAll('.drop-position-before').forEach(element => element.classList.remove('drop-position-before'));
693
        this.document.querySelectorAll('.drop-position-after').forEach(element => element.classList.remove('drop-position-after'));
694
        this.document.querySelectorAll('.drop-position-in').forEach(element => element.classList.remove('drop-position-in'));
695
    }
696

697
    private cleanupDragArtifacts() {
698
        this.nodeDropTarget = null;
699
        this.cleanupDropPositionPlaceholder();
700
    }
701

702
    // region Public Functions
703

704
    selectTreeNode(node: ThyTreeNode) {
705
        if (node && !node.isDisabled) {
706
            this._selectionModel.select(node);
707
            this.thyTreeService.syncFlattenTreeNodes();
708
        }
709
    }
710

711
    getRootNodes(): ThyTreeNode[] {
712
        return this.treeNodes;
713
    }
714

715
    getTreeNode(key: string) {
716
        return this.thyTreeService.getTreeNode(key);
717
    }
718

719
    getSelectedNode(): ThyTreeNode {
720
        return this._selectionModel ? this._selectionModel.selected[0] : null;
721
    }
722

723
    getSelectedNodes(): ThyTreeNode[] {
724
        return this._selectionModel ? this._selectionModel.selected : [];
725
    }
726

727
    getExpandedNodes(): ThyTreeNode[] {
728
        return this.thyTreeService.getExpandedNodes();
729
    }
730

731
    getCheckedNodes(): ThyTreeNode[] {
732
        return this.thyTreeService.getCheckedNodes();
733
    }
734

735
    addTreeNode(node: ThyTreeNodeData, parent?: ThyTreeNode, index = -1) {
736
        this.thyTreeService.addTreeNode(new ThyTreeNode(node, null, this.thyTreeService), parent, index);
737
        this.thyTreeService.syncFlattenTreeNodes();
738
    }
739

740
    deleteTreeNode(node: ThyTreeNode) {
741
        if (this.isSelected(node)) {
742
            this._selectionModel.toggle(node);
743
        }
744
        this.thyTreeService.deleteTreeNode(node);
745
        this.thyTreeService.syncFlattenTreeNodes();
746
    }
747

748
    expandAllNodes() {
749
        const nodes = this.getRootNodes();
750
        nodes.forEach(n => n.setExpanded(true, true));
751
    }
752

753
    collapsedAllNodes() {
754
        const nodes = this.getRootNodes();
755
        nodes.forEach(n => n.setExpanded(false, true));
756
    }
757

758
    // endregion
759

760
    ngOnDestroy(): void {
761
        this.destroy$.next();
762
        this.destroy$.complete();
763
    }
764
}
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