@schukai/monster 4.142.3 → 4.143.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/package.json +1 -1
- package/source/components/datatable/datatable.mjs +24 -9
- package/source/components/datatable/filter/util.mjs +7 -3
- package/source/components/datatable/filter.mjs +21 -5
- package/source/components/form/select.mjs +23 -3
- package/source/components/form/util/floating-layout-queue.mjs +65 -0
- package/source/components/form/util/floating-ui.mjs +21 -9
- package/source/components/host/config-manager.mjs +222 -32
- package/source/components/host/host.mjs +26 -0
- package/source/components/host/util.mjs +52 -2
- package/test/cases/components/datatable/config-keys.mjs +47 -0
- package/test/cases/components/host/config-manager.mjs +86 -0
- package/test/cases/components/host/util.mjs +98 -72
- package/test/web/import.js +2 -0
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.
|
|
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.0"}
|
|
@@ -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 {
|
|
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
|
|
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
|
|
670
|
-
return
|
|
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
|
|
686
|
-
addErrorAttribute(
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
@@ -2088,6 +2088,12 @@ function fetchIt(url, controlOptions) {
|
|
|
2088
2088
|
|
|
2089
2089
|
queueMicrotask(() => {
|
|
2090
2090
|
checkOptionState.call(this);
|
|
2091
|
+
if (
|
|
2092
|
+
getFilterMode.call(this) === FILTER_MODE_REMOTE &&
|
|
2093
|
+
this.getOption("options", []).length === 0
|
|
2094
|
+
) {
|
|
2095
|
+
calcAndSetOptionsDimension.call(this);
|
|
2096
|
+
}
|
|
2091
2097
|
setTotalText.call(this);
|
|
2092
2098
|
updatePopper.call(this);
|
|
2093
2099
|
setStatusOrRemoveBadges.call(this, "closed");
|
|
@@ -2509,14 +2515,22 @@ function scheduleSelectLayoutCycle(
|
|
|
2509
2515
|
}
|
|
2510
2516
|
|
|
2511
2517
|
function hasConfiguredOptionsWaitingForRender() {
|
|
2518
|
+
if (getFilterMode.call(this) === FILTER_MODE_REMOTE) {
|
|
2519
|
+
return false;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2512
2522
|
const options = this.getOption("options");
|
|
2513
2523
|
|
|
2514
2524
|
if (isArray(options)) {
|
|
2515
|
-
|
|
2525
|
+
if (options.length > 0) {
|
|
2526
|
+
return true;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
return getSlottedElements.call(this, "div").length > 0;
|
|
2516
2530
|
}
|
|
2517
2531
|
|
|
2518
2532
|
if (!isIterable(options) || isString(options)) {
|
|
2519
|
-
return
|
|
2533
|
+
return getSlottedElements.call(this, "div").length > 0;
|
|
2520
2534
|
}
|
|
2521
2535
|
|
|
2522
2536
|
for (const option of options) {
|
|
@@ -2546,7 +2560,9 @@ function retryPendingOpenIntent() {
|
|
|
2546
2560
|
}
|
|
2547
2561
|
|
|
2548
2562
|
if (getOptionElements.call(this).length === 0) {
|
|
2549
|
-
|
|
2563
|
+
if (!hasConfiguredOptionsWaitingForRender.call(this)) {
|
|
2564
|
+
clearPendingOpenIntent.call(this);
|
|
2565
|
+
}
|
|
2550
2566
|
return;
|
|
2551
2567
|
}
|
|
2552
2568
|
|
|
@@ -3658,6 +3674,8 @@ function getSelectPopperPositionOptions() {
|
|
|
3658
3674
|
popperOptions.placement = getDefaultSelectPopperPositionProfile().placement;
|
|
3659
3675
|
}
|
|
3660
3676
|
|
|
3677
|
+
popperOptions.adaptiveSize = false;
|
|
3678
|
+
|
|
3661
3679
|
if (
|
|
3662
3680
|
resolveParentPopperContentBoundary(
|
|
3663
3681
|
this[controlElementSymbol],
|
|
@@ -4969,6 +4987,8 @@ function show() {
|
|
|
4969
4987
|
const options = getOptionElements.call(this);
|
|
4970
4988
|
if (options.length === 0 && hasPopperFilterFlag === false) {
|
|
4971
4989
|
if (hasConfiguredOptionsWaitingForRender.call(this)) {
|
|
4990
|
+
removeErrorAttribute(this, "No options available.");
|
|
4991
|
+
setStatusOrRemoveBadges.call(this, "loading");
|
|
4972
4992
|
queuePendingOpenIntent.call(this);
|
|
4973
4993
|
}
|
|
4974
4994
|
return;
|
|
@@ -27,6 +27,12 @@ const jobs = new Map();
|
|
|
27
27
|
let queueFrame = null;
|
|
28
28
|
let queueTimeout = null;
|
|
29
29
|
let flushing = false;
|
|
30
|
+
const OSCILLATION_HISTORY_LIMIT = 4;
|
|
31
|
+
const OSCILLATION_TIME_WINDOW_MS = 1000;
|
|
32
|
+
const OSCILLATION_SAFE_REASONS =
|
|
33
|
+
FLOATING_LAYOUT_REASON.POSITION |
|
|
34
|
+
FLOATING_LAYOUT_REASON.RESIZE |
|
|
35
|
+
FLOATING_LAYOUT_REASON.SETTLE;
|
|
30
36
|
|
|
31
37
|
function enqueueFloatingLayout({
|
|
32
38
|
popperElement,
|
|
@@ -105,6 +111,7 @@ function createJob(popperElement) {
|
|
|
105
111
|
running: false,
|
|
106
112
|
pending: false,
|
|
107
113
|
cancelled: false,
|
|
114
|
+
layoutSignatures: [],
|
|
108
115
|
promise,
|
|
109
116
|
resolve,
|
|
110
117
|
};
|
|
@@ -220,9 +227,67 @@ async function flushJob(job) {
|
|
|
220
227
|
}
|
|
221
228
|
} finally {
|
|
222
229
|
job.running = false;
|
|
230
|
+
recordLayoutSignature(job);
|
|
231
|
+
if (job.pending && shouldSuppressOscillatingPendingLayout(job)) {
|
|
232
|
+
job.pending = false;
|
|
233
|
+
job.reasons = 0;
|
|
234
|
+
}
|
|
223
235
|
if (!job.pending && job.reasons === 0) {
|
|
224
236
|
job.resolve();
|
|
225
237
|
jobs.delete(job.popperElement);
|
|
226
238
|
}
|
|
227
239
|
}
|
|
228
240
|
}
|
|
241
|
+
|
|
242
|
+
function recordLayoutSignature(job) {
|
|
243
|
+
if (!(job?.popperElement instanceof HTMLElement)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const rect = job.popperElement.getBoundingClientRect();
|
|
248
|
+
const signature = [
|
|
249
|
+
Math.round(rect.x),
|
|
250
|
+
Math.round(rect.y),
|
|
251
|
+
Math.round(rect.width),
|
|
252
|
+
Math.round(rect.height),
|
|
253
|
+
].join("/");
|
|
254
|
+
|
|
255
|
+
job.layoutSignatures.push({
|
|
256
|
+
signature,
|
|
257
|
+
time: performanceNow(),
|
|
258
|
+
});
|
|
259
|
+
if (job.layoutSignatures.length > OSCILLATION_HISTORY_LIMIT) {
|
|
260
|
+
job.layoutSignatures.shift();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function shouldSuppressOscillatingPendingLayout(job) {
|
|
265
|
+
if ((job.reasons & ~OSCILLATION_SAFE_REASONS) !== 0) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const signatures = job.layoutSignatures;
|
|
270
|
+
if (signatures.length < OSCILLATION_HISTORY_LIMIT) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const [a, b, c, d] = signatures;
|
|
275
|
+
if (d.time - a.time > OSCILLATION_TIME_WINDOW_MS) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
a.signature === c.signature &&
|
|
281
|
+
b.signature === d.signature &&
|
|
282
|
+
a.signature !== b.signature
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function performanceNow() {
|
|
287
|
+
const globalObject = getGlobal();
|
|
288
|
+
if (globalObject?.performance?.now instanceof Function) {
|
|
289
|
+
return globalObject.performance.now();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return Date.now();
|
|
293
|
+
}
|
|
@@ -132,9 +132,12 @@ function syncFloatingPopover(
|
|
|
132
132
|
config.detectOverflowOptions,
|
|
133
133
|
popperElement,
|
|
134
134
|
syncCycleId,
|
|
135
|
+
config.adaptiveSize,
|
|
135
136
|
);
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
if (config.adaptiveSize !== false) {
|
|
139
|
+
resetAdaptiveFloatingElementSize(popperElement);
|
|
140
|
+
}
|
|
138
141
|
|
|
139
142
|
if (
|
|
140
143
|
arrowElement instanceof HTMLElement &&
|
|
@@ -143,7 +146,10 @@ function syncFloatingPopover(
|
|
|
143
146
|
floatingMiddleware.push(arrow({ element: arrowElement }));
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
if (
|
|
149
|
+
if (
|
|
150
|
+
config.adaptiveSize !== false &&
|
|
151
|
+
!config.middlewareTokens.includes("size")
|
|
152
|
+
) {
|
|
147
153
|
floatingMiddleware.push(
|
|
148
154
|
createAdaptiveSizeMiddleware(
|
|
149
155
|
config.detectOverflowOptions,
|
|
@@ -206,6 +212,7 @@ function normalizePopperConfig(options, controlElement, popperElement) {
|
|
|
206
212
|
placement: "top",
|
|
207
213
|
engine: "floating",
|
|
208
214
|
strategy: "absolute",
|
|
215
|
+
adaptiveSize: true,
|
|
209
216
|
},
|
|
210
217
|
options,
|
|
211
218
|
);
|
|
@@ -247,6 +254,7 @@ function buildFloatingMiddleware(
|
|
|
247
254
|
detectOverflowOptions,
|
|
248
255
|
popperElement,
|
|
249
256
|
syncCycleId = null,
|
|
257
|
+
adaptiveSize = true,
|
|
250
258
|
) {
|
|
251
259
|
const result = [...middleware];
|
|
252
260
|
|
|
@@ -272,7 +280,7 @@ function buildFloatingMiddleware(
|
|
|
272
280
|
case "shift":
|
|
273
281
|
result[key] = shift(normalizeShiftOptions(kv, detectOverflowOptions));
|
|
274
282
|
break;
|
|
275
|
-
case "autoPlacement":
|
|
283
|
+
case "autoPlacement": {
|
|
276
284
|
let defaultAllowedPlacements = ["top", "bottom", "left", "right"];
|
|
277
285
|
|
|
278
286
|
const defPlacement = kv?.shift();
|
|
@@ -298,18 +306,22 @@ function buildFloatingMiddleware(
|
|
|
298
306
|
}),
|
|
299
307
|
);
|
|
300
308
|
break;
|
|
309
|
+
}
|
|
301
310
|
case "arrow":
|
|
302
311
|
result[key] = null;
|
|
303
312
|
break;
|
|
304
313
|
case "size":
|
|
305
|
-
result[key] =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
314
|
+
result[key] =
|
|
315
|
+
adaptiveSize === false
|
|
316
|
+
? null
|
|
317
|
+
: createAdaptiveSizeMiddleware(
|
|
318
|
+
detectOverflowOptions,
|
|
319
|
+
popperElement,
|
|
320
|
+
syncCycleId,
|
|
321
|
+
);
|
|
310
322
|
break;
|
|
311
323
|
case "offset":
|
|
312
|
-
result[key] = offset(parseInt(kv?.shift()) || 10);
|
|
324
|
+
result[key] = offset(parseInt(kv?.shift(), 10) || 10);
|
|
313
325
|
break;
|
|
314
326
|
case "hide":
|
|
315
327
|
result[key] = hide(detectOverflowOptions);
|
|
@@ -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 {
|
|
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
|
|
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
|
+
});
|
|
@@ -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
|
|
2
|
+
import { expect } from "chai";
|
|
3
3
|
//import { JSDOM } from 'jsdom';
|
|
4
|
-
import {
|
|
5
|
-
|
|
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(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
});
|
package/test/web/import.js
CHANGED
|
@@ -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";
|