@schukai/monster 4.142.4 → 4.143.1

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/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"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.142.4"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"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.143.1"}
@@ -76,7 +76,7 @@ import {
76
76
  import { getDocumentTranslations } from "../../i18n/translations.mjs";
77
77
  import "../state/state.mjs";
78
78
  import "../host/collapse.mjs";
79
- import { generateUniqueConfigKey } from "../host/util.mjs";
79
+ import { generateComponentConfigKey } from "../host/util.mjs";
80
80
 
81
81
  import "./datasource/dom.mjs";
82
82
  import "./datasource/rest.mjs";
@@ -279,6 +279,10 @@ class DataTable extends CustomElement {
279
279
  "row-key": null,
280
280
  "filter-id": null,
281
281
  },
282
+
283
+ config: {
284
+ instanceKey: null,
285
+ },
282
286
  },
283
287
  initOptionsFromArguments.call(this),
284
288
  );
@@ -659,15 +663,21 @@ class DataTable extends CustomElement {
659
663
  * @return {string}
660
664
  */
661
665
  function getColumnVisibilityConfigKey() {
662
- return generateUniqueConfigKey("datatable", this?.id, "columns-visibility");
666
+ return generateComponentConfigKey("datatable", this?.id, "columns-visibility", {
667
+ instanceKey: this.getOption("config.instanceKey"),
668
+ });
663
669
  }
664
670
 
665
671
  /**
666
672
  * @private
667
673
  * @return {string}
668
674
  */
669
- function getFilterConfigKey() {
670
- return generateUniqueConfigKey("datatable", this?.id, "filter");
675
+ function hasConfigIdentity() {
676
+ return (
677
+ (isString(this.id) && this.id !== "") ||
678
+ (isString(this.getOption("config.instanceKey")) &&
679
+ this.getOption("config.instanceKey").trim() !== "")
680
+ );
671
681
  }
672
682
 
673
683
  /**
@@ -682,8 +692,11 @@ function getHostConfig(callback) {
682
692
  return Promise.resolve({});
683
693
  }
684
694
 
685
- if (!this.id) {
686
- addErrorAttribute(this, "no id found; id is required for config");
695
+ if (!hasConfigIdentity.call(this)) {
696
+ addErrorAttribute(
697
+ this,
698
+ "no id or config.instanceKey found; one is required for config",
699
+ );
687
700
  return Promise.resolve({});
688
701
  }
689
702
 
@@ -782,7 +795,7 @@ function updateConfigColumnBar() {
782
795
  }
783
796
 
784
797
  const host = findElementWithSelectorUpwards(this, "monster-host");
785
- if (!(host && this.id)) {
798
+ if (!(host && hasConfigIdentity.call(this))) {
786
799
  return;
787
800
  }
788
801
  const configKey = getColumnVisibilityConfigKey.call(this);
@@ -1616,7 +1629,9 @@ function getTranslations() {
1616
1629
  * @return {string}
1617
1630
  */
1618
1631
  export function getStoredOrderConfigKey() {
1619
- return generateUniqueConfigKey("datatable", this?.id, "stored-order");
1632
+ return generateComponentConfigKey("datatable", this?.id, "stored-order", {
1633
+ instanceKey: this.getOption("config.instanceKey"),
1634
+ });
1620
1635
  }
1621
1636
 
1622
1637
  /**
@@ -1629,7 +1644,7 @@ function storeOrderStatement(doFetch) {
1629
1644
  setDataSource.call(this, { order: statement }, doFetch);
1630
1645
 
1631
1646
  const host = findElementWithSelectorUpwards(this, "monster-host");
1632
- if (!(host && this.id)) {
1647
+ if (!(host && hasConfigIdentity.call(this))) {
1633
1648
  return;
1634
1649
  }
1635
1650
 
@@ -12,14 +12,16 @@
12
12
  * SPDX-License-Identifier: AGPL-3.0
13
13
  */
14
14
 
15
- import { generateUniqueConfigKey } from "../../host/util.mjs";
15
+ import { generateComponentConfigKey } from "../../host/util.mjs";
16
16
 
17
17
  /**
18
18
  * @private
19
19
  * @return {string}
20
20
  */
21
21
  export function getFilterConfigKey() {
22
- return generateUniqueConfigKey("datatable", this?.id, "filter");
22
+ return generateComponentConfigKey("datatable", this?.id, "filter", {
23
+ instanceKey: this.getOption("config.instanceKey"),
24
+ });
23
25
  }
24
26
 
25
27
  /**
@@ -27,7 +29,9 @@ export function getFilterConfigKey() {
27
29
  * @return {string}
28
30
  */
29
31
  export function getStoredFilterConfigKey() {
30
- return generateUniqueConfigKey("datatable", this?.id, "stored-filter");
32
+ return generateComponentConfigKey("datatable", this?.id, "stored-filter", {
33
+ instanceKey: this.getOption("config.instanceKey"),
34
+ });
31
35
  }
32
36
 
33
37
  /**
@@ -293,6 +293,10 @@ class Filter extends CustomElement {
293
293
  selector: "",
294
294
  },
295
295
 
296
+ config: {
297
+ instanceKey: null,
298
+ },
299
+
296
300
  timeouts: {
297
301
  message: 4000,
298
302
  },
@@ -915,7 +919,7 @@ function initEventHandler() {
915
919
  .then(() => {
916
920
  const configKey = getStoredFilterConfigKey.call(self);
917
921
  const host = getDocument().querySelector("monster-host");
918
- if (!host) {
922
+ if (!(host && hasConfigIdentity.call(self))) {
919
923
  return;
920
924
  }
921
925
 
@@ -1070,7 +1074,7 @@ function initTabEvents() {
1070
1074
  }
1071
1075
 
1072
1076
  const host = findElementWithSelectorUpwards(this, "monster-host");
1073
- if (!(host && this.id)) {
1077
+ if (!(host && hasConfigIdentity.call(this))) {
1074
1078
  return;
1075
1079
  }
1076
1080
 
@@ -1111,7 +1115,7 @@ function updateFilterTabs() {
1111
1115
  }
1112
1116
 
1113
1117
  const host = findElementWithSelectorUpwards(this, "monster-host");
1114
- if (!(host && this.id)) {
1118
+ if (!(host && hasConfigIdentity.call(this))) {
1115
1119
  return;
1116
1120
  }
1117
1121
 
@@ -1580,7 +1584,7 @@ function getControlValuesFromLabel(label) {
1580
1584
  function initFromConfig() {
1581
1585
  const host = findElementWithSelectorUpwards(this, "monster-host");
1582
1586
 
1583
- if (!(isInstance(host, Host) && this.id)) {
1587
+ if (!(isInstance(host, Host) && hasConfigIdentity.call(this))) {
1584
1588
  return Promise.resolve();
1585
1589
  }
1586
1590
 
@@ -1622,7 +1626,7 @@ function initFromConfig() {
1622
1626
  */
1623
1627
  function updateConfig() {
1624
1628
  const host = findElementWithSelectorUpwards(this, "monster-host");
1625
- if (!(host && this.id)) {
1629
+ if (!(host && hasConfigIdentity.call(this))) {
1626
1630
  return;
1627
1631
  }
1628
1632
  const configKey = getFilterConfigKey.call(this);
@@ -1634,6 +1638,18 @@ function updateConfig() {
1634
1638
  }
1635
1639
  }
1636
1640
 
1641
+ /**
1642
+ * @private
1643
+ * @return {boolean}
1644
+ */
1645
+ function hasConfigIdentity() {
1646
+ return (
1647
+ (isString(this.id) && this.id !== "") ||
1648
+ (isString(this.getOption("config.instanceKey")) &&
1649
+ this.getOption("config.instanceKey").trim() !== "")
1650
+ );
1651
+ }
1652
+
1637
1653
  /**
1638
1654
  * @private
1639
1655
  * @return {string}
@@ -145,6 +145,8 @@ class Pagination extends CustomElement {
145
145
  * @property {number} currentPage Current page
146
146
  * @property {number} pages Pages
147
147
  * @property {number} objectsPerPage Objects per page
148
+ * @property {Object} layout Layout configuration
149
+ * @property {"auto"|"compact"} layout.mode Layout mode. Use `"compact"` to always show the small arrow pagination.
148
150
  * @property {Object} mapping Mapping
149
151
  * @property {string} mapping.pages Pages mapping
150
152
  * @property {string} mapping.objectsPerPage Objects per page mapping
@@ -175,6 +177,10 @@ class Pagination extends CustomElement {
175
177
  objectsPerPage: 20,
176
178
  currentPage: null,
177
179
 
180
+ layout: {
181
+ mode: "auto",
182
+ },
183
+
178
184
  mapping: {
179
185
  pages: "sys.pagination.pages",
180
186
  objectsPerPage: "sys.pagination.objectsPerPage",
@@ -250,6 +256,11 @@ class Pagination extends CustomElement {
250
256
  }
251
257
 
252
258
  refreshLayout({ schedule = false } = {}) {
259
+ if (isForcedCompactPagination.call(this)) {
260
+ applyForcedCompactPaginationLayout.call(this);
261
+ return this;
262
+ }
263
+
253
264
  if (schedule === true) {
254
265
  if (isAdaptivePagination.call(this)) {
255
266
  schedulePaginationLayoutUpdate.call(this);
@@ -363,7 +374,15 @@ class Pagination extends CustomElement {
363
374
  });
364
375
  });
365
376
 
366
- if (isAdaptivePagination.call(this)) {
377
+ if (isForcedCompactPagination.call(this)) {
378
+ this[sizeDataSymbol] = {
379
+ last: {
380
+ parentWidth: 0,
381
+ },
382
+ showNumbers: false,
383
+ };
384
+ applyForcedCompactPaginationLayout.call(this);
385
+ } else if (isAdaptivePagination.call(this)) {
367
386
  this[sizeDataSymbol] = {
368
387
  last: {
369
388
  parentWidth: 0,
@@ -877,6 +896,11 @@ function syncPaginationStateToDom(pagination) {
877
896
  * @private
878
897
  */
879
898
  function schedulePaginationLayoutUpdate() {
899
+ if (isForcedCompactPagination.call(this)) {
900
+ applyForcedCompactPaginationLayout.call(this);
901
+ return;
902
+ }
903
+
880
904
  if (!isAdaptivePagination.call(this)) {
881
905
  return;
882
906
  }
@@ -895,6 +919,11 @@ function schedulePaginationLayoutUpdate() {
895
919
  * @private
896
920
  */
897
921
  function applyPaginationLayout() {
922
+ if (isForcedCompactPagination.call(this)) {
923
+ applyForcedCompactPaginationLayout.call(this);
924
+ return;
925
+ }
926
+
898
927
  if (!this.isConnected || !this.shadowRoot) {
899
928
  return;
900
929
  }
@@ -974,6 +1003,51 @@ function applyPaginationLayout() {
974
1003
  }
975
1004
  }
976
1005
 
1006
+ /**
1007
+ * @private
1008
+ */
1009
+ function applyForcedCompactPaginationLayout() {
1010
+ if (!this.isConnected || !this.shadowRoot) {
1011
+ return;
1012
+ }
1013
+
1014
+ if (this[layoutApplySymbol]) {
1015
+ return;
1016
+ }
1017
+
1018
+ this[layoutApplySymbol] = true;
1019
+ try {
1020
+ ensureLabelState.call(this);
1021
+
1022
+ const list = this.shadowRoot.querySelector(".pagination-list");
1023
+ if (!list) {
1024
+ return;
1025
+ }
1026
+
1027
+ const prevLink = this.shadowRoot.querySelector(
1028
+ "a[data-monster-role=pagination-prev]",
1029
+ );
1030
+ const nextLink = this.shadowRoot.querySelector(
1031
+ "a[data-monster-role=pagination-next]",
1032
+ );
1033
+
1034
+ setClassEnabled(list, "pagination-no-wrap", true);
1035
+ applyPaginationMode.call(this, list, prevLink, nextLink, "compact");
1036
+ setAttributeValue(list, "data-monster-adaptive-ready", "true");
1037
+ this[layoutModeSymbol] = "compact";
1038
+ } finally {
1039
+ this[layoutApplySymbol] = false;
1040
+ }
1041
+ }
1042
+
1043
+ /**
1044
+ * @private
1045
+ * @return {boolean}
1046
+ */
1047
+ function isForcedCompactPagination() {
1048
+ return this.getOption("layout.mode") === "compact";
1049
+ }
1050
+
977
1051
  /**
978
1052
  * @private
979
1053
  * @param {number} parentWidth
@@ -5769,7 +5769,8 @@ function getTemplate() {
5769
5769
  <div part="options" data-monster-role="options" data-monster-insert="options path:options"
5770
5770
  tabindex="-1"></div>
5771
5771
  </div>
5772
- <monster-pagination data-monster-role="pagination" part="pagination"></monster-pagination>
5772
+ <monster-pagination data-monster-role="pagination" part="pagination"
5773
+ data-monster-option-layout-mode="compact"></monster-pagination>
5773
5774
  <div part="remote-info"
5774
5775
  data-monster-role="remote-info"
5775
5776
  data-monster-attributes="class path:classes.remoteInfo"
@@ -21,6 +21,7 @@ import { ConfigManagerStyleSheet } from "./stylesheet/config-manager.mjs";
21
21
  import { getWindow } from "../../dom/util.mjs";
22
22
  import { instanceSymbol } from "../../constants.mjs";
23
23
  import { diff } from "../../data/diff.mjs";
24
+ import { isFunction, isObject } from "../../types/is.mjs";
24
25
 
25
26
  export { ConfigManager };
26
27
 
@@ -108,17 +109,42 @@ class ConfigManager extends CustomElement {
108
109
  keyPath: "key",
109
110
  },
110
111
  },
112
+ storage: {
113
+ adapter: null,
114
+ serverAuthoritative: false,
115
+ },
111
116
  });
112
117
  }
113
118
 
119
+ /**
120
+ * Register an external host configuration storage adapter.
121
+ *
122
+ * Supported adapter methods are getConfig/hasConfig/setConfig/deleteConfig
123
+ * or their short aliases get/has/set/delete. If managesConfigKey, managesKey
124
+ * or manages is present, it is used to decide whether a key is handled by
125
+ * the adapter. Without such a method, an adapter handles all keys.
126
+ *
127
+ * @param {Object|null} adapter
128
+ * @return {ConfigManager}
129
+ */
130
+ setStorageAdapter(adapter) {
131
+ this.setOption("storage.adapter", adapter);
132
+ return this;
133
+ }
134
+
135
+ /**
136
+ * @return {Object|null}
137
+ */
138
+ getStorageAdapter() {
139
+ return getStorageAdapter.call(this);
140
+ }
141
+
114
142
  /**
115
143
  * @param {string} key
116
144
  * @return {Promise<unknown>}
117
145
  */
118
146
  getConfig(key) {
119
- return this.ready().then(() => {
120
- return getBlob.call(this, key);
121
- });
147
+ return this.ready().then(() => readConfig.call(this, key));
122
148
  }
123
149
 
124
150
  /**
@@ -126,16 +152,7 @@ class ConfigManager extends CustomElement {
126
152
  * @return {Promise<boolean>}
127
153
  */
128
154
  hasConfig(key) {
129
- return this.ready()
130
- .then(() => {
131
- return getBlob.call(this, key);
132
- })
133
- .then(() => {
134
- return true;
135
- })
136
- .catch(() => {
137
- return false;
138
- });
155
+ return this.ready().then(() => hasConfig.call(this, key));
139
156
  }
140
157
 
141
158
  /**
@@ -144,28 +161,11 @@ class ConfigManager extends CustomElement {
144
161
  * @return {Promise<unknown>}
145
162
  */
146
163
  setConfig(key, value) {
147
- return this.ready().then(() => {
148
- return getBlob
149
- .call(this, key)
150
- .then((storedValue) => {
151
- if (diff(storedValue, value).length === 0) {
152
- return;
153
- }
154
- return setBlob.call(this, key, value);
155
- })
156
- .catch((error) => {
157
- if (error?.message?.match(/is not defined/)) {
158
- return setBlob.call(this, key, value);
159
- }
160
- throw error;
161
- });
162
- });
164
+ return this.ready().then(() => writeConfig.call(this, key, value));
163
165
  }
164
166
 
165
167
  deleteConfig(key) {
166
- return this.ready().then(() => {
167
- return deleteBlob.call(this, key);
168
- });
168
+ return this.ready().then(() => removeConfig.call(this, key));
169
169
  }
170
170
 
171
171
  /**
@@ -190,6 +190,192 @@ class ConfigManager extends CustomElement {
190
190
  }
191
191
  }
192
192
 
193
+ /**
194
+ * @private
195
+ * @return {Object|null}
196
+ */
197
+ function getStorageAdapter() {
198
+ const adapter = this.getOption("storage.adapter");
199
+ return isObject(adapter) ? adapter : null;
200
+ }
201
+
202
+ /**
203
+ * @private
204
+ * @param {Object} adapter
205
+ * @param {string} key
206
+ * @return {boolean}
207
+ */
208
+ function adapterManagesKey(adapter, key) {
209
+ for (const methodName of ["managesConfigKey", "managesKey", "manages"]) {
210
+ if (isFunction(adapter?.[methodName])) {
211
+ return adapter[methodName](key) === true;
212
+ }
213
+ }
214
+
215
+ return true;
216
+ }
217
+
218
+ /**
219
+ * @private
220
+ * @param {Object} adapter
221
+ * @param {string[]} methodNames
222
+ * @param {Array} args
223
+ * @return {Promise<unknown>}
224
+ */
225
+ function callAdapter(adapter, methodNames, args) {
226
+ for (const methodName of methodNames) {
227
+ if (isFunction(adapter?.[methodName])) {
228
+ return Promise.resolve(adapter[methodName](...args));
229
+ }
230
+ }
231
+
232
+ return Promise.reject(new Error("The storage adapter method is not defined."));
233
+ }
234
+
235
+ /**
236
+ * @private
237
+ * @return {boolean}
238
+ */
239
+ function isServerAuthoritative() {
240
+ return this.getOption("storage.serverAuthoritative") === true;
241
+ }
242
+
243
+ /**
244
+ * @private
245
+ * @param {string} key
246
+ * @return {Promise<unknown>}
247
+ */
248
+ function readConfig(key) {
249
+ const adapter = getStorageAdapter.call(this);
250
+ const managed = adapter && adapterManagesKey(adapter, key);
251
+
252
+ if (managed) {
253
+ return callAdapter(adapter, ["getConfig", "get"], [key]).catch((error) => {
254
+ if (isServerAuthoritative.call(this)) {
255
+ throw error;
256
+ }
257
+ return getBlob.call(this, key);
258
+ });
259
+ }
260
+
261
+ return getBlob.call(this, key);
262
+ }
263
+
264
+ /**
265
+ * @private
266
+ * @param {string} key
267
+ * @return {Promise<boolean>}
268
+ */
269
+ function hasConfig(key) {
270
+ const adapter = getStorageAdapter.call(this);
271
+ const managed = adapter && adapterManagesKey(adapter, key);
272
+
273
+ if (managed) {
274
+ return callAdapter(adapter, ["hasConfig", "has"], [key])
275
+ .catch(() => {
276
+ return callAdapter(adapter, ["getConfig", "get"], [key]).then(
277
+ () => true,
278
+ );
279
+ })
280
+ .then((result) => {
281
+ if (result === true || isServerAuthoritative.call(this)) {
282
+ return result === true;
283
+ }
284
+ return hasLocalConfig.call(this, key);
285
+ })
286
+ .catch(() => {
287
+ if (isServerAuthoritative.call(this)) {
288
+ return false;
289
+ }
290
+ return hasLocalConfig.call(this, key);
291
+ });
292
+ }
293
+
294
+ return hasLocalConfig.call(this, key);
295
+ }
296
+
297
+ /**
298
+ * @private
299
+ * @param {string} key
300
+ * @return {Promise<boolean>}
301
+ */
302
+ function hasLocalConfig(key) {
303
+ return getBlob
304
+ .call(this, key)
305
+ .then(() => true)
306
+ .catch(() => false);
307
+ }
308
+
309
+ /**
310
+ * @private
311
+ * @param {string} key
312
+ * @param {*} value
313
+ * @return {Promise<unknown>}
314
+ */
315
+ function writeConfig(key, value) {
316
+ const adapter = getStorageAdapter.call(this);
317
+ const managed = adapter && adapterManagesKey(adapter, key);
318
+
319
+ if (managed) {
320
+ return callAdapter(adapter, ["setConfig", "set"], [key, value]).catch(
321
+ (error) => {
322
+ if (isServerAuthoritative.call(this)) {
323
+ throw error;
324
+ }
325
+ return writeLocalConfig.call(this, key, value);
326
+ },
327
+ );
328
+ }
329
+
330
+ return writeLocalConfig.call(this, key, value);
331
+ }
332
+
333
+ /**
334
+ * @private
335
+ * @param {string} key
336
+ * @param {*} value
337
+ * @return {Promise<unknown>}
338
+ */
339
+ function writeLocalConfig(key, value) {
340
+ return getBlob
341
+ .call(this, key)
342
+ .then((storedValue) => {
343
+ if (diff(storedValue, value).length === 0) {
344
+ return;
345
+ }
346
+ return setBlob.call(this, key, value);
347
+ })
348
+ .catch((error) => {
349
+ if (error?.message?.match(/is not defined/)) {
350
+ return setBlob.call(this, key, value);
351
+ }
352
+ throw error;
353
+ });
354
+ }
355
+
356
+ /**
357
+ * @private
358
+ * @param {string} key
359
+ * @return {Promise<unknown>}
360
+ */
361
+ function removeConfig(key) {
362
+ const adapter = getStorageAdapter.call(this);
363
+ const managed = adapter && adapterManagesKey(adapter, key);
364
+
365
+ if (managed) {
366
+ return callAdapter(adapter, ["deleteConfig", "delete", "remove"], [
367
+ key,
368
+ ]).catch((error) => {
369
+ if (isServerAuthoritative.call(this)) {
370
+ throw error;
371
+ }
372
+ return deleteBlob.call(this, key);
373
+ });
374
+ }
375
+
376
+ return deleteBlob.call(this, key);
377
+ }
378
+
193
379
  /**
194
380
  * @private
195
381
  * @returns {Promise<unknown>}
@@ -206,6 +392,10 @@ function openDatabase() {
206
392
  throw new Error("The database name and version must be set.");
207
393
  }
208
394
 
395
+ if (!window.indexedDB) {
396
+ return Promise.resolve(null);
397
+ }
398
+
209
399
  const request = window.indexedDB.open(name, version);
210
400
 
211
401
  return new Promise((resolve, reject) => {
@@ -194,6 +194,32 @@ class Host extends CustomElement {
194
194
  return this[configManagerElementSymbol].setConfig(key, value);
195
195
  }
196
196
 
197
+ /**
198
+ * Register an external storage adapter for host configuration.
199
+ *
200
+ * @param {Object|null} adapter
201
+ * @return {Host}
202
+ */
203
+ setConfigStorageAdapter(adapter) {
204
+ if (this[configManagerElementSymbol] instanceof HTMLElement === false) {
205
+ throw new Error("There is no config manager element");
206
+ }
207
+
208
+ this[configManagerElementSymbol].setStorageAdapter(adapter);
209
+ return this;
210
+ }
211
+
212
+ /**
213
+ * @return {Object|null}
214
+ */
215
+ getConfigStorageAdapter() {
216
+ if (this[configManagerElementSymbol] instanceof HTMLElement === false) {
217
+ throw new Error("There is no config manager element");
218
+ }
219
+
220
+ return this[configManagerElementSymbol].getStorageAdapter();
221
+ }
222
+
197
223
  /**
198
224
  * @private
199
225
  * @fires Host#monster-host-connected
@@ -13,8 +13,14 @@
13
13
  */
14
14
 
15
15
  import { getWindow } from "../../dom/util.mjs";
16
+ import { isString } from "../../types/is.mjs";
16
17
 
17
- export { generateUniqueConfigKey };
18
+ export {
19
+ generateConfigKey,
20
+ generateComponentConfigKey,
21
+ generateUniqueConfigKey,
22
+ sanitizeConfigKey,
23
+ };
18
24
 
19
25
  /**
20
26
  * Generate a unique configuration key based on the current browser location,
@@ -38,5 +44,49 @@ function generateUniqueConfigKey(componentName, id, prefix) {
38
44
  const uniqueKey = `${prefix}_${urlWithoutParamsAndHash}_${componentName}_${id}`;
39
45
 
40
46
  // Replace any special characters and spaces with underscores
41
- return uniqueKey.replace(/[^\w\s]/gi, "_").replace(/\s+/g, "_");
47
+ return sanitizeConfigKey(uniqueKey);
48
+ }
49
+
50
+ /**
51
+ * Generate a stable host configuration key from an explicit technical instance key.
52
+ *
53
+ * @param {string} componentName - The name of the component.
54
+ * @param {string} instanceKey - Technical, host-defined instance key.
55
+ * @param {string} scope - Configuration scope, for example "filter".
56
+ * @return {string}
57
+ */
58
+ function generateConfigKey(componentName, instanceKey, scope) {
59
+ return sanitizeConfigKey(`${scope}_${componentName}_${instanceKey}`);
60
+ }
61
+
62
+ /**
63
+ * Generate a component host configuration key.
64
+ *
65
+ * If an explicit instance key is provided, the key is independent from the
66
+ * current URL and DOM id. Without an instance key, this keeps the historical
67
+ * URL-derived key for backwards compatibility.
68
+ *
69
+ * @param {string} componentName
70
+ * @param {string} id
71
+ * @param {string} scope
72
+ * @param {object} [options]
73
+ * @param {string} [options.instanceKey]
74
+ * @return {string}
75
+ */
76
+ function generateComponentConfigKey(componentName, id, scope, options = {}) {
77
+ if (isString(options.instanceKey) && options.instanceKey.trim() !== "") {
78
+ return generateConfigKey(componentName, options.instanceKey.trim(), scope);
79
+ }
80
+
81
+ return generateUniqueConfigKey(componentName, id, scope);
82
+ }
83
+
84
+ /**
85
+ * @param {string} key
86
+ * @return {string}
87
+ */
88
+ function sanitizeConfigKey(key) {
89
+ return String(key)
90
+ .replace(/[^\w\s]/gi, "_")
91
+ .replace(/\s+/g, "_");
42
92
  }
@@ -0,0 +1,47 @@
1
+ import { expect } from "chai";
2
+ import { initJSDOM } from "../../../util/jsdom.mjs";
3
+
4
+ let getStoredOrderConfigKey;
5
+ let getFilterConfigKey;
6
+ let getStoredFilterConfigKey;
7
+
8
+ describe("Datatable config keys", function () {
9
+ before(function (done) {
10
+ initJSDOM().then(() => {
11
+ import("../../../../source/components/datatable/datatable.mjs")
12
+ .then((m) => {
13
+ getStoredOrderConfigKey = m.getStoredOrderConfigKey;
14
+ return import(
15
+ "../../../../source/components/datatable/filter/util.mjs"
16
+ );
17
+ })
18
+ .then((m) => {
19
+ getFilterConfigKey = m.getFilterConfigKey;
20
+ getStoredFilterConfigKey = m.getStoredFilterConfigKey;
21
+ done();
22
+ })
23
+ .catch(done);
24
+ });
25
+ });
26
+
27
+ it("uses the same explicit instance-key contract for filter, stored filters and sorting", function () {
28
+ const context = {
29
+ id: "dom-id",
30
+ getOption(path) {
31
+ if (path === "config.instanceKey") {
32
+ return "orders.default";
33
+ }
34
+ },
35
+ };
36
+
37
+ expect(getFilterConfigKey.call(context)).to.equal(
38
+ "filter_datatable_orders_default",
39
+ );
40
+ expect(getStoredFilterConfigKey.call(context)).to.equal(
41
+ "stored_filter_datatable_orders_default",
42
+ );
43
+ expect(getStoredOrderConfigKey.call(context)).to.equal(
44
+ "stored_order_datatable_orders_default",
45
+ );
46
+ });
47
+ });
@@ -156,6 +156,38 @@ describe('Select', function () {
156
156
  expect(cssText.replace(/\s+/g, '')).to.contain('z-index:var(--monster-z-index-popover)');
157
157
  });
158
158
 
159
+ it('should render select pagination in the compact arrow mode immediately', async function () {
160
+ const mocks = document.getElementById('mocks');
161
+ const select = document.createElement('monster-select');
162
+
163
+ mocks.appendChild(select);
164
+
165
+ await waitForCondition(() => {
166
+ return select.shadowRoot?.querySelector('[data-monster-role="pagination"]');
167
+ });
168
+
169
+ const pagination = select.shadowRoot.querySelector('[data-monster-role="pagination"]');
170
+ expect(pagination.getOption('layout.mode')).to.equal('compact');
171
+
172
+ pagination.style.display = 'block';
173
+ pagination.setPaginationState({currentPage: 2, totalPages: 5});
174
+
175
+ await waitForCondition(() => {
176
+ const list = pagination.shadowRoot?.querySelector('.pagination-list');
177
+ const previous = pagination.shadowRoot?.querySelector(
178
+ 'a[data-monster-role="pagination-prev"]'
179
+ );
180
+ const next = pagination.shadowRoot?.querySelector(
181
+ 'a[data-monster-role="pagination-next"]'
182
+ );
183
+
184
+ return list?.classList.contains('pagination-compact') &&
185
+ list.getAttribute('data-monster-adaptive-ready') === 'true' &&
186
+ previous?.querySelector('svg') &&
187
+ next?.querySelector('svg');
188
+ });
189
+ });
190
+
159
191
  it('should deduplicate floating layout queue jobs by popper element', async function () {
160
192
  const popper = document.createElement('div');
161
193
  const reasons = [];
@@ -0,0 +1,86 @@
1
+ import * as chai from "chai";
2
+ import { initJSDOM } from "../../../util/jsdom.mjs";
3
+
4
+ const expect = chai.expect;
5
+
6
+ describe("ConfigManager storage adapter", function () {
7
+ before(function (done) {
8
+ initJSDOM().then(() => {
9
+ import("../../../../source/components/host/config-manager.mjs")
10
+ .then(() => {
11
+ done();
12
+ })
13
+ .catch((e) => done(e));
14
+ });
15
+ });
16
+
17
+ afterEach(() => {
18
+ let mocks = document.getElementById("mocks");
19
+ mocks.innerHTML = "";
20
+ });
21
+
22
+ it("uses an external storage adapter for managed keys", function (done) {
23
+ const config = document.createElement("monster-config-manager");
24
+ const values = new Map();
25
+ const calls = [];
26
+
27
+ config.setStorageAdapter({
28
+ managesConfigKey: (key) => key.startsWith("server_"),
29
+ hasConfig: (key) => values.has(key),
30
+ getConfig: (key) => values.get(key),
31
+ setConfig: (key, value) => {
32
+ calls.push(["set", key, value]);
33
+ values.set(key, value);
34
+ },
35
+ deleteConfig: (key) => {
36
+ calls.push(["delete", key]);
37
+ values.delete(key);
38
+ },
39
+ });
40
+
41
+ config.setOption("storage.serverAuthoritative", true);
42
+
43
+ config
44
+ .setConfig("server_table_filter", { visible: true })
45
+ .then(() => config.hasConfig("server_table_filter"))
46
+ .then((hasConfig) => {
47
+ expect(hasConfig).to.equal(true);
48
+ return config.getConfig("server_table_filter");
49
+ })
50
+ .then((value) => {
51
+ expect(value).to.deep.equal({ visible: true });
52
+ expect(calls).to.deep.equal([
53
+ ["set", "server_table_filter", { visible: true }],
54
+ ]);
55
+ return config.deleteConfig("server_table_filter");
56
+ })
57
+ .then(() => {
58
+ expect(values.has("server_table_filter")).to.equal(false);
59
+ done();
60
+ })
61
+ .catch(done);
62
+ });
63
+
64
+ it("does not fall back to IndexedDB for server-authoritative adapter keys", function (done) {
65
+ const config = document.createElement("monster-config-manager");
66
+
67
+ config.setStorageAdapter({
68
+ managesConfigKey: (key) => key === "server_key",
69
+ setConfig: () => Promise.reject(new Error("server unavailable")),
70
+ });
71
+ config.setOption("storage.serverAuthoritative", true);
72
+ config.setOption("indexDB.objectStore.name", "missing-store");
73
+
74
+ config
75
+ .setConfig("server_key", { value: 1 })
76
+ .then(() => done(new Error("setConfig should reject")))
77
+ .catch((error) => {
78
+ try {
79
+ expect(error.message).to.equal("server unavailable");
80
+ done();
81
+ } catch (e) {
82
+ done(e);
83
+ }
84
+ });
85
+ });
86
+ });
@@ -1,79 +1,105 @@
1
1
  // Import the required libraries
2
- import { expect } from 'chai';
2
+ import { expect } from "chai";
3
3
  //import { JSDOM } from 'jsdom';
4
- import { generateUniqueConfigKey } from '../../../../source/components/host/util.mjs';
5
- import {initJSDOM} from "../../../util/jsdom.mjs";
4
+ import {
5
+ generateComponentConfigKey,
6
+ generateConfigKey,
7
+ generateUniqueConfigKey,
8
+ } from "../../../../source/components/host/util.mjs";
9
+ import { initJSDOM } from "../../../util/jsdom.mjs";
6
10
 
7
11
  // Create a JSDOM instance to simulate the browser environment
8
12
  //const dom = new JSDOM();
9
13
 
10
-
11
14
  // Test suite for the generateUniqueConfigKey function
12
- describe('generateUniqueConfigKey', () => {
13
-
14
- //let originalWindow;
15
-
16
- // before(() => {
17
- // // Store the original window object
18
- // originalWindow = globalThis.window;
19
- //
20
- // // Create a JSDOM instance to simulate the browser environment
21
- // const dom = new JSDOM();
22
- // globalThis.window = dom.window;
23
- // });
24
-
25
-
26
- before(function (done) {
27
- initJSDOM().then(() => {
28
- done();
29
- });
30
- })
31
-
32
- // ... (same test cases as before)
33
-
34
- after(() => {
35
- // Restore the original window object
36
- // globalThis.window = originalWindow;
37
- });
38
-
39
- it('should generate a unique key with the given parameters', () => {
40
- const componentName = 'MyComponent';
41
- const id = '123';
42
- const prefix = 'myPrefix';
43
-
44
- const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
45
-
46
- // Ensure the unique key contains the given parameters and follows the expected format
47
- expect(uniqueKey).to.include(prefix);
48
- expect(uniqueKey).to.include(componentName);
49
- expect(uniqueKey).to.include(id);
50
- expect(uniqueKey).to.match(/^[a-zA-Z0-9_]+$/);
51
- });
52
-
53
- it('should replace special characters and spaces with underscores', () => {
54
- const componentName = 'My$Component';
55
- const id = '12#3';
56
- const prefix = 'my Prefix';
57
-
58
- const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
59
-
60
- // Ensure the unique key does not contain any special characters or spaces
61
- expect(uniqueKey).to.match(/^[a-zA-Z0-9_]+$/);
62
- });
63
-
64
- it('should include the browser location without parameters', () => {
65
- const componentName = 'MyComponent';
66
- const id = '123';
67
- const prefix = 'myPrefix';
68
-
69
- const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
70
-
71
- // Ensure the unique key contains the browser location without parameters
72
- const urlWithoutParams = window.location.href.split('?')[0];
73
- const sanitizedUrl = urlWithoutParams.replace(/[^\w\s]/gi, '_').replace(/\s+/g, '_');
74
- expect(uniqueKey).to.include(sanitizedUrl);
75
- });
76
-
77
-
78
-
79
- })
15
+ describe("generateUniqueConfigKey", () => {
16
+ //let originalWindow;
17
+
18
+ // before(() => {
19
+ // // Store the original window object
20
+ // originalWindow = globalThis.window;
21
+ //
22
+ // // Create a JSDOM instance to simulate the browser environment
23
+ // const dom = new JSDOM();
24
+ // globalThis.window = dom.window;
25
+ // });
26
+
27
+ before(function (done) {
28
+ initJSDOM().then(() => {
29
+ done();
30
+ });
31
+ });
32
+
33
+ // ... (same test cases as before)
34
+
35
+ after(() => {
36
+ // Restore the original window object
37
+ // globalThis.window = originalWindow;
38
+ });
39
+
40
+ it("should generate a unique key with the given parameters", () => {
41
+ const componentName = "MyComponent";
42
+ const id = "123";
43
+ const prefix = "myPrefix";
44
+
45
+ const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
46
+
47
+ // Ensure the unique key contains the given parameters and follows the expected format
48
+ expect(uniqueKey).to.include(prefix);
49
+ expect(uniqueKey).to.include(componentName);
50
+ expect(uniqueKey).to.include(id);
51
+ expect(uniqueKey).to.match(/^[a-zA-Z0-9_]+$/);
52
+ });
53
+
54
+ it("should replace special characters and spaces with underscores", () => {
55
+ const componentName = "My$Component";
56
+ const id = "12#3";
57
+ const prefix = "my Prefix";
58
+
59
+ const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
60
+
61
+ // Ensure the unique key does not contain any special characters or spaces
62
+ expect(uniqueKey).to.match(/^[a-zA-Z0-9_]+$/);
63
+ });
64
+
65
+ it("should include the browser location without parameters", () => {
66
+ const componentName = "MyComponent";
67
+ const id = "123";
68
+ const prefix = "myPrefix";
69
+
70
+ const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
71
+
72
+ // Ensure the unique key contains the browser location without parameters
73
+ const urlWithoutParams = window.location.href.split("?")[0];
74
+ const sanitizedUrl = urlWithoutParams
75
+ .replace(/[^\w\s]/gi, "_")
76
+ .replace(/\s+/g, "_");
77
+ expect(uniqueKey).to.include(sanitizedUrl);
78
+ });
79
+
80
+ it("should generate explicit instance keys without the browser location", () => {
81
+ const uniqueKey = generateConfigKey(
82
+ "datatable",
83
+ "orders.default",
84
+ "filter",
85
+ );
86
+
87
+ expect(uniqueKey).to.equal("filter_datatable_orders_default");
88
+ expect(uniqueKey).to.not.include("example_test");
89
+ });
90
+
91
+ it("should prefer explicit instance keys over legacy URL keys", () => {
92
+ const uniqueKey = generateComponentConfigKey(
93
+ "datatable",
94
+ "dom-id",
95
+ "stored-order",
96
+ {
97
+ instanceKey: "orders.default",
98
+ },
99
+ );
100
+
101
+ expect(uniqueKey).to.equal("stored_order_datatable_orders_default");
102
+ expect(uniqueKey).to.not.include("dom-id");
103
+ expect(uniqueKey).to.not.include("example_test");
104
+ });
105
+ });
@@ -25,8 +25,10 @@ import "../cases/components/notify/message.mjs";
25
25
  import "../cases/components/notify/notify.mjs";
26
26
  import "../cases/components/host/host.mjs";
27
27
  import "../cases/components/host/overlay.mjs";
28
+ import "../cases/components/host/config-manager.mjs";
28
29
  import "../cases/components/host/util.mjs";
29
30
  import "../cases/components/host/details.mjs";
31
+ import "../cases/components/datatable/config-keys.mjs";
30
32
  import "../cases/components/datatable/writeback-sanitizer.mjs";
31
33
  import "../cases/components/datatable/pagination.mjs";
32
34
  import "../cases/components/navigation/site-navigation.mjs";