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

atinc / ngx-tethys / #55

30 Jul 2025 07:08AM UTC coverage: 9.866% (-80.4%) from 90.297%
#55

push

why520crazy
feat(empty): add setMessage for update display text #TINFR-2616

92 of 6794 branches covered (1.35%)

Branch coverage included in aggregate %.

2014 of 14552 relevant lines covered (13.84%)

6.15 hits per line

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

3.13
/src/table/table.component.ts
1
import { InputCssPixel, 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 } from 'rxjs/operators';
6
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7

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

44
import { IThyTableColumnParentComponent, THY_TABLE_COLUMN_PARENT_COMPONENT, ThyTableColumnComponent } from './table-column.component';
45
import {
46
    PageChangedEvent,
47
    ThyMultiSelectEvent,
48
    ThyPage,
1✔
49
    ThyRadioSelectEvent,
50
    ThySwitchEvent,
51
    ThyTableSkeletonColumn,
52
    ThyTableDraggableEvent,
53
    ThyTableEmptyOptions,
54
    ThyTableEvent,
1✔
55
    ThyTableRowEvent,
56
    ThyTableSortDirection,
×
57
    ThyTableSortEvent
58
} from './table.interface';
59
import { TableRowDragDisabledPipe } from './pipes/drag.pipe';
×
60
import { TableIsValidModelValuePipe } from './pipes/table.pipe';
61
import { ThyPagination } from 'ngx-tethys/pagination';
62
import { ThyTableSkeleton } from './table-skeleton.component';
×
63
import { ThyEmpty } from 'ngx-tethys/empty';
64
import { ThySwitch } from 'ngx-tethys/switch';
65
import { FormsModule } from '@angular/forms';
×
66
import { ThyDragDropDirective, ThyContextMenuDirective } from 'ngx-tethys/shared';
67
import { ThyIcon } from 'ngx-tethys/icon';
68
import { CdkScrollable } from '@angular/cdk/scrolling';
×
69
import { ThyTableColumnSkeletonType } from './enums';
70
import { ThyTableTheme, ThyTableMode, ThyTableSize } from './table.type';
71

×
72
export enum ThyFixedDirection {
×
73
    left = 'left',
74
    right = 'right'
75
}
76

×
77
interface ThyTableGroup<T = unknown> {
×
78
    id?: string;
×
79
    expand?: boolean;
×
80
    children?: object[];
×
81
    origin?: T;
82
}
83

84
const tableThemeMap = {
×
85
    default: 'table-default',
×
86
    bordered: 'table-bordered',
87
    boxed: 'table-boxed'
88
};
×
89

×
90
const customType = {
91
    index: 'index',
92
    checkbox: 'checkbox',
×
93
    radio: 'radio',
×
94
    switch: 'switch'
×
95
};
×
96

97
const css = {
98
    tableBody: 'thy-table-body',
×
99
    tableScrollLeft: 'thy-table-scroll-left',
100
    tableScrollRight: 'thy-table-scroll-right',
×
101
    tableScrollMiddle: 'thy-table-scroll-middle'
×
102
};
103

104
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
×
105

106
/**
107
 * 表格组件
×
108
 * @name thy-table
109
 * @order 10
110
 */
