@schukai/monster 4.152.0 → 4.153.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +24 -1
  2. package/package.json +1 -1
  3. package/source/components/content/viewer/message.mjs +1 -0
  4. package/source/components/form/select.mjs +2 -0
  5. package/source/components/form/util/fetch.mjs +6 -7
  6. package/source/components/state/log.mjs +7 -0
  7. package/source/components/state/thread.mjs +11 -0
  8. package/source/components/time/month-calendar.mjs +80 -30
  9. package/source/data/buildmap.mjs +3 -2
  10. package/source/data/buildtree.mjs +0 -8
  11. package/source/data/pathfinder.mjs +48 -19
  12. package/source/data/transformer.mjs +2 -2
  13. package/source/dom/customcontrol.mjs +35 -9
  14. package/source/dom/customelement.mjs +21 -2
  15. package/source/dom/resource.mjs +19 -5
  16. package/source/dom/sanitize-html.mjs +34 -29
  17. package/source/dom/slotted.mjs +46 -31
  18. package/source/dom/template.mjs +4 -1
  19. package/source/dom/updater.mjs +51 -15
  20. package/source/types/node.mjs +1 -3
  21. package/source/types/proxyobserver.mjs +32 -10
  22. package/test/cases/components/datatable/drag-scroll.mjs +28 -1
  23. package/test/cases/components/form/button-bar.mjs +9 -6
  24. package/test/cases/components/form/confirm-button.mjs +4 -4
  25. package/test/cases/components/form/control-bar.mjs +73 -72
  26. package/test/cases/components/form/input-group.mjs +3 -1
  27. package/test/cases/components/form/password.mjs +2 -2
  28. package/test/cases/components/form/popper-button.mjs +2 -2
  29. package/test/cases/components/form/select-sort-callback.mjs +57 -0
  30. package/test/cases/components/form/select.mjs +2 -2
  31. package/test/cases/components/form/sheet.mjs +13 -1
  32. package/test/cases/components/layout/popper.mjs +3 -1
  33. package/test/cases/components/layout/tabs.mjs +4 -1
  34. package/test/cases/components/navigation/site-navigation.mjs +12 -26
  35. package/test/cases/components/navigation/table-of-content.mjs +3 -0
  36. package/test/cases/components/state/ticker-lifecycle.mjs +47 -0
  37. package/test/cases/components/time/month-calendar.mjs +100 -0
  38. package/test/cases/data/buildmap.mjs +14 -0
  39. package/test/cases/data/buildtree.mjs +49 -0
  40. package/test/cases/data/pathfinder.mjs +34 -0
  41. package/test/cases/data/transformer.mjs +33 -0
  42. package/test/cases/dom/customcontrol.mjs +87 -6
  43. package/test/cases/dom/customelement.mjs +54 -0
  44. package/test/cases/dom/ready.mjs +3 -3
  45. package/test/cases/dom/resource-availability.mjs +46 -0
  46. package/test/cases/dom/resourcemanager.mjs +12 -6
  47. package/test/cases/dom/sanitize-html.mjs +71 -0
  48. package/test/cases/dom/slotted-nodes.mjs +48 -1
  49. package/test/cases/dom/template.mjs +11 -0
  50. package/test/cases/dom/updater.mjs +119 -1
  51. package/test/cases/types/proxyobserver.mjs +75 -0
  52. package/test/cases/util/processing.mjs +3 -3
package/README.md CHANGED
@@ -78,6 +78,30 @@ Voilà!
78
78
  While we strive to work around some browser bugs, generally, we do not use polyfills or feature detection.
79
79
  However, many functions can be supplemented through polyfills, thus enhancing compatibility.
80
80
 
