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