×
111
@Component({
112
    selector: 'thy-table',
113
    templateUrl: './table.component.html',
×
114
    providers: [
115
        {
116
            provide: THY_TABLE_COLUMN_PARENT_COMPONENT,
×
117
            useExisting: ThyTable
×
118
        },
×
119
        UpdateHostClassService
120
    ],
121
    encapsulation: ViewEncapsulation.None,
122
    host: {
×
123
        class: 'thy-table',
124
        '[class.thy-table-bordered]': `theme === 'bordered'`,
125
        '[class.thy-table-boxed]': `theme === 'boxed'`,
×
126
        '[class.thy-table-fixed-header]': 'thyHeaderFixed'
127
    },
128
    imports: [
×
129
        CdkScrollable,
130
        NgClass,
131
        NgTemplateOutlet,
×
132
        ThyIcon,
×
133
        ThyDragDropDirective,
134
        CdkDropList,
×
135
        CdkDrag,
136
        ThyContextMenuDirective,
137
        NgStyle,
×
138
        FormsModule,
139
        ThySwitch,
140
        ThyEmpty,
×
141
        ThyTableSkeleton,
142
        ThyPagination,
143
        TableIsValidModelValuePipe,
×
144
        TableRowDragDisabledPipe
×
145
    ]
×
146
})
×
147
export class ThyTable implements OnInit, OnChanges, AfterViewInit, OnDestroy, IThyTableColumnParentComponent {
148
    elementRef = inject(ElementRef);
×
149
    private _differs = inject(IterableDiffers);
×
150
    private viewportRuler = inject(ViewportRuler);
×
151
    private updateHostClassService = inject(UpdateHostClassService);
152
    private document = inject(DOCUMENT);
153
    private platformId = inject(PLATFORM_ID);
154
    private ngZone = inject(NgZone);
×
155
    private renderer = inject(Renderer2);
×
156
    private cdr = inject(ChangeDetectorRef);
×
157

×
158
    private readonly destroyRef = inject(DestroyRef);
×
159

×
160
    public customType = customType;
×
161

×
162
    public model: object[] = [];
×
163

×
164
    public groups: ThyTableGroup[] = [];
×
165

×
166
    public rowKey = '_id';
×
167

×
168
    public groupBy: string;
×
169

×
170
    public mode: ThyTableMode = 'list';
×
171

×
172
    public theme: ThyTableTheme = 'default';
×
173

×
174
    public className = '';
×
175

×
176
    public size: ThyTableSize = 'md';
×
177

×
178
    public rowClassName: string | Function;
×
179

×
180
    public loadingDone = true;
×
181

×
182
    public loadingText: string;
×
183

×
184
    public emptyOptions: ThyTableEmptyOptions = {};
×
185

×
186
    public draggable = false;
×
187

×
188
    public selectedRadioRow: SafeAny = null;
×
189

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

192
    public trackByFn: SafeAny;
193

194
    public wholeRowSelect = false;
195

×
196
    public fixedDirection = ThyFixedDirection;
×
197

×
198
    public hasFixed = false;
×
199

×
200
    public columns: ThyTableColumnComponent[] = [];
×
201

×
202
    private _diff: IterableDiffer<SafeAny>;
×
203

×
204
    private initialized = false;
×
205

206
    private _oldThyClassName = '';
×
207

×
208
    private scrollClassName = css.tableScrollLeft;
×
209

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
314
    /**
315
     * 是否表头固定,若设置为 true, 需要同步设置 thyHeight
×
316
     * @default false
×
317
     */
318
    @Input({ transform: coerceBooleanProperty }) thyHeaderFixed: boolean;
319

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

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

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

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

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

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

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

391
    /**
392
     * 设置当前页码
393
     * @default 1
394
     */
395
    @Input({ transform: numberAttribute })
396
    set thyPageIndex(value: number) {
×
397
        this.pagination.index = value;
×
398
    }
399

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

×
409
    /**
410
     * 设置总页数
411
     */
×
412
    @Input({ transform: numberAttribute })
×
413
    set thyPageTotal(value: number) {
×
414
        this.pagination.total = value;
415
    }
416

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

×
429
    /**
430
     * 是否显示表格头
431
     * @default false
432
     */
×
433
    @Input({ transform: coerceBooleanProperty }) thyHeadless = false;
×
434

435
    /**
×
436
     * 是否显示表格头,已废弃,请使用 thyHeadless
×
437
     * @deprecated please use thyHeadless
438
     */
×
439
    @Input({ transform: coerceBooleanProperty })
440
    set thyShowHeader(value: boolean) {
441
        this.thyHeadless = !value;
442
    }
443

444
    /**
×
445
     * 是否显示左侧 Total
×
446
     */
447
    @Input({ alias: 'thyShowTotal', transform: coerceBooleanProperty }) showTotal = false;
448

449
    /**
×
450
     * 是否显示调整每页显示条数下拉框
×
451
     */
×
452
    @Input({ alias: 'thyShowSizeChanger', transform: coerceBooleanProperty }) showSizeChanger = false;
×
453

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

463
    /**
464
     * thyMode 为 tree 时,设置 Tree 树状数据展示时的缩进
465
     */
466
    @Input({ transform: numberAttribute }) thyIndent = 20;
×
467

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

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

×
482
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
×
483

484
    /**
×
485
     * 表格列的骨架类型
×
486
     * @type ThyTableColumnSkeletonType[]
487
     */
488
    @Input() thyColumnSkeletonTypes: ThyTableColumnSkeletonType[] = [
×
489
        ThyTableColumnSkeletonType.title,
490
        ThyTableColumnSkeletonType.member,
×
491
        ThyTableColumnSkeletonType.default
×
492
    ];
×
493

×
494
    /**
495
     * 切换组件回调事件
496
     */
497
    @Output() thyOnSwitchChange: EventEmitter<ThySwitchEvent> = new EventEmitter<ThySwitchEvent>();
×
498

×
499
    /**
×
500
     * 表格分页回调事件
×
501
     */
×
502
    @Output() thyOnPageChange: EventEmitter<PageChangedEvent> = new EventEmitter<PageChangedEvent>();
503

×
504
    /**
×
505
     * 表格分页当前页改变回调事件
×
506
     */
×
507
    @Output() thyOnPageIndexChange: EventEmitter<number> = new EventEmitter<number>();
×
508

509
    @Output() thyOnPageSizeChange: EventEmitter<number> = new EventEmitter<number>();
×
510

×
511
    /**
×
512
     * 多选回调事件
513
     */
514
    @Output() thyOnMultiSelectChange: EventEmitter<ThyMultiSelectEvent> = new EventEmitter<ThyMultiSelectEvent>();
515

×
516
    /**
517
     * 单选回调事件
518
     */
519
    @Output() thyOnRadioSelectChange: EventEmitter<ThyRadioSelectEvent> = new EventEmitter<ThyRadioSelectEvent>();
×
520

521
    /**
522
     * 拖动修改事件
523
     */
×
524
    @Output() thyOnDraggableChange: EventEmitter<ThyTableDraggableEvent> = new EventEmitter<ThyTableDraggableEvent>();
×
525

×
526
    /**
527
     * 表格行点击触发事件
×
528
     */
529
    @Output() thyOnRowClick: EventEmitter<ThyTableRowEvent> = new EventEmitter<ThyTableRowEvent>();
530

×
531
    /**
532
     * 列排序修改事件
533
     */
534
    @Output() thySortChange: EventEmitter<ThyTableSortEvent> = new EventEmitter<ThyTableSortEvent>();
×
535

536
    @Output() thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
537

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

×
540
    @ContentChildren(ThyTableColumnComponent)
541
    set listOfColumnComponents(components: QueryList<ThyTableColumnComponent>) {
542
        if (components) {
543
            this.columns = components.toArray();
544
            this.hasFixed = !!this.columns.find(item => {
×
545
                return item.fixed === this.fixedDirection.left || item.fixed === this.fixedDirection.right;
×
546
            });
547
            this.buildSkeletonColumns();
548
            this._initializeColumns();
549
            this._initializeDataModel();
×
550
        }
×
551
    }
×
552

×
553
    // 数据的折叠展开状态
×
554
    public expandStatusMap: Dictionary<boolean> = {};
×
555

556
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
557

×
558
    private expandStatusMapOfGroupBeforeDrag: Dictionary<boolean> = {};
559

×
560
    dragPreviewClass = 'thy-table-drag-preview';
561

562
    public skeletonColumns: ThyTableSkeletonColumn[] = [];
563

×
564
    constructor() {
×
565
        this._bindTrackFn();
×
566
    }
×
567

×
568
    private _initializeColumns() {
569
        if (!this.columns.some(item => item.expand === true) && this.columns.length > 0) {
570
            this.columns[0].expand = true;
571
        }
572
        this._initializeColumnFixedPositions();
×
573
    }
×
574

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

×
588
    private _initializeDataModel() {
×
589
        this.model.forEach(row => {
×
590
            this.columns.forEach(column => {
×
591
                this._initialSelections(row, column);
×
592
                this._initialCustomModelValue(row, column);
×
593
            });
×
594
        });
595
    }
×
596

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

×
611
    private _initialCustomModelValue(row: object, column: ThyTableColumnComponent) {
×
612
        if (column.type === customType.switch) {
×
613
            row[column.key] = get(row, column.model);
×
614
        }
615
    }
616

×
617
    private _refreshCustomModelValue(row: SafeAny) {
×
618
        this.columns.forEach(column => {
×
619
            this._initialCustomModelValue(row, column);
620
        });
×
621
    }
×
622

×
623
    private _applyDiffChanges(changes: IterableChanges<SafeAny>) {
624
        if (changes) {
625
            changes.forEachAddedItem((record: IterableChangeRecord<SafeAny>) => {
626
                this._refreshCustomModelValue(record.item);
627
            });
×
628
        }
×
629
    }
×
630

×
631
    private _bindTrackFn() {
×
632
        this.trackByFn = function (this: SafeAny, index: number, row: SafeAny): SafeAny {
633
            return row && this.rowKey ? row[this.rowKey] : index;
×
634
        }.bind(this);
635
    }
636

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

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

659
        this.updateHostClassService.updateClass(classNames);
×
660
    }
