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

atinc / ngx-tethys / d9ae709b-3c27-4b69-b125-b8b80b54f90b

pending completion
d9ae709b-3c27-4b69-b125-b8b80b54f90b

Pull #2757

circleci

mengshuicmq
fix: fix code review
Pull Request #2757: feat(color-picker): color-picker support disabled (#INFR-8645)

98 of 6315 branches covered (1.55%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

2392 of 13661 relevant lines covered (17.51%)

83.12 hits per line

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

4.98
/src/tree/tree.component.ts
1
import { InputBoolean, InputNumber } from 'ngx-tethys/core';
2
import {
3
    ThyDragDropEvent,
4
    ThyDragOverEvent,
5
    ThyDragStartEvent,
6
    ThyDropPosition,
7
    ThyDropContainerDirective,
8
    ThyDragDirective
9
} from 'ngx-tethys/drag-drop';
10
import { helpers } from 'ngx-tethys/util';
11
import { useHostRenderer } from '@tethys/cdk/dom';
12
import { SelectionModel } from '@angular/cdk/collections';
13
import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf } from '@angular/cdk/scrolling';
14
import {
15
    ChangeDetectionStrategy,
16
    ChangeDetectorRef,
17
    Component,
18
    ContentChild,
1✔
19
    EventEmitter,
20
    forwardRef,
21
    HostBinding,
22
    Input,
1✔
23
    OnChanges,
24
    OnInit,
25
    Output,
26
    SimpleChanges,
27
    TemplateRef,
28
    ViewChild,
29
    ViewEncapsulation
30
} from '@angular/core';
1✔
31
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
32
import { THY_TREE_ABSTRACT_TOKEN } from './tree-abstract';
×
33
import { ThyTreeNode } from './tree-node.class';
34
import {
35
    ThyClickBehavior,
×
36
    ThyTreeDragDropEvent,
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 } from '@angular/common';
×
45

×
46
type ThyTreeSize = 'sm' | 'default';
47

48
type ThyTreeType = 'default' | 'especial';
49

×
50
type ThyTreeNodeKey = number | string;
51

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

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

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

×
98
    private _emptyChildrenTemplateRef: TemplateRef<any>;
×
99

×
100
    private _draggable = false;
×
101

×
102
    private _expandedKeys: (string | number)[];
×
103

×
104
    private _selectedKeys: (string | number)[];
×
105

×
106
    private hostRenderer = useHostRenderer();
×
107

×
108
    public _selectionModel: SelectionModel<ThyTreeNode>;
×
109

×
110
    public treeNodes: ThyTreeNode[];
×
111

×
112
    public flattenTreeNodes: ThyTreeNode[] = [];
×
113

114
    /**
×
115
     * 虚拟化滚动的视口
×
116
     */
×
117
    @Output() @ViewChild('viewport', { static: false }) viewport: CdkVirtualScrollViewport;
×
118

×
119
    /**
×
120
     * TreeNode 展现所需的数据
×
121
     * @type ThyTreeNodeData[]
122
     */
123
    @Input() thyNodes: ThyTreeNodeData[];
×
124

125
    /**
×
126
     * 设置 TreeNode 是否支持展开
×
127
     * @type boolean | Function
×
128
     */
129
    @Input() thyShowExpand: boolean | ((_: ThyTreeNodeData) => boolean) = true;
×
130

×
131
    /**
×
132
     * 设置是否支持多选节点
133
     */
×
134
    @HostBinding(`class.thy-multiple-selection-list`)
135
    @Input()
136
    @InputBoolean()
137
    thyMultiple = false;
×
138

×
139
    /**
×
140
     * 设置 TreeNode 是否支持拖拽排序
×
141
     * @default false
×
142
     */
×
143
    @HostBinding('class.thy-tree-draggable')
×
144
    @Input()
×
145
    @InputBoolean()
146
    set thyDraggable(value: boolean) {
147
        this._draggable = value;
148
    }
×
149

×
150
    get thyDraggable() {
151
        return this._draggable;
×
152
    }
×
153

154
    /**
×
155
     * 设置 TreeNode 是否支持 Checkbox 选择
×
156
     * @default false
157
     */
