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

atinc / ngx-tethys / 8a6ba229-c82f-4a21-a1ed-95461f2ad66c

04 Sep 2023 08:37AM UTC coverage: 90.196% (-0.004%) from 90.2%
8a6ba229-c82f-4a21-a1ed-95461f2ad66c

Pull #2829

circleci

cmm-va
fix: delete f
Pull Request #2829: fix: add tabIndex

5164 of 6386 branches covered (0.0%)

Branch coverage included in aggregate %.

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

13024 of 13779 relevant lines covered (94.52%)

971.69 hits per line

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

87.25
/src/table/table.component.ts
1
import { InputBoolean, InputCssPixel, InputNumber, UnsubscribeMixin, UpdateHostClassService } from 'ngx-tethys/core';
2
import { Dictionary, SafeAny } from 'ngx-tethys/types';
3
import { coerceBooleanProperty, get, helpers, isString, keyBy, set } from 'ngx-tethys/util';
4
import { EMPTY, fromEvent, merge, Observable, of } from 'rxjs';
5
import { delay, startWith, switchMap, takeUntil } from 'rxjs/operators';
6

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

41
import { IThyTableColumnParentComponent, THY_TABLE_COLUMN_PARENT_COMPONENT, ThyTableColumnComponent } from './table-column.component';
42
import {
1✔
43
    PageChangedEvent,
44
    ThyMultiSelectEvent,
45
    ThyPage,
46
    ThyRadioSelectEvent,
47
    ThySwitchEvent,
48
    ThyTableSkeletonColumn,
1✔
49
    ThyTableDraggableEvent,
50
    ThyTableEmptyOptions,
51
    ThyTableEvent,
52
    ThyTableRowEvent,
53
    ThyTableSortDirection,
54
    ThyTableSortEvent
1✔
55
} from './table.interface';
56
import { TableRowDragDisabledPipe } from './pipes/drag.pipe';
148✔
57
import { TableIsValidModelValuePipe } from './pipes/table.pipe';
58
import { ThyPaginationComponent } from 'ngx-tethys/pagination';
59
import { ThyTableSkeletonComponent } from './table-skeleton.component';
66!
60
import { ThyEmptyComponent } from 'ngx-tethys/empty';
61
import { ThySwitchComponent } from 'ngx-tethys/switch';
62
import { FormsModule } from '@angular/forms';
60!
63
import { ThyDragDropDirective, ThyContextMenuDirective } from 'ngx-tethys/shared';
64
import { ThyIconComponent } from 'ngx-tethys/icon';
65
import { CdkScrollable } from '@angular/cdk/scrolling';
57✔
66
import { ThyTableColumnSkeletonType } from './enums';
67

68
export type ThyTableTheme = 'default' | 'bordered' | 'boxed';
70!
69

70
export type ThyTableMode = 'list' | 'group' | 'tree';
71

56✔
72
export type ThyTableSize = 'md' | 'sm' | 'xs' | 'lg' | 'xlg' | 'default';
3✔
73

74
export enum ThyFixedDirection {
75
    left = 'left',
76
    right = 'right'
67✔
77
}
67✔
78

67✔
79
interface ThyTableGroup<T = unknown> {
67✔
80
    id?: string;
1✔
81
    expand?: boolean;
82
    children?: object[];
83
    origin?: T;
84
}
45!
85

45✔
86
const tableThemeMap = {
87
    default: 'table-default',
88
    bordered: 'table-bordered',
50✔
89
    boxed: 'table-boxed'
50✔
90
};
91

92
const customType = {
48✔
93
    index: 'index',
45✔
94
    checkbox: 'checkbox',
45!
95
    radio: 'radio',
×
96
    switch: 'switch'
97
};
98

45✔
99
const css = {
100
    tableBody: 'thy-table-body',
45✔
101
    tableScrollLeft: 'thy-table-scroll-left',
45✔
102
    tableScrollRight: 'thy-table-scroll-right',
103
    tableScrollMiddle: 'thy-table-scroll-middle'
104
};
45✔
105

106
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
107

45✔
108
/**
109
 * 表格组件
110
 * @name thy-table
45✔
111
 * @order 10
112
 */
113
@Component({
44✔
114
    selector: 'thy-table',
115
    templateUrl: './table.component.html',
116
    providers: [
64✔
117
        {
64✔
118
            provide: THY_TABLE_COLUMN_PARENT_COMPONENT,
1✔
119
            useExisting: ThyTableComponent
120
        },
121
        UpdateHostClassService
122
    ],
55✔
123
    encapsulation: ViewEncapsulation.None,
124
    host: {
125
        class: 'thy-table',
55✔
126
        '[class.thy-table-bordered]': `theme === 'bordered'`,
127
        '[class.thy-table-boxed]': `theme === 'boxed'`,
128
        '[class.thy-table-fixed-header]': 'thyHeaderFixed'
55✔
129
    },
130
    standalone: true,
131
    imports: [
45✔
132
        CdkScrollable,
3✔
133
        NgClass,
134
        NgFor,
45✔
135
        NgIf,
136
        NgTemplateOutlet,
137
        ThyIconComponent,
1✔
138
        ThyDragDropDirective,
139
        CdkDropList,
140
        CdkDrag,
44✔
141
        ThyContextMenuDirective,
142
        NgStyle,
143
        FormsModule,
66!
144
        ThySwitchComponent,
66✔
145
        ThyEmptyComponent,
66✔
146
        ThyTableSkeletonComponent,
367✔
147
        ThyPaginationComponent,
148
        TableIsValidModelValuePipe,
66✔
149
        TableRowDragDisabledPipe
66✔
150
    ]
151
})
152
export class ThyTableComponent
153
    extends UnsubscribeMixin
