/** * 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 { ATTRIBUTE_ROLE } from "../../dom/constants.mjs"; import { assembleMethodSymbol, registerCustomElement, } from "../../dom/customelement.mjs"; import { getDocument } from "../../dom/util.mjs"; import { isFunction } from "../../types/is.mjs"; import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; import { Popper } from "../layout/popper.mjs"; import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs"; import { PopperButtonStyleSheet } from "./stylesheet/popper-button.mjs"; import { positionPopper } from "./util/floating-ui.mjs"; import "./button.mjs"; import { addErrorAttribute } from "../../dom/error.mjs"; export { PopperButton }; /** * @private * @type {symbol} */ const timerCallbackSymbol = Symbol("timerCallback"); /** * local symbol * @private * @type {symbol} */ const resizeObserverSymbol = Symbol("resizeObserver"); /** * local symbol * @private * @type {symbol} */ const closeEventHandler = Symbol("closeEventHandler"); /** * @private * @type {symbol} */ const controlElementSymbol = Symbol("controlElement"); /** * @private * @type {symbol} */ const buttonElementSymbol = Symbol("buttonElement"); /** * local symbol * @private * @type {symbol} */ const popperElementSymbol = Symbol("popperElement"); /** * local symbol * @private * @type {symbol} */ const arrowElementSymbol = Symbol("arrowElement"); /** * A beautiful popper button that can make your life easier and also looks good. * * @fragments /fragments/components/form/popper-button/ * * @example /examples/components/form/popper-button-simple * * @issue https://localhost.alvine.dev:8443/development/issues/closed/283.html * * @since 1.5.0 * @copyright schukai GmbH * @summary A beautiful popper button * @fires monster-options-set * @fires monster-selected * @fires monster-change * @fires monster-changed */ class PopperButton extends Popper { /** * This method is called by the `instanceof` operator. * @return {symbol} * @since 2.1.0 */ static get [instanceSymbol]() { return Symbol.for( "@schukai/monster/components/form/popper-button@@instance", ); } /** * 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 The templates for the control. * @property {string} templates.main The main template. * @property {Object} actions The actions for the control. * @property {Function} actions.click The click action. * @property {Object} classes The classes for the control. * @property {string} classes.button The button class. * @property {Object} labels The labels for the control. * @property {string} labels.button The button label. * @property {string} mode The mode of the control. * @property {string} value The value of the control. * @property {Object} aria The aria attributes for the control. * @property {string} aria.role The role of the control, only if the control is not a button. * @property {string} aria.label The label of the control. */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, actions: { click: (e) => { this.toggleDialog(); }, }, classes: { button: "monster-button-outline-primary", }, labels: { button: '<slot name="button"></slot>', }, mode: "click", value: null, aria: { role: null, label: null, }, }); } /** * * @return {void} */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); return; } /** * @return {string} */ static getTag() { return "monster-popper-button"; } /** * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { const styles = super.getCSSStyleSheet(); styles.push(PopperButtonStyleSheet); return styles; } /** * @return {void} */ connectedCallback() { super.connectedCallback(); const document = getDocument(); for (const [, type] of Object.entries(["click", "touch"])) { // close on outside ui-events document.addEventListener(type, this[closeEventHandler]); } updatePopper.call(this); attachResizeObserver.call(this); } /** * @return {void} */ disconnectedCallback() { super.disconnectedCallback(); // close on outside ui-events for (const [, type] of Object.entries(["click", "touch"])) { document.removeEventListener(type, this[closeEventHandler]); } disconnectResizeObserver.call(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(); } } /** * @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 * @throws {Error} unsupported type */ set value(value) { this.setOption("value", value); try { this?.setFormValue(this.value); } catch (e) { addErrorAttribute(this, e); } } } /** * @private * @return {initEventHandler} */ function initEventHandler() { this[closeEventHandler] = (event) => { const path = event.composedPath(); for (const [, element] of Object.entries(path)) { if (element === this) { return; } } this.hideDialog(); }; return this; } /** * @private */ function attachResizeObserver() { // against flickering this[resizeObserverSymbol] = new ResizeObserver(() => { if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { try { this[timerCallbackSymbol].touch(); return; } catch (e) { delete this[timerCallbackSymbol]; } } this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { updatePopper.call(this); }); }); requestAnimationFrame(() => { let parent = this.parentNode; while(!(parent instanceof HTMLElement) && parent !== null) { parent = parent.parentNode; } if (parent instanceof HTMLElement) { this[resizeObserverSymbol].observe(parent); } }); } function disconnectResizeObserver() { if (this[resizeObserverSymbol] instanceof ResizeObserver) { this[resizeObserverSymbol].disconnect(); } } /** * @private */ function updatePopper() { if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) { return; } if (this.getOption("disabled", false) === true) { return; } positionPopper.call( this, this[controlElementSymbol], this[popperElementSymbol], this.getOption("popper", {}), ); } /** * @private * @return {Select} */ function initControlReferences() { this[controlElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=control]`, ); this[buttonElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=button]`, ); this[popperElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=popper]`, ); this[arrowElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=arrow]`, ); } /** * @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" aria-labelledby="monster-popper-button-aria-label" data-monster-replace="path:labels.button"></button> <div id="monster-popper-button-aria-label" class="visually-hidden" data-monster-replace="path:aria.label">click me</div> <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1"> <div data-monster-role="arrow"></div> <div part="content" class="flex" data-monster-replace="path:content"> </div> </div> </div> `; } registerCustomElement(PopperButton);