Something went wrong on our end
Select Git revision
invalid.mjs
-
Volker Schukai authoredVolker Schukai authored
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);