70✔
154
    implements OnInit, OnChanges, AfterViewInit, OnDestroy, IThyTableColumnParentComponent
70✔
155
{
70✔
156
    public customType = customType;
70✔
157

70✔
158
    public model: object[] = [];
70✔
159

70✔
160
    public groups: ThyTableGroup[] = [];
70✔
161

70✔
162
    public rowKey = '_id';
70✔
163

70✔
164
    public groupBy: string;
70✔
165

70✔
166
    public mode: ThyTableMode = 'list';
70✔
167

70✔
168
    public theme: ThyTableTheme = 'default';
70✔
169

70✔
170
    public className = '';
70✔
171

70✔
172
    public size: ThyTableSize = 'md';
70✔
173

70✔
174
    public rowClassName: string | Function;
70✔
175

70✔
176
    public loadingDone = true;
70✔
177

70✔
178
    public loadingText: string;
70✔
179

70✔
180
    public emptyOptions: ThyTableEmptyOptions = {};
70✔
181

70✔
182
    public draggable = false;
70✔
183

70✔
184
    public selectedRadioRow: SafeAny = null;
70✔
185

70✔
186
    public pagination: ThyPage = { index: 1, size: 20, total: 0, sizeOptions: [20, 50, 100] };
70✔
187

70✔
188
    public trackByFn: SafeAny;
384✔
189

70✔
190
    public wholeRowSelect = false;
191

192
    public fixedDirection = ThyFixedDirection;
193

194
    public hasFixed = false;
70✔
195

70✔
196
    public columns: ThyTableColumnComponent[] = [];
70✔
197

70✔
198
    private _diff: IterableDiffer<SafeAny>;
70✔
199

70✔
200
    private initialized = false;
70✔
201

70✔
202
    private _oldThyClassName = '';
70✔
203

70✔
204
    private scrollClassName = css.tableScrollLeft;
205

70✔
206
    private get tableScrollElement(): HTMLElement {
70✔
207
        return this.elementRef.nativeElement.getElementsByClassName(css.tableBody)[0] as HTMLElement;
70✔
208
    }
70✔
209

70✔
210
    private get scroll$() {
70✔
211
        return merge(this.tableScrollElement ? fromEvent<MouseEvent>(this.tableScrollElement, 'scroll') : EMPTY);
×
212
    }
213

70✔
214
    /**
215
     * 设置数据为空时展示的模板
216
     * @type TemplateRef
374✔
217
     */
65✔
218
    @ContentChild('empty') emptyTemplate: TemplateRef<SafeAny>;
219

66✔
220
    @ViewChild('table', { static: true }) tableElementRef: ElementRef<SafeAny>;
221

222
    @ViewChildren('rows', { read: ElementRef }) rows: QueryList<ElementRef<HTMLElement>>;
377✔
223

66✔
224
    /**
4✔
225
     * 表格展示方式,列表/分组/树
4✔
226
     * @type list | group | tree
227
     * @default list
377✔
228
     */
66✔
229
    @Input()
1✔
230
    set thyMode(value: ThyTableMode) {
1!
231
        this.mode = value || this.mode;
232
    }
233

234
    /**
133✔
235
     * thyMode的值为 `group` 时分组的 Key
731✔
236
     */
2,097✔
237
    @Input()
2,097✔
238
    set thyGroupBy(value: string) {
239
        this.groupBy = value;
240
    }
241

242
    /**
2,097✔
243
     * 设置每行数据的唯一标识属性名
504✔
244
     * @default _id
246✔
245
     */
246✔
246
    @Input()
247
    set thyRowKey(value: SafeAny) {
504✔
248
        this.rowKey = value || this.rowKey;
6!
249
    }
×
250

251
    /**
252
     * 分组数据源
253
     */
254
    @Input()
255
    set thyGroups(value: SafeAny) {
2,132✔
256
        if (this.mode === 'group') {
356✔
257
            this.buildGroups(value);
258
        }
259
    }
260

369✔
261
    /**
35✔
262
     * 数据源
263
     */
264
    @Input()
265
    set thyModel(value: SafeAny) {
78✔
266
        this.model = value || [];
62✔
267
        this._diff = this._differs.find(this.model).create();
369✔
268
        this._initializeDataModel();
269

270
        if (this.mode === 'group') {
271
            this.buildModel();
272
        }
70✔
273
    }
634!
274

275
    /**
276
     * 表格的显示风格,`bordered` 时头部有背景色且分割线区别明显
277
     * @type default | bordered | boxed
70✔
278
     * @default default
363✔
279
     */
2,911✔
280
    @Input()
589✔
281
    set thyTheme(value: ThyTableTheme) {
282
        this.theme = value || this.theme;
283
        this._setClass();
284
    }
285

95✔
286
    /**
161✔
287
     * 表格的大小
90✔
288
     * @type xs | sm | md | lg | xlg | default
289
     * @default md
71✔
290
     */
71!
291
    @Input()
71✔
292
    set thySize(value: ThyTableSize) {
293
        this.size = value || this.size;
71!
294
        this._setClass();
71✔
295
    }
296

71✔
297
    /**
298
     * 设置表格最小宽度,一般是适用于设置列宽为百分之或auto时限制表格最小宽度'
299
     */
×
300
    @Input()
×
301
    @InputCssPixel()
×
302
    thyMinWidth: string | number;
303

304
    /**
305
     * 设置为 fixed 布局表格,设置 fixed 后,列宽将严格按照设置宽度展示,列宽将不会根据表格内容自动调整
13,820✔
306
     * @default false
307
     */
308
    @Input() @InputBoolean() thyLayoutFixed: boolean;
6,957✔
309

310
    /**
311
     * 是否表头固定,若设置为 true, 需要同步设置 thyHeight
1,220✔
312
     * @default false
410✔
313
     */
314
    @Input() @InputBoolean() thyHeaderFixed: boolean;
810!
315

810✔
316
    /**
317
     * 表格的高度
318
     */
×
319
    @HostBinding('style.height')
320
    @Input()
321
    @InputCssPixel()
322
    thyHeight: string;
249!
323

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

340
    /**
341
     * 设置表格行的样式,传入函数,支持 row、index
1✔
342
     * @type string | (row, index) => string
1✔
343
     */
344
    @Input()
345
    set thyRowClassName(value: string | Function) {
2✔
346
        this.rowClassName = value;
12✔
347
    }
348

2✔
349
    /**
350
     * 设置加载状态
351
     * @default true
352
     */
353
    @Input()
2✔
354
    @InputBoolean()
355
    set thyLoadingDone(value: boolean) {
356
        this.loadingDone = value;
1✔
357
    }
358

359
    /**
360
     * 设置加载时显示的文本,已废弃
1✔
361
     * @deprecated
362
     */
363
    @Input()
1✔
364
    set thyLoadingText(value: string) {
365
        this.loadingText = value;
366
    }
367

×
368
    /**
×
369
     * 配置空状态组件
×
370
     */
371
    @Input()
372
    set thyEmptyOptions(value: ThyTableEmptyOptions) {
373
        this.emptyOptions = value;
1✔
374
    }
375

376
    /**
180✔
377
     * 是否开启行拖拽
378
     * @default false
379
     */
367✔
380
    @Input()
381
    @InputBoolean()
382
    set thyDraggable(value: boolean) {
180!
383
        this.draggable = coerceBooleanProperty(value);
180✔
384
        if ((typeof ngDevMode === 'undefined' || ngDevMode) && this.draggable && this.mode === 'tree') {
385
            throw new Error('Tree mode sorting is not supported');
386
        }
387
    }
180✔
388

389
    /**
390
     * 设置当前页码
391
     * @default 1
392
     */
393
    @Input()
7✔
394
    @InputNumber()
3✔
395
    set thyPageIndex(value: number) {
396
        this.pagination.index = value;
397
    }
4✔
398

399
    /**
400
     * 设置每页显示数量
401
     * @default 20
1✔
402
     */
2✔
403
    @Input()
1✔
404
    @InputNumber()
1✔
405
    set thyPageSize(value: number) {
1✔
406
        this.pagination.size = value;
407
    }
408

2✔
409
    /**
1✔
410
     * 设置总页数
1✔
411
     */
412
    @Input()
413
    @InputNumber()
2✔
414
    set thyPageTotal(value: number) {
3✔
415
        this.pagination.total = value;
416
    }
2✔
417

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

2✔
431
    /**
432
     * 是否显示表格头
1✔
433
     * @default false
2✔
434
     */
435
    @Input() @InputBoolean() thyHeadless = false;
1✔
436

437
    /**
438
     * 是否显示表格头,已废弃,请使用 thyHeadless
439
     * @deprecated please use thyHeadless
440
     */
441
    @Input()
1✔
442
    @InputBoolean()
1✔
443
    set thyShowHeader(value: boolean) {
444
        this.thyHeadless = !value;
445
    }
446

3✔
447
    /**
3✔
448
     * 是否显示左侧 Total
3✔
449
     */
3!
450
    @Input('thyShowTotal') @InputBoolean() showTotal = false;
3✔
451

12✔
452
    /**
453
     * 是否显示调整每页显示条数下拉框
454
     */
455
    @Input('thyShowSizeChanger') @InputBoolean() showSizeChanger = false;
456

457
    /**
1✔
458
     * 每页显示条数下拉框可选项
459
     * @type number[]
460
     */
461
    @Input('thyPageSizeOptions')
462
    set pageSizeOptions(value: number[]) {
463
        this.pagination.sizeOptions = value;
1✔
464
    }
1✔
465

466
    /**
467
     * thyMode 为 tree 时,设置 Tree 树状数据展示时的缩进
3✔
468
     */
2✔
469
    @Input() @InputNumber() thyIndent = 20;
470

1!
471
    /**
1✔
472
     * thyMode 为 tree 时,设置 Tree 树状数据对象中的子节点 Key
473
     * @type string
474
     */
475
    @Input() thyChildrenKey = 'children';
1!
476

1✔
477
    /**
478
     * 开启 Hover 后显示操作,默认不显示操作区内容,鼠标 Hover 时展示
1!
479
     * @default false
1✔
480
     */
481
    @HostBinding('class.thy-table-hover-display-operation')
×
482
    @Input()
×
483
    @InputBoolean()
484
    thyHoverDisplayOperation: boolean;
485

×
486
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
487

1✔
488
    /**
1✔
489
     * 表格列的骨架类型
1✔
490
     * @type ThyTableColumnSkeletonType[]
1✔
491
     */
492
    @Input() thyColumnSkeletonTypes: ThyTableColumnSkeletonType[] = [
493
        ThyTableColumnSkeletonType.title,
494
        ThyTableColumnSkeletonType.member,
11✔
495
        ThyTableColumnSkeletonType.default
11✔
496
    ];
4✔
497

2✔
498
    /**
2✔
499
     * 切换组件回调事件
500
     */
2!
501
    @Output() thyOnSwitchChange: EventEmitter<ThySwitchEvent> = new EventEmitter<ThySwitchEvent>();
2✔
502

1✔
503
    /**
1✔
504
     * 表格分页回调事件
1✔
505
     */
506
    @Output() thyOnPageChange: EventEmitter<PageChangedEvent> = new EventEmitter<PageChangedEvent>();
2✔
507

1✔
508
    /**
1✔
509
     * 表格分页当前页改变回调事件
510
     */
511
    @Output() thyOnPageIndexChange: EventEmitter<number> = new EventEmitter<number>();
512

4✔
513
    @Output() thyOnPageSizeChange: EventEmitter<number> = new EventEmitter<number>();
514

515
    /**
516
     * 多选回调事件
4✔
517
     */
518
    @Output() thyOnMultiSelectChange: EventEmitter<ThyMultiSelectEvent> = new EventEmitter<ThyMultiSelectEvent>();
519

520
    /**
11✔
521
     * 单选回调事件
7✔
522
     */
7✔
523
    @Output() thyOnRadioSelectChange: EventEmitter<ThyRadioSelectEvent> = new EventEmitter<ThyRadioSelectEvent>();
524

4✔
525
    /**
526
     * 拖动修改事件
527
     */
1✔
528
    @Output() thyOnDraggableChange: EventEmitter<ThyTableDraggableEvent> = new EventEmitter<ThyTableDraggableEvent>();
529

530
    /**
531
     * 表格行点击触发事件
1✔
532
     */
533
    @Output() thyOnRowClick: EventEmitter<ThyTableRowEvent> = new EventEmitter<ThyTableRowEvent>();
534

8!
535
    /**
8✔
536
     * 列排序修改事件
47✔
537
     */
538
    @Output() thySortChange: EventEmitter<ThyTableSortEvent> = new EventEmitter<ThyTableSortEvent>();
539

540
    @Output() thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
541

8✔
542
    @ContentChild('group', { static: true }) groupTemplate: TemplateRef<SafeAny>;
47✔
543

544
    @ContentChildren(ThyTableColumnComponent)
545
    set listOfColumnComponents(components: QueryList<ThyTableColumnComponent>) {
546
        if (components) {
14✔
547
            this.columns = components.toArray();
14✔
548
            this.hasFixed = !!this.columns.find(item => {
14✔
549
                return item.fixed === this.fixedDirection.left || item.fixed === this.fixedDirection.right;
28✔
550
            });
28!
551
            this._initializeColumns();
×
552
            this._initializeDataModel();
553
        }
554
    }
28✔
555

556
    // 数据的折叠展开状态
28✔
557
    public expandStatusMap: Dictionary<boolean> = {};
558

559
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
560

13✔
561
    private expandStatusMapOfGroupBeforeDrag: Dictionary<boolean> = {};
13✔
562

79✔
563
    dragPreviewClass = 'thy-table-drag-preview';
79✔
564

78✔
565
    public skeletonColumns: ThyTableSkeletonColumn[] = [];
566

567
    constructor(
568
        public elementRef: ElementRef,
569
        private _differs: IterableDiffers,
4✔
570
        private viewportRuler: ViewportRuler,
4✔
571
        private updateHostClassService: UpdateHostClassService,
572
        @Inject(DOCUMENT) private document: SafeAny,
573
        @Inject(PLATFORM_ID) private platformId: string,
1✔
574
        private ngZone: NgZone,
×
575
        private renderer: Renderer2,
576
        private cdr: ChangeDetectorRef
577
    ) {
578
        super();
1✔
579
        this._bindTrackFn();
1✔
580
    }
581

582
    private _initializeColumns() {
583
        if (!this.columns.some(item => item.expand === true) && this.columns.length > 0) {
8✔
584
            this.columns[0].expand = true;
8✔
585
        }
8✔
586
        this._initializeColumnFixedPositions();
8✔
587
    }
8✔
588

8!
589
    private _initializeColumnFixedPositions() {
×
590
        const leftFixedColumns = this.columns.filter(item => item.fixed === ThyFixedDirection.left);
×
591
        leftFixedColumns.forEach((item, index) => {
592
            const previous = leftFixedColumns[index - 1];
×
593
            item.left = previous ? previous.left + parseInt(previous.width.toString(), 10) : 0;
×
594
        });
595
        const rightFixedColumns = this.columns.filter(item => item.fixed === ThyFixedDirection.right).reverse();
596
        rightFixedColumns.forEach((item, index) => {
×
597
            const previous = rightFixedColumns[index - 1];
598
            item.right = previous ? previous.right + parseInt(previous.width.toString(), 10) : 0;
599
        });
8!
600
    }
8✔
601

602
    private _initializeDataModel() {
8!
603
        this.model.forEach(row => {
×
604
            this.columns.forEach(column => {
605
                this._initialSelections(row, column);
606
                this._initialCustomModelValue(row, column);
607
            });
66✔
608
        });
66✔
609
    }
66✔
610

66✔
611
    private _initialSelections(row: object, column: ThyTableColumnComponent) {
612
        if (column.selections) {
613
            if (column.type === 'checkbox') {
8✔
614
                row[column.key] = column.selections.includes(row[this.rowKey]);
8✔
615
                this.onModelChange(row, column);
8✔
616
            }
8✔
617
            if (column.type === 'radio') {
618
                if (column.selections.includes(row[this.rowKey])) {
66✔
619
                    this.selectedRadioRow = row;
66✔
620
                }
×
621
            }
622
        }
623
    }
624

625
    private _initialCustomModelValue(row: object, column: ThyTableColumnComponent) {
8✔
626
        if (column.type === customType.switch) {
8✔
627
            row[column.key] = get(row, column.model);
47✔
628
        }
70✔
629
    }
86✔
630

631
    private _refreshCustomModelValue(row: SafeAny) {
47✔
632
        this.columns.forEach(column => {
633
            this._initialCustomModelValue(row, column);
634
        });
635
    }
66!
636

×
637
    private _applyDiffChanges(changes: IterableChanges<SafeAny>) {
638
        if (changes) {
66✔
639
            changes.forEachAddedItem((record: IterableChangeRecord<SafeAny>) => {
392✔
640
                this._refreshCustomModelValue(record.item);
641
            });
642
        }
643
    }
644

645
    private _bindTrackFn() {
646
        this.trackByFn = function (this: SafeAny, index: number, row: SafeAny): SafeAny {
647
            return row && this.rowKey ? row[this.rowKey] : index;
392!
648
        }.bind(this);
649
    }
×
650

651
    private _destroyInvalidAttribute() {
652
        this.model.forEach(row => {
653
            for (const key in row) {
654
                if (key.includes('[$$column]')) {
655
                    delete row[key];
656
                }
657
            }
1!
658
        });
1✔
659
    }
660

661
    private _setClass(first = false) {
662
        if (!first && !this.initialized) {
663
            return;
78✔
664
        }
78✔
665
        const classNames: string[] = [];
78✔
666
        if (this.size) {
78✔
667
            classNames.push(`table-${this.size}`);
12✔
668
        }
12✔
669
        if (tableThemeMap[this.theme]) {
670
            classNames.push(tableThemeMap[this.theme]);
78!
671
        }
78✔
672

78✔
673
        this.updateHostClassService.updateClass(classNames);
674
    }
675

676
    public updateColumnSelections(key: string, selections: SafeAny): void {
70✔
677
        const column = this.columns.find(item => item.key === key);
70✔
678
        this.model.forEach(row => {
679
            this._initialSelections(row, column);
1✔
680
        });
681
    }
682

683
    public isTemplateRef(ref: SafeAny) {
684
        return ref instanceof TemplateRef;
685
    }
686

687
    public getModelValue(row: SafeAny, path: string) {
688
        return get(row, path);
689
    }
690

1✔
691
    public renderRowClassName(row: SafeAny, index: number) {
692
        if (!this.rowClassName) {
693
            return null;
694
        }
695
        if (isString(this.rowClassName)) {
696
            return this.rowClassName;
697
        } else {
698
            return (this.rowClassName as Function)(row, index);
699
        }
700
    }
701

702
    public onModelChange(row: SafeAny, column: ThyTableColumnComponent) {
703
        if (column.model) {
704
            set(row, column.model, row[column.key]);
705
        }
706
    }
707

708
    public onStopPropagation(event: Event) {
709
        if (this.wholeRowSelect) {
710
            event.stopPropagation();
711
        }
712
    }
713

714
    public onPageChange(event: PageChangedEvent) {
715
        this.thyOnPageChange.emit(event);
716
    }
717

718
    public onPageIndexChange(event: number) {
719
        this.thyOnPageIndexChange.emit(event);
720
    }
721

722
    public onPageSizeChange(event: number) {
723
        this.thyOnPageSizeChange.emit(event);
724
    }
725

726
    public onCheckboxChange(row: SafeAny, column: ThyTableColumnComponent) {
727
        this.onModelChange(row, column);
728
        this.onMultiSelectChange(null, row, column);
729
    }
730

731
    public onMultiSelectChange(event: Event, row: SafeAny, column: ThyTableColumnComponent) {
732
        const rows = this.model.filter(item => {
733
            return item[column.key];
734
        });
735
        const multiSelectEvent: ThyMultiSelectEvent = {
736
            event: event,
737
            row: row,
738
            rows: rows
739
        };
1✔
740
        this.thyOnMultiSelectChange.emit(multiSelectEvent);
741
    }
742

743
    public onRadioSelectChange(event: Event, row: SafeAny) {
1✔
744
        const radioSelectEvent: ThyRadioSelectEvent = {
745
            event: event,
746
            row: row
747
        };
1✔
748
        this.thyOnRadioSelectChange.emit(radioSelectEvent);
749
    }
750

751
    public onSwitchChange(event: Event, row: SafeAny, column: SafeAny) {
1✔
752
        const switchEvent: ThySwitchEvent = {
753
            event: event,
754
            row: row,
755
            refresh: (value: SafeAny) => {
1✔
756
                value = value || row;
757
                setTimeout(() => {
758
                    value[column.key] = get(value, column.model);
759
                });
760
            }
1✔
761
        };
762
        this.thyOnSwitchChange.emit(switchEvent);
763
    }
764

765
    showExpand(row: SafeAny) {
1✔
766
        return row[this.thyChildrenKey] && row[this.thyChildrenKey].length > 0;
767
    }
768

769
    isExpanded(row: SafeAny) {
770
        return this.expandStatusMap[row[this.rowKey]];
1✔
771
    }
772

773
    iconIndentComputed(level: number) {
774
        if (this.mode === 'tree') {
775
            return level * this.thyIndent - 5;
1✔
776
        }
777
    }
778

779
    tdIndentComputed(level: number) {
780
        return {
1✔
781
            position: 'relative',
782
            paddingLeft: `${(level + 1) * this.thyIndent - 5}px`
783
        };
784
    }
785

1✔
786
    expandChildren(row: SafeAny) {
787
        if (this.isExpanded(row)) {
788
            this.expandStatusMap[row[this.rowKey]] = false;
789
        } else {
1✔
790
            this.expandStatusMap[row[this.rowKey]] = true;
791
        }
792
    }
793

794
    onDragGroupStarted(event: CdkDragStart<unknown>) {
1✔
795
        this.expandStatusMapOfGroupBeforeDrag = { ...this.expandStatusMapOfGroup };
796
        const groups = this.groups.filter(group => group.expand);
797
        this.foldGroups(groups);
798
        this.onDragStarted(event);
1✔
799
        this.cdr.detectChanges();
800
    }
801

802
    onDragGroupEnd(event: CdkDragEnd<unknown>) {
1✔
803
        const groups = this.groups.filter(group => this.expandStatusMapOfGroupBeforeDrag[group.id]);
804
        this.expandGroups(groups);
805
        this.cdr.detectChanges();
806
    }
1✔
807

808
    private onDragGroupDropped(event: CdkDragDrop<unknown>) {
809
        const group = this.groups.find(group => {
810
            return event.item.data.id === group.id;
1✔
811
        });
812
        if (group) {
813
            // drag group
814
            const dragEvent: ThyTableDraggableEvent = {
815
                model: event.item,
816
                models: this.groups,
817
                oldIndex: event.previousIndex,
818
                newIndex: event.currentIndex
819
            };
820
            moveItemInArray(this.groups, event.previousIndex, event.currentIndex);
821
            this.thyOnDraggableChange.emit(dragEvent);
822
        } else {
823
            // drag group children
824
            const group = this.groups.find(group => {
825
                return event.item.data[this.groupBy] === group.id;
826
            });
827
            const groupIndex =
828
                event.container.getSortedItems().findIndex(item => {
829
                    return item.data.id === event.item.data[this.groupBy];
830
                }) + 1;
831
            const dragEvent: ThyTableDraggableEvent = {
832
                model: event.item,
833
                models: group.children,
834
                oldIndex: event.previousIndex - groupIndex,
835
                newIndex: event.currentIndex - groupIndex
836
            };
837
            moveItemInArray(group.children, dragEvent.oldIndex, dragEvent.newIndex);
838
            this.thyOnDraggableChange.emit(dragEvent);
839
        }
840
    }
841

842
    onDragStarted(event: CdkDragStart<unknown>) {
843
        this.ngZone.runOutsideAngular(() =>
844
            setTimeout(() => {
845
                const preview = this.document.getElementsByClassName(this.dragPreviewClass)[0];
846
                const originalTds: HTMLCollection = event.source._dragRef.getPlaceholderElement()?.children;
847
                if (preview) {
848
                    Array.from(preview?.children).forEach((element: HTMLElement, index: number) => {
849
                        element.style.width = `${originalTds[index]?.clientWidth}px`;
850
                    });
851
                }
852
            })
853
        );
854
    }
855

856
    dropListEnterPredicate = (index: number, drag: CdkDrag, drop: CdkDropList) => {
857
        return drop.getSortedItems()[index].data.group_id === drag.data.group_id;
858
    };
859

860
    private onDragModelDropped(event: CdkDragDrop<unknown>) {
861
        const dragEvent: ThyTableDraggableEvent = {
862
            model: event.item,
863
            models: this.model,
864
            oldIndex: event.previousIndex,
865
            newIndex: event.currentIndex
866
        };
867
        moveItemInArray(this.model, event.previousIndex, event.currentIndex);
868
        this.thyOnDraggableChange.emit(dragEvent);
869
    }
870

871
    onDragDropped(event: CdkDragDrop<unknown>) {
872
        if (this.mode === 'group') {
873
            this.onDragGroupDropped(event);
874
        } else if (this.mode === 'list') {
875
            this.onDragModelDropped(event);
876
        }
877
    }
878

879
    onColumnHeaderClick(event: Event, column: ThyTableColumnComponent) {
880
        if (column.sortable) {
881
            const { sortDirection, model, sortChange } = column;
882
            let direction;
883
            if (sortDirection === ThyTableSortDirection.default) {
884
                direction = ThyTableSortDirection.asc;
885
            } else if (sortDirection === ThyTableSortDirection.asc) {
886
                direction = ThyTableSortDirection.desc;
887
            } else {
888
                direction = ThyTableSortDirection.default;
889
            }
890
            column.sortDirection = direction;
891
            const sortEvent = { event, key: model, direction };
892
            sortChange.emit(sortEvent);
893
            this.thySortChange.emit(sortEvent);
894
        }
895
    }
896

897
    public onRowClick(event: Event, row: SafeAny) {
898
        const next = this.onRowClickPropagationEventHandler(event, row);
899
        if (next) {
900
            if (this.wholeRowSelect) {
901
                const column = this.columns.find(item => {
902
                    return item.type === customType.checkbox || item.type === customType.radio;
903
                });
904
                if (column && !column.disabled) {
905
                    if (column.type === customType.checkbox) {
906
                        row[column.key] = !row[column.key];
907
                        this.onModelChange(row, column);
908
                        this.onMultiSelectChange(event, row, column);
909
                    }
910
                    if (column.type === customType.radio) {
911
                        this.selectedRadioRow = row;
912
                        this.onRadioSelectChange(event, row);
913
                    }
914
                }
915
            }
916
            const rowEvent = {
917
                event: event,
918
                row: row
919
            };
920
            this.thyOnRowClick.emit(rowEvent);
921
        }
922
    }
923

924
    private onRowClickPropagationEventHandler(event: Event, row: SafeAny): boolean {
925
        if ((event.target as Element).closest('.tree-expand-icon')) {
926
            this.expandChildren(row);
927
            return false;
928
        }
929
        return true;
930
    }
931

932
    public onRowContextMenu(event: Event, row: SafeAny) {
933
        const contextMenuEvent: ThyTableEvent = {
934
            event: event,
935
            row: row
936
        };
937
        this.thyOnRowContextMenu.emit(contextMenuEvent);
938
    }
939

940
    private _refreshColumns() {
941
        const components = this.columns || [];
942
        const _columns = components.map(component => {
943
            return {
944
                width: component.width,
945
                className: component.className
946
            };
947
        });
948

949
        this.columns.forEach((n, i) => {
950
            Object.assign(n, _columns[i]);
951
        });
952
    }
953

954
    private buildGroups(originGroups: SafeAny) {
955
        const originGroupsMap = helpers.keyBy(originGroups, 'id');
956
        this.groups = [];
957
        originGroups.forEach((origin: SafeAny) => {
958
            const group: ThyTableGroup = { id: origin[this.rowKey], children: [], origin };
959

960
            if (this.expandStatusMapOfGroup.hasOwnProperty(group.id)) {
961
                group.expand = this.expandStatusMapOfGroup[group.id];
962
            } else {
963
                group.expand = !!(originGroupsMap[group.id] as SafeAny).expand;
964
            }
965

966
            this.groups.push(group);
967
        });
968
    }
969

970
    private buildModel() {
971
        const groupsMap = keyBy(this.groups, 'id');
972
        this.model.forEach(row => {
973
            const group = groupsMap[row[this.groupBy]];
974
            if (group) {
975
                group.children.push(row);
976
            }
977
        });
978
    }
979

980
    public expandGroup(group: ThyTableGroup) {
981
        group.expand = !group.expand;
982
        this.expandStatusMapOfGroup[group.id] = group.expand;
983
    }
984

985
    private expandGroups(groups: ThyTableGroup[]) {
986
        groups.forEach(group => {
987
            this.expandGroup(group);
988
        });
989
    }
990

991
    private foldGroups(groups: ThyTableGroup[]) {
992
        groups.forEach(group => {
993
            this.expandGroup(group);
994
        });
995
    }
996

997
    private updateScrollClass() {
998
        const scrollElement = this.tableScrollElement;
999
        const maxScrollLeft = scrollElement.scrollWidth - scrollElement.offsetWidth;
1000
        const scrollX = scrollElement.scrollLeft;
1001
        const lastScrollClassName = this.scrollClassName;
1002
        this.scrollClassName = '';
1003
        if (scrollElement.scrollWidth > scrollElement.clientWidth) {
1004
            if (scrollX >= maxScrollLeft) {
1005
                this.scrollClassName = css.tableScrollRight;
1006
            } else if (scrollX === 0) {
1007
                this.scrollClassName = css.tableScrollLeft;
1008
            } else {
1009
                this.scrollClassName = css.tableScrollMiddle;
1010
            }
1011
        }
1012
        if (lastScrollClassName) {
1013
            this.renderer.removeClass(this.tableScrollElement, lastScrollClassName);
1014
        }
1015
        if (this.scrollClassName) {
1016
            this.renderer.addClass(this.tableScrollElement, this.scrollClassName);
1017
        }
1018
    }
1019

1020
    ngOnInit() {
1021
        this.updateHostClassService.initializeElement(this.tableElementRef.nativeElement);
1022
        this._setClass(true);
1023
        this.initialized = true;
1024

1025
        merge(this.viewportRuler.change(200), of(null).pipe(delay(200)))
1026
            .pipe(takeUntil(this.ngUnsubscribe$))
1027
            .subscribe(() => {
1028
                this._refreshColumns();
1029
                this.updateScrollClass();
1030
                this.buildSkeletonColumns();
1031
                this.cdr.detectChanges();
1032
            });
1033

1034
        this.ngZone.runOutsideAngular(() => {
1035
            this.scroll$.pipe(takeUntil(this.ngUnsubscribe$)).subscribe(() => {
1036
                this.updateScrollClass();
1037
            });
1038
        });
1039
    }
1040

1041
    private buildSkeletonColumns() {
1042
        this.skeletonColumns = [];
1043

1044
        this.columns.forEach((column: ThyTableColumnComponent, index: number) => {
1045
            const item = {
1046
                type: this.thyColumnSkeletonTypes[index] || ThyTableColumnSkeletonType.default,
1047
                width: column.width || 'auto'
1048
            };
1049
            this.skeletonColumns = [...this.skeletonColumns, item];
1050
        });
1051
    }
1052

1053
    ngAfterViewInit(): void {
1054
        if (isPlatformServer(this.platformId)) {
1055
            return;
1056
        }
1057

1058
        this.rows.changes
1059
            .pipe(
1060
                startWith(this.rows),
1061
                switchMap(
1062
                    () =>
1063
                        new Observable<Event>(subscriber =>
1064
                            this.ngZone.runOutsideAngular(() =>
1065
                                merge(
1066
                                    ...this.rows.map(row =>
1067
                                        fromEvent(
1068
                                            row.nativeElement,
1069
                                            // Note: there's no need to add touch, pointer and mouse event listeners together.
1070
                                            // There can be any number of rows, which will lead to adding N * 3 event listeners.
1071
                                            // According to the spec (https://www.w3.org/TR/pointerevents/#examples), we can use feature detection
1072
                                            // to determine if pointer events are available. If pointer events are available, we have to listen only
1073
                                            // to the `pointerdown` event. Otherwise, we have to determine if we're on a touch device or not.
1074
                                            // Touch events are handled earlier than mouse events, tho not all user agents dispatch mouse events
1075
                                            // after touch events. See the spec: https://www.w3.org/TR/touch-events/#mouse-events.
1076
                                            window.PointerEvent
1077
                                                ? 'pointerdown'
1078
                                                : 'ontouchstart' in row.nativeElement
1079
                                                ? 'touchstart'
1080
                                                : 'mousedown',
1081
                                            // Note: since Chrome 56 defaults document level `touchstart` listener to passive.
1082
                                            // The element `touchstart` listener is not passive by default
1083
                                            // We never call `preventDefault()` on it, so we're safe making it passive too.
1084
                                            <AddEventListenerOptions>passiveEventListenerOptions
1085
                                        )
1086
                                    )
1087
                                ).subscribe(subscriber)
1088
                            )
1089
                        )
1090
                ),
1091
                takeUntil(this.ngUnsubscribe$)
1092
            )
1093
            .subscribe(event => {
1094
                if (!this.draggable) {
1095
                    event.stopPropagation();
1096
                }
1097
            });
1098
    }
1099

1100
    ngOnChanges(simpleChanges: SimpleChanges) {
1101
        const modeChange = simpleChanges.thyMode;
1102
        const thyGroupsChange = simpleChanges.thyGroups;
1103
        const isGroupMode = modeChange && modeChange.currentValue === 'group';
1104
        if (isGroupMode && thyGroupsChange && thyGroupsChange.firstChange) {
1105
            this.buildGroups(thyGroupsChange.currentValue);
1106
            this.buildModel();
1107
        }
1108

1109
        if (this._diff) {
1110
            const changes = this._diff.diff(this.model);
1111
            this._applyDiffChanges(changes);
1112
        }
1113
    }
1114

1115
    ngOnDestroy() {
1116
        super.ngOnDestroy();
1117
        this._destroyInvalidAttribute();
1118
    }
1119
}
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