/** * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact schukai GmbH. * * SPDX-License-Identifier: AGPL-3.0 */ import {Datasource} from "./datasource.mjs"; import { assembleMethodSymbol, CustomElement, registerCustomElement, getSlottedElements, } from "../../dom/customelement.mjs"; import { findTargetElementFromEvent, fireCustomEvent, } from "../../dom/events.mjs"; import {clone} from "../../util/clone.mjs"; import { isString, isFunction, isInstance, isObject, isArray, } from "../../types/is.mjs"; import { validateArray, validateInteger, validateObject, } from "../../types/validate.mjs"; import {Observer} from "../../types/observer.mjs"; import { ATTRIBUTE_DATATABLE_HEAD, ATTRIBUTE_DATATABLE_GRID_TEMPLATE, ATTRIBUTE_DATASOURCE_SELECTOR, ATTRIBUTE_DATATABLE_ALIGN, ATTRIBUTE_DATATABLE_SORTABLE, ATTRIBUTE_DATATABLE_MODE, ATTRIBUTE_DATATABLE_INDEX, ATTRIBUTE_DATATABLE_MODE_HIDDEN, ATTRIBUTE_DATATABLE_FEATURES, ATTRIBUTE_DATATABLE_MODE_VISIBLE, ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT, ATTRIBUTE_DATATABLE_MODE_FIXED, } from "./constants.mjs"; import {instanceSymbol} from "../../constants.mjs"; import { Header, createOrderStatement, DIRECTION_ASC, DIRECTION_DESC, DIRECTION_NONE, } from "./datatable/header.mjs"; import {DatatableStyleSheet} from "./stylesheet/datatable.mjs"; import { handleDataSourceChanges, datasourceLinkedElementSymbol, } from "./util.mjs"; import "./columnbar.mjs"; import "./filter-button.mjs"; import { findElementWithSelectorUpwards, getDocument, getWindow, } from "../../dom/util.mjs"; import {addAttributeToken} from "../../dom/attributes.mjs"; import {ATTRIBUTE_ERRORMESSAGE} from "../../dom/constants.mjs"; import {getDocumentTranslations} from "../../i18n/translations.mjs"; import "../state/state.mjs"; import "../host/collapse.mjs"; import {generateUniqueConfigKey} from "../host/util.mjs"; import "./datasource/dom.mjs"; import "./datasource/rest.mjs"; import "../form/context-help.mjs"; import {getLocaleOfDocument} from "../../dom/locale.mjs"; export {DataTable}; /** * @private * @type {symbol} */ const gridElementSymbol = Symbol("gridElement"); /** * @private * @type {symbol} */ const dataControlElementSymbol = Symbol("dataControlElement"); /** * @private * @type {symbol} */ const gridHeadersElementSymbol = Symbol("gridHeadersElement"); /** * @private * @type {symbol} */ const columnBarElementSymbol = Symbol("columnBarElement"); /** * @private * @type {symbol} */ const copyAllElementSymbol = Symbol("copyAllElement"); /** * @private * @type {symbol} */ const resizeObserverSymbol = Symbol("resizeObserver"); /** * The DataTable component is used to show the data from a data source. * * @copyright schukai GmbH * @summary A data table */ /** * A DataTable * * @fragments /fragments/components/datatable/datatable/ * * @example /examples/components/datatable/empty The empty state * @example /examples/components/datatable/data-using-javascript The data using javascript * @example /examples/components/datatable/alignment The alignment * @example /examples/components/datatable/row-mode The row mode * @example /examples/components/datatable/grid-template The grid template * @example /examples/components/datatable/overview-class The overview class * @example /examples/components/datatable/datasource Use a datasource * @example /examples/components/datatable/pagination Use pagination * @example /examples/components/datatable/filter Filer the data * @example /examples/components/datatable/ Select rows * * @copyright schukai GmbH * @summary A beautiful and highly customizable data table. It can be used to display data from a data source. * @fires monster-datatable-row-copied * @fires monster-datatable-row-removed * @fires monster-datatable-row-added * @fires monster-datatable-row-selected * @fires monster-datatable-row-deselected * @fires monster-datatable-all-rows-selected * @fires monster-datatable-all-rows-deselected * @fires monster-datatable-selection-changed **/ class DataTable extends CustomElement { /** * This method is called by the `instanceof` operator. * @return {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/datatable@@instance"); } /** * To set the options via the HTML tag, the attribute `data-monster-options` must be used. * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} * * The individual configuration values can be found in the table. * * @property {Object} templates Template definitions * @property {string} templates.main Main template * @property {Object} datasource Datasource configuration * @property {string} datasource.selector Selector for the datasource * @property {Object} mapping Mapping configuration * @property {string} mapping.data Data mapping * @property {Array} data Data * @property {Array} headers Headers * @property {Object} responsive Responsive configuration * @property {number} responsive.breakpoint Breakpoint for responsive mode * @property {Object} labels Labels * @property {string} labels.theListContainsNoEntries Label for empty state * @property {Object} classes Classes * @property {string} classes.container Container class * @property {Object} features Features * @property {boolean} features.settings Settings feature * @property {boolean} features.footer Footer feature * @property {boolean} features.autoInit Auto init feature (init datasource automatically) * @property {boolean} features.doubleClickCopyToClipboard Double click copy to clipboard feature * @property {boolean} features.copyAll Copy all feature * @property {boolean} features.help Help feature * @property {Object} templateMapping Template mapping * @property {string} templateMapping.row-key Row key * @property {string} templateMapping.filter-id Filter id **/ get defaults() { return Object.assign( {}, super.defaults, { templates: { main: getTemplate(), emptyState: getEmptyTemplate(), }, datasource: { selector: null, }, mapping: { data: "dataset", }, data: [], headers: [], responsive: { breakpoint: 900, }, labels: getTranslations(), classes: { control: "monster-theme-control-container-1", container: "", row: "monster-theme-control-row-1", }, features: { settings: true, footer: true, autoInit: true, doubleClickCopyToClipboard: true, copyAll: true, help: true, }, copy: { delimiter: ";", quoteOpen: '"', quoteClose: '"', rowBreak: "\n", }, templateMapping: { "row-key": null, "filter-id": null, }, }, initOptionsFromArguments.call(this), ); } /** * * @param {string} selector * @return {NodeListOf<*>} */ getGridElements(selector) { return this[gridElementSymbol].querySelectorAll(selector); } /** * * @return {string} */ static getTag() { return "monster-datatable"; } /** * @return {void} */ disconnectedCallback() { super.disconnectedCallback(); if (this?.[resizeObserverSymbol] instanceof ResizeObserver) { this[resizeObserverSymbol].disconnect(); } } /** * @return {void} */ connectedCallback() { const self = this; super.connectedCallback(); this[resizeObserverSymbol] = new ResizeObserver((entries) => { updateGrid.call(self); }); this[resizeObserverSymbol].observe(this.parentNode); } /** * Get the row number of the selected rows as an array * * @returns {number[]} */ getSelectedRows() { const rows = this.getGridElements(`[data-monster-role="select-row"]`); const selectedRows = []; rows.forEach((row) => { if (row.checked) { const key = row.parentNode.getAttribute("data-monster-insert-reference"); const index = key.split("-").pop(); selectedRows.push(parseInt(index, 10)); } }); return selectedRows; } /** * @return void */ [assembleMethodSymbol]() { const rawKey = this.getOption("templateMapping.row-key"); if (rawKey === null) { if (this.id !== null && this.id !== "") { const rawKey = this.getOption("templateMapping.row-key"); if (rawKey === null) { this.setOption("templateMapping.row-key", this.id + "-row"); } } else { this.setOption("templateMapping.row-key", "row"); } } if (this.id !== null && this.id !== "") { this.setOption("templateMapping.filter-id", "" + this.id + "-filter"); } else { this.setOption("templateMapping.filter-id", "filter"); } super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); const selector = this.getOption("datasource.selector"); if (isString(selector)) { const element = findElementWithSelectorUpwards(this, selector); if (element === null) { throw new Error("the selector must match exactly one element"); } if (!isInstance(element, Datasource)) { throw new TypeError("the element must be a datasource"); } this[datasourceLinkedElementSymbol] = element; queueMicrotask(() => { handleDataSourceChanges.call(this); element.datasource.attachObserver( new Observer(handleDataSourceChanges.bind(this)), ); }); } getHostConfig .call(this, getColumnVisibilityConfigKey) .then((config) => { const headerOrderMap = new Map(); getHostConfig .call(this, getStoredOrderConfigKey) .then((orderConfig) => { if (isArray(orderConfig) || orderConfig.length > 0) { for (let i = 0; i < orderConfig.length; i++) { const item = orderConfig[i]; const parts = item.split(" "); const field = parts[0]; const direction = parts[1] || DIRECTION_ASC; headerOrderMap.set(field, direction); } } }) .then(() => { try { initGridAndStructs.call(this, config, headerOrderMap); } catch (error) { addAttributeToken( this, ATTRIBUTE_ERRORMESSAGE, error?.message || error.toString(), ); } updateColumnBar.call(this); }) .catch((error) => { addAttributeToken( this, ATTRIBUTE_ERRORMESSAGE, error?.message || error.toString(), ); }); }) .catch((error) => { addAttributeToken( this, ATTRIBUTE_ERRORMESSAGE, error?.message || error.toString(), ); }); } /** * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [DatatableStyleSheet]; } /** * Copy a row from the datatable * * @param {number|string} fromIndex * @param {number|string} toIndex * @return {DataTable} * @fires monster-datatable-row-copied */ copyRow(fromIndex, toIndex) { const datasource = this[datasourceLinkedElementSymbol]; if (!datasource) { return this; } let d = datasource.data; let c = clone(d); let rows = c; const mapping = this.getOption("mapping.data"); if (mapping) { rows = c?.[mapping]; } if (rows === undefined || rows === null) { rows = []; } if (toIndex === undefined) { toIndex = rows.length; } if (isString(fromIndex)) { fromIndex = parseInt(fromIndex); } if (isString(toIndex)) { toIndex = parseInt(toIndex); } if (toIndex < 0 || toIndex > rows.length) { throw new RangeError("index out of bounds"); } validateArray(rows); validateInteger(fromIndex); validateInteger(toIndex); if (fromIndex < 0 || fromIndex >= rows.length) { throw new RangeError("index out of bounds"); } rows.splice(toIndex, 0, clone(rows[fromIndex])); datasource.data = c; fireCustomEvent(this, "monster-datatable-row-copied", { index: toIndex, }); return this; } /** * Remove a row from the datatable * * @param {number|string} index * @return {DataTable} * @fires monster-datatable-row-removed */ removeRow(index) { const datasource = this[datasourceLinkedElementSymbol]; if (!datasource) { return this; } let d = datasource.data; let c = clone(d); let rows = c; const mapping = this.getOption("mapping.data"); if (mapping) { rows = c?.[mapping]; } if (rows === undefined || rows === null) { rows = []; } if (isString(index)) { index = parseInt(index); } validateArray(rows); validateInteger(index); if (index < 0 || index >= rows.length) { throw new RangeError("index out of bounds"); } if (mapping) { rows = c?.[mapping]; } rows.splice(index, 1); datasource.data = c; fireCustomEvent(this, "monster-datatable-row-removed", { index: index, }); return this; } /** * Add a row to the datatable * * @param {Object} data * @return {DataTable} * * @fires monster-datatable-row-added **/ addRow(data) { const datasource = this[datasourceLinkedElementSymbol]; if (!datasource) { return this; } let d = datasource.data; let c = clone(d); let rows = c; const mapping = this.getOption("mapping.data"); if (mapping) { rows = c?.[mapping]; } if (rows === undefined || rows === null) { rows = []; } validateArray(rows); validateObject(data); rows.push(data); datasource.data = c; fireCustomEvent(this, "monster-datatable-row-added", { index: rows.length - 1, }); return this; } } /** * @private * @return {string} */ function getColumnVisibilityConfigKey() { return generateUniqueConfigKey("datatable", this?.id, "columns-visibility"); } /** * @private * @return {string} */ function getFilterConfigKey() { return generateUniqueConfigKey("datatable", this?.id, "filter"); } /** * @private * @return {Promise} */ function getHostConfig(callback) { const host = findElementWithSelectorUpwards(this, "monster-host"); if (!host) { addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "no host found"); return Promise.resolve({}); } if (!this.id) { addAttributeToken( this, ATTRIBUTE_ERRORMESSAGE, "no id found; id is required for config", ); return Promise.resolve({}); } if (!host || !isFunction(host?.getConfig)) { throw new TypeError("the host must be a monster-host"); } const configKey = callback.call(this); return host.hasConfig(configKey).then((hasConfig) => { if (hasConfig) { return host.getConfig(configKey); } else { return {}; } }); } /** * @private */ function updateColumnBar() { if (!this[columnBarElementSymbol]) { return; } const columns = []; for (const header of this.getOption("headers")) { const mode = header.getInternal("mode"); if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) { continue; } columns.push({ visible: mode !== ATTRIBUTE_DATATABLE_MODE_HIDDEN, name: header.label, index: header.index, }); } this[columnBarElementSymbol].setOption("columns", columns); } /** * @private */ function updateHeaderFromColumnBar() { if (!this[columnBarElementSymbol]) { return; } const options = this[columnBarElementSymbol].getOption("columns"); if (!isArray(options)) return; const invisibleMap = {}; for (let i = 0; i < options.length; i++) { const option = options[i]; invisibleMap[option.index] = option.visible; } for (const header of this.getOption("headers")) { const mode = header.getInternal("mode"); if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) { continue; } if (invisibleMap[header.index] === false) { header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_HIDDEN); } else { header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_VISIBLE); } } } /** * @private */ function updateConfigColumnBar() { if (!this[columnBarElementSymbol]) { return; } const options = this[columnBarElementSymbol].getOption("columns"); if (!isArray(options)) return; const map = {}; for (let i = 0; i < options.length; i++) { const option = options[i]; map[option.name] = option.visible; } const host = findElementWithSelectorUpwards(this, "monster-host"); if (!(host && this.id)) { return; } const configKey = getColumnVisibilityConfigKey.call(this); try { host.setConfig(configKey, map); } catch (error) { addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error)); } } /** * @private */ function initEventHandler() { const self = this; const quoteOpenChar = this.getOption("copy.quoteOpen"); const quoteCloseChar = this.getOption("copy.quoteClose"); const delimiterChar = this.getOption("copy.delimiter"); const rowBreak = this.getOption("copy.rowBreak"); self[columnBarElementSymbol].attachObserver( new Observer((e) => { updateHeaderFromColumnBar.call(self); updateGrid.call(self); updateConfigColumnBar.call(self); }), ); self[gridHeadersElementSymbol].addEventListener("click", function (event) { let element = null; const datasource = self[datasourceLinkedElementSymbol]; if (!datasource) { return; } element = findTargetElementFromEvent(event, ATTRIBUTE_DATATABLE_SORTABLE); if (element) { const index = element.parentNode.getAttribute(ATTRIBUTE_DATATABLE_INDEX); const headers = self.getOption("headers"); event.preventDefault(); headers[index].changeDirection(); queueMicrotask(function () { /** hotfix, normally this should be done via the updater, no idea why this is not possible. */ element.setAttribute( ATTRIBUTE_DATATABLE_SORTABLE, `${headers[index].field} ${headers[index].direction}`, ); storeOrderStatement.call(self, true); }); } }); const eventHandlerDoubleClickCopyToClipboard = (event) => { const element = findTargetElementFromEvent(event, "data-monster-head"); if (element) { let text = ""; if (event.shiftKey) { const index = element.getAttribute("data-monster-insert-reference"); if (index) { const cols = self.getGridElements( `[data-monster-insert-reference="${index}"]`, ); const colTexts = []; for (let i = 0; i < cols.length; i++) { const col = cols[i]; if ( col.querySelector("monster-button-bar") || col.querySelector("monster-button") ) { continue; } if (col.textContent) { colTexts.push( quoteOpenChar + col.textContent.trim() + quoteCloseChar, ); } } text = colTexts.join(delimiterChar); } } else { if ( element.querySelector("monster-button-bar") || element.querySelector("monster-button") ) { return; } text = element.textContent.trim(); } if (getWindow().navigator.clipboard && text) { getWindow() .navigator.clipboard.writeText(text) .then( () => { }, (err) => { }, ); } } }; if (self.getOption("features.doubleClickCopyToClipboard")) { self[gridElementSymbol].addEventListener( "dblclick", eventHandlerDoubleClickCopyToClipboard, ); } if (self.getOption("features.copyAll") && this[copyAllElementSymbol]) { this[copyAllElementSymbol].addEventListener("click", (event) => { event.preventDefault(); const table = []; let currentRow = []; let currentIndex = null; const cols = self.getGridElements(`[data-monster-insert-reference]`); const rowIndexes = new Map(); cols.forEach((col) => { const index = col.getAttribute("data-monster-insert-reference"); rowIndexes.set(index, true); }); rowIndexes.forEach((value, key) => { const cols = self.getGridElements( `[data-monster-insert-reference="${key}"]`, ); for (let i = 0; i < cols.length; i++) { const col = cols[i]; if ( col.querySelector("monster-button-bar") || col.querySelector("monster-button") ) { continue; } if (col.textContent) { currentRow.push( quoteOpenChar + col.textContent.trim() + quoteCloseChar, ); } } if (currentRow.length > 0) { table.push(currentRow); } currentRow = []; }); if (table.length > 0) { const text = table.map((row) => row.join(delimiterChar)).join(rowBreak); if (getWindow().navigator.clipboard && text) { getWindow() .navigator.clipboard.writeText(text) .then( () => { }, (err) => { }, ); } } }); } const selectRowCallback = (event) => { const element = findTargetElementFromEvent(event, "data-monster-role", "select-row"); if (element) { const key = element.parentNode.getAttribute("data-monster-insert-reference"); const row = self.getGridElements( `[data-monster-insert-reference="${key}"]`, ); const index = key.split("-").pop(); if (element.checked) { row.forEach((col) => { col.classList.add("selected"); }); fireCustomEvent(self, "monster-datatable-row-selected", { index: index }) } else { row.forEach((col) => { col.classList.remove("selected"); }); fireCustomEvent(self, "monster-datatable-row-deselected", { index: index }) } fireCustomEvent(this, "monster-datatable-selection-changed", {}) } const rows = self.getGridElements(`[data-monster-role="select-row"]`); const allSelected = Array.from(rows).every((row) => row.checked); const selectAll = this[gridHeadersElementSymbol].querySelector(`[data-monster-role="select-all"]`); selectAll.checked = allSelected; } this[gridElementSymbol].addEventListener("click", selectRowCallback); this[gridElementSymbol].addEventListener("touch", selectRowCallback); const selectAllCallback = (event) => { const element = findTargetElementFromEvent(event, "data-monster-role", "select-all"); if (element) { const mode = element.checked const rows = this.getGridElements(`[data-monster-role="select-row"]`); rows.forEach((row) => { row.checked = mode; }); if (mode) { fireCustomEvent(this, "monster-datatable-all-rows-selected", {}) } else { fireCustomEvent(this, "monster-datatable-all-rows-deselected", {}) } fireCustomEvent(this, "monster-datatable-selection-changed", {}) } } this[gridHeadersElementSymbol].addEventListener("click", selectAllCallback) this[gridHeadersElementSymbol].addEventListener("touch", selectAllCallback) } /** * @private */ function initGridAndStructs(hostConfig, headerOrderMap) { const rowID = this.getOption("templateMapping.row-key"); if (!this[gridElementSymbol]) { throw new Error("no grid element is defined"); } let template; getSlottedElements.call(this).forEach((e) => { if (e instanceof HTMLTemplateElement && e.id === rowID) { template = e; } }); if (!template) { throw new Error("no template is defined"); } const rowCount = template.content.children.length; const headers = []; for (let i = 0; i < rowCount; i++) { let hClass = ""; const row = template.content.children[i]; let mode = ""; if (row.hasAttribute(ATTRIBUTE_DATATABLE_MODE)) { mode = row.getAttribute(ATTRIBUTE_DATATABLE_MODE); } let grid = row.getAttribute(ATTRIBUTE_DATATABLE_GRID_TEMPLATE); if (!grid || grid === "" || grid === "auto") { grid = "minmax(0, 1fr)"; } let label = ""; let labelKey = ""; if (row.hasAttribute(ATTRIBUTE_DATATABLE_HEAD)) { label = row.getAttribute(ATTRIBUTE_DATATABLE_HEAD); labelKey = label; try { if (label.startsWith("i18n:")) { label = label.substring(5, label.length); label = getDocumentTranslations().getText(label, label); } } catch (e) { label = "i18n error " + label; } } if (!label) { label = i + 1 + ""; mode = ATTRIBUTE_DATATABLE_MODE_FIXED; labelKey = label; } if (isObject(hostConfig) && hostConfig.hasOwnProperty(label)) { if (hostConfig[label] === false) { mode = ATTRIBUTE_DATATABLE_MODE_HIDDEN; } else { mode = ATTRIBUTE_DATATABLE_MODE_VISIBLE; } } let align = ""; if (row.hasAttribute(ATTRIBUTE_DATATABLE_ALIGN)) { align = row.getAttribute(ATTRIBUTE_DATATABLE_ALIGN); } switch (align) { case "center": hClass = "flex-center"; break; case "end": hClass = "flex-end"; break; case "start": hClass = "flex-start"; break; default: hClass = "flex-start"; } let field = ""; let direction = DIRECTION_NONE; if (row.hasAttribute(ATTRIBUTE_DATATABLE_SORTABLE)) { field = row.getAttribute(ATTRIBUTE_DATATABLE_SORTABLE).trim(); const parts = field.split(" ").map((item) => item.trim()); field = parts[0]; if (headerOrderMap.has(field)) { direction = headerOrderMap.get(field); } else if ( parts.length === 2 && [DIRECTION_ASC, DIRECTION_DESC].indexOf(parts[1]) !== -1 ) { direction = parts[1]; } } if (mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) { hClass += " hidden"; } const features = []; if (row.hasAttribute(ATTRIBUTE_DATATABLE_FEATURES)) { const features = row.getAttribute(ATTRIBUTE_DATATABLE_FEATURES).split(" "); features.forEach((feature) => { features.push(feature.trim()); if (feature === "select") { label = "<input type='checkbox' data-monster-role='select-all' />"; while (row.firstChild) { row.removeChild(row.firstChild); } const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.setAttribute("data-monster-role", "select-row"); row.appendChild(checkbox); } }); } const header = new Header(); header.setInternals({ field: field, label: label, classes: hClass, index: i, mode: mode, grid: grid, labelKey: labelKey, direction: direction, features: features, }); headers.push(header); } this.setOption("headers", headers); queueMicrotask(() => { storeOrderStatement.call(this, this.getOption("features.autoInit")); }); } /** * @private * @returns {object} */ function getTranslations() { const locale = getLocaleOfDocument(); switch (locale.language) { case 'de': return { theListContainsNoEntries: "Die Liste enthält keine Einträge", copyAll: "Alles kopieren", helpText: "<p>Sie können die Werte aus einzelnen Zeilen<br>" + "in die Zwischenablage kopieren, indem Sie auf die entsprechende Spalte doppelklicken.</p>" + "<p>Um eine ganze Zeile zu kopieren, halten Sie die Umschalttaste gedrückt, während Sie klicken.<br>" + "Wenn Sie alle Zeilen kopieren möchten, können Sie die Schaltfläche <strong>Alles kopieren</strong> verwenden.</p>", }; case 'fr': return { theListContainsNoEntries: "La liste ne contient aucune entrée", copyAll: "Copier tout", helpText: "<p>Vous pouvez copier les valeurs des rangées individuelles<br>" + "dans le presse-papiers en double-cliquant sur la colonne concernée.</p>" + "<p>Pour copier une rangée entière, maintenez la touche Maj enfoncée tout en cliquant.<br>" + "Si vous souhaitez copier toutes les rangées, vous pouvez utiliser le bouton <strong>Copier tout</strong>.</p>", }; case 'sp': return { theListContainsNoEntries: "La lista no contiene entradas", copyAll: "Copiar todo", helpText: "<p>Puedes copiar los valores de filas individuales<br>" + "al portapapeles haciendo doble clic en la columna correspondiente.</p>" + "<p>Para copiar una fila entera, mantén presionada la tecla Shift mientras haces clic.<br>" + "Si quieres copiar todas las filas, puedes usar el botón <strong>Copiar todo</strong>.</p>", }; case 'it': return { theListContainsNoEntries: "L'elenco non contiene voci", copyAll: "Copia tutto", helpText: "<p>Puoi copiare i valori dalle singole righe<br>" + "negli appunti facendo doppio clic sulla colonna relativa.</p>" + "<p>Per copiare un'intera riga, tieni premuto il tasto Shift mentre clicchi.<br>" + "Se vuoi copiare tutte le righe, puoi usare il pulsante <strong>Copia tutto</strong>.</p>", }; case 'pl': return { theListContainsNoEntries: "Lista nie zawiera wpisów", copyAll: "Kopiuj wszystko", helpText: "<p>Możesz skopiować wartości z poszczególnych wierszy<br>" + "do schowka, klikając dwukrotnie na odpowiednią kolumnę.</p>" + "<p>Aby skopiować cały wiersz, przytrzymaj klawisz Shift podczas klikania.<br>" + "Jeśli chcesz skopiować wszystkie wiersze, możesz użyć przycisku <strong>Kopiuj wszystko</strong>.</p>", }; case 'no': return { theListContainsNoEntries: "Listen inneholder ingen oppføringer", copyAll: "Kopier alt", helpText: "<p>Du kan kopiere verdier fra enkeltrader<br>" + "til utklippstavlen ved å dobbeltklikke på den relevante kolonnen.</p>" + "<p>For å kopiere en hel rad, hold nede Skift-tasten mens du klikker.<br>" + "Hvis du vil kopiere alle radene, kan du bruke knappen <strong>Kopier alt</strong>.</p>", }; case 'dk': return { theListContainsNoEntries: "Listen indeholder ingen poster", copyAll: "Kopiér alt", helpText: "<p>Du kan kopiere værdier fra enkelte rækker<br>" + "til udklipsholderen ved at dobbeltklikke på den relevante kolonne.</p>" + "<p>For at kopiere en hel række, hold Shift-tasten nede, mens du klikker.<br>" + "Hvis du vil kopiere alle rækker, kan du bruge knappen <strong>Kopiér alt</strong>.</p>", }; case 'sw': return { theListContainsNoEntries: "Listan innehåller inga poster", copyAll: "Kopiera allt", helpText: "<p>Du kan kopiera värden från enskilda rader<br>" + "till urklipp genom att dubbelklicka på den relevanta kolumnen.</p>" + "<p>För att kopiera en hel rad, håll ned Shift-tangenten medan du klickar.<br>" + "Om du vill kopiera alla rader kan du använda knappen <strong>Kopiera allt</strong>.</p>", }; case 'en': default: return { theListContainsNoEntries: "The list contains no entries", copyAll: "Copy all", helpText: "<p>You can copy the values from individual rows<br>" + "to the clipboard by double-clicking on the relevant column.</p>" + "<p>To copy an entire row, hold down the Shift key while clicking.<br>" + "If you want to copy all rows, you can use the <strong>Copy All</strong> button.</p>", }; } } /** * @private * @return {string} */ export function getStoredOrderConfigKey() { return generateUniqueConfigKey("datatable", this?.id, "stored-order"); } /** * @private */ function storeOrderStatement(doFetch) { const headers = this.getOption("headers"); const statement = createOrderStatement(headers); setDataSource.call(this, {orderBy: statement}, doFetch); const host = findElementWithSelectorUpwards(this, "monster-host"); if (!(host && this.id)) { return; } const configKey = getStoredOrderConfigKey.call(this); // statement explode with , and remove all empty const list = statement.split(",").filter((item) => item.trim() !== ""); if (list.length === 0) { return; } host.setConfig(configKey, list); } /** * @private */ function updateGrid() { if (!this[gridElementSymbol]) { throw new Error("no grid element is defined"); } let gridTemplateColumns = ""; const headers = this.getOption("headers"); let styles = ""; for (let i = 0; i < headers.length; i++) { const header = headers[i]; if (header.mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) { styles += `[data-monster-role=datatable]>[data-monster-head="${header.labelKey}"] { display: none; }\n`; styles += `[data-monster-role=datatable-headers]>[data-monster-index="${header.index}"] { display: none; }\n`; } else { gridTemplateColumns += `${header.grid} `; } } const sheet = new CSSStyleSheet(); if (styles !== "") sheet.replaceSync(styles); this.shadowRoot.adoptedStyleSheets = [...DataTable.getCSSStyleSheet(), sheet]; const bodyWidth = this.parentNode.clientWidth; const breakpoint = this.getOption("responsive.breakpoint"); this[dataControlElementSymbol].classList.toggle( "small", bodyWidth <= breakpoint, ); if (bodyWidth > breakpoint) { this[gridElementSymbol].style.gridTemplateColumns = `${gridTemplateColumns}`; this[gridHeadersElementSymbol].style.gridTemplateColumns = `${gridTemplateColumns}`; } else { this[gridElementSymbol].style.gridTemplateColumns = "auto"; this[gridHeadersElementSymbol].style.gridTemplateColumns = "auto"; } } /** * @private * @param {Header[]} headers * @param {bool} doFetch */ function setDataSource({orderBy}, doFetch) { const datasource = this[datasourceLinkedElementSymbol]; if (!datasource) { return; } if (isFunction(datasource?.setParameters)) { datasource.setParameters({orderBy}); } if (doFetch !== false && isFunction(datasource?.fetch)) { datasource.fetch(); } } /** * @private * @return {DataTable} */ function initControlReferences() { if (!this.shadowRoot) { throw new Error("no shadow-root is defined"); } this[dataControlElementSymbol] = this.shadowRoot.querySelector( "[data-monster-role=control]", ); this[gridElementSymbol] = this.shadowRoot.querySelector( "[data-monster-role=datatable]", ); this[gridHeadersElementSymbol] = this.shadowRoot.querySelector( "[data-monster-role=datatable-headers]", ); this[columnBarElementSymbol] = this.shadowRoot.querySelector("monster-column-bar"); this[copyAllElementSymbol] = this.shadowRoot.querySelector( "[data-monster-role=copy-all]", ); return this; } /** * @private * @return {object} * @throws {TypeError} incorrect arguments passed for the datasource * @throws {Error} the datasource could not be initialized */ function initOptionsFromArguments() { const options = {}; const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR); if (selector) { options.datasource = {selector: selector}; } const breakpoint = this.getAttribute( ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT, ); if (breakpoint) { options.responsive = {}; options.responsive.breakpoint = parseInt(breakpoint); } return options; } /** * @private * @return {string} */ function getEmptyTemplate() { return `<monster-state data-monster-role="empty-without-action"> <div part="visual"> <svg width="4rem" height="4rem" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="m21.5 22h-19c-1.378 0-2.5-1.121-2.5-2.5v-7c0-.07.015-.141.044-.205l3.969-8.82c.404-.896 1.299-1.475 2.28-1.475h11.414c.981 0 1.876.579 2.28 1.475l3.969 8.82c.029.064.044.135.044.205v7c0 1.379-1.122 2.5-2.5 2.5zm-20.5-9.393v6.893c0 .827.673 1.5 1.5 1.5h19c.827 0 1.5-.673 1.5-1.5v-6.893l-3.925-8.723c-.242-.536-.779-.884-1.368-.884h-11.414c-.589 0-1.126.348-1.368.885z"/> <path d="m16.807 17h-9.614c-.622 0-1.186-.391-1.404-.973l-1.014-2.703c-.072-.194-.26-.324-.468-.324h-3.557c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h3.557c.622 0 1.186.391 1.405.973l1.013 2.703c.073.194.261.324.468.324h9.613c.208 0 .396-.13.468-.324l1.013-2.703c.22-.582.784-.973 1.406-.973h3.807c.276 0 .5.224.5.5s-.224.5-.5.5h-3.807c-.208 0-.396.13-.468.324l-1.013 2.703c-.219.582-.784.973-1.405.973z"/> </svg> </div> <div part="content" data-monster-replace="path:labels.theListContainsNoEntries"> The list contains no entries. </div> </monster-state>`; } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div data-monster-role="control" part="control" data-monster-attributes="class path:classes.control"> <template id="headers-row"> <div data-monster-attributes="class path:headers-row.classes, data-monster-index path:headers-row.index" data-monster-replace="path:headers-row.html"></div> </template> <slot></slot> <div data-monster-attributes="class path:classes.container" data-monster-role="table-container" part="table-container"> <div class="filter"> <slot name="filter"></slot> </div> <div class="bar"> <monster-context-help data-monster-attributes="class path:features.help | ?::hidden" data-monster-replace="path:labels.helpText" ></monster-context-help> <a href="#" data-monster-attributes="class path:features.copyAll | ?::hidden" data-monster-role="copy-all" data-monster-replace="path:labels.copyAll">Copy all</a> <monster-column-bar data-monster-attributes="class path:features.settings | ?::hidden"></monster-column-bar> <slot name="bar"></slot> </div> <div data-monster-role="datatable-headers" data-monster-insert="headers-row path:headers"></div> <div data-monster-replace="path:templates.emptyState" data-monster-attributes="class path:data | has-entries | ?:hidden:empty-state-container"></div> <div data-monster-role="datatable" data-monster-insert="\${row-key} path:data"> </div> </div> <div data-monster-role="footer" data-monster-select-this="true" data-monster-attributes="class path:data | has-entries | ?::hidden"> <slot name="footer" data-monster-attributes="class path:features.footer | ?::hidden"></slot> </div> </div> `; } registerCustomElement(DataTable);