@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
486 lines (434 loc) • 10.8 kB
JavaScript
/**
* Copyright © schukai GmbH 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 schukai GmbH.
*
* 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 { fireEvent } from "../../dom/events.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
export { ToggleSwitch };
/**
* @private
* @type {symbol}
*/
const switchElementSymbol = Symbol("switchElement");
/**
* @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 schukai GmbH
* @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]();
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.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;
}
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.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.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 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);
showError.call(this);
return;
}
this.setOption("disabled", false);
this.formDisabledCallback(false);
if (value === this.getOption("values.on")) {
toggleOn.call(this);
return;
}
toggleOff.call(this);
}
/**
* @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 {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);