Something went wrong on our end
Select Git revision
dom-based-templating-implementation.md
-
Volker Schukai authoredVolker Schukai authored
button.mjs 8.38 KiB
/**
* Copyright schukai GmbH and contributors 2023. All Rights Reserved.
* Node module: @schukai/monster
* This file is licensed under the AGPLv3 License.
* License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
*/
import { instanceSymbol } from "../../constants.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import {
ATTRIBUTE_ERRORMESSAGE,
ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import {
assembleMethodSymbol,
attributeObserverSymbol,
registerCustomElement,
} from "../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { isFunction } from "../../types/is.mjs";
import { ATTRIBUTE_BUTTON_CLASS } from "./constants.mjs";
import { ButtonStyleSheet } from "./stylesheet/button.mjs";
import { RippleStyleSheet } from "../stylesheet/ripple.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { isString } from "../../types/is.mjs";
export { Button };
/**
* @private
* @type {symbol}
*/
export const buttonElementSymbol = Symbol("buttonElement");
/**
* This CustomControl creates a button element with a variety of options.
*
* <img src="./images/button.png">
*
* Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library
*
* You can create this control either by specifying the HTML tag <monster-button />` directly in the HTML or using
* Javascript via the `document.createElement('monster-button');` method.
*
* ```html
* <monster-button></monster-button>
* ```
*
* Or you can create this CustomControl directly in Javascript:
*
* ```js
* import {Button} from '@schukai/component-form/source/button.js';
* document.createElement('monster-button');
* ```
*
* The `data-monster-button-class` attribute can be used to change the CSS class of the button.
*
* @externalExample ../../../example/components/form/button.mjs
* @startuml button.png
* skinparam monochrome true
* skinparam shadowing false
* HTMLElement <|-- CustomElement
* CustomElement <|-- CustomControl
* CustomControl <|-- Button
* @enduml
*
* @since 1.5.0
* @copyright schukai GmbH
* @memberOf Monster.Components.Form
* @summary A simple button
*/
class Button extends CustomControl {
/**
* This method is called by the `instanceof` operator.
* @returns {symbol}
* @since 2.1.0
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/form/button@@instance");
}
/**
*
* @return {Monster.Components.Form.Button}
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventhandler.call(this);
return this;
}
/**
* The Button.click() method simulates a click on the internal button element.
*
* @since 3.27.0
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
*/
click() {
if (this.getOption("disabled") === true) {
return;
}
if (
this[buttonElementSymbol] &&
isFunction(this[buttonElementSymbol].click)
) {
this[buttonElementSymbol].click();
}
}
/**
* The Button.focus() method sets focus on the internal button element.
*
* @since 3.27.0
* @param {Object} options
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
*/
focus(options) {
if (this.getOption("disabled") === true) {
return;
}
if (
this[buttonElementSymbol] &&
isFunction(this[buttonElementSymbol].focus)
) {
this[buttonElementSymbol].focus(options);
}
}
/**
* The Button.blur() method removes focus from the internal button element.
*/
blur() {
if (
this[buttonElementSymbol] &&
isFunction(this[buttonElementSymbol].blur)
) {
this[buttonElementSymbol].blur();
}
}
/**
* This method determines which attributes are to be monitored by `attributeChangedCallback()`.
*
* @return {string[]}
*/
static get observedAttributes() {
const attributes = super.observedAttributes;
attributes.push(ATTRIBUTE_BUTTON_CLASS);
return attributes;
}
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
* @return {boolean}
*/
static get formAssociated() {
return true;
}
/**
* The current selection of the Select
*
* ```
* e = document.querySelector('monster-select');
* console.log(e.value)
* // ↦ 1
* // ↦ ['1','2']
* ```
*
* @property {string|array}
*/
get value() {
return this.getOption("value");
}
/**
* Set selection
*
* ```
* e = document.querySelector('monster-select');
* e.value=1
* ```
*
* @property {string|array} value
* @since 1.2.0
* @throws {Error} unsupported type
*/
set value(value) {
this.setOption("value", value);
try {
this?.setFormValue(this.value);
} catch (e) {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
}
}
/**
* 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} labels Labels
* @property {string} labels.button=<slot></slot> Button label
* @property {Object} actions Callbacks
* @property {string} actions.click="throw Error" Callback when clicked
* @property {Object} classes CSS classes
* @property {string} classes.button="monster-button-primary" CSS class for the button
* @property {boolean} disabled=false Disabled state
* @property {Object} effects Effects
* @property {boolean} effects.ripple=true Ripple effect
*/
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
labels: {
button: "<slot></slot>",
},
classes: {
button: "monster-button-primary",
},
disabled: false,
actions: {
click: (e) => {
throw new Error("the click action is not defined");
},
},
effects: {
ripple: true,
},
value: null,
});
}
/**
*
* @return {string}
*/
static getTag() {
return "monster-button";
}
/**
*
* @return {Array<CSSStyleSheet>}
*/
static getCSSStyleSheet() {
return [RippleStyleSheet, ButtonStyleSheet];
}
}
/**
* @private
* @return {initEventhandler}
* @fires Monster.Components.Form.event:monster-button-clicked
*/
function initEventhandler() {
const self = this;
const button = this[buttonElementSymbol];
const type = "click";
button.addEventListener(type, function (event) {
const callback = self.getOption("actions.click");
fireCustomEvent(self, "monster-button-clicked", {
button: self,
});
if (!isFunction(callback)) {
return;
}
const element = findTargetElementFromEvent(
event,
ATTRIBUTE_ROLE,
"control",
);
if (!(element instanceof Node && self.hasNode(element))) {
return;
}
callback.call(self, event);
});
if (self.getOption("effects.ripple")) {
button.addEventListener("click", createRipple.bind(self));
}
// data-monster-options
self[attributeObserverSymbol][ATTRIBUTE_BUTTON_CLASS] = function (value) {
self.setOption("classes.button", value);
};
return this;
}
/**
* @private
*/
function initControlReferences() {
this[buttonElementSymbol] = this.shadowRoot.querySelector(
`[${ATTRIBUTE_ROLE}=button]`,
);
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<div data-monster-role="control" part="control">
<button data-monster-attributes="disabled path:disabled | if:true, class path:classes.button"
data-monster-role="button"
part="button"
data-monster-replace="path:labels.button"></button>
</div>`;
}
function createRipple(event) {
const button = this[buttonElementSymbol];
const circle = document.createElement("span");
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - button.offsetLeft - radius}px`;
circle.style.top = `${event.clientY - button.offsetTop - radius}px`;
circle.classList.add("monster-fx-ripple");
const ripples = button.getElementsByClassName("monster-fx-ripple");
for (const ripple of ripples) {
ripple.remove();
}
button.appendChild(circle);
}
registerCustomElement(Button);