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

atinc / ngx-tethys / 3033f133-0f0d-43eb-a07d-e1848354018a

07 Mar 2024 01:58AM UTC coverage: 90.58% (-0.02%) from 90.604%
3033f133-0f0d-43eb-a07d-e1848354018a

Pull #3022

circleci

web-flow
feat(schematics): improve schematics for select and custom-select in template #INFR-11735 (#3047)
Pull Request #3022: feat: upgrade ng to 17 #INFR-11427 (#3021)

5422 of 6642 branches covered (81.63%)

Branch coverage included in aggregate %.

328 of 338 new or added lines in 193 files covered. (97.04%)

141 existing lines in 29 files now uncovered.

13502 of 14250 relevant lines covered (94.75%)

982.04 hits per line

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

87.91
/src/tree/tree.component.ts
1
import { InputBoolean, InputNumber } from 'ngx-tethys/core';
2
import { helpers } from 'ngx-tethys/util';
3
import { useHostRenderer } from '@tethys/cdk/dom';
4
import { SelectionModel } from '@angular/cdk/collections';
5
import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf } from '@angular/cdk/scrolling';
6
import {
7
    AfterViewInit,
8
    ChangeDetectionStrategy,
9
    ChangeDetectorRef,
10
    Component,
11
    ContentChild,
12
    EventEmitter,
13
    forwardRef,
14
    HostBinding,
15
    Inject,
16
    Input,
17
    OnChanges,
18
    OnDestroy,
19
    OnInit,
20
    Output,
1✔
21
    QueryList,
22
    SimpleChanges,
23
    TemplateRef,
24
    ViewChild,
1✔
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 {
32
    ThyTreeBeforeDragDropContext,
1✔
33
    ThyTreeBeforeDragStartContext,
34
    ThyClickBehavior,
23✔
35
    ThyTreeDragDropEvent,
36
    ThyTreeDropPosition,
37
    ThyTreeEmitEvent,
50✔
38
    ThyTreeIcons,
39
    ThyTreeNodeCheckState,
40
    ThyTreeNodeData
2,340✔
41
} from './tree.class';
42
import { ThyTreeService } from './tree.service';
43
import { ThyTreeNodeComponent } from './tree-node.component';
41✔
44
import { NgIf, NgFor, DOCUMENT } from '@angular/common';
2✔
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';
48
import { ThyTreeNodeDraggablePipe } from './tree.pipe';
40✔
49

40✔
50
type ThyTreeSize = 'sm' | 'default';
39✔
51

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

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

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

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

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

103
    private _draggable = false;
89✔
104

89✔
105
    private _expandedKeys: (string | number)[];
89✔
106

5!
107
    private _selectedKeys: (string | number)[];
108

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

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

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

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

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

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

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

89✔
124
    private startDragNodeClone: ThyTreeNode;
89✔
125

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

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

139
    public _selectionModel: SelectionModel<ThyTreeNode>;
140

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

74✔
145
    public flattenTreeNodes: ThyTreeNode[] = [];
730!
146

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

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

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

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

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

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

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

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

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

54✔
207
    /**
208
     * 设置 TreeNode 是否支持异步加载
209
     */
111✔
210
    @Input() @InputBoolean() thyAsync = false;
40✔
211

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

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

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

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

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

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

257
    /**
258
     * 设置是否开启虚拟滚动
259
     */
5✔
260
    @HostBinding('class.thy-virtual-scrolling-tree')
5!
UNCOV
261
    @Input()
×
262
    @InputBoolean()
×
263
    thyVirtualScroll = false;
264

5!
265
    private _thyItemSize = 44;
266

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

5!
280
    get thyItemSize() {
281
        return this._thyItemSize;
282
    }
283

284
    /**
5✔
285
     * 设置节点名称是否支持超出截取
286
     * @type boolean
UNCOV
287
     */
×
288
    @Input() @InputBoolean() thyTitleTruncate = true;
×
289

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

296
    /**
5✔
297
     * 设置缩进距离,缩进距离 = thyIndent * node.level
298
     * @type number
5✔
299
     */
5✔
300
    @Input() @InputNumber() thyIndent = 25;
5✔
301

302
    /**
303
     * 拖拽之前的回调,函数返回 false 则阻止拖拽
304
     */
5!
UNCOV
305
    @Input() thyBeforeDragStart: (context: ThyTreeBeforeDragStartContext) => boolean;
×
306

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

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

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

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

327
    /**
5✔
328
     * 设置 TreeNode 拖拽事件
5✔
329
     */
330
    @Output() thyOnDragDrop: EventEmitter<ThyTreeDragDropEvent> = new EventEmitter<ThyTreeDragDropEvent>();
1✔
331

1✔
332
    /**
1✔
333
     * 双击 TreeNode 事件
334
     */
1!
335
    @Output() thyDblClick: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
1✔
336

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

1✔
347
    get templateRef() {
1✔
348
        return this._templateRef;
349
    }
4✔
350

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

361
    get emptyChildrenTemplateRef() {
362
        return this._emptyChildrenTemplateRef;
5✔
363
    }
364

365
    @HostBinding('class.thy-tree')
5✔
366
    thyTreeClass = true;
5✔
367

368
    @HostBinding('class.thy-tree-dragging')
5✔
369
    dragging: boolean;
1✔
370

371
    @ViewChildren(CdkDrag) cdkDrags: QueryList<CdkDrag<ThyTreeNode>>;
4✔
372

1✔
373
    constructor(public thyTreeService: ThyTreeService, private cdr: ChangeDetectorRef, @Inject(DOCUMENT) private document: Document) {}
374

375
    ngOnInit(): void {
3✔
376
        this._initThyNodes();
377
        this._setTreeType();
378
        this._setTreeSize();
379
        this._instanceSelectionModel();
5✔
380
        this._selectTreeNodes(this.thySelectedKeys);
5!
381

5✔
382
        this.thyTreeService.flattenNodes$.subscribe(flattenTreeNodes => {
383
            this.flattenTreeNodes = flattenTreeNodes;
384
            this.cdr.markForCheck();
385
        });
10✔
386
    }
10✔
387

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

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

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

2✔
424
        if (changes.thySelectedKeys && !changes.thySelectedKeys.isFirstChange()) {
1✔
425
            this._selectedKeys = changes.thySelectedKeys.currentValue;
426
            this._selectTreeNodes(changes.thySelectedKeys.currentValue);
2✔
427
        }
2✔
428
    }
