Skip to content
Snippets Groups Projects
Select Git revision
  • 816012e53abace9be121138da60819ee78e4772c
  • master default protected
  • 1.31
  • 4.38.2
  • 4.38.1
  • 4.38.0
  • 4.37.2
  • 4.37.1
  • 4.37.0
  • 4.36.0
  • 4.35.0
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
  • 4.31.0
  • 4.30.1
  • 4.30.0
  • 4.29.1
  • 4.29.0
23 results

split-panel.mjs

Blame
  • select.mjs 96.04 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, internalSymbol } from "../../constants.mjs";
    import { buildMap, build as buildValue } from "../../data/buildmap.mjs";
    import {
    	addAttributeToken,
    	containsAttributeToken,
    	findClosestByAttribute,
    	removeAttributeToken,
    } from "../../dom/attributes.mjs";
    import { ATTRIBUTE_PREFIX, ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
    import { CustomControl } from "../../dom/customcontrol.mjs";
    import {
    	assembleMethodSymbol,
    	getSlottedElements,
    	registerCustomElement,
    } from "../../dom/customelement.mjs";
    import { addErrorAttribute, removeErrorAttribute } from "../../dom/error.mjs";
    import {
    	findTargetElementFromEvent,
    	fireCustomEvent,
    	fireEvent,
    } from "../../dom/events.mjs";
    import { getLocaleOfDocument } from "../../dom/locale.mjs";
    import { getDocument } from "../../dom/util.mjs";
    import {
    	getDocumentTranslations,
    	Translations,
    } from "../../i18n/translations.mjs";
    import { Formatter } from "../../text/formatter.mjs";
    import { getGlobal } from "../../types/global.mjs";
    import { ID } from "../../types/id.mjs";
    import {
    	isArray,
    	isFunction,
    	isInteger,
    	isIterable,
    	isObject,
    	isPrimitive,
    	isString,
    } from "../../types/is.mjs";
    import { Observer } from "../../types/observer.mjs";
    import { ProxyObserver } from "../../types/proxyobserver.mjs";
    import { validateArray, validateString } from "../../types/validate.mjs";
    import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
    import { Processing } from "../../util/processing.mjs";
    import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs";
    import { SelectStyleSheet } from "./stylesheet/select.mjs";
    import { positionPopper } from "./util/floating-ui.mjs";
    import { Pathfinder } from "../../data/pathfinder.mjs";
    import { TokenList } from "../../types/tokenlist.mjs";
    
    export {
    	getSelectionTemplate,
    	getSummaryTemplate,
    	popperElementSymbol,
    	Select,
    };
    
    /**
     * @private
     * @type {Symbol}
     */
    const timerCallbackSymbol = Symbol("timerCallback");
    
    /**
     * @private
     * @type {Symbol}
     */
    const keyFilterEventSymbol = Symbol("keyFilterEvent");
    
    /**
     * @private
     * @type {Symbol}
     */
    const lazyLoadDoneSymbol = Symbol("lazyLoadDone");
    
    /**
     * @private
     * @type {Symbol}
     */
    const isLoadingSymbol = Symbol("isLoading");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const closeEventHandler = Symbol("closeEventHandler");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const clearOptionEventHandler = Symbol("clearOptionEventHandler");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const resizeObserverSymbol = Symbol("resizeObserver");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const keyEventHandler = Symbol("keyEventHandler");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const lastFetchedDataSymbol = Symbol("lastFetchedData");
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const inputEventHandler = Symbol("inputEventHandler");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const changeEventHandler = Symbol("changeEventHandler");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const controlElementSymbol = Symbol("controlElement");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const selectionElementSymbol = Symbol("selectionElement");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const containerElementSymbol = Symbol("containerElement");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const popperElementSymbol = Symbol("popperElement");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const inlineFilterElementSymbol = Symbol("inlineFilterElement");
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const popperFilterElementSymbol = Symbol("popperFilterElement");
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const popperFilterContainerElementSymbol = Symbol(
    	"popperFilterContainerElement",
    );
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const optionsElementSymbol = Symbol("optionsElement");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const noOptionsAvailableElementSymbol = Symbol("noOptionsAvailableElement");
    
    /**
     * local symbol
     * @private
     * @type {Symbol}
     */
    const statusOrRemoveBadgesElementSymbol = Symbol("statusOrRemoveBadgesElement");
    
    /**
     * local symbol
     * @type {symbol}
     */
    const remoteInfoElementSymbol = Symbol("remoteInfoElement");
    /**
     * @private
     * @type {Symbol}
     */
    const areOptionsAvailableAndInitSymbol = Symbol("@@areOptionsAvailableAndInit");
    
    /**
     * @private
     * @type {symbol}
     */
    const disabledRequestMarker = Symbol("@@disabledRequestMarker");
    
    /**
     * @private
     * @type {symbol}
     */
    const runLookupOnceSymbol = Symbol("runLookupOnce");
    
    /**
     * @private
     * @type {symbol}
     */
    const cleanupOptionsListSymbol = Symbol("cleanupOptionsList");
    
    /**
     * @private
     * @type {symbol}
     */
    const debounceOptionsMutationObserverSymbol = Symbol(
    	"debounceOptionsMutationObserver",
    );
    
    /**
     * @private
     * @type {number}
     */
    const FOCUS_DIRECTION_UP = 1;
    /**
     * @private
     * @type {number}
     */
    const FOCUS_DIRECTION_DOWN = 2;
    
    /**
     * @private
     * @type {string}
     */
    const FILTER_MODE_REMOTE = "remote";
    
    /**
     * @private
     * @type {string}
     */
    const FILTER_MODE_OPTIONS = "options";
    
    /**
     * @private
     * @type {string}
     */
    const FILTER_MODE_DISABLED = "disabled";
    
    /**
     * @private
     * @type {string}
     */
    const FILTER_POSITION_POPPER = "popper";
    /**
     * @private
     * @type {string}
     */
    const FILTER_POSITION_INLINE = "inline";
    
    /**
     * A select control that can be used to select o
     *
     * @issue @issue https://localhost.alvine.dev:8440/development/issues/closed/280.html
     * @issue @issue https://localhost.alvine.dev:8440/development/issues/closed/287.html
     *
     * @fragments /fragments/components/form/select/
     *
     * @example /examples/components/form/select-with-options Select with options
     * @example /examples/components/form/select-with-html-options Select with HTML options
     * @example /examples/components/form/select-multiple Multiple selection
     * @example /examples/components/form/select-filter Filter
     * @example /examples/components/form/select-fetch Fetch options
     * @example /examples/components/form/select-lazy Lazy load
     * @example /examples/components/form/select-remote-filter Remote filter
     *
     * @copyright schukai GmbH
     * @summary A beautiful select control that can make your life easier and also looks good.
     * @fires monster-change
     * @fires monster-changed
     * @fires monster-options-set this event is fired when the options are set
     * @fires monster-selection-removed
     * @fires monster-selection-cleared
     */
    class Select extends CustomControl {
    	/**
    	 *
    	 */
    	constructor() {
    		super();
    		initOptionObserver.call(this);
    	}
    
    	/**
    	 * This method is called by the `instanceof` operator.
    	 * @return {Symbol}
    	 */
    	static get [instanceSymbol]() {
    		return Symbol.for("@schukai/monster/components/form/select@@instance");
    	}
    
    	/**
    	 * The current selection of the Select
    	 *
    	 * ```
    	 * e = document.querySelector('monster-select');
    	 * console.log(e.value)
    	 * // ↦ 1
    	 * // ↦ ['1','2']
    	 * ```
    	 *
    	 * @return {string}
    	 */
    	get value() {
    		return convertSelectionToValue.call(this, this.getOption("selection"));
    	}
    
    	/**
    	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
    	 * @return {boolean}
    	 */
    	static get formAssociated() {
    		return true;
    	}
    
    	/**
    	 * Set selection
    	 *
    	 * ```
    	 * e = document.querySelector('monster-select');
    	 * e.value=1
    	 * ```
    	 *
    	 * @property {string|array} value
    	 * @throws {Error} unsupported type
    	 * @fires monster-selected this event is fired when the selection is set
    	 */
    	set value(value) {
    		const result = convertValueToSelection.call(this, value);
    
    		setSelection
    			.call(this, result.selection)
    			.then(() => {})
    			.catch((e) => {
    				addErrorAttribute(this, e);
    			});
    	}
    
    	/**
    	 * 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 {string[]} toggleEventType Array of DOM event names (e.g. ["click","touch"]) to toggle the dropdown.
    	 * @property {boolean} delegatesFocus Whether the element delegates focus to its internal control (e.g. the filter input).
    	 * @property {Array<Object>} options Array of option objects {label,value,visibility?,data?} for static option list.
    	 * @property {string|string[]} selection Initial selected value(s) as string, comma-separated string, or array of strings.
    	 * @property {number} showMaxOptions Maximum visible options before the dropdown scrolls.
    	 * @property {"radio"|"checkbox"} type Selection mode: "radio" for single, "checkbox" for multiple.
    	 * @property {string} name Name of the hidden form field for form submission.
    	 * @property {string|null} url URL to dynamically fetch options via HTTP when opening or filtering.
    	 * @property {Object} lookup Configuration for lookup requests.
    	 * @property {string} lookup.url URL template with ${filter} placeholder to fetch only selected entries on init when `url` is set and either `features.lazyLoad` or `filter.mode==="remote"`.
    	 * @property {boolean} lookup.grouping Group lookup requests: true to fetch all selected values in one request, false to fetch each individually.
    	 * @property {string} fetch.redirect Fetch redirect mode (e.g. "error").
    	 * @property {string} fetch.method HTTP method for fetching options (e.g. "GET").
    	 * @property {string} fetch.mode Fetch mode (e.g. "same-origin").
    	 * @property {string} fetch.credentials Credentials policy for fetch (e.g. "same-origin").
    	 * @property {Object.<string,string>} fetch.headers HTTP headers for fetch requests.
    	 * @property {string} labels.cannot-be-loaded Message when options cannot be loaded.
    	 * @property {string} labels.no-options-available Message when no static options are available.
    	 * @property {string} labels.click-to-load-options Message prompting user to click to load options when `features.lazyLoad` is enabled.
    	 * @property {string} labels.select-an-option Placeholder text when no selection is made.
    	 * @property {string} labels.no-options Message when neither slots nor fetched options exist.
    	 * @property {string} labels.no-options-found Message when filter yields no matching options.
    	 * @property {string} labels.summary-text.zero Plural template for zero selected entries (e.g. "No entries were selected").
    	 * @property {string} labels.summary-text.one Plural template for one selected entry.
    	 * @property {string} labels.summary-text.other Plural template for multiple selected entries.
    	 * @property {boolean} features.clearAll Show a "clear all" badge to reset selection.
    	 * @property {boolean} features.clear Show remove icon on individual selection badges.
    	 * @property {boolean} features.lazyLoad Lazy-load options on first open (initial fetch on show and triggers `lookup.url` preload; automatically disabled if `filter.mode==="remote"`).
    	 * @property {boolean} features.closeOnSelect Automatically close dropdown after selection.
    	 * @property {boolean} features.emptyValueIfNoOptions Set value to empty when no options are available.
    	 * @property {boolean} features.storeFetchedData Persist raw fetched data for later retrieval via `getLastFetchedData()`.
    	 * @property {boolean} features.useStrictValueComparison Use strict (`===`) comparison when matching option values.
    	 * @property {boolean} features.showRemoteInfo When the filter mode is set to "remote," display a badge indicating the possibility of additional remote options.
    	 * @property {Object} remoteInfo Configuration for remote info badge.
    	 * @property {string} remoteInfo.path Path to the remote info badge.
    	 * @property {string} remoteInfo.url URL for total count of options when `filter.mode==="remote"` is set.
    	 * @property {Object} placeholder Placeholder text for the control.
    	 * @property {string} placeholder.filter Placeholder text for filter input.
    	 * @property {string|null} filter.defaultValue Default filter value for remote requests; if unset or empty, disabled marker prevents request.
    	 * @property {"options"|"remote"|"disabled"} filter.mode Client-side ("options"), server-side ("remote"; disables `features.lazyLoad`), or disabled filtering.
    	 * @property {"inline"|"popper"} filter.position Position of filter input: inline within control or inside popper dropdown.
    	 * @property {string} filter.marker.open Opening marker for embedding filter value in `filter.mode==="remote"` URLs.
    	 * @property {string} filter.marker.close Closing marker for embedding filter value in URLs.
    	 * @property {string|null} filter.defaultOptionsUrl URL for default options when `filter.mode==="remote"` is set and no filter value is provided.
    	 * @property {string} templates.main HTML template string for rendering options and selection badges.
    	 * @property {string} templateMapping.selected Template variant for selected items (e.g. badge vs summary view).
    	 * @property {string} popper.placement Popper.js placement strategy for dropdown (e.g. "bottom").
    	 * @property {Array<string|Object>} popper.middleware Popper.js middleware or offset configurations.
    	 * @property {string} mapping.selector Data path or selector to identify entries in imported data.
    	 * @property {string} mapping.labelTemplate Template for option labels using placeholders like `${name}`.
    	 * @property {string} mapping.valueTemplate Template for option values using placeholders like `${name}`.
    	 * @property {Function} mapping.filter Optional callback to filter imported map entries before building `options[]`.
    	 * @property {string} empty.defaultValueRadio Default radio-value when no selection exists.
    	 * @property {Array} empty.defaultValueCheckbox Default checkbox-values array when no selection exists.
    	 * @property {Array} empty.equivalents Values considered empty (e.g. `undefined`, `null`, `""`, `NaN`) and normalized to defaults.
    	 * @property {Function} formatter.selection Callback `(value)=>string` to format the display label of each selected value.
    	 * @property {Object} classes CSS classes for styling.
    	 * @property {string} classes.badge CSS class for the selection badge.
    	 * @property {string} classes.statusOrRemoveBadge CSS class for the status or remove badge.
    	 * @property {string} classes.remoteInfo CSS class for the remote info badge.
    	 * @property {string} classes.noOptions CSS class for the no options available message.
    	 * @property {number|null} total Total number of options available.
    	 */
    	get defaults() {
    		return Object.assign(
    			{},
    			super.defaults,
    			{
    				toggleEventType: ["click", "touch"],
    				delegatesFocus: false,
    				options: [],
    				selection: [],
    				showMaxOptions: 10,
    				type: "radio",
    				name: new ID("s").toString(),
    				features: {
    					clearAll: true,
    					clear: true,
    					lazyLoad: false,
    					closeOnSelect: false,
    					emptyValueIfNoOptions: false,
    					storeFetchedData: false,
    					useStrictValueComparison: false,
    					showRemoteInfo: true,
    				},
    				placeholder: {
    					filter: "",
    				},
    				url: null,
    
    				remoteInfo: {
    					path: null,
    					url: null,
    				},
    
    				lookup: {
    					url: null,
    					grouping: false,
    				},
    
    				labels: getTranslations(),
    				messages: {
    					control: null,
    					selected: null,
    					emptyOptions: null,
    					total: null,
    				},
    				fetch: {
    					redirect: "error",
    					method: "GET",
    					mode: "same-origin",
    					credentials: "same-origin",
    					headers: {
    						accept: "application/json",
    					},
    				},
    				filter: {
    					defaultValue: null,
    					mode: FILTER_MODE_DISABLED,
    					position: FILTER_POSITION_INLINE,
    					marker: {
    						open: "{",
    						close: "}",
    					},
    					defaultOptionsUrl: null,
    				},
    				classes: {
    					badge: "monster-badge-primary",
    					statusOrRemoveBadge: "empty",
    					remoteInfo: "monster-margin-start-4 monster-margin-top-4",
    					noOptions: "monster-margin-top-4 monster-margin-start-4",
    				},
    				mapping: {
    					selector: "*",
    					labelTemplate: "",
    					valueTemplate: "",
    					filter: null,
    					total: null,
    				},
    				empty: {
    					defaultValueRadio: "",
    					defaultValueCheckbox: [],
    					equivalents: [undefined, null, "", NaN],
    				},
    				formatter: {
    					selection: buildSelectionLabel,
    				},
    				templates: {
    					main: getTemplate(),
    				},
    				templateMapping: {
    					/** with the attribute `data-monster-selected-template` the template for the selected options can be defined. */
    					selected: getSelectionTemplate(),
    				},
    
    				total: null,
    
    				popper: {
    					placement: "bottom",
    					middleware: ["flip", "offset:1"],
    				},
    			},
    			initOptionsFromArguments.call(this),
    		);
    	}
    
    	/**
    	 * @return {Select}
    	 */
    	[assembleMethodSymbol]() {
    		const self = this;
    		super[assembleMethodSymbol]();
    
    		initControlReferences.call(self);
    		initEventHandler.call(self);
    
    		let lazyLoadFlag = self.getOption("features.lazyLoad", false);
    		const remoteFilterFlag = getFilterMode.call(this) === FILTER_MODE_REMOTE;
    
    		initTotal.call(self);
    
    		if (getFilterMode.call(this) === FILTER_MODE_REMOTE) {
    			self.setOption("features.lazyLoad", false);
    			lazyLoadFlag = false;
    		}
    
    		if (self.hasAttribute("value")) {
    			new Processing(10, () => {
    				const oldValue = self.value;
    				const newValue = self.getAttribute("value");
    				if (oldValue !== newValue) {
    					self.value = newValue;
    				}
    			})
    				.run()
    				.catch((e) => {
    					addErrorAttribute(this, e);
    				});
    		}
    
    		if (self.getOption("url") !== null) {
    			if (lazyLoadFlag || remoteFilterFlag) {
    				lookupSelection.call(self);
    			} else {
    				self
    					.fetch()
    					.then(() => {})
    					.catch((e) => {
    						addErrorAttribute(self, e);
    					});
    			}
    		}
    
    		setTimeout(() => {
    			let lastValue = self.value;
    			self[internalSymbol].attachObserver(
    				new Observer(function () {
    					if (isObject(this) && this instanceof ProxyObserver) {
    						const n = this.getSubject()?.options?.value;
    
    						if (lastValue !== n && n !== undefined) {
    							lastValue = n;
    							setSelection
    								.call(self, n)
    								.then(() => {})
    								.catch((e) => {
    									addErrorAttribute(self, e);
    								});
    						}
    					}
    				}),
    			);
    
    			areOptionsAvailableAndInit.call(self);
    		}, 0);
    
    		return this;
    	}
    
    	/**
    	 *
    	 * @return {*}
    	 * @throws {Error} storeFetchedData is not enabled
    	 * @since 3.66.0
    	 */
    	getLastFetchedData() {
    		if (this.getOption("features.storeFetchedData") === false) {
    			throw new Error("storeFetchedData is not enabled");
    		}
    
    		return this?.[lastFetchedDataSymbol];
    	}
    
    	/**
    	 * The Button.click() method simulates a click on the internal button element.
    	 *
    	 * @since 3.27.0
    	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
    	 */
    	click() {
    		if (this.getOption("disabled") === true) {
    			return;
    		}
    
    		toggle.call(this);
    	}
    
    	/**
    	 * The Button.focus() method sets focus on the internal button element.
    	 *
    	 * @since 3.27.0
    	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
    	 */
    	focus(options) {
    		if (this.getOption("disabled") === true) {
    			return;
    		}
    
    		new Processing(() => {
    			gatherState.call(this);
    			focusFilter.call(this, options);
    		})
    			.run()
    			.catch((e) => {
    				addErrorAttribute(this, e);
    			});
    	}
    
    	/**
    	 * The Button.blur() method removes focus from the internal button element.
    	 * @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur
    	 */
    	blur() {
    		new Processing(() => {
    			gatherState.call(this);
    			blurFilter.call(this);
    		})
    			.run()
    			.catch((e) => {
    				addErrorAttribute(this, e);
    			});
    	}
    
    	/**
    	 * If no url is specified, the options are taken from the Component itself.
    	 *
    	 * @param {string|URL} url URL to fetch the options
    	 * @return {Promise}
    	 */
    	fetch(url) {
    		return fetchIt.call(this, url);
    	}
    
    	/**
    	 * @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]);
    		}
    
    		parseSlotsToOptions.call(this);
    		attachResizeObserver.call(this);
    		updatePopper.call(this);
    
    		new Processing(() => {
    			gatherState.call(this);
    			focusFilter.call(this);
    		})
    			.run()
    			.catch((e) => {
    				addErrorAttribute(this, e);
    			});
    	}
    
    	/**
    	 * @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]);
    		}
    
    		disconnectResizeObserver.call(this);
    	}
    
    	/**
    	 * 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
    	 * @throws {Error} missing label configuration
    	 * @fires monster-options-set this event is fired when the options are set
    	 */
    	importOptions(data) {
    		this[cleanupOptionsListSymbol] = true;
    		return importOptionsIntern.call(this, data);
    	}
    
    	/**
    	 * @private
    	 * @return {Select}
    	 */
    	calcAndSetOptionsDimension() {
    		calcAndSetOptionsDimension.call(this);
    		return this;
    	}
    
    	/**
    	 *
    	 * @return {string}
    	 */
    	static getTag() {
    		return "monster-select";
    	}
    
    	/**
    	 *
    	 * @return {CSSStyleSheet[]}
    	 */
    	static getCSSStyleSheet() {
    		return [SelectStyleSheet];
    	}
    }
    
    /**
     * @private
     * @param data
     * @returns {any}
     */
    function importOptionsIntern(data) {
    	const self = this;
    	const mappingOptions = this.getOption("mapping", {});
    	const selector = mappingOptions?.["selector"];
    	const labelTemplate = mappingOptions?.["labelTemplate"];
    	const valueTemplate = mappingOptions?.["valueTemplate"];
    	let filter = mappingOptions?.["filter"];
    
    	let flag = false;
    	if (labelTemplate === "") {
    		addErrorAttribute(this, "empty label template");
    		flag = true;
    	}
    
    	if (valueTemplate === "") {
    		addErrorAttribute(this, "empty value template");
    		flag = true;
    	}
    
    	if (flag === true) {
    		throw new Error("missing label configuration");
    	}
    
    	if (isString(filter)) {
    		if (0 === filter.indexOf("run:")) {
    			const code = filter.replace("run:", "");
    			filter = (m, v, k) => {
    				const fkt = new Function("m", "v", "k", "control", code);
    				return fkt(m, v, k, self);
    			};
    		} else if (0 === filter.indexOf("call:")) {
    			const parts = filter.split(":");
    			parts.shift(); // remove prefix
    			const fkt = parts.shift();
    
    			switch (fkt) {
    				case "filterValueOfAttribute":
    					const attribute = parts.shift();
    					const attrValue = self.getAttribute(attribute);
    
    					filter = (m, v, k) => {
    						const mm = buildValue(m, valueTemplate);
    						return mm != attrValue; // no type check, no !==
    					};
    					break;
    
    				default:
    					addErrorAttribute(this, new Error(`Unknown filter function ${fkt}`));
    			}
    		}
    	}
    
    	const map = buildMap(data, selector, labelTemplate, valueTemplate, filter);
    
    	let options = [];
    	if (this[cleanupOptionsListSymbol] !== true) {
    		options = this.getOption("options", []);
    	}
    
    	if (!isIterable(map)) {
    		throw new Error("map is not iterable");
    	}
    
    	const visibility = "visible";
    
    	map.forEach((label, value) => {
    		for (const option of options) {
    			if (option.value === value) {
    				option.label = label;
    				option.visibility = visibility;
    				option.data = map.get(value);
    				return;
    			}
    		}
    
    		options.push({
    			value,
    			label,
    			visibility,
    			data: map.get(value),
    		});
    	});
    
    	this.setOption("options", options);
    
    	fireCustomEvent(this, "monster-options-set", {
    		options,
    	});
    
    	setTimeout(() => {
    		setSelection
    			.call(this, this.getOption("selection"))
    			.then(() => {})
    			.catch((e) => {
    				addErrorAttribute(this, e);
    			});
    	}, 10);
    
    	return this;
    }
    
    /**
     * @private
     * @returns {object}
     */
    function getTranslations() {
    	const locale = getLocaleOfDocument();
    	switch (locale.language) {
    		case "de":
    			return {
    				"cannot-be-loaded": "Kann nicht geladen werden",
    				"no-options-available": "Keine Auswahl verfügbar.",
    				"click-to-load-options": "Klicken, um Auswahl zu laden.",
    				"select-an-option": "Bitte Auswahl treffen",
    				"summary-text": {
    					zero: "Keine Auswahl getroffen",
    					one: '<span class="monster-badge-primary-pill">1</span> Auswahl getroffen',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> Auswahlen getroffen',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Leider gibt es keine Auswahlmöglichkeiten in der Liste.</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">Keine Auswahlmöglichkeiten verfügbar. Bitte ändern Sie den Filter.</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Es sind keine weiteren Auswahlmöglichkeiten verfügbar.</span>',
    					one: '<span class="monster-badge-primary-pill">Es ist 1 weitere Auswahlmöglichkeit verfügbar.</span>',
    					other:
    						'<span class="monster-badge-primary-pill">Es sind ${count} weitere Auswahlmöglichkeiten verfügbar.</span>',
    				},
    			};
    
    		case "es":
    			return {
    				"cannot-be-loaded": "No se puede cargar",
    				"no-options-available": "No hay opciones disponibles.",
    				"click-to-load-options": "Haga clic para cargar opciones.",
    				"select-an-option": "Seleccione una opción",
    				"summary-text": {
    					zero: "No se seleccionaron entradas",
    					one: '<span class="monster-badge-primary-pill">1</span> entrada seleccionada',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> entradas seleccionadas',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Desafortunadamente, no hay opciones disponibles en la lista.</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">No hay opciones disponibles en la lista. Por favor, modifique el filtro.</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">No hay entradas adicionales disponibles.</span>',
    					one: '<span class="monster-badge-primary-pill">1 entrada adicional está disponible.</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} entradas adicionales están disponibles.</span>',
    				},
    			};
    
    		case "zh":
    			return {
    				"cannot-be-loaded": "无法加载",
    				"no-options-available": "没有可用选项。",
    				"click-to-load-options": "点击以加载选项。",
    				"select-an-option": "选择一个选项",
    				"summary-text": {
    					zero: "未选择任何条目",
    					one: '<span class="monster-badge-primary-pill">1</span> 个条目已选择',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> 个条目已选择',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">很抱歉,列表中没有可用选项。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">列表中没有可用选项。请修改筛选条件。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">没有更多条目可用。</span>',
    					one: '<span class="monster-badge-primary-pill">还有 1 个可用条目。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">还有 ${count} 个可用条目。</span>',
    				},
    			};
    
    		case "hi":
    			return {
    				"cannot-be-loaded": "लोड नहीं किया जा सकता",
    				"no-options-available": "कोई विकल्प उपलब्ध नहीं है।",
    				"click-to-load-options": "विकल्प लोड करने के लिए क्लिक करें।",
    				"select-an-option": "एक विकल्प चुनें",
    				"summary-text": {
    					zero: "कोई प्रविष्टि चयनित नहीं",
    					one: '<span class="monster-badge-primary-pill">1</span> प्रविष्टि चयनित',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> प्रविष्टियाँ चयनित',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">क्षमा करें, सूची में कोई विकल्प उपलब्ध नहीं है।</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">सूची में कोई विकल्प उपलब्ध नहीं है। कृपया फ़िल्टर बदलें।</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">कोई अतिरिक्त प्रविष्टि उपलब्ध नहीं है।</span>',
    					one: '<span class="monster-badge-primary-pill">1 अतिरिक्त प्रविष्टि उपलब्ध है।</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} अतिरिक्त प्रविष्टियाँ उपलब्ध हैं।</span>',
    				},
    			};
    
    		case "bn":
    			return {
    				"cannot-be-loaded": "লোড করা যায়নি",
    				"no-options-available": "কোন বিকল্প উপলব্ধ নেই।",
    				"click-to-load-options": "বিকল্প লোড করতে ক্লিক করুন।",
    				"select-an-option": "একটি বিকল্প নির্বাচন করুন",
    				"summary-text": {
    					zero: "কোন এন্ট্রি নির্বাচিত হয়নি",
    					one: '<span class="monster-badge-primary-pill">1</span> এন্ট্রি নির্বাচিত',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> এন্ট্রি নির্বাচিত',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">দুঃখিত, তালিকায় কোন বিকল্প পাওয়া যায়নি।</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">তালিকায় কোন বিকল্প পাওয়া যায়নি। দয়া করে ফিল্টার পরিবর্তন করুন।</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">আর কোনো এন্ট্রি উপলব্ধ নেই।</span>',
    					one: '<span class="monster-badge-primary-pill">1 অতিরিক্ত এন্ট্রি উপলব্ধ।</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} অতিরিক্ত এন্ট্রি উপলব্ধ।</span>',
    				},
    			};
    
    		case "pt":
    			return {
    				"cannot-be-loaded": "Não é possível carregar",
    				"no-options-available": "Nenhuma opção disponível.",
    				"click-to-load-options": "Clique para carregar opções.",
    				"select-an-option": "Selecione uma opção",
    				"summary-text": {
    					zero: "Nenhuma entrada selecionada",
    					one: '<span class="monster-badge-primary-pill">1</span> entrada selecionada',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> entradas selecionadas',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Infelizmente, não há opções disponíveis na lista.</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">Nenhuma opção disponível na lista. Considere modificar o filtro.</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Não há entradas adicionais disponíveis.</span>',
    					one: '<span class="monster-badge-primary-pill">1 entrada adicional está disponível.</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} entradas adicionais estão disponíveis.</span>',
    				},
    			};
    
    		case "ru":
    			return {
    				"cannot-be-loaded": "Не удалось загрузить",
    				"no-options-available": "Нет доступных вариантов.",
    				"click-to-load-options": "Нажмите, чтобы загрузить варианты.",
    				"select-an-option": "Выберите вариант",
    				"summary-text": {
    					zero: "Нет выбранных записей",
    					one: '<span class="monster-badge-primary-pill">1</span> запись выбрана',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> записей выбрано',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">К сожалению, в списке нет доступных вариантов.</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">В списке нет доступных вариантов. Пожалуйста, измените фильтр.</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Дополнительных записей нет.</span>',
    					one: '<span class="monster-badge-primary-pill">Доступна 1 дополнительная запись.</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} дополнительных записей доступны.</span>',
    				},
    			};
    
    		case "ja":
    			return {
    				"cannot-be-loaded": "読み込めません",
    				"no-options-available": "利用可能なオプションがありません。",
    				"click-to-load-options": "クリックしてオプションを読み込む。",
    				"select-an-option": "オプションを選択",
    				"summary-text": {
    					zero: "選択された項目はありません",
    					one: '<span class="monster-badge-primary-pill">1</span> 件選択されました',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> 件選択されました',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">申し訳ありませんが、リストに利用可能なオプションがありません。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">リストに利用可能なオプションがありません。フィルターを変更してください。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">追加の項目はありません。</span>',
    					one: '<span class="monster-badge-primary-pill">1 件の追加項目があります。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} 件の追加項目があります。</span>',
    				},
    			};
    
    		case "pa":
    			return {
    				"cannot-be-loaded": "ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ",
    				"no-options-available": "ਕੋਈ ਚੋਣ ਉਪਲਬਧ ਨਹੀਂ।",
    				"click-to-load-options": "ਚੋਣਾਂ ਲੋਡ ਕਰਨ ਲਈ ਕਲਿੱਕ ਕਰੋ।",
    				"select-an-option": "ਇੱਕ ਚੋਣ ਚੁਣੋ",
    				"summary-text": {
    					zero: "ਕੋਈ ਐਂਟਰੀ ਚੁਣੀ ਨਹੀਂ ਗਈ",
    					one: '<span class="monster-badge-primary-pill">1</span> ਐਂਟਰੀ ਚੁਣੀ ਗਈ',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> ਐਂਟਰੀਆਂ ਚੁਣੀਆਂ ਗਈਆਂ',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">ਮਾਫ ਕਰਨਾ, ਸੂਚੀ ਵਿੱਚ ਕੋਈ ਚੋਣ ਉਪਲਬਧ ਨਹੀਂ ਹੈ।</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">ਸੂਚੀ ਵਿੱਚ ਕੋਈ ਚੋਣ ਉਪਲਬਧ ਨਹੀਂ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਫਿਲਟਰ ਬਦਲੋ।</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">ਕੋਈ ਹੋਰ ਐਂਟਰੀ ਉਪਲਬਧ ਨਹੀਂ ਹੈ.</span>',
    					one: '<span class="monster-badge-primary-pill">1 ਵਾਧੂ ਐਂਟਰੀ ਉਪਲਬਧ ਹੈ.</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} ਵਾਧੂ ਐਂਟਰੀਆਂ ਉਪਲਬਧ ਹਨ.</span>',
    				},
    			};
    
    		case "mr":
    			return {
    				"cannot-be-loaded": "लोड केले जाऊ शकत नाही",
    				"no-options-available": "कोणतीही पर्याय उपलब्ध नाहीत。",
    				"click-to-load-options": "पर्याय लोड करण्यासाठी क्लिक करा。",
    				"select-an-option": "एक पर्याय निवडा",
    				"summary-text": {
    					zero: "कोणीही नोंद निवडलेली नाही",
    					one: '<span class="monster-badge-primary-pill">1</span> नोंद निवडली',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> नोंदी निवडल्या',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">क्षमस्व, यादीमध्ये कोणतीही पर्याय उपलब्ध नाहीत。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">यादीमध्ये कोणतेही पर्याय उपलब्ध नाहीत। कृपया फिल्टर बदला。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">आणखी कोणतीही नोंद उपलब्ध नाही。</span>',
    					one: '<span class="monster-badge-primary-pill">1 अतिरिक्त नोंद उपलब्ध आहे。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} अतिरिक्त नोंदी उपलब्ध आहेत。</span>',
    				},
    			};
    
    		case "it":
    			return {
    				"cannot-be-loaded": "Non può essere caricato",
    				"no-options-available": "Nessuna opzione disponibile。",
    				"click-to-load-options": "Clicca per caricare le opzioni。",
    				"select-an-option": "Seleziona un'opzione",
    				"summary-text": {
    					zero: "Nessuna voce selezionata",
    					one: '<span class="monster-badge-primary-pill">1</span> voce selezionata',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> voci selezionate',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Purtroppo, non ci sono opzioni disponibili nella lista。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">Nessuna opzione disponibile nella lista。Si prega di modificare il filtro。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Non ci sono altre voci disponibili。</span>',
    					one: '<span class="monster-badge-primary-pill">C\'è 1 voce aggiuntiva disponibile。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">Ci sono ${count} voci aggiuntive disponibili。</span>',
    				},
    			};
    
    		case "nl":
    			return {
    				"cannot-be-loaded": "Kan niet worden geladen",
    				"no-options-available": "Geen opties beschikbaar。",
    				"click-to-load-options": "Klik om opties te laden。",
    				"select-an-option": "Selecteer een optie",
    				"summary-text": {
    					zero: "Er zijn geen items geselecteerd",
    					one: '<span class="monster-badge-primary-pill">1</span> item geselecteerd',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> items geselecteerd',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Helaas zijn er geen opties beschikbaar in de lijst。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">Geen opties beschikbaar in de lijst。Overweeg het filter aan te passen。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Er zijn geen extra items beschikbaar。</span>',
    					one: '<span class="monster-badge-primary-pill">1 extra item is beschikbaar。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} extra items zijn beschikbaar。</span>',
    				},
    			};
    
    		case "sv":
    			return {
    				"cannot-be-loaded": "Kan inte laddas",
    				"no-options-available": "Inga alternativ tillgängliga。",
    				"click-to-load-options": "Klicka för att ladda alternativ。",
    				"select-an-option": "Välj ett alternativ",
    				"summary-text": {
    					zero: "Inga poster valdes",
    					one: '<span class="monster-badge-primary-pill">1</span> post valdes',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> poster valdes',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Tyvärr finns det inga alternativ tillgängliga i listan。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">Inga alternativ finns tillgängliga i listan。Överväg att modifiera filtret。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Det finns inga fler poster tillgängliga。</span>',
    					one: '<span class="monster-badge-primary-pill">Det finns 1 ytterligare post tillgänglig。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">Det finns ${count} ytterligare poster tillgängliga。</span>',
    				},
    			};
    
    		case "pl":
    			return {
    				"cannot-be-loaded": "Nie można załadować",
    				"no-options-available": "Brak dostępnych opcji。",
    				"click-to-load-options": "Kliknij, aby załadować opcje。",
    				"select-an-option": "Wybierz opcję",
    				"summary-text": {
    					zero: "Nie wybrano żadnych wpisów",
    					one: '<span class="monster-badge-primary-pill">1</span> wpis został wybrany',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> wpisy zostały wybrane',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Niestety, nie ma dostępnych opcji na liście。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">Brak dostępnych opcji na liście。Rozważ zmianę filtra。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Nie ma więcej dostępnych wpisów。</span>',
    					one: '<span class="monster-badge-primary-pill">Jest 1 dodatkowy wpis dostępny。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">Jest ${count} dodatkowych wpisów dostępnych。</span>',
    				},
    			};
    
    		case "da":
    			return {
    				"cannot-be-loaded": "Kan ikke indlæses",
    				"no-options-available": "Ingen muligheder tilgængelige。",
    				"click-to-load-options": "Klik for at indlæse muligheder。",
    				"select-an-option": "Vælg en mulighed",
    				"summary-text": {
    					zero: "Ingen indlæg blev valgt",
    					one: '<span class="monster-badge-primary-pill">1</span> indlæg blev valgt',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> indlæg blev valgt',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Desværre er der ingen muligheder tilgængelige på listen。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">Ingen muligheder tilgængelige på listen。Overvej at ændre filteret。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Der er ingen yderligere poster tilgængelige。</span>',
    					one: '<span class="monster-badge-primary-pill">Der er 1 yderligere post tilgængelig。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">Der er ${count} yderligere poster tilgængelige。</span>',
    				},
    			};
    
    		case "fi":
    			return {
    				"cannot-be-loaded": "Ei voi ladata",
    				"no-options-available": "Ei vaihtoehtoja saatavilla。",
    				"click-to-load-options": "Napsauta ladataksesi vaihtoehtoja。",
    				"select-an-option": "Valitse vaihtoehto",
    				"summary-text": {
    					zero: "Ei valittuja kohteita",
    					one: '<span class="monster-badge-primary-pill">1</span> kohde valittu',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> kohdetta valittu',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Valitettavasti listalla ei ole vaihtoehtoja saatavilla。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">Listalla ei ole vaihtoehtoja saatavilla。Harkitse suodattimen muuttamista。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Lisäkohteita ei ole saatavilla。</span>',
    					one: '<span class="monster-badge-primary-pill">1 lisäkohde on saatavilla。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} lisäkohdetta on saatavilla。</span>',
    				},
    			};
    
    		case "no":
    			return {
    				"cannot-be-loaded": "Kan ikke lastes",
    				"no-options-available": "Ingen alternativer tilgjengelig。",
    				"click-to-load-options": "Klikk for å laste alternativer。",
    				"select-an-option": "Velg et alternativ",
    				"summary-text": {
    					zero: "Ingen oppføringer ble valgt",
    					one: '<span class="monster-badge-primary-pill">1</span> oppføring valgt',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> oppføringer valgt',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Dessverre er det ingen alternativer tilgjengelig i listen。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">Ingen alternativer tilgjengelig på listen。Vurder å endre filteret。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Det er ingen flere poster tilgjengelige。</span>',
    					one: '<span class="monster-badge-primary-pill">Det er 1 ytterligere post tilgjengelig。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">Det er ${count} ytterligere poster tilgjengelig。</span>',
    				},
    			};
    
    		case "cs":
    			return {
    				"cannot-be-loaded": "Nelze načíst",
    				"no-options-available": "Žádné možnosti nejsou k dispozici。",
    				"click-to-load-options": "Klikněte pro načtení možností。",
    				"select-an-option": "Vyberte možnost",
    				"summary-text": {
    					zero: "Žádné položky nebyly vybrány",
    					one: '<span class="monster-badge-primary-pill">1</span> položka vybrána',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> položky vybrány',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Bohužel nejsou k dispozici žádné možnosti v seznamu。</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">V seznamu nejsou k dispozici žádné možnosti。Zvažte změnu filtru。</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">Žádné další položky nejsou k dispozici。</span>',
    					one: '<span class="monster-badge-primary-pill">Je k dispozici 1 další položka。</span>',
    					other:
    						'<span class="monster-badge-primary-pill">K dispozici je ${count} dalších položek。</span>',
    				},
    			};
    
    		default:
    			// Fallback to English if locale.language is unrecognized
    			return {
    				"cannot-be-loaded": "Cannot be loaded",
    				"no-options-available": "No options available.",
    				"click-to-load-options": "Click to load options.",
    				"select-an-option": "Select an option",
    				"summary-text": {
    					zero: "No entries were selected",
    					one: '<span class="monster-badge-primary-pill">1</span> entry was selected',
    					other:
    						'<span class="monster-badge-primary-pill">${count}</span> entries were selected',
    				},
    				"no-options":
    					'<span class="monster-badge-error-pill">Unfortunately, there are no options available in the list.</span>',
    				"no-options-found":
    					'<span class="monster-badge-error-pill">No options are available in the list. Please consider modifying the filter.</span>',
    				total: {
    					zero: '<span class="monster-badge-primary-pill">No additional entries are available.</span>',
    					one: '<span class="monster-badge-primary-pill">1 additional entry is available.</span>',
    					other:
    						'<span class="monster-badge-primary-pill">${count} additional entries are available.</span>',
    				},
    			};
    	}
    }
    
    /**
     * @private
     */
    function lookupSelection() {
    	const self = this;
    
    	const observer = new IntersectionObserver((entries, obs) => {
    		for (const entry of entries) {
    			if (entry.isIntersecting) {
    
    				obs.disconnect(); // Only observe once
    
    				setTimeout(() => {
    					const selection = self.getOption("selection");
    					if (selection.length === 0) {
    						return;
    					}
    
    					if (self[isLoadingSymbol] === true) {
    						return;
    					}
    
    					if (self[lazyLoadDoneSymbol] === true) {
    						return;
    					}
    
    					let url = self.getOption("url");
    					const lookupUrl = self.getOption("lookup.url");
    					if (lookupUrl !== null) {
    						url = lookupUrl;
    					}
    
    					self[cleanupOptionsListSymbol] = false;
    
    					if (self.getOption("lookup.grouping") === true) {
    						filterFromRemoteByValue
    							.call(
    								self,
    								url,
    								selection.map((s) => s?.["value"]),
    							)
    							.catch((e) => {
    								addErrorAttribute(self, e);
    							});
    						return;
    					}
    
    					for (const s of selection) {
    						if (s?.["value"]) {
    							filterFromRemoteByValue.call(self, url, s["value"]).catch((e) => {
    								addErrorAttribute(self, e);
    							});
    						}
    					}
    				}, 100);
    			}
    		}
    	}, { threshold: 0.1 });
    
    	// Beobachte das Element selbst (dieses Element muss im DOM sein)
    	observer.observe(self);
    }
    
    
    /**
     *
     * @param url
     * @param controlOptions
     * @returns {Promise<never>|Promise<unknown>}
     */
    function fetchIt(url, controlOptions) {
    	const self = this;
    
    	if (url instanceof URL) {
    		url = url.toString();
    	}
    
    	if (url !== undefined && url !== null) {
    		url = validateString(url);
    	} else {
    		url = this.getOption("url");
    		if (url === null) {
    			return Promise.reject(new Error("No url defined"));
    		}
    	}
    
    	return new Promise((resolve, reject) => {
    		setStatusOrRemoveBadges.call(this, "loading");
    
    		new Processing(10, () => {
    			fetchData
    				.call(this, url)
    				.then((map) => {
    					if (
    						isObject(map) ||
    						isArray(map) ||
    						map instanceof Set ||
    						map instanceof Map
    					) {
    						try {
    							importOptionsIntern.call(self, map);
    						} catch (e) {
    							setStatusOrRemoveBadges.call(this, "error");
    							reject(e);
    							return;
    						}
    
    						this[lastFetchedDataSymbol] = map;
    
    						let result;
    						const selection = this.getOption("selection");
    						let newValue = [];
    						if (selection) {
    							newValue = selection;
    						} else if (this.hasAttribute("value")) {
    							newValue = this.getAttribute("value");
    						}
    
    						result = setSelection.call(this, newValue);
    
    						queueMicrotask(() => {
    							checkOptionState.call(this);
    							setTotalText.call(this);
    							updatePopper.call(this);
    							setStatusOrRemoveBadges.call(this, "closed");
    
    							resolve(result);
    						});
    
    						return;
    					}
    
    					setStatusOrRemoveBadges.call(this, "error");
    					reject(new Error("invalid response"));
    				})
    				.catch((e) => {
    					setStatusOrRemoveBadges.call(this, "error");
    					reject(e);
    				});
    		})
    			.run()
    			.catch((e) => {
    				setStatusOrRemoveBadges.call(this, "error");
    				addErrorAttribute(this, e);
    				reject(e);
    			});
    	});
    }
    
    /**
     * This attribute can be used to pass a URL to this select.
     *
     * ```
     * <monster-select data-monster-url="https://example.com/"></monster-select>
     * ```
     *
     * @private
     * @deprecated 2024-01-21 (you should use data-monster-option-...)
     * @return {object}
     */
    function initOptionsFromArguments() {
    	const options = {};
    
    	const template = this.getAttribute("data-monster-selected-template");
    	if (isString(template)) {
    		if (!options["templateMapping"]) options["templateMapping"] = {};
    
    		switch (template) {
    			case "summary":
    			case "default":
    				options["templateMapping"]["selected"] = getSummaryTemplate();
    				break;
    			case "selected":
    				options["templateMapping"]["selected"] = getSelectionTemplate();
    				break;
    			default:
    				addErrorAttribute(this, "invalid template, use summary or selected");
    		}
    	}
    
    	return options;
    }
    
    /**
     * @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, () => {
    			updatePopper.call(this);
    			delete this[timerCallbackSymbol];
    		});
    	});
    
    	let parent = this.parentNode;
    	while (!(parent instanceof HTMLElement) && parent !== null) {
    		parent = parent.parentNode;
    	}
    
    	if (parent instanceof HTMLElement) {
    		this[resizeObserverSymbol].observe(parent);
    	}
    }
    
    /**
     * @private
     */
    function disconnectResizeObserver() {
    	if (this[resizeObserverSymbol] instanceof ResizeObserver) {
    		this[resizeObserverSymbol].disconnect();
    	}
    }
    
    /**
     * @private
     * @returns {string}
     */
    function getSelectionTemplate() {
    	return `<div data-monster-role="selection" part="selection"
                     data-monster-insert="selection path:selection" role="search"
                ><input type="text" role="searchbox"
                        part="inline-filter" name="inline-filter"
                        data-monster-attributes="placeholder path:placeholder.filter"
                        data-monster-role="filter"
                        autocomplete="off"
                        tabindex="0"
                ><div data-monster-replace="path:messages.control"></div>
                </div>`;
    }
    
    /**
     * @private
     * @returns {string}
     */
    function getSummaryTemplate() {
    	return `<div data-monster-role="selection" role="search" part="summary">
        <input type="text" role="searchbox"
    		   data-monster-attributes="placeholder path:placeholder.filter"
               part="inline-filter" name="inline-filter"
               data-monster-role="filter"
               autocomplete="off"
               tabindex="0"
        >
        <div data-monster-replace="path:messages.selected"></div>
    </div>`;
    }
    
    /**
     * @return {void}
     * @private
     */
    function parseSlotsToOptions() {
    	let options = this.getOption("options");
    	if (!isIterable(options)) {
    		options = [];
    	}
    
    	let counter = 1;
    	getSlottedElements.call(this, "div").forEach((node) => {
    		let value = (counter++).toString();
    		let visibility = "visible";
    
    		if (node.hasAttribute("data-monster-value")) {
    			value = node.getAttribute("data-monster-value");
    		}
    
    		let label = node.outerHTML;
    
    		if (node.style.display === "none") {
    			visibility = "hidden";
    		}
    
    		options.push({
    			value,
    			label,
    			visibility,
    		});
    	});
    
    	this.setOption("options", options);
    }
    
    /**
     * @private
     * @param {*} value
     * @return {*}
     */
    function buildSelectionLabel(value) {
    	const options = this.getOption("options");
    
    	for (let i = 0; i < options.length; i++) {
    		let o = options?.[i];
    		let l, v, v2;
    
    		if (this.getOption("features.useStrictValueComparison") === true) {
    			v = value;
    		} else {
    			v = `${value}`;
    		}
    
    		if (isPrimitive(o) && o === value) {
    			return o;
    		} else if (!isObject(o)) {
    			continue;
    		}
    
    		if (this.getOption("features.useStrictValueComparison") === true) {
    			l = o?.["label"];
    			v2 = o?.["value"];
    		} else {
    			l = `${o?.["label"]}`;
    			v2 = `${o?.["value"]}`;
    		}
    
    		if (v2 === v) {
    			return l;
    		}
    	}
    
    	return undefined;
    }
    
    /**
     * @private
     * @param {*} value
     * @return {string}
     * @throws {Error} no value found
     */
    function getSelectionLabel(value) {
    	const callback = this.getOption("formatter.selection");
    	if (isFunction(callback)) {
    		const label = callback.call(this, value);
    		if (isString(label)) return label;
    	}
    
    	if (isString(value) || isInteger(value)) {
    		return `${value}`;
    	}
    
    	return this.getOption("labels.cannot-be-loaded", value);
    }
    
    /**
     * @private
     * @param {Event} event
     */
    function handleToggleKeyboardEvents(event) {
    	switch (event?.["code"]) {
    		case "Escape":
    			toggle.call(this);
    			event.preventDefault();
    			break;
    		case "Space":
    			toggle.call(this);
    			event.preventDefault();
    			break;
    		case "ArrowDown":
    			show.call(this);
    			activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
    			event.preventDefault();
    			break;
    		case "ArrowUp":
    			hide.call(this);
    			event.preventDefault();
    			break;
    	}
    }
    
    /**
     * @license AGPLv3
     * @since 1.15.0
     * @private
     * @this CustomElement
     */
    function initOptionObserver() {
    	const self = this;
    
    	self.attachObserver(
    		new Observer(function () {
    			new Processing(() => {
    				try {
    					self.updateI18n();
    				} catch (e) {
    					addErrorAttribute(self, e);
    					setStatusOrRemoveBadges.call(self, "error");
    				}
    				try {
    					areOptionsAvailableAndInit.call(self);
    				} catch (e) {
    					addErrorAttribute(self, e);
    					setStatusOrRemoveBadges.call(self, "error");
    				}
    
    				setSummaryAndControlText.call(self);
    			}).run();
    		}),
    	);
    }
    
    /**
     * @private
     * @returns {Translations}
     */
    function getDefaultTranslation() {
    	const translation = new Translations("en").assignTranslations(
    		this.getOption("labels", {}),
    	);
    
    	try {
    		const doc = getDocumentTranslations();
    		translation.locale = doc.locale;
    	} catch (e) {}
    
    	return translation;
    }
    
    /**
     * @private
     */
    function setTotalText() {
    	if (getFilterMode.call(this) !== FILTER_MODE_REMOTE) {
    		return;
    	}
    
    	if (this.getOption("features.showRemoteInfo") !== true) {
    		return;
    	}
    
    	const count = this.getOption("options").length;
    	const total = Number.parseInt(this.getOption("total"));
    
    	if (Number.isNaN(total)) {
    		this.setOption("messages.total", "");
    		return;
    	}
    
    	const translations = getDefaultTranslation.call(this);
    	const text = translations.getPluralRuleText("total", total, "");
    
    	const diff = total - count;
    	if (diff <= 0) {
    		this.setOption("messages.total", "");
    		return;
    	}
    
    	const selectedText = new Formatter({
    		count: String(diff),
    	}).format(text);
    
    	this.setOption("messages.total", selectedText);
    }
    
    /**
     * @private
     * @return {string|*}
     */
    function setSummaryAndControlText() {
    	const translations = getDefaultTranslation.call(this);
    	const selections = this.getOption("selection");
    
    	const text = translations.getPluralRuleText(
    		"summary-text",
    		selections.length,
    		"",
    	);
    
    	const selectedText = new Formatter({
    		count: String(selections.length),
    	}).format(text);
    
    	this.setOption("messages.selected", selectedText);
    
    	const current = this.getOption("messages.control");
    	const msg = this.getOption("labels.select-an-option");
    
    	if (
    		current === "" ||
    		current === undefined ||
    		current === msg ||
    		current === null
    	) {
    		if (selections.length === 0) {
    			this.setOption("messages.control", msg);
    		} else {
    			this.setOption("messages.control", "");
    		}
    	}
    }
    
    /**
     * @private
     * @return {NodeList}
     */
    function getOptionElements() {
    	return this[optionsElementSymbol].querySelectorAll(
    		`[${ATTRIBUTE_ROLE}=option]`,
    	);
    }
    
    /**
     * With the help of this filter callback, values can be filtered out. Only if the filter function returns true, the value is taken for the map.
     *
     * @callback function
     * @param {*} value Value
     * @param {string} key  Key
     * @see Monster.Data.buildMap
     */
    
    /**
     *
     * @callback Monster.Components.Form~formatterSelectionCallback
     * @param {*} value Value
     * @return {string|undefined}
     * @see Monster.Data.buildMap
     */
    
    /**
     * @private
     */
    function calcAndSetOptionsDimension() {
    	const options = getOptionElements.call(this);
    	const container = this[optionsElementSymbol];
    	if (!(container instanceof HTMLElement && options instanceof NodeList)) {
    		return;
    	}
    
    	let visible = 0;
    	let optionHeight = 0;
    	const max = this.getOption("showMaxOptions", 10);
    
    	let scrollFlag = false;
    	for (const [, option] of Object.entries(options)) {
    		const computedStyle = getGlobal().getComputedStyle(option);
    		if (computedStyle.display === "none") continue;
    
    		let h = option.getBoundingClientRect().height;
    		h += parseInt(computedStyle.getPropertyValue("margin-top"), 10);
    		h += parseInt(computedStyle.getPropertyValue("margin-bottom"), 10);
    		optionHeight += h;
    
    		visible++;
    
    		if (visible > max) {
    			break;
    		}
    	}
    
    	if (visible > max) {
    		visible = max;
    		scrollFlag = true;
    	}
    
    	if (visible === 0) {
    		if (getFilterMode.call(this) === FILTER_MODE_DISABLED) {
    			this.setOption(
    				"messages.emptyOptions",
    				this.getOption("labels.no-options-available"),
    			);
    		} else {
    			this.setOption(
    				"messages.emptyOptions",
    				this.getOption("labels.no-options-found"),
    			);
    		}
    
    		let classes = new TokenList(this.getOption("classes.noOptions"));
    		classes.remove("d-none");
    		this.setOption("classes.noOptions", classes.toString());
    	} else {
    		let classes = new TokenList(this.getOption("classes.noOptions"));
    		classes.add("d-none");
    		this.setOption("classes.noOptions", classes.toString());
    	}
    
    	const styles = getGlobal().getComputedStyle(this[optionsElementSymbol]);
    	let padding = parseInt(styles.getPropertyValue("padding-top"), 10);
    	padding += parseInt(styles.getPropertyValue("padding-bottom"), 10);
    
    	let margin = parseInt(styles.getPropertyValue("margin-top"), 10);
    	margin += parseInt(styles.getPropertyValue("margin-bottom"), 10);
    
    	const containerHeight = optionHeight + padding + margin;
    	container.style.height = `${containerHeight}px`;
    
    	if (scrollFlag === true) {
    		container.style.overflowY = "scroll";
    	} else {
    		container.style.overflowY = "auto";
    	}
    
    	const domRect = this[controlElementSymbol].getBoundingClientRect();
    
    	this[popperElementSymbol].style.width = `${domRect.width}px`;
    	container.style.overflowX = "auto";
    }
    
    /**
     * @private
     * @param {number} direction
     * @throws {Error} no shadow-root is defined
     */
    function activateCurrentOption(direction) {
    	if (!this.shadowRoot) {
    		throw new Error("no shadow-root is defined");
    	}
    
    	let focused = this.shadowRoot.querySelector(`[${ATTRIBUTE_PREFIX}focused]`);
    
    	if (
    		!(focused instanceof HTMLElement) ||
    		focused.matches("[data-monster-visibility=hidden]")
    	) {
    		for (const [, e] of Object.entries(
    			this.shadowRoot.querySelectorAll(`[${ATTRIBUTE_ROLE}=option]`),
    		)) {
    			if (e.matches("[data-monster-visibility=visible]")) {
    				focused = e;
    				break;
    			}
    		}
    	} else {
    		if (direction === FOCUS_DIRECTION_DOWN) {
    			while (focused.nextSibling) {
    				focused = focused.nextSibling;
    
    				if (
    					focused instanceof HTMLElement &&
    					focused.hasAttribute(ATTRIBUTE_ROLE) &&
    					focused.getAttribute(ATTRIBUTE_ROLE) === "option" &&
    					focused.matches("[data-monster-visibility=visible]") &&
    					focused.matches(":not([data-monster-filtered=true])")
    				) {
    					break;
    				}
    			}
    		} else {
    			let found = false;
    			while (focused.previousSibling) {
    				focused = focused.previousSibling;
    				if (
    					focused instanceof HTMLElement &&
    					focused.hasAttribute(ATTRIBUTE_ROLE) &&
    					focused.getAttribute(ATTRIBUTE_ROLE) === "option" &&
    					focused.matches("[data-monster-visibility=visible]") &&
    					focused.matches(":not([data-monster-filtered=true])")
    				) {
    					found = true;
    					break;
    				}
    			}
    			if (found === false) {
    				focusFilter.call(this);
    			}
    		}
    	}
    
    	new Processing(() => {
    		if (focused instanceof HTMLElement) {
    			this.shadowRoot
    				.querySelectorAll(`[${ATTRIBUTE_PREFIX}focused]`)
    				.forEach((e) => {
    					e.removeAttribute(`${ATTRIBUTE_PREFIX}focused`);
    				});
    
    			focused.focus();
    			focused.setAttribute(`${ATTRIBUTE_PREFIX}focused`, true);
    		}
    	})
    		.run()
    		.catch((e) => {
    			addErrorAttribute(this, e);
    		});
    }
    
    /**
     * @private
     */
    function filterOptions() {
    	new Processing(() => {
    		let filterValue;
    
    		switch (this.getOption("filter.position")) {
    			case FILTER_POSITION_INLINE:
    				if (this[inlineFilterElementSymbol] instanceof HTMLElement) {
    					filterValue = this[inlineFilterElementSymbol].value.toLowerCase();
    				} else {
    					return;
    				}
    
    				break;
    			case FILTER_POSITION_POPPER:
    			default:
    				if (this[popperFilterElementSymbol] instanceof HTMLInputElement) {
    					filterValue = this[popperFilterElementSymbol].value.toLowerCase();
    				} else {
    					return;
    				}
    		}
    
    		const options = this.getOption("options");
    		for (const [i, option] of Object.entries(options)) {
    			if (option.label.toLowerCase().indexOf(filterValue) === -1) {
    				this.setOption(`options.${i}.filtered`, "true");
    			} else {
    				this.setOption(`options.${i}.filtered`, undefined);
    			}
    		}
    	})
    		.run()
    		.then(() => {
    			new Processing(100, () => {
    				calcAndSetOptionsDimension.call(this);
    				focusFilter.call(this);
    			})
    				.run()
    				.catch((e) => {
    					addErrorAttribute(this, e);
    				});
    		})
    		.catch((e) => {
    			addErrorAttribute(this, e);
    		});
    }
    
    /**
     * @private
     * @param {Event} event
     */
    function handleFilterKeyboardEvents(event) {
    	const shiftKey = event?.["shiftKey"];
    
    	switch (event?.["code"]) {
    		case "Tab":
    			activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
    			event.preventDefault();
    			break;
    		case "Escape":
    			toggle.call(this);
    			event.preventDefault();
    			break;
    		case "Tab" && shiftKey === true:
    		case "ArrowUp":
    			activateCurrentOption.call(this, FOCUS_DIRECTION_UP);
    			event.preventDefault();
    			break;
    		case "Tab" && !shiftKey:
    		case "ArrowDown":
    			activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
    			event.preventDefault();
    			break;
    		default:
    			if (
    				this.getOption("features.lazyLoad") === true &&
    				this[lazyLoadDoneSymbol] !== true
    			) {
    				this.click();
    			}
    
    			handleFilterKeyEvents.call(this);
    	}
    }
    
    /**
     * Method handleFilterKeyEvents is used to handle filter key events.
     * Debounce is used to prevent multiple calls.
     *
     * @function
     * @name handleFilterKeyEvents
     *
     * @private
     * @return {void} This method does not return anything.
     */
    function handleFilterKeyEvents() {
    	if (this[keyFilterEventSymbol] instanceof DeadMansSwitch) {
    		try {
    			this[keyFilterEventSymbol].touch();
    			return;
    		} catch (e) {
    			delete this[keyFilterEventSymbol];
    		}
    	}
    
    	this[keyFilterEventSymbol] = new DeadMansSwitch(200, () => {
    		if (getFilterMode.call(this) !== FILTER_MODE_REMOTE) {
    			filterOptions.call(this);
    		} else {
    			this[cleanupOptionsListSymbol] = true;
    
    			filterFromRemote.call(this).catch((e) => {
    				addErrorAttribute(this, e);
    			});
    		}
    
    		delete this[keyFilterEventSymbol];
    	});
    }
    
    /**
     * @private
     */
    function filterFromRemote() {
    	if (
    		!(this[inlineFilterElementSymbol] instanceof HTMLElement) &&
    		!(this[popperFilterElementSymbol] instanceof HTMLElement)
    	) {
    		return Promise.reject(new Error("Missing Filter Element."));
    	}
    
    	show.call(this);
    
    	const url = this.getOption("url");
    	if (!url) {
    		addErrorAttribute(this, "Missing URL for Remote Filter.");
    		return Promise.reject(new Error("Missing URL for Remote Filter."));
    	}
    
    	let filterValue;
    	let showFlag = false;
    
    	switch (this.getOption("filter.position")) {
    		case FILTER_POSITION_INLINE:
    			if (this[inlineFilterElementSymbol] instanceof HTMLElement) {
    				filterValue = this[inlineFilterElementSymbol].value.toLowerCase();
    			}
    			showFlag = true;
    
    			break;
    		case FILTER_POSITION_POPPER:
    		default:
    			if (this[popperFilterElementSymbol] instanceof HTMLInputElement) {
    				filterValue = this[popperFilterElementSymbol].value.toLowerCase();
    			}
    	}
    
    	return filterFromRemoteByValue.call(this, url, filterValue, showFlag);
    }
    
    /**
     * @private
     * @param url
     * @param value
     * @returns {string}
     */
    function formatURL(url, value) {
    	if (value === undefined || value === null || value === "") {
    		value = this.getOption("filter.defaultValue");
    		if (value === undefined || value === null) {
    			value = disabledRequestMarker.toString();
    		}
    	}
    
    	const formatter = new Formatter({ filter: encodeURI(value) });
    	const openMarker = this.getOption("filter.marker.open");
    	let closeMarker = this.getOption("filter.marker.close");
    	if (!closeMarker) {
    		closeMarker = openMarker;
    	}
    
    	if (openMarker && closeMarker) {
    		formatter.setMarker(openMarker, closeMarker);
    	}
    
    	return formatter.format(url);
    }
    
    /**
     * Formats a given URL by applying specific filters and markers.
     *
     * @param {string} optionUrl The URL to format.
     * @param {string} [value] The filter value to apply. If not provided, it attempts to retrieve a default value.
     * @param {boolean} [openPopper] Flag indicating whether to open the popper.
     * @return {string} The formatted URL with the applied filters and markers.
     */
    function filterFromRemoteByValue(optionUrl, value, openPopper) {
    	return new Processing(() => {
    		let url = formatURL.call(this, optionUrl, value);
    		if (url.indexOf(disabledRequestMarker.toString()) !== -1) {
    			return;
    		}
    
    		fetchIt
    			.call(this, url, {
    				disableHiding: true,
    			})
    			.then(() => {
    				checkOptionState.call(this);
    				if (openPopper === true) {
    					show.call(this);
    				}
    			})
    			.catch((e) => {
    				throw e;
    			});
    	})
    		.run()
    		.catch((e) => {
    			throw e;
    		});
    }
    
    /**
     * @param {Event} event
     * @private
     */
    function handleOptionKeyboardEvents(event) {
    	const shiftKey = event?.["shiftKey"];
    
    	switch (event?.["code"]) {
    		case "Escape":
    			toggle.call(this);
    			event.preventDefault();
    			break;
    		case "Enter":
    		case "Space":
    			const path = event.composedPath();
    			const element = path?.[0];
    			if (element instanceof HTMLElement) {
    				const input = element.getElementsByTagName("input");
    				if (!input) {
    					return;
    				}
    				fireEvent(input, "click");
    			}
    			event.preventDefault();
    			break;
    
    		case "Tab" && shiftKey === true:
    		case "ArrowUp":
    			activateCurrentOption.call(this, FOCUS_DIRECTION_UP);
    			event.preventDefault();
    			break;
    
    		case "Tab" && !shiftKey:
    		case "ArrowLeft":
    		case "ArrowRight":
    			// handled by tree select
    			break;
    		case "ArrowDown":
    			activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
    			event.preventDefault();
    			break;
    		default:
    			const p = event.composedPath();
    			if (p?.[0] instanceof HTMLInputElement) {
    				return;
    			}
    			focusFilter.call(this);
    			break;
    	}
    }
    
    /**
     * @private
     * @return {string}
     */
    function getFilterMode() {
    	switch (this.getOption("filter.mode")) {
    		case FILTER_MODE_OPTIONS:
    			return FILTER_MODE_OPTIONS;
    		case FILTER_MODE_REMOTE:
    			return FILTER_MODE_REMOTE;
    		default:
    			return FILTER_MODE_DISABLED;
    	}
    }
    
    /**
     * @private
     */
    function blurFilter() {
    	if (!(this[inlineFilterElementSymbol] instanceof HTMLElement)) {
    		return;
    	}
    
    	if (getFilterMode.call(this) === FILTER_MODE_DISABLED) {
    		return;
    	}
    
    	this[popperFilterContainerElementSymbol].classList.remove("active");
    	this[popperFilterContainerElementSymbol].blur();
    
    	this[inlineFilterElementSymbol].classList.remove("active");
    	this[inlineFilterElementSymbol].blur();
    }
    
    /**
     * @private
     * @param focusOptions
     */
    function focusPopperFilter(focusOptions) {
    	this[popperFilterContainerElementSymbol].classList.remove("d-none");
    	this[popperFilterElementSymbol].classList.add("active");
    	this[inlineFilterElementSymbol].classList.remove("active");
    	this[inlineFilterElementSymbol].classList.add("d-none");
    
    	if (!(this[popperFilterElementSymbol] instanceof HTMLElement)) {
    		addErrorAttribute(this, "Missing Popper Filter Element.");
    		return;
    	}
    
    	// visibility is set to visible, because focus() does not work on invisible elements
    	// and the class definition is assigned later in the processing
    	setTimeout(() => {
    		if (focusOptions === undefined || focusOptions === null) {
    			this[popperFilterElementSymbol].focus();
    		} else {
    			this[popperFilterElementSymbol].focus(focusOptions);
    		}
    	}, 100);
    }
    
    /**
     * @private
     * @param focusOptions
     */
    function focusInlineFilter(focusOptions) {
    	const options = this.getOption("options");
    	if (
    		(!isArray(options) || options.length === 0) &&
    		getFilterMode.call(this) !== FILTER_MODE_REMOTE
    	) {
    		return;
    	}
    
    	this[popperFilterContainerElementSymbol].classList.add("d-none");
    	this[inlineFilterElementSymbol].classList.add("active");
    	this[inlineFilterElementSymbol].classList.remove("d-none");
    
    	// visibility is set to visible, because focus() does not work on invisible elements
    	// and the class definition is assigned later in the processing
    	setTimeout(() => {
    		if (focusOptions === undefined || focusOptions === null) {
    			this[inlineFilterElementSymbol].focus();
    		} else {
    			this[inlineFilterElementSymbol].focus(focusOptions);
    		}
    	}, 100);
    }
    
    /**
     * @private
     */
    function focusFilter(focusOptions) {
    	if (getFilterMode.call(this) === FILTER_MODE_DISABLED) {
    		this[popperFilterContainerElementSymbol].classList.add("d-none");
    		this[inlineFilterElementSymbol].classList.add("d-none");
    		return;
    	}
    
    	if (this.getOption("filter.position") === FILTER_POSITION_INLINE) {
    		return focusInlineFilter.call(this, focusOptions);
    	}
    
    	return focusPopperFilter.call(this, focusOptions);
    }
    
    /**
     * @private
     * @return {array}
     * @throws {Error} no shadow-root is defined
     * @throws {Error} unsupported type
     */
    function gatherState() {
    	const type = this.getOption("type");
    	if (["radio", "checkbox"].indexOf(type) === -1) {
    		throw new Error("unsupported type");
    	}
    
    	if (!this.shadowRoot) {
    		throw new Error("no shadow-root is defined");
    	}
    
    	const selection = [];
    	const elements = this.shadowRoot.querySelectorAll(
    		`input[type=${type}]:checked`,
    	);
    
    	for (const e of elements) {
    		selection.push({
    			label: getSelectionLabel.call(this, e.value),
    			value: e.value,
    		});
    	}
    
    	setSelection
    		.call(this, selection)
    		.then(() => {})
    		.catch((e) => {
    			addErrorAttribute(this, e);
    		});
    
    	if (this.getOption("features.closeOnSelect") === true) {
    		hide.call(this);
    	}
    
    	return this;
    }
    
    /**
     * @private
     * @throws {Error} no shadow-root is defined
     * @throws {Error} unsupported type
     */
    function clearSelection() {
    	const type = this.getOption("type");
    	if (["radio", "checkbox"].indexOf(type) === -1) {
    		throw new Error("unsupported type");
    	}
    
    	if (!this.shadowRoot) {
    		throw new Error("no shadow-root is defined");
    	}
    
    	setSelection
    		.call(this, [])
    		.then(() => {})
    		.catch((e) => {
    			addErrorAttribute(this, e);
    		});
    }
    
    const optionAvailableDeadManSymbol = Symbol("optionAvailableDeadManSymbol");
    
    /**
     * @private
     */
    function areOptionsAvailableAndInit() {
    	// against flickering
    	if (this[optionAvailableDeadManSymbol] instanceof DeadMansSwitch) {
    		try {
    			this[optionAvailableDeadManSymbol].touch();
    			return;
    		} catch (e) {
    			delete this[optionAvailableDeadManSymbol];
    		}
    	}
    
    	this[optionAvailableDeadManSymbol] = new DeadMansSwitch(200, () => {
    		areOptionsAvailableAndInitInternal.call(this);
    		delete this[timerCallbackSymbol];
    	});
    }
    
    function areOptionsAvailableAndInitInternal() {
    	// prevent multiple calls
    	if (this[areOptionsAvailableAndInitSymbol] === undefined) {
    		this[areOptionsAvailableAndInitSymbol] = 0;
    	}
    
    	if (this[areOptionsAvailableAndInitSymbol] > 0) {
    		this[areOptionsAvailableAndInitSymbol]--;
    		return true;
    	}
    
    	this[areOptionsAvailableAndInitSymbol]++;
    
    	const options = this.getOption("options");
    
    	if (
    		options === undefined ||
    		options === null ||
    		(isArray(options) && options.length === 0)
    	) {
    		setStatusOrRemoveBadges.call(this, "empty");
    
    		let msg = this.getOption("labels.no-options-available");
    
    		if (
    			this.getOption("url") !== null &&
    			this.getOption("features.lazyLoad") === true &&
    			this[lazyLoadDoneSymbol] !== true
    		) {
    			msg = this.getOption("labels.click-to-load-options");
    		}
    
    		if (this.getOption("filter.mode") === FILTER_MODE_REMOTE) {
    			msg = "";
    		} else {
    			this.setOption("filter.defaultValue", undefined);
    		}
    
    		if (
    			containsAttributeToken(this[controlElementSymbol], "class", "open") ===
    			true
    		) {
    			msg = "";
    		}
    
    		this.setOption("messages.control", msg);
    		this.setOption("messages.summary", "");
    
    		if (this.getOption("features.emptyValueIfNoOptions") === true) {
    			this.value = "";
    		}
    		addErrorAttribute(this, "No options available.");
    		return false;
    	}
    
    	const selections = this.getOption("selection");
    	if (
    		selections === undefined ||
    		selections === null ||
    		selections.length === 0
    	) {
    		this.setOption(
    			"messages.control",
    			this.getOption("labels.select-an-option"),
    		);
    	} else {
    		this.setOption("messages.control", "");
    	}
    
    	this.setOption("messages.summary", setSummaryAndControlText.call(this));
    
    	let updated = false;
    	let valueCounter = 1;
    	for (const option of options) {
    		if (option?.visibility === undefined) {
    			option.visibility = "visible";
    			updated = true;
    		}
    
    		if (option?.value === undefined && option?.label === undefined) {
    			option.value = `${valueCounter++}`;
    			option.label = option.value;
    			updated = true;
    			continue;
    		}
    
    		if (option?.value === undefined) {
    			option.value = option.label;
    			updated = true;
    		}
    
    		if (option?.label === undefined) {
    			option.label = option.value;
    			updated = true;
    		}
    	}
    
    	if (updated) {
    		this.setOption("options", options);
    	}
    
    	setStatusOrRemoveBadges.call(this);
    
    	removeErrorAttribute(this, "No options available.");
    	return true;
    }
    
    /**
     * @private
     * @throws {Error} no shadow-root is defined
     */
    function checkOptionState() {
    	if (!this.shadowRoot) {
    		throw new Error("no shadow-root is defined");
    	}
    
    	const elements = this.shadowRoot.querySelectorAll(
    		`[${ATTRIBUTE_ROLE}=option] input`,
    	);
    
    	let selection = this.getOption("selection");
    	if (!isArray(selection)) {
    		selection = [];
    	}
    
    	const checkedValues = selection.map((a) => {
    		return a.value;
    	});
    
    	for (const e of elements) {
    		if (checkedValues.indexOf(e.value) !== -1) {
    			if (e.checked !== true) e.checked = true;
    		} else {
    			if (e.checked !== false) e.checked = false;
    		}
    	}
    }
    
    /**
     * @private
     * @param {*} value
     * @return {Object}
     */
    function convertValueToSelection(value) {
    	const selection = [];
    
    	if (isString(value)) {
    		value = value
    			.split(",")
    			.map((a) => {
    				return a.trim();
    			})
    			.filter((a) => {
    				return a !== "";
    			});
    	}
    
    	if (isString(value) || isInteger(value)) {
    		selection.push({
    			label: getSelectionLabel.call(this, value),
    			value: value,
    		});
    	} else if (isArray(value)) {
    		for (const v of value) {
    			selection.push({
    				label: getSelectionLabel.call(this, v),
    				value: v,
    			});
    		}
    
    		value = value.join(",");
    	} else {
    		throw new Error("unsupported type");
    	}
    
    	return {
    		selection: selection,
    		value: value,
    	};
    }
    
    /**
     * @private
     * @param {array} selection
     * @return {string}
     */
    function convertSelectionToValue(selection) {
    	const value = [];
    
    	if (isArray(selection)) {
    		for (const obj of selection) {
    			const v = obj?.["value"];
    			if (v !== undefined) value.push(`${v}`);
    		}
    	}
    
    	if (value.length === 0) {
    		return "";
    	} else if (value.length === 1) {
    		const v = value.pop();
    		if (v === undefined) return "";
    		if (v === null) return "";
    		return `${v}`;
    	}
    
    	return value.join(",");
    }
    
    /**
     * @private
     * @param value
     * @returns {boolean}
     */
    function isValueIsEmpty(value) {
    	let equivalents = this.getOption("empty.equivalents");
    	if (!isArray(equivalents)) {
    		if (equivalents === undefined) {
    			return false;
    		}
    		equivalents = [equivalents];
    	}
    
    	return equivalents.indexOf(value) !== -1;
    }
    
    /**
     * @private
     * @param value
     * @returns {*}
     */
    function isValueIsEmptyThenGetNormalize(value) {
    	let emptyDefault = null;
    	if (this.getOption("type") === "checkbox") {
    		emptyDefault = this.getOption("empty.defaultValueCheckbox");
    	} else {
    		emptyDefault = this.getOption("empty.defaultValueRadio");
    	}
    
    	if (isValueIsEmpty.call(this, value)) {
    		return emptyDefault;
    	}
    
    	return value;
    }
    
    /**
     * @private
     * @param selection
     * @returns {Promise<unknown | void>}
     */
    function setSelection(selection) {
    	const self = this;
    
    	selection = isValueIsEmptyThenGetNormalize.call(this, selection);
    
    	if (isString(selection) || isInteger(selection)) {
    		const result = convertValueToSelection.call(this, selection);
    		selection = result?.selection;
    	}
    
    	validateArray(selection);
    
    	let resultSelection = [];
    	for (let i = 0; i < selection.length; i++) {
    		if (isValueIsEmpty.call(this, selection[i].value)) {
    			continue;
    		}
    
    		let l = getSelectionLabel.call(this, selection[i].value);
    		if (l === selection[i].value) {
    			l = selection[i].label;
    		}
    
    		resultSelection.push({
    			label: l,
    			value: selection[i].value,
    		});
    	}
    
    	selection = resultSelection;
    
    	this.setOption("selection", selection);
    
    	checkOptionState.call(this);
    	setSummaryAndControlText.call(this);
    
    	try {
    		this?.setFormValue(this.value);
    	} catch (e) {
    		addErrorAttribute(this, e);
    	}
    
    	fireCustomEvent(this, "monster-selected", {
    		selection,
    	});
    
    	fireEvent(this, "change"); // https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/291
    
    	if (this[runLookupOnceSymbol] !== true && selection.length > 0) {
    		this[runLookupOnceSymbol] = true;
    
    		const lazyLoadFlag =
    			this.getOption("features.lazyLoad") && this[lazyLoadDoneSymbol] !== true;
    		const remoteFilterFlag = getFilterMode.call(this) === FILTER_MODE_REMOTE;
    		if (lazyLoadFlag || remoteFilterFlag) {
    			lookupSelection.call(self);
    		}
    	}
    
    	return new Processing(() => {
    		const CLASSNAME = "selected";
    
    		if (!this.shadowRoot) {
    			throw new Error("no shadow-root is defined");
    		}
    
    		const notSelected = this.shadowRoot.querySelectorAll(":not(:checked)");
    
    		if (notSelected) {
    			notSelected.forEach((node) => {
    				const parent = node.closest(`[${ATTRIBUTE_ROLE}=option]`);
    				if (parent) {
    					parent.classList.remove(CLASSNAME);
    				}
    			});
    		}
    
    		const selected = this.shadowRoot.querySelectorAll(":checked");
    
    		if (selected) {
    			selected.forEach((node) => {
    				const parent = node.closest(`[${ATTRIBUTE_ROLE}=option]`);
    				if (parent) {
    					parent.classList.add(CLASSNAME);
    				}
    			});
    		}
    	})
    		.run()
    		.catch((e) => {
    			addErrorAttribute(this, e);
    		});
    }
    
    /**
     * @private
     * @param {string} url
     * @return {Promise}
     * @throws {TypeError} the result cannot be parsed
     * @throws {TypeError} unsupported response
     */
    function fetchData(url) {
    	const self = this;
    	if (!url) url = this.getOption("url");
    	if (!url) return Promise.resolve();
    
    	const fetchOptions = this.getOption("fetch", {});
    
    	let delayWatch = false;
    
    	// if fetch short time, do not show loading badge, because of flickering
    	requestAnimationFrame(() => {
    		if (delayWatch === true) return;
    		setStatusOrRemoveBadges.call(this, "loading");
    		delayWatch = true;
    	});
    
    	url = formatURL.call(this, url);
    
    	self[isLoadingSymbol] = true;
    	const global = getGlobal();
    	return global
    		.fetch(url, fetchOptions)
    		.then((response) => {
    			self[isLoadingSymbol] = false;
    			delayWatch = true;
    
    			if (!(response.status >= 200 && response.status < 300)) {
    				throw new Error(`HTTP error! status: ${response.status}`);
    			}
    
    			const contentType = response.headers.get("content-type");
    			if (contentType && contentType.indexOf("application/json") !== -1) {
    				return response.text();
    			}
    
    			throw new TypeError(`unsupported response ${contentType}`);
    		})
    		.then((text) => {
    			try {
    				return Promise.resolve(JSON.parse(String(text)));
    			} catch (e) {
    				throw new TypeError("the result cannot be parsed, check the URL");
    			}
    		})
    		.catch((e) => {
    			self[isLoadingSymbol] = false;
    			delayWatch = true;
    			throw e;
    		});
    }
    
    /**
     * @private
     */
    function hide() {
    	this[popperElementSymbol].style.display = "none";
    	setStatusOrRemoveBadges.call(this, "closed");
    	removeAttributeToken(this[controlElementSymbol], "class", "open");
    }
    
    /**
     * @private
     */
    function show() {
    	if (this.getOption("disabled", undefined) === true) {
    		return;
    	}
    
    	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
    		return;
    	}
    
    	setTotalText.call(this);
    
    	focusFilter.call(this);
    
    	const lazyLoadFlag =
    		this.getOption("features.lazyLoad") && this[lazyLoadDoneSymbol] !== true;
    
    	if (lazyLoadFlag === true) {
    		this[lazyLoadDoneSymbol] = true;
    		setStatusOrRemoveBadges.call(this, "loading");
    
    		new Processing(200, () => {
    			this.fetch()
    				.then(() => {
    					checkOptionState.call(this);
    					requestAnimationFrame(() => {
    						show.call(this);
    					});
    				})
    				.catch((e) => {
    					addErrorAttribute(this, e);
    					setStatusOrRemoveBadges.call(this, "error");
    				});
    		})
    			.run()
    			.catch((e) => {
    				addErrorAttribute(this, e);
    				setStatusOrRemoveBadges.call(this, "error");
    			});
    
    		return;
    	}
    
    	initDefaultOptionsFromUrl.call(this);
    
    	const hasPopperFilterFlag =
    		this.getOption("filter.position") === FILTER_POSITION_POPPER &&
    		getFilterMode.call(this) !== FILTER_MODE_DISABLED;
    
    	const options = getOptionElements.call(this);
    	if (options.length === 0 && hasPopperFilterFlag === false) {
    		return;
    	}
    
    	this[popperElementSymbol].style.visibility = "hidden";
    	this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK;
    	setStatusOrRemoveBadges.call(this, "open");
    
    	addAttributeToken(this[controlElementSymbol], "class", "open");
    
    	new Processing(() => {
    		calcAndSetOptionsDimension.call(this);
    		focusFilter.call(this);
    		this[popperElementSymbol].style.removeProperty("visibility");
    		updatePopper.call(this);
    	})
    		.run()
    		.catch((e) => {
    			addErrorAttribute(this, e);
    		});
    }
    
    function initDefaultOptionsFromUrl() {
    	const url = this.getOption("filter.defaultOptionsUrl");
    	if (!url) {
    		return;
    	}
    
    	this.setOption("filter.defaultOptionsUrl", null);
    
    	fetchData
    		.call(this, url)
    		.then((data) => {
    			this[cleanupOptionsListSymbol] = false;
    			importOptionsIntern.call(this, data);
    			setStatusOrRemoveBadges.call(this, "open");
    			initTotal.call(this, data);
    		})
    		.catch((e) => {
    			addErrorAttribute(this, e);
    			setStatusOrRemoveBadges.call(this, "error");
    		});
    }
    
    /**
     * @private
     */
    function initTotal() {
    	if (getFilterMode.call(this) !== FILTER_MODE_REMOTE) {
    		return;
    	}
    
    	const url = this.getOption("remoteInfo.url");
    	const mapping = this.getOption("mapping.total");
    
    	if (!isString(mapping) || !isString(url)) {
    		return;
    	}
    
    	getGlobal()
    		.fetch(url)
    		.then((response) => {
    			if (!response.ok) {
    				// Improved status checking using `response.ok`
    				addErrorAttribute(
    					this,
    					`HTTP error status: ${response.status} - ${response.statusText}`,
    				);
    				return;
    			}
    
    			return response.text();
    		})
    		.then((text) => {
    			try {
    				const data = JSON.parse(String(text));
    				const pathfinder = new Pathfinder(data);
    				const total = pathfinder.getVia(mapping);
    
    				if (!isInteger(total)) {
    					addErrorAttribute(this, "total is not an integer");
    					return;
    				}
    
    				this.setOption("total", total);
    			} catch (e) {
    				addErrorAttribute(this, e);
    			}
    		})
    		.catch((e) => {
    			addErrorAttribute(this, e);
    		});
    }
    
    /**
     * @private
     */
    function toggle() {
    	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
    		hide.call(this);
    	} else {
    		show.call(this);
    	}
    }
    
    /**
     * @private
     * @fires monster-selection-removed
     * @fires monster-selection-cleared
     */
    function initEventHandler() {
    	const self = this;
    
    	/**
    	 * @param {Event} event
    	 */
    	self[clearOptionEventHandler] = (event) => {
    		const element = findTargetElementFromEvent(
    			event,
    			ATTRIBUTE_ROLE,
    			"remove-badge",
    		);
    
    		if (element instanceof HTMLElement) {
    			const badge = findClosestByAttribute(element, ATTRIBUTE_ROLE, "badge");
    			if (badge instanceof HTMLElement) {
    				const value = badge.getAttribute(`${ATTRIBUTE_PREFIX}value`);
    
    				let selection = self.getOption("selection");
    				selection = selection.filter((b) => {
    					return value !== b.value;
    				});
    
    				setSelection
    					.call(self, selection)
    					.then(() => {
    						fireCustomEvent(self, "monster-selection-removed", {
    							value,
    						});
    					})
    					.catch((e) => {
    						addErrorAttribute(self, e);
    					});
    			}
    		}
    	};
    
    	/**
    	 * @param {Event} event
    	 */
    	self[closeEventHandler] = (event) => {
    		const path = event.composedPath();
    
    		for (const [, element] of Object.entries(path)) {
    			if (element === self) {
    				return;
    			}
    		}
    		hide.call(self);
    	};
    
    	/**
    	 * @param {Event} event
    	 */
    	self[inputEventHandler] = (event) => {
    		const path = event.composedPath();
    		const element = path?.[0];
    
    		if (element instanceof HTMLElement) {
    			if (
    				element.hasAttribute(ATTRIBUTE_ROLE) &&
    				element.getAttribute(ATTRIBUTE_ROLE) === "option-control"
    			) {
    				fireCustomEvent(self, "monster-change", {
    					type: event.type,
    					value: element.value,
    					checked: element.checked,
    				});
    			} else if (
    				element.hasAttribute(ATTRIBUTE_ROLE) &&
    				element.getAttribute(ATTRIBUTE_ROLE) === "filter"
    			) {
    			}
    		}
    	};
    
    	/**
    	 * @param {Event} event
    	 */
    	self[changeEventHandler] = (event) => {
    		gatherState.call(self);
    		fireCustomEvent(self, "monster-changed", event?.detail);
    	};
    
    	self[keyEventHandler] = (event) => {
    		const path = event.composedPath();
    		const element = path.shift();
    
    		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(self);
    			} else {
    				const e = element.closest(`[${ATTRIBUTE_ROLE}]`);
    				if (e instanceof HTMLElement && e.hasAttribute(ATTRIBUTE_ROLE)) {
    					role = e.getAttribute(ATTRIBUTE_ROLE);
    				}
    			}
    		} else {
    			return;
    		}
    
    		switch (role) {
    			case "filter":
    				handleFilterKeyboardEvents.call(self, event);
    				break;
    			case "option-label":
    			case "option-control":
    			case "option":
    				handleOptionKeyboardEvents.call(self, event);
    				break;
    			case "control":
    			case "toggle":
    				handleToggleKeyboardEvents.call(self, event);
    				break;
    		}
    	};
    
    	const types = self.getOption("toggleEventType", ["click"]);
    
    	for (const [, type] of Object.entries(types)) {
    		self[controlElementSymbol]
    			.querySelector(`[${ATTRIBUTE_ROLE}="container"]`)
    			.addEventListener(type, function (event) {
    				const element = findTargetElementFromEvent(
    					event,
    					ATTRIBUTE_ROLE,
    					"remove-badge",
    				);
    
    				if (element instanceof HTMLElement) {
    					return;
    				}
    
    				toggle.call(self);
    			});
    
    		self[controlElementSymbol]
    			.querySelector(`[${ATTRIBUTE_ROLE}="status-or-remove-badges"]`)
    			.addEventListener(type, function (event) {
    				if (self.getOption("disabled", undefined) === true) {
    					return;
    				}
    
    				const path = event.composedPath();
    				const element = path?.[0];
    				if (element instanceof HTMLElement) {
    					const control = element.closest(
    						`[${ATTRIBUTE_ROLE}="status-or-remove-badges"]`,
    					);
    					if (control instanceof HTMLElement) {
    						if (control.classList.contains("clear")) {
    							clearSelection.call(self);
    
    							fireCustomEvent(self, "monster-selection-cleared", {});
    						} else {
    							const element = findTargetElementFromEvent(
    								event,
    								ATTRIBUTE_ROLE,
    								"remove-badge",
    							);
    							if (element instanceof HTMLElement) {
    								return;
    							}
    
    							toggle.call(self);
    						}
    					}
    				}
    			});
    
    		// badge, selection
    		self.addEventListener(type, self[clearOptionEventHandler]);
    	}
    
    	self.addEventListener("monster-change", self[changeEventHandler]);
    	self.addEventListener("input", self[inputEventHandler]);
    	self.addEventListener("keydown", self[keyEventHandler]);
    
    	const callback = () => {
    		if (this[debounceOptionsMutationObserverSymbol] instanceof DeadMansSwitch) {
    			try {
    				this[debounceOptionsMutationObserverSymbol].touch();
    				return;
    			} catch (e) {
    				delete this[debounceOptionsMutationObserverSymbol];
    			}
    		}
    
    		this[debounceOptionsMutationObserverSymbol] = new DeadMansSwitch(
    			100,
    			() => {
    				checkOptionState.call(self);
    				calcAndSetOptionsDimension.call(self);
    				updatePopper.call(self);
    				delete this[debounceOptionsMutationObserverSymbol];
    			},
    		);
    	};
    
    	const observer = new MutationObserver(callback);
    	observer.observe(self[optionsElementSymbol], {
    		attributes: false,
    		childList: true,
    		subtree: true,
    	});
    
    	return self;
    }
    
    /**
     * @private
     * @return {Select}
     */
    function setStatusOrRemoveBadges(suggestion) {
    	requestAnimationFrame(() => {
    		const selection = this.getOption("selection");
    
    		const clearAllFlag =
    			isArray(selection) &&
    			selection.length > 0 &&
    			this.getOption("features.clearAll") === true;
    
    		const current = this.getOption("classes.statusOrRemoveBadge");
    
    		if (suggestion === "error") {
    			if (current !== "error") {
    				this.setOption("classes.statusOrRemoveBadge", "error");
    			}
    			return;
    		}
    
    		if (this[isLoadingSymbol] === true) {
    			if (current !== "loading") {
    				this.setOption("classes.statusOrRemoveBadge", "loading");
    			}
    			return;
    		}
    
    		if (suggestion === "loading") {
    			if (current !== "loading") {
    				this.setOption("classes.statusOrRemoveBadge", "loading");
    			}
    			return;
    		}
    
    		if (this[controlElementSymbol].classList.contains("open")) {
    			if (current !== "open") {
    				this.setOption("classes.statusOrRemoveBadge", "open");
    			}
    			return;
    		}
    
    		if (clearAllFlag) {
    			if (current !== "clear") {
    				this.setOption("classes.statusOrRemoveBadge", "clear");
    			}
    			return;
    		}
    
    		const options = this.getOption("options");
    		if (
    			options === undefined ||
    			options === null ||
    			(isArray(options) && options.length === 0)
    		) {
    			if (current !== "empty") {
    				this.setOption("classes.statusOrRemoveBadge", "empty");
    			}
    			return;
    		}
    
    		if (suggestion) {
    			if (current !== suggestion) {
    				this.setOption("classes.statusOrRemoveBadge", suggestion);
    			}
    		}
    	});
    }
    
    /**
     * @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[selectionElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}=selection]`,
    	);
    	this[containerElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}=container]`,
    	);
    	this[popperElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}=popper]`,
    	);
    	this[inlineFilterElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}=filter][name="inline-filter"]`,
    	);
    	this[popperFilterElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}=filter][name="popper-filter"]`,
    	);
    	this[popperFilterContainerElementSymbol] =
    		this[popperFilterElementSymbol].parentElement;
    	this[optionsElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}=options]`,
    	);
    	this[noOptionsAvailableElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}="no-options"]`,
    	);
    	this[statusOrRemoveBadgesElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}=status-or-remove-badges]`,
    	);
    
    	this[remoteInfoElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}=remote-info]`,
    	);
    }
    
    /**
     * @private
     */
    function updatePopper() {
    	if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) {
    		return;
    	}
    
    	if (this.getOption("disabled", false) === true) {
    		return;
    	}
    
    	new Processing(() => {
    		calcAndSetOptionsDimension.call(this);
    		positionPopper.call(
    			this,
    			this[controlElementSymbol],
    			this[popperElementSymbol],
    			this.getOption("popper", {}),
    		);
    	})
    		.run()
    		.catch((e) => {
    			addErrorAttribute(this, e);
    		});
    
    	return this;
    }
    
    /**
     * @private
     * @return {string}
     */
    function getTemplate() {
    	// language=HTML
    	return `
            <template id="options">
                <div data-monster-role="option" tabindex="-1"
                     data-monster-attributes="
                     data-monster-filtered path:options.filtered,
                     data-monster-visibility path:options.visibility">
                    <label part="option">
                        <input data-monster-role="option-control"
                               data-monster-attributes="
                type path:type,
                role path:role,
                value path:options.value,
                name path:name,
                part path:type | prefix:option- | suffix: form,
                class path:options.class
                " tabindex="-1">
                        <div data-monster-replace="path:options.label"
                             part="option-label"></div>
                    </label>
                </div>
            </template>
    
            <template id="selection">
                <div data-monster-role="badge"
                     part="badge"
                     data-monster-attributes="
                     data-monster-value path:selection | index:value,
                     class path:classes | index:badge,
            part path:type | suffix:-option | prefix: form-" tabindex="-1">
                    <div data-monster-replace="path:selection | index:label" part="badge-label"
                         data-monster-role="badge-label"></div>
                    <div part="remove-badge" data-monster-select-this
                         data-monster-attributes="class path:features.clear | ?::hidden "
                         data-monster-role="remove-badge" tabindex="-1"></div>
                </div>
            </template>
    
            <slot class="hidden"></slot>
    
            <div data-monster-role="control" part="control" tabindex="0">
                <div data-monster-role="container">
                    \${selected}
                </div>
    
                <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1">
                    <div class="option-filter-control" role="search">
                        <input type="text" role="searchbox"
                               data-monster-attributes="placeholder path:placeholder.filter"
                               part="popper-filter" name="popper-filter"
                               data-monster-role="filter"
                               autocomplete="off"
                               tabindex="0">
                    </div>
                    <div part="content" class="flex" data-monster-replace="path:content">
                        <div part="options" data-monster-role="options" data-monster-insert="options path:options"
                             tabindex="-1"></div>
                    </div>
                    <div part="remote-info"
                         data-monster-role="remote-info"
                         data-monster-attributes="class path:classes.remoteInfo"
                         data-monster-replace="path:messages.total"></div>
                    <div part="no-options" data-monster-role="no-options"
                         data-monster-attributes="class path:classes.noOptions"
                         data-monster-replace="path:messages.emptyOptions"></div>
                </div>
                <div part="status-or-remove-badges" data-monster-role="status-or-remove-badges"
                     data-monster-attributes="class path:classes.statusOrRemoveBadge"></div>
            </div>
        `;
    }
    
    registerCustomElement(Select);