×
661

662
    public updateColumnSelections(key: string, selections: SafeAny): void {
663
        const column = this.columns.find(item => item.key === key);
664
        this.model.forEach(row => {
665
            this._initialSelections(row, column);
×
666
        });
×
667
    }
×
668

×
669
    public isTemplateRef(ref: SafeAny) {
×
670
        return ref instanceof TemplateRef;
×
671
    }
672

×
673
    public getModelValue(row: SafeAny, path: string) {
×
674
        return get(row, path);
×
675
    }
676

677
    public renderRowClassName(row: SafeAny, index: number) {
678
        if (!this.rowClassName) {
×
679
            return null;
680
        }
1✔
681
        if (isString(this.rowClassName)) {
1✔
682
            return this.rowClassName;
683
        } else {
684
            return (this.rowClassName as Function)(row, index);
685
        }
686
    }
687

688
    public onModelChange(row: SafeAny, column: ThyTableColumnComponent) {
689
        if (column.model) {
690
            set(row, column.model, row[column.key]);
691
        }
692
    }
693

694
    public onStopPropagation(event: Event) {
695
        if (this.wholeRowSelect) {
696
            event.stopPropagation();
697
        }
698
    }
699

700
    public onPageChange(event: PageChangedEvent) {
701
        this.thyOnPageChange.emit(event);
702
    }
