@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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/source/components/datatable/filter/date-presets.mjs +13 -0
  3. package/source/components/datatable/filter/date-range.mjs +12 -0
  4. package/source/components/datatable/filter/date-time.mjs +14 -1
  5. package/source/components/datatable/filter/date.mjs +14 -1
  6. package/source/components/datatable/filter/input.mjs +14 -1
  7. package/source/components/datatable/filter/range.mjs +11 -0
  8. package/source/components/datatable/filter/text-operator.mjs +19 -0
  9. package/source/components/datatable/filter/time.mjs +14 -1
  10. package/source/components/form/buy-box.mjs +2 -0
  11. package/source/components/form/cart-control.mjs +2 -0
  12. package/source/components/state/state.mjs +2 -0
  13. package/source/data/extend.mjs +7 -1
  14. package/source/data/pathfinder.mjs +44 -6
  15. package/source/dom/customcontrol.mjs +28 -23
  16. package/source/dom/customelement.mjs +105 -39
  17. package/source/dom/updater.mjs +76 -25
  18. package/source/dom/util/extract-keys.mjs +2 -1
  19. package/source/dom/util/init-options-from-attributes.mjs +1 -1
  20. package/source/dom/util/set-option-from-attribute.mjs +14 -3
  21. package/source/types/proxyobserver.mjs +15 -0
  22. package/source/util/clone.mjs +8 -6
  23. package/test/cases/components/datatable/filter-form-value.mjs +98 -0
  24. package/test/cases/components/form/non-value-form-association.mjs +53 -0
  25. package/test/cases/data/extend.mjs +27 -1
  26. package/test/cases/data/pathfinder.mjs +105 -1
  27. package/test/cases/dom/customcontrol.mjs +188 -6
  28. package/test/cases/dom/customelement-initfromscripthost.mjs +62 -0
  29. package/test/cases/dom/customelement.mjs +123 -2
  30. package/test/cases/dom/updater.mjs +293 -0
  31. package/test/cases/dom/util/extract-keys.mjs +14 -0
  32. package/test/cases/dom/util/init-options-from-attributes.mjs +18 -0
  33. package/test/cases/dom/util/set-option-from-attribute.mjs +151 -0
  34. package/test/cases/types/proxyobserver.mjs +52 -0
  35. 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
- const options = initOptionsFromAttributes(this, extend({}, this.defaults));
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
- if (this[scriptHostElementSymbol].length === 0) {
892
- const targetId = this.getAttribute(ATTRIBUTE_SCRIPT_HOST);
893
- if (!targetId) {
894
- return;
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
- this[scriptHostElementSymbol].push(host);
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 syncDisabledState = () => {
1098
- const flag = self.getOption("disabled", false);
1109
+ const getDisabledTargets = () => {
1110
+ const targets = new Set();
1111
+ const shadowRoot = getManagedShadowRoot.call(self);
1099
1112
 
1100
- if (flag === self[internalSymbol].lastDisabledValue) {
1101
- return;
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 (!(shadowRoot instanceof ShadowRoot) && !self.childNodes.length) {
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
- const query =
1112
- "button, command, fieldset, keygen, optgroup, option, select, textarea, input, [data-monster-objectlink]";
1113
-
1114
- let elements = [];
1115
- if (shadowRoot instanceof ShadowRoot) {
1116
- elements = shadowRoot.querySelectorAll(query);
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
- let nodeList;
1120
- try {
1121
- const baseElements =
1122
- elements.length > 0 ? elements : self.querySelectorAll(query);
1123
- nodeList = new Set([
1124
- ...baseElements,
1125
- ...getSlottedElements.call(self, query),
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 [...nodeList]) {
1132
- if (flag === true) {
1198
+ for (const element of targets) {
1199
+ if (!element.hasAttribute(ATTRIBUTE_DISABLED)) {
1133
1200
  element.setAttribute(ATTRIBUTE_DISABLED, "");
1134
- } else {
1135
- element.removeAttribute(ATTRIBUTE_DISABLED);
1201
+ ownedElements.add(element);
1136
1202
  }
1137
1203
  }
1138
1204
  };
@@ -69,7 +69,8 @@ export { Updater, addObjectWithUpdaterToElement };
69
69
  * @private
70
70
  * @type {symbol}
71
71
  */
72
- const timerElementEventHandlerSymbol = Symbol("timerElementEventHandler");
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
- this[subjectObserverSymbol] = new Observer(() => {
171
- if (this[disposedSymbol] === true) {
172
- return Promise.resolve();
173
- }
174
-
175
- const real = this[internalSymbol].subject.getRealSubject();
176
- const diffResult = diff(this[queuedSnapshotSymbol], real);
177
- if (diffResult.length === 0) {
178
- return Promise.resolve();
179
- }
180
- const snapshot = clone(real);
181
- this[queuedSnapshotSymbol] = snapshot;
182
- this[pendingDiffsSymbol].push({ diffResult, snapshot });
183
- return this[processQueueSymbol]();
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[internalSymbol].eventTypes) {
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
- if (this[timerElementEventHandlerSymbol] instanceof DeadMansSwitch) {
540
+ const timers = this[controlEventTimersSymbol];
541
+ let timer = timers.get(element);
542
+ if (timer instanceof DeadMansSwitch) {
499
543
  try {
500
- this[timerElementEventHandlerSymbol].touch();
544
+ timer.touch();
501
545
  return;
502
546
  } catch (e) {
503
- delete this[timerElementEventHandlerSymbol];
547
+ timers.delete(element);
504
548
  }
505
549
  }
506
550
 
507
- this[timerElementEventHandlerSymbol] = new DeadMansSwitch(50, () => {
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 in currentObj) {
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
- mapping.hasOwnProperty(optionName) &&
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 (mapping.hasOwnProperty(optionName) && isFunction(mapping[optionName])) {
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 = finder.getVia(optionName);
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
+ }
@@ -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
- typeof obj.constructor === "function" &&
155
- typeof obj.constructor.call === "function"
155
+ sourcePrototype !== null &&
156
+ typeof fkt === "function" &&
157
+ typeof fkt.call === "function"
156
158
  ) {
157
- copy = new obj.constructor();
159
+ copy = new fkt();
158
160
  }
159
161
 
160
162
  for (const key in obj) {
161
- if (!obj.hasOwnProperty(key)) {
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
+ });