/** * 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 } 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 { addErrorAttribute } from "../../dom/error.mjs"; export { Button }; /** * @private * @type {symbol} */ export const buttonElementSymbol = Symbol("buttonElement"); /** * A button * * @fragments /fragments/components/form/button/ * * @example /examples/components/form/button-simple Simple Button * @example /examples/components/form/button-with-click-event Button with event * * @issue https://localhost.alvine.dev:8440/development/issues/closed/282.html * @issue https://localhost.alvine.dev:8440/development/issues/closed/283.html * * @copyright schukai GmbH * @summary A beautiful button that can make your life easier and also looks good. * @fires monster-button-clicked this event is triggered when the button is clicked. It contains the field {button} with the button instance. */ class Button extends CustomControl { /** * This method is called by the <code>instanceof</code> operator. * @return {symbol} * @since 2.1.0 */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/form/button@@instance"); } /** * * @return {Button} */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); return this; } /** * The <code>Button.click()</code> 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 value of the button. * * ```javascript * e = document.querySelector('monster-button'); * console.log(e.value) * ``` * * @return {string} The value of the button */ get value() { return this.getOption("value"); } /** * Set the value of the button. * * ```javascript * e = document.querySelector('monster-button'); * e.value=1 * ``` * * @param {string} value * @return {void} * @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 * @property {string} type="button" The default behavior of the button. Possible values are: submit, reset, button * @property {Object} aria Aria attributes * @property {string} aria.role The role of the button, should only be set if the button is not a button * @property {string} aria.label="click me!" The label of the button */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, labels: { button: "<slot></slot>", }, classes: { button: "monster-button-outline-primary", }, disabled: false, actions: { click: () => {}, }, effects: { ripple: true, }, value: null, type: "button", aria: { role: null, label: null, }, }); } /** * * @return {string} */ static getTag() { return "monster-button"; } /** * * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [RippleStyleSheet, ButtonStyleSheet]; } } /** * @private * @return {initEventHandler} */ 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, aria-role path:aria.role, path:aria.label | if:true, type path:type" data-monster-role="button" part="button" aria-labelledby="monster-button-aria-label" data-monster-replace="path:labels.button"></button> <div id="monster-button-aria-label" class="visually-hidden" data-monster-replace="path:aria.label">click me</div> </div>`; } /** * @private * @param event */ 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);