@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,534 lines (1,347 loc) • 37.8 kB
JavaScript
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { instanceSymbol } from "../../constants.mjs";
import {
findTargetElementFromEvent,
fireCustomEvent,
} from "../../dom/events.mjs";
import {
findElementWithIdUpwards,
findElementWithSelectorUpwards,
} from "../../dom/util.mjs";
import {
assembleMethodSymbol,
CustomElement,
getSlottedElements,
registerCustomElement,
} from "../../dom/customelement.mjs";
import { ID } from "../../types/id.mjs";
import { Settings } from "./filter/settings.mjs";
import { FilterStyleSheet } from "./stylesheet/filter.mjs";
import { getDocument, getWindow } from "../../dom/util.mjs";
import { getGlobal } from "../../types/global.mjs";
import {
isInstance,
isFunction,
isObject,
isArray,
isString,
} from "../../types/is.mjs";
import { Host } from "../host/host.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
import "../form/message-state-button.mjs";
import { Formatter } from "../../text/formatter.mjs";
import { generateRangeComparisonExpression } from "../../text/util.mjs";
import {
parseBracketedKeyValueHash,
createBracketedKeyValueHash,
} from "../../text/bracketed-key-value-hash.mjs";
import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
import { SpaceStyleSheet } from "../stylesheet/space.mjs";
import { FormStyleSheet } from "../stylesheet/form.mjs";
import {
getStoredFilterConfigKey,
getFilterConfigKey,
parseDateInput,
} from "./filter/util.mjs";
import "./filter/select.mjs";
import "../form/button.mjs";
import "../form/select.mjs";
import "../form/popper-button.mjs";
import "../form/toggle-switch.mjs";
import "../form/context-help.mjs";
import "../form/context-error.mjs";
import "../form/message-state-button.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { normalizeNumber } from "../../i18n/util.mjs";
export { Filter };
/**
* @private
* @type {symbol}
*/
const filterSelectElementSymbol = Symbol("filterSelectElement");
/**
* @private
* @type {symbol}
*/
const searchButtonElementSymbol = Symbol("searchButtonElement");
/**
* @private
* @type {symbol}
*/
const resetButtonElementSymbol = Symbol("resetButtonElement");
/**
* @private
* @type {symbol}
*/
const saveButtonElementSymbol = Symbol("saveButtonElement");
/**
* @private
* @type {symbol}
*/
const filterControlElementSymbol = Symbol("filterControlElement");
/**
* @private
* @type {symbol}
*/
const filterSaveActionButtonElementSymbol = Symbol(
"filterSaveActionButtonElement",
);
/**
* @private
* @type {symbol}
*/
const filterTabElementSymbol = Symbol("filterTabElement");
/**
* @private
* @type {symbol}
*/
const locationChangeHandlerSymbol = Symbol("locationChangeHandler");
/**
* @private
* @type {symbol}
*/
const settingsSymbol = Symbol("settings");
/**
* @private
* @type {symbol}
*/
const resizeObserverSymbol = Symbol("resizeObserver");
/**
* @private
* @type {symbol}
*/
const sizeDataSymbol = Symbol("sizeData");
/**
* @private
* @type {symbol}
*/
const debounceSizeSymbol = Symbol("debounceSize");
/**
* @private
* @type {symbol}
*/
const hashChangeSymbol = Symbol("hashChange");
/**
* The Filter component is used to show and handle the filter values.
*
* @fragments /fragments/components/datatable/filter
*
* @example /examples/components/datatable/filter-simple First filter
* @example /examples/components/datatable/filter-advanced Advanced filter
* @example /examples/components/datatable/filter-store Store filter
*
* @issue https://localhost.alvine.dev:8440/development/issues/closed/272.html
*
* @copyright schukai GmbH
* @summary The Filter component is used to show and handle the filter values.
*/
class Filter extends CustomElement {
/**
*
*/
constructor() {
super();
this[settingsSymbol] = new Settings();
// debounce the hash change event if doSearch is called by click the search button
this[hashChangeSymbol] = 0;
}
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/filter@@instance");
}
/**
*
* @param {string} message
* @return {Filter}
*/
showFailureMessage(message) {
this[searchButtonElementSymbol].setState(
"failed",
this.getOption("timeouts.message", 4000),
);
this[searchButtonElementSymbol]
.setMessage(message.toString())
.showMessage(this.getOption("timeouts.message", 4000));
return this;
}
/**
*
* @return {Filter}
*/
resetFailureMessage() {
this[searchButtonElementSymbol].hideMessage();
this[searchButtonElementSymbol].removeState();
return this;
}
/**
*
* @return {Filter}
*/
showSuccess() {
this[searchButtonElementSymbol].setState(
"successful",
this.getOption("timeouts.message", 4000),
);
return this;
}
/**
* To set the options via the HTML tag, the attribute `data-monster-options` must be used.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* The individual configuration values can be found in the table.
*
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {Object} labels Label definitions
* @property {string} labels.search Search button label
* @property {string} labels.reset Reset button label
* @property {string} labels.save Save button label
* @property {string} labels.filter-name Filter name label
* @property {string} labels.empty-query-and-no-default Empty query and no default query label
* @property {string} labels.query-not-changed Query not changed label
* @property {Object} formatter Formatter definitions
* @property {Object} formatter.marker Marker definitions
* @property {Object} formatter.marker.open Marker open
* @property {Object} formatter.marker.close Marker close
* @property {Object} features Feature definitions
* @property {boolean} features.storedConfig Stored configuration, this replaces the setting `storedConfig.enabled` @since 3.97.0
* @property {boolean} features.autoFilter Auto filter @since 3.100.0
* @property {boolean} features.preventSameQuery Prevent same query @since 3.103.0
* @property {Object} storedConfig Stored configuration
* @property {boolean} storedConfig.enabled The store has been enabled, this option will no longer have any effect. @deprecated 20250101
* @property {string} storedConfig.selector Selector
* @property {Object} timeouts Timeout definitions
* @property {number} timeouts.message Message timeout
* @property {Object} queries Query definitions
* @property {Function} queries.wrap Wrap callback
* @property {Function} queries.join Join callback
* @property {string} query Query
* @property {string} defaultQuery Default query
* @property {boolean} eventProcessing Event processing
*/
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
formatter: {
marker: {
open: null,
close: null,
},
},
labels: getTranslations(),
templateMapping: {
"filter-save-label": null,
"filter-name-label": name,
},
features: {
storedConfig: false,
autoFilter: true,
preventSameQuery: false,
},
storedConfig: {
enabled: true,
selector: "",
},
timeouts: {
message: 4000,
},
queries: {
wrap: (value, definition) => {
return value;
},
join: (queries) => {
if (queries.length === 0) {
return "";
}
return queries.join(" AND ");
},
},
query: null,
defaultQuery: null,
eventProcessing: true,
});
}
/**
*
* @return {string}
*/
static getTag() {
return "monster-datatable-filter";
}
/**
* @return {FilterButton}
* @fires monster-filter-initialized
*/
[assembleMethodSymbol]() {
const self = this;
this.setOption(
"templateMapping.filter-save-label",
this.getOption("labels.save"),
);
this.setOption(
"templateMapping.filter-name-label",
this.getOption("labels.filter-name"),
);
super[assembleMethodSymbol]();
initControlReferences.call(self);
getWindow().requestAnimationFrame(() => {
initEventHandler.call(self);
});
initFromConfig
.call(self)
.then(() => {})
.catch((error) => {
addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, error?.message);
})
.finally(() => {
initFilter.call(self);
updateFilterTabs.call(self);
if (self.getOption("features.autoFilter") === true) {
doSearch
.call(self, { showEffect: false })
.then(() => {
fireCustomEvent(self, "monster-filter-initialized");
})
.catch(() => {});
}
});
}
/**
*
*/
connectedCallback() {
super.connectedCallback();
getWindow().addEventListener(
"hashchange",
this[locationChangeHandlerSymbol],
);
}
/**
*
*/
disconnectedCallback() {
super.disconnectedCallback();
getWindow().removeEventListener(
"hashchange",
this[locationChangeHandlerSymbol],
);
}
/**
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [FilterStyleSheet, FormStyleSheet, ThemeStyleSheet, SpaceStyleSheet];
}
}
/**
* @private
* @returns {object}
*/
function getTranslations() {
const locale = getLocaleOfDocument();
switch (locale.language) {
case "de":
return {
search: "Suchen",
reset: "Zurücksetzen",
save: "Speichern",
"filter-name": "Filtername",
"empty-query-and-no-default":
"Die Abfrage ist leer und es gibt keine Standardabfrage.",
"query-not-changed":
"Die Suchanfrage hat sich nicht verändert, daher ist keine Suche erforderlich.",
};
case "fr":
return {
search: "Chercher",
reset: "Réinitialiser",
save: "Sauvegarder",
"filter-name": "Nom du filtre",
"empty-query-and-no-default":
"La requête est vide et il n'y a pas de requête par défaut.",
"query-not-changed":
"La requête de recherche n'a pas changé, donc aucune recherche n'est nécessaire.",
};
case "sp":
return {
search: "Buscar",
reset: "Restablecer",
save: "Guardar",
"filter-name": "Nombre del filtro",
"empty-query-and-no-default":
"La consulta está vacía y no hay una consulta predeterminada.",
"query-not-changed":
"La solicitud de búsqueda no ha cambiado, por lo que no se requiere búsqueda.",
};
case "it":
return {
search: "Cerca",
reset: "Reimposta",
save: "Salva",
"filter-name": "Nome del filtro",
"empty-query-and-no-default":
"La query è vuota e non c'è una query predefinita.",
"query-not-changed":
"La richiesta di ricerca non è cambiata, quindi non è necessaria una ricerca.",
};
case "pl":
return {
search: "Szukaj",
reset: "Resetuj",
save: "Zapisz",
"filter-name": "Nazwa filtra",
"empty-query-and-no-default":
"Zapytanie jest puste i nie ma domyślnego zapytania.",
"query-not-changed":
"Żądanie wyszukiwania nie zmieniło się, więc wyszukiwanie nie jest wymagane.",
};
case "no":
return {
search: "Søk",
reset: "Tilbakestill",
save: "Lagre",
"filter-name": "Filternavn",
"empty-query-and-no-default":
"Spørringen er tom og det er ingen standardspørring.",
"query-not-changed":
"Søkeforespørselen har ikke endret seg, så ingen søk er nødvendig.",
};
case "dk":
return {
search: "Søg",
reset: "Nulstil",
save: "Gem",
"filter-name": "Filternavn",
"empty-query-and-no-default":
"Forespørgslen er tom og der er ingen standardforespørgsel.",
"query-not-changed":
"Søgeanmodningen er ikke ændret, så ingen søgning er nødvendig.",
};
case "sw":
return {
search: "Sök",
reset: "Återställ",
save: "Spara",
"filter-name": "Filternamn",
"empty-query-and-no-default":
"Förfrågan är tom och det finns ingen standardförfrågan.",
"query-not-changed":
"Sökförfrågan har inte ändrats, så ingen sökning krävs.",
};
default:
case "en":
return {
search: "Search",
reset: "Reset",
save: "Save",
"filter-name": "Filter name",
"empty-query-and-no-default":
"The query is empty and there is no default query.",
"query-not-changed":
"The search request has not changed, so no search is required.",
};
}
}
/**
* @private
* @return {FilterButton}
*/
function initControlReferences() {
if (!this.shadowRoot) {
throw new Error("no shadow-root is defined");
}
this[filterControlElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=control]",
);
this[filterSelectElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=filter-select]",
);
this[searchButtonElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=search-button]",
);
this[resetButtonElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=reset-button]",
);
this[saveButtonElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=save-button]",
);
this[filterSaveActionButtonElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=save-action-button]",
);
this[filterTabElementSymbol] = findElementWithSelectorUpwards(
this,
this.getOption("storedConfig.selector", ""),
);
return this;
}
/**
* @private
*/
function updateFilterSelections() {
queueMicrotask(() => {
const options = this[settingsSymbol].getOptions();
this[filterSelectElementSymbol].setOption("options", options);
setTimeout(() => {
this[filterSelectElementSymbol].value =
this[settingsSymbol].getSelected();
}, 10);
});
}
/**
* @private
* @throws {Error} no filter label is defined
*/
function initFilter() {
const storedSetting = this[settingsSymbol];
this[settingsSymbol] = new Settings();
const result = parseBracketedKeyValueHash(getGlobal().location.hash);
let valuesFromHash = {};
if (isObject(result) && result?.[this.id]) {
valuesFromHash = result[this.id];
}
getSlottedElements
.call(this, "label[data-monster-label]")
.forEach((element) => {
const label = element.getAttribute("data-monster-label");
if (!label) {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
"no filter label is defined",
);
return;
}
let value = element.id;
if (!value) {
const prefix = label.replace(/\W/g, "-");
prefix.charAt(0).match(/[\d_]/g)?.length ? `f${prefix}` : prefix;
value = new ID(prefix + "-").toString();
element.id = value;
}
let setting = storedSetting.get(value);
if (setting) {
this[settingsSymbol].set(setting);
}
if (valuesFromHash?.[element.id]) {
const v = escapeAttributeValue(valuesFromHash[element.id]);
const searchInput = element.firstElementChild;
try {
searchInput.value = v;
} catch (error) {}
}
setting = this[settingsSymbol].get(value);
let visible = false;
if (setting) {
setSlotAttribute(element, setting.visible);
visible = setting.visible;
} else {
visible = getVisibilityFromSlotAttribute(element);
}
this[settingsSymbol].set({ value, label, visible });
});
updateFilterSelections.call(this);
}
/**
* @private
* @param {string} input
* @return {*}
*/
function escapeAttributeValue(input) {
if (input === undefined || input === null) {
return input;
}
return input
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/</g, "<")
.replace(/>/g, ">");
}
/**
*
* @param {HTMLElement} element
* @return {boolean}
*/
function getVisibilityFromSlotAttribute(element) {
return !(
element.hasAttribute("slot") && element.getAttribute("slot") === "hidden"
);
}
/**
* @private
* @param {HTMLElement} element
* @param {boolean} visible
*/
function setSlotAttribute(element, visible) {
if (visible) {
element.removeAttribute("slot");
return;
}
element.setAttribute("slot", "hidden");
}
/**
* @private
*/
function initEventHandler() {
const self = this;
let lastHash = getGlobal().location.hash;
self[locationChangeHandlerSymbol] = () => {
if (lastHash === getGlobal().location.hash) {
return;
}
/**
* debounce the hash change event if doSearch
* is called by click the search button
*/
if (self[hashChangeSymbol] > 0) {
self[hashChangeSymbol]--;
return;
}
initFilter.call(this);
doSearch
.call(self)
.then(() => {})
.catch((error) => {})
.finally(() => {
lastHash = getGlobal().location.hash;
});
};
/**
* Monster.Components.Form.event:monster-selection-cleared
*/
if (self[filterSelectElementSymbol]) {
self[filterSelectElementSymbol].addEventListener(
"monster-selection-cleared",
function () {
const settings = self[settingsSymbol].getOptions();
for (const setting of settings) {
const filterElement = findElementWithIdUpwards(self, setting.value);
if (filterElement) {
setSlotAttribute(filterElement, false);
self[settingsSymbol].set({ value: setting.value, visible: false });
}
}
updateConfig.call(self);
},
);
self[filterSelectElementSymbol].addEventListener(
"monster-changed",
function (event) {
const filterElement = findElementWithIdUpwards(
self,
event.detail.value,
);
if (filterElement) {
setSlotAttribute(filterElement, event.detail.checked);
}
self[settingsSymbol].set({
value: event.detail.value,
visible: event.detail.checked,
});
updateConfig.call(self);
},
);
}
/** save the current filter */
if (self[filterSaveActionButtonElementSymbol]) {
self[filterSaveActionButtonElementSymbol].setOption(
"actions.click",
function (event) {
const button = findTargetElementFromEvent(
event,
"data-monster-role",
"save-action-button",
);
const form = button.closest("[data-monster-role=form]");
if (!form) {
button.setState("failed", self.getOption("timeouts.message", 4000));
return;
}
const input = form.querySelector("input[name=filter-name]");
if (!input) {
button.setState("failed", self.getOption("timeouts.message", 4000));
return;
}
const name = input.value;
if (!name) {
button.setState("failed", self.getOption("timeouts.message", 4000));
button.setMessage("Please enter a name").showMessage();
return;
}
doSearch
.call(self, { showEffect: false })
.then(() => {
const configKey = getStoredFilterConfigKey.call(self);
const host = getDocument().querySelector("monster-host");
if (!host) {
return;
}
const query = self.getOption("query");
if (!query) {
button.setState(
"failed",
self.getOption(
"timeouts.message",
self.getOption("timeouts.message", 4000),
),
);
button
.setMessage("No query found")
.showMessage(self.getOption("timeouts.message", 4000));
return;
}
host
.hasConfig(configKey)
.then((hasConfig) => {
return new Promise((resolve, reject) => {
if (hasConfig) {
host.getConfig(configKey).then(resolve).catch(reject);
return;
}
return resolve({});
});
})
.then((config) => {
config[name] = query;
return host.setConfig(configKey, {
...config,
});
})
.then(() => {
button.setState(
"successful",
self.getOption("timeouts.message", 4000),
);
updateFilterTabs.call(self);
})
.catch((error) => {
button.setState(
"failed",
self.getOption("timeouts.message", 4000),
);
button
.setMessage(error.message)
.showMessage(self.getOption("timeouts.message", 4000));
});
})
.catch((error) => {
button.setState("failed", self.getOption("timeouts.message", 4000));
const msg = error.message || error;
button
.setMessage(msg)
.showMessage(self.getOption("timeouts.message", 4000));
});
},
);
}
self[searchButtonElementSymbol].setOption("actions.click", () => {
self[hashChangeSymbol] = 1;
doSearch
.call(self)
.then(() => {})
.catch((error) => {});
});
// the reset button should reset the filter and the search query
// all input elements should be reset to their default values
// which is the empty string. we search for all input elements
// in the filter and reset them to their default value
self[resetButtonElementSymbol].setOption("actions.click", () => {
getSlottedElements
.call(self, "label[data-monster-label]")
.forEach((element) => {
const label = element.getAttribute("data-monster-label");
if (!label) {
return;
}
const input = element.firstElementChild;
if (input) {
input.value = "";
}
});
doSearch
.call(self, { showEffect: false })
.then(() => {})
.catch((e) => addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.message));
});
self.addEventListener("keyup", (event) => {
const path = event.composedPath();
if (path.length === 0) {
return;
}
if (!(path[0] instanceof HTMLInputElement)) {
return;
}
if (event.keyCode === 13) {
doSearch
.call(self, { showEffect: false })
.then(() => {})
.catch((error) => {});
}
});
// tabs
const element = this[filterTabElementSymbol];
if (element) {
initTabEvents.call(this);
}
}
function initTabEvents() {
const self = this;
this[filterTabElementSymbol].addEventListener(
"monster-tab-changed",
(event) => {
const query = event?.detail?.data?.["data-monster-query"];
const q = this.getOption("query");
if (query !== q) {
this.setOption("query", query);
}
},
);
this[filterTabElementSymbol].addEventListener(
"monster-tab-remove",
(event) => {
const labels = [];
const buttons = this[filterTabElementSymbol].getOption("buttons");
const keys = ["popper", "standard"];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
for (const button of buttons[key]) {
if (button.label !== event.detail.label) {
labels.push(button.label);
}
}
}
const host = findElementWithSelectorUpwards(this, "monster-host");
if (!(host && this.id)) {
return;
}
const configKey = getStoredFilterConfigKey.call(this);
host
.hasConfig(configKey)
.then((hasConfig) => {
if (!hasConfig) {
return;
}
return host.getConfig(configKey);
})
.then((config) => {
for (const [name, query] of Object.entries(config)) {
if (labels.includes(name)) {
continue;
}
delete config[name];
}
return host.setConfig(configKey, {
...config,
});
});
},
);
}
/**
* @private
*/
function updateFilterTabs() {
const element = this[filterTabElementSymbol];
if (!element) {
return;
}
const host = findElementWithSelectorUpwards(this, "monster-host");
if (!(host && this.id)) {
return;
}
const configKey = getStoredFilterConfigKey.call(this);
host
.hasConfig(configKey)
.then((hasConfig) => {
if (!hasConfig) {
return;
}
return host.getConfig(configKey);
})
.then((config) => {
for (const [name, query] of Object.entries(config)) {
const found = element.querySelector(
`[data-monster-button-label="${name}"]`,
);
if (found) {
continue;
}
if (query === undefined || query === null) {
continue;
}
const escapedQuery = escapeAttributeValue(query);
element.insertAdjacentHTML(
"beforeend",
`<div data-monster-button-label="${name}"
data-monster-removable="true"
data-monster-query="${escapedQuery}" data-monster-role="filter-tab" >
</div>`,
);
}
})
.catch((error) => {
if (error instanceof Error) {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
error.message + " " + error.stack,
);
} else {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error + "");
}
});
}
/**
* @private
* @param showEffect
* @return {Promise}
*/
function doSearch({ showEffect } = { showEffect: true }) {
this.resetFailureMessage();
if (showEffect) {
this[searchButtonElementSymbol].setState(
"activity",
this.getOption("timeouts.message", 4000),
);
}
return collectSearchQueries
.call(this)
.then((query) => {
const buildQuery = buildSearchQuery.call(this, query);
if (buildQuery === null) {
const msg = this.getOption("labels.empty-query-and-no-default");
if (showEffect) {
this[searchButtonElementSymbol].removeState();
this[searchButtonElementSymbol]
.setMessage(msg)
.showMessage(this.getOption("timeouts.message", 4000));
}
return Promise.reject(new Error(msg));
}
if (buildQuery === "" && this.getOption("defaultQuery") === null) {
const msg = this.getOption("labels.empty-query-and-no-default");
if (showEffect) {
this[searchButtonElementSymbol].removeState();
this[searchButtonElementSymbol]
.setMessage(msg)
.showMessage(this.getOption("timeouts.message", 4000));
}
return Promise.reject(new Error(msg));
}
if (
this.getOption("features.preventSameQuery") &&
buildQuery === this.getOption("query")
) {
const msg = this.getOption("labels.query-not-changed");
if (showEffect) {
this[searchButtonElementSymbol].removeState();
this[searchButtonElementSymbol]
.setMessage(msg)
.showMessage(this.getOption("timeouts.message", 4000));
}
return Promise.reject(new Error(msg));
}
if (showEffect) {
this[searchButtonElementSymbol].setState(
"activity",
this.getOption("timeouts.message", 4000),
);
}
this.setOption("query", buildSearchQuery.call(this, query));
return Promise.resolve();
})
.catch((error) => {
if (error instanceof Error) {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
error.message + " " + error.stack,
);
} else {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error));
}
if (showEffect) {
this[searchButtonElementSymbol].setState(
"failed",
this.getOption("timeouts.message", 4000),
);
this[searchButtonElementSymbol].setMessage(error.message).showMessage();
}
return Promise.reject(error);
});
}
/**
* @private
* @param queries
* @return {*|string}
*/
function buildSearchQuery(queries) {
if (!isArray(queries) || queries.length === 0) {
return this.getOption("defaultQuery");
}
const joinCallback = this.getOption("queries.join");
if (isFunction(joinCallback)) {
return joinCallback(queries);
}
const q = queries.join(" ").trim();
if (q.length === 0) {
return this.getOption("defaultQuery");
}
return q;
}
/**
* @private
* @return {Promise<unknown>}
*/
function collectSearchQueries() {
const currentHash = parseBracketedKeyValueHash(getGlobal().location.hash);
const self = this;
return new Promise((resolve, reject) => {
const query = [];
const wrapCallback = this.getOption("queries.wrap");
let hasNoIdError = false;
getSlottedElements
.call(this, "label[data-monster-label]")
.forEach((element) => {
const label = element.getAttribute("data-monster-label");
if (!label) {
throw new Error("no filter label is defined");
}
const id = element.id;
if (!id) {
hasNoIdError = true;
return;
}
const visible = getVisibilityFromSlotAttribute(element);
if (!visible) {
return;
}
let template = element.getAttribute("data-monster-template");
if (!template) {
template = "${id}=${value}";
}
const controlValue = getControlValuesFromLabel(element);
if (!controlValue) {
if (controlValue === "" && currentHash?.[this.id]?.[id]) {
delete currentHash[this.id][id];
}
return;
}
if (!isObject(currentHash[this.id])) {
currentHash[this.id] = {};
}
currentHash[this.id][id] = controlValue;
const mapping = {
id,
value: controlValue,
label,
};
const formatter = new Formatter(mapping, {
callbacks: {
range: (value, key) => {
return generateRangeComparisonExpression(value, key, {
urlEncode: true,
andOp: "AND",
orOp: "OR",
eqOp: "=",
gtOp: ">",
ltOp: "<",
});
},
"tag-list": (value, key) => {
if (isString(value)) {
value = value.split(",");
}
if (!isArray(value)) {
return "";
}
return (
key +
" IN " +
value
.map((v) => {
return `"${encodeURIComponent(v)}"`;
})
.join(",")
);
},
"list-tag": (value, key) => {
if (isString(value)) {
value = value.split(",");
}
if (!isArray(value)) {
return "";
}
return (
value
.map((v) => {
return `"${encodeURIComponent(v)}"`;
})
.join(",") +
" IN " +
encodeURIComponent(key)
);
},
"tags-in-list": (value, key, op) => {
if (isString(value)) {
value = value.split(",");
}
if (!isArray(value)) {
return "";
}
if (!op || !isString(op)) op = "OR";
op = " " + op.toUpperCase().trim() + " ";
let query = "";
value.forEach((v) => {
if (query.length > 0) {
query += op;
}
query += `${encodeURIComponent(key)} IN "${encodeURIComponent(v)}"`;
});
return query;
},
"list-in-tags": (value, key, op) => {
if (isString(value)) {
value = value.split(",");
}
if (!isArray(value)) {
return "";
}
if (!op || !isString(op)) op = "OR";
op = " " + op.toUpperCase().trim() + " ";
let query = "";
value.forEach((v) => {
if (query.length > 0) {
query += op;
}
query += `"${encodeURIComponent(v)}" IN ${encodeURIComponent(key)}`;
});
return query;
},
"array-list": (value, key) => {
if (isString(value)) {
value = value.split(",");
}
if (!isArray(value)) {
return "";
}
return (
key +
"=" +
value
.map((v) => {
return `"${encodeURIComponent(v)}"`;
})
.join(",")
);
},
"date-range": (value, key) => {
const query = parseDateInput(value, key);
if (!query || query === "false") {
return "";
}
// return query as url encoded
return encodeURIComponent(query);
},
"to-int-2": (value, key) => {
const query = normalizeNumber(value);
if (Number.isNaN(query)) {
return key + " IS NULL";
}
return key + "=" + encodeURIComponent(Math.round(query * 100));
},
"to-int-3": (value, key) => {
const query = normalizeNumber(value);
if (Number.isNaN(query)) {
return "";
}
return key + "=" + encodeURIComponent(Math.round(query * 1000));
},
"to-int-4": (value, key) => {
const query = normalizeNumber(value);
if (Number.isNaN(query)) {
return "";
}
return key + "=" + encodeURIComponent(Math.round(query * 10000));
},
},
});
if (self.getOption("formatter.marker.open")) {
formatter.setMarker(
self.getOption("formatter.marker.open"),
self.getOption("formatter.marker.close"),
);
}
let queryPart = formatter.format(template);
if (queryPart) {
if (isFunction(wrapCallback)) {
queryPart = wrapCallback(queryPart, mapping);
}
query.push(queryPart);
}
});
if (hasNoIdError) {
reject(new Error("some or all filter elements have no id"));
return;
}
getGlobal().location.hash = createBracketedKeyValueHash(currentHash);
resolve(query);
});
}
/**
* @private
* @param label
* @return {null|Array|undefined|string}
*/
function getControlValuesFromLabel(label) {
// finde das erste Kind-Element vom type input
// wenn es ein input-Element ist, dann @todo
const foundControl = label.firstElementChild;
if (foundControl) {
if (foundControl.tagName === "INPUT") {
if (foundControl.type === "checkbox") {
const checkedControls = label.querySelectorAll(
`${foundControl}:checked`,
);
const values = [];
checkedControls.forEach((checkedControl) => {
values.push(checkedControl.value);
});
return values;
} else if (foundControl.type === "radio") {
const checkedControl = label.querySelector(`${foundControl}:checked`);
if (checkedControl) {
return checkedControl.value;
} else {
return null;
}
} else {
return foundControl.value;
}
} else {
return foundControl.value;
}
}
return null;
}
/**
* @private
* @return {Promise<unknown>}
*/
function initFromConfig() {
const host = findElementWithSelectorUpwards(this, "monster-host");
if (!(isInstance(host, Host) && this.id)) {
return Promise.resolve();
}
const configKey = getFilterConfigKey.call(this);
return new Promise((resolve, reject) => {
host
.getConfig(configKey)
.then((config) => {
if ((config && isObject(config)) || isArray(config)) {
this[settingsSymbol].setOptions(config);
}
resolve();
})
.catch((error) => {
if (error === undefined) {
resolve();
return;
}
// config not written
if (error?.message?.match(/is not defined/)) {
resolve();
return;
}
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
error?.message || error,
);
reject(error);
});
});
}
/**
* @private
*/
function updateConfig() {
const host = findElementWithSelectorUpwards(this, "monster-host");
if (!(host && this.id)) {
return;
}
const configKey = getFilterConfigKey.call(this);
try {
host.setConfig(configKey, this[settingsSymbol].getOptions());
} catch (error) {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error?.message || error);
}
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<div data-monster-role="control" part="control">
<div data-monster-role="container">
<div data-monster-role="layout">
<div data-monster-role="filter">
<slot></slot>
<slot name="hidden"></slot>
</div>
<div data-monster-role="select-and-search">
<monster-message-state-button data-monster-role="search-button" class="stretched-control"
data-monster-replace="path:labels.search">
</monster-message-state-button>
<monster-select class="stretched-control"
data-monster-selected-template="summary"
data-monster-option-type="checkbox"
data-monster-option-filter-mode="options"
data-monster-option-filter-position="popper"
data-monster-role="filter-select"></monster-select>
<monster-popper-button data-monster-role="save-button" class="stretched-control"
data-monster-attributes="data-monster-visible path:features.storedConfig">
<div slot="button">\${filter-save-label}</div>
<div class="monster-form" data-monster-role="form">
<label for="filter-name">\${filter-name-label}
<input name="filter-name"
type="search"
class="monster-margin-bottom-5"></label>
<monster-message-state-button
data-monster-role="save-action-button"
data-monster-option-labels-button="\${filter-save-label}">
</monster-message-state-button>
</div>
</monster-popper-button>
<monster-button data-monster-role="reset-button" class="stretched-control"
data-monster-replace="path:labels.reset">
</monster-button>
</div>
</div>
<input class="hidden" name="query" data-monster-role="query"
data-monster-attributes="value path:query">
</div>
</div>
`;
}
registerCustomElement(Filter);