@schukai/monster 4.152.0 → 4.153.0
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/dom/resource.mjs +19 -5
- package/source/dom/sanitize-html.mjs +34 -29
- 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/transformer.mjs +7 -0
- package/test/cases/dom/customcontrol.mjs +2 -6
- 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/updater.mjs +1 -1
- 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.0"}
|
|
@@ -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/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
|
|
|
@@ -16,39 +16,44 @@ export function sanitizeHtml(htmlString, options = {}) {
|
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
const blockedTags = options.blockedTags || [];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
const animationTags = new Set([
|
|
20
|
+
"animate",
|
|
21
|
+
"set",
|
|
22
|
+
"animatemotion",
|
|
23
|
+
"animatetransform",
|
|
24
|
+
"animatecolor",
|
|
25
|
+
"discard",
|
|
26
|
+
]);
|
|
27
|
+
const urlAttrs = new Set(["href", "xlink:href", "action", "formaction"]);
|
|
28
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: URL schemes must be checked after removing control characters.
|
|
29
|
+
const urlControlCharacters = /[\u0000-\u0020\u007f-\u009f]/g;
|
|
30
|
+
|
|
31
|
+
function sanitizeTree(root) {
|
|
32
|
+
for (const tag of blockedTags) {
|
|
33
|
+
for (const el of root.querySelectorAll(tag)) el.remove();
|
|
34
|
+
}
|
|
35
|
+
for (const el of root.querySelectorAll("*")) {
|
|
36
|
+
// SVG animations can change a safe URL after the attribute check.
|
|
37
|
+
if (animationTags.has(el.localName.toLowerCase())) {
|
|
38
|
+
el.remove();
|
|
39
|
+
continue;
|
|
37
40
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (urlAttrs.includes(name)) {
|
|
41
|
-
const val = value.trim().toLowerCase();
|
|
42
|
-
if (
|
|
43
|
-
val.startsWith("javascript:") ||
|
|
44
|
-
val.startsWith("data:") ||
|
|
45
|
-
val.startsWith("vbscript:")
|
|
46
|
-
) {
|
|
41
|
+
for (const { name, value } of [...el.attributes]) {
|
|
42
|
+
if (/^on/i.test(name)) {
|
|
47
43
|
el.removeAttribute(name);
|
|
44
|
+
} else if (urlAttrs.has(name.toLowerCase())) {
|
|
45
|
+
const normalized = value.replace(urlControlCharacters, "").toLowerCase();
|
|
46
|
+
if (/^(javascript|data|vbscript):/.test(normalized)) {
|
|
47
|
+
el.removeAttribute(name);
|
|
48
|
+
}
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if (el.localName === "template" && el.content) {
|
|
52
|
+
sanitizeTree(el.content);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
sanitizeTree(doc);
|
|
52
57
|
|
|
53
58
|
return doc.body.innerHTML;
|
|
54
59
|
}
|
|
@@ -6,6 +6,21 @@ import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
|
|
|
6
6
|
const expect = chai.expect;
|
|
7
7
|
chai.use(chaiDom);
|
|
8
8
|
|
|
9
|
+
// Supply geometry before the first layout can populate the measurement cache.
|
|
10
|
+
function installColumnBarGeometry(parentWidth) {
|
|
11
|
+
const original = HTMLElement.prototype.getBoundingClientRect;
|
|
12
|
+
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
13
|
+
if (this.localName === "monster-column-bar") return {width:0};
|
|
14
|
+
if (this.getRootNode().host?.localName === "monster-column-bar") {
|
|
15
|
+
const role = this.getAttribute("data-monster-role");
|
|
16
|
+
if (role === "control") return {width:parentWidth()};
|
|
17
|
+
if (role === "settings-button" || this.localName === "li") return {width:20};
|
|
18
|
+
}
|
|
19
|
+
return original.call(this);
|
|
20
|
+
};
|
|
21
|
+
return () => { HTMLElement.prototype.getBoundingClientRect = original; };
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
function dispatchPointerEvent(target, type, options = {}) {
|
|
10
25
|
const event = new Event(type, {
|
|
11
26
|
bubbles: true,
|
|
@@ -35,6 +50,9 @@ function dispatchPointerEvent(target, type, options = {}) {
|
|
|
35
50
|
|
|
36
51
|
function mockScrollableElement(element, { clientWidth, scrollWidth, scrollLeft = 0 }) {
|
|
37
52
|
let currentScrollLeft = scrollLeft;
|
|
53
|
+
// Synthetic events have no browser-owned active pointer to capture.
|
|
54
|
+
element.setPointerCapture = () => {};
|
|
55
|
+
element.releasePointerCapture = () => {};
|
|
38
56
|
|
|
39
57
|
Object.defineProperty(element, "clientWidth", {
|
|
40
58
|
configurable: true,
|
|
@@ -447,6 +465,7 @@ describe("Datatable drag scroll", function () {
|
|
|
447
465
|
});
|
|
448
466
|
|
|
449
467
|
it("defers column bar resize updates to the next animation frame", async function () {
|
|
468
|
+
let restoreGeometry = () => {};
|
|
450
469
|
const OriginalResizeObserver = window.ResizeObserver;
|
|
451
470
|
const originalGlobalResizeObserver = globalThis.ResizeObserver;
|
|
452
471
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
@@ -468,6 +487,7 @@ describe("Datatable drag scroll", function () {
|
|
|
468
487
|
const mocks = document.getElementById("mocks");
|
|
469
488
|
const wrapper = document.createElement("div");
|
|
470
489
|
let parentWidth = 400;
|
|
490
|
+
restoreGeometry = installColumnBarGeometry(() => parentWidth);
|
|
471
491
|
|
|
472
492
|
Object.defineProperty(wrapper, "getBoundingClientRect", {
|
|
473
493
|
configurable: true,
|
|
@@ -499,6 +519,7 @@ describe("Datatable drag scroll", function () {
|
|
|
499
519
|
"[data-monster-role=dots]",
|
|
500
520
|
);
|
|
501
521
|
const dots = Array.from(dotsContainer.querySelectorAll("li"));
|
|
522
|
+
dots.forEach(dot => { dot.style.margin = "0"; });
|
|
502
523
|
|
|
503
524
|
expect(control).to.exist;
|
|
504
525
|
expect(settingsButton).to.exist;
|
|
@@ -519,7 +540,7 @@ describe("Datatable drag scroll", function () {
|
|
|
519
540
|
});
|
|
520
541
|
});
|
|
521
542
|
|
|
522
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
543
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
523
544
|
|
|
524
545
|
expect(
|
|
525
546
|
dotsContainer.querySelector(".dots-overflow-indicator"),
|
|
@@ -551,6 +572,7 @@ describe("Datatable drag scroll", function () {
|
|
|
551
572
|
dotsContainer.querySelector(".dots-overflow-indicator"),
|
|
552
573
|
).to.exist;
|
|
553
574
|
} finally {
|
|
575
|
+
restoreGeometry();
|
|
554
576
|
window.ResizeObserver = OriginalResizeObserver;
|
|
555
577
|
globalThis.ResizeObserver = originalGlobalResizeObserver;
|
|
556
578
|
window.requestAnimationFrame = originalRequestAnimationFrame;
|
|
@@ -559,6 +581,7 @@ describe("Datatable drag scroll", function () {
|
|
|
559
581
|
});
|
|
560
582
|
|
|
561
583
|
it("coalesces column bar dot updates and ignores overflow indicator mutations", async function () {
|
|
584
|
+
let restoreGeometry = () => {};
|
|
562
585
|
const OriginalResizeObserver = window.ResizeObserver;
|
|
563
586
|
const originalGlobalResizeObserver = globalThis.ResizeObserver;
|
|
564
587
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
@@ -580,6 +603,7 @@ describe("Datatable drag scroll", function () {
|
|
|
580
603
|
const mocks = document.getElementById("mocks");
|
|
581
604
|
const wrapper = document.createElement("div");
|
|
582
605
|
let parentWidth = 80;
|
|
606
|
+
restoreGeometry = installColumnBarGeometry(() => parentWidth);
|
|
583
607
|
|
|
584
608
|
Object.defineProperty(wrapper, "getBoundingClientRect", {
|
|
585
609
|
configurable: true,
|
|
@@ -612,6 +636,7 @@ describe("Datatable drag scroll", function () {
|
|
|
612
636
|
"[data-monster-role=dots]",
|
|
613
637
|
);
|
|
614
638
|
const dots = Array.from(dotsContainer.querySelectorAll("li"));
|
|
639
|
+
dots.forEach(dot => { dot.style.margin = "0"; });
|
|
615
640
|
|
|
616
641
|
Object.defineProperty(control, "getBoundingClientRect", {
|
|
617
642
|
configurable: true,
|
|
@@ -628,6 +653,7 @@ describe("Datatable drag scroll", function () {
|
|
|
628
653
|
});
|
|
629
654
|
});
|
|
630
655
|
|
|
656
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
631
657
|
return { columnBar, control, dotsContainer };
|
|
632
658
|
};
|
|
633
659
|
|
|
@@ -669,6 +695,7 @@ describe("Datatable drag scroll", function () {
|
|
|
669
695
|
|
|
670
696
|
expect(scheduledCallbacks.length).to.equal(0);
|
|
671
697
|
} finally {
|
|
698
|
+
restoreGeometry();
|
|
672
699
|
window.ResizeObserver = OriginalResizeObserver;
|
|
673
700
|
globalThis.ResizeObserver = originalGlobalResizeObserver;
|
|
674
701
|
window.requestAnimationFrame = originalRequestAnimationFrame;
|
|
@@ -104,6 +104,9 @@ describe("ButtonBar", function () {
|
|
|
104
104
|
"Weitere Aktionen",
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
+
// The empty fixture has no overflow; expose the switch for the focus check.
|
|
108
|
+
switchButton.hidden = false;
|
|
109
|
+
switchButton.classList.remove("hidden");
|
|
107
110
|
bar.showDialog();
|
|
108
111
|
expect(switchButton.getAttribute("aria-expanded")).to.equal("true");
|
|
109
112
|
switchButton.dispatchEvent(
|
|
@@ -403,7 +406,7 @@ describe("ButtonBar", function () {
|
|
|
403
406
|
it("should allow themes to space main and overflow button groups", function () {
|
|
404
407
|
const cssText = ButtonBar.getCSSStyleSheet()
|
|
405
408
|
.flatMap((styleSheet) => Array.from(styleSheet.cssRules))
|
|
406
|
-
.map((rule) => rule.cssText)
|
|
409
|
+
.map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
|
|
407
410
|
.join("\n");
|
|
408
411
|
|
|
409
412
|
expect(cssText).to.contain("slot[name=popper]");
|
|
@@ -424,7 +427,7 @@ describe("ButtonBar", function () {
|
|
|
424
427
|
it("should stack a nested button bar inside a parent overflow item", function () {
|
|
425
428
|
const cssText = ButtonBar.getCSSStyleSheet()
|
|
426
429
|
.flatMap((styleSheet) => Array.from(styleSheet.cssRules))
|
|
427
|
-
.map((rule) => rule.cssText)
|
|
430
|
+
.map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
|
|
428
431
|
.join("\n");
|
|
429
432
|
|
|
430
433
|
expect(cssText).to.match(
|
|
@@ -517,11 +520,11 @@ describe("ButtonBar", function () {
|
|
|
517
520
|
|
|
518
521
|
expect(buttons[1].style.marginLeft).to.equal("");
|
|
519
522
|
expect(buttons[0].style.borderRadius).to.equal("8px");
|
|
520
|
-
expect(buttons[0].style.borderTopRightRadius).to.equal("");
|
|
521
|
-
expect(buttons[0].style.borderBottomRightRadius).to.equal("");
|
|
523
|
+
expect(buttons[0].style.borderTopRightRadius || buttons[0].style.borderRadius).to.equal("8px");
|
|
524
|
+
expect(buttons[0].style.borderBottomRightRadius || buttons[0].style.borderRadius).to.equal("8px");
|
|
522
525
|
expect(buttons[1].style.borderRadius).to.equal("8px");
|
|
523
|
-
expect(buttons[1].style.borderTopLeftRadius).to.equal("");
|
|
524
|
-
expect(buttons[1].style.borderBottomLeftRadius).to.equal("");
|
|
526
|
+
expect(buttons[1].style.borderTopLeftRadius || buttons[1].style.borderRadius).to.equal("8px");
|
|
527
|
+
expect(buttons[1].style.borderBottomLeftRadius || buttons[1].style.borderRadius).to.equal("8px");
|
|
525
528
|
} finally {
|
|
526
529
|
window.requestAnimationFrame = originalRequestAnimationFrame;
|
|
527
530
|
globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
|
|
@@ -150,7 +150,7 @@ describe('ConfirmButton', function () {
|
|
|
150
150
|
|
|
151
151
|
const cssText = ConfirmButton.getCSSStyleSheet()
|
|
152
152
|
.flatMap((styleSheet) => Array.from(styleSheet.cssRules))
|
|
153
|
-
.map((rule) => rule.cssText)
|
|
153
|
+
.map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
|
|
154
154
|
.join("\n");
|
|
155
155
|
|
|
156
156
|
expect(cssText).to.contain('div[data-monster-role=decision]');
|
|
@@ -162,14 +162,14 @@ describe('ConfirmButton', function () {
|
|
|
162
162
|
|
|
163
163
|
const cssText = ConfirmButton.getCSSStyleSheet()
|
|
164
164
|
.flatMap((styleSheet) => Array.from(styleSheet.cssRules))
|
|
165
|
-
.map((rule) => rule.cssText)
|
|
165
|
+
.map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
|
|
166
166
|
.join("\n");
|
|
167
167
|
|
|
168
168
|
expect(cssText).to.contain('div[data-monster-role=decision]');
|
|
169
169
|
expect(cssText).to.contain('max-width: 100%');
|
|
170
170
|
expect(cssText).to.contain('min-width: 0px');
|
|
171
171
|
expect(cssText).to.contain('flex-wrap: wrap');
|
|
172
|
-
expect(cssText).to.contain('div[data-monster-role=decision]>*');
|
|
172
|
+
expect(cssText.replace(/\s*>\s*/g, '>')).to.contain('div[data-monster-role=decision]>*');
|
|
173
173
|
expect(cssText).to.contain('flex: 1 1 max-content');
|
|
174
174
|
|
|
175
175
|
});
|
|
@@ -178,7 +178,7 @@ describe('ConfirmButton', function () {
|
|
|
178
178
|
|
|
179
179
|
const cssText = ConfirmButton.getCSSStyleSheet()
|
|
180
180
|
.flatMap((styleSheet) => Array.from(styleSheet.cssRules))
|
|
181
|
-
.map((rule) => rule.cssText)
|
|
181
|
+
.map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
|
|
182
182
|
.join("\n");
|
|
183
183
|
|
|
184
184
|
expect(cssText).to.contain(':host(monster-confirm-button)');
|