UNPKG

@limetech/lime-elements

Version:
983 lines (982 loc) • 30.9 kB
import { h, Host, } from '@stencil/core'; import TabulatorTable from 'tabulator-tables'; import { ColumnDefinitionFactory, createColumnSorter } from './columns'; import { isEqual, has } from 'lodash-es'; import { ElementPool } from './element-pool'; import { TableSelection } from './table-selection'; import { mapLayout } from './layout'; import { areRowsEqual } from './utils'; import translate from '../../global/translations'; const FIRST_PAGE = 1; /** * @exampleComponent limel-example-table * @exampleComponent limel-example-table-custom-components * @exampleComponent limel-example-table-header-menu * @exampleComponent limel-example-table-movable-columns * @exampleComponent limel-example-table-sorting-disabled * @exampleComponent limel-example-table-local * @exampleComponent limel-example-table-remote * @exampleComponent limel-example-table-activate-row * @exampleComponent limel-example-table-selectable-rows * @exampleComponent limel-example-table-default-sorted * @exampleComponent limel-example-table-layout-default * @exampleComponent limel-example-table-layout-stretch-last-column * @exampleComponent limel-example-table-layout-stretch-columns * @exampleComponent limel-example-table-layout-low-density * @exampleComponent limel-example-table-interactive-rows */ export class Table { constructor() { this.shouldSort = false; this.getActiveRows = () => { if (!this.tabulator) { return []; } return this.tabulator.getRows('active'); }; this.getActiveRowsData = () => { // Note: Tabulator.getData() creates copies of each data object // and will break this.selection.has checks, hence why this function // intentionally retrieves the data using the row components return this.getActiveRows().map((row) => row.getData()); }; this.selectAllOnChange = (ev) => { const selectAll = ev.detail; ev.stopPropagation(); ev.preventDefault(); const newSelection = selectAll ? this.getActiveRowsData() : []; this.select.emit(newSelection); this.tableSelection.setSelection(newSelection); this.selectAll.emit(selectAll); }; this.getColumnOptions = () => { if (!this.movableColumns) { return {}; } return { movableColumns: true, columnMoved: this.handleMoveColumn, }; }; this.handleMoveColumn = (_, components) => { const columns = components.map(this.findColumn).filter(Boolean); this.changeColumns.emit(columns); }; this.findColumn = (component) => { return this.columns.find((column) => { return (column.field === component.getField() && column.title === component.getDefinition().title); }); }; this.getTranslation = (key) => { return translate.get(key, this.language); }; this.data = []; this.columns = []; this.mode = 'local'; this.layout = undefined; this.pageSize = undefined; this.totalRows = undefined; this.sorting = []; this.activeRow = undefined; this.movableColumns = undefined; this.loading = false; this.page = FIRST_PAGE; this.emptyMessage = undefined; this.aggregates = undefined; this.selectable = undefined; this.selection = undefined; this.language = 'en'; this.handleDataSorting = this.handleDataSorting.bind(this); this.handlePageLoaded = this.handlePageLoaded.bind(this); this.handleRenderComplete = this.handleRenderComplete.bind(this); this.handleAjaxRequesting = this.handleAjaxRequesting.bind(this); this.requestData = this.requestData.bind(this); this.onClickRow = this.onClickRow.bind(this); this.formatRow = this.formatRow.bind(this); this.formatRows = this.formatRows.bind(this); this.updateMaxPage = this.updateMaxPage.bind(this); this.initTabulatorComponent = this.initTabulatorComponent.bind(this); this.setSelection = this.setSelection.bind(this); this.addColumnAggregator = this.addColumnAggregator.bind(this); this.pool = new ElementPool(document); this.columnFactory = new ColumnDefinitionFactory(this.pool); } componentWillLoad() { this.firstRequest = this.mode === 'remote'; this.initTableSelection(); } componentDidLoad() { this.init(); } disconnectedCallback() { this.pool.clear(); } totalRowsChanged() { this.updateMaxPage(); } pageSizeChanged() { this.updateMaxPage(); } pageChanged() { if (!this.tabulator) { return; } if (this.tabulator.getPage() === this.page) { return; } this.tabulator.setPage(this.page); } activeRowChanged() { if (!this.tabulator) { return; } this.formatRows(); } updateData(newData = [], oldData = []) { const shouldReplace = this.shouldReplaceData(newData, oldData); setTimeout(() => { if (!this.tabulator) { return; } if (shouldReplace) { this.pool.releaseAll(); this.tabulator.replaceData(newData); this.setSelection(); return; } this.tabulator.updateOrAddData(newData); }); } updateColumns(newColumns, oldColumns) { if (!this.tabulator) { return; } if (this.areSameColumns(newColumns, oldColumns)) { return; } const columnsInTable = this.tabulator .getColumns() .filter((c) => c.getField()); const oldColumnsInTable = columnsInTable.map((c) => oldColumns.find((old) => old.field === c.getField())); if (this.areSameColumns(newColumns, oldColumnsInTable)) { return; } this.tabulator.setColumns(this.getColumnDefinitions()); this.shouldSort = true; } updateAggregates(newAggregates, oldAggregates) { if (!this.tabulator) { return; } if (isEqual(newAggregates, oldAggregates)) { return; } if (!this.haveSameAggregateFields(newAggregates, oldAggregates)) { this.init(); return; } this.tabulator.recalc(); this.tabulator.rowManager.redraw(); } updateSelection(newSelection) { if (!this.tableSelection) { return; } this.tableSelection.setSelection(newSelection); } updateSelectable() { if (this.tableSelection && !this.selectable) { this.tableSelection = null; } this.initTableSelection(); this.init(); } updateSorting(newValue, oldValue) { const newSorting = this.getColumnSorter(newValue); const oldSorting = this.getColumnSorter(oldValue); if (isEqual(newSorting, oldSorting)) { return; } this.tabulator.setSort(newSorting); } shouldReplaceData(newData, oldData) { const newIds = newData.map((item) => { var _a; return (_a = item.id) !== null && _a !== void 0 ? _a : item; }); const oldIds = oldData.map((item) => { var _a; return (_a = item.id) !== null && _a !== void 0 ? _a : item; }); return (!this.areEqualIds(newIds, oldIds) || !this.isSameOrder(newIds, oldIds) || !areRowsEqual(newData, oldData)); } areEqualIds(newIds, oldIds) { const newIdSet = new Set(newIds); const oldIdSet = new Set(oldIds); return (newIdSet.size === oldIdSet.size && newIds.every((id) => oldIdSet.has(id))); } isSameOrder(newIds, oldIds) { return newIds.every((id, index) => id === oldIds[index]); } areSameColumns(newColumns, oldColumns) { return (newColumns.length === oldColumns.length && newColumns.every((column) => oldColumns.includes(column))); } haveSameAggregateFields(newAggregates, oldAggregates) { const oldAggregateFields = (oldAggregates === null || oldAggregates === void 0 ? void 0 : oldAggregates.map((a) => a.field)) || []; return ((newAggregates === null || newAggregates === void 0 ? void 0 : newAggregates.length) === (oldAggregates === null || oldAggregates === void 0 ? void 0 : oldAggregates.length) && !!(newAggregates === null || newAggregates === void 0 ? void 0 : newAggregates.every((a) => oldAggregateFields.includes(a.field)))); } init() { if (this.tabulator) { this.pool.releaseAll(); this.tabulator.destroy(); } const table = this.host.shadowRoot.querySelector('#tabulator-table'); this.initTabulatorComponent(table); } /* * Tabulator requires that the html element it's rendered inside * has a size before it's created, otherwise it doesn't consider * it self renderedy completely. (the callback "renderComplete" * is never run). * * @param table {HTMLElement} * */ initTabulatorComponent(table) { // Some browsers do not implement the ResizeObserver API... // If that's the case lets just create the table no // matter if its rendered or not. if (!('ResizeObserver' in window)) { this.tabulator = new TabulatorTable(table, this.getOptions()); this.setSelection(); return; } const observer = new ResizeObserver(() => { requestAnimationFrame(() => { this.tabulator = new TabulatorTable(table, this.getOptions()); this.setSelection(); observer.unobserve(table); observer.disconnect(); }); }); observer.observe(table); } initTableSelection() { if (this.selectable) { this.tableSelection = new TableSelection(() => this.tabulator, this.pool, this.select, (key) => this.getTranslation(key)); this.tableSelection.setSelection(this.selection); } } setSelection() { if (!(this.tabulator && this.tableSelection)) { return; } this.tableSelection.setSelection(this.selection); } updateMaxPage() { var _a; (_a = this.tabulator) === null || _a === void 0 ? void 0 : _a.setMaxPage(this.calculatePageCount()); } getOptions() { const ajaxOptions = this.getAjaxOptions(); const paginationOptions = this.getPaginationOptions(); const columnOptions = this.getColumnOptions(); return Object.assign(Object.assign(Object.assign(Object.assign({ data: this.data, layout: mapLayout(this.layout), columns: this.getColumnDefinitions(), dataSorting: this.handleDataSorting, pageLoaded: this.handlePageLoaded, renderComplete: this.handleRenderComplete }, ajaxOptions), paginationOptions), { rowClick: this.onClickRow, rowFormatter: this.formatRow, initialSort: this.getInitialSorting(), nestedFieldSeparator: false }), columnOptions); } getInitialSorting() { if (this.currentSorting && this.currentSorting.length > 0) { return this.getColumnSorter(this.currentSorting); } return this.getColumnSorter(this.sorting); } getColumnSorter(sorting) { return sorting.map((sorter) => { return { column: String(sorter.column.field), dir: sorter.direction.toLocaleLowerCase(), }; }); } getColumnDefinitions() { const columnDefinitions = this.columns .map(this.addColumnAggregator) .map(this.columnFactory.create); if (this.tableSelection) { return this.tableSelection.getColumnDefinitions(columnDefinitions); } return columnDefinitions; } addColumnAggregator(column) { var _a; if (!((_a = this.aggregates) === null || _a === void 0 ? void 0 : _a.length) || column.aggregator) { return column; } const aggregate = this.aggregates.find((a) => a.field === column.field); if (aggregate) { column.aggregator = (col) => { var _a; if (!col) { return; } const value = (_a = this.aggregates.find((a) => a.field === col.field)) === null || _a === void 0 ? void 0 : _a.value; if (col.formatter) { return col.formatter(value); } return value; }; } return column; } getAjaxOptions() { if (!this.isRemoteMode()) { return {}; } // Tabulator needs a URL to be set, even though this one will never be // used since we have our own custom `ajaxRequestFunc` const remoteUrl = 'https://localhost'; return { ajaxSorting: true, ajaxURL: remoteUrl, ajaxRequestFunc: this.requestData, ajaxRequesting: this.handleAjaxRequesting, }; } /* * The ajaxRequesting callback is triggered when ever an ajax request is made. * * Tabulator is requesting data with an AJAX request even though it has been * given data when it was created. * * It seems unnecessary for us to emit the `load` event as well when this * happens, since we can just initialize the table with the data that has been * given to us. Therefore, we abort the request if: * * * its the first time this method is called and, * * data has been sent in to the component as a prop * */ handleAjaxRequesting() { var _a; const abortRequest = this.firstRequest && !!((_a = this.data) === null || _a === void 0 ? void 0 : _a.length); this.firstRequest = false; if (abortRequest) { setTimeout(() => { this.updateMaxPage(); this.tabulator.replaceData(this.data); }); return false; } return true; } getPaginationOptions() { if (!this.pageSize) { return {}; } return { pagination: this.isRemoteMode() ? 'remote' : 'local', paginationSize: this.pageSize, paginationInitialPage: this.page, }; } requestData(_, __, params) { const sorters = params.sorters; const currentPage = params.page; if (this.page !== currentPage) { this.changePage.emit(currentPage); } const columnSorters = sorters.map(createColumnSorter(this.columns)); const load = { page: currentPage, sorters: columnSorters, }; // In order to make limel-table behave more like a controlled component, // we always return the existing data from this function, therefore // relying on the consumer component to handle the loading // state via the loading prop, if it actually decides to load new data. const resolveExistingData = Promise.resolve({ last_page: this.calculatePageCount(), data: this.data, }); if (!isEqual(this.currentLoad, load)) { this.currentSorting = columnSorters; this.currentLoad = load; this.load.emit(load); } return resolveExistingData; } isRemoteMode() { return this.mode === 'remote'; } handleDataSorting(sorters) { if (this.isRemoteMode()) { return; } const columnSorters = sorters.map(createColumnSorter(this.columns)); if (columnSorters.length === 0) { return; } this.sort.emit(columnSorters); } handlePageLoaded(page) { if (this.isRemoteMode()) { return; } this.changePage.emit(page); } handleRenderComplete() { if (this.tabulator && this.shouldSort) { this.shouldSort = false; this.tabulator.setSort(this.getColumnSorter(this.sorting)); } } onClickRow(_ev, row) { if (row.getPosition === undefined) { // Not a data row, probably a CalcComponent return; } if (this.isActiveRow(row)) { this.activeRow = null; } else { this.activeRow = row.getData(); } this.activate.emit(this.activeRow); } formatRows() { // eslint-disable-next-line unicorn/no-array-for-each this.tabulator.getRows().forEach(this.formatRow); } formatRow(row) { if (this.isActiveRow(row)) { row.getElement().classList.add('active'); } else { row.getElement().classList.remove('active'); } const interactiveFeedbackElement = row .getElement() .querySelectorAll('.interactive-feedback'); if (interactiveFeedbackElement.length === 0) { const element = row.getElement().ownerDocument.createElement('div'); element.classList.add('interactive-feedback'); row.getElement().prepend(element); } } isActiveRow(row) { var _a; if (!this.activeRow) { return false; } const activeRowId = (_a = this.activeRow.id) !== null && _a !== void 0 ? _a : null; if (activeRowId !== null) { return activeRowId === row.getData().id; } return this.activeRow === row.getData(); } calculatePageCount() { let total = this.totalRows; if (!total) { total = this.data.length; } return Math.ceil(total / this.pageSize); } hasAggregation(columns) { return columns.some((column) => has(column, 'aggregator')); } render() { var _a; return (h(Host, { class: { 'has-low-density': this.layout === 'lowDensity', } }, h("div", { id: "tabulator-container", class: { 'has-pagination': this.totalRows > this.pageSize, 'has-aggregation': this.hasAggregation(this.columns), 'has-movable-columns': this.movableColumns, 'has-rowselector': this.selectable, 'has-selection': (_a = this.tableSelection) === null || _a === void 0 ? void 0 : _a.hasSelection, } }, h("div", { id: "tabulator-loader", style: { display: this.loading ? 'flex' : 'none' } }, h("limel-spinner", { size: "large" })), this.renderEmptyMessage(), this.renderSelectAll(), h("div", { id: "tabulator-table" })))); } renderSelectAll() { var _a, _b, _c; if (!this.selectable) { return; } const showSelectAll = !this.loading && this.tableSelection; return (h("div", { class: "select-all", style: { display: showSelectAll ? 'inline-block' : 'none' } }, h("limel-checkbox", { class: "hide-label", onChange: this.selectAllOnChange, disabled: this.data.length === 0, checked: (_a = this.tableSelection) === null || _a === void 0 ? void 0 : _a.hasSelection, indeterminate: ((_b = this.tableSelection) === null || _b === void 0 ? void 0 : _b.hasSelection) && ((_c = this.selection) === null || _c === void 0 ? void 0 : _c.length) < this.data.length, label: this.getTranslation('table.select-all') }))); } renderEmptyMessage() { const showEmptyMessage = !this.loading && this.data.length === 0 && this.emptyMessage; return (h("div", { id: "tabulator-empty-text", style: { display: showEmptyMessage ? 'flex' : 'none' } }, h("span", null, this.emptyMessage))); } static get is() { return "limel-table"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["table.scss"] }; } static get styleUrls() { return { "$": ["table.css"] }; } static get properties() { return { "data": { "type": "unknown", "mutable": false, "complexType": { "original": "object[]", "resolved": "object[]", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Data to be displayed in the table" }, "defaultValue": "[]" }, "columns": { "type": "unknown", "mutable": false, "complexType": { "original": "Column[]", "resolved": "Column<any>[]", "references": { "Column": { "location": "import", "path": "./table.types" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Columns used to display the data" }, "defaultValue": "[]" }, "mode": { "type": "string", "mutable": false, "complexType": { "original": "'local' | 'remote'", "resolved": "\"local\" | \"remote\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to either `local` or `remote` to change how the table handles the\nloaded data. When in `local` mode, all sorting and pagination will be\ndone locally with the data given. When in `remote` mode, the consumer\nis responsible to give the table new data when a `load` event occurs" }, "attribute": "mode", "reflect": false, "defaultValue": "'local'" }, "layout": { "type": "string", "mutable": false, "complexType": { "original": "Layout", "resolved": "\"default\" | \"lowDensity\" | \"stretchColumns\" | \"stretchLastColumn\"", "references": { "Layout": { "location": "import", "path": "./layout" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Defines the layout of the table, based on how width of the columns are calculated.\n\n- `default`: makes columns as wide as their contents.\n- `stretchLastColumn`: makes columns as wide as their contents, stretch the last column to fill up the remaining table width.\n- `stretchColumns`: stretches all columns to fill the available width when possible.\n- `lowDensity`: makes columns as wide as their contents, and creates a low density and airy layout." }, "attribute": "layout", "reflect": false }, "pageSize": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Number of rows per page" }, "attribute": "page-size", "reflect": false }, "totalRows": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The number of total rows available for the data" }, "attribute": "total-rows", "reflect": false }, "sorting": { "type": "unknown", "mutable": false, "complexType": { "original": "ColumnSorter[]", "resolved": "ColumnSorter[]", "references": { "ColumnSorter": { "location": "import", "path": "./table.types" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "The initial sorted columns" }, "defaultValue": "[]" }, "activeRow": { "type": "unknown", "mutable": true, "complexType": { "original": "RowData", "resolved": "{ id?: string | number; }", "references": { "RowData": { "location": "import", "path": "./table.types" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Active row in the table" } }, "movableColumns": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to enable reordering of the columns by dragging them" }, "attribute": "movable-columns", "reflect": false }, "loading": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to trigger loading animation" }, "attribute": "loading", "reflect": false, "defaultValue": "false" }, "page": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The page to show" }, "attribute": "page", "reflect": false, "defaultValue": "FIRST_PAGE" }, "emptyMessage": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "A message to display when the table has no data" }, "attribute": "empty-message", "reflect": false }, "aggregates": { "type": "unknown", "mutable": false, "complexType": { "original": "ColumnAggregate[]", "resolved": "ColumnAggregate[]", "references": { "ColumnAggregate": { "location": "import", "path": "./table.types" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Column aggregates to be displayed in the table" } }, "selectable": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Enables row selection" }, "attribute": "selectable", "reflect": false }, "selection": { "type": "unknown", "mutable": false, "complexType": { "original": "object[]", "resolved": "object[]", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Selected data. Requires `selectable` to be true." } }, "language": { "type": "string", "mutable": false, "complexType": { "original": "Languages", "resolved": "\"da\" | \"de\" | \"en\" | \"fi\" | \"fr\" | \"nb\" | \"nl\" | \"no\" | \"sv\"", "references": { "Languages": { "location": "import", "path": "../date-picker/date.types" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Defines the language for translations." }, "attribute": "language", "reflect": true, "defaultValue": "'en'" } }; } static get events() { return [{ "method": "sort", "name": "sort", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when `mode` is `local` the data is sorted" }, "complexType": { "original": "ColumnSorter[]", "resolved": "ColumnSorter[]", "references": { "ColumnSorter": { "location": "import", "path": "./table.types" } } } }, { "method": "changePage", "name": "changePage", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when a new page has been set" }, "complexType": { "original": "number", "resolved": "number", "references": {} } }, { "method": "load", "name": "load", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when `mode` is `remote` and the table is loading new data. The\nconsumer is responsible for giving the table new data" }, "complexType": { "original": "TableParams", "resolved": "TableParams", "references": { "TableParams": { "location": "import", "path": "./table.types" } } } }, { "method": "activate", "name": "activate", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when a row is activated" }, "complexType": { "original": "object", "resolved": "object", "references": {} } }, { "method": "changeColumns", "name": "changeColumns", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the columns have been changed" }, "complexType": { "original": "Column[]", "resolved": "Column<any>[]", "references": { "Column": { "location": "import", "path": "./table.types" } } } }, { "method": "select", "name": "select", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the row selection has been changed" }, "complexType": { "original": "object[]", "resolved": "object[]", "references": {} } }, { "method": "selectAll", "name": "selectAll", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the select all rows state is toggled" }, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} } }]; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "totalRows", "methodName": "totalRowsChanged" }, { "propName": "pageSize", "methodName": "pageSizeChanged" }, { "propName": "page", "methodName": "pageChanged" }, { "propName": "activeRow", "methodName": "activeRowChanged" }, { "propName": "data", "methodName": "updateData" }, { "propName": "columns", "methodName": "updateColumns" }, { "propName": "aggregates", "methodName": "updateAggregates" }, { "propName": "selection", "methodName": "updateSelection" }, { "propName": "selectable", "methodName": "updateSelectable" }, { "propName": "sorting", "methodName": "updateSorting" }]; } } //# sourceMappingURL=table.js.map