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

atinc / ngx-tethys / c0ef8457-a839-451f-8b72-80fd73106231

02 Apr 2024 02:27PM UTC coverage: 90.524% (-0.06%) from 90.585%
c0ef8457-a839-451f-8b72-80fd73106231

Pull #3062

circleci

minlovehua
refactor(all): use the transform attribute of @Input() instead of @InputBoolean() and @InputNumber()
Pull Request #3062: refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()

4987 of 6108 branches covered (81.65%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

202 existing lines in 53 files now uncovered.

12246 of 12929 relevant lines covered (94.72%)

1055.59 hits per line

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

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

51
type ThyTreeSize = 'sm' | 'default';
52

53
type ThyTreeType = 'default' | 'especial';
162✔
54

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

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

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

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

89✔
104
    private _draggable = false;
89✔
105

5!
106
    private _expandedKeys: (string | number)[];
107

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

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

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

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

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

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

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

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

89✔
125
    private startDragNodeClone: ThyTreeNode;
89✔
126

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

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

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

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

146
    public flattenTreeNodes: ThyTreeNode[] = [];
730✔
147

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

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

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

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

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

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

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

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

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

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

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

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

226
    get thyType() {
227
        return this._thyType;
1,044✔
228
    }
20✔
229

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

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

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

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

263
    private _thyItemSize = 44;
5!
264

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

358
    get emptyChildrenTemplateRef() {
359
        return this._emptyChildrenTemplateRef;
360
    }
361

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

602
        this.thyTreeService.deleteTreeNode(sourceNode);
603

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

618
        this.thyTreeService.syncFlattenTreeNodes();
619

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

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

640
        this.cleanupDragArtifacts();
641
    }
642

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

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

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

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

674
    // region Public Functions
675

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

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

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

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

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

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

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

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

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

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

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

730
    // endregion
731

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