@schukai/monster 4.153.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.
- package/package.json +1 -1
- package/source/data/buildmap.mjs +3 -2
- package/source/data/buildtree.mjs +0 -8
- package/source/data/pathfinder.mjs +48 -19
- package/source/data/transformer.mjs +2 -2
- package/source/dom/customcontrol.mjs +35 -9
- package/source/dom/customelement.mjs +21 -2
- package/source/dom/slotted.mjs +46 -31
- package/source/dom/template.mjs +4 -1
- package/source/dom/updater.mjs +51 -15
- package/source/types/node.mjs +1 -3
- package/source/types/proxyobserver.mjs +32 -10
- package/test/cases/data/buildmap.mjs +14 -0
- package/test/cases/data/buildtree.mjs +49 -0
- package/test/cases/data/pathfinder.mjs +34 -0
- package/test/cases/data/transformer.mjs +26 -0
- package/test/cases/dom/customcontrol.mjs +85 -0
- package/test/cases/dom/customelement.mjs +54 -0
- package/test/cases/dom/slotted-nodes.mjs +48 -1
- package/test/cases/dom/template.mjs +11 -0
- package/test/cases/dom/updater.mjs +118 -0
- package/test/cases/types/proxyobserver.mjs +75 -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
|
+
{"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"}
|
package/source/data/buildmap.mjs
CHANGED
|
@@ -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)
|
|
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 (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
308
|
-
|
|
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 (!
|
|
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 (!
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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([
|
|
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(
|
|
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
|
}
|
package/source/dom/slotted.mjs
CHANGED
|
@@ -27,37 +27,33 @@ export { getSlottedElements, getSlottedNodes };
|
|
|
27
27
|
* @since 3.33.0
|
|
28
28
|
* @throws {Error} query must be a string
|
|
29
29
|
*/
|
|
30
|
-
function getSlottedNodes(query, name) {
|
|
30
|
+
function getSlottedNodes(query, name, managedShadowRoot) {
|
|
31
31
|
const result = new Set();
|
|
32
|
+
const shadowRoot = getShadowRoot(this, managedShadowRoot);
|
|
32
33
|
|
|
33
|
-
if (!
|
|
34
|
+
if (!(shadowRoot instanceof ShadowRoot)) {
|
|
34
35
|
return result;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
if (name !== undefined) {
|
|
39
|
-
if (name === null) {
|
|
40
|
-
selector += ":not([name])";
|
|
41
|
-
} else {
|
|
42
|
-
selector += `[name=${validateString(name)}]`;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const slots = this.shadowRoot.querySelectorAll(selector);
|
|
38
|
+
const slots = getSlotsByName(shadowRoot, name);
|
|
47
39
|
|
|
48
|
-
for (const
|
|
40
|
+
for (const slot of slots) {
|
|
49
41
|
slot.assignedNodes().forEach(function (node) {
|
|
50
42
|
if (node === null || node === undefined) {
|
|
51
43
|
return;
|
|
52
44
|
}
|
|
53
45
|
|
|
54
46
|
if (isString(query)) {
|
|
55
|
-
|
|
56
|
-
result.add(n);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (node.matches(query)) {
|
|
47
|
+
if (query.length === 0) {
|
|
60
48
|
result.add(node);
|
|
49
|
+
} else if (node instanceof Element) {
|
|
50
|
+
node.querySelectorAll(query).forEach(function (n) {
|
|
51
|
+
result.add(n);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (node.matches(query)) {
|
|
55
|
+
result.add(node);
|
|
56
|
+
}
|
|
61
57
|
}
|
|
62
58
|
} else if (query !== undefined) {
|
|
63
59
|
throw new Error("query must be a string");
|
|
@@ -80,25 +76,17 @@ function getSlottedNodes(query, name) {
|
|
|
80
76
|
* @since 1.23.0
|
|
81
77
|
* @throws {Error} query must be a string
|
|
82
78
|
*/
|
|
83
|
-
function getSlottedElements(query, name) {
|
|
79
|
+
function getSlottedElements(query, name, managedShadowRoot) {
|
|
84
80
|
const result = new Set();
|
|
81
|
+
const shadowRoot = getShadowRoot(this, managedShadowRoot);
|
|
85
82
|
|
|
86
|
-
if (!(
|
|
83
|
+
if (!(shadowRoot instanceof ShadowRoot)) {
|
|
87
84
|
return result;
|
|
88
85
|
}
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
if (name !== undefined) {
|
|
92
|
-
if (name === null) {
|
|
93
|
-
selector += ":not([name])";
|
|
94
|
-
} else {
|
|
95
|
-
selector += `[name=${validateString(name)}]`;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const slots = this.shadowRoot.querySelectorAll(selector);
|
|
87
|
+
const slots = getSlotsByName(shadowRoot, name);
|
|
100
88
|
|
|
101
|
-
for (const
|
|
89
|
+
for (const slot of slots) {
|
|
102
90
|
slot.assignedElements().forEach(function (node) {
|
|
103
91
|
if (
|
|
104
92
|
!(node instanceof HTMLElement) &&
|
|
@@ -129,3 +117,30 @@ function getSlottedElements(query, name) {
|
|
|
129
117
|
|
|
130
118
|
return result;
|
|
131
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {*} host
|
|
123
|
+
* @param {*} managedShadowRoot
|
|
124
|
+
* @return {ShadowRoot|null}
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
function getShadowRoot(host, managedShadowRoot) {
|
|
128
|
+
if (managedShadowRoot instanceof ShadowRoot) return managedShadowRoot;
|
|
129
|
+
return host?.shadowRoot instanceof ShadowRoot ? host.shadowRoot : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Filter slots by their DOM name instead of interpolating an unsafe selector.
|
|
134
|
+
* @param {ShadowRoot} shadowRoot
|
|
135
|
+
* @param {string|undefined|null} name
|
|
136
|
+
* @return {HTMLSlotElement[]}
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
function getSlotsByName(shadowRoot, name) {
|
|
140
|
+
if (name !== undefined && name !== null) validateString(name);
|
|
141
|
+
return Array.from(shadowRoot.querySelectorAll("slot")).filter((slot) => {
|
|
142
|
+
if (name === undefined) return true;
|
|
143
|
+
if (name === null) return slot.name === "";
|
|
144
|
+
return slot.name === name;
|
|
145
|
+
});
|
|
146
|
+
}
|
package/source/dom/template.mjs
CHANGED
|
@@ -156,7 +156,10 @@ export function findDocumentTemplate(id, currentNode) {
|
|
|
156
156
|
)
|
|
157
157
|
) {
|
|
158
158
|
if (currentNode instanceof Node) {
|
|
159
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
currentNode instanceof Element &&
|
|
161
|
+
currentNode.hasAttribute(ATTRIBUTE_TEMPLATE_PREFIX)
|
|
162
|
+
) {
|
|
160
163
|
prefixID = currentNode.getAttribute(ATTRIBUTE_TEMPLATE_PREFIX);
|
|
161
164
|
}
|
|
162
165
|
|
package/source/dom/updater.mjs
CHANGED
|
@@ -803,17 +803,34 @@ function parseIntArray(val) {
|
|
|
803
803
|
* @private
|
|
804
804
|
*/
|
|
805
805
|
function retrieveFromBindings() {
|
|
806
|
-
if (
|
|
806
|
+
if (
|
|
807
|
+
this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`) &&
|
|
808
|
+
!isUncheckedRadio(this[internalSymbol].element)
|
|
809
|
+
) {
|
|
807
810
|
retrieveAndSetValue.call(this, this[internalSymbol].element);
|
|
808
811
|
}
|
|
809
812
|
|
|
810
813
|
for (const [, element] of this[internalSymbol].element
|
|
811
814
|
.querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`)
|
|
812
815
|
.entries()) {
|
|
816
|
+
if (isUncheckedRadio(element)) continue;
|
|
813
817
|
retrieveAndSetValue.call(this, element);
|
|
814
818
|
}
|
|
815
819
|
}
|
|
816
820
|
|
|
821
|
+
/**
|
|
822
|
+
* @param {HTMLElement} element
|
|
823
|
+
* @return {boolean}
|
|
824
|
+
* @private
|
|
825
|
+
*/
|
|
826
|
+
function isUncheckedRadio(element) {
|
|
827
|
+
return (
|
|
828
|
+
element instanceof HTMLInputElement &&
|
|
829
|
+
element.type === "radio" &&
|
|
830
|
+
element.checked === false
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
817
834
|
/**
|
|
818
835
|
* @private
|
|
819
836
|
* @license AGPLv3
|
|
@@ -1201,7 +1218,10 @@ function getSlots(container, cache) {
|
|
|
1201
1218
|
return cache.get(container);
|
|
1202
1219
|
}
|
|
1203
1220
|
|
|
1204
|
-
const slots = container.querySelectorAll("slot");
|
|
1221
|
+
const slots = Array.from(container.querySelectorAll("slot"));
|
|
1222
|
+
if (container.localName === "slot") {
|
|
1223
|
+
slots.unshift(container);
|
|
1224
|
+
}
|
|
1205
1225
|
cache.set(container, slots);
|
|
1206
1226
|
return slots;
|
|
1207
1227
|
}
|
|
@@ -1565,6 +1585,9 @@ function replaceKeyedPatchedChildren(
|
|
|
1565
1585
|
const nextNode = preparePatchedNode(reused, renderedValue);
|
|
1566
1586
|
nextNode[patchNodeKeySymbol] = normalizedKey;
|
|
1567
1587
|
if (nextNode !== reused && reused.parentNode === element) {
|
|
1588
|
+
if (reused instanceof HTMLElement) {
|
|
1589
|
+
teardownManagedSubtree(reused);
|
|
1590
|
+
}
|
|
1568
1591
|
element.replaceChild(nextNode, reused);
|
|
1569
1592
|
}
|
|
1570
1593
|
nextNodes.push(nextNode);
|
|
@@ -1591,15 +1614,11 @@ function replaceKeyedPatchedChildren(
|
|
|
1591
1614
|
}
|
|
1592
1615
|
}
|
|
1593
1616
|
|
|
1594
|
-
for (
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
if (element.lastChild !== node) {
|
|
1601
|
-
element.appendChild(node);
|
|
1602
|
-
}
|
|
1617
|
+
for (let index = 0; index < nextNodes.length; index++) {
|
|
1618
|
+
const node = nextNodes[index];
|
|
1619
|
+
const current = element.childNodes[index] ?? null;
|
|
1620
|
+
if (current === node) continue;
|
|
1621
|
+
element.insertBefore(node, current);
|
|
1603
1622
|
}
|
|
1604
1623
|
}
|
|
1605
1624
|
|
|
@@ -1851,8 +1870,7 @@ function runUpdateAttributes(
|
|
|
1851
1870
|
if (
|
|
1852
1871
|
!element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
|
|
1853
1872
|
!attributes.includes(`path:${current}`) &&
|
|
1854
|
-
!attributes
|
|
1855
|
-
!attributes.startsWith("i18n:")
|
|
1873
|
+
!hasUnconditionalBinding(attributes)
|
|
1856
1874
|
) {
|
|
1857
1875
|
continue;
|
|
1858
1876
|
}
|
|
@@ -1933,8 +1951,7 @@ function runUpdateProperties(
|
|
|
1933
1951
|
if (
|
|
1934
1952
|
!element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
|
|
1935
1953
|
!properties.includes(`path:${current}`) &&
|
|
1936
|
-
!properties
|
|
1937
|
-
!properties.startsWith("i18n:")
|
|
1954
|
+
!hasUnconditionalBinding(properties)
|
|
1938
1955
|
) {
|
|
1939
1956
|
continue;
|
|
1940
1957
|
}
|
|
@@ -1981,6 +1998,9 @@ function runUpdateProperties(
|
|
|
1981
1998
|
|
|
1982
1999
|
function handleInputControlAttributeUpdate(element, name, value) {
|
|
1983
2000
|
if (element instanceof HTMLSelectElement) {
|
|
2001
|
+
if (name !== "value") {
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
1984
2004
|
switch (element.type) {
|
|
1985
2005
|
case "select-multiple":
|
|
1986
2006
|
const selectedValues = isArray(value)
|
|
@@ -2054,6 +2074,22 @@ function handleInputControlAttributeUpdate(element, name, value) {
|
|
|
2054
2074
|
}
|
|
2055
2075
|
}
|
|
2056
2076
|
|
|
2077
|
+
/**
|
|
2078
|
+
* Check whether an attribute/property definition contains a command that is
|
|
2079
|
+
* independent from the changed subject path.
|
|
2080
|
+
* @param {string} definitions
|
|
2081
|
+
* @return {boolean}
|
|
2082
|
+
* @private
|
|
2083
|
+
*/
|
|
2084
|
+
function hasUnconditionalBinding(definitions) {
|
|
2085
|
+
return definitions.split(",").some((definition) => {
|
|
2086
|
+
const separator = definition.indexOf(" ");
|
|
2087
|
+
if (separator < 0) return false;
|
|
2088
|
+
const command = trimSpaces(definition.substring(separator + 1));
|
|
2089
|
+
return command.startsWith("static:") || command.startsWith("i18n:");
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2057
2093
|
/**
|
|
2058
2094
|
* @private
|
|
2059
2095
|
* @param {HTMLElement|*} element
|
package/source/types/node.mjs
CHANGED
|
@@ -199,8 +199,6 @@ class Node extends Base {
|
|
|
199
199
|
* @return {setChildLevelAndParent}
|
|
200
200
|
*/
|
|
201
201
|
function setChildLevelAndParent(node, operand, visitedNodes) {
|
|
202
|
-
const self = this;
|
|
203
|
-
|
|
204
202
|
if (visitedNodes.has(node)) {
|
|
205
203
|
throw new Error(
|
|
206
204
|
"the node has already been visited and cannot be traversed again",
|
|
@@ -217,7 +215,7 @@ function setChildLevelAndParent(node, operand, visitedNodes) {
|
|
|
217
215
|
child[treeStructureSymbol].parent = node;
|
|
218
216
|
child[treeStructureSymbol].level =
|
|
219
217
|
node[treeStructureSymbol].level + operand;
|
|
220
|
-
setChildLevelAndParent.call(
|
|
218
|
+
setChildLevelAndParent.call(node, child, operand, visitedNodes);
|
|
221
219
|
});
|
|
222
220
|
return this;
|
|
223
221
|
}
|
|
@@ -156,6 +156,15 @@ function getHandler() {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
const value = Reflect.get(target, key, receiver);
|
|
159
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
|
|
160
|
+
if (
|
|
161
|
+
descriptor &&
|
|
162
|
+
descriptor.configurable === false &&
|
|
163
|
+
"value" in descriptor &&
|
|
164
|
+
descriptor.writable === false
|
|
165
|
+
) {
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
159
168
|
|
|
160
169
|
if (typeof key === "symbol") {
|
|
161
170
|
return value;
|
|
@@ -233,16 +242,12 @@ function getHandler() {
|
|
|
233
242
|
|
|
234
243
|
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-delete-p
|
|
235
244
|
deleteProperty: function (target, key) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
proxy.observers.notify(proxy);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return true;
|
|
245
|
+
const hadOwnProperty = Object.hasOwn(target, key);
|
|
246
|
+
const result = Reflect.deleteProperty(target, key);
|
|
247
|
+
if (result && hadOwnProperty && typeof key !== "symbol") {
|
|
248
|
+
proxy.observers.notify(proxy);
|
|
244
249
|
}
|
|
245
|
-
return
|
|
250
|
+
return result;
|
|
246
251
|
},
|
|
247
252
|
|
|
248
253
|
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc
|
|
@@ -277,5 +282,22 @@ function getHandler() {
|
|
|
277
282
|
* @private
|
|
278
283
|
*/
|
|
279
284
|
function isNativeObjectWithInternalSlots(value) {
|
|
280
|
-
|
|
285
|
+
if (
|
|
286
|
+
value instanceof Date ||
|
|
287
|
+
value instanceof RegExp ||
|
|
288
|
+
value instanceof Map ||
|
|
289
|
+
value instanceof Set ||
|
|
290
|
+
value instanceof WeakMap ||
|
|
291
|
+
value instanceof WeakSet ||
|
|
292
|
+
value instanceof Promise ||
|
|
293
|
+
value instanceof ArrayBuffer ||
|
|
294
|
+
ArrayBuffer.isView(value)
|
|
295
|
+
) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
typeof SharedArrayBuffer === "function" &&
|
|
301
|
+
value instanceof SharedArrayBuffer
|
|
302
|
+
);
|
|
281
303
|
}
|
|
@@ -6,6 +6,20 @@ import {buildMap} from "../../../source/data/buildmap.mjs";
|
|
|
6
6
|
|
|
7
7
|
describe('buildMap', function () {
|
|
8
8
|
|
|
9
|
+
describe('falsy keys and literal replacements issue #569', function () {
|
|
10
|
+
it('should preserve a zero key returned by a selector callback', function () {
|
|
11
|
+
const value = {label: 'zero'};
|
|
12
|
+
const map = buildMap({}, () => new Map([[0, value]]), '${label}');
|
|
13
|
+
expect([...map]).to.deep.equal([[0, 'zero']]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should insert replacement tokens literally', function () {
|
|
17
|
+
const value = {label: '$&-$`-$\''};
|
|
18
|
+
const map = buildMap({}, () => new Map([['key', value]]), '${label}');
|
|
19
|
+
expect(map.get('key')).to.equal('$&-$`-$\'');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
9
23
|
let convertMapResult = function (r) {
|
|
10
24
|
if (r instanceof Map) {
|
|
11
25
|
r = Object.fromEntries(r);
|
|
@@ -170,6 +170,55 @@ describe('buildTree', function () {
|
|
|
170
170
|
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
describe('parent metadata issue #568', function () {
|
|
174
|
+
const createTree = (records) => buildTree(
|
|
175
|
+
records,
|
|
176
|
+
(values) => new Map(values.map((value, index) => [index, value])),
|
|
177
|
+
'id',
|
|
178
|
+
'parent',
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
[
|
|
182
|
+
[
|
|
183
|
+
{id: 1, parent: null},
|
|
184
|
+
{id: 2, parent: 1},
|
|
185
|
+
{id: 3, parent: 2},
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
{id: 3, parent: 2},
|
|
189
|
+
{id: 2, parent: 1},
|
|
190
|
+
{id: 1, parent: null},
|
|
191
|
+
],
|
|
192
|
+
].forEach((records, index) => {
|
|
193
|
+
it(`should retain immediate parents independent of input order ${index + 1}`, function () {
|
|
194
|
+
const tree = createTree(records);
|
|
195
|
+
const root = [...tree][0];
|
|
196
|
+
const child = [...root.childNodes][0];
|
|
197
|
+
const leaf = [...child.childNodes][0];
|
|
198
|
+
|
|
199
|
+
expect(root.parent).to.equal(null);
|
|
200
|
+
expect(root.level).to.equal(0);
|
|
201
|
+
expect(child.parent).to.equal(root);
|
|
202
|
+
expect(child.level).to.equal(1);
|
|
203
|
+
expect(leaf.parent).to.equal(child);
|
|
204
|
+
expect(leaf.level).to.equal(2);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should preserve frozen input records and their identity', function () {
|
|
209
|
+
const rootValue = Object.freeze({id: 1, parent: null});
|
|
210
|
+
const childValue = Object.freeze({id: 2, parent: 1});
|
|
211
|
+
const tree = createTree(Object.freeze([childValue, rootValue]));
|
|
212
|
+
const root = [...tree][0];
|
|
213
|
+
const child = [...root.childNodes][0];
|
|
214
|
+
|
|
215
|
+
expect(root.value).to.equal(rootValue);
|
|
216
|
+
expect(child.value).to.equal(childValue);
|
|
217
|
+
expect(Reflect.ownKeys(rootValue)).to.deep.equal(['id', 'parent']);
|
|
218
|
+
expect(Reflect.ownKeys(childValue)).to.deep.equal(['id', 'parent']);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
173
222
|
describe('buildTree() test 2', function () {
|
|
174
223
|
|
|
175
224
|
/**
|
|
@@ -17,6 +17,8 @@ describe('Pathfinder', function () {
|
|
|
17
17
|
`constructor.prototype.${probe}`,
|
|
18
18
|
['__proto__', probe],
|
|
19
19
|
['constructor', 'prototype', probe],
|
|
20
|
+
[`safe.__proto__`, probe],
|
|
21
|
+
[`safe.constructor.prototype`, probe],
|
|
20
22
|
].forEach(function (path) {
|
|
21
23
|
it(`setVia(${JSON.stringify(path)}) should reject unsafe mutation paths`, function () {
|
|
22
24
|
try {
|
|
@@ -108,6 +110,38 @@ describe('Pathfinder', function () {
|
|
|
108
110
|
expect(() => new Pathfinder({values}).setVia('values.next', {}))
|
|
109
111
|
.to.throw(Error, 'unsupported action for this data type in setValueViaPath');
|
|
110
112
|
});
|
|
113
|
+
|
|
114
|
+
it('exists() should use collection membership semantics', function () {
|
|
115
|
+
expect(new Pathfinder(new Map([['entry', 0]])).exists('entry')).to.equal(true);
|
|
116
|
+
expect(new Pathfinder(new Map()).exists('entry')).to.equal(false);
|
|
117
|
+
expect(new Pathfinder(new Set([false])).exists('0')).to.equal(true);
|
|
118
|
+
expect(new Pathfinder(new Set()).exists('0')).to.equal(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('wildcards should enumerate Map and Set values', function () {
|
|
122
|
+
expect([...new Pathfinder(new Map([['entry', false]])).getVia('*')])
|
|
123
|
+
.to.deep.equal([['entry', false]]);
|
|
124
|
+
expect([...new Pathfinder(new Set(['first', 'second'])).getVia('*')])
|
|
125
|
+
.to.deep.equal([['0', 'first'], ['1', 'second']]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('collection indices should reject trailing characters', function () {
|
|
129
|
+
expect(() => new Pathfinder(['first', 'second']).getVia('1suffix'))
|
|
130
|
+
.to.throw(TypeError);
|
|
131
|
+
expect(() => new Pathfinder(new Set(['first', 'second'])).getVia('1suffix'))
|
|
132
|
+
.to.throw(TypeError);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should support null-prototype objects', function () {
|
|
136
|
+
const subject = Object.assign(Object.create(null), {present: false});
|
|
137
|
+
const pathfinder = new Pathfinder(subject);
|
|
138
|
+
|
|
139
|
+
expect(pathfinder.getVia('present')).to.equal(false);
|
|
140
|
+
expect(pathfinder.exists('present')).to.equal(true);
|
|
141
|
+
expect(pathfinder.exists('missing')).to.equal(false);
|
|
142
|
+
pathfinder.setVia('next', 0);
|
|
143
|
+
expect(subject.next).to.equal(0);
|
|
144
|
+
});
|
|
111
145
|
});
|
|
112
146
|
|
|
113
147
|
let convertMapResult = function (r) {
|
|
@@ -8,6 +8,32 @@ import {initJSDOM} from "../../util/jsdom.mjs";
|
|
|
8
8
|
|
|
9
9
|
describe('Transformer', function () {
|
|
10
10
|
|
|
11
|
+
describe('falsy property values issue #569', function () {
|
|
12
|
+
[
|
|
13
|
+
['key:value:fallback', {value: 0}, 0],
|
|
14
|
+
['property:value:fallback', {value: false}, false],
|
|
15
|
+
['index:0:fallback', [''], ''],
|
|
16
|
+
['key:value:fallback', {value: null}, null],
|
|
17
|
+
['first-key:fallback', {a: false}, false],
|
|
18
|
+
['last-key:fallback', {a: 1, z: 0}, 0],
|
|
19
|
+
].forEach(([definition, value, expected]) => {
|
|
20
|
+
it(`${definition} should preserve ${String(expected)}`, function () {
|
|
21
|
+
expect(new Transformer(definition).run(value)).to.equal(expected);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should distinguish inherited and missing properties', function () {
|
|
26
|
+
const inherited = Object.create({value: false});
|
|
27
|
+
expect(new Transformer('key:value:fallback').run(inherited)).to.equal(false);
|
|
28
|
+
expect(new Transformer('key:missing:fallback').run(inherited)).to.equal('fallback');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should support null-prototype objects', function () {
|
|
32
|
+
const value = Object.assign(Object.create(null), {entry: 0});
|
|
33
|
+
expect(new Transformer('key:entry:fallback').run(value)).to.equal(0);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
11
37
|
before(function (done) {
|
|
12
38
|
|
|
13
39
|
let promises = []
|
|
@@ -384,6 +384,91 @@ describe('DOM', function () {
|
|
|
384
384
|
control.remove();
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
+
it('should not let a stale value attribute overwrite a newer property issue #566', async function () {
|
|
388
|
+
const htmlTAG = 'monster-customcontrol-value-race-566';
|
|
389
|
+
|
|
390
|
+
class ValueRaceComponent extends CustomControl {
|
|
391
|
+
static getTag() {
|
|
392
|
+
return htmlTAG;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
get defaults() {
|
|
396
|
+
return Object.assign({}, super.defaults, {shadowMode: false});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
get value() {
|
|
400
|
+
return this._value ?? '';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
set value(value) {
|
|
404
|
+
this._value = value;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
registerCustomElement(ValueRaceComponent);
|
|
409
|
+
const control = document.createElement(htmlTAG);
|
|
410
|
+
document.body.appendChild(control);
|
|
411
|
+
|
|
412
|
+
control.setAttribute('value', 'attribute-value');
|
|
413
|
+
control.value = 'newer-property-value';
|
|
414
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
415
|
+
|
|
416
|
+
expect(control.getAttribute('value')).to.equal('attribute-value');
|
|
417
|
+
expect(control.value).to.equal('newer-property-value');
|
|
418
|
+
control.remove();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should coalesce rapid value attributes and preserve reflected setters issue #566', async function () {
|
|
422
|
+
const htmlTAG = 'monster-customcontrol-reflected-value-566';
|
|
423
|
+
|
|
424
|
+
class ReflectedValueComponent extends CustomControl {
|
|
425
|
+
static getTag() {
|
|
426
|
+
return htmlTAG;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
get defaults() {
|
|
430
|
+
return Object.assign({}, super.defaults, {shadowMode: false});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
get value() {
|
|
434
|
+
return this._value ?? '';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
set value(value) {
|
|
438
|
+
this._value = String(value ?? '');
|
|
439
|
+
if (this.getAttribute('value') !== this._value) {
|
|
440
|
+
this.setAttribute('value', this._value);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
registerCustomElement(ReflectedValueComponent);
|
|
446
|
+
const control = document.createElement(htmlTAG);
|
|
447
|
+
document.body.appendChild(control);
|
|
448
|
+
control.setAttribute('value', 'first');
|
|
449
|
+
control.setAttribute('value', 'second');
|
|
450
|
+
await new Promise((resolve, reject) => {
|
|
451
|
+
const deadline = Date.now() + 500;
|
|
452
|
+
const check = () => {
|
|
453
|
+
if (control.value === 'second') {
|
|
454
|
+
resolve();
|
|
455
|
+
} else if (Date.now() >= deadline) {
|
|
456
|
+
reject(new Error('value attribute synchronization timed out'));
|
|
457
|
+
} else {
|
|
458
|
+
setTimeout(check, 5);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
check();
|
|
462
|
+
});
|
|
463
|
+
expect(control.value).to.equal('second');
|
|
464
|
+
|
|
465
|
+
control.value = 'reflected';
|
|
466
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
467
|
+
expect(control.value).to.equal('reflected');
|
|
468
|
+
expect(control.getAttribute('value')).to.equal('reflected');
|
|
469
|
+
control.remove();
|
|
470
|
+
});
|
|
471
|
+
|
|
387
472
|
it('should replay a pre-upgrade value once after assembly issue #501', async function () {
|
|
388
473
|
const htmlTAG = 'monster-customcontrol-pre-upgrade';
|
|
389
474
|
const control = document.createElement(htmlTAG);
|
|
@@ -587,6 +587,60 @@ describe('DOM', function () {
|
|
|
587
587
|
linkedUpdater[internalSymbol].subject.detachObserver(observer);
|
|
588
588
|
}
|
|
589
589
|
});
|
|
590
|
+
|
|
591
|
+
it('should remove stale updater keys after replacing an option object issue #565', async function () {
|
|
592
|
+
const htmlTAG = 'monster-testclass-object-sync-565';
|
|
593
|
+
|
|
594
|
+
if (!customElements.get(htmlTAG)) {
|
|
595
|
+
class ObjectSyncComponent extends CustomElement {
|
|
596
|
+
static getTag() {
|
|
597
|
+
return htmlTAG;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
get defaults() {
|
|
601
|
+
return Object.assign({}, super.defaults, {
|
|
602
|
+
settings: {a: 'initial', b: 'retained'},
|
|
603
|
+
templates: {
|
|
604
|
+
main: '<span data-monster-replace="path:settings.a"></span>',
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
registerCustomElement(ObjectSyncComponent);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const element = document.createElement(htmlTAG);
|
|
614
|
+
document.getElementById('test1').appendChild(element);
|
|
615
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
616
|
+
|
|
617
|
+
let linkedUpdater;
|
|
618
|
+
for (const group of element[customElementUpdaterLinkSymbol]) {
|
|
619
|
+
for (const updater of group) {
|
|
620
|
+
linkedUpdater = updater;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
if (linkedUpdater) break;
|
|
624
|
+
}
|
|
625
|
+
expect(linkedUpdater).to.exist;
|
|
626
|
+
|
|
627
|
+
element.setOptions({settings: {a: 'merged'}});
|
|
628
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
629
|
+
expect(linkedUpdater.getSubject().settings).to.deep.include({
|
|
630
|
+
a: 'merged',
|
|
631
|
+
b: 'retained',
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const settingsProxy = linkedUpdater.getSubject().settings;
|
|
635
|
+
element.setOption('settings', {a: 'replacement'});
|
|
636
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
637
|
+
|
|
638
|
+
expect(linkedUpdater.getSubject().settings).to.equal(settingsProxy);
|
|
639
|
+
expect(linkedUpdater.getSubject().settings).to.deep.equal({
|
|
640
|
+
a: 'replacement',
|
|
641
|
+
});
|
|
642
|
+
expect(element.shadowRoot.textContent).to.equal('replacement');
|
|
643
|
+
});
|
|
590
644
|
})
|
|
591
645
|
|
|
592
646
|
describe('setOptions()', function () {
|
|
@@ -1,14 +1,61 @@
|
|
|
1
1
|
import {expect} from 'chai';
|
|
2
2
|
import {getSlottedNodes} from "../../../source/dom/slotted.mjs";
|
|
3
|
+
import {getSlottedElements} from "../../../source/dom/slotted.mjs";
|
|
4
|
+
import {initJSDOM} from "../../util/jsdom.mjs";
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
describe('getSlottedNodes', () => {
|
|
6
8
|
|
|
9
|
+
before(async () => {
|
|
10
|
+
await initJSDOM();
|
|
11
|
+
});
|
|
12
|
+
|
|
7
13
|
|
|
8
14
|
it('should return an empty Set if there is no ShadowRoot', () => {
|
|
9
15
|
expect(getSlottedNodes).is.a('function');
|
|
10
16
|
});
|
|
11
17
|
|
|
18
|
+
it('should ignore text nodes when applying an element query issue #567', () => {
|
|
19
|
+
const host = document.createElement('div');
|
|
20
|
+
host.append(' whitespace ');
|
|
21
|
+
const child = document.createElement('span');
|
|
22
|
+
child.className = 'match';
|
|
23
|
+
host.appendChild(child);
|
|
24
|
+
host.attachShadow({mode: 'open'}).innerHTML = '<slot></slot>';
|
|
25
|
+
document.body.appendChild(host);
|
|
26
|
+
|
|
27
|
+
expect([...getSlottedNodes.call(host, '.match')]).to.deep.equal([child]);
|
|
28
|
+
host.remove();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should match empty and spaced slot names without selector interpolation issue #567', () => {
|
|
32
|
+
const host = document.createElement('div');
|
|
33
|
+
const defaultChild = document.createElement('span');
|
|
34
|
+
const namedChild = document.createElement('span');
|
|
35
|
+
namedChild.slot = 'some slot';
|
|
36
|
+
host.append(defaultChild, namedChild);
|
|
37
|
+
host.attachShadow({mode: 'open'}).innerHTML =
|
|
38
|
+
'<slot name=""></slot><slot name="some slot"></slot>';
|
|
39
|
+
document.body.appendChild(host);
|
|
40
|
+
|
|
41
|
+
expect([...getSlottedElements.call(host, undefined, null)]).to.deep.equal([defaultChild]);
|
|
42
|
+
expect([...getSlottedElements.call(host, undefined, 'some slot')]).to.deep.equal([namedChild]);
|
|
43
|
+
host.remove();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should accept the internally managed closed shadow root issue #567', () => {
|
|
47
|
+
const host = document.createElement('div');
|
|
48
|
+
const child = document.createElement('span');
|
|
49
|
+
host.appendChild(child);
|
|
50
|
+
const root = host.attachShadow({mode: 'closed'});
|
|
51
|
+
root.innerHTML = '<slot></slot>';
|
|
52
|
+
document.body.appendChild(host);
|
|
53
|
+
|
|
54
|
+
expect(host.shadowRoot).to.equal(null);
|
|
55
|
+
expect([...getSlottedElements.call(host, undefined, undefined, root)])
|
|
56
|
+
.to.deep.equal([child]);
|
|
57
|
+
host.remove();
|
|
58
|
+
});
|
|
12
59
|
|
|
13
|
-
});
|
|
14
60
|
|
|
61
|
+
});
|
|
@@ -81,6 +81,17 @@ describe('Template', function () {
|
|
|
81
81
|
expect(t).is.instanceof(Template);
|
|
82
82
|
|
|
83
83
|
})
|
|
84
|
+
|
|
85
|
+
it('should resolve from a text node context issue #567', function () {
|
|
86
|
+
const container = document.createElement('div');
|
|
87
|
+
const text = document.createTextNode('context');
|
|
88
|
+
container.appendChild(text);
|
|
89
|
+
document.body.appendChild(container);
|
|
90
|
+
|
|
91
|
+
const template = findDocumentTemplate('mytemplate', text);
|
|
92
|
+
expect(template).to.be.instanceof(Template);
|
|
93
|
+
container.remove();
|
|
94
|
+
});
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
describe('Thmened Template', function () {
|
|
@@ -1665,6 +1665,78 @@ describe("DOM", function () {
|
|
|
1665
1665
|
.catch((e) => done(new Error(e)));
|
|
1666
1666
|
});
|
|
1667
1667
|
|
|
1668
|
+
it("should not move unchanged keyed nodes issue #564", async function () {
|
|
1669
|
+
const element = document.getElementById("test-patch");
|
|
1670
|
+
const a = document.createElement("span");
|
|
1671
|
+
const b = document.createElement("span");
|
|
1672
|
+
a.dataset.key = "a";
|
|
1673
|
+
b.dataset.key = "b";
|
|
1674
|
+
|
|
1675
|
+
const updater = new Updater(element, {
|
|
1676
|
+
text: "",
|
|
1677
|
+
contentNode: document.createElement("div"),
|
|
1678
|
+
fragmentNode: document.createDocumentFragment(),
|
|
1679
|
+
itemsArray: [],
|
|
1680
|
+
keyedItems: [a, b],
|
|
1681
|
+
keyedDataItems: [],
|
|
1682
|
+
items: [],
|
|
1683
|
+
});
|
|
1684
|
+
updater.setCallback("getPatchKey", (value) => value?.dataset?.key);
|
|
1685
|
+
await updater.run();
|
|
1686
|
+
|
|
1687
|
+
const target = document.getElementById("patch-keyed");
|
|
1688
|
+
let moves = 0;
|
|
1689
|
+
const appendChild = target.appendChild;
|
|
1690
|
+
const insertBefore = target.insertBefore;
|
|
1691
|
+
target.appendChild = function (...args) {
|
|
1692
|
+
moves++;
|
|
1693
|
+
return appendChild.apply(this, args);
|
|
1694
|
+
};
|
|
1695
|
+
target.insertBefore = function (...args) {
|
|
1696
|
+
moves++;
|
|
1697
|
+
return insertBefore.apply(this, args);
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
updater.getSubject().keyedItems = [a, b];
|
|
1701
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1702
|
+
|
|
1703
|
+
expect(moves).to.equal(0);
|
|
1704
|
+
expect(Array.from(target.children)).to.deep.equal([a, b]);
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
it("should dispose a keyed node replaced under the same key issue #564", async function () {
|
|
1708
|
+
const element = document.getElementById("test-patch");
|
|
1709
|
+
const first = document.createElement("span");
|
|
1710
|
+
const replacement = document.createElement("span");
|
|
1711
|
+
first.dataset.key = "same";
|
|
1712
|
+
replacement.dataset.key = "same";
|
|
1713
|
+
let disposeCount = 0;
|
|
1714
|
+
addToObjectLink(
|
|
1715
|
+
first,
|
|
1716
|
+
customElementUpdaterLinkSymbol,
|
|
1717
|
+
new Set([{dispose() { disposeCount++; }}]),
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
const updater = new Updater(element, {
|
|
1721
|
+
text: "",
|
|
1722
|
+
contentNode: document.createElement("div"),
|
|
1723
|
+
fragmentNode: document.createDocumentFragment(),
|
|
1724
|
+
itemsArray: [],
|
|
1725
|
+
keyedItems: [first],
|
|
1726
|
+
keyedDataItems: [],
|
|
1727
|
+
items: [],
|
|
1728
|
+
});
|
|
1729
|
+
updater.setCallback("getPatchKey", (value) => value?.dataset?.key);
|
|
1730
|
+
await updater.run();
|
|
1731
|
+
|
|
1732
|
+
updater.getSubject().keyedItems = [replacement];
|
|
1733
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1734
|
+
|
|
1735
|
+
expect(disposeCount).to.equal(1);
|
|
1736
|
+
expect(first.isConnected).to.equal(false);
|
|
1737
|
+
expect(document.getElementById("patch-keyed").firstChild).to.equal(replacement);
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1668
1740
|
it("should remove stale keyed nodes and keep surviving node identity", function (done) {
|
|
1669
1741
|
let element = document.getElementById("test-patch");
|
|
1670
1742
|
const a = document.createElement("span");
|
|
@@ -2620,6 +2692,52 @@ describe("DOM", function () {
|
|
|
2620
2692
|
});
|
|
2621
2693
|
});
|
|
2622
2694
|
|
|
2695
|
+
describe("Updater binding correctness issue #563", function () {
|
|
2696
|
+
it("should ignore unchecked radios during retrieve", function () {
|
|
2697
|
+
const mocks = document.getElementById("mocks");
|
|
2698
|
+
mocks.innerHTML = `
|
|
2699
|
+
<input type="radio" value="selected" checked data-monster-bind="path:choice">
|
|
2700
|
+
<input type="radio" value="unchecked" data-monster-bind="path:choice">
|
|
2701
|
+
`;
|
|
2702
|
+
|
|
2703
|
+
const updater = new Updater(mocks, {choice: "initial"});
|
|
2704
|
+
updater.retrieve();
|
|
2705
|
+
|
|
2706
|
+
expect(updater.getSubject().choice).to.equal("selected");
|
|
2707
|
+
});
|
|
2708
|
+
|
|
2709
|
+
it("should not change select state for non-value attributes", async function () {
|
|
2710
|
+
const mocks = document.getElementById("mocks");
|
|
2711
|
+
mocks.innerHTML = `
|
|
2712
|
+
<select id="binding-select" data-monster-attributes="title path:title">
|
|
2713
|
+
<option value="a">a</option>
|
|
2714
|
+
<option value="b" selected>b</option>
|
|
2715
|
+
</select>
|
|
2716
|
+
`;
|
|
2717
|
+
|
|
2718
|
+
const updater = new Updater(mocks, {title: "label"});
|
|
2719
|
+
await updater.run();
|
|
2720
|
+
|
|
2721
|
+
const select = document.getElementById("binding-select");
|
|
2722
|
+
expect(select.title).to.equal("label");
|
|
2723
|
+
expect(select.value).to.equal("b");
|
|
2724
|
+
});
|
|
2725
|
+
|
|
2726
|
+
it("should evaluate static attribute and property commands", async function () {
|
|
2727
|
+
const mocks = document.getElementById("mocks");
|
|
2728
|
+
mocks.innerHTML = `
|
|
2729
|
+
<div id="static-attribute" data-monster-attributes="title static:hello"></div>
|
|
2730
|
+
<input id="static-property" data-monster-properties="value static:world">
|
|
2731
|
+
`;
|
|
2732
|
+
|
|
2733
|
+
const updater = new Updater(mocks, {trigger: true});
|
|
2734
|
+
await updater.run();
|
|
2735
|
+
|
|
2736
|
+
expect(document.getElementById("static-attribute").title).to.equal("hello");
|
|
2737
|
+
expect(document.getElementById("static-property").value).to.equal("world");
|
|
2738
|
+
});
|
|
2739
|
+
});
|
|
2740
|
+
|
|
2623
2741
|
describe("Updater pipe failure recovery issue #498", function () {
|
|
2624
2742
|
const waitForUpdate = () => new Promise((resolve) => setTimeout(resolve, 20));
|
|
2625
2743
|
|
|
@@ -241,6 +241,40 @@ describe('ProxyObserver', function () {
|
|
|
241
241
|
proxy.attachObserver(observer);
|
|
242
242
|
delete proxy.getSubject().a;
|
|
243
243
|
});
|
|
244
|
+
|
|
245
|
+
it('should ignore missing and inherited deletes without notifying issue #570', async function () {
|
|
246
|
+
const prototype = {inherited: true};
|
|
247
|
+
const value = Object.create(prototype);
|
|
248
|
+
const proxy = new ProxyObserver(value);
|
|
249
|
+
let notifications = 0;
|
|
250
|
+
proxy.attachObserver(new Observer(function () {
|
|
251
|
+
notifications++;
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
expect(() => delete proxy.getSubject().missing).to.not.throw();
|
|
255
|
+
expect(() => delete proxy.getSubject().inherited).to.not.throw();
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
257
|
+
|
|
258
|
+
expect(proxy.getSubject().inherited).to.equal(true);
|
|
259
|
+
expect(notifications).to.equal(0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should report failed non-configurable deletes without notifying issue #570', async function () {
|
|
263
|
+
const value = {};
|
|
264
|
+
Object.defineProperty(value, 'fixed', {
|
|
265
|
+
configurable: false,
|
|
266
|
+
value: true,
|
|
267
|
+
});
|
|
268
|
+
const proxy = new ProxyObserver(value);
|
|
269
|
+
let notifications = 0;
|
|
270
|
+
proxy.attachObserver(new Observer(function () {
|
|
271
|
+
notifications++;
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
expect(Reflect.deleteProperty(proxy.getSubject(), 'fixed')).to.equal(false);
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
276
|
+
expect(notifications).to.equal(0);
|
|
277
|
+
});
|
|
244
278
|
});
|
|
245
279
|
|
|
246
280
|
describe('symbol properties', function () {
|
|
@@ -342,6 +376,47 @@ describe('ProxyObserver', function () {
|
|
|
342
376
|
expect(proxy.getSubject().map).to.equal(replacement);
|
|
343
377
|
expect(notifications).to.equal(1);
|
|
344
378
|
});
|
|
379
|
+
|
|
380
|
+
it('should obey frozen-property proxy invariants issue #570', function () {
|
|
381
|
+
const nested = {value: true};
|
|
382
|
+
const frozen = Object.freeze({nested});
|
|
383
|
+
const subject = new ProxyObserver({frozen}).getSubject();
|
|
384
|
+
|
|
385
|
+
expect(() => subject.frozen.nested).to.not.throw();
|
|
386
|
+
expect(subject.frozen.nested).to.equal(nested);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should preserve confirmed internal-slot native objects issue #570', async function () {
|
|
390
|
+
const weakKey = {};
|
|
391
|
+
const regexp = /monster/g;
|
|
392
|
+
const buffer = new ArrayBuffer(8);
|
|
393
|
+
const view = new DataView(buffer);
|
|
394
|
+
const typed = new Uint8Array(buffer);
|
|
395
|
+
const weakMap = new WeakMap([[weakKey, 'value']]);
|
|
396
|
+
const weakSet = new WeakSet([weakKey]);
|
|
397
|
+
const promise = Promise.resolve('resolved');
|
|
398
|
+
const subject = new ProxyObserver({
|
|
399
|
+
regexp,
|
|
400
|
+
buffer,
|
|
401
|
+
view,
|
|
402
|
+
typed,
|
|
403
|
+
weakMap,
|
|
404
|
+
weakSet,
|
|
405
|
+
promise,
|
|
406
|
+
}).getSubject();
|
|
407
|
+
|
|
408
|
+
expect(subject.regexp).to.equal(regexp);
|
|
409
|
+
expect(subject.regexp.test('monster')).to.equal(true);
|
|
410
|
+
expect(subject.buffer).to.equal(buffer);
|
|
411
|
+
expect(subject.buffer.byteLength).to.equal(8);
|
|
412
|
+
expect(subject.view).to.equal(view);
|
|
413
|
+
expect(subject.view.byteLength).to.equal(8);
|
|
414
|
+
expect(subject.typed).to.equal(typed);
|
|
415
|
+
expect(subject.typed.length).to.equal(8);
|
|
416
|
+
expect(subject.weakMap.get(weakKey)).to.equal('value');
|
|
417
|
+
expect(subject.weakSet.has(weakKey)).to.equal(true);
|
|
418
|
+
expect(await subject.promise).to.equal('resolved');
|
|
419
|
+
});
|
|
345
420
|
});
|
|
346
421
|
|
|
347
422
|
})
|