/** * 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 { createPopper } from "@popperjs/core"; import { extend } from "../../data/extend.mjs"; import { Pathfinder } from "../../data/pathfinder.mjs"; import { addAttributeToken, addToObjectLink, hasObjectLink, } from "../../dom/attributes.mjs"; import { ATTRIBUTE_ERRORMESSAGE, ATTRIBUTE_PREFIX, ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; import { assembleMethodSymbol, CustomElement, getSlottedElements, registerCustomElement, } from "../../dom/customelement.mjs"; import { findTargetElementFromEvent, fireCustomEvent, } from "../../dom/events.mjs"; import { getDocument, getWindow } 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, } from "../form/constants.mjs"; import { TabsStyleSheet } from "./stylesheet/tabs.mjs"; import { loadAndAssignContent } from "../form/util/fetch.mjs"; import { ThemeStyleSheet } from "../stylesheet/theme.mjs"; import { popperInstanceSymbol, setEventListenersModifiers, } from "../form/util/popper.mjs"; import { getLocaleOfDocument } from "../../dom/locale.mjs"; export { Tabs }; /** * @private * @type {symbol} */ const popperElementSymbol = Symbol("popperElement"); /** * @private * @type {symbol} */ const popperNavElementSymbol = Symbol("popperNavElement"); /** * @private * @type {symbol} */ const controlElementSymbol = Symbol("controlElement"); /** * @private * @type {symbol} */ const navElementSymbol = Symbol("navElement"); /** * @private * @type {symbol} */ const switchElementSymbol = Symbol("switchElement"); /** * @private * @type {symbol} */ const changeTabEventHandler = Symbol("changeTabEventHandler"); /** * @private * @type {symbol} */ const removeTabEventHandler = Symbol("removeTabEventHandler"); /** * @private * @type {symbol} */ const popperSwitchEventHandler = Symbol("popperSwitchEventHandler"); /** * local symbol * @private * @type {symbol} */ const closeEventHandler = Symbol("closeEventHandler"); /** * @private * @type {symbol} */ const mutationObserverSymbol = Symbol("mutationObserver"); /** * @private * @type {symbol} */ const dimensionsSymbol = Symbol("dimensions"); /** * @private * @type {symbol} */ const timerCallbackSymbol = Symbol("timerCallback"); /** * local symbol * @private * @type {symbol} */ const resizeObserverSymbol = Symbol("resizeObserver"); /** * A Tabs Control * * @fragments /fragments/components/layout/tabs/ * * @example /examples/components/layout/tabs-simple Simple Tabs * @example /examples/components/layout/tabs-active Active Tabs * @example /examples/components/layout/tabs-removable Removable Tabs * @example /examples/components/layout/tabs-with-icon Tabs with Icon * @example /examples/components/layout/tabs-fetch Fetch Tab Content from URL * * @issue https://localhost.alvine.dev:8440/development/issues/closed/268.html * @issue https://localhost.alvine.dev:8440/development/issues/closed/271.html * @issue https://localhost.alvine.dev:8440/development/issues/closed/273.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 {string} features.removeBehavior="auto" Remove behavior, auto (default), next, previous and none * @property {boolean} features.openFirst=true Open the first tab * @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: getTranslations(), buttons: { standard: [], popper: [], }, fetch: { redirect: "error", method: "GET", mode: "same-origin", credentials: "same-origin", headers: { accept: "text/html", }, }, features: { openDelay: null, removeBehavior: "auto", openFirst: true, }, 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 * @returns {object} */ function getTranslations() { const locale = getLocaleOfDocument(); switch (locale.language) { case "de": return { "new-tab-label": "Neuer Tab", }; case "fr": return { "new-tab-label": "Nouvel Onglet", }; case "sp": return { "new-tab-label": "Nueva Pestaña", }; case "it": return { "new-tab-label": "Nuova Scheda", }; case "pl": return { "new-tab-label": "Nowa Karta", }; case "no": return { "new-tab-label": "Ny Fane", }; case "dk": return { "new-tab-label": "Ny Fane", }; case "sw": return { "new-tab-label": "Ny Flik", }; default: case "en": return { "new-tab-label": "New Tab", }; } } /** * @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; } /** * @private */ function hidePopper() { if (!this[popperInstanceSymbol]) { return; } 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); }); } /** * @private */ function togglePopper() { 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]); } /** * @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, }); } /** * @private * @return {Select} * @external "external:createPopper" */ 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; } /** * @private * @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); } /** * @private */ function initEventHandler() { const self = this; if (!this.shadowRoot) { throw new Error("no shadow-root is defined"); } /** * @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`, ); let doChange = false; let nextName = null; let previousName = null; const btn = this.getOption("buttons"); for (let i = 0; i < btn.standard.length; i++) { if (btn.standard[i].reference === reference) { if (btn.standard[i].state === "active") { doChange = i; if (i < btn.standard.length - 1) { nextName = btn.standard[i + 1]?.reference; } if (i > 0) { previousName = btn.standard[i - 1]?.reference; } } break; } } if (reference) { const container = this.querySelector(`[id=${reference}]`); if (container instanceof HTMLElement) { if (doChange) { switch (this.getOption("features.removeBehavior")) { case "auto": if (nextName !== null) { self.activeTab(nextName); } else { if (previousName !== null) { self.activeTab(previousName); } } break; case "next": if (nextName !== null) { self.activeTab(nextName); } break; case "previous": if (previousName !== null) { self.activeTab(previousName); } break; default: // and "none" break; } } container.remove(); initTabButtons.call(this); fireCustomEvent(this, "monster-tab-remove", { reference, }); } } } } }; /** * @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 */ this[closeEventHandler] = (event) => { const path = event.composedPath(); for (const [, element] of Object.entries(path)) { if (element === this) { return; } } hidePopper.call(this); }; // the order is important, because the remove must be before the change this[navElementSymbol].addEventListener("touch", this[removeTabEventHandler]); this[navElementSymbol].addEventListener("click", this[removeTabEventHandler]); this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]); this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]); return this; } /** * @private * @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); } /** * @private * @return {Select} * @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]`, ); } /** * @private * @return {Promise<unknown>} * @throws {Error} no shadow-root is defined * */ 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" 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 && this.getOption("features.openFirst") === true) { const firstButton = this.getOption("buttons.standard").find( (button) => button.disabled !== true, ); if (firstButton) { activeReference = firstButton.reference; } } 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); } rearrangeButtons.call(this); } /** * @private * @return {Promise<unknown>} */ function adjustButtonVisibility() { 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; // in drawing if (defCount !== domCount) return; observer.disconnect(); checkAndRearrangeButtons.call(self); resolve(); }); observer.observe(self[navElementSymbol], { attributes: true, }); }); } /** * @private * @param {string} value * @return {number} */ function getDimValue(value) { if ([undefined, null].indexOf(value) !== -1) { return 0; } const valueAsInt = parseInt(value, 10); if (isNaN(valueAsInt)) { return 0; } return valueAsInt; } /** * @private * @param {HTMLElement} node * @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"]) ); } /** * @private * @return {Object} */ function rearrangeButtons() { getWindow().requestAnimationFrame(() => { const standardButtons = []; const popperButtons = []; let sum = 0; const space = this[dimensionsSymbol].getVia("data.space"); if (space <= 0) { return; } 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"); } } }); } /** * @private * @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.setOption("buttons.standard", clone(buttons)); getWindow().requestAnimationFrame(() => { this[dimensionsSymbol].setVia("data.calculated", true); }); } /** * @private * @param {HTMLElement} node * @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; } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <template id="buttons"> <button part="button" tabindex="0" data-monster-role="button" data-monster-attributes=" class path:classes.button, data-monster-state path:buttons.state, disabled path:buttons.disabled | if:true, data-monster-tab-reference path:buttons.reference"><span data-monster-replace="path:buttons.label"></span><span part="remove-tab" data-monster-attributes="class path:buttons.remove | ?:remove-tab:hidden " data-monster-role="remove-tab" tabindex="-1"></span></button> </template> <div data-monster-role="control" part="control"> <nav data-monster-role="nav" part="nav" data-monster-attributes="data-monster-marker path:marker, class path:classes.navigation" 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"> <div data-popper-arrow></div> <div part="popper-nav" data-monster-role="popper-nav" data-monster-insert="buttons path:buttons.popper" tabindex="-1"></div> </div> <slot name="popper" class="invisible"></slot> <slot name="end" class="invisible"></slot> </nav> <slot data-monster-role="slot" class="invisible"></slot> </div>`; } registerCustomElement(Tabs);