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