429

430
    renderView = () => {};
1✔
431

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

438
            case 'checkboxChange':
439
                this.thyOnCheckboxChange.emit(event);
89✔
440
                break;
89✔
441
        }
442
    }
1✔
443

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

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

461
    private _setTreeSize() {
462
        if (this.thySize) {
463
            this.hostRenderer.addClass(`thy-tree-${this.thySize}`);
464
        }
465
    }
466

467
    private _instanceSelectionModel() {
468
        this._selectionModel = new SelectionModel<any>(this.thyMultiple);
469
    }
470

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

1✔
480
    isSelected(node: ThyTreeNode) {
481
        return this._selectionModel.isSelected(node);
482
    }
483

1✔
484
    toggleTreeNode(node: ThyTreeNode) {
485
        if (node && !node.isDisabled) {
486
            this._selectionModel.toggle(node);
487
        }
488
    }
1✔
489

490
    trackByFn(index: number, item: any) {
491
        return item.key || index;
492
    }
1✔
493

494
    isShowExpand(node: ThyTreeNode) {
495
        if (helpers.isFunction(this.thyShowExpand)) {
496
            return (this.thyShowExpand as Function)(node);
1✔
497
        } else {
498
            return this.thyShowExpand;
499
        }
500
    }
1✔
501

502
    writeValue(value: ThyTreeNodeData[]): void {
503
        if (value) {
504
            this.thyNodes = value;
505
            this._initThyNodes();
1✔
506
        }
507
    }
508

509
    registerOnChange(fn: any): void {
1✔
510
        this._onChange = fn;
511
    }
512

513
    registerOnTouched(fn: any): void {
1✔
514
        this._onTouched = fn;
515
    }
516

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

525
    emitDragMoved(event: CdkDragMove) {
526
        this.nodeDragMoved.next(event);
527
    }
54✔
528

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

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

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

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

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

600
        if (this.thyBeforeDragDrop && !this.thyBeforeDragDrop(beforeDragDropContext)) {
601
            this.cleanupDragArtifacts();
602
            return;
603
        }
604

605
        this.thyTreeService.deleteTreeNode(sourceNode);
606

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

621
        this.thyTreeService.syncFlattenTreeNodes();
622

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

637
        this.thyOnDragDrop.emit({
638
            dragNode: this.thyTreeService.getTreeNode(sourceNode.key),
639
            targetNode: targe,
640
            afterNode: after
641
        });
642

643
        this.cleanupDragArtifacts();
644
    }
645

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

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

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

672
    private cleanupDragArtifacts() {
673
        this.nodeDropTarget = null;
674
        this.cleanupDropPositionPlaceholder();
675
    }
676

677
    // region Public Functions
678

679
    selectTreeNode(node: ThyTreeNode) {
680
        if (node && !node.isDisabled) {
681
            this._selectionModel.select(node);
682
            this.thyTreeService.syncFlattenTreeNodes();
683
        }
684
    }
685

686
    getRootNodes(): ThyTreeNode[] {
687
        return this.treeNodes;
688
    }
689

690
    getTreeNode(key: string) {
691
        return this.thyTreeService.getTreeNode(key);
692
    }
693

694
    getSelectedNode(): ThyTreeNode {
695
        return this._selectionModel ? this._selectionModel.selected[0] : null;
696
    }
697

698
    getSelectedNodes(): ThyTreeNode[] {
699
        return this._selectionModel ? this._selectionModel.selected : [];
700
    }
701

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

706
    getCheckedNodes(): ThyTreeNode[] {
707
        return this.thyTreeService.getCheckedNodes();
708
    }
709

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

715
    deleteTreeNode(node: ThyTreeNode) {
716
        if (this.isSelected(node)) {
717
            this._selectionModel.toggle(node);
718
        }
719
        this.thyTreeService.deleteTreeNode(node);
720
        this.thyTreeService.syncFlattenTreeNodes();
721
    }
722

723
    expandAllNodes() {
724
        const nodes = this.getRootNodes();
725
        nodes.forEach(n => n.setExpanded(true, true));
726
    }
727

728
    collapsedAllNodes() {
729
        const nodes = this.getRootNodes();
730
        nodes.forEach(n => n.setExpanded(false, true));
731
    }
732

733
    // endregion
734

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