UNPKG

@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
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;