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