From 660b649ece25e34b621250317ff6449c9b28058e Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Mon, 30 Dec 2024 14:32:11 +0100
Subject: [PATCH] fix(monster-toggle-switch): bouncing effekt #274

---
 source/components/form/toggle-switch.mjs | 694 ++++++++++++-----------
 1 file changed, 364 insertions(+), 330 deletions(-)

diff --git a/source/components/form/toggle-switch.mjs b/source/components/form/toggle-switch.mjs
index fc0e1eb2a..11bd4e833 100644
--- a/source/components/form/toggle-switch.mjs
+++ b/source/components/form/toggle-switch.mjs
@@ -12,26 +12,24 @@
  * SPDX-License-Identifier: AGPL-3.0
  */
 
-import { instanceSymbol } from "../../constants.mjs";
-import { internalSymbol } from "../../constants.mjs";
-import { CustomControl } from "../../dom/customcontrol.mjs";
-import { Observer } from "../../types/observer.mjs";
-import { ProxyObserver } from "../../types/proxyobserver.mjs";
+import {instanceSymbol, internalSymbol} from "../../constants.mjs";
+import {CustomControl} from "../../dom/customcontrol.mjs";
+import {Observer} from "../../types/observer.mjs";
+import {ProxyObserver} from "../../types/proxyobserver.mjs";
 
-import { addAttributeToken } from "../../dom/attributes.mjs";
+import {addAttributeToken} from "../../dom/attributes.mjs";
 import {
-	assembleMethodSymbol,
-	registerCustomElement,
-	updaterTransformerMethodsSymbol,
+    assembleMethodSymbol,
+    registerCustomElement,
+    updaterTransformerMethodsSymbol,
 } from "../../dom/customelement.mjs";
-import { isObject, isFunction } from "../../types/is.mjs";
-import { ToggleSwitchStyleSheet } from "./stylesheet/toggle-switch.mjs";
-import {
-	ATTRIBUTE_ERRORMESSAGE,
-	ATTRIBUTE_ROLE,
-} from "../../dom/constants.mjs";
+import {isFunction, isObject} from "../../types/is.mjs";
+import {ToggleSwitchStyleSheet} from "./stylesheet/toggle-switch.mjs";
+import {ATTRIBUTE_ERRORMESSAGE, ATTRIBUTE_ROLE,} from "../../dom/constants.mjs";
+import {getWindow} from "../../dom/util.mjs";
+import {fireEvent} from "../../dom/events.mjs";
 
