/** * 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 { instanceSymbol, internalSymbol } from "../../constants.mjs"; import { diff } from "../../data/diff.mjs"; import { addAttributeToken } from "../../dom/attributes.mjs"; import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs"; import { assembleMethodSymbol, CustomElement, attributeObserverSymbol, registerCustomElement, } from "../../dom/customelement.mjs"; import { findElementWithSelectorUpwards } from "../../dom/util.mjs"; import { isString, isArray } from "../../types/is.mjs"; import { Observer } from "../../types/observer.mjs"; import { TokenList } from "../../types/tokenlist.mjs"; import { clone } from "../../util/clone.mjs"; import { State } from "../form/types/state.mjs"; import { ATTRIBUTE_DATASOURCE_SELECTOR } from "./constants.mjs"; import { Datasource } from "./datasource.mjs"; import { BadgeStyleSheet } from "../stylesheet/badge.mjs"; import { SaveButtonStyleSheet } from "./stylesheet/save-button.mjs"; import { handleDataSourceChanges, datasourceLinkedElementSymbol, } from "./util.mjs"; export { SaveButton }; /** * @private * @type {symbol} */ const stateButtonElementSymbol = Symbol("stateButtonElement"); /** * @private * @type {symbol} */ const originValuesSymbol = Symbol("originValues"); /** * @private * @type {symbol} */ const badgeElementSymbol = Symbol("badgeElement"); class SaveButton extends CustomElement { /** * This method is called by the `instanceof` operator. * @return {symbol} */ static get [instanceSymbol]() { return Symbol.for( "@schukai/monster/components/datasource/save-button@@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 The datasource * @property {string} datasource.selector The selector of the datasource * @property {string} labels.button The button label * @property {Object} classes The classes * @property {string} classes.bar The bar class * @property {string} classes.badge The badge class * @property {Array} ignoreChanges The ignore changes (regex) * @property {Array} data The data * @return {Object} */ get defaults() { const obj = Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, labels: { button: "save", }, classes: { bar: "monster-button-primary", badge: "monster-badge-secondary hidden", }, datasource: { selector: null, }, changes: "0", ignoreChanges: [], data: {}, disabled: false, }); updateOptionsFromArguments.call(this, obj); return obj; } /** * * @return {string} */ static getTag() { return "monster-datasource-save-button"; } /** * 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](); const self = this; 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 (!(element instanceof Datasource)) { throw new TypeError("the element must be a datasource"); } this[datasourceLinkedElementSymbol] = element; element.datasource.attachObserver( new Observer(handleDataSourceChanges.bind(this)), ); self[originValuesSymbol] = null; element.datasource.attachObserver( new Observer(function () { if (!self[originValuesSymbol]) { self[originValuesSymbol] = clone( self[datasourceLinkedElementSymbol].data, ); } const currentValues = this.getRealSubject(); const ignoreChanges = self.getOption("ignoreChanges"); const result = diff(self[originValuesSymbol], currentValues); if (isArray(ignoreChanges) && ignoreChanges.length > 0) { const itemsToRemove = []; for (const item of result) { for (const ignorePattern of ignoreChanges) { const p = new RegExp(ignorePattern); if (p.test(item.path)) { itemsToRemove.push(item); break; } } } for (const itemToRemove of itemsToRemove) { const index = result.indexOf(itemToRemove); if (index > -1) { result.splice(index, 1); } } } if (isArray(result) && result.length > 0) { self[stateButtonElementSymbol].setState("changed"); self[stateButtonElementSymbol].setOption("disabled", false); self.setOption("changes", result.length); self.setOption( "classes.badge", new TokenList(self.getOption("classes.badge")) .remove("hidden") .toString(), ); } else { self[stateButtonElementSymbol].removeState(); self[stateButtonElementSymbol].setOption("disabled", true); self.setOption("changes", 0); self.setOption( "classes.badge", new TokenList(self.getOption("classes.badge")) .add("hidden") .toString(), ); } }), ); } this.attachObserver( new Observer(() => { handleDataSourceChanges.call(this); }), ); } /** * * @return [CSSStyleSheet] */ static getCSSStyleSheet() { return [SaveButtonStyleSheet, BadgeStyleSheet]; } } /** * @private * @return {Monster.Components.Datatable.Form} */ function initControlReferences() { if (!this.shadowRoot) { throw new Error("no shadow-root is defined"); } this[stateButtonElementSymbol] = this.shadowRoot.querySelector( "[data-monster-role=state-button]", ); this[badgeElementSymbol] = this.shadowRoot.querySelector( "[data-monster-role=badge]", ); if (this[stateButtonElementSymbol]) { queueMicrotask(() => { const states = { changed: new State( "changed", '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cloud-arrow-up" viewBox="0 0 16 16">\n' + ' <path fill-rule="evenodd" d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708z"/>\n' + ' <path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383m.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>\n' + "</svg>", ), }; this[stateButtonElementSymbol].removeState(); this[stateButtonElementSymbol].setOption("disabled", "disabled"); this[stateButtonElementSymbol].setOption("states", states); this[stateButtonElementSymbol].setOption( "labels.button", this.getOption("labels.button"), ); }); } return this; } /** * @private */ function initEventHandler() { queueMicrotask(() => { this[stateButtonElementSymbol].setOption("actions.click", () => { this[datasourceLinkedElementSymbol] .write() .then(() => { this[originValuesSymbol] = null; this[stateButtonElementSymbol].removeState(); this[stateButtonElementSymbol].setOption("disabled", true); this.setOption("changes", 0); this.setOption( "classes.badge", new TokenList(this.getOption("classes.badge")) .add("hidden") .toString(), ); }) .catch((error) => { addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString()); }); }); }); } /** * @param {Object} options */ function updateOptionsFromArguments(options) { 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" data-monster-attributes="disabled path:disabled | if:true"> <monster-state-button data-monster-role="state-button">save</monster-state-button> <div data-monster-attributes="disabled path:disabled | if:true, class path:classes.badge" data-monster-role="badge" data-monster-replace="path:changes"></div> </div> `; } registerCustomElement(SaveButton);