Skip to content
Snippets Groups Projects
Select Git revision
  • master
  • v1.23.2
  • v1.23.1
  • v1.23.0
  • v1.22.0
  • v1.21.1
  • v1.21.0
  • v1.20.3
  • v1.20.2
  • v1.20.1
  • v1.20.0
  • v1.19.4
  • v1.19.3
  • v1.19.2
  • v1.19.1
  • v1.19.0
  • v1.18.2
  • v1.18.1
  • v1.18.0
  • v1.17.0
  • v1.16.1
21 results

runnable_test.go

Blame
  • datatable.mjs 24.43 KiB
    /**
     * Copyright 2023 schukai GmbH
     * SPDX-License-Identifier: AGPL-3.0
     */
    
    import {Datasource} from "./datasource.mjs";
    import {
        assembleMethodSymbol,
        CustomElement,
        registerCustomElement,
        getSlottedElements,
    } from "../../dom/customelement.mjs";
    import {findTargetElementFromEvent} from "../../dom/events.mjs";
    import {
        isString,
        isFunction,
        isInstance,
        isObject,
        isArray,
    } from "../../types/is.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_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 {getStoredFilterConfigKey} from "./filter/util.mjs";
    import {DatatableStyleSheet} from "./stylesheet/datatable.mjs";
    import {
        handleDataSourceChanges,
        datasourceLinkedElementSymbol,
    } from "./util.mjs";
    import "./columnbar.mjs";
    import "./filter-button.mjs";
    import {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";
    
    export {DataTable};
    
    /**
     * @private
     * @type {symbol}
     */
    const gridElementSymbol = Symbol("gridElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const gridHeadersElementSymbol = Symbol("gridHeadersElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const columnBarElementSymbol = Symbol("columnBarElement");
    
    /**
     * The DataTable component is used to show the data from a data source.
     *
     * <img src="./images/datatable.png">
     *
     * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library
     *
     * You can create this control either by specifying the HTML tag <monster-datatable />` directly in the HTML or using
     * Javascript via the `document.createElement('monster-datatable');` method.
     *
     * ```html
     * <monster-datatable></monster-datatable>
     * ```
     *
     * Or you can create this CustomControl directly in Javascript:
     *
     * ```js
     * import '@schukai/component-datatable/source/datatable.mjs';
     * document.createElement('monster-datatable');
     * ```
     *
     * The Body should have a class "hidden" to ensure that the styles are applied correctly.
     *
     * ```css
     * body.hidden {
     *    visibility: hidden;
     * }
     * ```
     *
     * @startuml datatable.png
     * skinparam monochrome true
     * skinparam shadowing false
     * HTMLElement <|-- CustomElement
     * CustomElement <|-- Datatable
     * @enduml
     *
     * @copyright schukai GmbH
     * @memberOf Monster.Components.Datatable
     * @summary A data table
     */
    class DataTable extends CustomElement {
        /**
         * This method is called by the `instanceof` operator.
         * @returns {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} 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 {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: 800,
                    },
    
                    labels: {
                        theListContainsNoEntries: "The list contains no entries",
                    },
    
                    features: {
                        settings: true,
                        footer: true,
                        autoInit: true,
                    },
    
                    templateMapping: {
                        "row-key": null,
                        "filter-id": null,
                    },
                },
                initOptionsFromArguments.call(this),
            );
        }
    
        /**
         *
         * @param {string} selector
         * @returns {NodeListOf<*>}
         */
        getGridElements(selector) {
            return this[gridElementSymbol].querySelectorAll(selector);
        }
    
        /**
         *
         * @return {string}
         */
        static getTag() {
            return "monster-datatable";
        }
    
        /**
         *
         * @return {Monster.Components.Form.Form}
         */
        [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 elements = document.querySelectorAll(selector);
                if (elements.length !== 1) {
                    throw new Error("the selector must match exactly one element");
                }
    
                const element = elements[0];
    
                if (!isInstance(element, Datasource)) {
                    throw new TypeError("the element must be a datasource");
                }
    
                this[datasourceLinkedElementSymbol] = element;
    
                setTimeout(() => {
                    handleDataSourceChanges.call(this);
                    element.datasource.attachObserver(
                        new Observer(handleDataSourceChanges.bind(this)),
                    );
                }, 0);
            }
    
            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];
        }
    }
    
    /**
     * @private
     * @returns {string}
     */
    function getColumnVisibilityConfigKey() {
        return generateUniqueConfigKey("datatable", this?.id, "columns-visibility");
    }
    
    /**
     * @private
     * @returns {string}
     */
    function getFilterConfigKey() {
        return generateUniqueConfigKey("datatable", this?.id, "filter");
    }
    
    /**
     * @private
     * @returns {Promise}
     */
    function getHostConfig(callback) {
        const document = getDocument();
        const host = document.querySelector("monster-host");
    
        if (!(host && this.id)) {
            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 document = getDocument();
        const host = document.querySelector("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;
    
        getWindow().addEventListener("resize", (event) => {
            updateGrid.call(self);
        });
    
        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();
    
                setTimeout(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);
                }, 0);
            }
        });
    }
    
    /**
     * @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 header = new Header();
            header.setInternals({
                field: field,
                label: label,
                classes: hClass,
                index: i,
                mode: mode,
                grid: grid,
                labelKey: labelKey,
                direction: direction,
            });
    
            headers.push(header);
        }
    
        this.setOption("headers", headers);
        setTimeout(() => {
                storeOrderStatement.call(this, this.getOption("features.autoInit"));
    
            },
            0);
    }
    
    /**
     * @private
     * @returns {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 document = getDocument();
        const host = document.querySelector("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) {
            //     host.deleteConfig(configKey);
            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 = getDocument().body.getBoundingClientRect().width;
    
        const breakpoint = this.getOption("responsive.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 {Monster.Components.Datatable.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 {Monster.Components.Datatable.Form}
     */
    function initControlReferences() {
        if (!this.shadowRoot) {
            throw new Error("no shadow-root is defined");
        }
    
        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");
    
        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" 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">
                <template id="headers-row">
                    <div data-monster-attributes="class path:headers-row.classname,
                                                  data-monster-index path:headers-row.index"
                         data-monster-replace="path:headers-row.html"></div>
                </template>
                <slot></slot>
                <div class="table-container" part="table-container">
                    <div class="filter">
                        <slot name="filter"></slot>
                    </div>
                    <div class="bar">
                        <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);