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

atinc / ngx-tethys / edbc1d43-1648-411a-a6bc-f24c9aa3f654

27 Mar 2025 06:13AM UTC coverage: 90.236% (+0.06%) from 90.179%
edbc1d43-1648-411a-a6bc-f24c9aa3f654

push

circleci

web-flow
Merge pull request #3282 from atinc/v19.0.0-next

5598 of 6865 branches covered (81.54%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 7 files covered. (100.0%)

157 existing lines in 46 files now uncovered.

13357 of 14141 relevant lines covered (94.46%)

992.51 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
    imports: [
41✔
86
        CdkDrag,
87
        CdkDropList,
88
        CdkVirtualScrollViewport,
89
        CdkFixedSizeVirtualScroll,
165✔
90
        CdkVirtualForOf,
91
        ThyTreeNodeComponent,
92
        ThyTreeNodeDraggablePipe
44✔
93
    ]
44✔
94
})
43✔
95
export class ThyTree implements ControlValueAccessor, OnInit, OnChanges, AfterViewInit, OnDestroy {
96
    thyTreeService = inject(ThyTreeService);
97
    private cdr = inject(ChangeDetectorRef);
1✔
98
    private document = inject(DOCUMENT);
99

100
    private _templateRef: TemplateRef<any>;
101

115✔
102
    private _emptyChildrenTemplateRef: TemplateRef<any>;
103

104
    private _draggable = false;
5✔
105

1✔
106
    private _expandedKeys: (string | number)[];
107

4✔
108
    private _selectedKeys: (string | number)[];
109

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

112
    private _onTouched: () => void = () => {};
113

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

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

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

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

×
123
    // private startDragNodeExpanded: boolean;
124

125
    private startDragNodeClone: ThyTreeNode;
UNCOV
126

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

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

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

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

788!
146
    public flattenTreeNodes: ThyTreeNode[] = [];
147

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

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

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

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

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

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

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

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

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

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

211
    private _thyType: ThyTreeType = 'default';
212

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

226
    get thyType() {
227
        return this._thyType;
228
    }
229

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

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

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

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

5✔
263
    private _thyItemSize = 44;
2✔
264

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

442
    renderView = () => {};
1✔
443

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

627
        this.thyTreeService.deleteTreeNode(sourceNode);
628

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

643
        this.thyTreeService.syncFlattenTreeNodes();
644

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

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

665
        this.cleanupDragArtifacts();
666
    }
667

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

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

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

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

699
    // region Public Functions
700

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

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

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

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

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

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

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

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

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

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

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

755
    // endregion
756

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