Skip to content
Snippets Groups Projects
Select Git revision
  • master default protected
  • 1.31
  • 4.14.0
  • 4.13.1
  • 4.13.0
  • 4.12.0
  • 4.11.1
  • 4.11.0
  • 4.10.4
  • 4.10.3
  • 4.10.2
  • 4.10.1
  • 4.10.0
  • 4.9.0
  • 4.8.0
  • 4.7.0
  • 4.6.1
  • 4.6.0
  • 4.5.1
  • 4.5.0
  • 4.4.1
  • 4.4.0
22 results

tabs.mjs

Blame
  • 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);