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

atinc / ngx-tethys / cd64db52-e563-41a3-85f3-a0adb87ce135

30 Oct 2024 08:03AM UTC coverage: 90.402% (-0.04%) from 90.438%
cd64db52-e563-41a3-85f3-a0adb87ce135

push

circleci

web-flow
refactor: refactor constructor to the inject function (#3222)

5503 of 6730 branches covered (81.77%)

Branch coverage included in aggregate %.

422 of 429 new or added lines in 170 files covered. (98.37%)

344 existing lines in 81 files now uncovered.

13184 of 13941 relevant lines covered (94.57%)

997.19 hits per line

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

88.29
/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
    Input,
15
    numberAttribute,
16
    OnChanges,
17
    OnDestroy,
18
    OnInit,
19
    Output,
1✔
20
    QueryList,
21
    SimpleChanges,
22
    TemplateRef,
23
    ViewChild,
1✔
24
    ViewChildren,
25
    ViewEncapsulation,
26
    inject
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,
92✔
34
    ThyClickBehavior,
92✔
35
    ThyTreeDragDropEvent,
92✔
36
    ThyTreeDropPosition,
92✔
37
    ThyTreeEmitEvent,
92✔
38
    ThyTreeIcons,
92✔
39
    ThyTreeNodeCheckState,
92✔
40
    ThyTreeNodeData
92✔
41
} from './tree.class';
42
import { ThyTreeService } from './tree.service';
92✔
43
import { ThyTreeNodeComponent } from './tree-node.component';
92✔
44
import { DOCUMENT } from '@angular/common';
92✔
45
import { CdkDrag, CdkDragDrop, CdkDragEnd, CdkDragMove, CdkDragStart, CdkDropList } from '@angular/cdk/drag-drop';
5!
46
import { auditTime, filter, startWith, takeUntil } from 'rxjs/operators';
47
import { Subject } from 'rxjs';
92✔
48
import { ThyTreeNodeDraggablePipe } from './tree.pipe';
92✔
49

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

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

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

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

92✔
64
/**
92✔
65
 * 树形控件组件
92✔
66
 * @name thy-tree
92✔
67
 */