703

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

708
    public onPageSizeChange(event: number) {
709
        this.thyOnPageSizeChange.emit(event);
710
    }
711

712
    public onCheckboxChange(row: SafeAny, column: ThyTableColumnComponent) {
713
        this.onModelChange(row, column);
714
        this.onMultiSelectChange(null, row, column);
715
    }
716

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

729
    public onRadioSelectChange(event: Event, row: SafeAny) {
730
        const radioSelectEvent: ThyRadioSelectEvent = {
1✔
731
            event: event,
732
            row: row
733
        };
734
        this.thyOnRadioSelectChange.emit(radioSelectEvent);
1✔
735
    }
736

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

751
    showExpand(row: SafeAny) {
752
        return row[this.thyChildrenKey] && row[this.thyChildrenKey].length > 0;
753
    }
754

755
    isExpanded(row: SafeAny) {
756
        return this.expandStatusMap[row[this.rowKey]];
757
    }
758

759
    iconIndentComputed(level: number) {
760
        if (this.mode === 'tree') {
761
            return level * this.thyIndent - 5;
762
        }
763
    }
764

765
    tdIndentComputed(level: number, column: SafeAny) {
766
        return {
767
            left: `${column.left}px`,
768
            right: `${column.right}px`,
769
            position: 'relative',
770
            paddingLeft: `${(level + 1) * this.thyIndent - 5}px`
771
        };
772
    }
