@schukai/monster 4.148.1 → 4.148.3
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/filter/date-presets.mjs +13 -0
- package/source/components/datatable/filter/date-range.mjs +12 -0
- package/source/components/datatable/filter/date-time.mjs +14 -1
- package/source/components/datatable/filter/date.mjs +14 -1
- package/source/components/datatable/filter/input.mjs +14 -1
- package/source/components/datatable/filter/range.mjs +11 -0
- package/source/components/datatable/filter/text-operator.mjs +19 -0
- package/source/components/datatable/filter/time.mjs +14 -1
- package/source/components/form/buy-box.mjs +2 -0
- package/source/components/form/cart-control.mjs +2 -0
- package/source/components/state/state.mjs +2 -0
- package/source/data/extend.mjs +7 -1
- package/source/data/pathfinder.mjs +44 -6
- package/source/dom/customcontrol.mjs +28 -23
- package/source/dom/customelement.mjs +105 -39
- package/source/dom/updater.mjs +76 -25
- package/source/dom/util/extract-keys.mjs +2 -1
- package/source/dom/util/init-options-from-attributes.mjs +1 -1
- package/source/dom/util/set-option-from-attribute.mjs +14 -3
- package/source/types/proxyobserver.mjs +15 -0
- package/source/util/clone.mjs +8 -6
- package/test/cases/components/datatable/filter-form-value.mjs +98 -0
- package/test/cases/components/form/non-value-form-association.mjs +53 -0
- package/test/cases/data/extend.mjs +27 -1
- package/test/cases/data/pathfinder.mjs +105 -1
- package/test/cases/dom/customcontrol.mjs +188 -6
- package/test/cases/dom/customelement-initfromscripthost.mjs +62 -0
- package/test/cases/dom/customelement.mjs +123 -2
- package/test/cases/dom/updater.mjs +293 -0
- package/test/cases/dom/util/extract-keys.mjs +14 -0
- package/test/cases/dom/util/init-options-from-attributes.mjs +18 -0
- package/test/cases/dom/util/set-option-from-attribute.mjs +151 -0
- package/test/cases/types/proxyobserver.mjs +52 -0
- package/test/cases/util/clone.mjs +24 -0
|
@@ -117,6 +117,7 @@ const updateCloneDataSymbol = Symbol("@schukai/monster/dom/@@updateCloneData");
|
|
|
117
117
|
* @type {symbol}
|
|
118
118
|
*/
|
|
119
119
|
const scriptHostElementSymbol = Symbol("scriptHostElement");
|
|
120
|
+
const optionDefaultsSymbol = Symbol("optionDefaults");
|
|
120
121
|
const managedShadowRootSymbol = Symbol("managedShadowRoot");
|
|
121
122
|
const visibilityStateSymbol = Symbol("visibilityState");
|
|
122
123
|
let hostVisibilityStyleSheet = null;
|
|
@@ -215,7 +216,11 @@ class CustomElement extends HTMLElement {
|
|
|
215
216
|
|
|
216
217
|
this[attributeObserverSymbol] = {};
|
|
217
218
|
|
|
218
|
-
|
|
219
|
+
this[optionDefaultsSymbol] = this.defaults;
|
|
220
|
+
const options = initOptionsFromAttributes(
|
|
221
|
+
this,
|
|
222
|
+
extend({}, this[optionDefaultsSymbol]),
|
|
223
|
+
);
|
|
219
224
|
if (!isObject(options)) {
|
|
220
225
|
throw new Error(
|
|
221
226
|
`The options are not defined correctly in the ${this.getTag()} element.`,
|
|
@@ -819,6 +824,9 @@ class CustomElement extends HTMLElement {
|
|
|
819
824
|
this,
|
|
820
825
|
attrName,
|
|
821
826
|
this[internalSymbol].getSubject()["options"],
|
|
827
|
+
{},
|
|
828
|
+
"data-monster-option-",
|
|
829
|
+
this[optionDefaultsSymbol],
|
|
822
830
|
);
|
|
823
831
|
}
|
|
824
832
|
|
|
@@ -888,21 +896,22 @@ function callControlCallback(callBackFunctionName, ...args) {
|
|
|
888
896
|
return;
|
|
889
897
|
}
|
|
890
898
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
const list = targetId.split(",");
|
|
898
|
-
for (const id of list) {
|
|
899
|
-
const host = findElementWithIdUpwards(this, id.trim());
|
|
900
|
-
if (!(host instanceof HTMLElement)) {
|
|
901
|
-
continue;
|
|
902
|
-
}
|
|
899
|
+
const targetId = this.getAttribute(ATTRIBUTE_SCRIPT_HOST);
|
|
900
|
+
if (!targetId) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
903
|
|
|
904
|
-
|
|
904
|
+
// The attribute and DOM ancestry can change while the control remains alive.
|
|
905
|
+
// Resolve the small ordered host list against the current context for every call.
|
|
906
|
+
this[scriptHostElementSymbol].length = 0;
|
|
907
|
+
const list = targetId.split(",");
|
|
908
|
+
for (const id of list) {
|
|
909
|
+
const host = findElementWithIdUpwards(this, id.trim());
|
|
910
|
+
if (!(host instanceof HTMLElement)) {
|
|
911
|
+
continue;
|
|
905
912
|
}
|
|
913
|
+
|
|
914
|
+
this[scriptHostElementSymbol].push(host);
|
|
906
915
|
}
|
|
907
916
|
|
|
908
917
|
for (const host of this[scriptHostElementSymbol]) {
|
|
@@ -1091,48 +1100,105 @@ function containChildNode(node) {
|
|
|
1091
1100
|
*/
|
|
1092
1101
|
function initOptionObserver() {
|
|
1093
1102
|
const self = this;
|
|
1103
|
+
const disabledQuery =
|
|
1104
|
+
"button, command, fieldset, keygen, optgroup, option, select, textarea, input, [data-monster-objectlink]";
|
|
1094
1105
|
|
|
1095
1106
|
self[internalSymbol].lastDisabledValue = undefined;
|
|
1107
|
+
self[internalSymbol].parentDisabledElements = new Set();
|
|
1096
1108
|
|
|
1097
|
-
const
|
|
1098
|
-
const
|
|
1109
|
+
const getDisabledTargets = () => {
|
|
1110
|
+
const targets = new Set();
|
|
1111
|
+
const shadowRoot = getManagedShadowRoot.call(self);
|
|
1099
1112
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1113
|
+
try {
|
|
1114
|
+
for (const element of self.querySelectorAll(disabledQuery)) {
|
|
1115
|
+
targets.add(element);
|
|
1116
|
+
}
|
|
1117
|
+
if (shadowRoot instanceof ShadowRoot) {
|
|
1118
|
+
for (const element of shadowRoot.querySelectorAll(disabledQuery)) {
|
|
1119
|
+
targets.add(element);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
for (const element of getSlottedElements.call(self, disabledQuery)) {
|
|
1123
|
+
targets.add(element);
|
|
1124
|
+
}
|
|
1125
|
+
} catch (e) {}
|
|
1126
|
+
|
|
1127
|
+
return targets;
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
const disconnectDisabledObservers = () => {
|
|
1131
|
+
self[internalSymbol].disabledLightObserver?.disconnect();
|
|
1132
|
+
self[internalSymbol].disabledShadowObserver?.disconnect();
|
|
1133
|
+
delete self[internalSymbol].disabledLightObserver;
|
|
1134
|
+
delete self[internalSymbol].disabledShadowObserver;
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const observeDisabledTargets = () => {
|
|
1138
|
+
const callback = () => syncDisabledState(true);
|
|
1139
|
+
if (!self[internalSymbol].disabledLightObserver) {
|
|
1140
|
+
self[internalSymbol].disabledLightObserver = new MutationObserver(
|
|
1141
|
+
callback,
|
|
1142
|
+
);
|
|
1143
|
+
self[internalSymbol].disabledLightObserver.observe(self, {
|
|
1144
|
+
attributes: true,
|
|
1145
|
+
attributeFilter: [ATTRIBUTE_DISABLED, "slot"],
|
|
1146
|
+
childList: true,
|
|
1147
|
+
subtree: true,
|
|
1148
|
+
});
|
|
1102
1149
|
}
|
|
1103
1150
|
|
|
1104
1151
|
const shadowRoot = getManagedShadowRoot.call(self);
|
|
1105
|
-
if (
|
|
1152
|
+
if (
|
|
1153
|
+
shadowRoot instanceof ShadowRoot &&
|
|
1154
|
+
!self[internalSymbol].disabledShadowObserver
|
|
1155
|
+
) {
|
|
1156
|
+
self[internalSymbol].disabledShadowObserver = new MutationObserver(
|
|
1157
|
+
callback,
|
|
1158
|
+
);
|
|
1159
|
+
self[internalSymbol].disabledShadowObserver.observe(shadowRoot, {
|
|
1160
|
+
attributes: true,
|
|
1161
|
+
attributeFilter: [ATTRIBUTE_DISABLED, "slot"],
|
|
1162
|
+
childList: true,
|
|
1163
|
+
subtree: true,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const syncDisabledState = (force = false) => {
|
|
1169
|
+
const flag =
|
|
1170
|
+
self.getOption("disabled", false) === true ||
|
|
1171
|
+
self[internalSymbol].formDisabledValue === true;
|
|
1172
|
+
|
|
1173
|
+
if (force !== true && flag === self[internalSymbol].lastDisabledValue) {
|
|
1106
1174
|
return;
|
|
1107
1175
|
}
|
|
1108
1176
|
|
|
1109
1177
|
self[internalSymbol].lastDisabledValue = flag;
|
|
1178
|
+
const ownedElements = self[internalSymbol].parentDisabledElements;
|
|
1110
1179
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1180
|
+
if (flag !== true) {
|
|
1181
|
+
disconnectDisabledObservers();
|
|
1182
|
+
for (const element of ownedElements) {
|
|
1183
|
+
element.removeAttribute(ATTRIBUTE_DISABLED);
|
|
1184
|
+
}
|
|
1185
|
+
ownedElements.clear();
|
|
1186
|
+
return;
|
|
1117
1187
|
}
|
|
1118
1188
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
]);
|
|
1127
|
-
} catch (e) {
|
|
1128
|
-
nodeList = elements;
|
|
1189
|
+
observeDisabledTargets();
|
|
1190
|
+
const targets = getDisabledTargets();
|
|
1191
|
+
for (const element of [...ownedElements]) {
|
|
1192
|
+
if (!targets.has(element)) {
|
|
1193
|
+
element.removeAttribute(ATTRIBUTE_DISABLED);
|
|
1194
|
+
ownedElements.delete(element);
|
|
1195
|
+
}
|
|
1129
1196
|
}
|
|
1130
1197
|
|
|
1131
|
-
for (const element of
|
|
1132
|
-
if (
|
|
1198
|
+
for (const element of targets) {
|
|
1199
|
+
if (!element.hasAttribute(ATTRIBUTE_DISABLED)) {
|
|
1133
1200
|
element.setAttribute(ATTRIBUTE_DISABLED, "");
|
|
1134
|
-
|
|
1135
|
-
element.removeAttribute(ATTRIBUTE_DISABLED);
|
|
1201
|
+
ownedElements.add(element);
|
|
1136
1202
|
}
|
|
1137
1203
|
}
|
|
1138
1204
|
};
|
package/source/dom/updater.mjs
CHANGED
|
@@ -69,7 +69,8 @@ export { Updater, addObjectWithUpdaterToElement };
|
|
|
69
69
|
* @private
|
|
70
70
|
* @type {symbol}
|
|
71
71
|
*/
|
|
72
|
-
const
|
|
72
|
+
const controlEventTimersSymbol = Symbol("controlEventTimers");
|
|
73
|
+
const registeredEventTypesSymbol = Symbol("registeredEventTypes");
|
|
73
74
|
|
|
74
75
|
/**
|
|
75
76
|
* @private
|
|
@@ -87,9 +88,29 @@ const applyChangeSymbol = Symbol("applyChange");
|
|
|
87
88
|
const updaterRootSymbol = Symbol.for("@schukai/monster/dom/@@updater-root");
|
|
88
89
|
const disposedSymbol = Symbol("disposed");
|
|
89
90
|
const subjectObserverSymbol = Symbol("subjectObserver");
|
|
91
|
+
const subjectRevisionSymbol = Symbol("subjectRevision");
|
|
90
92
|
const patchNodeKeySymbol = Symbol("patchNodeKey");
|
|
91
93
|
const queuedSnapshotSymbol = Symbol("queuedSnapshot");
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Tracks every notification request, including requests that Observer deduplicates
|
|
97
|
+
* while its asynchronous callback is already running.
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
class UpdaterSubjectObserver extends Observer {
|
|
101
|
+
#onNotification;
|
|
102
|
+
|
|
103
|
+
constructor(callback, onNotification) {
|
|
104
|
+
super(callback);
|
|
105
|
+
this.#onNotification = onNotification;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
update(subject) {
|
|
109
|
+
this.#onNotification();
|
|
110
|
+
return super.update(subject);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
/**
|
|
94
115
|
* The updater class connects an object with the DOM. In this way, structures and contents in the DOM can be
|
|
95
116
|
* programmatically adapted via attributes.
|
|
@@ -165,23 +186,34 @@ class Updater extends Base {
|
|
|
165
186
|
this[pendingDiffsSymbol] = [];
|
|
166
187
|
this[processingSymbol] = false;
|
|
167
188
|
this[disposedSymbol] = false;
|
|
189
|
+
this[subjectRevisionSymbol] = 0;
|
|
168
190
|
this[queuedSnapshotSymbol] = clone(this[internalSymbol].last);
|
|
191
|
+
this[controlEventTimersSymbol] = new Map();
|
|
192
|
+
this[registeredEventTypesSymbol] = new Set();
|
|
193
|
+
|
|
194
|
+
this[subjectObserverSymbol] = new UpdaterSubjectObserver(
|
|
195
|
+
async () => {
|
|
196
|
+
let processedRevision;
|
|
197
|
+
do {
|
|
198
|
+
processedRevision = this[subjectRevisionSymbol];
|
|
199
|
+
if (this[disposedSymbol] === true) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
169
202
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
});
|
|
203
|
+
const real = this[internalSymbol].subject.getRealSubject();
|
|
204
|
+
const diffResult = diff(this[queuedSnapshotSymbol], real);
|
|
205
|
+
if (diffResult.length > 0) {
|
|
206
|
+
const snapshot = clone(real);
|
|
207
|
+
this[queuedSnapshotSymbol] = snapshot;
|
|
208
|
+
this[pendingDiffsSymbol].push({ diffResult, snapshot });
|
|
209
|
+
await this[processQueueSymbol]();
|
|
210
|
+
}
|
|
211
|
+
} while (processedRevision !== this[subjectRevisionSymbol]);
|
|
212
|
+
},
|
|
213
|
+
() => {
|
|
214
|
+
this[subjectRevisionSymbol]++;
|
|
215
|
+
},
|
|
216
|
+
);
|
|
185
217
|
this[internalSymbol].subject.attachObserver(this[subjectObserverSymbol]);
|
|
186
218
|
}
|
|
187
219
|
|
|
@@ -317,6 +349,7 @@ class Updater extends Base {
|
|
|
317
349
|
passive: true,
|
|
318
350
|
},
|
|
319
351
|
);
|
|
352
|
+
this[registeredEventTypesSymbol].add(type);
|
|
320
353
|
}
|
|
321
354
|
|
|
322
355
|
return this;
|
|
@@ -329,12 +362,23 @@ class Updater extends Base {
|
|
|
329
362
|
* @return {Updater}
|
|
330
363
|
*/
|
|
331
364
|
disableEventProcessing() {
|
|
332
|
-
for (const type of this[
|
|
365
|
+
for (const type of this[registeredEventTypesSymbol]) {
|
|
333
366
|
this[internalSymbol].element.removeEventListener(
|
|
334
367
|
type,
|
|
335
368
|
getControlEventHandler.call(this),
|
|
369
|
+
{ capture: true },
|
|
336
370
|
);
|
|
337
371
|
}
|
|
372
|
+
this[registeredEventTypesSymbol].clear();
|
|
373
|
+
|
|
374
|
+
for (const timer of this[controlEventTimersSymbol].values()) {
|
|
375
|
+
try {
|
|
376
|
+
timer.defuse();
|
|
377
|
+
} catch (e) {
|
|
378
|
+
// A timer that already ran no longer has pending work to cancel.
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
this[controlEventTimersSymbol].clear();
|
|
338
382
|
|
|
339
383
|
delete this[internalSymbol].element[updaterRootSymbol];
|
|
340
384
|
|
|
@@ -354,8 +398,6 @@ class Updater extends Base {
|
|
|
354
398
|
this[internalSymbol].subject.detachObserver(this[subjectObserverSymbol]);
|
|
355
399
|
}
|
|
356
400
|
|
|
357
|
-
delete this[timerElementEventHandlerSymbol];
|
|
358
|
-
|
|
359
401
|
return this;
|
|
360
402
|
}
|
|
361
403
|
|
|
@@ -495,21 +537,24 @@ function getControlEventHandler() {
|
|
|
495
537
|
return;
|
|
496
538
|
}
|
|
497
539
|
|
|
498
|
-
|
|
540
|
+
const timers = this[controlEventTimersSymbol];
|
|
541
|
+
let timer = timers.get(element);
|
|
542
|
+
if (timer instanceof DeadMansSwitch) {
|
|
499
543
|
try {
|
|
500
|
-
|
|
544
|
+
timer.touch();
|
|
501
545
|
return;
|
|
502
546
|
} catch (e) {
|
|
503
|
-
delete
|
|
547
|
+
timers.delete(element);
|
|
504
548
|
}
|
|
505
549
|
}
|
|
506
550
|
|
|
507
|
-
|
|
551
|
+
timer = new DeadMansSwitch(50, () => {
|
|
508
552
|
if (
|
|
509
553
|
this[disposedSymbol] === true ||
|
|
510
554
|
!this[internalSymbol].element?.isConnected ||
|
|
511
555
|
!element?.isConnected
|
|
512
556
|
) {
|
|
557
|
+
timers.delete(element);
|
|
513
558
|
return;
|
|
514
559
|
}
|
|
515
560
|
|
|
@@ -517,8 +562,11 @@ function getControlEventHandler() {
|
|
|
517
562
|
retrieveAndSetValue.call(this, element);
|
|
518
563
|
} catch (e) {
|
|
519
564
|
addErrorAttribute(element, e);
|
|
565
|
+
} finally {
|
|
566
|
+
timers.delete(element);
|
|
520
567
|
}
|
|
521
568
|
});
|
|
569
|
+
timers.set(element, timer);
|
|
522
570
|
};
|
|
523
571
|
|
|
524
572
|
return this[symbol];
|
|
@@ -1091,6 +1139,7 @@ function runUpdateContent(container, parts, subject) {
|
|
|
1091
1139
|
value = pipe.run(subject);
|
|
1092
1140
|
} catch (e) {
|
|
1093
1141
|
element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
|
|
1142
|
+
continue;
|
|
1094
1143
|
}
|
|
1095
1144
|
|
|
1096
1145
|
if (value instanceof HTMLElement) {
|
|
@@ -1529,6 +1578,7 @@ function runUpdateAttributes(container, parts, subject) {
|
|
|
1529
1578
|
}
|
|
1530
1579
|
|
|
1531
1580
|
const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);
|
|
1581
|
+
element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
|
|
1532
1582
|
|
|
1533
1583
|
for (let [, def] of Object.entries(attributes.split(","))) {
|
|
1534
1584
|
def = trimSpaces(def);
|
|
@@ -1544,10 +1594,10 @@ function runUpdateAttributes(container, parts, subject) {
|
|
|
1544
1594
|
|
|
1545
1595
|
let value;
|
|
1546
1596
|
try {
|
|
1547
|
-
element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
|
|
1548
1597
|
value = pipe.run(subject);
|
|
1549
1598
|
} catch (e) {
|
|
1550
1599
|
addErrorAttribute(element, e);
|
|
1600
|
+
continue;
|
|
1551
1601
|
}
|
|
1552
1602
|
|
|
1553
1603
|
if (value === undefined) {
|
|
@@ -1604,6 +1654,7 @@ function runUpdateProperties(container, parts, subject) {
|
|
|
1604
1654
|
}
|
|
1605
1655
|
|
|
1606
1656
|
const properties = element.getAttribute(ATTRIBUTE_UPDATER_PROPERTIES);
|
|
1657
|
+
element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
|
|
1607
1658
|
|
|
1608
1659
|
for (let [, def] of Object.entries(properties.split(","))) {
|
|
1609
1660
|
def = trimSpaces(def);
|
|
@@ -1619,10 +1670,10 @@ function runUpdateProperties(container, parts, subject) {
|
|
|
1619
1670
|
|
|
1620
1671
|
let value;
|
|
1621
1672
|
try {
|
|
1622
|
-
element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
|
|
1623
1673
|
value = pipe.run(subject);
|
|
1624
1674
|
} catch (e) {
|
|
1625
1675
|
addErrorAttribute(element, e);
|
|
1676
|
+
continue;
|
|
1626
1677
|
}
|
|
1627
1678
|
|
|
1628
1679
|
handleInputControlPropertyUpdate.call(this, element, name, value);
|
|
@@ -55,7 +55,7 @@ function extractKeys(
|
|
|
55
55
|
currentKebabKeyPrefix,
|
|
56
56
|
currentValuePrefix,
|
|
57
57
|
) {
|
|
58
|
-
for (const key
|
|
58
|
+
for (const key of Object.keys(currentObj)) {
|
|
59
59
|
const compactSegment = normalizeKeySegment(key);
|
|
60
60
|
const kebabSegment = toKebabCase(key);
|
|
61
61
|
|
|
@@ -73,6 +73,7 @@ function extractKeys(
|
|
|
73
73
|
const newValuePrefix = currentValuePrefix
|
|
74
74
|
? currentValuePrefix + valueSeparator + key
|
|
75
75
|
: key;
|
|
76
|
+
appendKeys(newCompactKeyPrefix, newKebabKeyPrefix, newValuePrefix);
|
|
76
77
|
helper(
|
|
77
78
|
currentObj[key],
|
|
78
79
|
newCompactKeyPrefix,
|
|
@@ -84,7 +84,7 @@ function initOptionsFromAttributes(
|
|
|
84
84
|
if (element.hasAttribute(name)) {
|
|
85
85
|
let value = element.getAttribute(name);
|
|
86
86
|
if (
|
|
87
|
-
|
|
87
|
+
Object.prototype.hasOwnProperty.call(mapping, optionName) &&
|
|
88
88
|
isFunction(mapping[optionName])
|
|
89
89
|
) {
|
|
90
90
|
value = mapping[optionName](value);
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "../../types/is.mjs";
|
|
25
25
|
import { attributeObserverSymbol } from "../customelement.mjs";
|
|
26
26
|
import { extractKeys } from "./extract-keys.mjs";
|
|
27
|
+
import { clone } from "../../util/clone.mjs";
|
|
27
28
|
|
|
28
29
|
export { setOptionFromAttribute };
|
|
29
30
|
|
|
@@ -56,6 +57,7 @@ export { setOptionFromAttribute };
|
|
|
56
57
|
* @param {Object} options - The options object to be initialized.
|
|
57
58
|
* @param {Object} mapping - A mapping between the attribute value and the property value.
|
|
58
59
|
* @param {string} prefix - The prefix of the attributes to be considered.
|
|
60
|
+
* @param {Object} defaults - The default option schema used for coercion and removal.
|
|
59
61
|
* @return {Object} - The initialized options object.
|
|
60
62
|
* @this HTMLElement - The context of the DOM element.
|
|
61
63
|
*/
|
|
@@ -65,12 +67,13 @@ function setOptionFromAttribute(
|
|
|
65
67
|
options,
|
|
66
68
|
mapping = {},
|
|
67
69
|
prefix = "data-monster-option-",
|
|
70
|
+
defaults = options,
|
|
68
71
|
) {
|
|
69
72
|
if (!(element instanceof HTMLElement)) return options;
|
|
70
|
-
if (!element.hasAttributes()) return options;
|
|
71
73
|
|
|
72
74
|
const keyMap = extractKeys(options);
|
|
73
75
|
const finder = new Pathfinder(options);
|
|
76
|
+
const defaultFinder = new Pathfinder(defaults);
|
|
74
77
|
|
|
75
78
|
// check if the attribute name is a valid option.
|
|
76
79
|
// the mapping between the attribute is simple. The dash is replaced by a dot.
|
|
@@ -79,15 +82,23 @@ function setOptionFromAttribute(
|
|
|
79
82
|
if (!finder.exists(optionName)) return;
|
|
80
83
|
|
|
81
84
|
if (!element.hasAttribute(name)) {
|
|
85
|
+
if (defaultFinder.exists(optionName)) {
|
|
86
|
+
finder.setVia(optionName, clone(defaultFinder.getVia(optionName)));
|
|
87
|
+
}
|
|
82
88
|
return options;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
let value = element.getAttribute(name);
|
|
86
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
Object.prototype.hasOwnProperty.call(mapping, optionName) &&
|
|
94
|
+
isFunction(mapping[optionName])
|
|
95
|
+
) {
|
|
87
96
|
value = mapping[optionName](value);
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
let optionValue =
|
|
99
|
+
let optionValue = defaultFinder.exists(optionName)
|
|
100
|
+
? defaultFinder.getVia(optionName)
|
|
101
|
+
: finder.getVia(optionName);
|
|
91
102
|
if (optionValue === null || optionValue === undefined) {
|
|
92
103
|
optionValue = value;
|
|
93
104
|
}
|
|
@@ -173,6 +173,10 @@ function getHandler() {
|
|
|
173
173
|
return value;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
if (isNativeObjectWithInternalSlots(value)) {
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
|
|
176
180
|
// set value as proxy if object or array
|
|
177
181
|
if (isArray(value) || isObject(value)) {
|
|
178
182
|
if (proxy.objectMap.has(value)) {
|
|
@@ -264,3 +268,14 @@ function getHandler() {
|
|
|
264
268
|
|
|
265
269
|
return handler;
|
|
266
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Native objects with internal slots cannot be used through a transparent Proxy
|
|
274
|
+
* because their prototype methods require the original receiver.
|
|
275
|
+
* @param {*} value
|
|
276
|
+
* @return {boolean}
|
|
277
|
+
* @private
|
|
278
|
+
*/
|
|
279
|
+
function isNativeObjectWithInternalSlots(value) {
|
|
280
|
+
return value instanceof Date || value instanceof Map || value instanceof Set;
|
|
281
|
+
}
|
package/source/util/clone.mjs
CHANGED
|
@@ -135,13 +135,14 @@ function cloneObject(obj) {
|
|
|
135
135
|
validateObject(obj);
|
|
136
136
|
|
|
137
137
|
const fkt = obj?.["constructor"];
|
|
138
|
+
const sourcePrototype = Object.getPrototypeOf(obj);
|
|
138
139
|
|
|
139
140
|
/** Object has clone method */
|
|
140
141
|
if (typeOf(fkt) === "function") {
|
|
141
142
|
const prototype = fkt?.prototype;
|
|
142
143
|
if (typeof prototype === "object") {
|
|
143
144
|
if (
|
|
144
|
-
prototype.hasOwnProperty("getClone") &&
|
|
145
|
+
Object.prototype.hasOwnProperty.call(prototype, "getClone") &&
|
|
145
146
|
typeOf(obj.getClone) === "function"
|
|
146
147
|
) {
|
|
147
148
|
return obj.getClone();
|
|
@@ -149,16 +150,17 @@ function cloneObject(obj) {
|
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
let copy = {};
|
|
153
|
+
let copy = sourcePrototype === null ? Object.create(null) : {};
|
|
153
154
|
if (
|
|
154
|
-
|
|
155
|
-
typeof
|
|
155
|
+
sourcePrototype !== null &&
|
|
156
|
+
typeof fkt === "function" &&
|
|
157
|
+
typeof fkt.call === "function"
|
|
156
158
|
) {
|
|
157
|
-
copy = new
|
|
159
|
+
copy = new fkt();
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
for (const key in obj) {
|
|
161
|
-
if (!
|
|
163
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
162
164
|
continue;
|
|
163
165
|
}
|
|
164
166
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import { getDocument } from '../../../../source/dom/util.mjs';
|
|
5
|
+
import { initJSDOM } from '../../../util/jsdom.mjs';
|
|
6
|
+
|
|
7
|
+
const cases = [
|
|
8
|
+
{ tag: 'monster-filter-date', value: '2026-07-16' },
|
|
9
|
+
{ tag: 'monster-filter-date-time', value: '2026-07-16T12:30' },
|
|
10
|
+
{ tag: 'monster-filter-time', value: '12:30' },
|
|
11
|
+
{ tag: 'monster-filter-input', value: 'search term' },
|
|
12
|
+
{ tag: 'monster-filter-text-operator', value: ':search term', userValue: 'search term' },
|
|
13
|
+
{ tag: 'monster-filter-date-presets', preset: true },
|
|
14
|
+
{ tag: 'monster-filter-date-range', value: '2026-07-01-2026-07-16' },
|
|
15
|
+
{ tag: 'monster-filter-range', value: '10-20' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
describe('datatable filter form values issue #502', function () {
|
|
19
|
+
let document;
|
|
20
|
+
|
|
21
|
+
before(async function () {
|
|
22
|
+
await initJSDOM({});
|
|
23
|
+
await import('element-internals-polyfill');
|
|
24
|
+
await Promise.all([
|
|
25
|
+
import('../../../../source/components/datatable/filter/date.mjs'),
|
|
26
|
+
import('../../../../source/components/datatable/filter/date-time.mjs'),
|
|
27
|
+
import('../../../../source/components/datatable/filter/time.mjs'),
|
|
28
|
+
import('../../../../source/components/datatable/filter/input.mjs'),
|
|
29
|
+
import('../../../../source/components/datatable/filter/text-operator.mjs'),
|
|
30
|
+
import('../../../../source/components/datatable/filter/date-presets.mjs'),
|
|
31
|
+
import('../../../../source/components/datatable/filter/date-range.mjs'),
|
|
32
|
+
import('../../../../source/components/datatable/filter/range.mjs'),
|
|
33
|
+
]);
|
|
34
|
+
document = getDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(function () {
|
|
38
|
+
document.getElementById('mocks').innerHTML = '';
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
for (const definition of cases) {
|
|
42
|
+
it(`${definition.tag} should submit programmatic and empty values`, async function () {
|
|
43
|
+
const form = document.createElement('form');
|
|
44
|
+
const control = document.createElement(definition.tag);
|
|
45
|
+
control.setAttribute('name', 'filter');
|
|
46
|
+
form.appendChild(control);
|
|
47
|
+
document.getElementById('mocks').appendChild(form);
|
|
48
|
+
|
|
49
|
+
expect(new window.FormData(form).get('filter')).to.equal('');
|
|
50
|
+
|
|
51
|
+
const value = definition.preset
|
|
52
|
+
? control.shadowRoot.querySelector('select').options[1].value
|
|
53
|
+
: definition.value;
|
|
54
|
+
control.value = value;
|
|
55
|
+
|
|
56
|
+
expect(new window.FormData(form).get('filter')).to.equal(control.value);
|
|
57
|
+
|
|
58
|
+
control.setAttribute('value', value);
|
|
59
|
+
control.value = '';
|
|
60
|
+
control.formResetCallback();
|
|
61
|
+
expect(new window.FormData(form).get('filter')).to.equal(control.value);
|
|
62
|
+
|
|
63
|
+
control.formStateRestoreCallback(value, 'restore');
|
|
64
|
+
expect(new window.FormData(form).get('filter')).to.equal(control.value);
|
|
65
|
+
|
|
66
|
+
control.remove();
|
|
67
|
+
form.appendChild(control);
|
|
68
|
+
expect(new window.FormData(form).get('filter')).to.equal(control.value);
|
|
69
|
+
|
|
70
|
+
control.setAttribute('disabled', '');
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
72
|
+
if (!window.navigator.userAgent.includes('jsdom')) {
|
|
73
|
+
expect(new window.FormData(form).get('filter')).to.equal(null);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it(`${definition.tag} should submit user-driven values`, function () {
|
|
78
|
+
const form = document.createElement('form');
|
|
79
|
+
const control = document.createElement(definition.tag);
|
|
80
|
+
control.setAttribute('name', 'filter');
|
|
81
|
+
form.appendChild(control);
|
|
82
|
+
document.getElementById('mocks').appendChild(form);
|
|
83
|
+
|
|
84
|
+
const input = definition.tag === 'monster-filter-text-operator'
|
|
85
|
+
? control.shadowRoot.querySelector('[data-monster-role=query]')
|
|
86
|
+
: definition.preset
|
|
87
|
+
? control.shadowRoot.querySelector('select')
|
|
88
|
+
: control.shadowRoot.querySelector('[data-monster-role=query], [data-monster-role=input]');
|
|
89
|
+
input.value = definition.preset
|
|
90
|
+
? input.options[1].value
|
|
91
|
+
: definition.userValue ?? definition.value;
|
|
92
|
+
input.dispatchEvent(new window.Event('input', { bubbles: true, composed: true }));
|
|
93
|
+
input.dispatchEvent(new window.Event('change', { bubbles: true, composed: true }));
|
|
94
|
+
|
|
95
|
+
expect(new window.FormData(form).get('filter')).to.equal(control.value);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|