68
@Component({
69
    selector: 'thy-tree',
23✔
70
    templateUrl: './tree.component.html',
71
    encapsulation: ViewEncapsulation.None,
72
    changeDetection: ChangeDetectionStrategy.OnPush,
51✔
73
    providers: [
74
        {
75
            provide: NG_VALUE_ACCESSOR,
2,496✔
76
            useExisting: forwardRef(() => ThyTree),
77
            multi: true
78
        },
43✔
79
        {
2✔
80
            provide: THY_TREE_ABSTRACT_TOKEN,
81
            useExisting: forwardRef(() => ThyTree)
82
        },
83
        ThyTreeService
42✔
84
    ],
42✔
85
    standalone: true,
41✔
86
    imports: [
87
        CdkDrag,
88
        CdkDropList,
89
        CdkVirtualScrollViewport,
165✔
90
        CdkFixedSizeVirtualScroll,
91
        CdkVirtualForOf,
92
        ThyTreeNodeComponent,
44✔
93
        ThyTreeNodeDraggablePipe
44✔
94
    ]
43✔
95
})
96
export class ThyTree implements ControlValueAccessor, OnInit, OnChanges, AfterViewInit, OnDestroy {
97
    thyTreeService = inject(ThyTreeService);
1✔
98
    private cdr = inject(ChangeDetectorRef);
99
    private document = inject(DOCUMENT);
100

101
    private _templateRef: TemplateRef<any>;
115✔
102

103
    private _emptyChildrenTemplateRef: TemplateRef<any>;
104

5✔
105
    private _draggable = false;
1✔
106

107
    private _expandedKeys: (string | number)[];
4✔
108

109
    private _selectedKeys: (string | number)[];
110

1,124✔
111
    private hostRenderer = useHostRenderer();
112

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

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

117
    private destroy$ = new Subject<void>();
118

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

×
UNCOV
122
    private nodeDragMoved = new Subject<CdkDragMove>();
×
123

124
    // private startDragNodeExpanded: boolean;
125

UNCOV
126
    private startDragNodeClone: ThyTreeNode;
×
127

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

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

55✔
141
    public _selectionModel: SelectionModel<ThyTreeNode>;
142

143
    public get treeNodes() {
76✔
144
        return this.thyTreeService.treeNodes;
76✔
145
    }
788!
146

147
    public flattenTreeNodes: ThyTreeNode[] = [];
788✔
148

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

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

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

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

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

6!
183
    get thyDraggable() {
184
        return this._draggable;
6✔
185
    }
6✔
186

UNCOV
187
    /**
×
UNCOV
188
     * 设置 TreeNode 是否支持 Checkbox 选择
×
189
     * @default false
190
     */
191
    @Input({ transform: coerceBooleanProperty }) thyCheckable: boolean;
192

1,111✔
193
    /**
58✔
194
     * 点击节点的行为,`default` 为选中当前节点,`selectCheckbox` 为选中节点的 Checkbox, `thyCheckable` 为 true 时生效。
58✔
195
     */
58✔
196
    @Input() thyClickBehavior: ThyClickBehavior = 'default';
58✔
197

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

55!
207
    /**
55✔
208
     * 设置 TreeNode 是否支持异步加载
55✔
209
     */
210
    @Input({ transform: coerceBooleanProperty }) thyAsync = false;
211

212
    private _thyType: ThyTreeType = 'default';
213

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

227
    get thyType() {
228
        return this._thyType;
229
    }
1,112✔
230

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

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

253
    get thySize() {
254
        return this._thySize;
1✔
255
    }
256

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

2✔
264
    private _thyItemSize = 44;
265

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

278
    get thyItemSize() {
279
        return this._thyItemSize;
5!
UNCOV
280
    }
×
UNCOV
281

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

288
    /**
289
     * 已选中的 node 节点集合
5!
290
     * @default []
5✔
291
     */
5!
292
    @Input() thySelectedKeys: string[];
293

294
    /**
295
     * 展开指定的树节点
296
     */
5✔
297
    @Input() thyExpandedKeys: (string | number)[];
298

UNCOV
299
    /**
×
UNCOV
300
     * 是否展开所有树节点
×
301
     */
302
    @Input({ transform: coerceBooleanProperty }) thyExpandAll: boolean = false;
303

304
    /**
×
305
     * 设置缩进距离,缩进距离 = thyIndent * node.level
306
     * @type number
307
     */
308
    @Input({ transform: numberAttribute }) thyIndent = 25;
5✔
309

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

315
    /**
316
     * 拖放到元素时回调,函数返回 false 则阻止拖放到当前元素
5!
UNCOV
317
     */
×
318
    @Input() thyBeforeDragDrop: (context: ThyTreeBeforeDragDropContext) => boolean;
319

5!
UNCOV
320
    /**
×
321
     * 设置子 TreeNode 点击事件
×
322
     */
323
    @Output() thyOnClick: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
5✔
324

5✔
325
    /**
19✔
326
     * 设置 check 选择事件
5✔
327
     */
5✔
328
    @Output() thyOnCheckboxChange: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
5✔
329

330
    /**
331
     * 设置点击展开触发事件
332
     */
333
    @Output() thyOnExpandChange: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
334

335
    /**
5!
UNCOV
336
     * 设置 TreeNode 拖拽事件
×
UNCOV
337
     */
×
338
    @Output() thyOnDragDrop: EventEmitter<ThyTreeDragDropEvent> = new EventEmitter<ThyTreeDragDropEvent>();
339

5✔
340
    /**
5✔
341
     * 双击 TreeNode 事件
342
     */
1✔
343
    @Output() thyDblClick: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
1✔
344

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

5✔
355
    get templateRef() {
5✔
356
        return this._templateRef;
5✔
357
    }
1✔
358

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

369
    get emptyChildrenTemplateRef() {
5✔
370
        return this._emptyChildrenTemplateRef;
371
    }
372

373
    @HostBinding('class.thy-tree')
374
    thyTreeClass = true;
5✔
375

376
    @HostBinding('class.thy-tree-dragging')
377
    dragging: boolean;
5✔
378

5✔
379
    @ViewChildren(CdkDrag) cdkDrags: QueryList<CdkDrag<ThyTreeNode>>;
380

5✔
381
    ngOnInit(): void {
1✔
382
        this._initThyNodes();
383
        this._setTreeType();
4✔
384
        this._setTreeSize();
1✔
385
        this._instanceSelectionModel();
386
        this._selectTreeNodes(this.thySelectedKeys);
387

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

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

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

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

430
        if (changes.thySelectedKeys && !changes.thySelectedKeys.isFirstChange()) {
3✔
431
            this._selectedKeys = changes.thySelectedKeys.currentValue;
4✔
432
            this._selectTreeNodes(changes.thySelectedKeys.currentValue);
4✔
433
        }
434

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

1✔
443
    renderView = () => {};
2✔
444

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

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

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

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

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

484
    private _setTreeSize() {
485
        if (this.thySize) {
486
            this.hostRenderer.addClass(`thy-tree-${this.thySize}`);
487
        }
488
    }
1✔
489

490
    private _instanceSelectionModel() {
491
        this._selectionModel = new SelectionModel<any>(this.thyMultiple);
492
    }
493

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

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

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

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

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

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

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

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

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

548
    emitDragMoved(event: CdkDragMove) {
549
        this.nodeDragMoved.next(event);
550
    }
551

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

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

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

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

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

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

628
        this.thyTreeService.deleteTreeNode(sourceNode);
629

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

644
        this.thyTreeService.syncFlattenTreeNodes();
645

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

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

666
        this.cleanupDragArtifacts();
667
    }
668

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

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

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

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

700
    // region Public Functions
701

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

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

713
    getTreeNode(key: string) {
714
        return this.thyTreeService.getTreeNode(key);
715
    }
716

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

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

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

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

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

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

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

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

756
    // endregion
757

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