UNPKG

@swimlane/ngx-datatable

Version:

ngx-datatable is an Angular table grid component for presenting large and complex data.

1,582 lines (1,549 loc) 180 kB
import { Injectable, Inject, Directive, TemplateRef, EventEmitter, ElementRef, NgZone, HostBinding, Output, Input, Renderer2, HostListener, KeyValueDiffers, ContentChildren, Component, ChangeDetectionStrategy, ContentChild, ChangeDetectorRef, ViewChild, ViewEncapsulation, SkipSelf, Optional, ViewContainerRef, NgModule } from '@angular/core'; import { DOCUMENT, CommonModule } from '@angular/common'; import { Subject, fromEvent, BehaviorSubject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { __decorate } from 'tslib'; /** * Gets the width of the scrollbar. Nesc for windows * http://stackoverflow.com/a/13382873/888165 */ class ScrollbarHelper { constructor(document) { this.document = document; this.width = this.getWidth(); } getWidth() { const outer = this.document.createElement('div'); outer.style.visibility = 'hidden'; outer.style.width = '100px'; outer.style.msOverflowStyle = 'scrollbar'; 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; outer.parentNode.removeChild(outer); return widthNoScroll - widthWithScroll; } } ScrollbarHelper.decorators = [ { type: Injectable } ]; ScrollbarHelper.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] } ]; /** * Gets the width of the scrollbar. Nesc for windows * http://stackoverflow.com/a/13382873/888165 */ class DimensionsHelper { getDimensions(element) { return element.getBoundingClientRect(); } } DimensionsHelper.decorators = [ { type: Injectable } ]; /** * 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(); } } ColumnChangesService.decorators = [ { type: Injectable } ]; class DataTableFooterTemplateDirective { constructor(template) { this.template = template; } } DataTableFooterTemplateDirective.decorators = [ { type: Directive, args: [{ selector: '[ngx-datatable-footer-template]' },] } ]; DataTableFooterTemplateDirective.ctorParameters = () => [ { type: TemplateRef } ]; /** * Visibility Observer Directive * * Usage: * * <div * visibilityObserver * (visible)="onVisible($event)"> * </div> * */ class VisibilityDirective { constructor(element, zone) { this.element = element; this.zone = zone; this.isVisible = false; this.visible = new EventEmitter(); } ngOnInit() { this.runCheck(); } ngOnDestroy() { clearTimeout(this.timeout); } onVisibilityChange() { // trigger zone recalc for columns this.zone.run(() => { this.isVisible = 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 = setTimeout(() => check(), 50); }); } }; this.timeout = setTimeout(() => check()); } } VisibilityDirective.decorators = [ { type: Directive, args: [{ selector: '[visibilityObserver]' },] } ]; VisibilityDirective.ctorParameters = () => [ { type: ElementRef }, { type: NgZone } ]; VisibilityDirective.propDecorators = { isVisible: [{ type: HostBinding, args: ['class.visible',] }], visible: [{ type: Output }] }; /** * Draggable Directive for Angular2 * * Inspiration: * https://github.com/AngularClass/angular2-examples/blob/master/rx-draggable/directives/draggable.ts * http://stackoverflow.com/questions/35662530/how-to-implement-drag-and-drop-in-angular2 * */ class DraggableDirective { constructor(element) { this.dragX = true; this.dragY = true; this.dragStart = new EventEmitter(); this.dragging = new EventEmitter(); this.dragEnd = new EventEmitter(); this.isDragging = false; this.element = element.nativeElement; } ngOnChanges(changes) { if (changes['dragEventTarget'] && changes['dragEventTarget'].currentValue && this.dragModel.dragging) { this.onMousedown(changes['dragEventTarget'].currentValue); } } ngOnDestroy() { this._destroySubscription(); } onMouseup(event) { if (!this.isDragging) return; this.isDragging = false; this.element.classList.remove('dragging'); if (this.subscription) { this._destroySubscription(); this.dragEnd.emit({ event, element: this.element, model: this.dragModel }); } } onMousedown(event) { // we only want to drag the inner header text const isDragElm = event.target.classList.contains('draggable'); if (isDragElm && (this.dragX || this.dragY)) { event.preventDefault(); this.isDragging = true; const mouseDownPos = { x: event.clientX, y: event.clientY }; const mouseup = fromEvent(document, 'mouseup'); this.subscription = mouseup.subscribe((ev) => this.onMouseup(ev)); const mouseMoveSub = fromEvent(document, 'mousemove') .pipe(takeUntil(mouseup)) .subscribe((ev) => this.move(ev, mouseDownPos)); this.subscription.add(mouseMoveSub); this.dragStart.emit({ event, element: this.element, model: this.dragModel }); } } move(event, mouseDownPos) { if (!this.isDragging) return; const x = event.clientX - mouseDownPos.x; const y = event.clientY - mouseDownPos.y; if (this.dragX) this.element.style.left = `${x}px`; if (this.dragY) this.element.style.top = `${y}px`; this.element.classList.add('dragging'); this.dragging.emit({ event, element: this.element, model: this.dragModel }); } _destroySubscription() { if (this.subscription) { this.subscription.unsubscribe(); this.subscription = undefined; } } } DraggableDirective.decorators = [ { type: Directive, args: [{ selector: '[draggable]' },] } ]; DraggableDirective.ctorParameters = () => [ { type: ElementRef } ]; DraggableDirective.propDecorators = { dragEventTarget: [{ type: Input }], dragModel: [{ type: Input }], dragX: [{ type: Input }], dragY: [{ type: Input }], dragStart: [{ type: Output }], dragging: [{ type: Output }], dragEnd: [{ type: Output }] }; class ResizeableDirective { constructor(element, renderer) { this.renderer = renderer; this.resizeEnabled = true; this.resize = new EventEmitter(); this.resizing = false; this.element = element.nativeElement; } ngAfterViewInit() { const renderer2 = this.renderer; this.resizeHandle = renderer2.createElement('span'); if (this.resizeEnabled) { renderer2.addClass(this.resizeHandle, 'resize-handle'); } else { renderer2.addClass(this.resizeHandle, 'resize-handle--not-resizable'); } renderer2.appendChild(this.element, this.resizeHandle); } ngOnDestroy() { this._destroySubscription(); if (this.renderer.destroyNode) { this.renderer.destroyNode(this.resizeHandle); } else if (this.resizeHandle) { this.renderer.removeChild(this.renderer.parentNode(this.resizeHandle), this.resizeHandle); } } onMouseup() { this.resizing = false; if (this.subscription && !this.subscription.closed) { this._destroySubscription(); this.resize.emit(this.element.clientWidth); } } onMousedown(event) { const isHandle = event.target.classList.contains('resize-handle'); const initialWidth = this.element.clientWidth; const mouseDownScreenX = event.screenX; if (isHandle) { event.stopPropagation(); this.resizing = true; const mouseup = fromEvent(document, 'mouseup'); this.subscription = mouseup.subscribe((ev) => this.onMouseup()); const mouseMoveSub = fromEvent(document, 'mousemove') .pipe(takeUntil(mouseup)) .subscribe((e) => this.move(e, initialWidth, mouseDownScreenX)); this.subscription.add(mouseMoveSub); } } move(event, initialWidth, mouseDownScreenX) { const movementX = event.screenX - mouseDownScreenX; const newWidth = initialWidth + movementX; const overMinWidth = !this.minWidth || newWidth >= this.minWidth; const underMaxWidth = !this.maxWidth || newWidth <= this.maxWidth; if (overMinWidth && underMaxWidth) { this.element.style.width = `${newWidth}px`; } } _destroySubscription() { if (this.subscription) { this.subscription.unsubscribe(); this.subscription = undefined; } } } ResizeableDirective.decorators = [ { type: Directive, args: [{ selector: '[resizeable]', host: { '[class.resizeable]': 'resizeEnabled' } },] } ]; ResizeableDirective.ctorParameters = () => [ { type: ElementRef }, { type: Renderer2 } ]; ResizeableDirective.propDecorators = { resizeEnabled: [{ type: Input }], minWidth: [{ type: Input }], maxWidth: [{ type: Input }], resize: [{ type: Output }], onMousedown: [{ type: HostListener, args: ['mousedown', ['$event'],] }] }; class OrderableDirective { constructor(differs, document) { this.document = document; this.reorder = new EventEmitter(); this.targetChanged = new EventEmitter(); this.differ = differs.find({}).create(); } ngAfterContentInit() { // HACK: Investigate Better Way this.updateSubscriptions(); this.draggables.changes.subscribe(this.updateSubscriptions.bind(this)); } ngOnDestroy() { this.draggables.forEach(d => { d.dragStart.unsubscribe(); d.dragging.unsubscribe(); d.dragEnd.unsubscribe(); }); } updateSubscriptions() { const diffs = this.differ.diff(this.createMapDiffs()); if (diffs) { const subscribe = ({ currentValue, previousValue }) => { unsubscribe({ previousValue }); if (currentValue) { currentValue.dragStart.subscribe(this.onDragStart.bind(this)); currentValue.dragging.subscribe(this.onDragging.bind(this)); currentValue.dragEnd.subscribe(this.onDragEnd.bind(this)); } }; const unsubscribe = ({ previousValue }) => { if (previousValue) { previousValue.dragStart.unsubscribe(); previousValue.dragging.unsubscribe(); previousValue.dragEnd.unsubscribe(); } }; diffs.forEachAddedItem(subscribe); // diffs.forEachChangedItem(subscribe.bind(this)); diffs.forEachRemovedItem(unsubscribe); } } onDragStart() { this.positions = {}; let i = 0; for (const dragger of this.draggables.toArray()) { const elm = dragger.element; const left = parseInt(elm.offsetLeft.toString(), 0); this.positions[dragger.dragModel.prop] = { left, right: left + parseInt(elm.offsetWidth.toString(), 0), index: i++, element: elm }; } } onDragging({ element, model, event }) { const prevPos = this.positions[model.prop]; const target = this.isTarget(model, event); if (target) { if (this.lastDraggingIndex !== target.i) { this.targetChanged.emit({ prevIndex: this.lastDraggingIndex, newIndex: target.i, initialIndex: prevPos.index }); this.lastDraggingIndex = target.i; } } else if (this.lastDraggingIndex !== prevPos.index) { this.targetChanged.emit({ prevIndex: this.lastDraggingIndex, initialIndex: prevPos.index }); this.lastDraggingIndex = prevPos.index; } } onDragEnd({ element, model, event }) { const prevPos = this.positions[model.prop]; const target = this.isTarget(model, event); if (target) { this.reorder.emit({ prevIndex: prevPos.index, newIndex: target.i, model }); } this.lastDraggingIndex = undefined; element.style.left = 'auto'; } isTarget(model, event) { let i = 0; const x = event.x || event.clientX; const y = event.y || event.clientY; const targets = this.document.elementsFromPoint(x, y); for (const prop in this.positions) { // current column position which throws event. const pos = this.positions[prop]; // since we drag the inner span, we need to find it in the elements at the cursor if (model.prop !== prop && targets.find((el) => el === pos.element)) { return { pos, i }; } i++; } } createMapDiffs() { return this.draggables.toArray().reduce((acc, curr) => { acc[curr.dragModel.$$id] = curr; return acc; }, {}); } } OrderableDirective.decorators = [ { type: Directive, args: [{ selector: '[orderable]' },] } ]; OrderableDirective.ctorParameters = () => [ { type: KeyValueDiffers }, { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] } ]; OrderableDirective.propDecorators = { reorder: [{ type: Output }], targetChanged: [{ type: Output }], draggables: [{ type: ContentChildren, args: [DraggableDirective, { descendants: true },] }] }; class LongPressDirective { constructor() { this.pressEnabled = true; this.duration = 500; this.longPressStart = new EventEmitter(); this.longPressing = new EventEmitter(); this.longPressEnd = new EventEmitter(); this.mouseX = 0; this.mouseY = 0; } get press() { return this.pressing; } get isLongPress() { return this.isLongPressing; } onMouseDown(event) { // don't do right/middle clicks if (event.which !== 1 || !this.pressEnabled) return; // don't start drag if its on resize handle const target = event.target; if (target.classList.contains('resize-handle')) return; this.mouseX = event.clientX; this.mouseY = event.clientY; this.pressing = true; this.isLongPressing = false; const mouseup = fromEvent(document, 'mouseup'); this.subscription = mouseup.subscribe((ev) => this.onMouseup()); this.timeout = setTimeout(() => { this.isLongPressing = true; this.longPressStart.emit({ event, model: this.pressModel }); this.subscription.add(fromEvent(document, 'mousemove') .pipe(takeUntil(mouseup)) .subscribe((mouseEvent) => this.onMouseMove(mouseEvent))); this.loop(event); }, this.duration); this.loop(event); } onMouseMove(event) { if (this.pressing && !this.isLongPressing) { const xThres = Math.abs(event.clientX - this.mouseX) > 10; const yThres = Math.abs(event.clientY - this.mouseY) > 10; if (xThres || yThres) { this.endPress(); } } } loop(event) { if (this.isLongPressing) { this.timeout = setTimeout(() => { this.longPressing.emit({ event, model: this.pressModel }); this.loop(event); }, 50); } } endPress() { clearTimeout(this.timeout); this.isLongPressing = false; this.pressing = false; this._destroySubscription(); this.longPressEnd.emit({ model: this.pressModel }); } onMouseup() { this.endPress(); } ngOnDestroy() { this._destroySubscription(); } _destroySubscription() { if (this.subscription) { this.subscription.unsubscribe(); this.subscription = undefined; } } } LongPressDirective.decorators = [ { type: Directive, args: [{ selector: '[long-press]' },] } ]; LongPressDirective.propDecorators = { pressEnabled: [{ type: Input }], pressModel: [{ type: Input }], duration: [{ type: Input }], longPressStart: [{ type: Output }], longPressing: [{ type: Output }], longPressEnd: [{ type: Output }], press: [{ type: HostBinding, args: ['class.press',] }], isLongPress: [{ type: HostBinding, args: ['class.longpress',] }], onMouseDown: [{ type: HostListener, args: ['mousedown', ['$event'],] }] }; class ScrollerComponent { constructor(ngZone, element, renderer) { this.ngZone = ngZone; this.renderer = renderer; this.scrollbarV = false; this.scrollbarH = false; this.scroll = new EventEmitter(); this.scrollYPos = 0; this.scrollXPos = 0; this.prevScrollYPos = 0; this.prevScrollXPos = 0; this._scrollEventListener = null; this.element = element.nativeElement; } ngOnInit() { // manual bind so we don't always listen if (this.scrollbarV || this.scrollbarH) { const renderer = this.renderer; this.parentElement = renderer.parentNode(renderer.parentNode(this.element)); this._scrollEventListener = this.onScrolled.bind(this); this.parentElement.addEventListener('scroll', this._scrollEventListener); } } ngOnDestroy() { if (this._scrollEventListener) { this.parentElement.removeEventListener('scroll', this._scrollEventListener); this._scrollEventListener = null; } } setOffset(offsetY) { if (this.parentElement) { this.parentElement.scrollTop = offsetY; } } onScrolled(event) { const dom = event.currentTarget; requestAnimationFrame(() => { this.scrollYPos = dom.scrollTop; this.scrollXPos = dom.scrollLeft; this.updateOffset(); }); } updateOffset() { let direction; if (this.scrollYPos < this.prevScrollYPos) { direction = 'down'; } else if (this.scrollYPos > this.prevScrollYPos) { direction = 'up'; } this.scroll.emit({ direction, scrollYPos: this.scrollYPos, scrollXPos: this.scrollXPos }); this.prevScrollYPos = this.scrollYPos; this.prevScrollXPos = this.scrollXPos; } } ScrollerComponent.decorators = [ { type: Component, args: [{ selector: 'datatable-scroller', template: ` <ng-content></ng-content> `, host: { class: 'datatable-scroll' }, changeDetection: ChangeDetectionStrategy.OnPush },] } ]; ScrollerComponent.ctorParameters = () => [ { type: NgZone }, { type: ElementRef }, { type: Renderer2 } ]; ScrollerComponent.propDecorators = { scrollbarV: [{ type: Input }], scrollbarH: [{ type: Input }], scrollHeight: [{ type: HostBinding, args: ['style.height.px',] }, { type: Input }], scrollWidth: [{ type: HostBinding, args: ['style.width.px',] }, { type: Input }], scroll: [{ type: Output }] }; class DatatableGroupHeaderTemplateDirective { constructor(template) { this.template = template; } } DatatableGroupHeaderTemplateDirective.decorators = [ { type: Directive, args: [{ selector: '[ngx-datatable-group-header-template]' },] } ]; DatatableGroupHeaderTemplateDirective.ctorParameters = () => [ { type: TemplateRef } ]; class DatatableGroupHeaderDirective { constructor() { /** * Row height is required when virtual scroll is enabled. */ this.rowHeight = 0; /** * 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 }); } } DatatableGroupHeaderDirective.decorators = [ { type: Directive, args: [{ selector: 'ngx-datatable-group-header' },] } ]; DatatableGroupHeaderDirective.propDecorators = { rowHeight: [{ type: Input }], _templateInput: [{ type: Input, args: ['template',] }], _templateQuery: [{ type: ContentChild, args: [DatatableGroupHeaderTemplateDirective, { read: TemplateRef, static: true },] }], toggle: [{ type: Output }] }; /** * Always returns the empty string '' */ function emptyStringGetter() { return ''; } /** * Returns the appropriate getter function for this kind of prop. * If prop == null, returns the emptyStringGetter. */ function getterForProp(prop) { if (prop == null) { return emptyStringGetter; } if (typeof prop === 'number') { return numericIndexGetter; } else { // deep or simple if (prop.indexOf('.') !== -1) { 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 */ function 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 */ function 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'] */ function 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 split = path.split('.'); if (split.length) { for (let i = 0; i < split.length; i++) { current = current[split[i]]; // if found undefined, return empty string if (current === undefined || current === null) { return ''; } } } return current; } function optionalGetterForProp(prop) { return prop && (row => getterForProp(prop)(row, prop)); } /** * 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 * */ function groupRowsByParents(rows, from, to) { if (from && to) { const nodeById = {}; const l = rows.length; let node = null; nodeById[0] = new TreeNode(); // that's the root node const uniqIDs = rows.reduce((arr, item) => { const toValue = to(item); if (arr.indexOf(toValue) === -1) { arr.push(toValue); } return arr; }, []); for (let i = 0; i < l; i++) { // make TreeNode objects for each item nodeById[to(rows[i])] = new TreeNode(rows[i]); } for (let i = 0; i < l; i++) { // link all TreeNode objects node = nodeById[to(rows[i])]; let parent = 0; const fromValue = from(node.row); if (!!fromValue && uniqIDs.indexOf(fromValue) > -1) { parent = fromValue; } node.parent = nodeById[parent]; node.row['level'] = node.parent.row['level'] + 1; node.parent.children.push(node); } let resolvedRows = []; nodeById[0].flatten(function () { resolvedRows = [...resolvedRows, this.row]; }, true); return resolvedRows; } else { return rows; } } class TreeNode { constructor(row = null) { if (!row) { row = { level: -1, treeStatus: 'expanded' }; } this.row = row; this.parent = null; this.children = []; } flatten(f, recursive) { if (this.row['treeStatus'] === 'expanded') { for (let i = 0, l = this.children.length; i < l; i++) { const child = this.children[i]; f.apply(child, Array.prototype.slice.call(arguments, 2)); if (recursive) child.flatten.apply(child, arguments); } } } } /** * Converts strings from something to camel case * http://stackoverflow.com/questions/10425287/convert-dash-separated-string-to-camelcase */ function 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, function (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 */ function deCamelCase(str) { return str.replace(/([A-Z])/g, match => ` ${match}`).replace(/^./, match => match.toUpperCase()); } /** * Creates a unique object id. * http://stackoverflow.com/questions/6248666/how-to-generate-short-uid-like-ax4j9z-in-js */ function id() { return ('0000' + ((Math.random() * Math.pow(36, 4)) << 0).toString(36)).slice(-4); } /** * Sets the column defaults */ function setColumnDefaults(columns) { if (!columns) return; // Only one column should hold the tree view // Thus if multiple columns are provided with // isTreeColumn as true we take only the first one let treeColumnFound = false; for (const column of columns) { if (!column.$$id) { column.$$id = id(); } // prop can be numeric; zero is valid not a missing prop // translate name => prop if (isNullOrUndefined(column.prop) && column.name) { column.prop = camelCase(column.name); } if (!column.$$valueGetter) { column.$$valueGetter = getterForProp(column.prop); } // format props if no name passed if (!isNullOrUndefined(column.prop) && isNullOrUndefined(column.name)) { column.name = deCamelCase(String(column.prop)); } if (isNullOrUndefined(column.prop) && isNullOrUndefined(column.name)) { column.name = ''; // Fixes IE and Edge displaying `null` } if (!column.hasOwnProperty('resizeable')) { column.resizeable = true; } if (!column.hasOwnProperty('sortable')) { column.sortable = true; } if (!column.hasOwnProperty('draggable')) { column.draggable = true; } if (!column.hasOwnProperty('canAutoResize')) { column.canAutoResize = true; } if (!column.hasOwnProperty('width')) { column.width = 150; } if (!column.hasOwnProperty('isTreeColumn')) { column.isTreeColumn = false; } else { if (column.isTreeColumn && !treeColumnFound) { // If the first column with isTreeColumn is true found // we mark that treeCoulmn is found treeColumnFound = true; } else { // After that isTreeColumn property for any other column // will be set as false column.isTreeColumn = false; } } } } function isNullOrUndefined(value) { return value === null || value === undefined; } /** * Translates templates definitions to objects */ function translateTemplates(templates) { const result = []; for (const temp of templates) { const col = {}; const props = Object.getOwnPropertyNames(temp); for (const prop of props) { col[prop] = temp[prop]; } if (temp.headerTemplate) { col.headerTemplate = temp.headerTemplate; } if (temp.cellTemplate) { col.cellTemplate = temp.cellTemplate; } if (temp.summaryFunc) { col.summaryFunc = temp.summaryFunc; } if (temp.summaryTemplate) { col.summaryTemplate = temp.summaryTemplate; } result.push(col); } return result; } var ColumnMode; (function (ColumnMode) { ColumnMode["standard"] = "standard"; ColumnMode["flex"] = "flex"; ColumnMode["force"] = "force"; })(ColumnMode || (ColumnMode = {})); var SelectionType; (function (SelectionType) { SelectionType["single"] = "single"; SelectionType["multi"] = "multi"; SelectionType["multiClick"] = "multiClick"; SelectionType["cell"] = "cell"; SelectionType["checkbox"] = "checkbox"; })(SelectionType || (SelectionType = {})); var SortType; (function (SortType) { SortType["single"] = "single"; SortType["multi"] = "multi"; })(SortType || (SortType = {})); var ContextmenuType; (function (ContextmenuType) { ContextmenuType["header"] = "header"; ContextmenuType["body"] = "body"; })(ContextmenuType || (ContextmenuType = {})); class DataTableColumnHeaderDirective { constructor(template) { this.template = template; } } DataTableColumnHeaderDirective.decorators = [ { type: Directive, args: [{ selector: '[ngx-datatable-header-template]' },] } ]; DataTableColumnHeaderDirective.ctorParameters = () => [ { type: TemplateRef } ]; class DataTableColumnCellDirective { constructor(template) { this.template = template; } } DataTableColumnCellDirective.decorators = [ { type: Directive, args: [{ selector: '[ngx-datatable-cell-template]' },] } ]; DataTableColumnCellDirective.ctorParameters = () => [ { type: TemplateRef } ]; class DataTableColumnCellTreeToggle { constructor(template) { this.template = template; } } DataTableColumnCellTreeToggle.decorators = [ { type: Directive, args: [{ selector: '[ngx-datatable-tree-toggle]' },] } ]; DataTableColumnCellTreeToggle.ctorParameters = () => [ { type: TemplateRef } ]; class DataTableColumnDirective { constructor(columnChangesService) { this.columnChangesService = 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; } ngOnChanges() { if (this.isFirstChange) { this.isFirstChange = false; } else { this.columnChangesService.onInputChange(); } } } DataTableColumnDirective.decorators = [ { type: Directive, args: [{ selector: 'ngx-datatable-column' },] } ]; DataTableColumnDirective.ctorParameters = () => [ { type: ColumnChangesService } ]; DataTableColumnDirective.propDecorators = { name: [{ type: Input }], prop: [{ type: Input }], frozenLeft: [{ type: Input }], frozenRight: [{ type: Input }], flexGrow: [{ type: Input }], resizeable: [{ type: Input }], comparator: [{ type: Input }], pipe: [{ type: Input }], sortable: [{ type: Input }], draggable: [{ type: Input }], canAutoResize: [{ type: Input }], minWidth: [{ type: Input }], width: [{ type: Input }], maxWidth: [{ type: Input }], checkboxable: [{ type: Input }], headerCheckboxable: [{ type: Input }], headerClass: [{ type: Input }], cellClass: [{ type: Input }], isTreeColumn: [{ type: Input }], 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 },] }] }; class DatatableRowDetailTemplateDirective { constructor(template) { this.template = template; } } DatatableRowDetailTemplateDirective.decorators = [ { type: Directive, args: [{ selector: '[ngx-datatable-row-detail-template]' },] } ]; DatatableRowDetailTemplateDirective.ctorParameters = () => [ { type: TemplateRef } ]; class DatatableRowDetailDirective { constructor() { /** * The detail row height is required especially * when virtual scroll is enabled. */ this.rowHeight = 0; /** * Row detail row visbility was toggled. */ this.toggle = new EventEmitter(); } get template() { return this._templateInput || this._templateQuery; } /** * Toggle the expansion of the row */ toggleExpandRow(row) { this.toggle.emit({ type: 'row', value: row }); } /** * API method to expand all the rows. */ expandAllRows() { this.toggle.emit({ type: 'all', value: true }); } /** * API method to collapse all the rows. */ collapseAllRows() { this.toggle.emit({ type: 'all', value: false }); } } DatatableRowDetailDirective.decorators = [ { type: Directive, args: [{ selector: 'ngx-datatable-row-detail' },] } ]; DatatableRowDetailDirective.propDecorators = { rowHeight: [{ type: Input }], _templateInput: [{ type: Input, args: ['template',] }], _templateQuery: [{ type: ContentChild, args: [DatatableRowDetailTemplateDirective, { read: TemplateRef, static: true },] }], toggle: [{ type: Output }] }; class DatatableFooterDirective { get template() { return this._templateInput || this._templateQuery; } } DatatableFooterDirective.decorators = [ { type: Directive, args: [{ selector: 'ngx-datatable-footer' },] } ]; DatatableFooterDirective.propDecorators = { footerHeight: [{ type: Input }], totalMessage: [{ type: Input }], selectedMessage: [{ type: Input }], pagerLeftArrowIcon: [{ type: Input }], pagerRightArrowIcon: [{ type: Input }], pagerPreviousIcon: [{ type: Input }], pagerNextIcon: [{ type: Input }], _templateInput: [{ type: Input, args: ['template',] }], _templateQuery: [{ type: ContentChild, args: [DataTableFooterTemplateDirective, { read: TemplateRef },] }] }; /** * Returns the columns by pin. */ function 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 */ function 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 and their groups */ function columnTotalWidth(columns, prop) { let totalWidth = 0; if (columns) { for (const c of columns) { const has = prop && c[prop]; const width = has ? c[prop] : c.width; totalWidth = totalWidth + parseFloat(width); } } return totalWidth; } /** * Calculates the total width of all columns and their groups */ function columnsTotalWidth(columns, prop) { let totalWidth = 0; for (const column of columns) { const has = prop && column[prop]; totalWidth = totalWidth + (has ? column[prop] : column.width); } return totalWidth; } function columnsByPinArr(val) { const colsByPinArr = []; const colsByPin = columnsByPin(val); colsByPinArr.push({ type: 'left', columns: colsByPin['left'] }); colsByPinArr.push({ type: 'center', columns: colsByPin['center'] }); colsByPinArr.push({ type: 'right', columns: colsByPin['right'] }); return colsByPinArr; } /** * 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, rowCount, rowIndexes, 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 = rowIndexes.get(row); 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; while (atRowIndex < n) { this.treeArray[atRowIndex] += byRowHeight; atRowIndex |= atRowIndex + 1; } } /** * Range Sum query from 1 to the rowIndex */ query(atIndex) { if (!this.treeArray.length) { throw new Error(`query at index ${atIndex} failed: Fenwick tree array not initialized.`); } let sum = 0; atIndex |= 0; while (atIndex >= 0) { sum += this.treeArray[atIndex]; atIndex = (atIndex & (atIndex + 1)) - 1; } return sum; } /** * Find the total height between 2 row indexes */ queryBetween(atIndexA, atIndexB) { return this.query(atIndexB) - this.query(atIndexA - 1); } /** * Given the ScrollY position i.e. sum, provide the rowIndex * that is present in the current view port. */ calcRowIndex(sum) { if (!this.treeArray.length) return 0; let pos = -1; const dataLength = this.treeArray.length; // Get the highest bit for the block size. const highestBit = Math.pow(2, dataLength.toString(2).length - 1); for (let blockSize = highestBit; blockSize !== 0; blockSize >>= 1) { const nextPos = pos + blockSize; if (nextPos < dataLength && sum >= this.treeArray[nextPos]) { sum -= this.treeArray[nextPos]; pos = nextPos; } } return pos + 1; } } const cache = {}; const testStyle = typeof document !== 'undefined' ? document.createElement('div').style : undefined; const ɵ0 = function () { const styles = typeof window !== 'undefined' ? window.getComputedStyle(document.documentElement, '') : undefined; const match = typeof styles !== 'undefined' ? Array.prototype.slice .call(styles) .join('') .match(/-(moz|webkit|ms)-/) : null; const pre = match !== null ? match[1] : undefined; // tslint:disable-next-line: tsr-detect-non-literal-regexp const dom = typeof pre !== 'undefined' ? 'WebKit|Moz|MS|O'.match(new RegExp('(' + pre + ')', 'i'))[1] : undefined; return dom ? { dom, lowercase: pre, css: `-${pre}-`, js: pre[0].toUpperCase() + pre.substr(1) } : undefined; }; // Get Prefix // http://davidwalsh.name/vendor-prefix const prefix = (ɵ0)(); function getVendorPrefixedName(property) { const name = camelCase(property); if (!cache[name]) { if (prefix !== undefined && testStyle[prefix.css + property] !== undefined) { cache[name] = prefix.css + property; } else if (testStyle[property] !== undefined) { cache[name] = property; } } return cache[name]; } // browser detection and prefixing tools const transform = typeof window !== 'undefined' ? getVendorPrefixedName('transform') : undefined; const backfaceVisibility = typeof window !== 'undefined' ? getVendorPrefixedName('backfaceVisibility') : undefined; const hasCSSTransforms = typeof window !== 'undefined' ? !!getVendorPrefixedName('transform') : undefined; const hasCSS3DTransforms = typeof window !== 'undefined' ? !!getVendorPrefixedName('perspective') : undefined; const ua = typeof window !== 'undefined' ? window.navigator.userAgent : 'Chrome'; const isSafari = /Safari\//.test(ua) && !/Chrome\//.test(ua); function translateXY(styles, x, y) { if (typeof transform !== 'undefined' && hasCSSTransforms) { if (!isSafari && hasCSS3DTransforms) { styles[transform] = `translate3d(${x}px, ${y}px, 0)`; styles[backfaceVisibility] = 'hidden'; } else { styles[camelCase(transform)] = `translate(${x}px, ${y}px)`; } } else { styles.top = `${y}px`; styles.left = `${x}px`; } } class DataTableBodyComponent { /** * Creates an instance of DataTableBodyComponent. */ constructor(cd) { this.cd = cd; this.selected = []; this.scroll = new EventEmitter(); this.page = new EventEmitter(); this.activate = new EventEmitter(); this.select = new EventEmitter(); this.detailToggle = new EventEmitter(); this.rowContextmenu = new EventEmitter(false); this.treeAction = new EventEmitter(); this.rowHeightsCache = new RowHeightCache(); this.temp = []; this.offsetY = 0; this.indexes = {}; this.rowIndexes = new WeakMap(); this.rowExpansions = []; /** * Get the height of the detail row. */ this.getDetailRowHeight = (row, index) => { if (!this.rowDetail) { return 0; } const rowHeight = this.rowDetail.rowHeight; return typeof rowHeight === 'function' ? rowHeight(row, index) : rowHeight; }; // declare fn here so we can get access to the `this` property this.rowTrackingFn = (index, row) => { const idx = this.getRowIndex(row); if (this.trackByProp) { return row[this.trackByProp]; } else { return idx; } }; } set pageSize(val) { this._pageSize = val; this.recalcLayout(); } get pageSize() { return this._pageSize; } set rows(val) { this._rows = val; this.recalcLayout(); } get rows() { return this._rows; } set columns(val) { this._columns = val; const colsByPin = columnsByPin(val); this.columnGroupWidths = columnGroupWidths(colsByPin, val); } get columns() { return this._columns; } set offset(val) { this._offset = val; if (!this.scrollbarV || (this.scrollbarV && !this.virtualization)) this.recalcLayout(); } get offset() { return this._offset; } set rowCount(val) { this._rowCount = val;