-export { ToggleSwitch };
+export {ToggleSwitch};
 
 /**
  * @private
@@ -54,7 +52,7 @@ export const STATE_OFF = "off";
  *
  * @fragments /fragments/components/form/toggle-switch
  *
- * @example /examples/components/form/toggle-switch-simple
+ * @example /examples/components/form/toggle-switch-simple Simple example
  *
  * @since 3.57.0
  * @copyright schukai GmbH
@@ -65,336 +63,351 @@ export const STATE_OFF = "off";
  * @fires monster-changed
  */
 class ToggleSwitch extends CustomControl {
-	/**
-	 * 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 {string} value=current value of the element
-	 * @property {Boolean} disabled=disabled=false Disabled state
-	 * @property {Object} classes
-	 * @property {string} classes.on=specifies the class for the on state.
-	 * @property {string} classes.off=specifies the class for the off state.
-	 * @property {Object} values
-	 * @property {string} values.off=specifies the value of the element if it is not selected
-	 * @property {Object} labels
-	 * @property {string} labels.on=specifies the label for the on state.
-	 * @property {string} labels.off=specifies the label for the off state.
-	 * @property {string} actions
-	 * @property {string} actions.on=specifies the action for the on state.
-	 * @property {string} actions.off=specifies the action for the off state.
-	 * @property {Object} templates
-	 * @property {string} templates.main=specifies the main template used by the control.
-	 */
-	get defaults() {
-		return Object.assign({}, super.defaults, {
-			value: null,
-			disabled: false,
-			classes: {
-				on: "monster-theme-on",
-				off: "monster-theme-off",
-				handle: "monster-theme-primary-1",
-			},
-			values: {
-				on: "on",
-				off: "off",
-			},
-			labels: {
-				toggleSwitchOn: "✔",
-				toggleSwitchOff: "✖",
-			},
-			templates: {
-				main: getTemplate(),
-			},
-			actions: {
-				on: () => {},
-				off: () => {},
-			},
-		});
-	}
-
-	/**
-	 * @return {ToggleSwitch}
-	 */
-	[assembleMethodSymbol]() {
-		const self = this;
-		super[assembleMethodSymbol]();
-		initControlReferences.call(this);
-		initEventHandler.call(this);
-
-		/**
-		 * init value to off
-		 * if the value was not defined before inserting it into the HTML
-		 */
-		if (self.getOption("value") === null) {
-			self.setOption("value", self.getOption("values.off"));
-		}
-
-		/**
-		 * value from attribute
-		 */
-		if (self.hasAttribute("value")) {
-			self.setOption("value", self.getAttribute("value"));
-		}
-
-		/**
-		 * validate value
-		 */
-		validateAndSetValue.call(self);
-
-		if (this.state === STATE_ON) {
-			toggleClassOn.call(self);
-		} else {
-			toggleClassOff.call(self);
-		}
-
-		/**
-		 * is called when options changed
-		 */
-		self[internalSymbol].attachObserver(
-			new Observer(function () {
-				if (isObject(this) && this instanceof ProxyObserver) {
-					validateAndSetValue.call(self);
-					toggleClass.call(self);
-				}
-			}),
-		);
-
-		return this;
-	}
-
-	/**
-	 * updater transformer methods for pipe
-	 *
-	 * @return {function}
-	 */
-	[updaterTransformerMethodsSymbol]() {
-		return {
-			"state-callback": (Wert) => {
-				return this.state;
-			},
-		};
-	}
-
-	/**
-	 * @return [CSSStyleSheet]
-	 */
-	static getCSSStyleSheet() {
-		return [ToggleSwitchStyleSheet];
-	}
-
-	/**
-	 * toggle switch
-	 *
-	 * ```
-	 * e = document.querySelector('monster-toggle-switch');
-	 * e.click()
-	 * ```
-	 */
-	click() {
-		toggleValues.call(this);
-	}
-
-	/**
-	 * toggle switch on/off
-	 *
-	 * ```
-	 * e = document.querySelector('monster-toggle-switch');
-	 * e.toggle()
-	 * ```
-	 *
-	 * @return {ToggleSwitch}
-	 */
-	toggle() {
-		this.click();
-		return this;
-	}
-
-	/**
-	 * toggle switch on
-	 *
-	 * ```
-	 * e = document.querySelector('monster-toggle-switch');
-	 * e.toggleOn()
-	 * ```
-	 *
-	 * @return {ToggleSwitch}
-	 */
-	toggleOn() {
-		this.setOption("value", this.getOption("values.on"));
-		return this;
-	}
-
-	/**
-	 * toggle switch off
-	 *
-	 * ```
-	 * e = document.querySelector('monster-toggle-switch');
-	 * e.toggleOff()
-	 * ```
-	 *
-	 * @return {ToggleSwitch}
-	 */
-	toggleOff() {
-		this.setOption("value", this.getOption("values.off"));
-		return this;
-	}
-
-	/**
-	 * returns the status of the element
-	 *
-	 * ```
-	 * e = document.querySelector('monster-toggle-switch');
-	 * console.log(e.state)
-	 * // ↦ off
-	 * ```
-	 *
-	 * @return {string}
-	 */
-	get state() {
-		return this.getOption("value") === this.getOption("values.on")
-			? STATE_ON
-			: STATE_OFF;
-	}
-
-	/**
-	 * The current value of the Switch
-	 *
-	 * ```
-	 * e = document.querySelector('monster-toggle-switch');
-	 * console.log(e.value)
-	 * // ↦ on
-	 * ```
-	 *
-	 * @return {string}
-	 */
-	get value() {
-		return this.state === STATE_ON
-			? this.getOption("values.on")
-			: this.getOption("values.off");
-	}
-
-	/**
-	 * Set value
-	 *
-	 * ```
-	 * e = document.querySelector('monster-toggle-switch');
-	 * e.value="on"
-	 * ```
-	 *
-	 * @property {string} value
-	 */
-	set value(value) {
-		this.setOption("value", value);
-	}
-
-	/**
-	 * This method is called by the `instanceof` operator.
-	 * @return {symbol}
-	 */
-	static get [instanceSymbol]() {
-		return Symbol.for(
-			"@schukai/monster/components/form/toggle-switch@@instance",
-		);
-	}
-
-	static getTag() {
-		return "monster-toggle-switch";
-	}
+    /**
+     * 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 {string} value=current value of the element
+     * @property {Boolean} disabled=disabled=false Disabled state
+     * @property {Object} classes
+     * @property {string} classes.on=specifies the class for the on state.
+     * @property {string} classes.off=specifies the class for the off state.
+     * @property {Object} values
+     * @property {string} values.off=specifies the value of the element if it is not selected
+     * @property {Object} labels
+     * @property {string} labels.on=specifies the label for the on state.
+     * @property {string} labels.off=specifies the label for the off state.
+     * @property {string} actions
+     * @property {string} actions.on=specifies the action for the on state.
+     * @property {string} actions.off=specifies the action for the off state.
+     * @property {Object} templates
+     * @property {string} templates.main the main template used by the control.
+     */
+    get defaults() {
+        return Object.assign({}, super.defaults, {
+            value: null,
+            disabled: false,
+            classes: {
+                on: "monster-theme-on",
+                off: "monster-theme-off",
+                handle: "monster-theme-primary-1",
+            },
+            values: {
+                on: "on",
+                off: "off",
+            },
+            labels: {
+                toggleSwitchOn: "✔",
+                toggleSwitchOff: "✖",
+            },
+            templates: {
+                main: getTemplate(),
+            },
+            actions: {
+                on: () => {
+                },
+                off: () => {
+                },
+            },
+        });
+    }
+
+    /**
+     * @return {void}
+     */
+    [assembleMethodSymbol]() {
+        const self = this;
+        super[assembleMethodSymbol]();
+
+        initControlReferences.call(this);
+        initEventHandler.call(this);
+
+        getWindow().requestAnimationFrame(() => {
+
+            /**
+             * init value to off
+             * if the value was not defined before inserting it into the HTML
+             */
+            if (self.getOption("value") === null) {
+                self.setOption("value", self.getOption("values.off"));
+            }
+
+            /**
+             * value from attribute
+             */
+            if (self.hasAttribute("value")) {
+                self.setOption("value", self.getAttribute("value"));
+            }
+
+            /**
+             * validate value
+             */
+            validateAndSetValue.call(self);
+
+            // this state is a getter
+            if (this.state === STATE_ON) {
+                toggleOn.call(self);
+            } else {
+                toggleOff.call(self);
+            }
+
+        });
+
+    }
+
+    /**
+     * updater transformer methods for pipe
+     *
+     * @return {function}
+     */
+    [updaterTransformerMethodsSymbol]() {
+        return {
+            "state-callback": (Wert) => {
+                return this.state;
+            },
+        };
+    }
+
+    /**
+     * @return [CSSStyleSheet]
+     */
+    static getCSSStyleSheet() {
+        return [ToggleSwitchStyleSheet];
+    }
+
+    /**
+     * toggle switch
+     *
+     * ```
+     * e = document.querySelector('monster-toggle-switch');
+     * e.click()
+     * ```
+     */
+    click() {
+        this.toggle();
+    }
+
+    /**
+     * toggle switch on/off
+     *
+     * ```
+     * e = document.querySelector('monster-toggle-switch');
+     * e.toggle()
+     * ```
+     *
+     * @return {ToggleSwitch}
+     */
+    toggle() {
+        if (this.getOption("value") === this.getOption("values.on")) {
+            return this.toggleOff()
+        }
+        return this.toggleOn()
+    }
+
+    /**
+     * toggle switch on
+     *
+     * ```
+     * e = document.querySelector('monster-toggle-switch');
+     * e.toggleOn()
+     * ```
+     *
+     * @return {ToggleSwitch}
+     */
+    toggleOn() {
+        this.setOption("value", this.getOption("values.on"));
+        fireEvent(this, "change");
+        return this;
+    }
+
+    /**
+     * toggle switch off
+     *
+     * ```
+     * e = document.querySelector('monster-toggle-switch');
+     * e.toggleOff()
+     * ```
+     *
+     * @return {ToggleSwitch}
+     */
+    toggleOff() {
+        this.setOption("value", this.getOption("values.off"));
+        fireEvent(this, "change");
+        return this;
+    }
+
+    /**
+     * returns the status of the element
+     *
+     * ```
+     * e = document.querySelector('monster-toggle-switch');
+     * console.log(e.state)
+     * // ↦ off
+     * ```
+     *
+     * @return {string}
+     */
+    get state() {
+        return this.getOption("value") === this.getOption("values.on")
+            ? STATE_ON
+            : STATE_OFF;
+    }
+
+    /**
+     * The current value of the Switch
+     *
+     * ```
+     * e = document.querySelector('monster-toggle-switch');
+     * console.log(e.value)
+     * // ↦ on
+     * ```
+     *
+     * @return {string}
+     */
+    get value() {
+        return this.getOption("value");
+    }
+
+    /**
+     * Set value
+     *
+     * ```
+     * e = document.querySelector('monster-toggle-switch');
+     * e.value="on"
+     * ```
+     *
+     * @property {string} value
+     */
+    set value(value) {
+
+        if (value === this.getOption("values.on") || value === this.getOption("values.off")) {
+            if (this.state !== (value === this.getOption("values.on") ? STATE_ON : STATE_OFF)) {
+                this.setOption("value", value);
+            }
+            return;
+        }
+
+        addAttributeToken(
+            this,
+            ATTRIBUTE_ERRORMESSAGE,
+            'The value "' +
+            value +
+            '" must be "' +
+            this.getOption("values.on") +
+            '" or "' +
+            this.getOption("values.off"),
+        );
+
+
+    }
+
+    /**
+     * This method is called by the `instanceof` operator.
+     * @return {symbol}
+     */
+    static get [instanceSymbol]() {
+        return Symbol.for(
+            "@schukai/monster/components/form/toggle-switch@@instance",
+        );
+    }
+
+    /**
+     *
+     * @returns {string}
+     */
+    static getTag() {
+        return "monster-toggle-switch";
+    }
 }
 
 /**
  * @private
  */
 function initControlReferences() {
-	this[switchElementSymbol] = this.shadowRoot.querySelector(
-		`[${ATTRIBUTE_ROLE}=switch]`,
-	);
+    this[switchElementSymbol] = this.shadowRoot.querySelector(
+        `[${ATTRIBUTE_ROLE}=switch]`,
+    );
 }
 
