Something went wrong on our end
Select Git revision
-
Volker Schukai authoredVolker Schukai authored
tabs.mjs 29.97 KiB
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { instanceSymbol } from "../../constants.mjs";
import { 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);