Skip to content
Snippets Groups Projects
Select Git revision
  • 3125bf9eaeaf7cb032fadcb6422341d4da54feb9
  • 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

change-button.mjs

Blame
  • save-button.mjs 9.95 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 { 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);