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

atinc / ngx-tethys / eb2d4535-96a3-4525-be0e-cfa40b1c8e2c

02 Jul 2024 09:59AM UTC coverage: 90.449% (+0.01%) from 90.437%
eb2d4535-96a3-4525-be0e-cfa40b1c8e2c

Pull #3110

circleci

helingkaih
feat(tree): thy-tree support thyExpandedKeys and thyExpandAll #INFR-12795
Pull Request #3110: feat(tree): thy-tree support thyExpandedKeys and thyExpandAll #INFR-12795

5484 of 6708 branches covered (81.75%)

Branch coverage included in aggregate %.

8 of 10 new or added lines in 2 files covered. (80.0%)

11 existing lines in 1 file now uncovered.

13228 of 13980 relevant lines covered (94.62%)

986.9 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,
52✔
37
    ThyTreeEmitEvent,
38
    ThyTreeIcons,
39
    ThyTreeNodeCheckState,
2,494✔
40
    ThyTreeNodeData
41
} from './tree.class';
42
import { ThyTreeService } from './tree.service';
43✔
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';
42✔
48
import { ThyTreeNodeDraggablePipe } from './tree.pipe';
42✔
49

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

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

168✔
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
 * 树形控件组件
118✔
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,123✔
75
            provide: NG_VALUE_ACCESSOR,
76
            useExisting: forwardRef(() => ThyTree),
77
            multi: true
93✔
78
        },
79✔
79
        {
80
            provide: THY_TREE_ABSTRACT_TOKEN,
81
            useExisting: forwardRef(() => ThyTree)
82
        },
1,107✔
83
        ThyTreeService
84
    ],
85
    standalone: true,
×
86
    imports: [
×
87
        NgIf,
88
        CdkDrag,
89
        CdkDropList,
90
        CdkVirtualScrollViewport,
×
91
        CdkFixedSizeVirtualScroll,
92
        CdkVirtualForOf,
93
        ThyTreeNodeComponent,
93✔
94
        NgFor,
93✔
95
        ThyTreeNodeDraggablePipe
93✔
96
    ]
93✔
97
})
93✔
98
export class ThyTree implements ControlValueAccessor, OnInit, OnChanges, AfterViewInit, OnDestroy {
93✔
99
    private _templateRef: TemplateRef<any>;
93✔
100

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

93✔
103
    private _draggable = false;
93✔
104

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

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

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

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

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

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

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

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

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

93✔
124
    private startDragNodeClone: ThyTreeNode;
93✔
125

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

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

139
    public _selectionModel: SelectionModel<ThyTreeNode>;
140

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

78✔
145
    public flattenTreeNodes: ThyTreeNode[] = [];
803!
146

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

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

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

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

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

181
    get thyDraggable() {
182
        return this._draggable;
6!
183
    }
184

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

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

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

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

210
    private _thyType: ThyTreeType = 'default';
211

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

42✔
225
    get thyType() {
226
        return this._thyType;
227
    }
228

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

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

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

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

5✔
262
    private _thyItemSize = 44;
5✔
263

2✔
264
    /**
265
     * 开启虚拟滚动时,单行节点的高度,当`thySize`为`default`时,该参数才生效
266
     * @default 44
267
     */
5✔
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
        }
5!
UNCOV
273
        this._thyItemSize = itemSize;
×
UNCOV
274
    }
×
275

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

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

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

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

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

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

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

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

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

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

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

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

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

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

353
    get templateRef() {
5✔
354
        return this._templateRef;
5✔
355
    }
5✔
356

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

3!
367
    get emptyChildrenTemplateRef() {
3✔
368
        return this._emptyChildrenTemplateRef;
369
    }
5✔
370

371
    @HostBinding('class.thy-tree')
372
    thyTreeClass = true;
373

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

377
    @ViewChildren(CdkDrag) cdkDrags: QueryList<CdkDrag<ThyTreeNode>>;
5✔
378

5✔
379
    constructor(public thyTreeService: ThyTreeService, private cdr: ChangeDetectorRef, @Inject(DOCUMENT) private document: Document) {}
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
        }
64✔
423
        if (changes.thyType && !changes.thyType.isFirstChange()) {
424
            this._setTreeType();
425
        }
66✔
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':
93✔
452
                this.thyOnCheckboxChange.emit(event);
93✔
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);
1✔
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
    }
489

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

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

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

507
    toggleTreeNode(node: ThyTreeNode) {
56✔
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