@schukai/monster 4.153.1 → 4.154.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 (41) hide show
  1. package/package.json +1 -1
  2. package/source/components/form/message-state-button.mjs +1 -1
  3. package/source/components/form/select-link.mjs +12 -1
  4. package/source/components/state/log.mjs +3 -7
  5. package/source/components/state/thread.mjs +3 -7
  6. package/source/components/time/day.mjs +0 -19
  7. package/source/components/time/month-calendar.mjs +5 -1
  8. package/source/data/buildmap.mjs +3 -3
  9. package/source/data/datasource.mjs +3 -10
  10. package/source/data/diff.mjs +13 -12
  11. package/source/data/extend.mjs +23 -1
  12. package/source/data/pathfinder.mjs +186 -19
  13. package/source/dom/customelement.mjs +273 -30
  14. package/source/dom/updater.mjs +6 -2
  15. package/source/types/base.mjs +7 -6
  16. package/source/types/basewithoptions.mjs +1 -8
  17. package/source/types/internal.mjs +4 -10
  18. package/source/types/observerlist.mjs +9 -17
  19. package/source/types/proxyobserver.mjs +19 -2
  20. package/source/types/tokenlist.mjs +13 -8
  21. package/source/util/clone.mjs +44 -14
  22. package/test/cases/browser-bundle.mjs +75 -0
  23. package/test/cases/components/datatable/drag-scroll.mjs +41 -9
  24. package/test/cases/components/navigation/site-navigation.mjs +10 -1
  25. package/test/cases/components/time/month-calendar.mjs +115 -0
  26. package/test/cases/data/diff.mjs +86 -0
  27. package/test/cases/data/extend.mjs +52 -0
  28. package/test/cases/data/pathfinder-resolution-baseline.mjs +322 -0
  29. package/test/cases/data/pathfinder.mjs +129 -0
  30. package/test/cases/dom/customelement-rebind-baseline.mjs +323 -0
  31. package/test/cases/dom/customelement-teardown.mjs +408 -0
  32. package/test/cases/dom/customelement.mjs +354 -0
  33. package/test/cases/dom/dimension.mjs +34 -10
  34. package/test/cases/dom/updater-remove.mjs +198 -0
  35. package/test/cases/packaging.mjs +213 -0
  36. package/test/cases/types/base.mjs +50 -0
  37. package/test/cases/types/observerlist.mjs +103 -1
  38. package/test/cases/types/proxyobserver.mjs +114 -0
  39. package/test/cases/types/regex.mjs +4 -0
  40. package/test/cases/types/tokenlist.mjs +64 -1
  41. package/test/cases/util/clone.mjs +115 -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.","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"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","exports":{".":"./source/monster.mjs","./CHANGELOG.md":"./CHANGELOG.md","./LICENSE":"./LICENSE","./README.md":"./README.md","./package.json":"./package.json","./source/*":"./source/*","./test/*":"./test/*"},"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"},"sideEffects":["./source/monster.mjs","./source/components/**","./source/i18n/**","./test/**"],"type":"module","version":"4.154.1"}
@@ -333,8 +333,8 @@ class MessageStateButton extends Popper {
333
333
  }
334
334
 
335
335
  disconnectedCallback() {
336
- clearAutoHideTimer.call(this);
337
336
  super.disconnectedCallback();
337
+ clearAutoHideTimer.call(this);
338
338
  }
339
339
 
340
340
  /**
@@ -67,11 +67,22 @@ class SelectLink extends CustomElement {
67
67
 
68
68
  [assembleMethodSymbol]() {
69
69
  super[assembleMethodSymbol]();
70
- bindSelects.call(this);
71
70
  return this;
72
71
  }
73
72
 
73
+ /**
74
+ * Binding belongs to the connect lifecycle, not to assembly: disconnecting
75
+ * unbinds, so a reconnect has to bind again. bindSelects() is idempotent.
76
+ *
77
+ * @inheritdoc
78
+ */
79
+ connectedCallback() {
80
+ super.connectedCallback();
81
+ bindSelects.call(this);
82
+ }
83
+
74
84
  disconnectedCallback() {
85
+ super.disconnectedCallback();
75
86
  unbindSelects.call(this);
76
87
  }
