@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.
- package/README.md +24 -1
- package/package.json +1 -1
- package/source/components/content/viewer/message.mjs +1 -0
- package/source/components/form/select.mjs +2 -0
- package/source/components/form/util/fetch.mjs +6 -7
- package/source/components/state/log.mjs +7 -0
- package/source/components/state/thread.mjs +11 -0
- package/source/components/time/month-calendar.mjs +80 -30
- 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/resource.mjs +19 -5
- package/source/dom/sanitize-html.mjs +34 -29
- 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/components/datatable/drag-scroll.mjs +28 -1
- package/test/cases/components/form/button-bar.mjs +9 -6
- package/test/cases/components/form/confirm-button.mjs +4 -4
- package/test/cases/components/form/control-bar.mjs +73 -72
- package/test/cases/components/form/input-group.mjs +3 -1
- package/test/cases/components/form/password.mjs +2 -2
- package/test/cases/components/form/popper-button.mjs +2 -2
- package/test/cases/components/form/select-sort-callback.mjs +57 -0
- package/test/cases/components/form/select.mjs +2 -2
- package/test/cases/components/form/sheet.mjs +13 -1
- package/test/cases/components/layout/popper.mjs +3 -1
- package/test/cases/components/layout/tabs.mjs +4 -1
- package/test/cases/components/navigation/site-navigation.mjs +12 -26
- package/test/cases/components/navigation/table-of-content.mjs +3 -0
- package/test/cases/components/state/ticker-lifecycle.mjs +47 -0
- package/test/cases/components/time/month-calendar.mjs +100 -0
- 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 +33 -0
- package/test/cases/dom/customcontrol.mjs +87 -6
- package/test/cases/dom/customelement.mjs +54 -0
- package/test/cases/dom/ready.mjs +3 -3
- package/test/cases/dom/resource-availability.mjs +46 -0
- package/test/cases/dom/resourcemanager.mjs +12 -6
- package/test/cases/dom/sanitize-html.mjs +71 -0
- package/test/cases/dom/slotted-nodes.mjs +48 -1
- package/test/cases/dom/template.mjs +11 -0
- package/test/cases/dom/updater.mjs +119 -1
- package/test/cases/types/proxyobserver.mjs +75 -0
- 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.
|
|
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"}
|
|
@@ -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
|
|
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
|
-
|
|
86
|
-
this.
|
|
87
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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,
|
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/resource.mjs
CHANGED
|
@@ -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
|
|
183
|
-
|
|
184
|
-
self[internalStateSymbol].
|
|
185
|
-
|
|
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
|
|