From efa3cf498dd40ccbb114cb0074409c1adf2bf6a7 Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Tue, 2 Jul 2024 22:30:40 +0200 Subject: [PATCH] fix: originValues in the savebutton is now reset. --- CHANGELOG.md | 6 +- source/components/datatable/save-button.mjs | 529 ++++++++++---------- 2 files changed, 269 insertions(+), 266 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f328ba0c..cb448a3da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,10 @@ # Changelog - - ## [3.73.4] - 2024-07-02 ### Bug Fixes -- eventprocessing is now only active in selected controls: form, filter. [#224](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/224) - - +- event processing is now only active in selected controls: form, filter. [#224](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/224) ## [3.73.3] - 2024-07-01 diff --git a/source/components/datatable/save-button.mjs b/source/components/datatable/save-button.mjs index e5490d278..83a347c1b 100644 --- a/source/components/datatable/save-button.mjs +++ b/source/components/datatable/save-button.mjs @@ -12,33 +12,33 @@ * 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 {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, + 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 {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, + handleDataSourceChanges, + datasourceLinkedElementSymbol, } from "./util.mjs"; -export { SaveButton }; +export {SaveButton}; /** * @private @@ -46,6 +46,12 @@ export { SaveButton }; */ const stateButtonElementSymbol = Symbol("stateButtonElement"); +/** + * @private + * @type {symbol} + */ +const originValuesSymbol = Symbol("originValues"); + /** * @private * @type {symbol} @@ -53,185 +59,185 @@ const stateButtonElementSymbol = Symbol("stateButtonElement"); const badgeElementSymbol = Symbol("badgeElement"); class SaveButton extends CustomElement { - /** - * This method is called by the `instanceof` operator. - * @returns {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)), - ); - - let originValues; - - element.datasource.attachObserver( - new Observer(function () { - if (!originValues) { - originValues = clone(self[datasourceLinkedElementSymbol].data); - } - - const currentValues = this.getRealSubject(); - const ignoreChanges = self.getOption("ignoreChanges"); - - const result = diff(originValues, 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]; - } + /** + * This method is called by the `instanceof` operator. + * @returns {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]; + } } /** @@ -239,77 +245,78 @@ class SaveButton extends CustomElement { * @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]) { - setTimeout(() => { - 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"), - ); - }, 1); - } - - return this; + 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]) { + setTimeout(() => { + 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"), + ); + }, 1); + } + + return this; } /** * @private */ function initEventHandler() { - setTimeout(() => { - this[stateButtonElementSymbol].setOption("actions.click", () => { - this[datasourceLinkedElementSymbol] - .write() - .then(() => { - 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()); - }); - }); - }, 1); + setTimeout(() => { + 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()); + }); + }); + }, 1); } /** * @param {Object} options */ function updateOptionsFromArguments(options) { - const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR); - if (selector) { - options.datasource.selector = selector; - } + const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR); + if (selector) { + options.datasource.selector = selector; + } } /** @@ -317,8 +324,8 @@ function updateOptionsFromArguments(options) { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // 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> -- GitLab