@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,594 lines (1,442 loc) • 127 kB
JavaScript
/**
* Copyright © Volker Schukai 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 Volker Schukai.
*
* 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 {
resetErrorAttribute,
addErrorAttribute,
removeErrorAttribute,
} from "../../dom/error.mjs";
import {
findTargetElementFromEvent,
fireCustomEvent,
fireEvent,
} from "../../dom/events.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import {
findElementWithSelectorUpwards,
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";
import "../datatable/pagination.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");
const hostElementSymbol = Symbol("hostElement");
const dismissRecordSymbol = Symbol("dismissRecord");
const usesHostDismissSymbol = Symbol("usesHostDismiss");
/**
* 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 paginationElementSymbol = Symbol("paginationElement");
/**
* 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");
const optionsVersionSymbol = Symbol("optionsVersion");
const pendingSelectionSymbol = Symbol("pendingSelection");
const selectionSyncScheduledSymbol = Symbol("selectionSyncScheduled");
const optionsSnapshotSymbol = Symbol("optionsSnapshot");
const strictModeSnapshotSymbol = Symbol("strictModeSnapshot");
const optionsMapSymbol = Symbol("optionsMap");
const optionsMapVersionSnapshotSymbol = Symbol("optionsMapVersionSnapshot");
const selectionVersionSymbol = Symbol("selectionVersion");
const closeOnSelectAutoSymbol = Symbol("closeOnSelectAuto");
/**
* @private
* @type {symbol}
*/
const debounceOptionsMutationObserverSymbol = Symbol(
"debounceOptionsMutationObserver",
);
/**
* @private
* @type {symbol}
*/
const currentPageSymbol = Symbol("currentPage");
/**
* @private
* @type {symbol}
*/
const remoteFilterFirstOpendSymbol = Symbol("remoteFilterFirstOpend");
/**
* @private
* @type {symbol}
*/
const lookupCacheSymbol = Symbol("lookupCache");
/**
* @private
* @type {symbol}
*/
const lookupInProgressSymbol = Symbol("lookupInProgress");
const fetchRequestVersionSymbol = Symbol("fetchRequestVersion");
/**
* @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
* @example /examples/components/form/select-remote-filter Server-side filtering with a remote URL
* @example /examples/components/form/select-remote-pagination Server-side filtering with pagination
* @example /examples/components/form/select-summary-template Using a summary template for selections
*
* @copyright Volker Schukai
* @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();
this[currentPageSymbol] = 1;
this[lookupCacheSymbol] = new Map();
this[lookupInProgressSymbol] = new Map();
this[optionsMapSymbol] = new Map();
this[closeOnSelectAutoSymbol] = true;
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|null} value
* @throws {Error} unsupported type
* @fires monster-selected this event is fired when the selection is set
*/
set value(value) {
const valValue = isValueIsEmptyThenGetNormalize.call(this, value);
const result = convertValueToSelection.call(this, valValue);
setSelection
.call(this, result.selection)
.then(() => {})
.catch((e) => {
addErrorAttribute(this, e);
});
}
/**
* Defines the default configuration options for the monster-control.
* These options can be overridden via the HTML attribute `data-monster-options`.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* @property {string[]} toggleEventType - Array of DOM event names (e.g., ["click", "touch"]) that toggle the dropdown.
* @property {boolean} delegatesFocus - If `true`, 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 a static option list.
* @property {string|string[]} selection - Initial selected value(s), as a string, comma-separated string, or array of strings.
* @property {number} showMaxOptions - Maximum number of visible options before the list becomes scrollable.
* @property {"radio"|"checkbox"} type - Selection mode: "radio" for single selection, "checkbox" for multiple selections.
* @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 {number|null} total - Total number of available options, useful for pagination with remote data.
* @property {Object} lookup - Configuration for fetching initially selected values.
* @property {string|null} lookup.url - URL template with a `${filter}` placeholder to look up selected entries on initialization. Used when `url` is set and either `features.lazyLoad` is active or `filter.mode` is `"remote"`.
* @property {boolean} lookup.grouping - If `true`, all selected values are fetched in a single request; otherwise, a separate request is sent for each value.
* @property {Object} fetch - Configuration for HTTP requests via `fetch`.
* @property {string} fetch.redirect - Fetch redirect mode (e.g., "error", "follow").
* @property {string} fetch.method - HTTP method for fetching options (e.g., "GET", "POST").
* @property {string} fetch.mode - Fetch mode (e.g., "cors", "same-origin").
* @property {string} fetch.credentials - Credentials policy for fetch (e.g., "include", "same-origin").
* @property {Object.<string, string>} fetch.headers - HTTP headers to be sent with every request.
* @property {Object} labels - Text labels for various states and UI elements.
* @property {string} labels.cannot-be-loaded - Message displayed when options cannot be loaded.
* @property {string} labels.no-options-available - Message displayed when no static options are provided.
* @property {string} labels.click-to-load-options - Prompt to load options when `features.lazyLoad` is enabled.
* @property {string} labels.select-an-option - Placeholder text when no selection has been made.
* @property {string} labels.no-options - Message displayed when no options are available from slots or fetch.
* @property {string} labels.no-options-found - Message displayed when the filter yields no matching options.
* @property {string} labels.summary-text.zero - Pluralization template for zero selected entries (e.g., "No entries selected").
* @property {string} labels.summary-text.one - Pluralization template for one selected entry.
* @property {string} labels.summary-text.other - Pluralization template for multiple selected entries.
* @property {Object} features - Toggles to enable/disable functionalities.
* @property {boolean} features.clearAll - Shows a "Clear all" button to reset the selection.
* @property {boolean} features.clear - Shows a "remove" icon on individual selection badges.
* @property {boolean} features.lazyLoad - If `true`, options are fetched from the `url` only when the dropdown is first opened. Ignored if `filter.mode` is `"remote"`.
* @property {boolean} features.closeOnSelect - If `true`, the dropdown closes automatically after a selection is made.
* @property {boolean} features.emptyValueIfNoOptions - Sets the hidden field's value to empty if no options are available.
* @property {boolean} features.storeFetchedData - If `true`, the raw fetched data is stored and can be retrieved via `getLastFetchedData()`.
* @property {boolean} features.useStrictValueComparison - Uses strict comparison (`===`) when matching option values.
* @property {boolean} features.showRemoteInfo - When `filter.mode === "remote"`, displays an info badge indicating that more options may exist on the server.
* @property {Object} remoteInfo - Configuration for the remote info display.
* @property {string|null} remoteInfo.url - URL to fetch the total count of remote options (used when `filter.mode === "remote"`).
* @property {Object} placeholder - Placeholder texts for input fields.
* @property {string} placeholder.filter - Placeholder text for the filter input field.
* @property {Object} filter - Configuration for the filtering functionality.
* @property {string|null} filter.defaultValue - Default filter value for remote requests. An empty string will prevent the initial request.
* @property {"options"|"remote"|"disabled"} filter.mode - Filter mode: `"options"` (client-side), `"remote"` (server-side, `lazyLoad` is ignored), or `"disabled"`.
* @property {"inline"|"popper"} filter.position - Position of the filter input: `"inline"` (inside the control) or `"popper"` (inside the dropdown).
* @property {string|null} filter.defaultOptionsUrl - URL to load an initial list of options when `filter.mode` is `"remote"` and no filter value has been entered.
* @property {Object} filter.marker - Markers for embedding the filter value into the `url` for server-side filtering.
* @property {string} filter.marker.open - Opening marker (e.g., `{`).
* @property {string} filter.marker.close - Closing marker (e.g., `}`).
* @property {Object} templates - HTML templates for the main components.
* @property {string} templates.main - HTML template string for the control's basic structure.
* @property {Object} templateMapping - Mapping of templates for specific use cases.
* @property {string} templateMapping.selected - Template variant for selected items (e.g., for rendering as a badge).
* @property {Object} popper - Configuration for Popper.js to position the dropdown.
* @property {string} popper.placement - Popper.js placement strategy (e.g., "bottom-start").
* @property {Array<string|Object>} popper.middleware - Array of middleware configurations for Popper.js (e.g., ["flip", "offset:1"]).
* @property {Object} mapping - Defines how fetched data is transformed into `options`.
* @property {string} mapping.selector - Path/selector to find the array of items within the fetched data (e.g., "results" for `data.results`).
* @property {string} mapping.labelTemplate - Template for the option label using placeholders (e.g., `Label: ${name}`).
* @property {string} mapping.valueTemplate - Template for the option value using placeholders (e.g., `ID: ${id}`).
* @property {Function|null} mapping.filter - Optional callback function `(item) => boolean` to filter fetched items before processing.
* @property {string|null} mapping.sort - Optional sorting strategy for fetched items. Can be a string like `"asc"`/`"desc"` for label sorting, or a custom function defined as `"run:<code>"` or `"call:<functionName>"`.
* @property {string|null} mapping.total - Path/selector to the total number of items for pagination (e.g., "pagination.total").
* @property {string|null} mapping.currentPage - Path/selector to the current page number.
* @property {string|null} mapping.objectsPerPage - Path/selector to the number of objects per page.
* @property {Object} empty - Handling of empty or undefined values.
* @property {string} empty.defaultValueRadio - Default value for `type="radio"` when no selection exists.
* @property {Array} empty.defaultValueCheckbox - Default value (empty array) for `type="checkbox"`.
* @property {Array} empty.equivalents - Values that are considered "empty" (e.g., `undefined`, `null`, `""`) and are normalized to the default value.
* @property {Object} formatter - Functions for formatting display values.
* @property {Function} formatter.selection - Callback `(value, option) => string` to format the display text of selected values.
* @property {Object} classes - CSS classes for various elements.
* @property {string} classes.badge - CSS class for selection badges.
* @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" message.
* @property {Object} messages - Internal messages for ARIA attributes and screen readers (should not normally be changed).
* @property {string|null} messages.control - Message for the main control element.
* @property {string|null} messages.selected - Message for the selected items area.
* @property {string|null} messages.emptyOptions - Message for an empty options list.
* @property {string|null} messages.total - Message that communicates the total number of options.
*/
get defaults() {
return Object.assign(
{},
super.defaults,
{
toggleEventType: ["click", "touch"],
delegatesFocus: false,
options: [],
selection: [],
showMaxOptions: 40,
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: "}",
},
params: {},
paramsDefaults: {},
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,
currentPage: null,
objectsPerPage: null,
sort: 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),
);
}
/**
* Resets the select control: clears selection, resets options, removes status/errors.
*/
reset() {
try {
hide.call(this);
// Clear the lookup cache
this[lookupCacheSymbol].clear();
this[lookupInProgressSymbol].clear();
setSelection
.call(this, null)
.then(() => {
const lazyLoadFlag = this.getOption("features.lazyLoad");
const remoteFilterFlag =
getFilterMode.call(this) === FILTER_MODE_REMOTE;
if (lazyLoadFlag || remoteFilterFlag) {
this.setOption("options", []);
}
this.setOption("messages.selected", "");
this.setOption("messages.total", "");
this.setOption("messages.summary", "");
this.setOption("total", null);
resetPaginationState.call(this);
resetErrorAttribute(this);
this[lazyLoadDoneSymbol] = false;
this[runLookupOnceSymbol] = false;
checkOptionState.call(this);
calcAndSetOptionsDimension.call(this);
updatePopper.call(this);
initTotal.call(this);
})
.catch((e) => {
addErrorAttribute(this, e);
});
} catch (e) {
addErrorAttribute(this, e);
}
}
/**
* @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 () {
// this is here not the Control, but the ProxyObserver
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();
this[hostElementSymbol] = findElementWithSelectorUpwards(
this,
"monster-host",
);
this[usesHostDismissSymbol] =
this[hostElementSymbol] &&
typeof this[hostElementSymbol].registerDismissable === "function";
if (!this[usesHostDismissSymbol]) {
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);
});
if (this.parentElement?.nodeName === "MONSTER-BUTTON-BAR") {
this.shadowRoot
.querySelector("[data-monster-role=control]")
?.classList.add("in-button-bar");
}
}
/**
* @return {void}
*/
disconnectedCallback() {
super.disconnectedCallback();
if (!this[usesHostDismissSymbol]) {
const document = getDocument();
// close on outside ui-events
for (const [, type] of Object.entries(["click", "touch"])) {
document.removeEventListener(type, this[closeEventHandler]);
}
}
unregisterFromHost.call(this);
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 {object} data Die rohen Daten aus der API-Antwort.
*/
function processAndApplyPaginationData(data) {
if (!this[paginationElementSymbol]) {
return;
}
let dataCount;
const mappingSelector = this.getOption("mapping.selector");
if (isString(mappingSelector)) {
try {
const pathfinder = new Pathfinder(data);
const mapped = pathfinder.getVia(mappingSelector);
if (isArray(mapped)) {
dataCount = mapped.length;
} else if (mapped instanceof Map) {
dataCount = mapped.size;
} else if (isObject(mapped)) {
dataCount = Object.keys(mapped).length;
}
} catch (e) {}
}
const mappingTotal = this.getOption("mapping.total");
const mappingCurrentPage = this.getOption("mapping.currentPage");
const mappingObjectsPerPage = this.getOption("mapping.objectsPerPage");
if (!mappingTotal || !mappingCurrentPage || !mappingObjectsPerPage) {
this.setOption("total", null);
resetPaginationState.call(this);
return;
}
try {
const pathfinder = new Pathfinder(data);
const total = pathfinder.getVia(mappingTotal);
const currentPage = pathfinder.getVia(mappingCurrentPage);
const objectsPerPage = pathfinder.getVia(mappingObjectsPerPage);
if (!isInteger(total)) {
addErrorAttribute(this, "total is not an integer");
this.setOption("total", null);
resetPaginationState.call(this);
return;
}
this.setOption("total", total);
if (total === 0) {
resetPaginationState.call(this);
return;
}
if (
isInteger(currentPage) &&
currentPage > 0 &&
isInteger(objectsPerPage) &&
objectsPerPage > 0
) {
if (
isInteger(dataCount) &&
(dataCount === 0 ||
dataCount > objectsPerPage ||
total < dataCount ||
currentPage > Math.ceil(total / objectsPerPage))
) {
addErrorAttribute(this, "Invalid pagination data.");
this.setOption("total", null);
resetPaginationState.call(this);
return;
}
updatePagination.call(this, total, currentPage, objectsPerPage);
}
} catch (e) {
addErrorAttribute(this, e);
this.setOption("total", null);
resetPaginationState.call(this);
}
}
/**
* @private
* @param {object} data Die rohen Daten aus der API-Antwort.
*/
function processAndApplyRemoteInfoTotal(data) {
const mappingTotal = this.getOption("mapping.total");
if (!isString(mappingTotal)) {
return;
}
try {
const pathfinder = new Pathfinder(data);
const total = pathfinder.getVia(mappingTotal);
if (!isInteger(total)) {
addErrorAttribute(this, "total is not an integer");
this.setOption("total", null);
return;
}
this.setOption("total", total);
// Note: remoteInfo is a lightweight request (count=1). Only update total/message here.
setTotalText.call(this);
} catch (e) {
addErrorAttribute(this, e);
this.setOption("total", null);
}
}
/**
* @private
* @returns {number}
*/
function bumpOptionsVersion() {
if (!isInteger(this[optionsVersionSymbol])) {
this[optionsVersionSymbol] = 0;
}
this[optionsVersionSymbol] += 1;
return this[optionsVersionSymbol];
}
/**
* @private
* @returns {*}
*/
function getSelectionSyncState() {
const attrValue = this.getAttribute("value");
const selection = this.getOption("selection");
const options = this.getOption("options");
const optionsLength = Array.isArray(options) ? options.length : 0;
return { attrValue, selection, optionsLength };
}
/**
* @private
* @param {number} version
*/
function scheduleSelectionSync(version) {
const state = getSelectionSyncState.call(this);
const selectionIsEmpty =
Array.isArray(state.selection) && state.selection.length === 0;
if (state.attrValue === null && selectionIsEmpty) {
return;
}
// Prefer the existing selection when it is already set to avoid
// resetting user changes back to the initial attribute value.
const shouldUseAttrValue = selectionIsEmpty && state.attrValue !== null;
const pending = {
version,
selectionVersion: this[selectionVersionSymbol] || 0,
value: shouldUseAttrValue ? state.attrValue : state.selection,
};
this[pendingSelectionSymbol] = pending;
if (this[selectionSyncScheduledSymbol] === true) {
return;
}
this[selectionSyncScheduledSymbol] = true;
queueMicrotask(() => {
this[selectionSyncScheduledSymbol] = false;
const current = this[pendingSelectionSymbol];
if (!current) {
return;
}
if (current.version !== this[optionsVersionSymbol]) {
return;
}
if (current.selectionVersion !== (this[selectionVersionSymbol] || 0)) {
return;
}
setSelection
.call(this, current.value)
.then(() => {})
.catch((e) => {
addErrorAttribute(this, e);
});
});
}
/**
* @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 sort = mappingOptions?.["sort"];
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 = [];
const currentOptions = this.getOption("options");
if (this[cleanupOptionsListSymbol] !== true) {
options = currentOptions ?? [];
}
this[cleanupOptionsListSymbol] = false;
if (!isIterable(map)) {
throw new Error("map is not iterable");
}
const visibility = "visible";
let entries = Array.from(map.entries());
if (
sort === "false" ||
sort === false ||
sort === null ||
sort === "null" ||
sort === "" ||
sort === "none" ||
sort === "no"
) {
// no sorting
} else if (isString(sort)) {
if (sort.startsWith("run:")) {
const code = sort.replace("run:", "");
const sortFn = new Function("a", "b", "control", code);
entries.sort((a, b) => sortFn(a, b, self));
} else if (sort.startsWith("call:")) {
const parts = sort.split(":");
parts.shift();
const fkt = parts.shift();
switch (fkt) {
case "asc":
entries.sort((a, b) => a[1].localeCompare(b[1]));
break;
case "desc":
entries.sort((a, b) => b[1].localeCompare(a[1]));
break;
default:
addErrorAttribute(this, new Error(`Unknown sort function ${fkt}`));
}
}
} else {
entries.sort((a, b) => {
const la = a[1]?.toString() ?? "";
const lb = b[1]?.toString() ?? "";
return la.localeCompare(lb);
});
}
for (const [value, label] of entries) {
let found = false;
for (const option of options) {
if (option.value === value) {
option.label = label;
option.visibility = visibility;
option.data = map.get(value);
found = true;
break;
}
}
if (!found) {
options.push({
value,
label,
visibility,
data: map.get(value),
});
}
}
this.setOption("options", options);
fireCustomEvent(this, "monster-options-set", {
options,
});
if (options === currentOptions) {
const version = bumpOptionsVersion.call(this);
scheduleSelectionSync.call(this, version);
}
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 f