×
158
    @Input() @InputBoolean() thyCheckable: boolean;
×
159

×
160
    /**
161
     * 点击节点的行为,`default` 为选中当前节点,`selectCheckbox` 为选中节点的 Checkbox, `thyCheckable` 为 true 时生效。
162
     */
163
    @Input() thyClickBehavior: ThyClickBehavior = 'default';
×
164

165
    /**
×
166
     * 设置 check 状态的计算策略
×
167
     */
168
    @Input() set thyCheckStateResolve(resolve: (node: ThyTreeNode) => ThyTreeNodeCheckState) {
×
169
        if (resolve) {
×
170
            this.thyTreeService.setCheckStateResolve(resolve);
171
        }
172
    }
173

×
174
    /**
×
175
     * 设置 TreeNode 是否支持异步加载
×
176
     */
×
177
    @Input() @InputBoolean() thyAsync = false;
×
178

×
179
    private _thyType: ThyTreeType = 'default';
×
180

181
    /**
182
     * 设置不同展示类型的 Tree,`default` 为小箭头展示, `especial` 为 加减号图标展示
×
183
     * @type ThyTreeType
×
184
     * @default default
×
185
     */
186
    @Input()
187
    set thyType(type: ThyTreeType) {
188
        this._thyType = type;
189
        if (type === 'especial') {
×
190
            this.thyIcons = { expand: 'minus-square', collapse: 'plus-square' };
×
191
        }
192
    }
193

194
    get thyType() {
×
195
        return this._thyType;
196
    }
197

×
198
    /**
×
199
     * 设置不同 Tree 展开折叠的图标,`expand` 为展开状态的图标,`collapse` 为折叠状态的图标
×
200
     * @type { expand: string, collapse: string }
×
201
     */
202
    @Input() thyIcons: ThyTreeIcons = {};
203

204
    private _thySize: ThyTreeSize = 'default';
205
    /**
×
206
     * 支持 `sm` | `default` 两种大小,默认值为 `default`
207
     * @type ThyTreeSize
208
     * @default default
×
209
     */
×
210
    @Input()
211
    set thySize(size: ThyTreeSize) {
212
        this._thySize = size;
213
        if (this._thySize) {
×
214
            this._thyItemSize = treeItemSizeMap[this._thySize];
215
        } else {
216
            this._thyItemSize = treeItemSizeMap.default;
×
217
        }
×
218
    }
×
219

220
    get thySize() {
221
        return this._thySize;
222
    }
×
223

×
224
    /**
225
     * 设置是否开启虚拟滚动
×
226
     */
×
227
    @HostBinding('class.thy-virtual-scrolling-tree')
×
228
    @Input()
229
    @InputBoolean()
230
    thyVirtualScroll = false;
×
231

232
    private _thyItemSize = 44;
×
233

234
    /**
×
235
     * 开启虚拟滚动时,单行节点的高度,当`thySize`为`default`时,该参数才生效
×
236
     * @default 44
×
237
     */
238
    @Input()
239
    @InputNumber()
×
240
    set thyItemSize(itemSize: number) {
×
241
        if (this.thySize !== 'default') {
×
242
            throw new Error('setting thySize and thyItemSize at the same time is not allowed');
×
243
        }
×
244
        this._thyItemSize = itemSize;
245
    }
246

×
247
    get thyItemSize() {
248
        return this._thyItemSize;
×
249
    }
250

×
251
    /**
×
252
     * 设置节点名称是否支持超出截取
×
253
     * @type boolean
×
254
     */
×
255
    @Input() @InputBoolean() thyTitleTruncate = true;
×
256

257
    /**
×
258
     * 已选中的 node 节点集合
×
259
     * @default []
×
260
     */
261
    @Input() thySelectedKeys: string[];
262

×
263
    /**
×
264
     * 设置缩进距离,缩进距离 = thyIndent * node.level
265
     * @type number
×
266
     */
×
267
    @Input() @InputNumber() thyIndent = 25;
×
268

269
    /**
×
270
     * 拖拽之前的回调,函数返回 false 则阻止拖拽
×
271
     */
272
    @Input() thyBeforeDragStart: (e: ThyDragStartEvent) => boolean;
273

