@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
647 lines (577 loc) • 15 kB
JavaScript
/**
* Copyright © Volker Schukai 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 Volker Schukai.
*
* SPDX-License-Identifier: AGPL-3.0
*/
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 {
assembleMethodSymbol,
registerCustomElement,
updaterTransformerMethodsSymbol,
} from "../../dom/customelement.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 { fireCustomEvent, fireEvent } from "../../dom/events.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
export { ToggleSwitch };
/**
* @private
* @type {symbol}
*/
const switchElementSymbol = Symbol("switchElement");
const invalidDisabledSymbol = Symbol("invalidDisabled");
/**
* @type {string}
*/
export const STATE_ON = "on";
/**
* @type {string}
*/
export const STATE_OFF = "off";
/**
* A simple toggle switch
*
* @fragments /fragments/components/form/toggle-switch
*
* @example /examples/components/form/toggle-switch-simple Simple example
*
* @since 3.57.0
* @copyright Volker Schukai
* @summary A beautiful switch element
* @fires monster-options-set
* @fires monster-selected
* @fires monster-change
* @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 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",
error: "monster-theme-error-1",
},
values: {
on: "on",
off: "off",
},
labels: {
toggleSwitchOn: "✔",
toggleSwitchOff: "×",
},
templates: {
main: getTemplate(),
},
actions: {
on: () => {},
off: () => {},
},
});
}
/**
* @return {void}
*/
[assembleMethodSymbol]() {
const self = this;
super[assembleMethodSymbol]();
initDisabledSync.call(this);
initControlReferences.call(this);
initEventHandler.call(this);
setTimeout(() => {
/**
* 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);
}
}, 0);
}
/**
* updater transformer methods for pipe
*
* @return {function}
*/
[updaterTransformerMethodsSymbol]() {
return {
"state-callback": () => {
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.hasAttribute("disabled") || this.getOption("disabled", false)) {
return this;
}
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");
fireCustomEvent(this, "monster-change", { value: this.value });
fireCustomEvent(this, "monster-changed", { value: this.value });
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");
fireCustomEvent(this, "monster-change", { value: this.value });
fireCustomEvent(this, "monster-changed", { value: this.value });
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) {
const normalized = normalizeToggleValue.call(this, value);
if (
normalized === this.getOption("values.on") ||
normalized === this.getOption("values.off")
) {
if (this.getOption("value") !== normalized) {
this.setOption("value", normalized);
} else {
validateAndSetValue.call(this);
}
return;
}
addErrorAttribute(
this,
'The value "' +
value +
'" must be "' +
this.getOption("values.on") +
'" or "' +
this.getOption("values.off"),
);
showError.call(this);
}
/**
* 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]`,
);
}
/**
* @private
*/
function toggleOn() {
if (!this[switchElementSymbol]) {
return;
}
this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
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 toggleOff() {
if (!this[switchElementSymbol]) {
return;
}
this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
this[switchElementSymbol].classList.remove(this.getOption("classes.on")); // change color
this[switchElementSymbol].classList.add(this.getOption("classes.off")); // change color
const callback = this.getOption("actions.off");
if (isFunction(callback)) {
callback.call(this);
}
if (typeof this.setFormValue === "function") {
this.setFormValue(this.getOption("values.off"));
}
}
/**
* @private
*/
function showError() {
if (!this[switchElementSymbol]) {
return;
}
this[switchElementSymbol].classList.remove(this.getOption("classes.on"));
this[switchElementSymbol].classList.remove(this.getOption("classes.off"));
this[switchElementSymbol].classList.add(this.getOption("classes.error"));
}
/**
* @private
*/
function clearError() {
if (this[switchElementSymbol]) {
this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
}
this.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
}
/**
* @private
*/
function validateAndSetValue() {
const rawValue = this.getOption("value");
const value = normalizeToggleValue.call(this, rawValue);
if (value !== rawValue) {
this.setOption("value", value);
}
const offValue = this.getOption("values.off");
if (value === undefined || value === null || value === "") {
clearError.call(this);
if (this[invalidDisabledSymbol]) {
this.setOption("disabled", false);
this.formDisabledCallback(false);
this[invalidDisabledSymbol] = false;
}
if (this.getOption("value") !== offValue) {
this.setOption("value", offValue);
}
toggleOff.call(this);
return;
}
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[invalidDisabledSymbol] =
!!this.getOption("disabled", false) !== true &&
this.hasAttribute("disabled") === false;
if (this[invalidDisabledSymbol]) {
this.setOption("disabled", true);
}
this.formDisabledCallback(true);
showError.call(this);
return;
}
if (this[invalidDisabledSymbol]) {
this.setOption("disabled", false);
this.formDisabledCallback(false);
this[invalidDisabledSymbol] = false;
}
clearError.call(this);
if (value === this.getOption("values.on")) {
toggleOn.call(this);
return;
}
toggleOff.call(this);
}
/**
* @private
* @param {*} value
* @return {*}
*/
function normalizeToggleValue(value) {
const onValue = this.getOption("values.on");
const offValue = this.getOption("values.off");
if (value === onValue || value === offValue) {
return value;
}
const token = normalizeToggleToken(value);
if (!token) {
return value;
}
const onToken = normalizeToggleToken(onValue);
const offToken = normalizeToggleToken(offValue);
if (token === "on") {
if (onToken === "on" || offToken === "on") {
return onToken === "on" ? onValue : offValue;
}
return onValue;
}
if (token === "off") {
if (onToken === "off" || offToken === "off") {
return onToken === "off" ? onValue : offValue;
}
return offValue;
}
if (token === "true") {
if (onToken === "true" || offToken === "true") {
return onToken === "true" ? onValue : offValue;
}
return onValue;
}
if (token === "false") {
if (onToken === "false" || offToken === "false") {
return onToken === "false" ? onValue : offValue;
}
return offValue;
}
return value;
}
/**
* @private
* @param {*} value
* @return {string|undefined}
*/
function normalizeToggleToken(value) {
if (value === true || value === "true" || value === "TRUE") {
return "true";
}
if (value === false || value === "false" || value === "FALSE") {
return "false";
}
if (value === "on" || value === "ON") {
return "on";
}
if (value === "off" || value === "OFF") {
return "off";
}
return undefined;
}
/**
* @private
* @return {initEventHandler}
*/
function initEventHandler() {
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;
}
/**
* @private
* @return {ToggleSwitch}
*/
function initDisabledSync() {
const self = this;
const syncDisabled = () => {
const disabledOption = !!self.getOption("disabled", false);
const hasDisabledAttr = self.hasAttribute("disabled");
const optionAttr = self.getAttribute("data-monster-option-disabled");
const optionAttrDisabled =
optionAttr !== null && optionAttr.toLowerCase() !== "false";
const desiredDisabled =
disabledOption || hasDisabledAttr || optionAttrDisabled;
if (desiredDisabled && !disabledOption) {
self.setOption("disabled", true);
}
if (desiredDisabled && !hasDisabledAttr) {
self.setAttribute("disabled", "");
}
if (!desiredDisabled) {
if (hasDisabledAttr) {
self.removeAttribute("disabled");
}
if (disabledOption) {
self.setOption("disabled", false);
}
}
};
syncDisabled();
self.attachObserver(new Observer(syncDisabled));
return this;
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// 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">
<div class="label on" data-monster-replace="path:labels.toggleSwitchOn"></div>
<div class="label off" data-monster-replace="path:labels.toggleSwitchOff"></div>
<div data-monster-attributes="class path:classes.handle | suffix:\\ switch-slider"></div>
</div>
</div>`;
}
registerCustomElement(ToggleSwitch);