@schukai/monster 4.112.0 → 4.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.113.0] - 2026-01-30
6
+
7
+ ### Add Features
8
+
9
+ - Improve selection handling in select.mjs
10
+ - Implement two linked selection components and associated functionality [#381](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/381)
11
+ - Add two selection dependency with UI and mock data
12
+ ### Changes
13
+
14
+ - move mock to development mock
15
+
16
+
17
+
5
18
  ## [4.112.0] - 2026-01-29
6
19
 
7
20
  ### Add Features
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.112.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.113.0"}
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact Volker Schukai.
11
+ *
12
+ * SPDX-License-Identifier: AGPL-3.0
13
+ */
14
+
15
+ import { instanceSymbol } from "../../constants.mjs";
16
+ import {
17
+ assembleMethodSymbol,
18
+ registerCustomElement,
19
+ } from "../../dom/customelement.mjs";
20
+ import { addErrorAttribute } from "../../dom/error.mjs";
21
+ import { getDocument } from "../../dom/util.mjs";
22
+ import { isArray, isObject, isString } from "../../types/is.mjs";
23
+ import { CustomElement } from "../../dom/customelement.mjs";
24
+
25
+ export { SelectLink };
26
+
27
+ const sourceElementSymbol = Symbol("sourceElement");
28
+ const targetElementSymbol = Symbol("targetElement");
29
+ const observerSymbol = Symbol("observer");
30
+ const handlerSymbol = Symbol("handler");
31
+ const boundSymbol = Symbol("bound");
32
+
33
+ class SelectLink extends CustomElement {
34
+ static get [instanceSymbol]() {
35
+ return Symbol.for("@schukai/monster/components/form/select-link@@instance");
36
+ }
37
+
38
+ static getTag() {
39
+ return "monster-select-link";
40
+ }
41
+
42
+ get defaults() {
43
+ return Object.assign({}, super.defaults, {
44
+ shadowMode: false,
45
+ source: "",
46
+ target: "",
47
+ param: "",
48
+ emptyValue: "",
49
+ disableTarget: true,
50
+ clearSelection: true,
51
+ clearOptions: true,
52
+ clearTotalMessage: true,
53
+ clearMessageOnValue: true,
54
+ emptyMessage: "",
55
+ autoFetch: true,
56
+ autoFetchOnEmpty: false,
57
+ syncOnInit: true,
58
+ events: [
59
+ "monster-selected",
60
+ "monster-changed",
61
+ "monster-selection-removed",
62
+ "monster-selection-cleared",
63
+ ],
64
+ debug: false,
65
+ });
66
+ }
67
+
68
+ [assembleMethodSymbol]() {
69
+ super[assembleMethodSymbol]();
70
+ bindSelects.call(this);
71
+ return this;
72
+ }
73
+
74
+ disconnectedCallback() {
75
+ unbindSelects.call(this);
76
+ }
77
+ }
78
+
79
+ function resolveElement(value) {
80
+ if (value instanceof HTMLElement) {
81
+ return value;
82
+ }
83
+
84
+ if (!isString(value) || value.trim() === "") {
85
+ return null;
86
+ }
87
+
88
+ const selector = value.trim();
89
+ const doc = getDocument();
90
+ let element = null;
91
+
92
+ try {
93
+ element = doc.querySelector(selector);
94
+ } catch (e) {
95
+ element = null;
96
+ }
97
+
98
+ if (!element) {
99
+ element = doc.getElementById(selector.replace(/^#/, ""));
100
+ }
101
+
102
+ return element instanceof HTMLElement ? element : null;
103
+ }
104
+
105
+ function bindSelects() {
106
+ const source = resolveElement(this.getOption("source"));
107
+ const target = resolveElement(this.getOption("target"));
108
+
109
+ if (!(source && target)) {
110
+ if (!this[observerSymbol] && getDocument().body) {
111
+ const observer = new MutationObserver(() => {
112
+ bindSelects.call(this);
113
+ });
114
+ observer.observe(getDocument().body, {
115
+ childList: true,
116
+ subtree: true,
117
+ });
118
+ this[observerSymbol] = observer;
119
+ }
120
+ return;
121
+ }
122
+
123
+ if (this[observerSymbol]) {
124
+ this[observerSymbol].disconnect();
125
+ delete this[observerSymbol];
126
+ }
127
+
128
+ this[sourceElementSymbol] = source;
129
+ this[targetElementSymbol] = target;
130
+
131
+ if (this[boundSymbol]) {
132
+ return;
133
+ }
134
+
135
+ const events = this.getOption("events");
136
+ const handler = () => syncTarget.call(this);
137
+ this[handlerSymbol] = handler;
138
+
139
+ if (isArray(events)) {
140
+ for (const eventName of events) {
141
+ if (isString(eventName) && eventName !== "") {
142
+ source.addEventListener(eventName, handler);
143
+ }
144
+ }
145
+ }
146
+
147
+ this[boundSymbol] = true;
148
+
149
+ if (this.getOption("syncOnInit") === true) {
150
+ queueMicrotask(() => {
151
+ syncTarget.call(this);
152
+ });
153
+ }
154
+ }
155
+
156
+ function unbindSelects() {
157
+ if (this[observerSymbol]) {
158
+ this[observerSymbol].disconnect();
159
+ delete this[observerSymbol];
160
+ }
161
+
162
+ const source = this[sourceElementSymbol];
163
+ const handler = this[handlerSymbol];
164
+ const events = this.getOption("events");
165
+
166
+ if (source && handler && isArray(events)) {
167
+ for (const eventName of events) {
168
+ if (isString(eventName) && eventName !== "") {
169
+ source.removeEventListener(eventName, handler);
170
+ }
171
+ }
172
+ }
173
+
174
+ delete this[sourceElementSymbol];
175
+ delete this[targetElementSymbol];
176
+ delete this[handlerSymbol];
177
+ delete this[boundSymbol];
178
+ }
179
+
180
+ function syncTarget() {
181
+ const source = this[sourceElementSymbol];
182
+ const target = this[targetElementSymbol];
183
+ const param = this.getOption("param");
184
+
185
+ if (!(source && target)) {
186
+ return;
187
+ }
188
+
189
+ if (!isString(param) || param.trim() === "") {
190
+ addErrorAttribute(this, "Missing param option.");
191
+ return;
192
+ }
193
+
194
+ let value = source.value;
195
+ if (isArray(value)) {
196
+ value = value.join(",");
197
+ }
198
+
199
+ const isEmpty = value === null || value === undefined || value === "";
200
+ const emptyValue = this.getOption("emptyValue");
201
+ const autoFetch = this.getOption("autoFetch") === true;
202
+ const autoFetchOnEmpty = this.getOption("autoFetchOnEmpty") === true;
203
+ const disableTarget = this.getOption("disableTarget") === true;
204
+ const debug = this.getOption("debug") === true;
205
+
206
+ if (debug) {
207
+ console.log("[select-link]", {
208
+ source: source.id || source.tagName,
209
+ target: target.id || target.tagName,
210
+ param,
211
+ value,
212
+ isEmpty,
213
+ disableTarget,
214
+ autoFetch,
215
+ autoFetchOnEmpty,
216
+ });
217
+ }
218
+
219
+ if (isEmpty) {
220
+ if (disableTarget === true) {
221
+ target.setAttribute("disabled", "");
222
+ if (debug) {
223
+ console.log("[select-link] target disabled (empty)");
224
+ }
225
+ }
226
+
227
+ if (this.getOption("clearSelection") === true) {
228
+ target.setOption("selection", []);
229
+ }
230
+ if (this.getOption("clearOptions") === true) {
231
+ target.setOption("options", []);
232
+ }
233
+ if (this.getOption("clearTotalMessage") === true) {
234
+ target.setOption("messages.total", "");
235
+ }
236
+ const emptyMessage = this.getOption("emptyMessage");
237
+ if (isString(emptyMessage) && emptyMessage !== "") {
238
+ target.setOption("messages.control", emptyMessage);
239
+ }
240
+
241
+ const params = Object.assign({}, target.getOption("filter.params", {}));
242
+ params[param] = emptyValue;
243
+ target.setOption("filter.params", params);
244
+
245
+ if (autoFetchOnEmpty && typeof target.fetch === "function") {
246
+ target.fetch().catch((e) => {
247
+ addErrorAttribute(target, e);
248
+ });
249
+ }
250
+ return;
251
+ }
252
+
253
+ if (disableTarget === true) {
254
+ target.removeAttribute("disabled");
255
+ if (debug) {
256
+ console.log("[select-link] target enabled (value)");
257
+ }
258
+ }
259
+
260
+ if (this.getOption("clearMessageOnValue") === true) {
261
+ target.setOption("messages.control", "");
262
+ }
263
+
264
+ const params = Object.assign({}, target.getOption("filter.params", {}));
265
+ params[param] = value;
266
+ target.setOption("filter.params", params);
267
+
268
+ if (autoFetch && typeof target.fetch === "function") {
269
+ target.fetch().catch((e) => {
270
+ addErrorAttribute(target, e);
271
+ });
272
+ }
273
+ }
274
+
275
+ registerCustomElement(SelectLink);
@@ -256,7 +256,11 @@ const optionsVersionSymbol = Symbol("optionsVersion");
256
256
  const pendingSelectionSymbol = Symbol("pendingSelection");
257
257
  const selectionSyncScheduledSymbol = Symbol("selectionSyncScheduled");
258
258
  const optionsSnapshotSymbol = Symbol("optionsSnapshot");
259
+ const strictModeSnapshotSymbol = Symbol("strictModeSnapshot");
260
+ const optionsMapSymbol = Symbol("optionsMap");
261
+ const optionsMapVersionSnapshotSymbol = Symbol("optionsMapVersionSnapshot");
259
262
  const selectionVersionSymbol = Symbol("selectionVersion");
263
+ const closeOnSelectAutoSymbol = Symbol("closeOnSelectAuto");
260
264
 
261
265
  /**
262
266
  * @private
@@ -288,6 +292,7 @@ const lookupCacheSymbol = Symbol("lookupCache");
288
292
  * @type {symbol}
289
293
  */
290
294
  const lookupInProgressSymbol = Symbol("lookupInProgress");
295
+ const fetchRequestVersionSymbol = Symbol("fetchRequestVersion");
291
296
 
292
297
  /**
293
298
  * @private
@@ -365,6 +370,8 @@ class Select extends CustomControl {
365
370
  this[currentPageSymbol] = 1;
366
371
  this[lookupCacheSymbol] = new Map();
367
372
  this[lookupInProgressSymbol] = new Map();
373
+ this[optionsMapSymbol] = new Map();
374
+ this[closeOnSelectAutoSymbol] = true;
368
375
  initOptionObserver.call(this);
369
376
  }
370
377
 
@@ -574,6 +581,8 @@ class Select extends CustomControl {
574
581
  open: "{",
575
582
  close: "}",
576
583
  },
584
+ params: {},
585
+ paramsDefaults: {},
577
586
  defaultOptionsUrl: null,
578
587
  },
579
588
 
@@ -650,6 +659,8 @@ class Select extends CustomControl {
650
659
  this.setOption("messages.selected", "");
651
660
  this.setOption("messages.total", "");
652
661
  this.setOption("messages.summary", "");
662
+ this.setOption("total", null);
663
+ resetPaginationState.call(this);
653
664
 
654
665
  resetErrorAttribute(this);
655
666
 
@@ -921,11 +932,29 @@ function processAndApplyPaginationData(data) {
921
932
  return;
922
933
  }
923
934
 
935
+ let dataCount;
936
+ const mappingSelector = this.getOption("mapping.selector");
937
+ if (isString(mappingSelector)) {
938
+ try {
939
+ const pathfinder = new Pathfinder(data);
940
+ const mapped = pathfinder.getVia(mappingSelector);
941
+ if (isArray(mapped)) {
942
+ dataCount = mapped.length;
943
+ } else if (mapped instanceof Map) {
944
+ dataCount = mapped.size;
945
+ } else if (isObject(mapped)) {
946
+ dataCount = Object.keys(mapped).length;
947
+ }
948
+ } catch (e) {}
949
+ }
950
+
924
951
  const mappingTotal = this.getOption("mapping.total");
925
952
  const mappingCurrentPage = this.getOption("mapping.currentPage");
926
953
  const mappingObjectsPerPage = this.getOption("mapping.objectsPerPage");
927
954
 
928
955
  if (!mappingTotal || !mappingCurrentPage || !mappingObjectsPerPage) {
956
+ this.setOption("total", null);
957
+ resetPaginationState.call(this);
929
958
  return;
930
959
  }
931
960
 
@@ -937,15 +966,15 @@ function processAndApplyPaginationData(data) {
937
966
 
938
967
  if (!isInteger(total)) {
939
968
  addErrorAttribute(this, "total is not an integer");
969
+ this.setOption("total", null);
970
+ resetPaginationState.call(this);
940
971
  return;
941
972
  }
942
973
 
943
974
  this.setOption("total", total);
944
975
 
945
976
  if (total === 0) {
946
- this[paginationElementSymbol].style.display = "none";
947
- this[paginationElementSymbol].setOption("pages", null);
948
- this[paginationElementSymbol].setOption("currentPage", null);
977
+ resetPaginationState.call(this);
949
978
  return;
950
979
  }
951
980
 
@@ -955,10 +984,53 @@ function processAndApplyPaginationData(data) {
955
984
  isInteger(objectsPerPage) &&
956
985
  objectsPerPage > 0
957
986
  ) {
987
+ if (
988
+ isInteger(dataCount) &&
989
+ (dataCount === 0 ||
990
+ dataCount > objectsPerPage ||
991
+ total < dataCount ||
992
+ currentPage > Math.ceil(total / objectsPerPage))
993
+ ) {
994
+ addErrorAttribute(this, "Invalid pagination data.");
995
+ this.setOption("total", null);
996
+ resetPaginationState.call(this);
997
+ return;
998
+ }
958
999
  updatePagination.call(this, total, currentPage, objectsPerPage);
959
1000
  }
960
1001
  } catch (e) {
961
1002
  addErrorAttribute(this, e);
1003
+ this.setOption("total", null);
1004
+ resetPaginationState.call(this);
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * @private
1010
+ * @param {object} data Die rohen Daten aus der API-Antwort.
1011
+ */
1012
+ function processAndApplyRemoteInfoTotal(data) {
1013
+ const mappingTotal = this.getOption("mapping.total");
1014
+ if (!isString(mappingTotal)) {
1015
+ return;
1016
+ }
1017
+
1018
+ try {
1019
+ const pathfinder = new Pathfinder(data);
1020
+ const total = pathfinder.getVia(mappingTotal);
1021
+
1022
+ if (!isInteger(total)) {
1023
+ addErrorAttribute(this, "total is not an integer");
1024
+ this.setOption("total", null);
1025
+ return;
1026
+ }
1027
+
1028
+ this.setOption("total", total);
1029
+ // Note: remoteInfo is a lightweight request (count=1). Only update total/message here.
1030
+ setTotalText.call(this);
1031
+ } catch (e) {
1032
+ addErrorAttribute(this, e);
1033
+ this.setOption("total", null);
962
1034
  }
963
1035
  }
964
1036
 
@@ -999,10 +1071,14 @@ function scheduleSelectionSync(version) {
999
1071
  return;
1000
1072
  }
1001
1073
 
1074
+ const shouldUseAttrValue =
1075
+ state.attrValue !== null &&
1076
+ !(state.attrValue === "" && selectionIsEmpty === false);
1077
+
1002
1078
  const pending = {
1003
1079
  version,
1004
1080
  selectionVersion: this[selectionVersionSymbol] || 0,
1005
- value: state.attrValue !== null ? state.attrValue : state.selection,
1081
+ value: shouldUseAttrValue ? state.attrValue : state.selection,
1006
1082
  };
1007
1083
  this[pendingSelectionSymbol] = pending;
1008
1084
 
@@ -1731,10 +1807,20 @@ function fetchIt(url, controlOptions) {
1731
1807
  return new Promise((resolve, reject) => {
1732
1808
  setStatusOrRemoveBadges.call(this, "loading");
1733
1809
 
1810
+ if (!isInteger(this[fetchRequestVersionSymbol])) {
1811
+ this[fetchRequestVersionSymbol] = 0;
1812
+ }
1813
+ this[fetchRequestVersionSymbol] += 1;
1814
+ const requestVersion = this[fetchRequestVersionSymbol];
1815
+
1734
1816
  new Processing(10, () => {
1735
1817
  fetchData
1736
1818
  .call(this, url)
1737
1819
  .then((map) => {
1820
+ if (requestVersion !== this[fetchRequestVersionSymbol]) {
1821
+ resolve();
1822
+ return;
1823
+ }
1738
1824
  if (
1739
1825
  isObject(map) ||
1740
1826
  isArray(map) ||
@@ -1765,10 +1851,20 @@ function fetchIt(url, controlOptions) {
1765
1851
  }
1766
1852
 
1767
1853
  setStatusOrRemoveBadges.call(this, "error");
1854
+ clearOptionsOnError.call(this);
1855
+ this.setOption("total", null);
1856
+ resetPaginationState.call(this);
1768
1857
  reject(new Error("invalid response"));
1769
1858
  })
1770
1859
  .catch((e) => {
1860
+ if (requestVersion !== this[fetchRequestVersionSymbol]) {
1861
+ resolve();
1862
+ return;
1863
+ }
1771
1864
  setStatusOrRemoveBadges.call(this, "error");
1865
+ clearOptionsOnError.call(this);
1866
+ this.setOption("total", null);
1867
+ resetPaginationState.call(this);
1772
1868
  reject(e);
1773
1869
  });
1774
1870
  })
@@ -1776,6 +1872,9 @@ function fetchIt(url, controlOptions) {
1776
1872
  .catch((e) => {
1777
1873
  setStatusOrRemoveBadges.call(this, "error");
1778
1874
  addErrorAttribute(this, e);
1875
+ clearOptionsOnError.call(this);
1876
+ this.setOption("total", null);
1877
+ resetPaginationState.call(this);
1779
1878
  reject(e);
1780
1879
  });
1781
1880
  });
@@ -1940,35 +2039,12 @@ function buildSelectionLabel(value) {
1940
2039
  return this[lookupCacheSymbol].get(value);
1941
2040
  }
1942
2041
 
1943
- const options = this.getOption("options");
1944
-
1945
- for (let i = 0; i < options.length; i++) {
1946
- let o = options?.[i];
1947
- let l, v, v2;
1948
-
1949
- if (this.getOption("features.useStrictValueComparison") === true) {
1950
- v = value;
1951
- } else {
1952
- v = `${value}`;
1953
- }
1954
-
1955
- if (isPrimitive(o) && o === value) {
1956
- return o;
1957
- } else if (!isObject(o)) {
1958
- continue;
1959
- }
1960
-
1961
- if (this.getOption("features.useStrictValueComparison") === true) {
1962
- l = o?.["label"];
1963
- v2 = o?.["value"];
1964
- } else {
1965
- l = `${o?.["label"]}`;
1966
- v2 = `${o?.["value"]}`;
1967
- }
2042
+ const strict = this.getOption("features.useStrictValueComparison") === true;
2043
+ const map = this[optionsMapSymbol];
2044
+ const key = strict ? value : String(value);
1968
2045
 
1969
- if (v2 === v) {
1970
- return l;
1971
- }
2046
+ if (map && map.has(key)) {
2047
+ return map.get(key);
1972
2048
  }
1973
2049
 
1974
2050
  return undefined;
@@ -2113,9 +2189,55 @@ function initOptionObserver() {
2113
2189
 
2114
2190
  self.attachObserver(
2115
2191
  new Observer(function () {
2192
+ if (self[closeOnSelectAutoSymbol] === true) {
2193
+ self[closeOnSelectAutoSymbol] = false;
2194
+ if (
2195
+ self.hasAttribute("data-monster-option-features-closeonselect") ===
2196
+ false
2197
+ ) {
2198
+ const type = self.getOption("type");
2199
+ const shouldClose = type !== "checkbox";
2200
+ self.setOption("features.closeOnSelect", shouldClose);
2201
+ }
2202
+ }
2116
2203
  const options = self.getOption("options");
2117
- if (options !== self[optionsSnapshotSymbol]) {
2204
+ const strict =
2205
+ self.getOption("features.useStrictValueComparison") === true;
2206
+ const optionsVersion = self[optionsVersionSymbol] || 0;
2207
+
2208
+ if (
2209
+ options !== self[optionsSnapshotSymbol] ||
2210
+ strict !== self[strictModeSnapshotSymbol] ||
2211
+ optionsVersion !== self[optionsMapVersionSnapshotSymbol]
2212
+ ) {
2118
2213
  self[optionsSnapshotSymbol] = options;
2214
+ self[strictModeSnapshotSymbol] = strict;
2215
+ self[optionsMapVersionSnapshotSymbol] = optionsVersion;
2216
+
2217
+ const map = new Map();
2218
+ if (isArray(options)) {
2219
+ for (const o of options) {
2220
+ if (isPrimitive(o)) {
2221
+ const key = strict ? o : String(o);
2222
+ if (!map.has(key)) {
2223
+ map.set(key, o);
2224
+ }
2225
+ } else if (isObject(o)) {
2226
+ const v = o?.value;
2227
+ const l = o?.label;
2228
+ // If strict is true, use value as is (could be number, symbol, etc.)
2229
+ // If strict is false, use String(value) to allow loose matching
2230
+ if (v !== undefined) {
2231
+ const key = strict ? v : String(v);
2232
+ if (!map.has(key)) {
2233
+ map.set(key, l);
2234
+ }
2235
+ }
2236
+ }
2237
+ }
2238
+ }
2239
+ self[optionsMapSymbol] = map;
2240
+
2119
2241
  const version = bumpOptionsVersion.call(self);
2120
2242
  scheduleSelectionSync.call(self, version);
2121
2243
  }
@@ -2291,7 +2413,20 @@ function calcAndSetOptionsDimension() {
2291
2413
  }
2292
2414
 
2293
2415
  if (visible === 0) {
2294
- if (getFilterMode.call(this) === FILTER_MODE_DISABLED) {
2416
+ if (this.getOption("classes.statusOrRemoveBadge") === "error") {
2417
+ this.setOption(
2418
+ "messages.emptyOptions",
2419
+ this.getOption("labels.cannot-be-loaded"),
2420
+ );
2421
+ } else if (getFilterMode.call(this) === FILTER_MODE_DISABLED) {
2422
+ this.setOption(
2423
+ "messages.emptyOptions",
2424
+ this.getOption("labels.no-options-available"),
2425
+ );
2426
+ } else if (
2427
+ getFilterMode.call(this) === FILTER_MODE_REMOTE &&
2428
+ getCurrentFilterValue.call(this) === ""
2429
+ ) {
2295
2430
  this.setOption(
2296
2431
  "messages.emptyOptions",
2297
2432
  this.getOption("labels.no-options-available"),
@@ -2619,6 +2754,29 @@ function filterFromRemote() {
2619
2754
  * @returns {string}
2620
2755
  */
2621
2756
  function formatURL(url, params = {}) {
2757
+ const paramsDefaults = this.getOption("filter.paramsDefaults");
2758
+ const externalParams = this.getOption("filter.params");
2759
+ if (isObject(paramsDefaults) || isObject(externalParams)) {
2760
+ params = Object.assign(
2761
+ {},
2762
+ paramsDefaults || {},
2763
+ externalParams || {},
2764
+ params,
2765
+ );
2766
+ if (isObject(paramsDefaults)) {
2767
+ for (const key in paramsDefaults) {
2768
+ if (!Object.hasOwn(paramsDefaults, key)) continue;
2769
+ if (
2770
+ params[key] === "" ||
2771
+ params[key] === null ||
2772
+ params[key] === undefined
2773
+ ) {
2774
+ params[key] = paramsDefaults[key];
2775
+ }
2776
+ }
2777
+ }
2778
+ }
2779
+
2622
2780
  // Die Logik für den Default-Filterwert bleibt erhalten
2623
2781
  if (
2624
2782
  params.filter === undefined ||
@@ -2668,6 +2826,8 @@ function filterFromRemoteByValue(optionUrl, params, openPopper) {
2668
2826
  let url = formatURL.call(this, optionUrl, params);
2669
2827
 
2670
2828
  if (url.indexOf(disabledRequestMarker.toString()) !== -1) {
2829
+ this.setOption("total", null);
2830
+ resetPaginationState.call(this);
2671
2831
  return Promise.resolve();
2672
2832
  }
2673
2833
 
@@ -2682,6 +2842,10 @@ function filterFromRemoteByValue(optionUrl, params, openPopper) {
2682
2842
  }
2683
2843
  })
2684
2844
  .catch((e) => {
2845
+ if (getFilterMode.call(this) === FILTER_MODE_REMOTE) {
2846
+ this.setOption("total", null);
2847
+ resetPaginationState.call(this);
2848
+ }
2685
2849
  addErrorAttribute(this, e);
2686
2850
  setStatusOrRemoveBadges.call(this, "error");
2687
2851
  throw e;
@@ -2755,6 +2919,24 @@ function getFilterMode() {
2755
2919
  }
2756
2920
  }
2757
2921
 
2922
+ function getCurrentFilterValue() {
2923
+ if (this.getOption("filter.position") === FILTER_POSITION_INLINE) {
2924
+ if (this[inlineFilterElementSymbol] instanceof HTMLInputElement) {
2925
+ return this[inlineFilterElementSymbol].value?.trim() ?? "";
2926
+ }
2927
+ }
2928
+
2929
+ if (this[popperFilterElementSymbol] instanceof HTMLInputElement) {
2930
+ return this[popperFilterElementSymbol].value?.trim() ?? "";
2931
+ }
2932
+
2933
+ if (this[inlineFilterElementSymbol] instanceof HTMLInputElement) {
2934
+ return this[inlineFilterElementSymbol].value?.trim() ?? "";
2935
+ }
2936
+
2937
+ return "";
2938
+ }
2939
+
2758
2940
  /**
2759
2941
  * @private
2760
2942
  */
@@ -2930,6 +3112,10 @@ function clearSelection() {
2930
3112
  throw new Error("no shadow-root is defined");
2931
3113
  }
2932
3114
 
3115
+ if (this.hasAttribute("value")) {
3116
+ this.removeAttribute("value");
3117
+ }
3118
+
2933
3119
  setSelection
2934
3120
  .call(this, [])
2935
3121
  .then(() => {})
@@ -2996,6 +3182,12 @@ function areOptionsAvailableAndInitInternal() {
2996
3182
  (isArray(options) && options.length === 0)
2997
3183
  ) {
2998
3184
  setStatusOrRemoveBadges.call(this, "empty");
3185
+ if (getFilterMode.call(this) === FILTER_MODE_REMOTE) {
3186
+ if (this[isLoadingSymbol] !== true) {
3187
+ this.setOption("total", null);
3188
+ resetPaginationState.call(this);
3189
+ }
3190
+ }
2999
3191
 
3000
3192
  let msg = this.getOption("labels.no-options-available");
3001
3193
 
@@ -3035,7 +3227,9 @@ function areOptionsAvailableAndInitInternal() {
3035
3227
  if (this.getOption("features.emptyValueIfNoOptions") === true) {
3036
3228
  this.value = "";
3037
3229
  }
3038
- addErrorAttribute(this, "No options available.");
3230
+ if (this[isLoadingSymbol] !== true) {
3231
+ addErrorAttribute(this, "No options available.");
3232
+ }
3039
3233
  return false;
3040
3234
  }
3041
3235
 
@@ -3342,6 +3536,7 @@ function setSelection(selection) {
3342
3536
 
3343
3537
  checkOptionState.call(this);
3344
3538
  setSummaryAndControlText.call(this);
3539
+ setStatusOrRemoveBadges.call(this);
3345
3540
 
3346
3541
  if (valuesChanged) {
3347
3542
  try {
@@ -3622,7 +3817,7 @@ function initTotal() {
3622
3817
  try {
3623
3818
  const data = JSON.parse(String(text));
3624
3819
 
3625
- processAndApplyPaginationData.call(this, data);
3820
+ processAndApplyRemoteInfoTotal.call(this, data);
3626
3821
  } catch (e) {
3627
3822
  addErrorAttribute(this, e);
3628
3823
  }
@@ -3642,6 +3837,24 @@ function updatePagination(total, currentPage, objectsPerPage) {
3642
3837
  });
3643
3838
  }
3644
3839
 
3840
+ function resetPaginationState() {
3841
+ const paginationElement = this[paginationElementSymbol];
3842
+ if (!paginationElement) {
3843
+ return;
3844
+ }
3845
+
3846
+ paginationElement.style.display = "none";
3847
+ paginationElement.setOption("pages", null);
3848
+ paginationElement.setOption("currentPage", null);
3849
+ paginationElement.setOption("objectsPerPage", null);
3850
+ this.setOption("messages.total", "");
3851
+ }
3852
+
3853
+ function clearOptionsOnError() {
3854
+ this[cleanupOptionsListSymbol] = true;
3855
+ this.setOption("options", []);
3856
+ }
3857
+
3645
3858
  function refreshSelectPaginationLayout() {
3646
3859
  const paginationElement = this[paginationElementSymbol];
3647
3860
  if (!paginationElement || typeof paginationElement.getOption !== "function") {
@@ -3709,8 +3922,12 @@ function initEventHandler() {
3709
3922
  return value !== b.value;
3710
3923
  });
3711
3924
 
3712
- setSelection
3713
- .call(self, selection)
3925
+ const applyRemoval =
3926
+ Array.isArray(selection) && selection.length === 0
3927
+ ? clearSelection.call(self)
3928
+ : setSelection.call(self, selection);
3929
+
3930
+ applyRemoval
3714
3931
  .then(() => {
3715
3932
  fireCustomEvent(self, "monster-selection-removed", {
3716
3933
  value,
@@ -3771,6 +3988,10 @@ function initEventHandler() {
3771
3988
  };
3772
3989
 
3773
3990
  self[keyEventHandler] = (event) => {
3991
+ if (event?.monsterFilterHandled === true) {
3992
+ return;
3993
+ }
3994
+
3774
3995
  const path = event.composedPath();
3775
3996
  const element = path.shift();
3776
3997
 
@@ -3808,6 +4029,23 @@ function initEventHandler() {
3808
4029
  }
3809
4030
  };
3810
4031
 
4032
+ const attachFilterKeyListener = (element) => {
4033
+ if (!(element instanceof HTMLElement)) {
4034
+ return;
4035
+ }
4036
+
4037
+ element.addEventListener("keydown", (event) => {
4038
+ if (event?.monsterFilterHandled === true) {
4039
+ return;
4040
+ }
4041
+ event.monsterFilterHandled = true;
4042
+ handleFilterKeyboardEvents.call(self, event);
4043
+ });
4044
+ };
4045
+
4046
+ attachFilterKeyListener(this[inlineFilterElementSymbol]);
4047
+ attachFilterKeyListener(this[popperFilterElementSymbol]);
4048
+
3811
4049
  const types = self.getOption("toggleEventType", ["click"]);
3812
4050
 
3813
4051
  for (const [, type] of Object.entries(types)) {
@@ -3960,6 +4198,20 @@ function setStatusOrRemoveBadges(suggestion) {
3960
4198
  return;
3961
4199
  }
3962
4200
 
4201
+ if (current === "clear") {
4202
+ const options = this.getOption("options");
4203
+ if (
4204
+ options === undefined ||
4205
+ options === null ||
4206
+ (isArray(options) && options.length === 0)
4207
+ ) {
4208
+ this.setOption("classes.statusOrRemoveBadge", "empty");
4209
+ } else {
4210
+ this.setOption("classes.statusOrRemoveBadge", "closed");
4211
+ }
4212
+ return;
4213
+ }
4214
+
3963
4215
  const options = this.getOption("options");
3964
4216
  if (
3965
4217
  options === undefined ||
@@ -29,6 +29,7 @@ export * from "./components/layout/width-toggle.mjs";
29
29
  export * from "./components/layout/board.mjs";
30
30
  export * from "./components/layout/panel.mjs";
31
31
  export * from "./components/layout/details.mjs";
32
+ export * from "./components/layout/vertical-tabs.mjs";
32
33
  export * from "./components/layout/slider.mjs";
33
34
  export * from "./components/content/fetch-box.mjs";
34
35
  export * from "./components/content/viewer.mjs";
@@ -73,6 +74,7 @@ export * from "./components/form/action-button.mjs";
73
74
  export * from "./components/form/form.mjs";
74
75
  export * from "./components/form/repeat-field-set.mjs";
75
76
  export * from "./components/form/api-button.mjs";
77
+ export * from "./components/form/select-link.mjs";
76
78
  export * from "./components/form/digits.mjs";
77
79
  export * from "./components/form/tree-select.mjs";
78
80
  export * from "./components/form/popper-button.mjs";
@@ -184,6 +184,122 @@ describe('Select', function () {
184
184
 
185
185
  });
186
186
 
187
+ describe('Remote filter pagination', function () {
188
+ let requestCount = 0;
189
+
190
+ beforeEach((done) => {
191
+ let mocks = document.getElementById('mocks');
192
+ mocks.innerHTML = html1;
193
+
194
+ requestCount = 0;
195
+ global['fetch'] = function () {
196
+ requestCount += 1;
197
+ let headers = new Map;
198
+ headers.set('content-type', 'application/json');
199
+
200
+ if (requestCount === 1) {
201
+ return Promise.resolve({
202
+ ok: true,
203
+ status: 200,
204
+ headers: headers,
205
+ text: function () {
206
+ return Promise.resolve(JSON.stringify({
207
+ items: [
208
+ {id: 1, name: "Alpha"}
209
+ ],
210
+ pagination: {
211
+ total: 2,
212
+ page: 1,
213
+ perPage: 1
214
+ }
215
+ }));
216
+ }
217
+ });
218
+ }
219
+
220
+ return Promise.resolve({
221
+ ok: false,
222
+ status: 500,
223
+ headers: headers,
224
+ text: function () {
225
+ return Promise.resolve(JSON.stringify({}));
226
+ }
227
+ });
228
+ };
229
+
230
+ done();
231
+ });
232
+
233
+ afterEach(() => {
234
+ let mocks = document.getElementById('mocks');
235
+ mocks.innerHTML = "";
236
+ global['fetch'] = fetchReference;
237
+ });
238
+
239
+ it('should reset pagination and clear options on fetch error', function (done) {
240
+ this.timeout(4000);
241
+
242
+ let mocks = document.getElementById('mocks');
243
+ const select = document.createElement('monster-select');
244
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
245
+ select.setOption('filter.mode', 'remote');
246
+ select.setOption('mapping.selector', 'items.*');
247
+ select.setOption('mapping.labelTemplate', '${name}');
248
+ select.setOption('mapping.valueTemplate', '${id}');
249
+ select.setOption('mapping.total', 'pagination.total');
250
+ select.setOption('mapping.currentPage', 'pagination.page');
251
+ select.setOption('mapping.objectsPerPage', 'pagination.perPage');
252
+ mocks.appendChild(select);
253
+
254
+ const pagination = () => select.shadowRoot.querySelector('[data-monster-role=pagination]');
255
+
256
+ const triggerFilter = (value) => {
257
+ const filterInput = select.shadowRoot.querySelector('[data-monster-role=filter]');
258
+ filterInput.value = value;
259
+ filterInput.dispatchEvent(new KeyboardEvent('keydown', {
260
+ code: 'KeyA',
261
+ bubbles: true
262
+ }));
263
+ };
264
+
265
+ select.addEventListener('monster-options-set', () => {
266
+ setTimeout(() => {
267
+ try {
268
+ const options = select.getOption('options');
269
+ expect(options.length).to.equal(1);
270
+ expect(pagination().getOption('currentPage')).to.equal(1);
271
+ expect(pagination().getOption('pages')).to.equal(2);
272
+ expect(pagination().getOption('objectsPerPage')).to.equal(1);
273
+
274
+ triggerFilter('b');
275
+
276
+ setTimeout(() => {
277
+ try {
278
+ const optionsAfterError = select.getOption('options');
279
+ expect(optionsAfterError.length).to.equal(0);
280
+ expect(pagination().getOption('currentPage')).to.equal(null);
281
+ expect(pagination().getOption('pages')).to.equal(null);
282
+ expect(pagination().getOption('objectsPerPage')).to.equal(null);
283
+ expect(select.getOption('total')).to.equal(null);
284
+ expect(select.getOption('messages.total')).to.equal("");
285
+ } catch (e) {
286
+ return done(e);
287
+ }
288
+
289
+ done();
290
+ }, 500);
291
+ } catch (e) {
292
+ done(e);
293
+ }
294
+ }, 50);
295
+ }, {once: true});
296
+
297
+ setTimeout(() => {
298
+ triggerFilter('a');
299
+ }, 0);
300
+ });
301
+ });
302
+
187
303
  describe('document.createElement()', function () {
188
304
 
189
305
  afterEach(() => {