@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,592 lines (1,451 loc) • 99.3 kB
JavaScript
/**
* 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.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: {
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) {
try {
const result = fetchIt.call(this, url);
if (result instanceof Promise) {
return result;
}
} catch (e) {
addErrorAttribute(this, e);
return Promise.reject(e);
}
}
/**
* @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" na