Skip to content
Snippets Groups Projects
Select Git revision
  • 32d43486403593de2d98457a874d36f6b2e88c3e
  • master default protected
2 results

Tab.js

Blame
  • popper-button.mjs 9.87 KiB
    /**
     * 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);