UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

1,592 lines (1,451 loc) 99.3 kB
/** * 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