77
88
  }
@@ -136,13 +136,9 @@ class Log extends CustomElement {
136
136
  */
137
137
  getOption(path, defaultValue = undefined) {
138
138
  if (path === "entries" || path?.startsWith("entries.")) {
139
- try {
140
- return new Pathfinder({
141
- entries: this[entriesSymbol],
142
- }).getVia(path);
143
- } catch (e) {
144
- return defaultValue;
145
- }
139
+ return new Pathfinder({
140
+ entries: this[entriesSymbol],
141
+ }).getVia(path, defaultValue);
146
142
  }
147
143
 
148
144
  return super.getOption(path, defaultValue);
@@ -143,13 +143,9 @@ class Thread extends CustomElement {
143
143
  */
144
144
  getOption(path, defaultValue = undefined) {
145
145
  if (path === "entries" || path?.startsWith("entries.")) {
146
- try {
147
- return new Pathfinder({
148
- entries: this[entriesSymbol],
149
- }).getVia(path);
150
- } catch (e) {
151
- return defaultValue;
152
- }
146
+ return new Pathfinder({
147
+ entries: this[entriesSymbol],
148
+ }).getVia(path, defaultValue);
153
149
  }
154
150
 
155
151
  return super.getOption(path, defaultValue);
@@ -363,25 +363,6 @@ function getLabels() {
363
363
  }
364
364
  }
365
365
 
366
- /**
367
- * @private
368
- * @param format
369
- * @returns {*[]}
370
- */
371
- function getWeekdays(format = "long") {
372
- const locale = getLocaleOfDocument();
373
-
374
- const weekdays = [];
375
- for (let i = 1; i < 8; i++) {
376
- const date = new Date(1970, 0, 4 + i); // 4. Jan. 1970 = Sonntag
377
- weekdays.push(
378
- new Intl.DateTimeFormat(locale, { weekday: format }).format(date),
379
- );
380
- }
381
-
382
- return weekdays;
383
- }
384
-
385
366
  /**
386
367
  * @private
387
368
  * @return {void}
@@ -363,7 +363,11 @@ function getAppointmentRowsUsingCalendar(
363
363
  * @returns {*[]}
364
364
  */
365
365
  function getWeekdays(format = "long") {
366
- const locale = getLocaleOfDocument();
366
+ // Intl needs the language tag. Handing it the Locale instance makes
367
+ // CanonicalizeLocaleList treat the object as array like, and its missing
368
+ // length yields an empty locale list, so the formatter silently falls back
369
+ // to the host system locale and the document language is ignored.
370
+ const locale = getLocaleOfDocument().toString();
367
371
 
368
372
  const weekdays = [];
369
373
  for (let i = 1; i < 8; i++) {
@@ -181,9 +181,9 @@ function buildFlatMap(subject, selector, key, parentMap) {
181
181
  for (const [k, o] of map) {
182
182
  const copyKey = clone(key);
183
183
 
184
- currentPath.map((a) => {
185
- copyKey.push(a === WILDCARD ? k : a);
186
- });
184
+ for (const segment of currentPath) {
185
+ copyKey.push(segment === WILDCARD ? k : segment);
186
+ }
187
187
 
188
188
  const kk = copyKey.join(DELIMITER);
189
189
  const sub = buildFlatMap.call(
@@ -165,16 +165,9 @@ class Datasource extends Base {
165
165
  * @return {*}
166
166
  */
167
167
  getOption(path, defaultValue) {
168
- let value;
169
-
170
- try {
171
- value = new Pathfinder(
172
- this[internalSymbol].getRealSubject()["options"],
173
- ).getVia(path);
174
- } catch (e) {}
175
-
176
- if (value === undefined) return defaultValue;
177
- return value;
168
+ return new Pathfinder(
169
+ this[internalSymbol].getRealSubject()["options"],
170
+ ).getVia(path, defaultValue);
178
171
  }
179
172
 
180
173
  /**
@@ -35,20 +35,15 @@ function diff(first, second) {
35
35
  }
36
36
 
37
37
  /**
38
+ * Own keys of both sides. Arrays are covered by this as well: their indices are
39
+ * own enumerable keys, and reporting them as string path segments is the
40
+ * established contract for every diff path.
38
41
  * @private
39
42
  * @param a
40
43
  * @param b
41
- * @param type
42
- * @return {Set<string>|Set<number>}
44
+ * @return {Set<string>}
43
45
  */
44
- function getKeys(a, b, type) {
45
- if (isArray(type)) {
46
- const keys =
47
- a.length > b.length ? new Array(a.length) : new Array(b.length);
48
- keys.fill(0);
49
- return new Set(keys.map((_, i) => i));
50
- }
51
-
46
+ function getKeys(a, b) {
52
47
  return new Set(Object.keys(a).concat(Object.keys(b)));
53
48
  }
54
49
 
@@ -68,7 +63,7 @@ function doDiff(a, b, path, diff) {
68
63
  const currDiff = diff || [];
69
64
 
70
65
  if (typeA === typeB && (typeA === "object" || typeA === "array")) {
71
- const keys = getKeys(a, b, typeA);
66
+ const keys = getKeys(a, b);
72
67
  if (keys.size === 0) {
73
68
  return currDiff;
74
69
  }
@@ -83,7 +78,7 @@ function doDiff(a, b, path, diff) {
83
78
  }
84
79
  });
85
80
  } else {
86
- const o = getOperator(a, b, typeA, typeB);
81
+ const o = getOperator(a, b);
87
82
  if (o !== undefined) {
88
83
  currDiff.push(buildResult(a, b, o, path));
89
84
  }
@@ -153,6 +148,12 @@ function isNotEqual(a, b) {
153
148
  return a.getTime() !== b.getTime();
154
149
  }
155
150
 
151
+ // Two NaN values at the same path are the same value. Object.is() is not used
152
+ // here because it would additionally report 0 and -0 as a change.
153
+ if (Number.isNaN(a) && Number.isNaN(b)) {
154
+ return false;
155
+ }
156
+
156
157
  return a !== b;
157
158
  }
158
159
 
@@ -33,6 +33,16 @@ const unsafeMutationKeys = new Set(["__proto__", "prototype", "constructor"]);
33
33
  * @param args
34
34
  */
35
35
  function extend(...args) {
36
+ return extendValues(args, new Map());
37
+ }
38
+
39
+ /**
40
+ * @private
41
+ * @param {array} args
42
+ * @param {Map} path source objects currently being merged, mapped to their target
43
+ * @return {object}
44
+ */
45
+ function extendValues(args, path) {
36
46
  let o;
37
47
  let i;
38
48
 
@@ -65,6 +75,13 @@ function extend(...args) {
65
75
  }
66
76
 
67
77
  if ((isObject(v) && typeOf(v) === "object") || isArray(v)) {
78
+ // A source object that is still being merged further up the call
79
+ // chain is a cycle. Reuse its target instead of recursing forever.
80
+ if (path.has(v)) {
81
+ o[k] = path.get(v);
82
+ continue;
83
+ }
84
+
68
85
  if (o[k] === undefined) {
69
86
  if (isArray(v)) {
70
87
  o[k] = [];
@@ -87,7 +104,12 @@ function extend(...args) {
87
104
  continue;
88
105
  }
89
106
 
90
- o[k] = extend(o[k], v);
107
+ path.set(v, o[k]);
108
+ try {
109
+ o[k] = extendValues([o[k], v], path);
110
+ } finally {
111
+ path.delete(v);
112
+ }
91
113
  } else {
92
114
  if (isArray(o)) {
93
115
  o.push(v);
@@ -133,8 +133,14 @@ class Pathfinder extends Base {
133
133
  }
134
134
 
135
135
  /**
136
+ * Reads the value at the given path.
137
+ *
138
+ * Without a second argument a path that cannot be traversed throws, and a
139
+ * missing leaf resolves to undefined. With a second argument the value is
140
+ * returned instead of the error, and also instead of an undefined result.
136
141
  *
137
142
  * @param {string|array} path
143
+ * @param {*} defaultValue returned when the path cannot be resolved
138
144
  * @since 1.4.0
139
145
  * @return {*}
140
146
  * @throws {TypeError} unsupported type
@@ -143,8 +149,18 @@ class Pathfinder extends Base {
143
149
  * @throws {TypeError} value is not an integer
144
150
  * @throws {Error} unsupported action for this data type
145
151
  */
146
- getVia(path) {
147
- return getValueViaPath.call(this, this.object, path);
152
+ getVia(path, defaultValue) {
153
+ // Without a second argument the throwing contract is unchanged.
154
+ if (arguments.length < 2) {
155
+ return getValueViaPath.call(this, this.object, path);
156
+ }
157
+
158
+ const value = resolveValueViaPath.call(this, this.object, path, false);
159
+ if (value === NOT_FOUND || value === undefined) {
160
+ return defaultValue;
161
+ }
162
+
163
+ return value;
148
164
  }
149
165
 
150
166
  /**
@@ -189,13 +205,173 @@ class Pathfinder extends Base {
189
205
  * @since 1.4.0
190
206
  */
191
207
  exists(path) {
208
+ return (
209
+ resolveValueViaPath.call(this, this.object, path, true) !== NOT_FOUND
210
+ );
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Returned instead of throwing when a path cannot be resolved.
216
+ * @private
217
+ * @type {symbol}
218
+ */
219
+ const NOT_FOUND = Symbol("@schukai/monster/data/pathfinder@@notFound");
220
+
221
+ /**
222
+ * Split paths are memoized. getOption() resolves about 1393 option paths across
223
+ * the library, and the same handful of strings is used over and over.
224
+ * @private
225
+ * @type {Map<string, string[]>}
226
+ */
227
+ const pathSplitCache = new Map();
228
+
229
+ /**
230
+ * @private
231
+ * @type {number}
232
+ */
233
+ const PATH_SPLIT_CACHE_LIMIT = 2048;
234
+
235
+ /**
236
+ * The returned array is shared and must never be mutated by the caller.
237
+ * @private
238
+ * @param {string} path
239
+ * @return {string[]}
240
+ */
241
+ function splitPath(path) {
242
+ const cached = pathSplitCache.get(path);
243
+ if (cached !== undefined) {
244
+ return cached;
245
+ }
246
+
247
+ const parts = path.split(DELIMITER);
248
+
249
+ if (pathSplitCache.size >= PATH_SPLIT_CACHE_LIMIT) {
250
+ pathSplitCache.clear();
251
+ }
252
+ pathSplitCache.set(path, parts);
253
+
254
+ return parts;
255
+ }
256
+
257
+ /**
258
+ * A complete integer path segment, or undefined when the segment is not one.
259
+ * @private
260
+ * @param {*} value
261
+ * @return {number|undefined}
262
+ */
263
+ function toPathIndex(value) {
264
+ if (!/^-?\d+$/.test(`${value}`)) {
265
+ return undefined;
266
+ }
267
+
268
+ const index = Number(value);
269
+ if (!Number.isInteger(index)) {
270
+ return undefined;
271
+ }
272
+
273
+ return index;
274
+ }
275
+
276
+ /**
277
+ * Non throwing counterpart of getValueViaPath(). Returns NOT_FOUND instead of
278
+ * throwing, so that resolving an option does not construct an exception, and
279
+ * walks the path iteratively instead of copying and shifting it per segment.
280
+ *
281
+ * The membership semantics of `check` are the ones exists() relies on: a key
282
+ * that is present with value undefined counts as found, and a malformed path is
283
+ * simply not found rather than an error.
284
+ *
285
+ * @private
286
+ * @param {*} subject
287
+ * @param {string|array} path
288
+ * @param {boolean} check
289
+ * @return {*|symbol} the resolved value or NOT_FOUND
290
+ */
291
+ function resolveValueViaPath(subject, path, check) {
292
+ let parts;
293
+ if (isArray(path)) {
294
+ if (path.length === 0) {
295
+ return subject;
296
+ }
297
+ parts = path;
298
+ } else if (isString(path)) {
299
+ if (path === "") {
300
+ return subject;
301
+ }
302
+ parts = splitPath(path);
303
+ } else {
304
+ return NOT_FOUND;
305
+ }
306
+
307
+ // A wildcard builds a Map through iterate(), which stays on the throwing
308
+ // implementation. Those paths are rare, so fall back for them.
309
+ if (parts.includes(this.wildCard)) {
192
310
  try {
193
- getValueViaPath.call(this, this.object, path, true);
194
- return true;
195
- } catch (e) {}
311
+ return getValueViaPath.call(this, subject, path, check === true);
312
+ } catch (e) {
313
+ return NOT_FOUND;
314
+ }
315
+ }
196
316
 
197
- return false;
317
+ let cursor = subject;
318
+ const last = parts.length - 1;
319
+
320
+ for (let i = 0; i <= last; i++) {
321
+ if (!(isObject(cursor) || isArray(cursor))) {
322
+ return NOT_FOUND;
323
+ }
324
+
325
+ if (cursor instanceof WeakSet) {
326
+ return NOT_FOUND;
327
+ }
328
+
329
+ if (typeof WeakRef === "function" && cursor instanceof WeakRef) {
330
+ return NOT_FOUND;
331
+ }
332
+
333
+ const segment = parts[i];
334
+ const isLast = i === last;
335
+ let value;
336
+
337
+ if (cursor instanceof Map || cursor instanceof WeakMap) {
338
+ if (isLast && check === true && !cursor.has(segment)) {
339
+ return NOT_FOUND;
340
+ }
341
+ value = cursor.get(segment);
342
+ } else if (cursor instanceof Set) {
343
+ const index = toPathIndex(segment);
344
+ if (index === undefined) {
345
+ return NOT_FOUND;
346
+ }
347
+ if (isLast && check === true && !(index >= 0 && index < cursor.size)) {
348
+ return NOT_FOUND;
349
+ }
350
+ value = [...cursor][index];
351
+ } else if (isArray(cursor)) {
352
+ const index = toPathIndex(segment);
353
+ if (index === undefined) {
354
+ return NOT_FOUND;
355
+ }
356
+ if (isLast && check === true && !Reflect.has(cursor, index)) {
357
+ return NOT_FOUND;
358
+ }
359
+ value = cursor[index];
360
+ } else {
361
+ if (isLast && check === true && !Reflect.has(cursor, segment)) {
362
+ return NOT_FOUND;
363
+ }
364
+ value = cursor[segment];
365
+ }
366
+
367
+ if (isLast) {
368
+ return value;
369
+ }
370
+
371
+ cursor = value;
198
372
  }
373
+
374
+ return NOT_FOUND;
199
375
  }
200
376
 
201
377
  /**
@@ -224,10 +400,7 @@ function iterate(subject, path, check) {
224
400
  } else if (subject instanceof Set) {
225
401
  let index = 0;
226
402
  for (const value of subject) {
227
- result.set(
228
- `${index++}`,
229
- getValueViaPath.call(this, value, path, check),
230
- );
403
+ result.set(`${index++}`, getValueViaPath.call(this, value, path, check));
231
404
  }
232
405
  } else if (isObject(subject) || isArray(subject)) {
233
406
  for (const [key, value] of Object.entries(subject)) {
@@ -423,15 +596,9 @@ function setValueViaPath(subject, path, value) {
423
596
  * @param {*} value
424
597
  */
425
598
  function assignProperty(object, key, value) {
426
- if (!Object.hasOwn(object, key)) {
427
- object[key] = value;
428
- return;
429
- }
430
-
431
- if (value === undefined) {
432
- delete object[key];
433
- }
434
-
599
+ // Writing undefined keeps the key. Deletion is deliberately left to
600
+ // deleteVia(), because a default option declared as undefined must stay
601
+ // present for setOptionFromAttribute() to keep accepting changes for it.
435
602
  object[key] = value;
436
603
  }
437
604