/** * Copyright 2023 schukai GmbH * SPDX-License-Identifier: AGPL-3.0 */ import { instanceSymbol, internalSymbol } from "../../constants.mjs"; import { Pathfinder } from "../../data/pathfinder.mjs"; import { getLinkedObjects, hasObjectLink } from "../../dom/attributes.mjs"; import { customElementUpdaterLinkSymbol } from "../../dom/constants.mjs"; import { assembleMethodSymbol, CustomElement, attributeObserverSymbol, registerCustomElement, } from "../../dom/customelement.mjs"; import { isString } from "../../types/is.mjs"; import { Observer } from "../../types/observer.mjs"; import { clone } from "../../util/clone.mjs"; import { ATTRIBUTE_DATASOURCE_SELECTOR, ATTRIBUTE_DATATABLE_INDEX, } from "./constants.mjs"; import { Datasource } from "./datasource.mjs"; import { DatasetStyleSheet } from "./stylesheet/dataset.mjs"; import { handleDataSourceChanges, datasourceLinkedElementSymbol, } from "./util.mjs"; import { FormStyleSheet } from "../stylesheet/form.mjs"; export { DataSet }; /** * The data set component is used to show the data of a data source. * * <img src="./images/dataset.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-dataset />` directly in the HTML or using * Javascript via the `document.createElement('monster-dataset');` method. * * ```html * <monster-dataset></monster-dataset> * ``` * * Or you can create this CustomControl directly in Javascript: * * ```js * import '@schukai/component-datatable/source/dataset.mjs'; * document.createElement('monster-dataset'); * ``` * * The Body should have a class "hidden" to ensure that the styles are applied correctly. * * ```css * body.hidden { * visibility: hidden; * } * ``` * * @startuml dataset.png * skinparam monochrome true * skinparam shadowing false * HTMLElement <|-- CustomElement * CustomElement <|-- DataSet * @enduml * * @copyright schukai GmbH * @memberOf Monster.Components.Datatable * @summary A data set */ class DataSet extends CustomElement { /** * This method is called by the `instanceof` operator. * @returns {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/dataset@@instance"); } /** * This method determines which attributes are to be monitored by `attributeChangedCallback()`. * * @return {string[]} * @since 1.15.0 */ static get observedAttributes() { const attributes = super.observedAttributes; attributes.push(ATTRIBUTE_DATATABLE_INDEX); return attributes; } /** * 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 The datasource * @property {string} datasource.selector The selector of the datasource * @property {object} mapping The mapping * @property {string} mapping.data The data * @property {number} mapping.index The index * @property {Array} data The data */ get defaults() { const obj = Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, datasource: { selector: null, }, mapping: { data: "dataset", index: 0, }, data: {}, }); updateOptionsFromArguments.call(this, obj); return obj; } /** * * @return {string} */ static getTag() { return "monster-dataset"; } write() {; return new Promise((resolve, reject) => { if (!this[datasourceLinkedElementSymbol]) { reject(new Error("No datasource")); return; } const internalUpdateCloneData = this.getInternalUpdateCloneData(); if (!internalUpdateCloneData) { reject(new Error("No update data")); return; } const internalData = internalUpdateCloneData?.["data"]; if ( internalData === undefined || internalData === null || internalData === "" ) { reject(new Error("No data")); return; } setTimeout(() => { const path = this.getOption("mapping.data"); const index = this.getOption("mapping.index"); let pathWithIndex; if (isString(path) && path !== "") { pathWithIndex = path + "." + index; } else { pathWithIndex = index; } const data = this[datasourceLinkedElementSymbol].data; const unref = JSON.stringify(data); const ref = JSON.parse(unref); new Pathfinder(ref).setVia(pathWithIndex, internalData); this[datasourceLinkedElementSymbol].data = ref; resolve(); }, 0); }); } /** * This method is responsible for assembling the component. * * It calls the parent's assemble method first, then initializes control references and event handlers. * If the `datasource.selector` option is provided and is a string, it searches for the corresponding * element in the DOM using that selector. * * If the selector matches exactly one element, it checks if the element is an instance of the `Datasource` class. * * If it is, the component's `datasourceLinkedElementSymbol` property is set to the element, and the component * attaches an observer to the datasource's changes. * * The observer is a function that calls the `handleDataSourceChanges` method in the context of the component. * Additionally, the component attaches an observer to itself, which also calls the `handleDataSourceChanges` * method in the component's context. */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); // initControlReferences.call(self); 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 (!(element instanceof Datasource)) { throw new TypeError("the element must be a datasource"); } this[datasourceLinkedElementSymbol] = element; element.datasource.attachObserver( new Observer(handleDataSourceChanges.bind(this)), ); } this.attachObserver( new Observer(() => { handleDataSourceChanges.call(this); }), ); } /** * * @return [CSSStyleSheet] */ static getCSSStyleSheet() { return [FormStyleSheet, DatasetStyleSheet]; } } /** * @private */ function initEventHandler() { this[attributeObserverSymbol][ATTRIBUTE_DATATABLE_INDEX] = () => { const index = this.getAttribute(ATTRIBUTE_DATATABLE_INDEX); if (index) { this.setOption("mapping.index", parseInt(index, 10)); } }; } /** * * @param {Object} options */ function updateOptionsFromArguments(options) { const index = this.getAttribute(ATTRIBUTE_DATATABLE_INDEX); if (index !== null && index !== undefined) { options.mapping.index = parseInt(index, 10); } const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR); if (selector) { options.datasource.selector = selector; } } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div data-monster-role="control" part="control"> <slot></slot> </div> `; } registerCustomElement(DataSet);