274
    /**
275
     * 拖放到元素时回调,函数返回 false 则组织拖放到当前元素
276
     */
277
    @Input() thyBeforeDragDrop: (e: ThyDragDropEvent) => boolean;
278

279
    /**
×
280
     * 设置子 TreeNode 点击事件
×
281
     */
282
    @Output() thyOnClick: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
283

×
284
    /**
285
     * 设置 check 选择事件
286
     */
287
    @Output() thyOnCheckboxChange: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
×
288

×
289
    /**
×
290
     * 设置点击展开触发事件
291
     */
292
    @Output() thyOnExpandChange: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
293

×
294
    /**
295
     * 设置 TreeNode 拖拽事件
296
     */
×
297
    @Output() thyOnDragDrop: EventEmitter<ThyTreeDragDropEvent> = new EventEmitter<ThyTreeDragDropEvent>();
298

299
    /**
300
     * 双击 TreeNode 事件
×
301
     */
×
302
    @Output() thyDblClick: EventEmitter<ThyTreeEmitEvent> = new EventEmitter<ThyTreeEmitEvent>();
×
303

304
    /**
305
     * 设置 TreeNode 的渲染模板
306
     */
×
307
    @ContentChild('treeNodeTemplate', { static: true })
308
    set templateRef(template: TemplateRef<any>) {
309
        if (template) {
×
310
            this._templateRef = template;
311
        }
312
    }
×
313

314
    get templateRef() {
315
        return this._templateRef;
×
316
    }
317

318
    /**
×
319
     * 设置子的空数据渲染模板
320
     */
321
    @ContentChild('emptyChildrenTemplate', { static: true }) emptyChildrenTemplate: TemplateRef<any>;
×
322
    set emptyChildrenTemplateRef(template: TemplateRef<any>) {
323
        if (template) {
×
324
            this._emptyChildrenTemplateRef = template;
×
325
        }
326
    }
327

×
328
    get emptyChildrenTemplateRef() {
×
329
        return this._emptyChildrenTemplateRef;
330
    }
×
331

332
    @HostBinding('class.thy-tree') thyTreeClass = true;
333

×
334
    beforeDragOver = (event: ThyDragOverEvent<ThyTreeNode>) => {
×
335
        return this.isShowExpand(event.item) || (!this.isShowExpand(event.item) && event.position !== ThyDropPosition.in);
336
    };
337

×
338
    private _onTouched: () => void = () => {};
×
339

340
    private _onChange: (value: any) => void = (_: any) => {};
1✔
341

342
    private dragItem: ThyTreeNode;
343

344
    constructor(public thyTreeService: ThyTreeService, private cdr: ChangeDetectorRef) {}
1✔
345

346
    ngOnInit(): void {
347
        this._initThyNodes();
348
        this._setTreeType();
349
        this._setTreeSize();
350
        this._instanceSelectionModel();
351
        this._selectTreeNodes(this.thySelectedKeys);
352

353
        this.thyTreeService.flattenNodes$.subscribe(flattenTreeNodes => {
354
            this.flattenTreeNodes = flattenTreeNodes;
355
            this.cdr.markForCheck();
356
        });
357
    }
358

359
    ngOnChanges(changes: SimpleChanges): void {
360
        if (changes.thyNodes && !changes.thyNodes.isFirstChange()) {
361
            this._initThyNodes();
362
        }
363
        if (changes.thyType && !changes.thyType.isFirstChange()) {
364
            this._setTreeType();
365
        }
366
        if (changes.thyMultiple && !changes.thyMultiple.isFirstChange()) {
367
            this._instanceSelectionModel();
368
        }
369

370
        if (changes.thySelectedKeys && !changes.thySelectedKeys.isFirstChange()) {
371
            this._selectedKeys = changes.thySelectedKeys.currentValue;
372
            this._selectTreeNodes(changes.thySelectedKeys.currentValue);
373
        }
374
    }
1✔
375

376
    renderView = () => {};
377

378
    eventTriggerChanged(event: ThyTreeEmitEvent): void {
1✔
379
        switch (event.eventName) {
380
            case 'expand':
381
                this.thyOnExpandChange.emit(event);
382
                break;
383

1✔
384
            case 'checkboxChange':
385
                this.thyOnCheckboxChange.emit(event);
386
                break;
387
        }
1✔
388
    }
