@siemens/ngx-datatable
Version:
ngx-datatable is an Angular table grid component for presenting large and complex data.
1,312 lines (1,286 loc) • 278 kB
JavaScript
import * as i0 from '@angular/core';
import { Directive, EventEmitter, TemplateRef, Output, ContentChild, Input, inject, InjectionToken, Component, ViewContainerRef, Injector, Injectable, booleanAttribute, numberAttribute, ElementRef, NgZone, signal, output, input, computed, ChangeDetectionStrategy, KeyValueDiffers, HostListener, linkedSignal, ChangeDetectorRef, HostBinding, Renderer2, model, ViewChild, effect, ContentChildren, contentChild, IterableDiffers, viewChild, NgModule } from '@angular/core';
import { NgTemplateOutlet, DOCUMENT, NgClass, NgStyle } from '@angular/common';
import { Subject, startWith } from 'rxjs';
import { __decorate } from 'tslib';
class DatatableGroupHeaderTemplateDirective {
static ngTemplateContextGuard(directive, context) {
return true;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableGroupHeaderTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: DatatableGroupHeaderTemplateDirective, isStandalone: true, selector: "[ngx-datatable-group-header-template]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableGroupHeaderTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngx-datatable-group-header-template]'
}]
}] });
class DatatableGroupHeaderDirective {
constructor() {
/**
* Row height is required when virtual scroll is enabled.
*/
this.rowHeight = 0;
/**
* Show checkbox at group header to select all rows of the group.
*/
this.checkboxable = false;
/**
* Track toggling of group visibility
*/
this.toggle = new EventEmitter();
}
get template() {
return this._templateInput ?? this._templateQuery;
}
/**
* Toggle the expansion of a group
*/
toggleExpandGroup(group) {
this.toggle.emit({
type: 'group',
value: group
});
}
/**
* Expand all groups
*/
expandAllGroups() {
this.toggle.emit({
type: 'all',
value: true
});
}
/**
* Collapse all groups
*/
collapseAllGroups() {
this.toggle.emit({
type: 'all',
value: false
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableGroupHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: DatatableGroupHeaderDirective, isStandalone: true, selector: "ngx-datatable-group-header", inputs: { rowHeight: "rowHeight", checkboxable: "checkboxable", _templateInput: ["template", "_templateInput"] }, outputs: { toggle: "toggle" }, queries: [{ propertyName: "_templateQuery", first: true, predicate: DatatableGroupHeaderTemplateDirective, descendants: true, read: TemplateRef, static: true }], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableGroupHeaderDirective, decorators: [{
type: Directive,
args: [{
selector: 'ngx-datatable-group-header'
}]
}], propDecorators: { rowHeight: [{
type: Input
}], checkboxable: [{
type: Input
}], _templateInput: [{
type: Input,
args: ['template']
}], _templateQuery: [{
type: ContentChild,
args: [DatatableGroupHeaderTemplateDirective, { read: TemplateRef, static: true }]
}], toggle: [{
type: Output
}] } });
/**
* This component is passed as ng-template and rendered by BodyComponent.
* BodyComponent uses rowDefInternal to first inject actual row template.
* This component will render that actual row template.
*/
class DatatableRowDefComponent {
constructor() {
this.rowDef = inject(ROW_DEF_TOKEN);
this.rowContext = {
...this.rowDef.rowDefInternal,
disabled: this.rowDef.rowDefInternalDisabled
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableRowDefComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.15", type: DatatableRowDefComponent, isStandalone: true, selector: "datatable-row-def", ngImport: i0, template: `@if (rowDef.rowDefInternal.rowTemplate) {
<ng-container
[ngTemplateOutlet]="rowDef.rowDefInternal.rowTemplate"
[ngTemplateOutletContext]="rowContext"
/>
}`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableRowDefComponent, decorators: [{
type: Component,
args: [{
selector: 'datatable-row-def',
imports: [NgTemplateOutlet],
template: `@if (rowDef.rowDefInternal.rowTemplate) {
<ng-container
[ngTemplateOutlet]="rowDef.rowDefInternal.rowTemplate"
[ngTemplateOutletContext]="rowContext"
/>
}`
}]
}] });
class DatatableRowDefDirective {
static ngTemplateContextGuard(_dir, ctx) {
return true;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableRowDefDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: DatatableRowDefDirective, isStandalone: true, selector: "[rowDef]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableRowDefDirective, decorators: [{
type: Directive,
args: [{
selector: '[rowDef]'
}]
}] });
/**
* @internal To be used internally by ngx-datatable.
*/
class DatatableRowDefInternalDirective {
constructor() {
this.vc = inject(ViewContainerRef);
}
ngOnInit() {
this.vc.createEmbeddedView(this.rowDefInternal.template, {
...this.rowDefInternal
}, {
injector: Injector.create({
providers: [
{
provide: ROW_DEF_TOKEN,
useValue: this
}
]
})
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableRowDefInternalDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: DatatableRowDefInternalDirective, isStandalone: true, selector: "[rowDefInternal]", inputs: { rowDefInternal: "rowDefInternal", rowDefInternalDisabled: "rowDefInternalDisabled" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DatatableRowDefInternalDirective, decorators: [{
type: Directive,
args: [{
selector: '[rowDefInternal]'
}]
}], propDecorators: { rowDefInternal: [{
type: Input
}], rowDefInternalDisabled: [{
type: Input
}] } });
const ROW_DEF_TOKEN = new InjectionToken('RowDef');
class DataTableColumnCellDirective {
constructor() {
this.template = inject(TemplateRef);
}
static ngTemplateContextGuard(dir, ctx) {
return true;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnCellDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: DataTableColumnCellDirective, isStandalone: true, selector: "[ngx-datatable-cell-template]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnCellDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngx-datatable-cell-template]'
}]
}] });
class DataTableColumnGhostCellDirective {
static ngTemplateContextGuard(directive, context) {
return true;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnGhostCellDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: DataTableColumnGhostCellDirective, isStandalone: true, selector: "[ngx-datatable-ghost-cell-template]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnGhostCellDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngx-datatable-ghost-cell-template]'
}]
}] });
class DataTableColumnHeaderDirective {
static ngTemplateContextGuard(directive, context) {
return true;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: DataTableColumnHeaderDirective, isStandalone: true, selector: "[ngx-datatable-header-template]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnHeaderDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngx-datatable-header-template]'
}]
}] });
/**
* service to make DatatableComponent aware of changes to
* input bindings of DataTableColumnDirective
*/
class ColumnChangesService {
constructor() {
this.columnInputChanges = new Subject();
}
get columnInputChanges$() {
return this.columnInputChanges.asObservable();
}
onInputChange() {
this.columnInputChanges.next(undefined);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ColumnChangesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ColumnChangesService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ColumnChangesService, decorators: [{
type: Injectable
}] });
class DataTableColumnCellTreeToggle {
constructor() {
this.template = inject(TemplateRef);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnCellTreeToggle, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: DataTableColumnCellTreeToggle, isStandalone: true, selector: "[ngx-datatable-tree-toggle]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnCellTreeToggle, decorators: [{
type: Directive,
args: [{
selector: '[ngx-datatable-tree-toggle]'
}]
}] });
class DataTableColumnDirective {
constructor() {
this.columnChangesService = inject(ColumnChangesService);
this.isFirstChange = true;
}
get cellTemplate() {
return this._cellTemplateInput ?? this._cellTemplateQuery;
}
get headerTemplate() {
return this._headerTemplateInput ?? this._headerTemplateQuery;
}
get treeToggleTemplate() {
return this._treeToggleTemplateInput ?? this._treeToggleTemplateQuery;
}
get ghostCellTemplate() {
return this._ghostCellTemplateInput ?? this._ghostCellTemplateQuery;
}
ngOnChanges() {
if (this.isFirstChange) {
this.isFirstChange = false;
}
else {
this.columnChangesService.onInputChange();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.15", type: DataTableColumnDirective, isStandalone: true, selector: "ngx-datatable-column", inputs: { name: "name", prop: "prop", bindAsUnsafeHtml: ["bindAsUnsafeHtml", "bindAsUnsafeHtml", booleanAttribute], frozenLeft: ["frozenLeft", "frozenLeft", booleanAttribute], frozenRight: ["frozenRight", "frozenRight", booleanAttribute], flexGrow: ["flexGrow", "flexGrow", numberAttribute], resizeable: ["resizeable", "resizeable", booleanAttribute], comparator: "comparator", pipe: "pipe", sortable: ["sortable", "sortable", booleanAttribute], draggable: ["draggable", "draggable", booleanAttribute], canAutoResize: ["canAutoResize", "canAutoResize", booleanAttribute], minWidth: ["minWidth", "minWidth", numberAttribute], width: ["width", "width", numberAttribute], maxWidth: ["maxWidth", "maxWidth", numberAttribute], checkboxable: ["checkboxable", "checkboxable", booleanAttribute], headerCheckboxable: ["headerCheckboxable", "headerCheckboxable", booleanAttribute], headerClass: "headerClass", cellClass: "cellClass", isTreeColumn: ["isTreeColumn", "isTreeColumn", booleanAttribute], treeLevelIndent: "treeLevelIndent", summaryFunc: "summaryFunc", summaryTemplate: "summaryTemplate", _cellTemplateInput: ["cellTemplate", "_cellTemplateInput"], _headerTemplateInput: ["headerTemplate", "_headerTemplateInput"], _treeToggleTemplateInput: ["treeToggleTemplate", "_treeToggleTemplateInput"], _ghostCellTemplateInput: ["ghostCellTemplate", "_ghostCellTemplateInput"] }, queries: [{ propertyName: "_cellTemplateQuery", first: true, predicate: DataTableColumnCellDirective, descendants: true, read: TemplateRef, static: true }, { propertyName: "_headerTemplateQuery", first: true, predicate: DataTableColumnHeaderDirective, descendants: true, read: TemplateRef, static: true }, { propertyName: "_treeToggleTemplateQuery", first: true, predicate: DataTableColumnCellTreeToggle, descendants: true, read: TemplateRef, static: true }, { propertyName: "_ghostCellTemplateQuery", first: true, predicate: DataTableColumnGhostCellDirective, descendants: true, read: TemplateRef, static: true }], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DataTableColumnDirective, decorators: [{
type: Directive,
args: [{
selector: 'ngx-datatable-column'
}]
}], propDecorators: { name: [{
type: Input
}], prop: [{
type: Input
}], bindAsUnsafeHtml: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], frozenLeft: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], frozenRight: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], flexGrow: [{
type: Input,
args: [{ transform: numberAttribute }]
}], resizeable: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], comparator: [{
type: Input
}], pipe: [{
type: Input
}], sortable: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], draggable: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], canAutoResize: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], minWidth: [{
type: Input,
args: [{ transform: numberAttribute }]
}], width: [{
type: Input,
args: [{ transform: numberAttribute }]
}], maxWidth: [{
type: Input,
args: [{ transform: numberAttribute }]
}], checkboxable: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], headerCheckboxable: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], headerClass: [{
type: Input
}], cellClass: [{
type: Input
}], isTreeColumn: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], treeLevelIndent: [{
type: Input
}], summaryFunc: [{
type: Input
}], summaryTemplate: [{
type: Input
}], _cellTemplateInput: [{
type: Input,
args: ['cellTemplate']
}], _cellTemplateQuery: [{
type: ContentChild,
args: [DataTableColumnCellDirective, { read: TemplateRef, static: true }]
}], _headerTemplateInput: [{
type: Input,
args: ['headerTemplate']
}], _headerTemplateQuery: [{
type: ContentChild,
args: [DataTableColumnHeaderDirective, { read: TemplateRef, static: true }]
}], _treeToggleTemplateInput: [{
type: Input,
args: ['treeToggleTemplate']
}], _treeToggleTemplateQuery: [{
type: ContentChild,
args: [DataTableColumnCellTreeToggle, { read: TemplateRef, static: true }]
}], _ghostCellTemplateInput: [{
type: Input,
args: ['ghostCellTemplate']
}], _ghostCellTemplateQuery: [{
type: ContentChild,
args: [DataTableColumnGhostCellDirective, { read: TemplateRef, static: true }]
}] } });
/**
* Visibility Observer Directive
*
* Usage:
*
* <div
* visibilityObserver
* (visible)="onVisible($event)">
* </div>
*
*/
class VisibilityDirective {
constructor() {
this.element = inject(ElementRef);
this.zone = inject(NgZone);
this.isVisible = signal(false);
this.visible = output();
}
ngOnInit() {
this.runCheck();
}
ngOnDestroy() {
clearTimeout(this.timeout);
}
onVisibilityChange() {
// trigger zone recalc for columns
this.zone.run(() => {
this.isVisible.set(true);
this.visible.emit(true);
});
}
runCheck() {
const check = () => {
// https://davidwalsh.name/offsetheight-visibility
const { offsetHeight, offsetWidth } = this.element.nativeElement;
if (offsetHeight && offsetWidth) {
clearTimeout(this.timeout);
this.onVisibilityChange();
}
else {
clearTimeout(this.timeout);
this.zone.runOutsideAngular(() => {
this.timeout = window.setTimeout(() => check(), 50);
});
}
};
this.timeout = window.setTimeout(() => check());
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: VisibilityDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: VisibilityDirective, isStandalone: true, selector: "[visibilityObserver]", outputs: { visible: "visible" }, host: { properties: { "class.visible": "isVisible()" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: VisibilityDirective, decorators: [{
type: Directive,
args: [{
selector: '[visibilityObserver]',
host: {
'[class.visible]': 'isVisible()'
}
}]
}] });
const NGX_DATATABLE_CONFIG = new InjectionToken('ngx-datatable.config');
/**
* Provides a global configuration for ngx-datatable.
*
* @param overrides The overrides of the table configuration.
*/
const providedNgxDatatableConfig = (overrides) => {
return {
provide: NGX_DATATABLE_CONFIG,
useValue: overrides
};
};
/**
* Gets the width of the scrollbar. Nesc for windows
* http://stackoverflow.com/a/13382873/888165
*/
class ScrollbarHelper {
constructor() {
this.document = inject(DOCUMENT);
this.width = this.getWidth();
}
getWidth() {
const outer = this.document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.width = '100px';
this.document.body.appendChild(outer);
const widthNoScroll = outer.offsetWidth;
outer.style.overflow = 'scroll';
const inner = this.document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);
const widthWithScroll = inner.offsetWidth;
this.document.body.removeChild(outer);
return widthNoScroll - widthWithScroll;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ScrollbarHelper, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ScrollbarHelper, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ScrollbarHelper, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* @deprecated The constant `SortDirection` should no longer be used. Instead use the value directly:
* ```
* // old
* const sortDir: SortDirection = SortDirection.asc;
* // new
* const sortDir: SortDirection = 'asc';
* ```
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const SortDirection = {
asc: 'asc',
desc: 'desc'
};
/**
* @deprecated The constant `SortType` should no longer be used. Instead use the value directly:
* ```
* // old
* const sortType: SortType = SortType.single;
* // new
* const sortType: SortType = 'single';
* ```
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const SortType = {
single: 'single',
multi: 'multi'
};
/**
* @deprecated The constant `ColumnMode` should no longer be used. Instead use the value directly:
* ```
* // old
* <ngx-datatable [columnMode]="ColumnMode.force"></ngx-datatable>
* // new
* <ngx-datatable [columnMode]="'force'"></ngx-datatable>
* ```
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const ColumnMode = {
standard: 'standard',
flex: 'flex',
force: 'force'
};
/**
* @deprecated The constant `ContextmenuType` should no longer be used. Instead use the value directly:
* ```
* // old
* const contextmenuType: ContextmenuType = ContextmenuType.header;
* // new
* const contextmenuType: ContextmenuType = 'header';
* ```
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const ContextmenuType = {
header: 'header',
body: 'body'
};
/**
* @deprecated The constant `SelectionType` should no longer be used. Instead use the value directly:
* ```
* // old
* <ngx-datatable [selectionType]="SelectionType.multi"></ngx-datatable>
* // new
* <ngx-datatable [selectionType]="'multi'"></ngx-datatable>
* ```
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const SelectionType = {
single: 'single',
multi: 'multi',
multiClick: 'multiClick',
cell: 'cell',
checkbox: 'checkbox'
};
/**
* Converts strings from something to camel case
* http://stackoverflow.com/questions/10425287/convert-dash-separated-string-to-camelcase
*/
const camelCase = (str) => {
// Replace special characters with a space
str = str.replace(/[^a-zA-Z0-9 ]/g, ' ');
// put a space before an uppercase letter
str = str.replace(/([a-z](?=[A-Z]))/g, '$1 ');
// Lower case first character and some other stuff
str = str
.replace(/([^a-zA-Z0-9 ])|^[0-9]+/g, '')
.trim()
.toLowerCase();
// uppercase characters preceded by a space or number
str = str.replace(/([ 0-9]+)([a-zA-Z])/g, (a, b, c) => {
return b.trim() + c.toUpperCase();
});
return str;
};
/**
* Converts strings from camel case to words
* http://stackoverflow.com/questions/7225407/convert-camelcasetext-to-camel-case-text
*/
const deCamelCase = (str) => {
return str.replace(/([A-Z])/g, match => ` ${match}`).replace(/^./, match => match.toUpperCase());
};
/**
* Always returns the empty string ''
*/
const emptyStringGetter = () => {
return '';
};
/**
* Returns the appropriate getter function for this kind of prop.
* If prop == null, returns the emptyStringGetter.
*/
const getterForProp = (prop) => {
// TODO requires better typing which will also involve adjust TableColum. So postponing it.
if (prop == null) {
return emptyStringGetter;
}
if (typeof prop === 'number') {
return numericIndexGetter;
}
else {
// deep or simple
if (prop.includes('.')) {
return deepValueGetter;
}
else {
return shallowValueGetter;
}
}
};
/**
* Returns the value at this numeric index.
* @param row array of values
* @param index numeric index
* @returns any or '' if invalid index
*/
const numericIndexGetter = (row, index) => {
if (row == null) {
return '';
}
// mimic behavior of deepValueGetter
if (!row || index == null) {
return row;
}
const value = row[index];
if (value == null) {
return '';
}
return value;
};
/**
* Returns the value of a field.
* (more efficient than deepValueGetter)
* @param obj object containing the field
* @param fieldName field name string
*/
const shallowValueGetter = (obj, fieldName) => {
if (obj == null) {
return '';
}
if (!obj || !fieldName) {
return obj;
}
const value = obj[fieldName];
if (value == null) {
return '';
}
return value;
};
/**
* Returns a deep object given a string. zoo['animal.type']
*/
const deepValueGetter = (obj, path) => {
if (obj == null) {
return '';
}
if (!obj || !path) {
return obj;
}
// check if path matches a root-level field
// { "a.b.c": 123 }
let current = obj[path];
if (current !== undefined) {
return current;
}
current = obj;
const splits = path.split('.');
if (splits.length) {
for (const split of splits) {
current = current[split];
// if found undefined, return empty string
if (current === undefined || current === null) {
return '';
}
}
}
return current;
};
/**
* Creates a unique object id.
* http://stackoverflow.com/questions/6248666/how-to-generate-short-uid-like-ax4j9z-in-js
*/
const id = () => {
return ('0000' + ((Math.random() * Math.pow(36, 4)) << 0).toString(36)).slice(-4);
};
/**
* Gets the next sort direction
*/
const nextSortDir = (sortType, current) => {
if (sortType === SortType.single) {
if (current === SortDirection.asc) {
return SortDirection.desc;
}
else {
return SortDirection.asc;
}
}
else {
if (!current) {
return SortDirection.asc;
}
else if (current === SortDirection.asc) {
return SortDirection.desc;
}
else if (current === SortDirection.desc) {
return undefined;
}
// avoid TS7030: Not all code paths return a value.
return undefined;
}
};
/**
* Adapted from fueld-ui on 6/216
* https://github.com/FuelInteractive/fuel-ui/tree/master/src/pipes/OrderBy
*/
const orderByComparator = (a, b) => {
if (a === null || typeof a === 'undefined') {
a = 0;
}
if (b === null || typeof b === 'undefined') {
b = 0;
}
if (a instanceof Date && b instanceof Date) {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
}
else if (isNaN(parseFloat(a)) || !isFinite(a) || isNaN(parseFloat(b)) || !isFinite(b)) {
// Convert to string in case of a=0 or b=0
a = String(a);
b = String(b);
// Isn't a number so lowercase the string to properly compare
if (a.toLowerCase() < b.toLowerCase()) {
return -1;
}
if (a.toLowerCase() > b.toLowerCase()) {
return 1;
}
}
else {
// Parse strings as numbers to compare properly
if (parseFloat(a) < parseFloat(b)) {
return -1;
}
if (parseFloat(a) > parseFloat(b)) {
return 1;
}
}
// equal each other
return 0;
};
/**
* creates a shallow copy of the `rows` input and returns the sorted copy. this function
* does not sort the `rows` argument in place
*/
const sortRows = (rows, columns, dirs, sortOnGroupHeader) => {
if (!rows) {
return [];
}
if (!dirs?.length || !columns) {
return [...rows];
}
const temp = [...rows];
const cols = columns.reduce((obj, col) => {
if (col.sortable) {
obj[col.prop] = col.comparator;
}
return obj;
}, {});
// cache valueGetter and compareFn so that they
// do not need to be looked-up in the sort function body
const cachedDirs = dirs.map(dir => {
// When sorting on group header, override prop to 'key'
const prop = sortOnGroupHeader?.prop === dir.prop ? 'key' : dir.prop;
const compareFn = cols[dir.prop];
return {
prop,
dir: dir.dir,
valueGetter: getterForProp(prop),
compareFn
};
});
return temp.sort((rowA, rowB) => {
for (const cachedDir of cachedDirs) {
// Get property and valuegetters for column to be sorted
const { prop, valueGetter } = cachedDir;
// Get A and B cell values from rows based on properties of the columns
const propA = valueGetter(rowA, prop);
const propB = valueGetter(rowB, prop);
// Compare function gets five parameters:
// Two cell values to be compared as propA and propB
// Two rows corresponding to the cells as rowA and rowB
// Direction of the sort for this column as SortDirection
// Compare can be a standard JS comparison function (a,b) => -1|0|1
// as additional parameters are silently ignored. The whole row and sort
// direction enable more complex sort logic.
const comparison = cachedDir.dir !== SortDirection.desc
? cachedDir.compareFn(propA, propB, rowA, rowB, cachedDir.dir)
: -cachedDir.compareFn(propA, propB, rowA, rowB, cachedDir.dir);
// Don't return 0 yet in case of needing to sort by next property
if (comparison !== 0) {
return comparison;
}
}
return 0;
});
};
const sortGroupedRows = (groupedRows, columns, dirs, sortOnGroupHeader) => {
if (sortOnGroupHeader) {
groupedRows = sortRows(groupedRows, columns, dirs, sortOnGroupHeader);
}
return groupedRows.map(group => ({ ...group, value: sortRows(group.value, columns, dirs) }));
};
const toInternalColumn = (columns, defaultColumnWidth = 150) => {
let hasTreeColumn = false;
// TS fails to infer the type here.
return columns.map(column => {
const prop = column.prop ?? (column.name ? camelCase(column.name) : undefined);
// Only one column should hold the tree view,
// Thus if multiple columns are provided with
// isTreeColumn as true, we take only the first one
const isTreeColumn = !!column.isTreeColumn && !hasTreeColumn;
hasTreeColumn = hasTreeColumn || isTreeColumn;
// TODO: add check if prop or name is provided if sorting is enabled.
return {
...column,
$$id: id(),
$$valueGetter: getterForProp(prop),
prop,
name: column.name ?? (prop ? deCamelCase(String(prop)) : ''),
resizeable: column.resizeable ?? true,
sortable: column.sortable ?? true,
comparator: column.comparator ?? orderByComparator,
draggable: column.draggable ?? true,
canAutoResize: column.canAutoResize ?? true,
width: column.width ?? defaultColumnWidth,
isTreeColumn,
// in case of the directive, those are getters, so call them explicitly.
headerTemplate: column.headerTemplate,
cellTemplate: column.cellTemplate,
summaryTemplate: column.summaryTemplate,
ghostCellTemplate: column.ghostCellTemplate,
treeToggleTemplate: column.treeToggleTemplate
}; // TS cannot cast here
});
};
/**
* Returns the columns by pin.
*/
const columnsByPin = (cols) => {
const ret = {
left: [],
center: [],
right: []
};
if (cols) {
for (const col of cols) {
if (col.frozenLeft) {
ret.left.push(col);
}
else if (col.frozenRight) {
ret.right.push(col);
}
else {
ret.center.push(col);
}
}
}
return ret;
};
/**
* Returns the widths of all group sets of a column
*/
const columnGroupWidths = (groups, all) => {
return {
left: columnTotalWidth(groups.left),
center: columnTotalWidth(groups.center),
right: columnTotalWidth(groups.right),
total: Math.floor(columnTotalWidth(all))
};
};
/**
* Calculates the total width of all columns
*/
const columnTotalWidth = (columns) => {
return columns?.reduce((total, column) => total + column.width, 0) ?? 0;
};
const columnsByPinArr = (val) => {
const colsByPin = columnsByPin(val);
return [
{ type: 'left', columns: colsByPin.left },
{ type: 'center', columns: colsByPin.center },
{ type: 'right', columns: colsByPin.right }
];
};
/**
* Calculates the Total Flex Grow
*/
const getTotalFlexGrow = (columns) => {
let totalFlexGrow = 0;
for (const c of columns) {
totalFlexGrow += c.flexGrow ?? 0;
}
return totalFlexGrow;
};
/**
* Adjusts the column widths.
* Inspired by: https://github.com/facebookarchive/fixed-data-table/blob/master/src/FixedDataTableWidthHelper.js
*/
const adjustColumnWidths = (allColumns, expectedWidth) => {
const columnsWidth = columnTotalWidth(allColumns);
const totalFlexGrow = getTotalFlexGrow(allColumns);
const colsByGroup = columnsByPin(allColumns);
if (columnsWidth !== expectedWidth) {
scaleColumns(colsByGroup, expectedWidth, totalFlexGrow);
}
};
/**
* Resizes columns based on the flexGrow property, while respecting manually set widths
*/
const scaleColumns = (colsByGroup, maxWidth, totalFlexGrow) => {
const columns = Object.values(colsByGroup).flat();
let remainingWidth = maxWidth;
// calculate total width and flexgrow points for columns that can be resized
for (const column of columns) {
if (column.$$oldWidth) {
// when manually resized, switch off auto-resize
column.canAutoResize = false;
}
if (!column.canAutoResize) {
remainingWidth -= column.width;
totalFlexGrow -= column.flexGrow ?? 0;
}
else {
column.width = 0;
}
}
const hasMinWidth = {};
// resize columns until no width is left to be distributed
do {
const widthPerFlexPoint = remainingWidth / totalFlexGrow;
remainingWidth = 0;
for (const column of columns) {
// if the column can be resize and it hasn't reached its minimum width yet
if (column.canAutoResize && !hasMinWidth[column.prop]) {
const newWidth = column.width + column.flexGrow * widthPerFlexPoint;
if (column.minWidth !== undefined && newWidth < column.minWidth) {
remainingWidth += newWidth - column.minWidth;
column.width = column.minWidth;
hasMinWidth[column.prop] = true;
}
else {
column.width = newWidth;
}
}
}
} while (remainingWidth !== 0);
// Adjust for any remaining offset in computed widths vs maxWidth
const totalWidthAchieved = columns.reduce((acc, col) => acc + col.width, 0);
const delta = maxWidth - totalWidthAchieved;
if (delta === 0) {
return;
}
// adjust the first column that can be auto-resized respecting the min/max widths
for (const col of columns.filter(c => c.canAutoResize).sort((a, b) => a.width - b.width)) {
if ((delta > 0 && (!col.maxWidth || col.width + delta <= col.maxWidth)) ||
(delta < 0 && (!col.minWidth || col.width + delta >= col.minWidth))) {
col.width += delta;
break;
}
}
};
/**
* Forces the width of the columns to
* distribute equally but overflowing when necessary
*
* Rules:
*
* - If combined withs are less than the total width of the grid,
* proportion the widths given the min / max / normal widths to fill the width.
*
* - If the combined widths, exceed the total width of the grid,
* use the standard widths.
*
* - If a column is resized, it should always use that width
*
* - The proportional widths should never fall below min size if specified.
*
* - If the grid starts off small but then becomes greater than the size ( + / - )
* the width should use the original width; not the newly proportioned widths.
*/
const forceFillColumnWidths = (allColumns, expectedWidth, startIdx, allowBleed, defaultColWidth = 150, verticalScrollWidth = 0) => {
const columnsToResize = allColumns
.slice(startIdx + 1, allColumns.length)
.filter(c => c.canAutoResize !== false);
let additionWidthPerColumn = 0;
let exceedsWindow = false;
let contentWidth = getContentWidth(allColumns, defaultColWidth);
let remainingWidth = expectedWidth - contentWidth;
const initialRemainingWidth = remainingWidth;
const columnsProcessed = [];
const remainingWidthLimit = 1; // when to stop
// This loop takes care of the
do {
additionWidthPerColumn = remainingWidth / columnsToResize.length;
exceedsWindow = contentWidth >= expectedWidth;
for (const column of columnsToResize) {
// don't bleed if the initialRemainingWidth is same as verticalScrollWidth
if (exceedsWindow && allowBleed && initialRemainingWidth !== -1 * verticalScrollWidth) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
column.width = column.width || defaultColWidth;
}
else {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const newSize = (column.width || defaultColWidth) + additionWidthPerColumn;
if (column.minWidth && newSize < column.minWidth) {
column.width = column.minWidth;
columnsProcessed.push(column);
}
else if (column.maxWidth && newSize > column.maxWidth) {
column.width = column.maxWidth;
columnsProcessed.push(column);
}
else {
column.width = newSize;
}
}
column.width = Math.max(0, column.width);
}
contentWidth = getContentWidth(allColumns, defaultColWidth);
remainingWidth = expectedWidth - contentWidth;
removeProcessedColumns(columnsToResize, columnsProcessed);
} while (remainingWidth > remainingWidthLimit && columnsToResize.length !== 0);
};
/**
* Remove the processed columns from the current active columns.
*/
const removeProcessedColumns = (columnsToResize, columnsProcessed) => {
for (const column of columnsProcessed) {
const index = columnsToResize.indexOf(column);
columnsToResize.splice(index, 1);
}
};
/**
* Gets the width of the columns
*/
const getContentWidth = (allColumns, defaultColWidth = 150) => {
let contentWidth = 0;
for (const column of allColumns) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
contentWidth += column.width || defaultColWidth;
}
return contentWidth;
};
/**
* Same as {@link numberAttribute} but returns `undefined` if the value is `undefined`.
* {@link numberAttribute} would return `NaN` in that case.
* @param value
*/
// Must be a function.
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function numberOrUndefinedAttribute(value) {
if (value === undefined) {
return undefined;
}
return numberAttribute(value);
}
/**
* This token is created to break cycling import error which occurs when we import
* DatatableComponent in DataTableRowWrapperComponent.
*/
const DATATABLE_COMPONENT_TOKEN = new InjectionToken('DatatableComponentToken');
/**
* Throttle a function
*/
const throttle = (func, wait, options) => {
options ??= {};
let args;
let result;
let timeout = null;
let previous = 0;
const later = () => {
previous = options.leading === false ? 0 : +new Date();
timeout = null;
result = func(...args);
};
return (...argsNew) => {
const now = +new Date();
if (!previous && options.leading === false) {
previous = now;
}
const remaining = wait - (now - previous);
args = argsNew;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func(...args);
}
else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
/**
* Throttle decorator
*
* class MyClass {
* throttleable(10)
* myFn() { ... }
* }
*/
const throttleable = (duration, options) => {
return (target, key, descriptor) => {
return {
configurable: true,
enumerable: descriptor.enumerable,
get: function () {
Object.defineProperty(this, key, {
configurable: true,
enumerable: descriptor.enumerable,
value: throttle(descriptor.value.bind(this), duration, options)
});
return this[key];
}
};
};
};
const optionalGetterForProp = (prop) => {
return prop ? row => getterForProp(prop)(row, prop) : undefined;
};
/**
* This functions rearrange items by their parents
* Also sets the level value to each of the items
*
* Note: Expecting each item has a property called parentId
* Note: This algorithm will fail if a list has two or more items with same ID
* NOTE: This algorithm will fail if there is a deadlock of relationship
*
* For example,
*
* Input
*
* id -> parent
* 1 -> 0
* 2 -> 0
* 3 -> 1
* 4 -> 1
* 5 -> 2
* 7 -> 8
* 6 -> 3
*
*
* Output
* id -> level
* 1 -> 0
* --3 -> 1
* ----6 -> 2
* --4 -> 1
* 2 -> 0
* --5 -> 1
* 7 -> 8
*
*
* @param rows
*
*/
const groupRowsByParents = (rows, from, to) => {
if (from && to) {
const treeRows = rows.filter(row => !!row).map(row => new TreeNode(row));
const uniqIDs = new Map(treeRows.map(node => [to(node.row), node]));
const rootNodes = treeRows.reduce((root, node) => {
const fromValue = from(node.row);
const parent = uniqIDs.get(fromValue);
if (parent) {
node.row.level = parent.row.level + 1; // TODO: should be reflected by type, that level is defined
node.parent = parent;
parent.children.push(node);
}
else {
node.row.level = 0;
root.push(node);
}
return root;
}, []);
return rootNodes.flatMap(child => child.flatten());
}
else {
return rows;
}
};
class TreeNode {
constructor(row) {
this.row = row;
this.children = [];
}
flatten() {
if (this.row.treeStatus === 'expanded') {
return [this.row, ...this.children.flatMap(child => child.flatten())];
}
else {
return [this.row];
}
}
}
const ARROW_UP = 'ArrowUp';
const ARROW_DOWN = 'ArrowDown';
const ENTER = 'Enter';
const ESCAPE = 'Escape';
const ARROW_LEFT = 'ArrowLeft';
const ARROW_RIGHT = 'ArrowRight';
/**
* This object contains the cache of the various row heights that are present inside
* the data table. Its based on Fenwick tree data structure that helps with
* querying sums that have time complexity of log n.
*
* Fenwick Tree Credits: http://petr-mitrichev.blogspot.com/2013/05/fenwick-tree-range-updates.html
* https://github.com/mikolalysenko/fenwick-tree
*
*/
class RowHeightCache {
constructor() {
/**
* Tree Array stores the cumulative information of the row heights to perform efficient
* range queries and updates. Currently the tree is initialized to the base row
* height instead of the detail row height.
*/
this.treeArray = [];
}
/**
* Clear the Tree array.
*/
clearCache() {
this.treeArray = [];
}
/**
* Initialize the Fenwick tree with row Heights.
*
* @param rows The array of rows which contain the expanded status.
* @param rowHeight The row height.
* @param detailRowHeight The detail row height.
*/
initCache(details) {
const { rows, rowHeight, detailRowHeight, externalVirtual, indexOffset, rowCount, rowExpansions } = details;
const isFn = typeof rowHeight === 'function';
const isDetailFn = typeof detailRowHeight === 'function';
if (!isFn && isNaN(rowHeight)) {
throw new Error(`Row Height cache initialization failed. Please ensure that 'rowHeight' is a
valid number or function value: (${rowHeight}) when 'scrollbarV' is enabled.`);
}
// Add this additional guard in case detailRowHeight is set to 'auto' as it wont work.
if (!isDetailFn && isNaN(detailRowHeight)) {
throw new Error(`Row Height cache initialization failed. Please ensure that 'detailRowHeight' is a
valid number or function value: (${detailRowHeight}) when 'scrollbarV' is enabled.`);
}
const n = externalVirtual ? rowCount : rows.length;
this.treeArray = new Array(n);
for (let i = 0; i < n; ++i) {
this.treeArray[i] = 0;
}
for (let i = 0; i < n; ++i) {
const row = rows[i];
let currentRowHeight = rowHeight;
if (isFn) {
currentRowHeight = rowHeight(row);
}
// Add the detail row height to the already expanded rows.
// This is useful for the table that goes through a filter or sort.
const expanded = rowExpansions.has(row);
if (row && expanded) {
if (isDetailFn) {
const index = indexOffset + i;
currentRowHeight += detailRowHeight(row, index);
}
else {
currentRowHeight += detailRowHeight;
}
}
this.update(i, currentRowHeight);
}
}
/**
* Given the ScrollY position i.e. sum, provide the rowIndex
* that is present in the current view port. Below handles edge cases.
*/
getRowIndex(scrollY) {
if (scrollY === 0) {
return 0;
}
return this.calcRowIndex(scrollY);
}
/**
* When a row is expanded or rowHeight is changed, update the height. This can
* be utilized in future when Angular Data table supports dynamic row heights.
*/
update(atRowIndex, byRowHeight) {
if (!this.treeArray.length) {
throw new Error(`Update at index ${atRowIndex} with value ${byRowHeight} failed:
Row Height cache not initialized.`);
}
const n = this.treeArray.length;
atRowIndex |= 0;