773

774
    expandChildren(row: SafeAny) {
775
        if (this.isExpanded(row)) {
776
            this.expandStatusMap[row[this.rowKey]] = false;
777
        } else {
778
            this.expandStatusMap[row[this.rowKey]] = true;
779
        }
780
    }
781

782
    onDragGroupStarted(event: CdkDragStart<unknown>) {
783
        this.expandStatusMapOfGroupBeforeDrag = { ...this.expandStatusMapOfGroup };
784
        const groups = this.groups.filter(group => group.expand);
785
        this.foldGroups(groups);
786
        this.onDragStarted(event);
787
        this.cdr.detectChanges();
788
    }
789

790
    onDragGroupEnd(event: CdkDragEnd<unknown>) {
791
        const groups = this.groups.filter(group => this.expandStatusMapOfGroupBeforeDrag[group.id]);
792
        this.expandGroups(groups);
793
        this.cdr.detectChanges();
794
    }
795

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

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

844
    dropListEnterPredicate = (index: number, drag: CdkDrag, drop: CdkDropList) => {
845
        return drop.getSortedItems()[index].data.group_id === drag.data.group_id;
846
    };
847

848
    private onDragModelDropped(event: CdkDragDrop<unknown>) {
849
        const dragEvent: ThyTableDraggableEvent = {
850
            model: event.item,
851
            models: this.model,
852
            oldIndex: event.previousIndex,
853
            newIndex: event.currentIndex
854
        };
855
        moveItemInArray(this.model, event.previousIndex, event.currentIndex);
856
        this.thyOnDraggableChange.emit(dragEvent);
857
    }
858

859
    onDragDropped(event: CdkDragDrop<unknown>) {
860
        if (this.mode === 'group') {
861
            this.onDragGroupDropped(event);
862
        } else if (this.mode === 'list') {
863
            this.onDragModelDropped(event);
864
        }
865
    }
866

867
    onColumnHeaderClick(event: Event, column: ThyTableColumnComponent) {
868
        if (column.sortable) {
869
            const { sortDirection, model, sortChange } = column;
870
            let direction;
871
            if (sortDirection === ThyTableSortDirection.default) {
872
                direction = ThyTableSortDirection.desc;
873
            } else if (sortDirection === ThyTableSortDirection.desc) {
874
                direction = ThyTableSortDirection.asc;
875
            } else {
876
                direction = ThyTableSortDirection.default;
877
            }
878
            column.sortDirection = direction;
879
            const sortEvent = { event, key: model, direction };
880
            sortChange.emit(sortEvent);
881
            this.thySortChange.emit(sortEvent);
882
        }
883
    }
884

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

912
    private onRowClickPropagationEventHandler(event: Event, row: SafeAny): boolean {
913
        if ((event.target as Element).closest('.tree-expand-icon')) {
914
            this.expandChildren(row);
915
            return false;
916
        }
917
        return true;
918
    }
919

920
    public onRowContextMenu(event: Event, row: SafeAny) {
921
        const contextMenuEvent: ThyTableEvent = {
922
            event: event,
923
            row: row
924
        };
925
        this.thyOnRowContextMenu.emit(contextMenuEvent);
926
    }