389

390
    private _initThyNodes() {
391
        this._expandedKeys = this.getExpandedNodes().map(node => node.key);
1✔
392
        this._selectedKeys = this.getSelectedNodes().map(node => node.key);
393
        this.treeNodes = (this.thyNodes || []).map(node => new ThyTreeNode(node, null, this.thyTreeService));
394
        this.thyTreeService.initializeTreeNodes(this.treeNodes);
395
        this.flattenTreeNodes = this.thyTreeService.flattenTreeNodes;
1✔
396
        this._selectTreeNodes(this._selectedKeys);
397
        this.thyTreeService.expandTreeNodes(this._expandedKeys);
398
    }
399

400
    private _setTreeType() {
1✔
401
        if (this.thyType && treeTypeClassMap[this.thyType]) {
402
            treeTypeClassMap[this.thyType].forEach(className => {
403
                this.hostRenderer.addClass(className);
404
            });
1✔
405
        }
406
    }
407

408
    private _setTreeSize() {
1✔
409
        if (this.thySize) {
410
            this.hostRenderer.addClass(`thy-tree-${this.thySize}`);
411
        }
412
    }
413

414
    private _instanceSelectionModel() {
415
        this._selectionModel = new SelectionModel<any>(this.thyMultiple);
416
    }
417

×
418
    private _selectTreeNodes(keys: (string | number)[]) {
419
        (keys || []).forEach(key => {
420
            const node = this.thyTreeService.getTreeNode(key);
421
            if (node) {
422
                this.selectTreeNode(this.thyTreeService.getTreeNode(key));
×
423
            }
424
        });
425
    }
426

427
    public beforeDragDrop = (event: ThyDragDropEvent<ThyTreeNode>) => {
428
        event.previousItem = this.dragItem;
429
        if (event.item.level > 0) {
430
            event.containerItems = event.item.parentNode.children;
431
        } else {
432
            event.containerItems = event.containerItems.filter(item => item.level === 0);
433
        }
434
        event.currentIndex = (event.containerItems || []).findIndex(item => item.key === event.item.key);
435

436
        if (event.previousItem.level > 0) {
437
            event.previousContainerItems = event.previousItem.parentNode.children;
438
        }
439
        event.previousIndex = (event.previousContainerItems || []).findIndex(item => item.key === event.previousItem.key);
440

441
        if (this.thyBeforeDragDrop) {
442
            return this.thyBeforeDragDrop(event);
443
        }
444
        return true;
445
    };
446

447
    public isSelected(node: ThyTreeNode) {
448
        return this._selectionModel.isSelected(node);
449
    }
450

451
    public toggleTreeNode(node: ThyTreeNode) {
452
        if (node && !node.isDisabled) {
453
            this._selectionModel.toggle(node);
454
        }
455
    }
456

457
    public trackByFn(index: number, item: any) {
458
        return item.key || index;
459
    }
460

461
    public onDragStart(event: ThyDragStartEvent<ThyTreeNode>) {
462
        this.dragItem = event.item;
463
        if (this.isShowExpand(event.item) && event.item.isExpanded) {
464
            event.item.setExpanded(false);
465
        }
466
    }
467

