ng2-smart-table
Version:
Angular Smart Table
1,468 lines (1,439 loc) • 110 kB
JavaScript
import { EventEmitter, Component, Input, Output, ComponentFactoryResolver, ViewChild, ViewContainerRef, ChangeDetectionStrategy, NgModule, ElementRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, FormControl, NgControl, ReactiveFormsModule } from '@angular/forms';
import { CompleterService, Ng2CompleterModule } from 'ng2-completer';
import { Subject } from 'rxjs';
import { cloneDeep } from 'lodash';
import { debounceTime, map, distinctUntilChanged, skip, takeUntil } from 'rxjs/operators';
import { HttpParams } from '@angular/common/http';
/**
* Extending object that entered in first argument.
*
* Returns extended object or false if have no target object or incorrect type.
*
* If you wish to clone source object (without modify it), just use empty new
* object as first argument, like this:
* deepExtend({}, yourObj_1, [yourObj_N]);
*/
const deepExtend = function (...objects) {
if (arguments.length < 1 || typeof arguments[0] !== 'object') {
return false;
}
if (arguments.length < 2) {
return arguments[0];
}
const target = arguments[0];
// convert arguments to array and cut off target object
const args = Array.prototype.slice.call(arguments, 1);
let val, src;
args.forEach((obj) => {
// skip argument if it is array or isn't object
if (typeof obj !== 'object' || Array.isArray(obj)) {
return;
}
Object.keys(obj).forEach(function (key) {
src = target[key]; // source value
val = obj[key]; // new value
// recursion prevention
if (val === target) {
return;
/**
* if new value isn't object then just overwrite by new value
* instead of extending.
*/
}
else if (typeof val !== 'object' || val === null) {
target[key] = val;
return;
// just clone arrays (and recursive clone objects inside)
}
else if (Array.isArray(val)) {
target[key] = cloneDeep(val);
return;
// overwrite by new value if source isn't object or array
}
else if (typeof src !== 'object' || src === null || Array.isArray(src)) {
target[key] = deepExtend({}, val);
return;
// source value and new value is objects both, extending...
}
else {
target[key] = deepExtend(src, val);
return;
}
});
});
return target;
};
class Deferred {
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
// getDeepFromObject({result: {data: 1}}, 'result.data', 2); // returns 1
function getDeepFromObject(object = {}, name, defaultValue) {
const keys = name.split('.');
// clone the object
let level = deepExtend({}, object);
keys.forEach((k) => {
if (level && typeof level[k] !== 'undefined') {
level = level[k];
}
});
return typeof level === 'undefined' ? defaultValue : level;
}
function getPageForRowIndex(index, perPage) {
// we need to add 1 to convert 0-based index to 1-based page number.
return Math.floor(index / perPage) + 1;
}
function prepareValue(value) { return value; }
class Cell {
constructor(value, row, column, dataSet) {
this.value = value;
this.row = row;
this.column = column;
this.dataSet = dataSet;
this.newValue = '';
this.newValue = value;
}
getColumn() {
return this.column;
}
getRow() {
return this.row;
}
getValue() {
const valid = this.column.getValuePrepareFunction() instanceof Function;
const prepare = valid ? this.column.getValuePrepareFunction() : Cell.PREPARE;
return prepare.call(null, this.value, this.row.getData(), this);
}
setValue(value) {
this.newValue = value;
}
getId() {
return this.getColumn().id;
}
getTitle() {
return this.getColumn().title;
}
isEditable() {
if (this.getRow().index === -1) {
return this.getColumn().isAddable;
}
else {
return this.getColumn().isEditable;
}
}
}
Cell.PREPARE = prepareValue;
class Row {
constructor(index, data, _dataSet) {
this.index = index;
this.data = data;
this._dataSet = _dataSet;
this.isSelected = false;
this.isInEditing = false;
this.cells = [];
this.process();
}
getCell(column) {
return this.cells.find(el => el.getColumn() === column);
}
getCells() {
return this.cells;
}
getData() {
return this.data;
}
getIsSelected() {
return this.isSelected;
}
getNewData() {
const values = Object.assign({}, this.data);
this.getCells().forEach((cell) => values[cell.getColumn().id] = cell.newValue);
return values;
}
setData(data) {
this.data = data;
this.process();
}
process() {
this.cells = [];
this._dataSet.getColumns().forEach((column) => {
const cell = this.createCell(column);
this.cells.push(cell);
});
}
createCell(column) {
const defValue = column.settings.defaultValue ? column.settings.defaultValue : '';
const value = typeof this.data[column.id] === 'undefined' ? defValue : this.data[column.id];
return new Cell(value, this, column, this._dataSet);
}
}
class Column {
constructor(id, settings, dataSet) {
this.id = id;
this.settings = settings;
this.dataSet = dataSet;
this.title = '';
this.type = '';
this.class = '';
this.width = '';
this.hide = false;
this.isSortable = false;
this.isEditable = true;
this.isAddable = true;
this.isFilterable = false;
this.sortDirection = '';
this.defaultSortDirection = '';
this.editor = { type: '', config: {}, component: null };
this.filter = { type: '', config: {}, component: null };
this.renderComponent = null;
this.process();
}
getOnComponentInitFunction() {
return this.onComponentInitFunction;
}
getCompareFunction() {
return this.compareFunction;
}
getValuePrepareFunction() {
return this.valuePrepareFunction;
}
getFilterFunction() {
return this.filterFunction;
}
getConfig() {
return this.editor && this.editor.config;
}
getFilterType() {
return this.filter && this.filter.type;
}
getFilterConfig() {
return this.filter && this.filter.config;
}
process() {
this.title = this.settings['title'];
this.class = this.settings['class'];
this.width = this.settings['width'];
this.hide = !!this.settings['hide'];
this.type = this.prepareType();
this.editor = this.settings['editor'];
this.filter = this.settings['filter'];
this.renderComponent = this.settings['renderComponent'];
this.isFilterable = typeof this.settings['filter'] === 'undefined' ? true : !!this.settings['filter'];
this.defaultSortDirection = ['asc', 'desc']
.indexOf(this.settings['sortDirection']) !== -1 ? this.settings['sortDirection'] : '';
this.isSortable = typeof this.settings['sort'] === 'undefined' ? true : !!this.settings['sort'];
this.isEditable = typeof this.settings['editable'] === 'undefined' ? true : !!this.settings['editable'];
this.isAddable = typeof this.settings['addable'] === 'undefined' ? true : !!this.settings['addable'];
this.sortDirection = this.prepareSortDirection();
this.compareFunction = this.settings['compareFunction'];
this.valuePrepareFunction = this.settings['valuePrepareFunction'];
this.filterFunction = this.settings['filterFunction'];
this.onComponentInitFunction = this.settings['onComponentInitFunction'];
}
prepareType() {
return this.settings['type'] || this.determineType();
}
prepareSortDirection() {
return this.settings['sort'] === 'desc' ? 'desc' : 'asc';
}
determineType() {
// TODO: determine type by data
return 'text';
}
}
class DataSet {
constructor(data = [], columnSettings) {
this.columnSettings = columnSettings;
this.data = [];
this.columns = [];
this.rows = [];
this.createColumns(columnSettings);
this.setData(data);
this.createNewRow();
}
setData(data) {
this.data = data;
this.createRows();
}
getColumns() {
return this.columns;
}
getRows() {
return this.rows;
}
getFirstRow() {
return this.rows[0];
}
getLastRow() {
return this.rows[this.rows.length - 1];
}
findRowByData(data) {
return this.rows.find((row) => row.getData() === data);
}
deselectAll() {
this.rows.forEach((row) => {
row.isSelected = false;
});
// we need to clear selectedRow field because no one row selected
this.selectedRow = undefined;
}
selectRow(row) {
const previousIsSelected = row.isSelected;
this.deselectAll();
row.isSelected = !previousIsSelected;
this.selectedRow = row;
return this.selectedRow;
}
multipleSelectRow(row) {
row.isSelected = !row.isSelected;
this.selectedRow = row;
return this.selectedRow;
}
selectPreviousRow() {
if (this.rows.length > 0) {
let index = this.selectedRow ? this.selectedRow.index : 0;
if (index > this.rows.length - 1) {
index = this.rows.length - 1;
}
this.selectRow(this.rows[index]);
return this.selectedRow;
}
}
selectFirstRow() {
if (this.rows.length > 0) {
this.selectRow(this.rows[0]);
return this.selectedRow;
}
}
selectLastRow() {
if (this.rows.length > 0) {
this.selectRow(this.rows[this.rows.length - 1]);
return this.selectedRow;
}
}
selectRowByIndex(index) {
const rowsLength = this.rows.length;
if (rowsLength === 0) {
return;
}
if (!index) {
this.selectFirstRow();
return this.selectedRow;
}
if (index > 0 && index < rowsLength) {
this.selectRow(this.rows[index]);
return this.selectedRow;
}
// we need to deselect all rows if we got an incorrect index
this.deselectAll();
}
willSelectFirstRow() {
this.willSelect = 'first';
}
willSelectLastRow() {
this.willSelect = 'last';
}
select(selectedRowIndex) {
if (this.getRows().length === 0) {
return;
}
if (this.willSelect) {
if (this.willSelect === 'first') {
this.selectFirstRow();
}
if (this.willSelect === 'last') {
this.selectLastRow();
}
this.willSelect = '';
}
else {
this.selectRowByIndex(selectedRowIndex);
}
return this.selectedRow;
}
createNewRow() {
this.newRow = new Row(-1, {}, this);
this.newRow.isInEditing = true;
}
/**
* Create columns by mapping from the settings
* @param settings
* @private
*/
createColumns(settings) {
for (const id in settings) {
if (settings.hasOwnProperty(id)) {
this.columns.push(new Column(id, settings[id], this));
}
}
}
/**
* Create rows based on current data prepared in data source
* @private
*/
createRows() {
this.rows = [];
this.data.forEach((el, index) => {
this.rows.push(new Row(index, el, this));
});
}
}
class Grid {
constructor(source, settings) {
this.createFormShown = false;
this.onSelectRowSource = new Subject();
this.onDeselectRowSource = new Subject();
this.setSettings(settings);
this.setSource(source);
}
detach() {
if (this.sourceOnChangedSubscription) {
this.sourceOnChangedSubscription.unsubscribe();
}
if (this.sourceOnUpdatedSubscription) {
this.sourceOnUpdatedSubscription.unsubscribe();
}
}
showActionColumn(position) {
return this.isCurrentActionsPosition(position) && this.isActionsVisible();
}
isCurrentActionsPosition(position) {
return position == this.getSetting('actions.position');
}
isActionsVisible() {
return this.getSetting('actions.add') || this.getSetting('actions.edit') || this.getSetting('actions.delete') || this.getSetting('actions.custom').length;
}
isMultiSelectVisible() {
return this.getSetting('selectMode') === 'multi';
}
getNewRow() {
return this.dataSet.newRow;
}
setSettings(settings) {
this.settings = settings;
this.dataSet = new DataSet([], this.getSetting('columns'));
if (this.source) {
this.source.refresh();
}
}
getDataSet() {
return this.dataSet;
}
setSource(source) {
this.source = this.prepareSource(source);
this.detach();
this.sourceOnChangedSubscription = this.source.onChanged().subscribe((changes) => this.processDataChange(changes));
this.sourceOnUpdatedSubscription = this.source.onUpdated().subscribe((data) => {
const changedRow = this.dataSet.findRowByData(data);
changedRow.setData(data);
});
}
getSetting(name, defaultValue) {
return getDeepFromObject(this.settings, name, defaultValue);
}
getColumns() {
return this.dataSet.getColumns();
}
getRows() {
return this.dataSet.getRows();
}
selectRow(row) {
this.dataSet.selectRow(row);
}
multipleSelectRow(row) {
this.dataSet.multipleSelectRow(row);
}
onSelectRow() {
return this.onSelectRowSource.asObservable();
}
onDeselectRow() {
return this.onDeselectRowSource.asObservable();
}
edit(row) {
row.isInEditing = true;
}
create(row, confirmEmitter) {
const deferred = new Deferred();
deferred.promise.then((newData) => {
newData = newData ? newData : row.getNewData();
if (deferred.resolve.skipAdd) {
this.createFormShown = false;
}
else {
this.source.prepend(newData).then(() => {
this.createFormShown = false;
this.dataSet.createNewRow();
});
}
}).catch((err) => {
// doing nothing
});
if (this.getSetting('add.confirmCreate')) {
confirmEmitter.emit({
newData: row.getNewData(),
source: this.source,
confirm: deferred,
});
}
else {
deferred.resolve();
}
}
save(row, confirmEmitter) {
const deferred = new Deferred();
deferred.promise.then((newData) => {
newData = newData ? newData : row.getNewData();
if (deferred.resolve.skipEdit) {
row.isInEditing = false;
}
else {
this.source.update(row.getData(), newData).then(() => {
row.isInEditing = false;
});
}
}).catch((err) => {
// doing nothing
});
if (this.getSetting('edit.confirmSave')) {
confirmEmitter.emit({
data: row.getData(),
newData: row.getNewData(),
source: this.source,
confirm: deferred,
});
}
else {
deferred.resolve();
}
}
delete(row, confirmEmitter) {
const deferred = new Deferred();
deferred.promise.then(() => {
this.source.remove(row.getData());
}).catch((err) => {
// doing nothing
});
if (this.getSetting('delete.confirmDelete')) {
confirmEmitter.emit({
data: row.getData(),
source: this.source,
confirm: deferred,
});
}
else {
deferred.resolve();
}
}
processDataChange(changes) {
if (this.shouldProcessChange(changes)) {
this.dataSet.setData(changes['elements']);
if (this.getSetting('selectMode') !== 'multi') {
const row = this.determineRowToSelect(changes);
if (row) {
this.onSelectRowSource.next(row);
}
else {
this.onDeselectRowSource.next(null);
}
}
}
}
shouldProcessChange(changes) {
if (['filter', 'sort', 'page', 'remove', 'refresh', 'load', 'paging'].indexOf(changes['action']) !== -1) {
return true;
}
else if (['prepend', 'append'].indexOf(changes['action']) !== -1 && !this.getSetting('pager.display')) {
return true;
}
return false;
}
/**
* @breaking-change 1.8.0
* Need to add `| null` in return type
*
* TODO: move to selectable? Separate directive
*/
determineRowToSelect(changes) {
if (['load', 'page', 'filter', 'sort', 'refresh'].indexOf(changes['action']) !== -1) {
return this.dataSet.select(this.getRowIndexToSelect());
}
if (this.shouldSkipSelection()) {
return null;
}
if (changes['action'] === 'remove') {
if (changes['elements'].length === 0) {
// we have to store which one to select as the data will be reloaded
this.dataSet.willSelectLastRow();
}
else {
return this.dataSet.selectPreviousRow();
}
}
if (changes['action'] === 'append') {
// we have to store which one to select as the data will be reloaded
this.dataSet.willSelectLastRow();
}
if (changes['action'] === 'add') {
return this.dataSet.selectFirstRow();
}
if (changes['action'] === 'update') {
return this.dataSet.selectFirstRow();
}
if (changes['action'] === 'prepend') {
// we have to store which one to select as the data will be reloaded
this.dataSet.willSelectFirstRow();
}
return null;
}
prepareSource(source) {
const initialSource = this.getInitialSort();
if (initialSource && initialSource['field'] && initialSource['direction']) {
source.setSort([initialSource], false);
}
if (this.getSetting('pager.display') === true) {
source.setPaging(this.getPageToSelect(source), this.getSetting('pager.perPage'), false);
}
source.refresh();
return source;
}
getInitialSort() {
const sortConf = {};
this.getColumns().forEach((column) => {
if (column.isSortable && column.defaultSortDirection) {
sortConf['field'] = column.id;
sortConf['direction'] = column.defaultSortDirection;
sortConf['compare'] = column.getCompareFunction();
}
});
return sortConf;
}
getSelectedRows() {
return this.dataSet.getRows()
.filter(r => r.isSelected);
}
selectAllRows(status) {
this.dataSet.getRows()
.forEach(r => r.isSelected = status);
}
getFirstRow() {
return this.dataSet.getFirstRow();
}
getLastRow() {
return this.dataSet.getLastRow();
}
getSelectionInfo() {
const switchPageToSelectedRowPage = this.getSetting('switchPageToSelectedRowPage');
const selectedRowIndex = Number(this.getSetting('selectedRowIndex', 0)) || 0;
const { perPage, page } = this.getSetting('pager');
return { perPage, page, selectedRowIndex, switchPageToSelectedRowPage };
}
getRowIndexToSelect() {
const { switchPageToSelectedRowPage, selectedRowIndex, perPage } = this.getSelectionInfo();
const dataAmount = this.source.count();
/**
* source - contains all table data
* dataSet - contains data for current page
* selectedRowIndex - contains index for data in all data
*
* because of that, we need to count index for a specific row in page
* if
* `switchPageToSelectedRowPage` - we need to change page automatically
* `selectedRowIndex < dataAmount && selectedRowIndex >= 0` - index points to existing data
* (if index points to non-existing data and we calculate index for current page - we will get wrong selected row.
* if we return index witch not points to existing data - no line will be highlighted)
*/
return (switchPageToSelectedRowPage &&
selectedRowIndex < dataAmount &&
selectedRowIndex >= 0) ?
selectedRowIndex % perPage :
selectedRowIndex;
}
getPageToSelect(source) {
const { switchPageToSelectedRowPage, selectedRowIndex, perPage, page } = this.getSelectionInfo();
let pageToSelect = Math.max(1, page);
if (switchPageToSelectedRowPage && selectedRowIndex >= 0) {
pageToSelect = getPageForRowIndex(selectedRowIndex, perPage);
}
const maxPageAmount = Math.ceil(source.count() / perPage);
return maxPageAmount ? Math.min(pageToSelect, maxPageAmount) : pageToSelect;
}
shouldSkipSelection() {
/**
* For backward compatibility when using `selectedRowIndex` with non-number values - ignored.
*
* Therefore, in order to select a row after some changes,
* the `selectedRowIndex` value must be invalid or >= 0 (< 0 means that no row is selected).
*
* `Number(value)` returns `NaN` on all invalid cases, and comparisons with `NaN` always return `false`.
*
* !!! We should skip a row only in cases when `selectedRowIndex` < 0
* because when < 0 all lines must be deselected
*/
const selectedRowIndex = Number(this.getSetting('selectedRowIndex'));
return selectedRowIndex < 0;
}
}
class CellComponent {
constructor() {
this.inputClass = '';
this.mode = 'inline';
this.isInEditing = false;
this.edited = new EventEmitter();
}
onEdited(event) {
if (this.isNew) {
this.grid.create(this.grid.getNewRow(), this.createConfirm);
}
else {
this.grid.save(this.row, this.editConfirm);
}
}
}
CellComponent.decorators = [
{ type: Component, args: [{
selector: 'ng2-smart-table-cell',
template: `
<table-cell-view-mode *ngIf="!isInEditing" [cell]="cell"></table-cell-view-mode>
<table-cell-edit-mode *ngIf="isInEditing" [cell]="cell"
[inputClass]="inputClass"
(edited)="onEdited($event)">
</table-cell-edit-mode>
`
},] }
];
CellComponent.propDecorators = {
grid: [{ type: Input }],
row: [{ type: Input }],
editConfirm: [{ type: Input }],
createConfirm: [{ type: Input }],
isNew: [{ type: Input }],
cell: [{ type: Input }],
inputClass: [{ type: Input }],
mode: [{ type: Input }],
isInEditing: [{ type: Input }],
edited: [{ type: Output }]
};
class EditCellDefault {
constructor() {
this.inputClass = '';
this.edited = new EventEmitter();
}
onEdited(event) {
this.edited.next(event);
return false;
}
onStopEditing() {
this.cell.getRow().isInEditing = false;
return false;
}
onClick(event) {
event.stopPropagation();
}
}
EditCellDefault.decorators = [
{ type: Component, args: [{
template: ''
},] }
];
EditCellDefault.propDecorators = {
cell: [{ type: Input }],
inputClass: [{ type: Input }],
edited: [{ type: Output }]
};
class CustomEditComponent extends EditCellDefault {
constructor(resolver) {
super();
this.resolver = resolver;
}
ngOnChanges(changes) {
if (this.cell && !this.customComponent) {
const componentFactory = this.resolver.resolveComponentFactory(this.cell.getColumn().editor.component);
this.customComponent = this.dynamicTarget.createComponent(componentFactory);
// set @Inputs and @Outputs of custom component
this.customComponent.instance.cell = this.cell;
this.customComponent.instance.inputClass = this.inputClass;
this.customComponent.instance.onStopEditing.subscribe(() => this.onStopEditing());
this.customComponent.instance.onEdited.subscribe((event) => this.onEdited(event));
this.customComponent.instance.onClick.subscribe((event) => this.onClick(event));
}
}
ngOnDestroy() {
if (this.customComponent) {
this.customComponent.destroy();
}
}
}
CustomEditComponent.decorators = [
{ type: Component, args: [{
selector: 'table-cell-custom-editor',
template: `
<ng-template #dynamicTarget></ng-template>
`
},] }
];
CustomEditComponent.ctorParameters = () => [
{ type: ComponentFactoryResolver }
];
CustomEditComponent.propDecorators = {
dynamicTarget: [{ type: ViewChild, args: ['dynamicTarget', { read: ViewContainerRef, static: true },] }]
};
class DefaultEditComponent extends EditCellDefault {
constructor() {
super();
}
getEditorType() {
return this.cell.getColumn().editor && this.cell.getColumn().editor.type;
}
}
DefaultEditComponent.decorators = [
{ type: Component, args: [{
selector: 'table-cell-default-editor',
template: "<div [ngSwitch]=\"getEditorType()\">\n <select-editor *ngSwitchCase=\"'list'\"\n [cell]=\"cell\"\n [inputClass]=\"inputClass\"\n (onClick)=\"onClick($event)\"\n (onEdited)=\"onEdited($event)\"\n (onStopEditing)=\"onStopEditing()\">\n </select-editor>\n\n <textarea-editor *ngSwitchCase=\"'textarea'\"\n [cell]=\"cell\"\n [inputClass]=\"inputClass\"\n (onClick)=\"onClick($event)\"\n (onEdited)=\"onEdited($event)\"\n (onStopEditing)=\"onStopEditing()\">\n </textarea-editor>\n\n <checkbox-editor *ngSwitchCase=\"'checkbox'\"\n [cell]=\"cell\"\n [inputClass]=\"inputClass\"\n (onClick)=\"onClick($event)\">\n </checkbox-editor>\n\n <completer-editor *ngSwitchCase=\"'completer'\"\n [cell]=\"cell\">\n </completer-editor>\n\n <input-editor *ngSwitchDefault\n [cell]=\"cell\"\n [inputClass]=\"inputClass\"\n (onClick)=\"onClick($event)\"\n (onEdited)=\"onEdited($event)\"\n (onStopEditing)=\"onStopEditing()\">\n </input-editor>\n</div>"
},] }
];
DefaultEditComponent.ctorParameters = () => [];
class EditCellComponent {
constructor() {
this.inputClass = '';
this.edited = new EventEmitter();
}
onEdited(event) {
this.edited.next(event);
return false;
}
getEditorType() {
return this.cell.getColumn().editor && this.cell.getColumn().editor.type;
}
}
EditCellComponent.decorators = [
{ type: Component, args: [{
selector: 'table-cell-edit-mode',
template: `
<div [ngSwitch]="getEditorType()">
<table-cell-custom-editor *ngSwitchCase="'custom'"
[cell]="cell"
[inputClass]="inputClass"
(edited)="onEdited($event)">
</table-cell-custom-editor>
<table-cell-default-editor *ngSwitchDefault
[cell]="cell"
[inputClass]="inputClass"
(edited)="onEdited($event)">
</table-cell-default-editor>
</div>
`
},] }
];
EditCellComponent.propDecorators = {
cell: [{ type: Input }],
inputClass: [{ type: Input }],
edited: [{ type: Output }]
};
class DefaultEditor {
constructor() {
this.onStopEditing = new EventEmitter();
this.onEdited = new EventEmitter();
this.onClick = new EventEmitter();
}
}
DefaultEditor.decorators = [
{ type: Component, args: [{
template: ''
},] }
];
DefaultEditor.propDecorators = {
cell: [{ type: Input }],
inputClass: [{ type: Input }],
onStopEditing: [{ type: Output }],
onEdited: [{ type: Output }],
onClick: [{ type: Output }]
};
class CheckboxEditorComponent extends DefaultEditor {
constructor() {
super();
}
onChange(event) {
const trueVal = (this.cell.getColumn().getConfig() && this.cell.getColumn().getConfig().true) || true;
const falseVal = (this.cell.getColumn().getConfig() && this.cell.getColumn().getConfig().false) || false;
this.cell.newValue = event.target.checked ? trueVal : falseVal;
}
}
CheckboxEditorComponent.decorators = [
{ type: Component, args: [{
selector: 'checkbox-editor',
template: `
<input [ngClass]="inputClass"
type="checkbox"
class="form-control"
[name]="cell.getId()"
[disabled]="!cell.isEditable()"
[checked]="cell.getValue() == (cell.getColumn().getConfig()?.true || true)"
(click)="onClick.emit($event)"
(change)="onChange($event)">
`,
styles: [":host input,:host textarea{line-height:normal;padding:.375em .75em;width:100%}"]
},] }
];
CheckboxEditorComponent.ctorParameters = () => [];
class CompleterEditorComponent extends DefaultEditor {
constructor(completerService) {
super();
this.completerService = completerService;
this.completerStr = '';
}
ngOnInit() {
if (this.cell.getColumn().editor && this.cell.getColumn().editor.type === 'completer') {
const config = this.cell.getColumn().getConfig().completer;
config.dataService = this.completerService.local(config.data, config.searchFields, config.titleField);
config.dataService.descriptionField(config.descriptionField);
}
}
onEditedCompleter(event) {
this.cell.newValue = event.title;
return false;
}
}
CompleterEditorComponent.decorators = [
{ type: Component, args: [{
selector: 'completer-editor',
template: `
<ng2-completer [(ngModel)]="completerStr"
[dataService]="cell.getColumn().getConfig().completer.dataService"
[minSearchLength]="cell.getColumn().getConfig().completer.minSearchLength || 0"
[pause]="cell.getColumn().getConfig().completer.pause || 0"
[placeholder]="cell.getColumn().getConfig().completer.placeholder || 'Start typing...'"
(selected)="onEditedCompleter($event)">
</ng2-completer>
`
},] }
];
CompleterEditorComponent.ctorParameters = () => [
{ type: CompleterService }
];
class InputEditorComponent extends DefaultEditor {
constructor() {
super();
}
}
InputEditorComponent.decorators = [
{ type: Component, args: [{
selector: 'input-editor',
template: `
<input [ngClass]="inputClass"
class="form-control"
[(ngModel)]="cell.newValue"
[name]="cell.getId()"
[placeholder]="cell.getTitle()"
[disabled]="!cell.isEditable()"
(click)="onClick.emit($event)"
(keydown.enter)="onEdited.emit($event)"
(keydown.esc)="onStopEditing.emit()">
`,
styles: [":host input,:host textarea{line-height:normal;padding:.375em .75em;width:100%}"]
},] }
];
InputEditorComponent.ctorParameters = () => [];
class SelectEditorComponent extends DefaultEditor {
constructor() {
super();
}
}
SelectEditorComponent.decorators = [
{ type: Component, args: [{
selector: 'select-editor',
template: `
<select [ngClass]="inputClass"
class="form-control"
[(ngModel)]="cell.newValue"
[name]="cell.getId()"
[disabled]="!cell.isEditable()"
(click)="onClick.emit($event)"
(keydown.enter)="onEdited.emit($event)"
(keydown.esc)="onStopEditing.emit()">
<option *ngFor="let option of cell.getColumn().getConfig()?.list" [value]="option.value"
[selected]="option.value === cell.getValue()">{{ option.title }}
</option>
</select>
`
},] }
];
SelectEditorComponent.ctorParameters = () => [];
class TextareaEditorComponent extends DefaultEditor {
constructor() {
super();
}
}
TextareaEditorComponent.decorators = [
{ type: Component, args: [{
selector: 'textarea-editor',
template: `
<textarea [ngClass]="inputClass"
class="form-control"
[(ngModel)]="cell.newValue"
[name]="cell.getId()"
[disabled]="!cell.isEditable()"
[placeholder]="cell.getTitle()"
(click)="onClick.emit($event)"
(keydown.enter)="onEdited.emit($event)"
(keydown.esc)="onStopEditing.emit()">
</textarea>
`,
styles: [":host input,:host textarea{line-height:normal;padding:.375em .75em;width:100%}"]
},] }
];
TextareaEditorComponent.ctorParameters = () => [];
class CustomViewComponent {
constructor(resolver) {
this.resolver = resolver;
}
ngOnInit() {
if (this.cell && !this.customComponent) {
this.createCustomComponent();
this.callOnComponentInit();
this.patchInstance();
}
}
ngOnDestroy() {
if (this.customComponent) {
this.customComponent.destroy();
}
}
createCustomComponent() {
const componentFactory = this.resolver.resolveComponentFactory(this.cell.getColumn().renderComponent);
this.customComponent = this.dynamicTarget.createComponent(componentFactory);
}
callOnComponentInit() {
const onComponentInitFunction = this.cell.getColumn().getOnComponentInitFunction();
onComponentInitFunction && onComponentInitFunction(this.customComponent.instance);
}
patchInstance() {
Object.assign(this.customComponent.instance, this.getPatch());
}
getPatch() {
return {
value: this.cell.getValue(),
rowData: this.cell.getRow().getData()
};
}
}
CustomViewComponent.decorators = [
{ type: Component, args: [{
selector: 'custom-view-component',
template: `
<ng-template #dynamicTarget></ng-template>
`
},] }
];
CustomViewComponent.ctorParameters = () => [
{ type: ComponentFactoryResolver }
];
CustomViewComponent.propDecorators = {
cell: [{ type: Input }],
dynamicTarget: [{ type: ViewChild, args: ['dynamicTarget', { read: ViewContainerRef, static: true },] }]
};
class ViewCellComponent {
}
ViewCellComponent.decorators = [
{ type: Component, args: [{
selector: 'table-cell-view-mode',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div [ngSwitch]="cell.getColumn().type">
<custom-view-component *ngSwitchCase="'custom'" [cell]="cell"></custom-view-component>
<div *ngSwitchCase="'html'" [innerHTML]="cell.getValue()"></div>
<div *ngSwitchDefault>{{ cell.getValue() }}</div>
</div>
`
},] }
];
ViewCellComponent.propDecorators = {
cell: [{ type: Input }]
};
const CELL_COMPONENTS = [
CellComponent,
EditCellDefault,
DefaultEditor,
CustomEditComponent,
DefaultEditComponent,
EditCellComponent,
CheckboxEditorComponent,
CompleterEditorComponent,
InputEditorComponent,
SelectEditorComponent,
TextareaEditorComponent,
CustomViewComponent,
ViewCellComponent,
];
class CellModule {
}
CellModule.decorators = [
{ type: NgModule, args: [{
imports: [
CommonModule,
FormsModule,
Ng2CompleterModule,
],
declarations: [
...CELL_COMPONENTS,
],
exports: [
...CELL_COMPONENTS,
],
},] }
];
class DataSource {
constructor() {
this.onChangedSource = new Subject();
this.onAddedSource = new Subject();
this.onUpdatedSource = new Subject();
this.onRemovedSource = new Subject();
}
refresh() {
this.emitOnChanged('refresh');
}
load(data) {
this.emitOnChanged('load');
return Promise.resolve();
}
onChanged() {
return this.onChangedSource.asObservable();
}
onAdded() {
return this.onAddedSource.asObservable();
}
onUpdated() {
return this.onUpdatedSource.asObservable();
}
onRemoved() {
return this.onRemovedSource.asObservable();
}
prepend(element) {
this.emitOnAdded(element);
this.emitOnChanged('prepend');
return Promise.resolve();
}
append(element) {
this.emitOnAdded(element);
this.emitOnChanged('append');
return Promise.resolve();
}
add(element) {
this.emitOnAdded(element);
this.emitOnChanged('add');
return Promise.resolve();
}
remove(element) {
this.emitOnRemoved(element);
this.emitOnChanged('remove');
return Promise.resolve();
}
update(element, values) {
this.emitOnUpdated(element);
this.emitOnChanged('update');
return Promise.resolve();
}
empty() {
this.emitOnChanged('empty');
return Promise.resolve();
}
setSort(conf, doEmit) {
if (doEmit) {
this.emitOnChanged('sort');
}
}
setFilter(conf, andOperator, doEmit) {
if (doEmit) {
this.emitOnChanged('filter');
}
}
addFilter(fieldConf, andOperator, doEmit) {
if (doEmit) {
this.emitOnChanged('filter');
}
}
setPaging(page, perPage, doEmit) {
if (doEmit) {
this.emitOnChanged('paging');
}
}
setPage(page, doEmit) {
if (doEmit) {
this.emitOnChanged('page');
}
}
emitOnRemoved(element) {
this.onRemovedSource.next(element);
}
emitOnUpdated(element) {
this.onUpdatedSource.next(element);
}
emitOnAdded(element) {
this.onAddedSource.next(element);
}
emitOnChanged(action) {
this.getElements().then((elements) => this.onChangedSource.next({
action: action,
elements: elements,
paging: this.getPaging(),
filter: this.getFilter(),
sort: this.getSort(),
}));
}
}
class FilterDefault {
constructor() {
this.inputClass = '';
this.filter = new EventEmitter();
this.query = '';
}
onFilter(query) {
this.source.addFilter({
field: this.column.id,
search: query,
filter: this.column.getFilterFunction(),
});
}
}
FilterDefault.decorators = [
{ type: Component, args: [{
template: ''
},] }
];
FilterDefault.propDecorators = {
column: [{ type: Input }],
source: [{ type: Input }],
inputClass: [{ type: Input }],
filter: [{ type: Output }]
};
class FilterComponent extends FilterDefault {
constructor() {
super(...arguments);
this.query = '';
}
ngOnChanges(changes) {
if (changes.source) {
if (!changes.source.firstChange) {
this.dataChangedSub.unsubscribe();
}
this.dataChangedSub = this.source.onChanged().subscribe((dataChanges) => {
const filterConf = this.source.getFilter();
if (filterConf && filterConf.filters && filterConf.filters.length === 0) {
this.query = '';
// add a check for existing filters an set the query if one exists for this column
// this covers instances where the filter is set by user code while maintaining existing functionality
}
else if (filterConf && filterConf.filters && filterConf.filters.length > 0) {
filterConf.filters.forEach((k, v) => {
if (k.field == this.column.id) {
this.query = k.search;
}
});
}
});
}
}
}
FilterComponent.decorators = [
{ type: Component, args: [{
selector: 'ng2-smart-table-filter',
template: `
<div class="ng2-smart-filter" *ngIf="column.isFilterable" [ngSwitch]="column.getFilterType()">
<custom-table-filter *ngSwitchCase="'custom'"
[query]="query"
[column]="column"
[source]="source"
[inputClass]="inputClass"
(filter)="onFilter($event)">
</custom-table-filter>
<default-table-filter *ngSwitchDefault
[query]="query"
[column]="column"
[source]="source"
[inputClass]="inputClass"
(filter)="onFilter($event)">
</default-table-filter>
</div>
`,
styles: [":host .ng2-smart-filter ::ng-deep input,:host .ng2-smart-filter ::ng-deep select{font-weight:400;line-height:normal;padding:.375em .75em;width:100%}:host .ng2-smart-filter ::ng-deep input[type=search]{box-sizing:inherit}:host .ng2-smart-filter ::ng-deep .completer-dropdown-holder,:host .ng2-smart-filter ::ng-deep a{font-weight:400}"]
},] }
];
class DefaultFilterComponent extends FilterDefault {
}
DefaultFilterComponent.decorators = [
{ type: Component, args: [{
selector: 'default-table-filter',
template: `
<ng-container [ngSwitch]="column.getFilterType()">
<select-filter *ngSwitchCase="'list'"
[query]="query"
[ngClass]="inputClass"
[column]="column"
(filter)="onFilter($event)">
</select-filter>
<checkbox-filter *ngSwitchCase="'checkbox'"
[query]="query"
[ngClass]="inputClass"
[column]="column"
(filter)="onFilter($event)">
</checkbox-filter>
<completer-filter *ngSwitchCase="'completer'"
[query]="query"
[ngClass]="inputClass"
[column]="column"
(filter)="onFilter($event)">
</completer-filter>
<input-filter *ngSwitchDefault
[query]="query"
[ngClass]="inputClass"
[column]="column"
(filter)="onFilter($event)">
</input-filter>
</ng-container>
`
},] }
];
DefaultFilterComponent.propDecorators = {
query: [{ type: Input }]
};
class CustomFilterComponent extends FilterDefault {
constructor(resolver) {
super();
this.resolver = resolver;
}
ngOnChanges(changes) {
if (this.column && !this.customComponent) {
const componentFactory = this.resolver.resolveComponentFactory(this.column.filter.component);
this.customComponent = this.dynamicTarget.createComponent(componentFactory);
// set @Inputs and @Outputs of custom component
this.customComponent.instance.query = this.query;
this.customComponent.instance.column = this.column;
this.customComponent.instance.source = this.source;
this.customComponent.instance.inputClass = this.inputClass;
this.customComponent.instance.filter.subscribe((event) => this.onFilter(event));
}
if (this.customComponent) {
this.customComponent.instance.ngOnChanges(changes);
}
}
ngOnDestroy() {
if (this.customComponent) {
this.customComponent.destroy();
}
}
}
CustomFilterComponent.decorators = [
{ type: Component, args: [{
selector: 'custom-table-filter',
template: `<ng-template #dynamicTarget></ng-template>`
},] }
];
CustomFilterComponent.ctorParameters = () => [
{ type: ComponentFactoryResolver }
];
CustomFilterComponent.propDecorators = {
query: [{ type: Input }],
dynamicTarget: [{ type: ViewChild, args: ['dynamicTarget', { read: ViewContainerRef, static: true },] }]
};
class DefaultFilter {
constructor() {
this.delay = 300;
this.filter = new EventEmitter();
}
ngOnDestroy() {
if (this.changesSubscription) {
this.changesSubscription.unsubscribe();
}
}
setFilter() {
this.filter.emit(this.query);
}
}
DefaultFilter.decorators = [
{ type: Component, args: [{
template: ''
},] }
];
DefaultFilter.propDecorators = {
query: [{ type: Input }],
inputClass: [{ type: Input }],
column: [{ type: Input }],
filter: [{ type: Output }]
};
class CheckboxFilterComponent extends DefaultFilter {
constructor() {
super();
this.filterActive = false;
this.inputControl = new FormControl();
}
ngOnInit() {
this.changesSubscription = this.inputControl.valueChanges
.pipe(debounceTime(this.delay))
.subscribe((checked) => {
this.filterActive = true;
const trueVal = (this.column.getFilterConfig() && this.column.getFilterConfig().true) || true;
const falseVal = (this.column.getFilterConfig() && this.column.getFilterConfig().false) || false;
this.query = checked ? trueVal : falseVal;
this.setFilter();
});
}
resetFilter(event) {
event.preventDefault();
this.query = '';
this.inputControl.setValue(false, { emitEvent: false });
this.filterActive = false;
this.setFilter();
}
}
CheckboxFilterComponent.decorators = [
{ type: Component, args: [{
selector: 'checkbox-filter',
template: `
<input type="checkbox" [formControl]="inputControl" [ngClass]="inputClass" class="form-control">
<a href="#" *ngIf="filterActive"
(click)="resetFilter($event)">{{column.getFilterConfig()?.resetText || 'reset'}}</a>
`
},] }
];
CheckboxFilterComponent.ctorParameters = () => [];
class CompleterFilterComponent extends DefaultFilter {
constructor(completerService) {
super();
this.completerService = completerService;
this.completerContent = new Subject();
}
ngOnInit() {
const config = this.column.getFilterConfig().completer;
config.dataService = this.completerService.local(config.data, config.searchFields, config.titleField);
config.dataService.descriptionField(config.descriptionField);
this.changesSubscription = this.completerContent
.pipe(map((ev) => (ev && ev.title) || ev || ''), distinctUntilChanged(), debounceTime(this.delay))
.subscribe((search) => {
this.query = search;
this.setFilter();
});
}
inputTextChanged(event) {
// workaround to trigger the search event when the home/end buttons are clicked
// when this happens the [(ngModel)]="query" is set to "" but the (selected) method is not called
// so here it gets called manually
if (event === '') {
this.completerContent.next(event);
}
}
}
CompleterFilterComponent.decorators = [
{ type: Component, args: [{
selector: 'completer-filter',
template: `
<ng2-completer [(ngModel)]="query"
(ngModelChange)="inputTextChanged($event)"
[dataService]="column.getFilterConfig().completer.dataService"
[minSearchLength]="column.getFilterConfig().completer.minSearchLength || 0"
[pause]="column.getFilterConfig().completer.pause || 0"
[placeholder]="column.get