-/**
- * @private
- */
-function toggleClassOn() {
-	this[switchElementSymbol].classList.remove(this.getOption("classes.off")); // change color
-	this[switchElementSymbol].classList.add(this.getOption("classes.on")); // change color
-}
 
 /**
  * @private
  */
-function toggleClassOff() {
-	this[switchElementSymbol].classList.remove(this.getOption("classes.on")); // change color
-	this[switchElementSymbol].classList.add(this.getOption("classes.off")); // change color
-}
+function toggleOn() {
 
-/**
- * @private
- */
-function toggleClass() {
-	if (this.getOption("value") === this.getOption("values.on")) {
-		toggleClassOn.call(this);
-	} else {
-		toggleClassOff.call(this);
-	}
+    this[switchElementSymbol].classList.remove(this.getOption("classes.off")); // change color
+    this[switchElementSymbol].classList.add(this.getOption("classes.on")); // change color
+
+    const callback = this.getOption("actions.on");
+    if (isFunction(callback)) {
+        callback.call(this);
+    }
+
+    if (typeof this.setFormValue === "function") {
+        this.setFormValue(this.getOption("values.on"));
+    }
 }
 
 /**
  * @private
  */
-function toggleValues() {
-	if (this.getOption("disabled") === true) {
-		return;
-	}
+function toggleOff() {
 
-	let callback, value;
+    this[switchElementSymbol].classList.remove(this.getOption("classes.on")); // change color
+    this[switchElementSymbol].classList.add(this.getOption("classes.off")); // change color
 
-	if (this.getOption("value") === this.getOption("values.on")) {
-		value = this.getOption("values.off");
-		callback = this.getOption("actions.off");
-	} else {
-		value = this.getOption("values.on");
-		callback = this.getOption("actions.on");
-	}
+    const callback = this.getOption("actions.off");
+    if (isFunction(callback)) {
+        callback.call(this);
+    }
 
-	this.setOption("value", value);
-	this?.setFormValue(value);
+    if (typeof this.setFormValue === "function") {
+        this.setFormValue(this.getOption("values.off"));
+    }
 
-	if (isFunction(callback)) {
-		callback.call(this);
-	}
 
-	this.setOption("state", this.state);
 }
 
 /**
  * @private
  */
 function validateAndSetValue() {
-	const value = this.getOption("value");
-
-	const validatedValues = [];
-	validatedValues.push(this.getOption("values.on"));
-	validatedValues.push(this.getOption("values.off"));
-
-	if (validatedValues.includes(value) === false) {
-		addAttributeToken(
-			this,
-			ATTRIBUTE_ERRORMESSAGE,
-			'The value "' +
-				value +
-				'" must be "' +
-				this.getOption("values.on") +
-				'" or "' +
-				this.getOption("values.off"),
-		);
-		this.setOption("disabled", true);
-		this.formDisabledCallback(true);
-	} else {
-		this.setOption("disabled", false);
-		this.formDisabledCallback(false);
-	}
+
+    const value = this.getOption("value");
+
+    const validatedValues = [];
+    validatedValues.push(this.getOption("values.on"));
+    validatedValues.push(this.getOption("values.off"));
+
+    if (validatedValues.includes(value) === false) {
+        addAttributeToken(
+            this,
+            ATTRIBUTE_ERRORMESSAGE,
+            'The value "' +
+            value +
+            '" must be "' +
+            this.getOption("values.on") +
+            '" or "' +
+            this.getOption("values.off"),
+        );
+        this.setOption("disabled", true);
+        this.formDisabledCallback(true);
+        return;
+    }
+
+    this.setOption("disabled", false);
+    this.formDisabledCallback(false);
+
+    if (value === this.getOption("values.on")) {
+        toggleOn.call(this);
+        return;
+    }
+
+    toggleOff.call(this);
+
+
 }
 
 /**
@@ -402,16 +415,37 @@ function validateAndSetValue() {
  * @return {initEventHandler}
  */
 function initEventHandler() {
-	const self = this;
-	self.addEventListener("keyup", function (event) {
-		if (event.code === "Space") {
-			self[switchElementSymbol].click();
-		}
-	});
-	self.addEventListener("click", function (event) {
-		toggleValues.call(self);
-	});
-	return this;
+    const self = this;
+
+    let lastValue = self.value;
+    self[internalSymbol].attachObserver(
+        new Observer(function () {
+            if (isObject(this) && this instanceof ProxyObserver) {
+                const n = this.getSubject()?.options?.value;
+                if (lastValue !== n) {
+                    lastValue = n;
+                    validateAndSetValue.call(self);
+                }
+            }
+        }),
+    );
+
+
+    self.addEventListener("keyup", (event) => {
+        if (event.keyCode === 32) {
+            self.toggle();
+        }
+    });
+
+    self.addEventListener("click", (event) => {
+        self.toggle();
+    });
+
+    self.addEventListener("touch", (event) => {
+        self.toggle();
+    });
+
+    return this;
 }
 
 /**
@@ -419,8 +453,8 @@ function initEventHandler() {
  * @return {string}
  */
 function getTemplate() {
-	// language=HTML
-	return `
+    // language=HTML
+    return `
         <div data-monster-role="control" part="control" tabindex="0">
             <div class="switch" data-monster-role="switch"
                  data-monster-attributes="data-monster-state path:value | call:state-callback">
-- 
GitLab