81
+ ### Content Security Policy
82
+
83
+ String expressions starting with `run:` use `new Function()` and require CSP
84
+ permission for string evaluation. Existing configurations remain supported.
85
+ For Select sorting under a policy without `unsafe-eval`, use `call:asc`,
86
+ `call:desc`, or set a function from your application script:
87
+
88
+ ```javascript
89
+ select.setOption("mapping.sort", (a, b, control) =>
90
+ a[1].localeCompare(b[1])
91
+ );
92
+ ```
93
+
94
+ Each comparator argument is a `[value, label]` entry; the third argument is the
95
+ owning Select. A `null` sort keeps the imported order. Configure function values
96
+ through JavaScript, since JSON attributes cannot contain functions.
97
+
98
+ ### Browser verification
99
+
100
+ The [browser test runner](test/web/README.md) builds the current checkout and runs
101
+ Chromium, Firefox and WebKit with normal web security, including CSP and timezone
102
+ regressions. Browser versions are pinned with the Nix environment. This does not
103
+ establish support for older browsers or replace testing Safari on Apple devices.
104
+
81
105
  ## Questions
82
106
 
83
107
  For questions and commercial support, please contact [Volker Schukai](https://www.schukai.com/).
@@ -99,4 +123,3 @@ Licensed under [AGPL](https://www.gnu.org/licenses/agpl-3.0.de.html). Commercial
99
123
 
100
124
  Detailed changes for each release are documented in the
101
125
  [CHANGELOG](https://gitlab.schukai.com/oss/libraries/javascript/monster/-/blob/master/application/CHANGELOG.md).
102
-
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.","files":["source","test/cases","test/util","CHANGELOG.md"],"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.152.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","files":["source","test/cases","test/util","CHANGELOG.md"],"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.153.1"}
@@ -386,6 +386,7 @@ class MessageContent extends CustomElement {
386
386
  switch (contentTransferEncoding) {
387
387
  case "base64":
388
388
  content = atob(content);
389
+ break;
389
390
 
390
391
  case "quoted-printable":
391
392
  content = decodeQuotedPrintable(content);
@@ -1414,6 +1414,8 @@ function importOptionsIntern(data) {
1414
1414
  sort === "no"
1415
1415
  ) {
1416
1416
  // no sorting
1417
+ } else if (typeof sort === "function") {
1418
+ entries.sort((a, b) => sort(a, b, self));
1417
1419
  } else if (isString(sort)) {
1418
1420
  if (sort.startsWith("run:")) {
1419
1421
  const code = sort.replace("run:", "");
@@ -66,8 +66,12 @@ function loadAndAssignContent(element, url, options, filter) {
66
66
  const tempDiv = document.createElement("div");
67
67
  tempDiv.innerHTML = content;
68
68
 
69
- // Extract and execute all <script> elements by appending them to the document head
69
+ // Extract scripts before inserting the fragment, then execute them in head.
70
70
  const scriptElements = tempDiv.querySelectorAll("script");
71
+ for (const script of scriptElements) script.remove();
72
+ // Inline scripts run synchronously when appended. Their fragment must
73
+ // already exist, including any upgraded custom elements inside it.
74
+ validateInstance(element, HTMLElement).innerHTML = tempDiv.innerHTML;
71
75
  scriptElements.forEach((oldScript) => {
72
76
  const newScript = document.createElement("script");
73
77
  if (oldScript.src) newScript.src = oldScript.src;
@@ -76,18 +80,13 @@ function loadAndAssignContent(element, url, options, filter) {
76
80
  if (oldScript.defer) newScript.defer = oldScript.defer;
77
81
  if (oldScript.crossOrigin) newScript.crossOrigin = oldScript.crossOrigin;
78
82
  if (oldScript.integrity) newScript.integrity = oldScript.integrity;
83
+ if (oldScript.nonce) newScript.nonce = oldScript.nonce;
79
84
  if (oldScript.referrerPolicy)
80
85
  newScript.referrerPolicy = oldScript.referrerPolicy;
81
86
  newScript.textContent = oldScript.textContent;
82
87
  document.head.appendChild(newScript);
83
- if (oldScript.parentNode) {
84
- oldScript.parentNode.removeChild(oldScript);
85
- }
86
88
  });
87
89
 
88
- // Assign the processed content to the target element
89
- validateInstance(element, HTMLElement).innerHTML = tempDiv.innerHTML;
90
-
91
90
  // If the element is within a Shadow DOM, use the host as the event target
92
91
  const shadowRoot = findShadowRoot(element);
93
92
  const eventTarget = shadowRoot !== null ? shadowRoot.host : element;
@@ -190,6 +190,7 @@ class Log extends CustomElement {
190
190
  */
191
191
  connectedCallback() {
192
192
  super.connectedCallback();
193
+ initTimeAgoTicker.call(this);
193
194
 
194
195
  const slottedElements = getSlottedElements.call(this);
195
196
  if (slottedElements.size > 0) {
@@ -197,6 +198,12 @@ class Log extends CustomElement {
197
198
  }
198
199
  }
199
200
 
201
+ disconnectedCallback() {
202
+ super.disconnectedCallback();
203
+ clearInterval(this[timeAgoIntervalSymbol]);
204
+ this[timeAgoIntervalSymbol] = undefined;
205
+ }
206
+
200
207
  /**
201
208
  * Clear the log
202
209
  *
@@ -63,6 +63,17 @@ const timeAgoIntervalSymbol = Symbol("timeAgoInterval");
63
63
  * @summary A threaded discussion layout for nested replies, comments and conversation-like content.
64
64
  **/
65
65
  class Thread extends CustomElement {
66
+ connectedCallback() {
67
+ super.connectedCallback();
68
+ initTimeAgoTicker.call(this);
69
+ }
70
+
71
+ disconnectedCallback() {
72
+ super.disconnectedCallback();
73
+ clearInterval(this[timeAgoIntervalSymbol]);
74
+ this[timeAgoIntervalSymbol] = undefined;
75
+ }
76
+
66
77
  /**
67
78
  * @return {void}
68
79
  */
@@ -31,7 +31,7 @@ import { isFunction, isString } from "../../types/is.mjs";
31
31
 
32
32
  import { fireCustomEvent } from "../../dom/events.mjs";
33
33
  import { getLocaleOfDocument } from "../../dom/locale.mjs";
34
- import { addErrorAttribute } from "../../dom/error.mjs";
34
+ import { addErrorAttribute, removeErrorAttribute } from "../../dom/error.mjs";
35
35
  import { MonthCalendarStyleSheet } from "./stylesheet/month-calendar.mjs";
36
36
  import { AccessibilityStyleSheet } from "../stylesheet/accessibility.mjs";
37
37
  import {
@@ -58,6 +58,13 @@ const calendarElementSymbol = Symbol("calendarElement");
58
58
  * @type {symbol}
59
59
  */
60
60
  const calendarBodyElementSymbol = Symbol("calendarBodyElement");
61
+ const calendarInputSymbol = Symbol("calendarInput");
62
+ const calendarFormatSymbol = Symbol("calendarFormat");
63
+ const calendarRefreshSymbol = Symbol("calendarRefresh");
64
+ const lastValidDateSymbol = Symbol("lastValidDate");
65
+ const calendarDateInvalidSymbol = Symbol("calendarDateInvalid");
66
+ const appointmentObserverSymbol = Symbol("appointmentObserver");
67
+ const boundDaysSymbol = Symbol("boundDays");
61
68
 
62
69
  /**
63
70
  * A Calendar
@@ -82,9 +89,17 @@ class MonthCalendar extends CustomElement {
82
89
  [initMethodSymbol]() {
83
90
  super[initMethodSymbol]();
84
91
 
85
- const def = generateCalendarData.call(this);
86
- this.setOption("calendarDays", def.calendarDays);
87
- this.setOption("calendarWeekdays", def.calendarWeekdays);
92
+ refreshCalendar.call(this);
93
+ this.attachObserver(
94
+ new Observer(() => {
95
+ if (this[calendarRefreshSymbol]) return;
96
+ this[calendarRefreshSymbol] = true;
97
+ queueMicrotask(() => {
98
+ this[calendarRefreshSymbol] = false;
99
+ refreshCalendar.call(this);
100
+ });
101
+ }),
102
+ );
88
103
  }
89
104
 
90
105
  /**
@@ -93,6 +108,7 @@ class MonthCalendar extends CustomElement {
93
108
  */
94
109
  [assembleMethodSymbol]() {
95
110
  super[assembleMethodSymbol]();
111
+ refreshCalendar.call(this);
96
112
 
97
113
  setTimeout(() => {
98
114
  initControlReferences.call(this);
@@ -797,35 +813,28 @@ function calcHeaderAndFooterHeight() {
797
813
  * - calendarWeekdays: Array of seven objects, each representing a weekday header.
798
814
  */
799
815
  function generateCalendarData() {
800
- let selectedDate = this.getOption("startDate");
801
- if (!(selectedDate instanceof Date)) {
802
- if (typeof selectedDate === "string") {
803
- try {
804
- selectedDate = new Date(selectedDate);
805
- } catch (e) {
806
- addErrorAttribute(this, "Invalid calendar date");
807
- return { calendarDays, calendarWeekdays };
808
- }
809
- } else {
816
+ let selectedDate = parseCalendarDate(this.getOption("startDate"));
817
+ this[calendarDateInvalidSymbol] = selectedDate === null;
818
+ if (selectedDate) this[lastValidDateSymbol] = selectedDate;
819
+ else selectedDate = this[lastValidDateSymbol] || new Date();
820
+ // Constructors must not add attributes. Report after construction and use
821
+ // the latest state if options changed again during the same turn.
822
+ queueMicrotask(() => {
823
+ if (this[calendarDateInvalidSymbol])
810
824
  addErrorAttribute(this, "Invalid calendar date");
811
- return { calendarDays, calendarWeekdays };
812
- }
813
- }
814
-
825
+ else if (this.hasAttribute(ATTRIBUTE_ERRORMESSAGE))
826
+ removeErrorAttribute(this, "Invalid calendar date");
827
+ });
815
828
  const calendarDays = [];
816
829
  let calendarWeekdays = [];
817
830
 
818
- if (!(selectedDate instanceof Date)) {
819
- addErrorAttribute(this, "Invalid calendar date");
820
- return { calendarDays, calendarWeekdays };
821
- }
822
-
823
- // Get the year and month from the provided date
824
- const year = selectedDate.getFullYear();
831
+ // Get the local month from the provided date.
825
832
  const month = selectedDate.getMonth(); // 0-based index (0 = January)
826
833
 
827
834
  // Create a Date object for the 1st of the given month
828
- const firstDayOfMonth = new Date(year, month, 1);
835
+ const firstDayOfMonth = new Date(selectedDate);
836
+ firstDayOfMonth.setDate(1);
837
+ firstDayOfMonth.setHours(0, 0, 0, 0);
829
838
 
830
839
  // Determine the weekday index of the 1st day, ensuring Monday = 0
831
840
  const weekdayIndex = (firstDayOfMonth.getDay() + 6) % 7;
@@ -888,6 +897,43 @@ function generateCalendarData() {
888
897
  return { calendarDays, calendarWeekdays };
889
898
  }
890
899
 
900
+ function parseCalendarDate(value) {
901
+ if (value instanceof Date)
902
+ return Number.isNaN(value.getTime()) ? null : new Date(value.getTime());
903
+ if (typeof value !== "string") return null;
904
+ const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
905
+ if (parts) {
906
+ const [, year, month, day] = parts.map(Number);
907
+ const date = new Date(0);
908
+ date.setHours(0, 0, 0, 0);
909
+ date.setFullYear(year, month - 1, day);
910
+ return date.getFullYear() === year &&
911
+ date.getMonth() === month - 1 &&
912
+ date.getDate() === day
913
+ ? date
914
+ : null;
915
+ }
916
+ const date = new Date(value);
917
+ return Number.isNaN(date.getTime()) ? null : date;
918
+ }
919
+
920
+ function refreshCalendar() {
921
+ const value = this.getOption("startDate");
922
+ const input = value instanceof Date ? value.getTime() : value;
923
+ const format = this.getOption("locale.weekdayFormat");
924
+ if (
925
+ Object.is(input, this[calendarInputSymbol]) &&
926
+ format === this[calendarFormatSymbol]
927
+ )
928
+ return;
929
+ this[calendarInputSymbol] = input;
930
+ this[calendarFormatSymbol] = format;
931
+ const data = generateCalendarData.call(this);
932
+ this.setOption("calendarDays", data.calendarDays);
933
+ this.setOption("calendarWeekdays", data.calendarWeekdays);
934
+ if (this[calendarElementSymbol]) initEventHandler.call(this);
935
+ }
936
+
891
937
  /**
892
938
  * Generates a map that contains an array of appointments for each day within the calendar range.
893
939
  * Multi-day appointments will appear on each day they span.
@@ -947,15 +993,19 @@ function initEventHandler() {
947
993
  const self = this;
948
994
 
949
995
  setTimeout(() => {
950
- this.attachObserver(
951
- new Observer(() => {
996
+ if (!this[appointmentObserverSymbol]) {
997
+ this[appointmentObserverSymbol] = new Observer(() => {
952
998
  placeAppointments.call(this);
953
- }),
954
- );
999
+ });
1000
+ this.attachObserver(this[appointmentObserverSymbol]);
1001
+ }
1002
+ this[boundDaysSymbol] ??= new WeakSet();
955
1003
 
956
1004
  this[calendarElementSymbol]
957
1005
  .querySelectorAll("[data-monster-role='day-cell']")
958
1006
  .forEach((element) => {
1007
+ if (this[boundDaysSymbol].has(element)) return;
1008
+ this[boundDaysSymbol].add(element);
959
1009
  element.addEventListener("click", (event) => {
960
1010
  const hoveredElement = this.shadowRoot.elementFromPoint(
961
1011
  event.clientX,
@@ -362,7 +362,8 @@ function buildFlatMap(subject, selector, key, parentMap) {
362
362
  * @return {*}
363
363
  */
364
364
  export function build(subject, definition, defaultValue) {
365
- if (definition === undefined) return defaultValue ? defaultValue : subject;
365
+ if (definition === undefined)
366
+ return defaultValue !== undefined ? defaultValue : subject;
366
367
  validateString(definition);
367
368
 
368
369
  const regexp = /(?<placeholder>\${(?<path>[a-z\^A-Z.\-_0-9]*)})/gm;
@@ -384,7 +385,7 @@ export function build(subject, definition, defaultValue) {
384
385
  let v = finder.getVia(path);
385
386
  if (v === undefined) v = defaultValue;
386
387
 
387
- definition = definition.replaceAll(placeholder, v);
388
+ definition = definition.replaceAll(placeholder, () => `${v}`);
388
389
  });
389
390
 
390
391
  return definition;
@@ -21,12 +21,6 @@ import { extend } from "./extend.mjs";
21
21
 
22
22
  export { buildTree };
23
23
 
24
- /**
25
- * @private
26
- * @type {symbol}
27
- */
28
- const parentSymbol = Symbol("parent");
29
-
30
24
  /**
31
25
  * @private
32
26
  * @type {symbol}
@@ -171,8 +165,6 @@ function buildTree(subject, selector, idKey, parentIDKey, options) {
171
165
  throw new Error("the object has no value for the specified id");
172
166
  }
173
167
 
174
- o[parentSymbol] = ref;
175
-
176
168
  const node = new Node(o);
177
169
  this.has(ref)
178
170
  ? this.get(ref).add(node)
@@ -217,16 +217,24 @@ function iterate(subject, path, check) {
217
217
 
218
218
  const result = new Map();
219
219
 
220
- if (isArray(path)) {
221
- path = path.join(DELIMITER);
222
- }
223
-
224
- if (isObject(subject) || isArray(subject)) {
220
+ if (subject instanceof Map) {
221
+ for (const [key, value] of subject) {
222
+ result.set(key, getValueViaPath.call(this, value, path, check));
223
+ }
224
+ } else if (subject instanceof Set) {
225
+ let index = 0;
226
+ for (const value of subject) {
227
+ result.set(
228
+ `${index++}`,
229
+ getValueViaPath.call(this, value, path, check),
230
+ );
231
+ }
232
+ } else if (isObject(subject) || isArray(subject)) {
225
233
  for (const [key, value] of Object.entries(subject)) {
226
234
  result.set(key, getValueViaPath.call(this, value, path, check));
227
235
  }
228
236
  } else {
229
- const key = path.split(DELIMITER).shift();
237
+ const key = isArray(path) ? path[0] : path.split(DELIMITER).shift();
230
238
  result.set(key, getValueViaPath.call(this, subject, path, check));
231
239
  }
232
240
 
@@ -272,7 +280,7 @@ function getValueViaPath(subject, path, check) {
272
280
  let current = parts.shift();
273
281
 
274
282
  if (current === this.wildCard) {
275
- return iterate.call(this, subject, parts.join(DELIMITER), check);
283
+ return iterate.call(this, subject, parts, check);
276
284
  }
277
285
 
278
286
  if (isObject(subject) || isArray(subject)) {
@@ -280,23 +288,21 @@ function getValueViaPath(subject, path, check) {
280
288
  if (subject instanceof Map || subject instanceof WeakMap) {
281
289
  anchor = subject.get(current);
282
290
  } else if (subject instanceof Set) {
283
- current = parseInt(current);
284
- validateInteger(current);
291
+ current = parsePathIndex(current);
285
292
  anchor = [...subject]?.[current];
286
293
  } else if (subject instanceof WeakSet) {
287
294
  throw Error("unsupported action for this data type (WeakSet)");
288
295
  } else if (typeof WeakRef === "function" && subject instanceof WeakRef) {
289
296
  throw Error("unsupported action for this data type (WeakRef)");
290
297
  } else if (isArray(subject)) {
291
- current = parseInt(current);
292
- validateInteger(current);
298
+ current = parsePathIndex(current);
293
299
  anchor = subject?.[current];
294
300
  } else {
295
301
  anchor = subject?.[current];
296
302
  }
297
303
 
298
304
  if (isObject(anchor) || isArray(anchor)) {
299
- return getValueViaPath.call(this, anchor, parts.join(DELIMITER), check);
305
+ return getValueViaPath.call(this, anchor, parts, check);
300
306
  }
301
307
 
302
308
  if (parts.length > 0) {
@@ -304,12 +310,18 @@ function getValueViaPath(subject, path, check) {
304
310
  }
305
311
 
306
312
  if (check === true) {
307
- const descriptor = Object.getOwnPropertyDescriptor(
308
- Object.getPrototypeOf(subject),
309
- current,
310
- );
313
+ let exists;
314
+ if (subject instanceof Map || subject instanceof WeakMap) {
315
+ exists = subject.has(current);
316
+ } else if (subject instanceof Set) {
317
+ exists = current >= 0 && current < subject.size;
318
+ } else if (isArray(subject)) {
319
+ exists = Reflect.has(subject, current);
320
+ } else {
321
+ exists = Reflect.has(subject, current);
322
+ }
311
323
 
312
- if (!subject.hasOwnProperty(current) && descriptor === undefined) {
324
+ if (!exists) {
313
325
  throw Error("unknown value " + current);
314
326
  }
315
327
  }
@@ -411,7 +423,7 @@ function setValueViaPath(subject, path, value) {
411
423
  * @param {*} value
412
424
  */
413
425
  function assignProperty(object, key, value) {
414
- if (!object.hasOwnProperty(key)) {
426
+ if (!Object.hasOwn(object, key)) {
415
427
  object[key] = value;
416
428
  return;
417
429
  }
@@ -423,6 +435,21 @@ function assignProperty(object, key, value) {
423
435
  object[key] = value;
424
436
  }
425
437
 
438
+ /**
439
+ * Convert a complete integer path segment without accepting trailing text.
440
+ * @param {*} value
441
+ * @return {number}
442
+ * @private
443
+ */
444
+ function parsePathIndex(value) {
445
+ if (!/^-?\d+$/.test(`${value}`)) {
446
+ throw new TypeError("value is not an integer");
447
+ }
448
+ const index = Number(value);
449
+ validateInteger(index);
450
+ return index;
451
+ }
452
+
426
453
  /**
427
454
  *
428
455
  * @param {object} subject
@@ -486,7 +513,9 @@ function deleteValueViaPath(subject, path) {
486
513
  * @private
487
514
  */
488
515
  function validateMutationPath(path) {
489
- const parts = isArray(path) ? path : path.split(DELIMITER);
516
+ const parts = (isArray(path) ? path : [path]).flatMap((part) =>
517
+ `${part}`.split(DELIMITER),
518
+ );
490
519
  for (const part of parts) {
491
520
  if (UNSAFE_MUTATION_PATH_SEGMENTS.has(`${part}`)) {
492
521
  throw new Error(`unsafe mutation path segment: ${part}`);
@@ -530,7 +530,7 @@ function transform(value) {
530
530
 
531
531
  const useKey = keys?.[key];
532
532
 
533
- if (value?.[useKey]) {
533
+ if (useKey !== undefined && Reflect.has(value, useKey)) {
534
534
  return value?.[useKey];
535
535
  }
536
536
 
@@ -555,7 +555,7 @@ function transform(value) {
555
555
  }
556
556
 
557
557
  if (isObject(value) || isArray(value)) {
558
- if (value?.[key]) {
558
+ if (Reflect.has(value, key)) {
559
559
  return value?.[key];
560
560
  }
561
561
 
@@ -377,6 +377,7 @@ function getInternal() {
377
377
  }
378
378
 
379
379
  const debounceValueSymbol = Symbol("debounceValue");
380
+ const pendingValueAttributeSymbol = Symbol("pendingValueAttribute");
380
381
 
381
382
  /**
382
383
  *
@@ -388,7 +389,17 @@ const debounceValueSymbol = Symbol("debounceValue");
388
389
  */
389
390
  function initValueAttributeObserver() {
390
391
  const self = this;
391
- this[attributeObserverSymbol]["value"] = () => {
392
+ this[attributeObserverSymbol]["value"] = (newValue, oldValue) => {
393
+ if (!self[pendingValueAttributeSymbol]) {
394
+ self[pendingValueAttributeSymbol] = {
395
+ oldValue: normalizeAttributeValue(oldValue),
396
+ newValue: normalizeAttributeValue(newValue),
397
+ };
398
+ } else {
399
+ self[pendingValueAttributeSymbol].newValue =
400
+ normalizeAttributeValue(newValue);
401
+ }
402
+
392
403
  if (self[debounceValueSymbol] instanceof DeadMansSwitch) {
393
404
  try {
394
405
  self[debounceValueSymbol].touch();
@@ -402,18 +413,33 @@ function initValueAttributeObserver() {
402
413
  }
403
414
 
404
415
  self[debounceValueSymbol] = new DeadMansSwitch(10, () => {
405
- const oldValue = self.getAttribute("value");
406
- const newValue = self.value;
407
-
408
- if (oldValue !== newValue) {
409
- setTimeout(() => {
410
- self.value = self.getAttribute("value") ?? "";
411
- }, 0);
412
- }
416
+ const pending = self[pendingValueAttributeSymbol];
417
+ delete self[pendingValueAttributeSymbol];
418
+ delete self[debounceValueSymbol];
419
+ if (!pending) return;
420
+
421
+ setTimeout(() => {
422
+ const currentValue = normalizeAttributeValue(self.value);
423
+ if (
424
+ currentValue === pending.oldValue &&
425
+ currentValue !== pending.newValue
426
+ ) {
427
+ self.value = pending.newValue;
428
+ }
429
+ }, 0);
413
430
  });
414
431
  };
415
432
  }
416
433
 
434
+ /**
435
+ * @param {*} value
436
+ * @return {string}
437
+ * @private
438
+ */
439
+ function normalizeAttributeValue(value) {
440
+ return value === null || value === undefined ? "" : String(value);
441
+ }
442
+
417
443
  const disabledSyncSymbol = Symbol("disabledSync");
418
444
  const disabledAttributeSyncSymbol = Symbol("disabledAttributeSync");
419
445
 
@@ -734,7 +734,10 @@ class CustomElement extends HTMLElement {
734
734
  // Initialize the custom elements inside the shadow root and the slotted elements
735
735
  initFromCallbackHost.call(this);
736
736
  try {
737
- nodeList = new Set([...elements, ...getSlottedElements.call(this)]);
737
+ nodeList = new Set([
738
+ ...elements,
739
+ ...getSlottedElements.call(this, undefined, undefined, getManagedShadowRoot.call(this)),
740
+ ]);
738
741
  } catch (e) {
739
742
  nodeList = elements;
740
743
  }
@@ -1133,7 +1136,12 @@ function initOptionObserver() {
1133
1136
  targets.add(element);
1134
1137
  }
1135
1138
  }
1136
- for (const element of getSlottedElements.call(self, disabledQuery)) {
1139
+ for (const element of getSlottedElements.call(
1140
+ self,
1141
+ disabledQuery,
1142
+ undefined,
1143
+ getManagedShadowRoot.call(self),
1144
+ )) {
1137
1145
  targets.add(element);
1138
1146
  }
1139
1147
  } catch (e) {}
@@ -1293,6 +1301,17 @@ function syncUpdaterSubject(target, source) {
1293
1301
  return;
1294
1302
  }
1295
1303
 
1304
+ // Empty option objects are also used as refresh markers by editable
1305
+ // datasource components. Keep their established merge behavior while
1306
+ // allowing non-empty replacement objects to remove stale keys.
1307
+ if (Object.keys(source).length > 0) {
1308
+ for (const key of Object.keys(target)) {
1309
+ if (!Object.hasOwn(source, key)) {
1310
+ delete target[key];
1311
+ }
1312
+ }
1313
+ }
1314
+
1296
1315
  for (const [key, value] of Object.entries(source)) {
1297
1316
  syncUpdaterValue(target, key, value);
1298
1317
  }
@@ -175,17 +175,31 @@ class Resource extends BaseWithOptions {
175
175
  }
176
176
 
177
177
  return new Promise(function (resolve, reject) {
178
+ let settled = false;
179
+ const cleanup = () => {
180
+ settled = true;
181
+ clearTimeout(timeout);
182
+ self[internalStateSymbol].detachObserver(observer);
183
+ };
178
184
  const timeout = setTimeout(() => {
185
+ cleanup();
179
186
  reject("timeout");
180
187
  }, self.getOption("timeout"));
181
188
 
182
- const observer = new Observer(() => {
183
- clearTimeout(timeout);
184
- self[internalStateSymbol].detachObserver(observer);
185
- resolve();
186
- });
189
+ const check = () => {
190
+ if (settled) return;
191
+ const state = self[internalStateSymbol].getSubject();
192
+ if (state.loaded !== true) return;
193
+ cleanup();
194
+ if (state.error !== undefined) reject(state.error);
195
+ else resolve();
196
+ };
197
+ // setSubject updates several properties. Observe its final state and
198
+ // avoid removing observers while their notification list is iterating.
199
+ const observer = new Observer(() => queueMicrotask(check));
187
200
 
188
201
  self[internalStateSymbol].attachObserver(observer);
202
+ queueMicrotask(check);
189
203
  });
190
204
  }
191
205