diff --git a/development/issues/closed/268.html b/development/issues/closed/268.html new file mode 100644 index 0000000000000000000000000000000000000000..36a5b82c995dd286a90eee9d116785cbce456876 --- /dev/null +++ b/development/issues/closed/268.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>auto activate tab after close #268</title> + <script src="268.mjs" type="module"></script> +</head> +<body> + <h1>auto activate tab after close #268</h1> + <p>When one tab is closed, another should be activated.</p> + <ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/268">Issue #268</a></li> + <li><a href="/">Back to overview</a></li> + </ul> + <main> + + <monster-tabs data-monster-option-features-removeBehavior="auto"> + <div data-monster-button-label="A1" data-monster-removable>test</div> + <div data-monster-button-label="A2" data-monster-removable>test</div> + <div data-monster-button-label="A3" class="active" data-monster-removable>test</div> + <div data-monster-button-label="A4" data-monster-removable>test</div> + <div data-monster-button-label="A5" data-monster-removable>test</div> + </monster-tabs> + + </main> +</body> +</html> diff --git a/development/issues/closed/268.mjs b/development/issues/closed/268.mjs new file mode 100644 index 0000000000000000000000000000000000000000..201c5bd657796fda21cc22f34b978ae7fb6179ab --- /dev/null +++ b/development/issues/closed/268.mjs @@ -0,0 +1,15 @@ +/** +* @file development/issues/open/268.mjs +* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/268 +* @description auto activate tab after close +* @issue 268 +*/ + +import "../../../source/components/style/property.pcss"; +import "../../../source/components/style/link.pcss"; +import "../../../source/components/style/color.pcss"; +import "../../../source/components/style/theme.pcss"; +import "../../../source/components/style/normalize.pcss"; +import "../../../source/components/style/typography.pcss"; +import "../../../source/components/layout/tabs.mjs"; + diff --git a/source/components/datatable/save-button.mjs b/source/components/datatable/save-button.mjs index 6220fadf301111fb6f8f2bc4b1162a6b78ec4187..f7f8a492241e522c9b0ef873a100489d97d5ba6c 100644 --- a/source/components/datatable/save-button.mjs +++ b/source/components/datatable/save-button.mjs @@ -58,6 +58,9 @@ const originValuesSymbol = Symbol("originValues"); */ const badgeElementSymbol = Symbol("badgeElement"); +/** + * + */ class SaveButton extends CustomElement { /** * This method is called by the `instanceof` operator. diff --git a/source/components/form/context-error.mjs b/source/components/form/context-error.mjs index 561d296a981e697284e5714a17075574382d2026..a79b9831943105e535b29b05b6a65d76532f63e6 100644 --- a/source/components/form/context-error.mjs +++ b/source/components/form/context-error.mjs @@ -53,23 +53,12 @@ const popperElementSymbol = Symbol("popperElement"); */ const iconElementSymbol = Symbol("iconElement"); -/** - * The ContextError control shows an error message in a popper. - * - * @fragments /fragments/components/form/context-error/ - * - * @example /examples/components/form/context-error-simple - * - * @copyright schukai GmbH - * @summary A control that can be used to display a tooltip or a popover with an error message. - **/ - /** * A context error control. * - * @fragments /fragments/components/form/select/ + * @fragments /fragments/components/form/context-error * - * @example /examples/components/form/select-simple + * @example /examples/components/form/context-error-simple * * @since 3.55.0 * @copyright schukai GmbH diff --git a/source/components/form/context-help.mjs b/source/components/form/context-help.mjs index 9aea72cb56340bd237b66cfa61d0f66cec730a7c..01e7b371a24807defc82ebad0f06da5a01ca9fe8 100644 --- a/source/components/form/context-help.mjs +++ b/source/components/form/context-help.mjs @@ -23,7 +23,7 @@ export { ContextHelp }; /** * A context help control. * - * @fragments /fragments/components/form/context-help/ + * @fragments /fragments/components/form/context-help * * @example /examples/components/form/context-help-simple * diff --git a/source/components/form/password.mjs b/source/components/form/password.mjs index 8415ffaded497c0ba44dd96c8e26ccd0d2ff64ed..b71055b7cf669d5703851587891e729bcbbc6510 100644 --- a/source/components/form/password.mjs +++ b/source/components/form/password.mjs @@ -50,9 +50,9 @@ export const inputElementSymbol = Symbol("inputIconElement"); /** * A password field * - * @fragments /fragments/components/components/form/password/ + * @fragments /fragments/components/form/password * - * @example /examples/components/components/form/password-simple + * @example /examples/components/form/password-simple * * @since 3.89.0 * @copyright schukai GmbH diff --git a/source/components/form/reload.mjs b/source/components/form/reload.mjs index d4acf9792f18f3607f2358dad1633aa6e9979e0f..86c288af1ef0443fcfc926ec492a9635844cb0d0 100644 --- a/source/components/form/reload.mjs +++ b/source/components/form/reload.mjs @@ -121,7 +121,7 @@ class Reload extends CustomElement { * @property {string} url=undefined * @property {string} reload=undefined currently the values defined are `onshow` and `always`. The default `onshow` removes the IntersectionObserver. This means that the content is only loaded once. reloading of the content does not occur. * @property {string} filter=undefined dom selectors to search for elements, if undefined then everything is taken - * @property {Monster.Components.Form.Processor[]} processors + * @property {Object[]} processors * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) * @property {String} fetch.redirect=error * @property {String} fetch.method=GET diff --git a/source/components/form/shadow-reload.mjs b/source/components/form/shadow-reload.mjs index 42d4f80f6273999e147a6bd6192ca7851458b5cc..f5ca623ae7d5f4d7ac3fca322d427cd09d99b5e0 100644 --- a/source/components/form/shadow-reload.mjs +++ b/source/components/form/shadow-reload.mjs @@ -50,7 +50,7 @@ class ShadowReload extends Reload { * @property {string} url=undefined * @property {string} reload=undefined currently the values defined are `onshow` and `always`. The default `onshow` removes the IntersectionObserver. This means that the content is only loaded once. reloading of the content does not occur. * @property {string} filter=undefined dom selectors to search for elements, if undefined then everything is taken - * @property {Monster.Components.Form.Processor[]} processors + * @property {Object[]} processors * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) * @property {String} fetch.redirect=error * @property {String} fetch.method=GET diff --git a/source/components/form/template.mjs b/source/components/form/template.mjs index 860067c36b783789b2210e75e896052631afa3ed..0b1cf5b9c07f0014d78e636945821636c22a0374 100644 --- a/source/components/form/template.mjs +++ b/source/components/form/template.mjs @@ -40,9 +40,12 @@ const intersectionObserverWasInitialized = Symbol("wasInitialized"); /** * A Template control is a control that can be used to load content from a URL and display it in the ShadowRoot. * - * @fragments /fragments/components/form/template/ + * @fragments /fragments/components/form/template * * @example /examples/components/form/template-simple + * @example /examples/components/form/template-with-default + * @example /examples/components/form/template-with-processor + * @example /examples/components/form/template-onshow * * @since 1.11.0 * @copyright schukai GmbH @@ -69,7 +72,7 @@ class Template extends CustomElement { * @property {string} templates.main Main template * @property {string} url=undefined * @property {string} reload=undefined currently the only value defined is `onshow`. Currently the only value defined is onshow. this removes the IntersectionObserver. this means that the content is only loaded once. reloading of the content does not occur. - * @property {Monster.Components.Form.Processor[]} processors + * @property {Object[]} processors * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) * @property {String} fetch.redirect=error * @property {String} fetch.method=GET @@ -85,8 +88,8 @@ class Template extends CustomElement { templates: { main: getTemplate(), }, - url: undefined, - reload: undefined, + url: null, + reload: null, processors: [], fetch: { redirect: "error", @@ -123,8 +126,6 @@ class Template extends CustomElement { this[attributeObserverSymbol][ATTRIBUTE_FORM_URL] = (url) => { if (this.hasAttribute(ATTRIBUTE_FORM_URL)) { this.setOption("url", new URL(url, document.location).toString()); - } else { - this.setOption("url", undefined); } }; } @@ -139,7 +140,7 @@ class Template extends CustomElement { * @throws {Error} not found * @throws {Error} undefined status or type * @fires monster-fetched - * @return {Monster.Components.Form.Form} + * @return {void} */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); @@ -181,13 +182,6 @@ class Template extends CustomElement { } } -/** - * @typedef {Object} Processor - * @property {String} destination - * @property {String} source - * @since 1.11.8 - */ - /** * This attribute can be used to pass a URL to this select. * @@ -237,6 +231,7 @@ function initIntersectionObserver() { }; const callback = (entries, observer) => { + for (const [, entry] of entries.entries()) { if (entry.isIntersecting === true) { if (this.getOption("reload") === "onshow") { @@ -276,7 +271,12 @@ function loadContent() { throw new Error("no shadow-root is defined"); } - const url = this.getOption("url", undefined); + let url = this.getOption("url", undefined); + + if (url instanceof URL) { + url = url.toString(); + } + if (!isString(url) || url === "") { throw new Error("missing url"); } @@ -303,6 +303,7 @@ function loadContent() { loadAndAssignContent(container, url, options) .then(() => { defaultSlot.style.display = "none"; + container.style.display = "block"; runProcessors.call(this); }) .catch((e) => { @@ -316,15 +317,33 @@ function loadContent() { */ function runProcessors() { const processors = this.getOption("processors"); - if (!isArray(processors)) return; + if (!isArray(processors)) return this; for (const [, processor] of processors.entries()) { const source = processor?.source; - const destination = processor?.destination; + let destination = processor?.destination; + + if (source === null) { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "missing source"); + continue; + } + + if (destination === null || destination === undefined || destination === "") { + destination = "["+ATTRIBUTE_ROLE+"=container]"; + } + if (isString(source) && isString(destination)) { const sourceNode = this.shadowRoot.querySelector(source); - const destinationNode = document.querySelector(destination); + let destinationNode = document.querySelector(destination); + + if (destinationNode===null) { + destinationNode = this.shadowRoot.querySelector(destination); + if (destinationNode===null) { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "destination not found"); + continue; + } + } if ( sourceNode instanceof HTMLElement && @@ -332,6 +351,8 @@ function runProcessors() { ) { destinationNode.innerHTML = sourceNode.cloneNode(true).innerHTML; } + } else { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "invalid source or destination"); } } diff --git a/source/components/form/tree-select.mjs b/source/components/form/tree-select.mjs index d2a6db5e4a89639c5ff8735e7926de8a6d66e16e..c616459c6c61020b23b6e84df4968aeb6e8da924 100644 --- a/source/components/form/tree-select.mjs +++ b/source/components/form/tree-select.mjs @@ -12,33 +12,34 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import { buildTree } from "../../data/buildtree.mjs"; -import { findClosestByAttribute } from "../../dom/attributes.mjs"; +import {buildTree} from "../../data/buildtree.mjs"; +import {addAttributeToken, findClosestByAttribute} from "../../dom/attributes.mjs"; import { - ATTRIBUTE_ROLE, - ATTRIBUTE_UPDATER_INSERT_REFERENCE, + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_ROLE, + ATTRIBUTE_UPDATER_INSERT_REFERENCE, } from "../../dom/constants.mjs"; -import { instanceSymbol } from "../../constants.mjs"; +import {instanceSymbol} from "../../constants.mjs"; import { - assembleMethodSymbol, - registerCustomElement, + assembleMethodSymbol, + registerCustomElement, } from "../../dom/customelement.mjs"; import { - findTargetElementFromEvent, - fireCustomEvent, - fireEvent, + findTargetElementFromEvent, + fireCustomEvent, + fireEvent, } from "../../dom/events.mjs"; -import { Formatter } from "../../text/formatter.mjs"; -import { isString } from "../../types/is.mjs"; -import { Node } from "../../types/node.mjs"; -import { NodeRecursiveIterator } from "../../types/noderecursiveiterator.mjs"; -import { validateInstance } from "../../types/validate.mjs"; -import { ATTRIBUTE_FORM_URL, ATTRIBUTE_INTEND } from "./constants.mjs"; -import { Select } from "./select.mjs"; -import { SelectStyleSheet } from "./stylesheet/select.mjs"; -import { TreeSelectStyleSheet } from "./stylesheet/tree-select.mjs"; - -export { TreeSelect, formatHierarchicalSelection }; +import {Formatter} from "../../text/formatter.mjs"; +import {isString} from "../../types/is.mjs"; +import {Node} from "../../types/node.mjs"; +import {NodeRecursiveIterator} from "../../types/noderecursiveiterator.mjs"; +import {validateInstance} from "../../types/validate.mjs"; +import {ATTRIBUTE_FORM_URL, ATTRIBUTE_INTEND} from "./constants.mjs"; +import {Select} from "./select.mjs"; +import {SelectStyleSheet} from "./stylesheet/select.mjs"; +import {TreeSelectStyleSheet} from "./stylesheet/tree-select.mjs"; + +export {TreeSelect, formatHierarchicalSelection}; /** * @private @@ -57,7 +58,7 @@ const keyEventHandler = Symbol("keyEventHandler"); * * @fragments /fragments/components/form/tree-select * - * @example /examples/components/form/tree-select + * @example /examples/components/form/tree-select-simple * * @since 1.9.0 * @copyright schukai GmbH @@ -68,136 +69,148 @@ const keyEventHandler = Symbol("keyEventHandler"); * @fires monster-changed */ class TreeSelect extends Select { - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - * @since 2.1.0 - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/form/tree-select@@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. - * - * @extends Select - * @property {String} mapping.rootReferences=['0', undefined, null] - * @property {String} mapping.idTemplate=id - * @property {String} mapping.parentTemplate=parent - * @property {String} mapping.selection - * @property {Object} formatter - * @property {String} formatter.separator=" / " - */ - get defaults() { - return Object.assign( - {}, - super.defaults, - { - mapping: { - rootReferences: ["0", undefined, null], - idTemplate: "id", - parentTemplate: "parent", - }, - formatter: { - selection: formatHierarchicalSelection, - separator: " / ", - }, - templates: { - main: getTemplate(), - }, - }, - initOptionsFromArguments.call(this), - ); - } - - /** - * - * @return {string} - */ - static getTag() { - return "monster-tree-select"; - } - - /** - * - * @return {CSSStyleSheet[]} - */ - static getCSSStyleSheet() { - return [SelectStyleSheet, TreeSelectStyleSheet]; - } - - /** - * Import Select Options from dataset - * Not to be confused with the control defaults/options - * - * @param {array|object|Map|Set} data - * @return {Select} - * @throws {Error} map is not iterable - */ - importOptions(data) { - this[internalNodesSymbol] = new Map(); - - const mappingOptions = this.getOption("mapping", {}); - - const filter = mappingOptions?.["filter"]; - const rootReferences = mappingOptions?.["rootReferences"]; - - const id = this.getOption("mapping.idTemplate", "id"); - const parentID = this.getOption("mapping.parentTemplate", "parent"); - - const selector = mappingOptions?.["selector"]; - - const nodes = buildTree(data, selector, id, parentID, { - filter, - rootReferences, - }); - - const options = []; - for (const node of nodes) { - const iterator = new NodeRecursiveIterator(node); - for (const n of iterator) { - const formattedValues = formatKeyLabel.call(this, n); - - const label = formattedValues.label; - const value = formattedValues.value; - const intend = n.level; - - const visibility = intend > 0 ? "hidden" : "visible"; - const state = "close"; - - this[internalNodesSymbol].set(value, n); - - options.push({ - value, - label, - intend, - state, - visibility, - ["has-children"]: n.hasChildNodes(), - }); - } - } - - this.setOption("options", options); - - fireCustomEvent(this, "monster-options-set", { - options, - }); - - return this; - } - - /** - * - * @return {Monster.Components.Form.Select} - */ - [assembleMethodSymbol]() { - super[assembleMethodSymbol](); - initEventHandler.call(this); - } + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + * @since 2.1.0 + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/form/tree-select@@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. + * + * @extends Select + * @property {String} mapping.rootReferences=['0', undefined, null] + * @property {String} mapping.idTemplate=id + * @property {String} mapping.parentTemplate=parent + * @property {String} mapping.selection + * @property {Object} formatter + * @property {String} formatter.separator=" / " + */ + get defaults() { + return Object.assign( + {}, + super.defaults, + { + mapping: { + rootReferences: ["0", undefined, null], + id: "id", + parent: "parent", + + selector: "*", + labelTemplate: "", + valueTemplate: "", + }, + formatter: { + selection: formatHierarchicalSelection, + separator: " / ", + }, + templates: { + main: getTemplate(), + }, + }, + initOptionsFromArguments.call(this), + ); + } + + /** + * + * @return {string} + */ + static getTag() { + return "monster-tree-select"; + } + + /** + * + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [SelectStyleSheet, TreeSelectStyleSheet]; + } + + /** + * Import Select Options from dataset + * Not to be confused with the control defaults/options + * + * @param {array|object|Map|Set} data + * @return {Select} + * @throws {Error} map is not iterable + */ + importOptions(data) { + this[internalNodesSymbol] = new Map(); + + const mappingOptions = this.getOption("mapping", {}); + + const filter = mappingOptions?.["filter"]; + const rootReferences = mappingOptions?.["rootReferences"]; + + const id = this.getOption("mapping.id", "id"); + const parentID = this.getOption("mapping.parent", "parent"); + + const selector = mappingOptions?.["selector"]; + const options = []; + + try { + const nodes = buildTree(data, selector, id, parentID, { + filter, + rootReferences, + }); + + for (const node of nodes) { + const iterator = new NodeRecursiveIterator(node); + for (const n of iterator) { + + const formattedValues = formatKeyLabel.call(this, n); + + const label = formattedValues.label; + const value = formattedValues.value; + const intend = n.level; + + const visibility = intend > 0 ? "hidden" : "visible"; + const state = "close"; + + this[internalNodesSymbol].set(value, n); + + options.push({ + value, + label, + intend, + state, + visibility, + ["has-children"]: n.hasChildNodes(), + }); + } + } + + this.setOption("options", options); + + fireCustomEvent(this, "monster-options-set", { + options, + }); + + + } catch (e) { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e?.message || e); + } + + + return this; + } + + /** + * + * @return {TreeSelect} + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + initEventHandler.call(this); + } } /** @@ -205,16 +218,16 @@ class TreeSelect extends Select { * @param event */ function handleOptionKeyboardEvents(event) { - switch (event?.["code"]) { - case "ArrowLeft": - closeOrOpenCurrentOption.call(this, event, "close"); - event.preventDefault(); - break; - case "ArrowRight": - closeOrOpenCurrentOption.call(this, event, "open"); - event.preventDefault(); - break; - } + switch (event?.["code"]) { + case "ArrowLeft": + closeOrOpenCurrentOption.call(this, event, "close"); + event.preventDefault(); + break; + case "ArrowRight": + closeOrOpenCurrentOption.call(this, event, "open"); + event.preventDefault(); + break; + } } /** @@ -222,24 +235,24 @@ function handleOptionKeyboardEvents(event) { * @param {event} event */ function closeOrOpenCurrentOption(event, mode) { - validateInstance(event, Event); - - if (typeof event.composedPath !== "function") { - throw new Error("unsupported event"); - } - - const path = event.composedPath(); - const optionNode = path.shift(); - - const state = optionNode.getAttribute("data-monster-state"); - if (state !== mode) { - const handler = optionNode.querySelector( - "[data-monster-role=folder-handler]", - ); - if (handler instanceof HTMLElement) { - fireEvent(handler, "click"); - } - } + validateInstance(event, Event); + + if (typeof event.composedPath !== "function") { + throw new Error("unsupported event"); + } + + const path = event.composedPath(); + const optionNode = path.shift(); + + const state = optionNode.getAttribute("data-monster-state"); + if (state !== mode) { + const handler = optionNode.querySelector( + "[data-monster-role=folder-handler]", + ); + if (handler instanceof HTMLElement) { + fireEvent(handler, "click"); + } + } } /** @@ -249,19 +262,25 @@ function closeOrOpenCurrentOption(event, mode) { * @private */ function formatKeyLabel(node) { - validateInstance(node, Node); - - const label = new Formatter(node.value).format( - this.getOption("mapping.labelTemplate", ""), - ); - const value = new Formatter(node.value).format( - this.getOption("mapping.valueTemplate", ""), - ); - - return { - value, - label, - }; + validateInstance(node, Node); + + const v = node.value; + if (v === undefined) { + throw new Error("the object has no value for the specified id"); + } + + const label = new Formatter(v).format( + this.getOption("mapping.labelTemplate", ""), + ); + + const value = new Formatter(v).format( + this.getOption("mapping.valueTemplate", ""), + ); + + return { + value, + label, + }; } /** @@ -270,23 +289,23 @@ function formatKeyLabel(node) { * @return {Array} */ function buildTreeLabels(value) { - let node = this[internalNodesSymbol].get(value); - if (node === undefined) { - node = this[internalNodesSymbol].get(parseInt(value)); - } - - const parts = []; - - if (node instanceof Node) { - let ptr = node; - while (ptr) { - const formattedValues = formatKeyLabel.call(this, ptr); - parts.unshift(formattedValues.label); - ptr = ptr.parent; - } - } - - return parts; + let node = this[internalNodesSymbol].get(value); + if (node === undefined) { + node = this[internalNodesSymbol].get(parseInt(value)); + } + + const parts = []; + + if (node instanceof Node) { + let ptr = node; + while (ptr) { + const formattedValues = formatKeyLabel.call(this, ptr); + parts.unshift(formattedValues.label); + ptr = ptr.parent; + } + } + + return parts; } /** @@ -304,9 +323,9 @@ function buildTreeLabels(value) { * @return {string} */ function formatHierarchicalSelection(value) { - return buildTreeLabels - .call(this, value) - .join(this.getOption("formatter.separator", " / ")); + return buildTreeLabels + .call(this, value) + .join(this.getOption("formatter.separator", " / ")); } /** @@ -320,99 +339,99 @@ const openOptionEventHandler = Symbol("openOptionEventHandler"); * @throws {Error} no shadow-root is defined */ function initEventHandler() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - this[openOptionEventHandler] = (event) => { - const element = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "folder-handler", - ); - if (!(element instanceof HTMLElement)) { - return; - } - - const container = findClosestByAttribute(element, ATTRIBUTE_ROLE, "option"); - const index = container - .getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE) - .split("-") - .pop(); - - const currentState = this.getOption(`options.${index}.state`); - - const newState = currentState === "close" ? "open" : "close"; - this.setOption(`options.${index}.state`, newState); - - const newVisibility = newState === "open" ? "visible" : "hidden"; - - if (container.hasAttribute(ATTRIBUTE_INTEND)) { - const intend = container.getAttribute(ATTRIBUTE_INTEND); - - let ref = container.nextElementSibling; - const childIntend = parseInt(intend) + 1; - - const cmp = (a, b) => { - if (newState === "open") { - return a === b; - } - - return a >= b; - }; - - while ( - ref?.hasAttribute(ATTRIBUTE_INTEND) && - cmp(parseInt(ref.getAttribute(ATTRIBUTE_INTEND)), childIntend) - ) { - const refIndex = ref - .getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE) - .split("-") - .pop(); - this.setOption(`options.${refIndex}.visibility`, newVisibility); - - if (newState === "close") { - this.setOption(`options.${refIndex}.state`, "close"); - } - - ref = ref.nextElementSibling; - } - } - }; - - this[keyEventHandler] = (event) => { - const path = event.composedPath(); - const element = path?.[0]; - - let role; - - if (element instanceof HTMLElement) { - if (element.hasAttribute(ATTRIBUTE_ROLE)) { - role = element.getAttribute(ATTRIBUTE_ROLE); - } else if (element === this) { - show.call(this); - focusFilter.call(this); - } else { - const e = element.closest(`[${ATTRIBUTE_ROLE}]`); - if (e instanceof HTMLElement && e.hasAttribute()) { - role = e.getAttribute(ATTRIBUTE_ROLE); - } - } - } else { - return; - } - - switch (role) { - case "option-label": - case "option-control": - case "option": - handleOptionKeyboardEvents.call(this, event); - break; - } - }; - - this.shadowRoot.addEventListener("keydown", this[keyEventHandler]); - this.shadowRoot.addEventListener("click", this[openOptionEventHandler]); + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + this[openOptionEventHandler] = (event) => { + const element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "folder-handler", + ); + if (!(element instanceof HTMLElement)) { + return; + } + + const container = findClosestByAttribute(element, ATTRIBUTE_ROLE, "option"); + const index = container + .getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE) + .split("-") + .pop(); + + const currentState = this.getOption(`options.${index}.state`); + + const newState = currentState === "close" ? "open" : "close"; + this.setOption(`options.${index}.state`, newState); + + const newVisibility = newState === "open" ? "visible" : "hidden"; + + if (container.hasAttribute(ATTRIBUTE_INTEND)) { + const intend = container.getAttribute(ATTRIBUTE_INTEND); + + let ref = container.nextElementSibling; + const childIntend = parseInt(intend) + 1; + + const cmp = (a, b) => { + if (newState === "open") { + return a === b; + } + + return a >= b; + }; + + while ( + ref?.hasAttribute(ATTRIBUTE_INTEND) && + cmp(parseInt(ref.getAttribute(ATTRIBUTE_INTEND)), childIntend) + ) { + const refIndex = ref + .getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE) + .split("-") + .pop(); + this.setOption(`options.${refIndex}.visibility`, newVisibility); + + if (newState === "close") { + this.setOption(`options.${refIndex}.state`, "close"); + } + + ref = ref.nextElementSibling; + } + } + }; + + this[keyEventHandler] = (event) => { + const path = event.composedPath(); + const element = path?.[0]; + + let role; + + if (element instanceof HTMLElement) { + if (element.hasAttribute(ATTRIBUTE_ROLE)) { + role = element.getAttribute(ATTRIBUTE_ROLE); + } else if (element === this) { + show.call(this); + focusFilter.call(this); + } else { + const e = element.closest(`[${ATTRIBUTE_ROLE}]`); + if (e instanceof HTMLElement && e.hasAttribute()) { + role = e.getAttribute(ATTRIBUTE_ROLE); + } + } + } else { + return; + } + + switch (role) { + case "option-label": + case "option-control": + case "option": + handleOptionKeyboardEvents.call(this, event); + break; + } + }; + + this.shadowRoot.addEventListener("keydown", this[keyEventHandler]); + this.shadowRoot.addEventListener("click", this[openOptionEventHandler]); } /** @@ -426,14 +445,14 @@ function initEventHandler() { * @return {object} */ function initOptionsFromArguments() { - const options = {}; + const options = {}; - const url = this.getAttribute(ATTRIBUTE_FORM_URL); - if (isString(url)) { - options["url"] = new URL(url).toString(); - } + const url = this.getAttribute(ATTRIBUTE_FORM_URL); + if (isString(url)) { + options["url"] = new URL(url).toString(); + } - return options; + return options; } /** @@ -441,8 +460,8 @@ function initOptionsFromArguments() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <template id="options"> <div data-monster-role="option" tabindex="-1" diff --git a/source/components/layout/tabs.mjs b/source/components/layout/tabs.mjs index 99b441a576f8dda8b7870340e3ae85e6e2ec2804..ee02a7f215e77d8bab1f860a68718f02ee965402 100644 --- a/source/components/layout/tabs.mjs +++ b/source/components/layout/tabs.mjs @@ -12,55 +12,55 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import { instanceSymbol } from "../../constants.mjs"; -import { createPopper } from "@popperjs/core"; -import { extend } from "../../data/extend.mjs"; -import { Pathfinder } from "../../data/pathfinder.mjs"; +import {instanceSymbol} from "../../constants.mjs"; +import {createPopper} from "@popperjs/core"; +import {extend} from "../../data/extend.mjs"; +import {Pathfinder} from "../../data/pathfinder.mjs"; import { - addAttributeToken, - addToObjectLink, - hasObjectLink, + addAttributeToken, + addToObjectLink, + hasObjectLink, } from "../../dom/attributes.mjs"; import { - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_PREFIX, - ATTRIBUTE_ROLE, + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_PREFIX, + ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; import { - assembleMethodSymbol, - CustomElement, - getSlottedElements, - registerCustomElement, + assembleMethodSymbol, + CustomElement, + getSlottedElements, + registerCustomElement, } from "../../dom/customelement.mjs"; import { - findTargetElementFromEvent, - fireCustomEvent, + findTargetElementFromEvent, + fireCustomEvent, } from "../../dom/events.mjs"; -import { getDocument } from "../../dom/util.mjs"; -import { random } from "../../math/random.mjs"; -import { getGlobal } from "../../types/global.mjs"; -import { ID } from "../../types/id.mjs"; -import { isArray, isString } from "../../types/is.mjs"; -import { TokenList } from "../../types/tokenlist.mjs"; -import { clone } from "../../util/clone.mjs"; -import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; -import { Processing } from "../../util/processing.mjs"; +import {getDocument} from "../../dom/util.mjs"; +import {random} from "../../math/random.mjs"; +import {getGlobal} from "../../types/global.mjs"; +import {ID} from "../../types/id.mjs"; +import {isArray, isString} from "../../types/is.mjs"; +import {TokenList} from "../../types/tokenlist.mjs"; +import {clone} from "../../util/clone.mjs"; +import {DeadMansSwitch} from "../../util/deadmansswitch.mjs"; +import {Processing} from "../../util/processing.mjs"; import { - ATTRIBUTE_BUTTON_LABEL, - ATTRIBUTE_FORM_RELOAD, - ATTRIBUTE_FORM_URL, - STYLE_DISPLAY_MODE_BLOCK, + ATTRIBUTE_BUTTON_LABEL, + ATTRIBUTE_FORM_RELOAD, + ATTRIBUTE_FORM_URL, + STYLE_DISPLAY_MODE_BLOCK, } from "../form/constants.mjs"; -import { TabsStyleSheet } from "./stylesheet/tabs.mjs"; -import { loadAndAssignContent } from "../form/util/fetch.mjs"; -import { ThemeStyleSheet } from "../stylesheet/theme.mjs"; +import {TabsStyleSheet} from "./stylesheet/tabs.mjs"; +import {loadAndAssignContent} from "../form/util/fetch.mjs"; +import {ThemeStyleSheet} from "../stylesheet/theme.mjs"; import { - popperInstanceSymbol, - setEventListenersModifiers, + popperInstanceSymbol, + setEventListenersModifiers, } from "../form/util/popper.mjs"; -export { Tabs }; +export {Tabs}; /** * @private @@ -146,408 +146,414 @@ const resizeObserverSymbol = Symbol("resizeObserver"); * @fragments /fragments/components/layout/tabs/ * * @example /examples/components/layout/tabs-simple + * @example /examples/components/layout/tabs-active + * @example /examples/components/layout/tabs-removable + * + * @issue https://localhost.alvine.dev:8443/development/issues/closed/268.html * * @since 3.74.0 * @copyright schukai GmbH * @summary This CustomControl creates a tab element with a variety of options. */ class Tabs extends CustomElement { - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/layout/tabs"); - } - - /** - * 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 - * @property {string} labels.new-tab-label="New Tab" - * @property {Object} features - * @property {number} features.openDelay=500 Open delay in milliseconds - * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) - * @property {String} fetch.redirect=error - * @property {String} fetch.method=GET - * @property {String} fetch.mode=same-origin - * @property {String} fetch.credentials=same-origin - * @property {Object} fetch.headers={"accept":"text/html"}} - * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/) - * @property {string} popper.placement=bottom PopperJS placement - * @property {Object[]} modifiers={name:offset} PopperJS placement - */ - get defaults() { - return Object.assign({}, super.defaults, { - templates: { - main: getTemplate(), - }, - labels: { - "new-tab-label": "New Tab", - }, - buttons: { - standard: [], - popper: [], - }, - fetch: { - redirect: "error", - method: "GET", - mode: "same-origin", - credentials: "same-origin", - headers: { - accept: "text/html", - }, - }, - - features: { - openDelay: null, - }, - - classes: { - button: "monster-theme-primary-1", - popper: "monster-theme-primary-1", - navigation: "monster-theme-primary-1", - }, - - popper: { - placement: "bottom", - modifiers: [ - { - name: "offset", - options: { - offset: [0, 2], - }, - }, - - { - name: "eventListeners", - enabled: false, - }, - ], - }, - }); - } - - /** - * This method is called internal and should not be called directly. - */ - [assembleMethodSymbol]() { - super[assembleMethodSymbol](); - - initControlReferences.call(this); - - this[dimensionsSymbol] = new Pathfinder({ data: {} }); - - initEventHandler.call(this); - - // setup structure - initTabButtons.call(this).then(() => { - initPopperSwitch.call(this); - initPopper.call(this); - attachResizeObserver.call(this); - attachTabChangeObserver.call(this); - }); - } - - /** - * This method is called internal and should not be called directly. - * - * @return {CSSStyleSheet[]} - */ - static getCSSStyleSheet() { - return [TabsStyleSheet]; - } - - /** - * This method is called internal and should not be called directly. - * - * @return {string} - */ - static getTag() { - return "monster-tabs"; - } - - /** - * A function that activates a tab based on the provided name. - * - * The tabs have to be named with the `data-monster-name` attribute. - * - * @param {type} idOrName - the name or id of the tab to activate - * @return {Tabs} - The current instance - */ - activeTab(idOrName) { - let found = false; - - getSlottedElements.call(this).forEach((node) => { - if (found === true) { - return; - } - - if (node.getAttribute("data-monster-name") === idOrName) { - this.shadowRoot - .querySelector( - `[data-monster-tab-reference="${node.getAttribute("id")}"]`, - ) - .click(); - found = true; - } - - if (node.getAttribute("id") === idOrName) { - this.shadowRoot - .querySelector( - `[data-monster-tab-reference="${node.getAttribute("id")}"]`, - ) - .click(); - found = true; - } - }); - - return this; - } - - /** - * A function that returns the name or id of the currently active tab. - * - * The tabs have to be named with the `data-monster-name` attribute. - * - * @return {string|null} - */ - getActiveTab() { - const nodes = getSlottedElements.call(this); - for (const node of nodes) { - if (node.matches(".active") === true) { - if (node.hasAttribute("data-monster-name")) { - return node.getAttribute("data-monster-name"); - } - - return node.getAttribute("id"); - } - } - return null; - } - - /** - * This method is called by the dom and should not be called directly. - * - * @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]); - } - } - - /** - * This method is called by the dom and should not be called directly. - * - * @return {void} - */ - disconnectedCallback() { - super.disconnectedCallback(); - - const document = getDocument(); - - // close on outside ui-events - for (const [, type] of Object.entries(["click", "touch"])) { - document.removeEventListener(type, this[closeEventHandler]); - } - } + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/layout/tabs"); + } + + /** + * 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 + * @property {string} labels.new-tab-label="New Tab" + * @property {Object} features + * @property {number} features.openDelay=500 Open delay in milliseconds + * @property {string} features.removeBehavior="auto" Remove behavior, auto (default), next, previous and none + * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) + * @property {String} fetch.redirect=error + * @property {String} fetch.method=GET + * @property {String} fetch.mode=same-origin + * @property {String} fetch.credentials=same-origin + * @property {Object} fetch.headers={"accept":"text/html"}} + * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/) + * @property {string} popper.placement=bottom PopperJS placement + * @property {Object[]} modifiers={name:offset} PopperJS placement + */ + get defaults() { + return Object.assign({}, super.defaults, { + templates: { + main: getTemplate(), + }, + labels: { + "new-tab-label": "New Tab", + }, + buttons: { + standard: [], + popper: [], + }, + fetch: { + redirect: "error", + method: "GET", + mode: "same-origin", + credentials: "same-origin", + headers: { + accept: "text/html", + }, + }, + + features: { + openDelay: null, + removeBehavior: "auto", + }, + + classes: { + button: "monster-theme-primary-1", + popper: "monster-theme-primary-1", + navigation: "monster-theme-primary-1", + }, + + popper: { + placement: "bottom", + modifiers: [ + { + name: "offset", + options: { + offset: [0, 2], + }, + }, + + { + name: "eventListeners", + enabled: false, + }, + ], + }, + }); + } + + /** + * This method is called internal and should not be called directly. + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + + initControlReferences.call(this); + + this[dimensionsSymbol] = new Pathfinder({data: {}}); + + initEventHandler.call(this); + + // setup structure + initTabButtons.call(this).then(() => { + initPopperSwitch.call(this); + initPopper.call(this); + attachResizeObserver.call(this); + attachTabChangeObserver.call(this); + }); + } + + /** + * This method is called internal and should not be called directly. + * + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [TabsStyleSheet]; + } + + /** + * This method is called internal and should not be called directly. + * + * @return {string} + */ + static getTag() { + return "monster-tabs"; + } + + /** + * A function that activates a tab based on the provided name. + * + * The tabs have to be named with the `data-monster-name` attribute. + * + * @param {type} idOrName - the name or id of the tab to activate + * @return {Tabs} - The current instance + */ + activeTab(idOrName) { + let found = false; + + getSlottedElements.call(this).forEach((node) => { + if (found === true) { + return; + } + + if (node.getAttribute("data-monster-name") === idOrName) { + this.shadowRoot + .querySelector( + `[data-monster-tab-reference="${node.getAttribute("id")}"]`, + ) + .click(); + found = true; + } + + if (node.getAttribute("id") === idOrName) { + this.shadowRoot + .querySelector( + `[data-monster-tab-reference="${node.getAttribute("id")}"]`, + ) + .click(); + found = true; + } + }); + + return this; + } + + /** + * A function that returns the name or id of the currently active tab. + * + * The tabs have to be named with the `data-monster-name` attribute. + * + * @return {string|null} + */ + getActiveTab() { + const nodes = getSlottedElements.call(this); + for (const node of nodes) { + if (node.matches(".active") === true) { + if (node.hasAttribute("data-monster-name")) { + return node.getAttribute("data-monster-name"); + } + + return node.getAttribute("id"); + } + } + return null; + } + + /** + * This method is called by the dom and should not be called directly. + * + * @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]); + } + } + + /** + * This method is called by the dom and should not be called directly. + * + * @return {void} + */ + disconnectedCallback() { + super.disconnectedCallback(); + + const document = getDocument(); + + // close on outside ui-events + for (const [, type] of Object.entries(["click", "touch"])) { + document.removeEventListener(type, this[closeEventHandler]); + } + } } /** * @private */ function initPopperSwitch() { - const nodes = getSlottedElements.call(this, `[${ATTRIBUTE_ROLE}="switch"]`); // null ↦ only unnamed slots - let switchButton; - if (nodes.size === 0) { - switchButton = document.createElement("button"); - switchButton.setAttribute(ATTRIBUTE_ROLE, "switch"); - switchButton.setAttribute("part", "switch"); - switchButton.classList.add("hidden"); - const classList = this.getOption("classes.button"); - if (classList) { - switchButton.classList.add(classList); - } - switchButton.innerHTML = - '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>'; - this[navElementSymbol].prepend(switchButton); - } else { - switchButton = nodes.next(); - } - - /** - * @param {Event} event - */ - this[popperSwitchEventHandler] = (event) => { - const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "switch"); - - if (element instanceof HTMLButtonElement) { - togglePopper.call(this); - } - }; - - for (const type of ["click", "touch"]) { - switchButton.addEventListener(type, this[popperSwitchEventHandler]); - } - - this[switchElementSymbol] = switchButton; + const nodes = getSlottedElements.call(this, `[${ATTRIBUTE_ROLE}="switch"]`); // null ↦ only unnamed slots + let switchButton; + if (nodes.size === 0) { + switchButton = document.createElement("button"); + switchButton.setAttribute(ATTRIBUTE_ROLE, "switch"); + switchButton.setAttribute("part", "switch"); + switchButton.classList.add("hidden"); + const classList = this.getOption("classes.button"); + if (classList) { + switchButton.classList.add(classList); + } + switchButton.innerHTML = + '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>'; + this[navElementSymbol].prepend(switchButton); + } else { + switchButton = nodes.next(); + } + + /** + * @param {Event} event + */ + this[popperSwitchEventHandler] = (event) => { + const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "switch"); + + if (element instanceof HTMLButtonElement) { + togglePopper.call(this); + } + }; + + for (const type of ["click", "touch"]) { + switchButton.addEventListener(type, this[popperSwitchEventHandler]); + } + + this[switchElementSymbol] = switchButton; } /** * @private */ function hidePopper() { - if (!this[popperInstanceSymbol]) { - return; - } + if (!this[popperInstanceSymbol]) { + return; + } - this[popperElementSymbol].style.display = "none"; - // performance https://popper.js.org/docs/v2/tutorial/#performance - setEventListenersModifiers.call(this, false); + this[popperElementSymbol].style.display = "none"; + // performance https://popper.js.org/docs/v2/tutorial/#performance + setEventListenersModifiers.call(this, false); } /** * @private */ function showPopper() { - if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { - return; - } - - this[popperElementSymbol].style.visibility = "hidden"; - this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK; - // performance https://popper.js.org/docs/v2/tutorial/#performance - setEventListenersModifiers.call(this, true); - - this[popperInstanceSymbol].update(); - - new Processing(() => { - this[popperElementSymbol].style.removeProperty("visibility"); - }) - .run(undefined) - .then(() => {}) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); + if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { + return; + } + + this[popperElementSymbol].style.visibility = "hidden"; + this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK; + // performance https://popper.js.org/docs/v2/tutorial/#performance + setEventListenersModifiers.call(this, true); + + this[popperInstanceSymbol].update(); + + new Processing(() => { + this[popperElementSymbol].style.removeProperty("visibility"); + }) + .run(undefined) + .then(() => { + }) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); } /** * @private */ function togglePopper() { - if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { - hidePopper.call(this); - } else { - showPopper.call(this); - } + if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { + hidePopper.call(this); + } else { + showPopper.call(this); + } } /** * @private */ function attachResizeObserver() { - // against flickering - this[resizeObserverSymbol] = new ResizeObserver((entries) => { - if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { - try { - this[timerCallbackSymbol].touch(); - return; - } catch (e) { - delete this[timerCallbackSymbol]; - } - } - - this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { - this[dimensionsSymbol].setVia("data.calculated", false); - checkAndRearrangeButtons.call(this); - }); - }); - - this[resizeObserverSymbol].observe(this[navElementSymbol]); + // against flickering + this[resizeObserverSymbol] = new ResizeObserver((entries) => { + if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { + try { + this[timerCallbackSymbol].touch(); + return; + } catch (e) { + delete this[timerCallbackSymbol]; + } + } + + this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { + this[dimensionsSymbol].setVia("data.calculated", false); + checkAndRearrangeButtons.call(this); + }); + }); + + this[resizeObserverSymbol].observe(this[navElementSymbol]); } /** * @private */ function attachTabChangeObserver() { - // against flickering - new MutationObserver((mutations) => { - let runUpdate = false; - - for (const mutation of mutations) { - if (mutation.type === "childList") { - if ( - mutation.addedNodes.length > 0 || - mutation.removedNodes.length > 0 - ) { - runUpdate = true; - break; - } - } - } - - if (runUpdate === true) { - this[dimensionsSymbol].setVia("data.calculated", false); - initTabButtons.call(this); - } - }).observe(this, { - childList: true, - }); + // against flickering + new MutationObserver((mutations) => { + let runUpdate = false; + + for (const mutation of mutations) { + if (mutation.type === "childList") { + if ( + mutation.addedNodes.length > 0 || + mutation.removedNodes.length > 0 + ) { + runUpdate = true; + break; + } + } + } + + if (runUpdate === true) { + this[dimensionsSymbol].setVia("data.calculated", false); + initTabButtons.call(this); + } + }).observe(this, { + childList: true, + }); } /** * @private * @return {Select} * @external "external:createPopper" - * @see {@link Plugins} */ function initPopper() { - const self = this; - - const options = extend({}, self.getOption("popper")); - - self[popperInstanceSymbol] = createPopper( - self[switchElementSymbol], - self[popperElementSymbol], - options, - ); - - const observer1 = new MutationObserver(function (mutations) { - let runUpdate = false; - for (const mutation of mutations) { - if (mutation.type === "childList") { - if ( - mutation.addedNodes.length > 0 || - mutation.removedNodes.length > 0 - ) { - runUpdate = true; - break; - } - } - } - - if (runUpdate === true) { - self[popperInstanceSymbol].update(); - } - }); - - observer1.observe(self[popperNavElementSymbol], { - childList: true, - subtree: true, - }); - - return self; + const self = this; + + const options = extend({}, self.getOption("popper")); + + self[popperInstanceSymbol] = createPopper( + self[switchElementSymbol], + self[popperElementSymbol], + options, + ); + + const observer1 = new MutationObserver(function (mutations) { + let runUpdate = false; + for (const mutation of mutations) { + if (mutation.type === "childList") { + if ( + mutation.addedNodes.length > 0 || + mutation.removedNodes.length > 0 + ) { + runUpdate = true; + break; + } + } + } + + if (runUpdate === true) { + self[popperInstanceSymbol].update(); + } + }); + + observer1.observe(self[popperNavElementSymbol], { + childList: true, + subtree: true, + }); + + return self; } /** @@ -555,172 +561,203 @@ function initPopper() { * @param {HTMLElement} element */ function show(element) { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - const reference = element.getAttribute(`${ATTRIBUTE_PREFIX}tab-reference`); - - const nodes = getSlottedElements.call(this); - for (const node of nodes) { - const id = node.getAttribute("id"); - - if (id === reference) { - node.classList.add("active"); - - const openDelay = parseInt(this.getOption("features.openDelay"), 10); - - if (!isNaN(openDelay) && openDelay > 0) { - node.style.visibility = "hidden"; - - setTimeout(() => { - node.style.visibility = "visible"; - }, openDelay); - } - - // get all data- from button and filter out data-monster-attributes and data-monster-insert - const data = {}; - const mask = [ - "data-monster-attributes", - "data-monster-insert-reference", - "data-monster-state", - "data-monster-button-label", - "data-monster-objectlink", - "data-monster-role", - ]; - - for (const [, attr] of Object.entries(node.attributes)) { - if (attr.name.startsWith("data-") && mask.indexOf(attr.name) === -1) { - data[attr.name] = attr.value; - } - } - - if (node.hasAttribute(ATTRIBUTE_FORM_URL)) { - const url = node.getAttribute(ATTRIBUTE_FORM_URL); - - if ( - !node.hasAttribute(ATTRIBUTE_FORM_RELOAD) || - node.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase() === "onshow" - ) { - node.removeAttribute(ATTRIBUTE_FORM_URL); - } - - const options = this.getOption("fetch", {}); - const filter = undefined; - loadAndAssignContent(node, url, options, filter) - .then(() => { - fireCustomEvent(this, "monster-tab-changed", { - reference, - }); - }) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - } else { - fireCustomEvent(this, "monster-tab-changed", { - reference, - data, - }); - } - } else { - node.classList.remove("active"); - } - } - - const standardButtons = this.getOption("buttons.standard"); - for (const index in standardButtons) { - const button = standardButtons[index]; - const state = button["reference"] === reference ? "active" : "inactive"; - this.setOption(`buttons.standard.${index}.state`, state); - } - - const popperButton = this.getOption("buttons.popper"); - for (const index in popperButton) { - const button = popperButton[index]; - const state = button["reference"] === reference ? "active" : "inactive"; - this.setOption(`buttons.popper.${index}.state`, state); - } - - hidePopper.call(this); + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + const reference = element.getAttribute(`${ATTRIBUTE_PREFIX}tab-reference`); + + const nodes = getSlottedElements.call(this); + for (const node of nodes) { + const id = node.getAttribute("id"); + + if (id === reference) { + node.classList.add("active"); + + const openDelay = parseInt(this.getOption("features.openDelay"), 10); + + if (!isNaN(openDelay) && openDelay > 0) { + node.style.visibility = "hidden"; + + setTimeout(() => { + node.style.visibility = "visible"; + }, openDelay); + } + + // get all data- from button and filter out data-monster-attributes and data-monster-insert + const data = {}; + const mask = [ + "data-monster-attributes", + "data-monster-insert-reference", + "data-monster-state", + "data-monster-button-label", + "data-monster-objectlink", + "data-monster-role", + ]; + + for (const [, attr] of Object.entries(node.attributes)) { + if (attr.name.startsWith("data-") && mask.indexOf(attr.name) === -1) { + data[attr.name] = attr.value; + } + } + + if (node.hasAttribute(ATTRIBUTE_FORM_URL)) { + const url = node.getAttribute(ATTRIBUTE_FORM_URL); + + if ( + !node.hasAttribute(ATTRIBUTE_FORM_RELOAD) || + node.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase() === "onshow" + ) { + node.removeAttribute(ATTRIBUTE_FORM_URL); + } + + const options = this.getOption("fetch", {}); + const filter = undefined; + loadAndAssignContent(node, url, options, filter) + .then(() => { + fireCustomEvent(this, "monster-tab-changed", { + reference, + }); + }) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + } else { + fireCustomEvent(this, "monster-tab-changed", { + reference, + data, + }); + } + } else { + node.classList.remove("active"); + } + } + + const standardButtons = this.getOption("buttons.standard"); + for (const index in standardButtons) { + const button = standardButtons[index]; + const state = button["reference"] === reference ? "active" : "inactive"; + this.setOption(`buttons.standard.${index}.state`, state); + } + + const popperButton = this.getOption("buttons.popper"); + for (const index in popperButton) { + const button = popperButton[index]; + const state = button["reference"] === reference ? "active" : "inactive"; + this.setOption(`buttons.popper.${index}.state`, state); + } + + hidePopper.call(this); } /** * @private */ function initEventHandler() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - /** - * @param {Event} event - */ - this[changeTabEventHandler] = (event) => { - const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button"); - - if (element instanceof HTMLButtonElement && element.disabled !== true) { - show.call(this, element); - } - }; - - /** - * @param {Event} event - * @fires monster-tab-remove - */ - this[removeTabEventHandler] = (event) => { - const element = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "remove-tab", - ); - - if (element instanceof HTMLElement) { - const button = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "button", - ); - - if (button instanceof HTMLButtonElement && button.disabled !== true) { - const reference = button.getAttribute( - `${ATTRIBUTE_PREFIX}tab-reference`, - ); - if (reference) { - const container = this.querySelector(`[id=${reference}]`); - if (container instanceof HTMLElement) { - container.remove(); - initTabButtons.call(this); - fireCustomEvent(this, "monster-tab-remove", { - reference, - }); - } - } - } - } - }; - - this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]); - this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]); - - this[navElementSymbol].addEventListener("touch", this[removeTabEventHandler]); - this[navElementSymbol].addEventListener("click", this[removeTabEventHandler]); - - /** - * @param {Event} event - */ - this[closeEventHandler] = (event) => { - const path = event.composedPath(); - - for (const [, element] of Object.entries(path)) { - if (element === this) { - return; - } - } - - hidePopper.call(this); - }; - - return this; + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + /** + * @param {Event} event + */ + this[changeTabEventHandler] = (event) => { + const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button"); + + if (element instanceof HTMLButtonElement && element.disabled !== true) { + show.call(this, element); + } + }; + + /** + * @param {Event} event + * @fires monster-tab-remove + */ + this[removeTabEventHandler] = (event) => { + const element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "remove-tab", + ); + + if (element instanceof HTMLElement) { + const button = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "button", + ); + + if (button instanceof HTMLButtonElement && button.disabled !== true) { + const reference = button.getAttribute( + `${ATTRIBUTE_PREFIX}tab-reference`, + ); + + const previous = button.previousElementSibling; + const next = button.nextElementSibling; + + if (reference) { + const container = this.querySelector(`[id=${reference}]`); + if (container instanceof HTMLElement) { + container.remove(); + initTabButtons.call(this); + fireCustomEvent(this, "monster-tab-remove", { + reference, + }); + } + } + + switch (this.getOption("features.removeBehavior")) { + case "auto": + if (next instanceof HTMLButtonElement) { + next.click(); + } else { + // get previous button + if (previous instanceof HTMLButtonElement) { + previous.click(); + } + } + break; + case "next": + if (next instanceof HTMLButtonElement) { + next.click(); + } + break; + case "previous": + if (previous instanceof HTMLButtonElement) { + previous.click(); + } + break; + + default: // and "none" + break; + } + } + + } + }; + + this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]); + this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]); + + this[navElementSymbol].addEventListener("touch", this[removeTabEventHandler]); + this[navElementSymbol].addEventListener("click", this[removeTabEventHandler]); + + /** + * @param {Event} event + */ + this[closeEventHandler] = (event) => { + const path = event.composedPath(); + + for (const [, element] of Object.entries(path)) { + if (element === this) { + return; + } + } + + hidePopper.call(this); + }; + + return this; } /** @@ -728,37 +765,37 @@ function initEventHandler() { * @param observedNode */ function attachTabMutationObserver(observedNode) { - const self = this; - - if (hasObjectLink(observedNode, mutationObserverSymbol)) { - return; - } - - /** - * this construct monitors a node whether it is disabled or modified - * @type {MutationObserver} - */ - const observer = new MutationObserver(function (mutations) { - if (isArray(mutations)) { - const mutation = mutations.pop(); - if (mutation instanceof MutationRecord) { - initTabButtons.call(self); - } - } - }); - - observer.observe(observedNode, { - childList: false, - attributes: true, - subtree: false, - attributeFilter: [ - "disabled", - ATTRIBUTE_BUTTON_LABEL, - `${ATTRIBUTE_PREFIX}button-icon`, - ], - }); - - addToObjectLink(observedNode, mutationObserverSymbol, observer); + const self = this; + + if (hasObjectLink(observedNode, mutationObserverSymbol)) { + return; + } + + /** + * this construct monitors a node whether it is disabled or modified + * @type {MutationObserver} + */ + const observer = new MutationObserver(function (mutations) { + if (isArray(mutations)) { + const mutation = mutations.pop(); + if (mutation instanceof MutationRecord) { + initTabButtons.call(self); + } + } + }); + + observer.observe(observedNode, { + childList: false, + attributes: true, + subtree: false, + attributeFilter: [ + "disabled", + ATTRIBUTE_BUTTON_LABEL, + `${ATTRIBUTE_PREFIX}button-icon`, + ], + }); + + addToObjectLink(observedNode, mutationObserverSymbol, observer); } /** @@ -767,22 +804,22 @@ function attachTabMutationObserver(observedNode) { * @throws {Error} no shadow-root is defined */ function initControlReferences() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - this[controlElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=control]`, - ); - this[navElementSymbol] = this.shadowRoot.querySelector( - `nav[${ATTRIBUTE_ROLE}=nav]`, - ); - this[popperElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=popper]`, - ); - this[popperNavElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=popper-nav]`, - ); + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + this[controlElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=control]`, + ); + this[navElementSymbol] = this.shadowRoot.querySelector( + `nav[${ATTRIBUTE_ROLE}=nav]`, + ); + this[popperElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=popper]`, + ); + this[popperNavElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=popper-nav]`, + ); } /** @@ -792,101 +829,102 @@ function initControlReferences() { * */ function initTabButtons() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - let activeReference; - - const dimensionsCalculated = this[dimensionsSymbol].getVia( - "data.calculated", - false, - ); - - const buttons = []; - const nodes = getSlottedElements.call(this, undefined, null); // null ↦ only unnamed slots - - for (const node of nodes) { - if (!(node instanceof HTMLElement)) continue; - let label = getButtonLabel.call(this, node); - - let reference; - if (node.hasAttribute("id")) { - reference = node.getAttribute("id"); - } - - let disabled; - if (node.hasAttribute("disabled") || node.disabled === true) { - disabled = true; - } - - if (!reference) { - reference = new ID("tab").toString(); - node.setAttribute("id", reference); - } - - if (node.hasAttribute(`${ATTRIBUTE_PREFIX}button-icon`)) { - label = `<span part="label">${label}</span><img part="icon" src="${node.getAttribute( - `${ATTRIBUTE_PREFIX}button-icon`, - )}">`; - } - - let remove = false; - if (node.hasAttribute(`${ATTRIBUTE_PREFIX}removable`)) { - remove = true; - } - - if (node.matches(".active") === true && disabled !== true) { - node.classList.remove("active"); - activeReference = reference; - } - - const state = ""; - const classes = dimensionsCalculated ? "" : "invisible"; - - buttons.push({ - reference, - label, - state, - class: classes, - disabled, - remove, - }); - - attachTabMutationObserver.call(this, node); - } - - this.setOption("buttons.standard", clone(buttons)); - this.setOption("buttons.popper", []); - this.setOption("marker", random()); - - return adjustButtonVisibility.call(this).then(() => { - if (activeReference) { - return new Processing(() => { - const button = this.shadowRoot.querySelector( - `[${ATTRIBUTE_PREFIX}tab-reference="${activeReference}"]`, - ); - if (button instanceof HTMLButtonElement && button.disabled !== true) { - show.call(this, button); - } - }) - .run(undefined) - .then(() => {}) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - } - - return Promise.resolve(); - }); + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + let activeReference; + + const dimensionsCalculated = this[dimensionsSymbol].getVia( + "data.calculated", + false, + ); + + const buttons = []; + const nodes = getSlottedElements.call(this, undefined, null); // null ↦ only unnamed slots + + for (const node of nodes) { + if (!(node instanceof HTMLElement)) continue; + let label = getButtonLabel.call(this, node); + + let reference; + if (node.hasAttribute("id")) { + reference = node.getAttribute("id"); + } + + let disabled; + if (node.hasAttribute("disabled") || node.disabled === true) { + disabled = true; + } + + if (!reference) { + reference = new ID("tab").toString(); + node.setAttribute("id", reference); + } + + if (node.hasAttribute(`${ATTRIBUTE_PREFIX}button-icon`)) { + label = `<span part="label">${label}</span><img part="icon" alt="this is an icon" src="${node.getAttribute( + `${ATTRIBUTE_PREFIX}button-icon`, + )}">`; + } + + let remove = false; + if (node.hasAttribute(`${ATTRIBUTE_PREFIX}removable`)) { + remove = true; + } + + if (node.matches(".active") === true && disabled !== true) { + node.classList.remove("active"); + activeReference = reference; + } + + const state = ""; + const classes = dimensionsCalculated ? "" : "invisible"; + + buttons.push({ + reference, + label, + state, + class: classes, + disabled, + remove, + }); + + attachTabMutationObserver.call(this, node); + } + + this.setOption("buttons.standard", clone(buttons)); + this.setOption("buttons.popper", []); + this.setOption("marker", random()); + + return adjustButtonVisibility.call(this).then(() => { + if (activeReference) { + return new Processing(() => { + const button = this.shadowRoot.querySelector( + `[${ATTRIBUTE_PREFIX}tab-reference="${activeReference}"]`, + ); + if (button instanceof HTMLButtonElement && button.disabled !== true) { + show.call(this, button); + } + }) + .run(undefined) + .then(() => { + }) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + } + + return Promise.resolve(); + }); } function checkAndRearrangeButtons() { - if (this[dimensionsSymbol].getVia("data.calculated", false) !== true) { - calculateNavigationButtonsDimensions.call(this); - } + if (this[dimensionsSymbol].getVia("data.calculated", false) !== true) { + calculateNavigationButtonsDimensions.call(this); + } - rearrangeButtons.call(this); + rearrangeButtons.call(this); } /** @@ -894,29 +932,29 @@ function checkAndRearrangeButtons() { * @return {Promise<unknown>} */ function adjustButtonVisibility() { - const self = this; + const self = this; - return new Promise((resolve) => { - const observer = new MutationObserver(function (mutations) { - const defCount = self.getOption("buttons.standard").length; - const domCount = self[navElementSymbol].querySelectorAll( - 'button[data-monster-role="button"]', - ).length; + return new Promise((resolve) => { + const observer = new MutationObserver(function (mutations) { + const defCount = self.getOption("buttons.standard").length; + const domCount = self[navElementSymbol].querySelectorAll( + 'button[data-monster-role="button"]', + ).length; - // in drawing - if (defCount !== domCount) return; + // in drawing + if (defCount !== domCount) return; - observer.disconnect(); + observer.disconnect(); - checkAndRearrangeButtons.call(self); + checkAndRearrangeButtons.call(self); - resolve(); - }); + resolve(); + }); - observer.observe(self[navElementSymbol], { - attributes: true, - }); - }); + observer.observe(self[navElementSymbol], { + attributes: true, + }); + }); } /** @@ -925,17 +963,17 @@ function adjustButtonVisibility() { * @return {number} */ function getDimValue(value) { - if ([undefined, null].indexOf(value) !== -1) { - return 0; - } + if ([undefined, null].indexOf(value) !== -1) { + return 0; + } - const valueAsInt = parseInt(value, 10); + const valueAsInt = parseInt(value, 10); - if (isNaN(valueAsInt)) { - return 0; - } + if (isNaN(valueAsInt)) { + return 0; + } - return valueAsInt; + return valueAsInt; } /** @@ -944,18 +982,18 @@ function getDimValue(value) { * @return {number} */ function calcBoxWidth(node) { - const dim = getGlobal("window").getComputedStyle(node); - const bounding = node.getBoundingClientRect(); - - return ( - getDimValue(dim["border-left-width"]) + - getDimValue(dim["padding-left"]) + - getDimValue(dim["margin-left"]) + - getDimValue(bounding["width"]) + - getDimValue(dim["border-right-width"]) + - getDimValue(dim["margin-right"]) + - getDimValue(dim["padding-left"]) - ); + const dim = getGlobal("window").getComputedStyle(node); + const bounding = node.getBoundingClientRect(); + + return ( + getDimValue(dim["border-left-width"]) + + getDimValue(dim["padding-left"]) + + getDimValue(dim["margin-left"]) + + getDimValue(bounding["width"]) + + getDimValue(dim["border-right-width"]) + + getDimValue(dim["margin-right"]) + + getDimValue(dim["padding-left"]) + ); } /** @@ -963,35 +1001,35 @@ function calcBoxWidth(node) { * @return {Object} */ function rearrangeButtons() { - const standardButtons = []; - const popperButtons = []; - - let sum = 0; - const space = this[dimensionsSymbol].getVia("data.space"); - - const buttons = this.getOption("buttons.standard"); - for (const [, button] of buttons.entries()) { - const ref = button?.reference; - - sum += this[dimensionsSymbol].getVia(`data.button.${ref}`); - - if (sum > space) { - popperButtons.push(clone(button)); - } else { - standardButtons.push(clone(button)); - } - } - - this.setOption("buttons.standard", standardButtons); - this.setOption("buttons.popper", popperButtons); - - if (this[switchElementSymbol]) { - if (popperButtons.length > 0) { - this[switchElementSymbol].classList.remove("hidden"); - } else { - this[switchElementSymbol].classList.add("hidden"); - } - } + const standardButtons = []; + const popperButtons = []; + + let sum = 0; + const space = this[dimensionsSymbol].getVia("data.space"); + + const buttons = this.getOption("buttons.standard"); + for (const [, button] of buttons.entries()) { + const ref = button?.reference; + + sum += this[dimensionsSymbol].getVia(`data.button.${ref}`); + + if (sum > space) { + popperButtons.push(clone(button)); + } else { + standardButtons.push(clone(button)); + } + } + + this.setOption("buttons.standard", standardButtons); + this.setOption("buttons.popper", popperButtons); + + if (this[switchElementSymbol]) { + if (popperButtons.length > 0) { + this[switchElementSymbol].classList.remove("hidden"); + } else { + this[switchElementSymbol].classList.add("hidden"); + } + } } /** @@ -999,50 +1037,50 @@ function rearrangeButtons() { * @return {Object} */ function calculateNavigationButtonsDimensions() { - const width = this[navElementSymbol].getBoundingClientRect().width; - - let startEndWidth = 0; - - getSlottedElements.call(this, undefined, "start").forEach((node) => { - startEndWidth += calcBoxWidth.call(this, node); - }); - - getSlottedElements.call(this, undefined, "end").forEach((node) => { - startEndWidth += calcBoxWidth.call(this, node); - }); - - this[dimensionsSymbol].setVia("data.space", width - startEndWidth - 2); - this[dimensionsSymbol].setVia("data.visible", !(width === 0)); - - const buttons = this.getOption("buttons.standard").concat( - this.getOption("buttons.popper"), - ); - - for (const [i, button] of buttons.entries()) { - const ref = button?.reference; - const element = this[navElementSymbol].querySelector( - `:scope > [${ATTRIBUTE_PREFIX}tab-reference="${ref}"]`, - ); - if (!(element instanceof HTMLButtonElement)) continue; - - this[dimensionsSymbol].setVia( - `data.button.${ref}`, - calcBoxWidth.call(this, element), - ); - button["class"] = new TokenList(button["class"]) - .remove("invisible") - .toString(); - } - - const slots = this[controlElementSymbol].querySelectorAll( - `nav[${ATTRIBUTE_PREFIX}role=nav] > slot.invisible, slot[${ATTRIBUTE_PREFIX}role=slot].invisible`, - ); - for (const [, slot] of slots.entries()) { - slot.classList.remove("invisible"); - } - - this[dimensionsSymbol].setVia("data.calculated", true); - this.setOption("buttons.standard", clone(buttons)); + const width = this[navElementSymbol].getBoundingClientRect().width; + + let startEndWidth = 0; + + getSlottedElements.call(this, undefined, "start").forEach((node) => { + startEndWidth += calcBoxWidth.call(this, node); + }); + + getSlottedElements.call(this, undefined, "end").forEach((node) => { + startEndWidth += calcBoxWidth.call(this, node); + }); + + this[dimensionsSymbol].setVia("data.space", width - startEndWidth - 2); + this[dimensionsSymbol].setVia("data.visible", !(width === 0)); + + const buttons = this.getOption("buttons.standard").concat( + this.getOption("buttons.popper"), + ); + + for (const [i, button] of buttons.entries()) { + const ref = button?.reference; + const element = this[navElementSymbol].querySelector( + `:scope > [${ATTRIBUTE_PREFIX}tab-reference="${ref}"]`, + ); + if (!(element instanceof HTMLButtonElement)) continue; + + this[dimensionsSymbol].setVia( + `data.button.${ref}`, + calcBoxWidth.call(this, element), + ); + button["class"] = new TokenList(button["class"]) + .remove("invisible") + .toString(); + } + + const slots = this[controlElementSymbol].querySelectorAll( + `nav[${ATTRIBUTE_PREFIX}role=nav] > slot.invisible, slot[${ATTRIBUTE_PREFIX}role=slot].invisible`, + ); + for (const [, slot] of slots.entries()) { + slot.classList.remove("invisible"); + } + + this[dimensionsSymbol].setVia("data.calculated", true); + this.setOption("buttons.standard", clone(buttons)); } /** @@ -1051,34 +1089,34 @@ function calculateNavigationButtonsDimensions() { * @return {string} */ function getButtonLabel(node) { - let label; - let setLabel = false; - if (node.hasAttribute(ATTRIBUTE_BUTTON_LABEL)) { - label = node.getAttribute(ATTRIBUTE_BUTTON_LABEL); - } else { - label = node.innerText; - setLabel = true; - } - - if (!isString(label)) { - label = ""; - } - - label = label.trim(); - - if (label === "") { - label = this.getOption("labels.new-tab-label", "New Tab"); - } - - if (label.length > 100) { - label = `${label.substring(0, 99)}…`; - } - - if (setLabel === true) { - node.setAttribute(ATTRIBUTE_BUTTON_LABEL, label); - } - - return label; + let label; + let setLabel = false; + if (node.hasAttribute(ATTRIBUTE_BUTTON_LABEL)) { + label = node.getAttribute(ATTRIBUTE_BUTTON_LABEL); + } else { + label = node.innerText; + setLabel = true; + } + + if (!isString(label)) { + label = ""; + } + + label = label.trim(); + + if (label === "") { + label = this.getOption("labels.new-tab-label", "New Tab"); + } + + if (label.length > 100) { + label = `${label.substring(0, 99)}…`; + } + + if (setLabel === true) { + node.setAttribute(ATTRIBUTE_BUTTON_LABEL, label); + } + + return label; } /** @@ -1086,8 +1124,8 @@ function getButtonLabel(node) { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <template id="buttons"> <button part="button" data-monster-role="button" @@ -1107,7 +1145,7 @@ function getTemplate() { data-monster-insert="buttons path:buttons.standard"> <slot name="start" class="invisible"></slot> <div data-monster-role="popper" part="popper" tabindex="-1" - data-monster-attributes="class path:classes.popper"> + data-monster-attributes="class path:classes.popper"> <div data-popper-arrow></div>