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

atinc / ngx-tethys / 0bbb2cec-209e-4d8a-b1b3-6bc54e05daa6

04 Sep 2023 08:40AM UTC coverage: 15.616% (-74.6%) from 90.2%
0bbb2cec-209e-4d8a-b1b3-6bc54e05daa6

Pull #2829

circleci

cmm-va
fix: add test
Pull Request #2829: fix: add tabIndex

300 of 6386 branches covered (0.0%)

Branch coverage included in aggregate %.

78 of 78 new or added lines in 26 files covered. (100.0%)

2849 of 13779 relevant lines covered (20.68%)

83.41 hits per line

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

4.13
/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,
21
    QueryList,
1✔
22
    SimpleChanges,
23
    TemplateRef,
24
    ViewChild,
25
    ViewChildren,
1✔
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,
33
    ThyTreeBeforeDragStartContext,
1✔
34
    ThyClickBehavior,
35
    ThyTreeDragDropEvent,
×
36
    ThyTreeDropPosition,
37
    ThyTreeEmitEvent,
38
    ThyTreeIcons,
×
39
    ThyTreeNodeCheckState,
40
    ThyTreeNodeData
41
} from './tree.class';
×
42
import { ThyTreeService } from './tree.service';
43
import { ThyTreeNodeComponent } from './tree-node.component';
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';
48
import { ThyTreeNodeDraggablePipe } from './tree.pipe';
49

×
50
type ThyTreeSize = 'sm' | 'default';
×
51

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

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

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

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

×
101
    private _emptyChildrenTemplateRef: TemplateRef<any>;
×
102

×
103
    private _draggable = false;
104

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

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

109
    private hostRenderer = useHostRenderer();
×
110

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

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

×
115
    private destroy$ = new Subject();
×
116

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

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

×
122
    // private startDragNodeExpanded: boolean;
×
123

×
124
    private startDragNodeClone: ThyTreeNode;
×
125

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

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

139
    public _selectionModel: SelectionModel<ThyTreeNode>;
140

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

×
145
    public flattenTreeNodes: ThyTreeNode[] = [];
×
146

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

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

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

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

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

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

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

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

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

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

×
212
    private _thyType: ThyTreeType = 'default';
×
213

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

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

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

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

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

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

265
    private _thyItemSize = 44;
×
266

267
    /**
268
     * 开启虚拟滚动时,单行节点的高度,当`thySize`为`default`时,该参数才生效
×
269
     * @default 44
×
270
     */
×
271
    @Input()
272
    @InputNumber()
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;
278
    }
×
279

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

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

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

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

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

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

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

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

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

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

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

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

×
347
    get templateRef() {
×
348
        return this._templateRef;
×
349
    }
350

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

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

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

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

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

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

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

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

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

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

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

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

430
    renderView = () => {};
431

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

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

1✔
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);
448
        this.flattenTreeNodes = this.thyTreeService.flattenTreeNodes;
1✔
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

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

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

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

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

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

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

513
    registerOnTouched(fn: any): void {
514
        this._onTouched = fn;
1✔
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
        }
523
    }
×
524

525
    emitDragMoved(event: CdkDragMove) {
526
        this.nodeDragMoved.next(event);
527
    }
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