Skip to content
Snippets Groups Projects
Select Git revision
  • dd8967a88c88bd37cbc1a99b52544b33de204fd1
  • master default protected
  • 1.31
  • 4.24.2
  • 4.24.1
  • 4.24.0
  • 4.23.6
  • 4.23.5
  • 4.23.4
  • 4.23.3
  • 4.23.2
  • 4.23.1
  • 4.23.0
  • 4.22.3
  • 4.22.2
  • 4.22.1
  • 4.22.0
  • 4.21.0
  • 4.20.1
  • 4.20.0
  • 4.19.0
  • 4.18.0
  • 4.17.0
23 results

overlay.mjs

Blame
  • datatable.mjs 44.74 KiB
    /**
     * 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);