468
    public onDragDrop(event: ThyDragDropEvent<ThyTreeNode>) {
469
        if (!this.isShowExpand(event.item) && event.position === ThyDropPosition.in) {
470
            return;
471
        }
472
        const parent = event.previousItem.parentNode;
473
        if (parent) {
474
            parent.children = parent.children.filter(item => item !== event.previousItem);
475
        } else {
476
            this.treeNodes.splice(this.thyTreeService.treeNodes.indexOf(event.previousItem), 1);
477
        }
478
        switch (event.position) {
479
            case ThyDropPosition.in:
480
                event.previousItem.parentNode = event.item;
481
                event.item.addChildren(event.previousItem.origin);
482
                break;
483
            case ThyDropPosition.after:
484
            case ThyDropPosition.before:
485
                event.previousItem.parentNode = event.item.parentNode;
486
                const targetParent = event.item.parentNode;
487
                const index = event.position === ThyDropPosition.before ? 0 : 1;
488
                if (targetParent) {
489
                    targetParent.addChildren(event.previousItem.origin, targetParent.children.indexOf(event.item) + index);
490
                } else {
491
                    this.treeNodes.splice(this.treeNodes.indexOf(event.item) + index, 0, event.previousItem);
492
                }
493
                break;
494
        }
495
        this.thyTreeService.resetSortedTreeNodes(this.treeNodes);
496

497
        let afterNode = null;
498
        let targetNode = null;
499
        if (event.position === ThyDropPosition.before) {
500
            afterNode = event.containerItems[event.currentIndex - 1];
501
            targetNode = event.item.parentNode;
502
        } else if (event.position === ThyDropPosition.after) {
503
            afterNode = event.containerItems[event.currentIndex];
504
            targetNode = event.item.parentNode;
505
        } else {
506
            afterNode = event.item.children[event.item.children.length - 2];
507
            targetNode = event.item;
508
        }
509
        this.thyTreeService.syncNodeCheckState(this.thyTreeService.getTreeNode(event.previousItem.key));
510
        if (parent) {
511
            this.thyTreeService.syncNodeCheckState(parent);
512
        }
513
        this.thyTreeService.syncFlattenTreeNodes();
514
        this.thyOnDragDrop.emit({
515
            event,
516
            currentIndex: event.currentIndex,
517
            dragNode: event.previousItem,
518
            targetNode: targetNode,
519
            afterNode: afterNode
520
        });
521
    }
522

523
    public isShowExpand(node: ThyTreeNode) {
524
        if (helpers.isFunction(this.thyShowExpand)) {
525
            return (this.thyShowExpand as Function)(node);
526
        } else {
527
            return this.thyShowExpand;
528
        }
529
    }
530

531
    writeValue(value: ThyTreeNodeData[]): void {
532
        if (value) {
533
            this.thyNodes = value;
534
            this._initThyNodes();
535
        }
536
    }
537

538
    registerOnChange(fn: any): void {
539
        this._onChange = fn;
540
    }
541

542
    registerOnTouched(fn: any): void {
543
        this._onTouched = fn;
544
    }
545

546
    // region Public Functions
547

548
    public selectTreeNode(node: ThyTreeNode) {
549
        if (node && !node.isDisabled) {
550
            this._selectionModel.select(node);
551
            this.thyTreeService.syncFlattenTreeNodes();
552
        }
553
    }
554

555
    public getRootNodes(): ThyTreeNode[] {
556
        return this.treeNodes;
557
    }
558

559
    public getTreeNode(key: string) {
560
        return this.thyTreeService.getTreeNode(key);
561
    }
562

563
    public getSelectedNode(): ThyTreeNode {
564
        return this._selectionModel ? this._selectionModel.selected[0] : null;
565
    }
566

567
    public getSelectedNodes(): ThyTreeNode[] {
568
        return this._selectionModel ? this._selectionModel.selected : [];
569
    }
570

571
    public getExpandedNodes(): ThyTreeNode[] {
572
        return this.thyTreeService.getExpandedNodes();
573
    }
574

575
    public getCheckedNodes(): ThyTreeNode[] {
576
        return this.thyTreeService.getCheckedNodes();
577
    }
578

579
    public addTreeNode(node: ThyTreeNodeData, parent?: ThyTreeNode, index = -1) {
580
        this.thyTreeService.addTreeNode(new ThyTreeNode(node, null, this.thyTreeService), parent, index);
581
    }
582

583
    public deleteTreeNode(node: ThyTreeNode) {
584
        if (this.isSelected(node)) {
585
            this._selectionModel.toggle(node);
586
        }
587
        this.thyTreeService.deleteTreeNode(node);
588
    }
589

590
    public expandAllNodes() {
591
        const nodes = this.getRootNodes();
592
        nodes.forEach(n => n.setExpanded(true, true));
593
    }
594

595
    public collapsedAllNodes() {
596
        const nodes = this.getRootNodes();
597
        nodes.forEach(n => n.setExpanded(false, true));
598
    }
599

600
    // endregion
601
}
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