927

928
    private _refreshColumns() {
929
        const components = this.columns || [];
930
        const _columns = components.map(component => {
931
            return {
932
                width: component.width,
933
                className: component.className
934
            };
935
        });
936

937
        this.columns.forEach((n, i) => {
938
            Object.assign(n, _columns[i]);
939
        });
940
    }
941

942
    private buildGroups(originGroups: SafeAny) {
943
        const originGroupsMap = helpers.keyBy(originGroups, 'id');
944
        this.groups = [];
945
        originGroups.forEach((origin: SafeAny) => {
946
            const group: ThyTableGroup = { id: origin[this.rowKey], children: [], origin };
947

948
            if (this.expandStatusMapOfGroup.hasOwnProperty(group.id)) {
949
                group.expand = this.expandStatusMapOfGroup[group.id];
950
            } else {
951
                group.expand = !!(originGroupsMap[group.id] as SafeAny).expand;
952
            }
953

954
            this.groups.push(group);
955
        });
956
    }
957

958
    private buildModel() {
959
        const groupsMap = keyBy(this.groups, 'id');
960
        this.model.forEach(row => {
961
            const group = groupsMap[row[this.groupBy]];
962
            if (group) {
963
                group.children.push(row);
964
            }
965
        });
966
    }
967

968
    public expandGroup(group: ThyTableGroup) {
969
        group.expand = !group.expand;
970
        this.expandStatusMapOfGroup[group.id] = group.expand;
971
    }
972

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

979
    private foldGroups(groups: ThyTableGroup[]) {
980
        groups.forEach(group => {
981
            this.expandGroup(group);
982
        });
983
    }
984

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

1008
    ngOnInit() {
1009
        this.updateHostClassService.initializeElement(this.tableElementRef.nativeElement);
1010
        this._setClass(true);
1011
        this.initialized = true;
1012

1013
        merge(this.viewportRuler.change(200), of(null).pipe(delay(200)))
1014
            .pipe(takeUntilDestroyed(this.destroyRef))
1015
            .subscribe(() => {
1016
                this._refreshColumns();
1017
                this.updateScrollClass();
1018
                this.cdr.detectChanges();
1019
            });
1020

1021
        this.ngZone.runOutsideAngular(() => {
1022
            this.scroll$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
1023
                this.updateScrollClass();
1024
            });
1025
        });
1026
    }
1027

1028
    private buildSkeletonColumns() {
1029
        this.skeletonColumns = [];
1030

1031
        this.columns.forEach((column: ThyTableColumnComponent, index: number) => {
1032
            const item = {
1033
                type: this.thyColumnSkeletonTypes[index] || ThyTableColumnSkeletonType.default,
1034
                width: column.width || 'auto'
1035
            };
1036
            this.skeletonColumns = [...this.skeletonColumns, item];
1037
        });
1038
    }
1039

1040
    ngAfterViewInit(): void {
1041
        if (isPlatformServer(this.platformId)) {
1042
            return;
1043
        }
1044

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

1087
    ngOnChanges(simpleChanges: SimpleChanges) {
1088
        const modeChange = simpleChanges.thyMode;
1089
        const thyGroupsChange = simpleChanges.thyGroups;
1090
        const isGroupMode = modeChange && modeChange.currentValue === 'group';
1091
        if (isGroupMode && thyGroupsChange && thyGroupsChange.firstChange) {
1092
            this.buildGroups(thyGroupsChange.currentValue);
1093
            this.buildModel();
1094
        }
1095

1096
        if (this._diff) {
1097
            const changes = this._diff.diff(this.model);
1098
            this._applyDiffChanges(changes);
1099
        }
1100
    }
1101

1102
    ngOnDestroy() {
1103
        this._destroyInvalidAttribute();
1104
    }
1105
}
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