• 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

6.02
/src/table/table.component.ts
1
import {
2
    Constructor,
3
    InputBoolean,
4
    InputCssPixel,
5
    InputNumber,
6
    MixinBase,
7
    mixinUnsubscribe,
8
    ThyUnsubscribe,
9
    UpdateHostClassService
10
} from 'ngx-tethys/core';
11
import { Dictionary, SafeAny } from 'ngx-tethys/types';
12
import { coerceBooleanProperty, get, helpers, isString, keyBy, set } from 'ngx-tethys/util';
13
import { EMPTY, fromEvent, merge, Observable, of } from 'rxjs';
14
import { delay, startWith, switchMap, takeUntil } from 'rxjs/operators';
15

16
import { CdkDrag, CdkDragDrop, CdkDragEnd, CdkDragStart, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
17
import { ViewportRuler } from '@angular/cdk/overlay';
18
import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
19
import { DOCUMENT, isPlatformServer, NgClass, NgFor, NgIf, NgTemplateOutlet, NgStyle } from '@angular/common';
20
import {
21
    AfterViewInit,
22
    ChangeDetectorRef,
23
    Component,
24
    ContentChild,
25
    ContentChildren,
26
    ElementRef,
1✔
27
    EventEmitter,
1✔
28
    HostBinding,
1✔
29
    Inject,
2✔
30
    Input,
1✔
31
    IterableChangeRecord,
32
    IterableChanges,
33
    IterableDiffer,
34
    IterableDiffers,
35
    NgZone,
1✔
36
    OnChanges,
37
    OnDestroy,
38
    OnInit,
39
    Output,
40
    PLATFORM_ID,
41
    QueryList,
1✔
42
    Renderer2,
43
    SimpleChanges,
44
    TemplateRef,
45
    ViewChild,
46
    ViewChildren,
47
    ViewEncapsulation
1✔
48
} from '@angular/core';
1✔
49

50
import { IThyTableColumnParentComponent, THY_TABLE_COLUMN_PARENT_COMPONENT, ThyTableColumnComponent } from './table-column.component';
51
import {
52
    PageChangedEvent,
53
    ThyMultiSelectEvent,
54
    ThyPage,
1✔
55
    ThyRadioSelectEvent,
56
    ThySwitchEvent,
×
57
    ThyTableDraggableEvent,
58
    ThyTableEmptyOptions,
59
    ThyTableEvent,
×
60
    ThyTableRowEvent,
61
    ThyTableSortDirection,
62
    ThyTableSortEvent
×
63
} from './table.interface';
64
import { TableRowDragDisabledPipe } from './pipes/drag.pipe';
65
import { TableIsValidModelValuePipe } from './pipes/table.pipe';
×
66
import { ThyPaginationComponent } from 'ngx-tethys/pagination';
67
import { ThyLoadingComponent } from 'ngx-tethys/loading';
68
import { ThyEmptyComponent } from 'ngx-tethys/empty';
×
69
import { ThySwitchComponent } from 'ngx-tethys/switch';
70
import { FormsModule } from '@angular/forms';
71
import { ThyDragDropDirective, ThyContextMenuDirective } from 'ngx-tethys/shared';
×
72
import { ThyIconComponent } from 'ngx-tethys/icon';
×
73
import { CdkScrollable } from '@angular/cdk/scrolling';
74

75
export type ThyTableTheme = 'default' | 'bordered' | 'boxed';
76

×
77
export type ThyTableMode = 'list' | 'group' | 'tree';
×
78

×
79
export type ThyTableSize = 'md' | 'sm' | 'xs' | 'lg' | 'xlg' | 'default';
×
80

×
81
export enum ThyFixedDirection {
82
    left = 'left',
83
    right = 'right'
84
}
×
85

×
86
interface ThyTableGroup<T = unknown> {
87
    id?: string;
88
    expand?: boolean;
×
89
    children?: object[];
×
90
    origin?: T;
91
}
92

×
93
const tableThemeMap = {
×
94
    default: 'table-default',
×
95
    bordered: 'table-bordered',
×
96
    boxed: 'table-boxed'
97
};
98

×
99
const customType = {
100
    index: 'index',
×
101
    checkbox: 'checkbox',
×
102
    radio: 'radio',
103
    switch: 'switch'
104
};
×
105

106
const css = {
107
    tableBody: 'thy-table-body',
×
108
    tableScrollLeft: 'thy-table-scroll-left',
109
    tableScrollRight: 'thy-table-scroll-right',
110
    tableScrollMiddle: 'thy-table-scroll-middle'
×
111
};
112

113
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
×
114

115
const _MixinBase: Constructor<ThyUnsubscribe> & typeof MixinBase = mixinUnsubscribe(MixinBase);
116

×
117
/**
×
118
 * 表格组件
×
119
 * @name thy-table
120
 * @order 10
121
 */
122
@Component({
×
123
    selector: 'thy-table',
124
    templateUrl: './table.component.html',
125
    providers: [
×
126
        {
127
            provide: THY_TABLE_COLUMN_PARENT_COMPONENT,
128
            useExisting: ThyTableComponent
×
129
        },
130
        UpdateHostClassService
131
    ],
×
132
    encapsulation: ViewEncapsulation.None,
×
133
    host: {
134
        class: 'thy-table',
×
135
        '[class.thy-table-bordered]': `theme === 'bordered'`,
136
        '[class.thy-table-boxed]': `theme === 'boxed'`,
137
        '[class.thy-table-fixed-header]': 'thyHeaderFixed'
×
138
    },
139
    standalone: true,
140
    imports: [
×
141
        CdkScrollable,
×
142
        NgClass,
×
143
        NgFor,
×
144
        NgIf,
145
        NgTemplateOutlet,
×
146
        ThyIconComponent,
×
147
        ThyDragDropDirective,
148
        CdkDropList,
149
        CdkDrag,
150
        ThyContextMenuDirective,
×
151
        NgStyle,
×
152
        FormsModule,
×
153
        ThySwitchComponent,
×
154
        ThyEmptyComponent,
×
155
        ThyLoadingComponent,
×
156
        ThyPaginationComponent,
×
157
        TableIsValidModelValuePipe,
×
158
        TableRowDragDisabledPipe
×
159
    ]
×
160
})
×
161
export class ThyTableComponent extends _MixinBase implements OnInit, OnChanges, AfterViewInit, OnDestroy, IThyTableColumnParentComponent {
×
162
    public customType = customType;
×
163

×
164
    public model: object[] = [];
×
165

×
166
    public groups: ThyTableGroup[] = [];
×
167

×
168
    public rowKey = '_id';
×
169

×
170
    public groupBy: string;
×
171

×
172
    public mode: ThyTableMode = 'list';
×
173

×
174
    public theme: ThyTableTheme = 'default';
×
175

×
176
    public className = '';
×
177

×
178
    public size: ThyTableSize = 'md';
×
179

×
180
    public rowClassName: string | Function;
×
181

×
182
    public loadingDone = true;
×
183

×
184
    public loadingText: string;
×
185

×
186
    public emptyOptions: ThyTableEmptyOptions = {};
×
187

×
188
    public draggable = false;
×
189

×
190
    public selectedRadioRow: SafeAny = null;
×
191

×
192
    public pagination: ThyPage = { index: 1, size: 20, total: 0, sizeOptions: [20, 50, 100] };
×
193

×
194
    public trackByFn: SafeAny;
×
195

×
196
    public wholeRowSelect = false;
197

×
198
    public fixedDirection = ThyFixedDirection;
×
199

×
200
    public hasFixed = false;
×
201

×
202
    public columns: ThyTableColumnComponent[] = [];
×
203

204
    private _diff: IterableDiffer<SafeAny>;
×
205

206
    private initialized = false;
207

×
208
    private _oldThyClassName = '';
×
209

210
    private scrollClassName = css.tableScrollLeft;
×
211

212
    private get tableScrollElement(): HTMLElement {
213
        return this.elementRef.nativeElement.getElementsByClassName(css.tableBody)[0] as HTMLElement;
×
214
    }
×
215

×
216
    private get scroll$() {
×
217
        return merge(this.tableScrollElement ? fromEvent<MouseEvent>(this.tableScrollElement, 'scroll') : EMPTY);
218
    }
×
219

×
220
    /**
×
221
     * 设置数据为空时展示的模板
×
222
     * @type TemplateRef
223
     */
224
    @ContentChild('empty') emptyTemplate: TemplateRef<SafeAny>;
225

×
226
    @ViewChild('table', { static: true }) tableElementRef: ElementRef<SafeAny>;
×
227

×
228
    @ViewChildren('rows', { read: ElementRef }) rows: QueryList<ElementRef<HTMLElement>>;
×
229

230
    /**
231
     * 表格展示方式,列表/分组/树
232
     * @type list | group | tree
233
     * @default list
×
234
     */
×
235
    @Input()
×
236
    set thyMode(value: ThyTableMode) {
×
237
        this.mode = value || this.mode;
238
    }
×
239

×
240
    /**
×
241
     * thyMode的值为 `group` 时分组的 Key
242
     */
243
    @Input()
244
    set thyGroupBy(value: string) {
245
        this.groupBy = value;
246
    }
×
247

×
248
    /**
249
     * 设置每行数据的唯一标识属性名
250
     * @default _id
251
     */
×
252
    @Input()
×
253
    set thyRowKey(value: SafeAny) {
254
        this.rowKey = value || this.rowKey;
255
    }
256

×
257
    /**
×
258
     * 分组数据源
×
259
     */
260
    @Input()
261
    set thyGroups(value: SafeAny) {
262
        if (this.mode === 'group') {
263
            this.buildGroups(value);
×
264
        }
×
265
    }
266

267
    /**
268
     * 数据源
×
269
     */
×
270
    @Input()
×
271
    set thyModel(value: SafeAny) {
×
272
        this.model = value || [];
273
        this._diff = this._differs.find(this.model).create();
274
        this._initializeDataModel();
275

276
        if (this.mode === 'group') {
×
277
            this.buildModel();
×
278
        }
×
279
    }
280

×
281
    /**
×
282
     * 表格的显示风格,`bordered` 时头部有背景色且分割线区别明显
×
283
     * @type default | bordered | boxed
284
     * @default default
×
285
     */
×
286
    @Input()
287
    set thyTheme(value: ThyTableTheme) {
×
288
        this.theme = value || this.theme;
289
        this._setClass();
290
    }
×
291

×
292
    /**
×
293
     * 表格的大小
294
     * @type md | sm | xs | lg | xlg | default
295
     * @default md
296
     */
×
297
    @Input()
298
    set thySize(value: ThyTableSize) {
299
        this.size = value || this.size;
×
300
        this._setClass();
301
    }
302

×
303
    /**
×
304
     * 设置表格最小宽度,一般是适用于设置列宽为百分之或auto时限制表格最小宽度'
305
     */
×
306
    @Input()
×
307
    @InputCssPixel()
308
    thyMinWidth: string | number;
309

×
310
    /**
311
     * 设置为 fixed 布局表格,设置 fixed 后,列宽将严格按照设置宽度展示,列宽将不会根据表格内容自动调整
312
     * @default false
313
     */
×
314
    @Input() @InputBoolean() thyLayoutFixed: boolean;
×
315

316
    /**
317
     * 是否表头固定,若设置为 true, 需要同步设置 thyHeight
318
     * @default false
×
319
     */
×
320
    @Input() @InputBoolean() thyHeaderFixed: boolean;
321

322
    /**
323
     * 表格的高度
×
324
     */
325
    @HostBinding('style.height')
326
    @Input()
×
327
    @InputCssPixel()
328
    thyHeight: string;
329

×
330
    /**
331
     * 设置表格的样式
332
     */
×
333
    @Input()
×
334
    set thyClassName(value: string) {
335
        const list = this.className.split(' ').filter(a => a.trim());
336
        const index: number = list.findIndex(item => item === this._oldThyClassName);
×
337
        if (index !== -1) {
×
338
            list.splice(index, 1, value);
339
        } else {
×
340
            list.push(value);
341
        }
342
        this._oldThyClassName = value;
343
        this.className = list.join(' ');
344
    }
×
345

346
    /**
347
     * 设置表格行的样式,传入函数,支持 row、index
×
348
     * @type string | (row, index) => string
349
     */
350
    @Input()
351
    set thyRowClassName(value: string | Function) {
×
352
        this.rowClassName = value;
353
    }
354

×
355
    /**
356
     * 设置加载状态
357
     * @default true
358
     */
×
359
    @Input()
×
360
    @InputBoolean()
×
361
    set thyLoadingDone(value: boolean) {
362
        this.loadingDone = value;
363
    }
364

×
365
    /**
366
     * 设置加载时显示的文本
367
     */
×
368
    @Input()
369
    set thyLoadingText(value: string) {
370
        this.loadingText = value;
×
371
    }
372

373
    /**
×
374
     * 配置空状态组件
×
375
     */
376
    @Input()
377
    set thyEmptyOptions(value: ThyTableEmptyOptions) {
378
        this.emptyOptions = value;
×
379
    }
380

381
    /**
382
     * 是否开启行拖拽
383
     * @default false
384
     */
×
385
    @Input()
×
386
    @InputBoolean()
387
    set thyDraggable(value: boolean) {
388
        this.draggable = coerceBooleanProperty(value);
×
389
        if ((typeof ngDevMode === 'undefined' || ngDevMode) && this.draggable && this.mode === 'tree') {
390
            throw new Error('Tree mode sorting is not supported');
391
        }
392
    }
×
393

×
394
    /**
×
395
     * 设置当前页码
×
396
     * @default 1
×
397
     */
398
    @Input()
399
    @InputNumber()
×
400
    set thyPageIndex(value: number) {
×
401
        this.pagination.index = value;
×
402
    }
403

404
    /**
×
405
     * 设置每页显示数量
×
406
     * @default 20
407
     */
×
408
    @Input()
409
    @InputNumber()
×
410
    set thyPageSize(value: number) {
411
        this.pagination.size = value;
412
    }
413

414
    /**
415
     * 设置总页数
×
416
     */
×
417
    @Input()
418
    @InputNumber()
419
    set thyPageTotal(value: number) {
420
        this.pagination.total = value;
×
421
    }
×
422

423
    /**
×
424
     * 选中当前行是否自动选中 Checkbox,不开启时只有点击 Checkbox 列时才会触发选中
×
425
     * @default false
426
     */
×
427
    @Input()
428
    @InputBoolean()
429
    set thyWholeRowSelect(value: boolean) {
430
        if (value) {
431
            this.className += ' table-hover';
432
        }
×
433
        this.wholeRowSelect = value;
×
434
    }
435

436
    /**
437
     * 是否显示表格头
×
438
     */
×
439
    @Input() @InputBoolean() thyShowHeader = true;
×
440

×
441
    /**
×
442
     * 是否显示左侧 Total
×
443
     */
444
    @Input('thyShowTotal') @InputBoolean() showTotal = false;
445

446
    /**
447
     * 是否显示调整每页显示条数下拉框
448
     */
×
449
    @Input('thyShowSizeChanger') @InputBoolean() showSizeChanger = false;
450

451
    /**
452
     * 每页显示条数下拉框可选项
453
     * @type number[]
454
     */
×
455
    @Input('thyPageSizeOptions')
×
456
    set pageSizeOptions(value: number[]) {
457
        this.pagination.sizeOptions = value;
458
    }
×
459

×
460
    /**
461
     * thyMode 为 tree 时,设置 Tree 树状数据展示时的缩进
×
462
     */
×
463
    @Input() @InputNumber() thyIndent = 20;
464

465
    /**
466
     * thyMode 为 tree 时,设置 Tree 树状数据对象中的子节点 Key
×
467
     * @type string
×
468
     */
469
    @Input() thyChildrenKey = 'children';
×
470

×
471
    /**
472
     * 开启 Hover 后显示操作,默认不显示操作区内容,鼠标 Hover 时展示
×
473
     * @default false
×
474
     */
475
    @HostBinding('class.thy-table-hover-display-operation')
476
    @Input()
×
477
    @InputBoolean()
478
    thyHoverDisplayOperation: boolean;
×
479

×
480
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
×
481

×
482
    /**
483
     * 切换组件回调事件
484
     */
485
    @Output() thyOnSwitchChange: EventEmitter<ThySwitchEvent> = new EventEmitter<ThySwitchEvent>();
×
486

×
487
    /**
×
488
     * 表格分页回调事件
×
489
     */
×
490
    @Output() thyOnPageChange: EventEmitter<PageChangedEvent> = new EventEmitter<PageChangedEvent>();
491

×
492
    /**
×
493
     * 表格分页当前页改变回调事件
×
494
     */
×
495
    @Output() thyOnPageIndexChange: EventEmitter<number> = new EventEmitter<number>();
×
496

497
    @Output() thyOnPageSizeChange: EventEmitter<number> = new EventEmitter<number>();
×
498

×
499
    /**
×
500
     * 多选回调事件
501
     */
502
    @Output() thyOnMultiSelectChange: EventEmitter<ThyMultiSelectEvent> = new EventEmitter<ThyMultiSelectEvent>();
503

×
504
    /**
505
     * 单选回调事件
506
     */
507
    @Output() thyOnRadioSelectChange: EventEmitter<ThyRadioSelectEvent> = new EventEmitter<ThyRadioSelectEvent>();
×
508

509
    /**
510
     * 拖动修改事件
511
     */
×
512
    @Output() thyOnDraggableChange: EventEmitter<ThyTableDraggableEvent> = new EventEmitter<ThyTableDraggableEvent>();
×
513

×
514
    /**
515
     * 表格行点击触发事件
×
516
     */
517
    @Output() thyOnRowClick: EventEmitter<ThyTableRowEvent> = new EventEmitter<ThyTableRowEvent>();
518

×
519
    /**
520
     * 列排序修改事件
521
     */
522
    @Output() thySortChange: EventEmitter<ThyTableSortEvent> = new EventEmitter<ThyTableSortEvent>();
×
523

524
    @Output() thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
525

×
526
    @ContentChild('group', { static: true }) groupTemplate: TemplateRef<SafeAny>;
×
527

×
528
    @ContentChildren(ThyTableColumnComponent)
529
    set listOfColumnComponents(components: QueryList<ThyTableColumnComponent>) {
530
        if (components) {
531
            this.columns = components.toArray();
532
            this.hasFixed = !!this.columns.find(item => {
×
533
                return item.fixed === this.fixedDirection.left || item.fixed === this.fixedDirection.right;
×
534
            });
535
            this._initializeColumns();
536
            this._initializeDataModel();
537
        }
×
538
    }
×
539

×
540
    // 数据的折叠展开状态
×
541
    public expandStatusMap: Dictionary<boolean> = {};
×
542

×
543
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
544

545
    private expandStatusMapOfGroupBeforeDrag: Dictionary<boolean> = {};
×
546

547
    dragPreviewClass = 'thy-table-drag-preview';
×
548

549
    constructor(
550
        public elementRef: ElementRef,
551
        private _differs: IterableDiffers,
×
552
        private viewportRuler: ViewportRuler,
×
553
        private updateHostClassService: UpdateHostClassService,
×
554
        @Inject(DOCUMENT) private document: SafeAny,
×
555
        @Inject(PLATFORM_ID) private platformId: string,
×
556
        private ngZone: NgZone,
557
        private renderer: Renderer2,
558
        private cdr: ChangeDetectorRef
559
    ) {
560
        super();
×
561
        this._bindTrackFn();
×
562
    }
563

564
    private _initializeColumns() {
×
565
        if (!this.columns.some(item => item.expand === true) && this.columns.length > 0) {
×
566
            this.columns[0].expand = true;
567
        }
568
        this._initializeColumnFixedPositions();
569
    }
×
570

×
571
    private _initializeColumnFixedPositions() {
572
        const leftFixedColumns = this.columns.filter(item => item.fixed === ThyFixedDirection.left);
573
        leftFixedColumns.forEach((item, index) => {
574
            const previous = leftFixedColumns[index - 1];
×
575
            item.left = previous ? previous.left + parseInt(previous.width.toString(), 10) : 0;
×
576
        });
×
577
        const rightFixedColumns = this.columns.filter(item => item.fixed === ThyFixedDirection.right).reverse();
×
578
        rightFixedColumns.forEach((item, index) => {
×
579
            const previous = rightFixedColumns[index - 1];
×
580
            item.right = previous ? previous.right + parseInt(previous.width.toString(), 10) : 0;
×
581
        });
×
582
    }
583

×
584
    private _initializeDataModel() {
×
585
        this.model.forEach(row => {
586
            this.columns.forEach(column => {
587
                this._initialSelections(row, column);
×
588
                this._initialCustomModelValue(row, column);
589
            });
590
        });
×
591
    }
×
592

593
    private _initialSelections(row: object, column: ThyTableColumnComponent) {
×
594
        if (column.selections) {
×
595
            if (column.type === 'checkbox') {
596
                row[column.key] = column.selections.includes(row[this.rowKey]);
597
                this.onModelChange(row, column);
598
            }
×
599
            if (column.type === 'radio') {
×
600
                if (column.selections.includes(row[this.rowKey])) {
×
601
                    this.selectedRadioRow = row;
×
602
                }
603
            }
604
        }
×
605
    }
×
606

×
607
    private _initialCustomModelValue(row: object, column: ThyTableColumnComponent) {
608
        if (column.type === customType.switch) {
×
609
            row[column.key] = get(row, column.model);
×
610
        }
×
611
    }
612

613
    private _refreshCustomModelValue(row: SafeAny) {
614
        this.columns.forEach(column => {
615
            this._initialCustomModelValue(row, column);
×
616
        });
×
617
    }
618

×
619
    private _applyDiffChanges(changes: IterableChanges<SafeAny>) {
×
620
        if (changes) {
621
            changes.forEachAddedItem((record: IterableChangeRecord<SafeAny>) => {
622
                this._refreshCustomModelValue(record.item);
623
            });
624
        }
625
    }
626

627
    private _bindTrackFn() {
×
628
        this.trackByFn = function (this: SafeAny, index: number, row: SafeAny): SafeAny {
629
            return row && this.rowKey ? row[this.rowKey] : index;
×
630
        }.bind(this);
631
    }
632

633
    private _destroyInvalidAttribute() {
634
        this.model.forEach(row => {
635
            for (const key in row) {
636
                if (key.includes('[$$column]')) {
637
                    delete row[key];
×
638
                }
×
639
            }
640
        });
641
    }
642

643
    private _setClass(first = false) {
×
644
        if (!first && !this.initialized) {
×
645
            return;
×
646
        }
×
647
        const classNames: string[] = [];
×
648
        if (this.size) {
×
649
            classNames.push(`table-${this.size}`);
650
        }
×
651
        if (tableThemeMap[this.theme]) {
×
652
            classNames.push(tableThemeMap[this.theme]);
×
653
        }
654

655
        this.updateHostClassService.updateClass(classNames);
656
    }
×
657

×
658
    public updateColumnSelections(key: string, selections: SafeAny): void {
659
        const column = this.columns.find(item => item.key === key);
1✔
660
        this.model.forEach(row => {
661
            this._initialSelections(row, column);
662
        });
663
    }
664

665
    public isTemplateRef(ref: SafeAny) {
666
        return ref instanceof TemplateRef;
667
    }
668

669
    public getModelValue(row: SafeAny, path: string) {
670
        return get(row, path);
1✔
671
    }
672

673
    public renderRowClassName(row: SafeAny, index: number) {
674
        if (!this.rowClassName) {
675
            return null;
676
        }
677
        if (isString(this.rowClassName)) {
678
            return this.rowClassName;
679
        } else {
680
            return (this.rowClassName as Function)(row, index);
681
        }
682
    }
683

684
    public onModelChange(row: SafeAny, column: ThyTableColumnComponent) {
685
        if (column.model) {
686
            set(row, column.model, row[column.key]);
687
        }
688
    }
689

690
    public onStopPropagation(event: Event) {
691
        if (this.wholeRowSelect) {
692
            event.stopPropagation();
693
        }
694
    }
695

696
    public onPageChange(event: PageChangedEvent) {
697
        this.thyOnPageChange.emit(event);
698
    }
699

700
    public onPageIndexChange(event: number) {
701
        this.thyOnPageIndexChange.emit(event);
702
    }
703

704
    public onPageSizeChange(event: number) {
705
        this.thyOnPageSizeChange.emit(event);
706
    }
707

708
    public onCheckboxChange(row: SafeAny, column: ThyTableColumnComponent) {
709
        this.onModelChange(row, column);
710
        this.onMultiSelectChange(null, row, column);
711
    }
712

713
    public onMultiSelectChange(event: Event, row: SafeAny, column: ThyTableColumnComponent) {
714
        const rows = this.model.filter(item => {
715
            return item[column.key];
716
        });
717
        const multiSelectEvent: ThyMultiSelectEvent = {
1✔
718
            event: event,
719
            row: row,
720
            rows: rows
721
        };
1✔
722
        this.thyOnMultiSelectChange.emit(multiSelectEvent);
723
    }
724

725
    public onRadioSelectChange(event: Event, row: SafeAny) {
1✔
726
        const radioSelectEvent: ThyRadioSelectEvent = {
727
            event: event,
728
            row: row
729
        };
1✔
730
        this.thyOnRadioSelectChange.emit(radioSelectEvent);
731
    }
732

733
    public onSwitchChange(event: Event, row: SafeAny, column: SafeAny) {
1✔
734
        const switchEvent: ThySwitchEvent = {
735
            event: event,
736
            row: row,
737
            refresh: (value: SafeAny) => {
738
                value = value || row;
1✔
739
                setTimeout(() => {
740
                    value[column.key] = get(value, column.model);
741
                });
742
            }
743
        };
1✔
744
        this.thyOnSwitchChange.emit(switchEvent);
745
    }
746

747
    showExpand(row: SafeAny) {
748
        return row[this.thyChildrenKey] && row[this.thyChildrenKey].length > 0;
1✔
749
    }
750

751
    isExpanded(row: SafeAny) {
752
        return this.expandStatusMap[row[this.rowKey]];
753
    }
1✔
754

755
    iconIndentComputed(level: number) {
756
        if (this.mode === 'tree') {
757
            return level * this.thyIndent - 5;
758
        }
1✔
759
    }
760

761
    tdIndentComputed(level: number) {
762
        return {
763
            position: 'relative',
1✔
764
            paddingLeft: `${(level + 1) * this.thyIndent - 5}px`
765
        };
766
    }
767

1✔
768
    expandChildren(row: SafeAny) {
769
        if (this.isExpanded(row)) {
770
            this.expandStatusMap[row[this.rowKey]] = false;
771
        } else {
1✔
772
            this.expandStatusMap[row[this.rowKey]] = true;
773
        }
774
    }
775

1✔
776
    onDragGroupStarted(event: CdkDragStart<unknown>) {
777
        this.expandStatusMapOfGroupBeforeDrag = { ...this.expandStatusMapOfGroup };
778
        const groups = this.groups.filter(group => group.expand);
779
        this.foldGroups(groups);
1✔
780
        this.onDragStarted(event);
781
        this.cdr.detectChanges();
782
    }
783

1✔
784
    onDragGroupEnd(event: CdkDragEnd<unknown>) {
785
        const groups = this.groups.filter(group => this.expandStatusMapOfGroupBeforeDrag[group.id]);
786
        this.expandGroups(groups);
787
        this.cdr.detectChanges();
788
    }
789

790
    private onDragGroupDropped(event: CdkDragDrop<unknown>) {
791
        const group = this.groups.find(group => {
792
            return event.item.data.id === group.id;
793
        });
794
        if (group) {
795
            // drag group
796
            const dragEvent: ThyTableDraggableEvent = {
797
                model: event.item,
798
                models: this.groups,
799
                oldIndex: event.previousIndex,
800
                newIndex: event.currentIndex
801
            };
802
            moveItemInArray(this.groups, event.previousIndex, event.currentIndex);
803
            this.thyOnDraggableChange.emit(dragEvent);
804
        } else {
805
            // drag group children
806
            const group = this.groups.find(group => {
807
                return event.item.data[this.groupBy] === group.id;
808
            });
809
            const groupIndex =
810
                event.container.getSortedItems().findIndex(item => {
811
                    return item.data.id === event.item.data[this.groupBy];
812
                }) + 1;
813
            const dragEvent: ThyTableDraggableEvent = {
814
                model: event.item,
815
                models: group.children,
816
                oldIndex: event.previousIndex - groupIndex,
817
                newIndex: event.currentIndex - groupIndex
818
            };
819
            moveItemInArray(group.children, dragEvent.oldIndex, dragEvent.newIndex);
820
            this.thyOnDraggableChange.emit(dragEvent);
821
        }
822
    }
823

824
    onDragStarted(event: CdkDragStart<unknown>) {
825
        this.ngZone.runOutsideAngular(() =>
826
            setTimeout(() => {
827
                const preview = this.document.getElementsByClassName(this.dragPreviewClass)[0];
828
                const originalTds: HTMLCollection = event.source._dragRef.getPlaceholderElement()?.children;
829
                if (preview) {
830
                    Array.from(preview?.children).forEach((element: HTMLElement, index: number) => {
831
                        element.style.width = `${originalTds[index]?.clientWidth}px`;
832
                    });
833
                }
834
            })
835
        );
836
    }
837

838
    dropListEnterPredicate = (index: number, drag: CdkDrag, drop: CdkDropList) => {
839
        return drop.getSortedItems()[index].data.group_id === drag.data.group_id;
840
    };
841

842
    private onDragModelDropped(event: CdkDragDrop<unknown>) {
843
        const dragEvent: ThyTableDraggableEvent = {
844
            model: event.item,
845
            models: this.model,
846
            oldIndex: event.previousIndex,
847
            newIndex: event.currentIndex
848
        };
849
        moveItemInArray(this.model, event.previousIndex, event.currentIndex);
850
        this.thyOnDraggableChange.emit(dragEvent);
851
    }
852

853
    onDragDropped(event: CdkDragDrop<unknown>) {
854
        if (this.mode === 'group') {
855
            this.onDragGroupDropped(event);
856
        } else if (this.mode === 'list') {
857
            this.onDragModelDropped(event);
858
        }
859
    }
860

861
    onColumnHeaderClick(event: Event, column: ThyTableColumnComponent) {
862
        if (column.sortable) {
863
            const { sortDirection, model, sortChange } = column;
864
            let direction;
865
            if (sortDirection === ThyTableSortDirection.default) {
866
                direction = ThyTableSortDirection.asc;
867
            } else if (sortDirection === ThyTableSortDirection.asc) {
868
                direction = ThyTableSortDirection.desc;
869
            } else {
870
                direction = ThyTableSortDirection.default;
871
            }
872
            column.sortDirection = direction;
873
            const sortEvent = { event, key: model, direction };
874
            sortChange.emit(sortEvent);
875
            this.thySortChange.emit(sortEvent);
876
        }
877
    }
878

879
    public onRowClick(event: Event, row: SafeAny) {
880
        const next = this.onRowClickPropagationEventHandler(event, row);
881
        if (next) {
882
            if (this.wholeRowSelect) {
883
                const column = this.columns.find(item => {
884
                    return item.type === customType.checkbox || item.type === customType.radio;
885
                });
886
                if (column && !column.disabled) {
887
                    if (column.type === customType.checkbox) {
888
                        row[column.key] = !row[column.key];
889
                        this.onModelChange(row, column);
890
                        this.onMultiSelectChange(event, row, column);
891
                    }
892
                    if (column.type === customType.radio) {
893
                        this.selectedRadioRow = row;
894
                        this.onRadioSelectChange(event, row);
895
                    }
896
                }
897
            }
898
            const rowEvent = {
899
                event: event,
900
                row: row
901
            };
902
            this.thyOnRowClick.emit(rowEvent);
903
        }
904
    }
905

906
    private onRowClickPropagationEventHandler(event: Event, row: SafeAny): boolean {
907
        if ((event.target as Element).closest('.tree-expand-icon')) {
908
            this.expandChildren(row);
909
            return false;
910
        }
911
        return true;
912
    }
913

914
    public onRowContextMenu(event: Event, row: SafeAny) {
915
        const contextMenuEvent: ThyTableEvent = {
916
            event: event,
917
            row: row
918
        };
919
        this.thyOnRowContextMenu.emit(contextMenuEvent);
920
    }
921

922
    private _refreshColumns() {
923
        const components = this.columns || [];
924
        const _columns = components.map(component => {
925
            return {
926
                width: component.width,
927
                className: component.className
928
            };
929
        });
930

931
        this.columns.forEach((n, i) => {
932
            Object.assign(n, _columns[i]);
933
        });
934
    }
935

936
    private buildGroups(originGroups: SafeAny) {
937
        const originGroupsMap = helpers.keyBy(originGroups, 'id');
938
        this.groups = [];
939
        originGroups.forEach((origin: SafeAny) => {
940
            const group: ThyTableGroup = { id: origin[this.rowKey], children: [], origin };
941

942
            if (this.expandStatusMapOfGroup.hasOwnProperty(group.id)) {
943
                group.expand = this.expandStatusMapOfGroup[group.id];
944
            } else {
945
                group.expand = !!(originGroupsMap[group.id] as SafeAny).expand;
946
            }
947

948
            this.groups.push(group);
949
        });
950
    }
951

952
    private buildModel() {
953
        const groupsMap = keyBy(this.groups, 'id');
954
        this.model.forEach(row => {
955
            const group = groupsMap[row[this.groupBy]];
956
            if (group) {
957
                group.children.push(row);
958
            }
959
        });
960
    }
961

962
    public expandGroup(group: ThyTableGroup) {
963
        group.expand = !group.expand;
964
        this.expandStatusMapOfGroup[group.id] = group.expand;
965
    }
966

967
    private expandGroups(groups: ThyTableGroup[]) {
968
        groups.forEach(group => {
969
            this.expandGroup(group);
970
        });
971
    }
972

973
    private foldGroups(groups: ThyTableGroup[]) {
974
        groups.forEach(group => {
975
            this.expandGroup(group);
976
        });
977
    }
978

979
    private updateScrollClass() {
980
        const scrollElement = this.tableScrollElement;
981
        const maxScrollLeft = scrollElement.scrollWidth - scrollElement.offsetWidth;
982
        const scrollX = scrollElement.scrollLeft;
983
        const lastScrollClassName = this.scrollClassName;
984
        this.scrollClassName = '';
985
        if (scrollElement.scrollWidth > scrollElement.clientWidth) {
986
            if (scrollX >= maxScrollLeft) {
987
                this.scrollClassName = css.tableScrollRight;
988
            } else if (scrollX === 0) {
989
                this.scrollClassName = css.tableScrollLeft;
990
            } else {
991
                this.scrollClassName = css.tableScrollMiddle;
992
            }
993
        }
994
        if (lastScrollClassName) {
995
            this.renderer.removeClass(this.tableScrollElement, lastScrollClassName);
996
        }
997
        if (this.scrollClassName) {
998
            this.renderer.addClass(this.tableScrollElement, this.scrollClassName);
999
        }
1000
    }
1001

1002
    ngOnInit() {
1003
        this.updateHostClassService.initializeElement(this.tableElementRef.nativeElement);
1004
        this._setClass(true);
1005
        this.initialized = true;
1006

1007
        merge(this.viewportRuler.change(200), of(null).pipe(delay(200)))
1008
            .pipe(takeUntil(this.ngUnsubscribe$))
1009
            .subscribe(() => {
1010
                this._refreshColumns();
1011
                this.updateScrollClass();
1012
                this.cdr.detectChanges();
1013
            });
1014

1015
        this.ngZone.runOutsideAngular(() => {
1016
            this.scroll$.pipe(takeUntil(this.ngUnsubscribe$)).subscribe(() => {
1017
                this.updateScrollClass();
1018
            });
1019
        });
1020
    }
1021

1022
    ngAfterViewInit(): void {
1023
        if (isPlatformServer(this.platformId)) {
1024
            return;
1025
        }
1026

1027
        this.rows.changes
1028
            .pipe(
1029
                startWith(this.rows),
1030
                switchMap(
1031
                    () =>
1032
                        new Observable<Event>(subscriber =>
1033
                            this.ngZone.runOutsideAngular(() =>
1034
                                merge(
1035
                                    ...this.rows.map(row =>
1036
                                        fromEvent(
1037
                                            row.nativeElement,
1038
                                            // Note: there's no need to add touch, pointer and mouse event listeners together.
1039
                                            // There can be any number of rows, which will lead to adding N * 3 event listeners.
1040
                                            // According to the spec (https://www.w3.org/TR/pointerevents/#examples), we can use feature detection
1041
                                            // to determine if pointer events are available. If pointer events are available, we have to listen only
1042
                                            // to the `pointerdown` event. Otherwise, we have to determine if we're on a touch device or not.
1043
                                            // Touch events are handled earlier than mouse events, tho not all user agents dispatch mouse events
1044
                                            // after touch events. See the spec: https://www.w3.org/TR/touch-events/#mouse-events.
1045
                                            window.PointerEvent
1046
                                                ? 'pointerdown'
1047
                                                : 'ontouchstart' in row.nativeElement
1048
                                                ? 'touchstart'
1049
                                                : 'mousedown',
1050
                                            // Note: since Chrome 56 defaults document level `touchstart` listener to passive.
1051
                                            // The element `touchstart` listener is not passive by default
1052
                                            // We never call `preventDefault()` on it, so we're safe making it passive too.
1053
                                            <AddEventListenerOptions>passiveEventListenerOptions
1054
                                        )
1055
                                    )
1056
                                ).subscribe(subscriber)
1057
                            )
1058
                        )
1059
                ),
1060
                takeUntil(this.ngUnsubscribe$)
1061
            )
1062
            .subscribe(event => {
1063
                if (!this.draggable) {
1064
                    event.stopPropagation();
1065
                }
1066
            });
1067
    }
1068

1069
    ngOnChanges(simpleChanges: SimpleChanges) {
1070
        const modeChange = simpleChanges.thyMode;
1071
        const thyGroupsChange = simpleChanges.thyGroups;
1072
        const isGroupMode = modeChange && modeChange.currentValue === 'group';
1073
        if (isGroupMode && thyGroupsChange && thyGroupsChange.firstChange) {
1074
            this.buildGroups(thyGroupsChange.currentValue);
1075
            this.buildModel();
1076
        }
1077

1078
        if (this._diff) {
1079
            const changes = this._diff.diff(this.model);
1080
            this._applyDiffChanges(changes);
1081
        }
1082
    }
1083

1084
    ngOnDestroy() {
1085
        super.ngOnDestroy();
1086
        this._destroyInvalidAttribute();
1087
    }
1088
}
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