buninu-browser 0.2.1 → 0.2.3
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/AGENTS.md +4 -0
- package/README.md +74 -9
- package/ai-debug.log +5537 -0
- package/bunfig.toml +1 -0
- package/buninu-browser.js +4 -0
- package/index.js +5 -0
- package/lib/audio/piano.js +24 -1
- package/lib/audio/playback-buffer.js +27 -0
- package/lib/audio/pulse-client.js +137 -19
- package/lib/cdp-server.js +22 -11
- package/lib/happy-dom/parser.js +29 -4
- package/lib/happy-dom/selector-pseudo-adapter.js +14 -0
- package/lib/headless-shell.js +69 -19
- package/lib/input/hit-test.js +25 -7
- package/lib/layout/text-layout.js +268 -85
- package/lib/log.js +5 -2
- package/lib/media/byte-pipe.js +82 -0
- package/lib/media/position-audio.js +42 -0
- package/lib/media/video-keyframe.js +29 -0
- package/lib/network/network-service.js +161 -14
- package/lib/paint/display-list.js +316 -15
- package/lib/paint/gradient.js +11 -5
- package/lib/render-tree/index.js +19 -2
- package/lib/render-tree/svg.js +2 -1
- package/lib/renderer/bindings.js +995 -69
- package/lib/renderer/canvas-2d.js +92 -0
- package/lib/renderer/compositor.js +16 -1
- package/lib/renderer/custom-element-reactions.js +18 -0
- package/lib/renderer/history.js +42 -0
- package/lib/renderer/host.js +166 -9
- package/lib/renderer/image-loading.js +27 -0
- package/lib/renderer/interfaces.js +215 -0
- package/lib/renderer/keyboard-input.js +12 -0
- package/lib/renderer/main.js +162 -12
- package/lib/renderer/media-playback-events.js +13 -0
- package/lib/renderer/media-source.js +819 -0
- package/lib/renderer/messaging.js +411 -0
- package/lib/renderer/native-functions.js +113 -0
- package/lib/renderer/page-realm.js +1408 -242
- package/lib/renderer/page-renderer.js +1893 -223
- package/lib/renderer/piano-input.js +22 -0
- package/lib/renderer/pointer-move.js +29 -0
- package/lib/renderer/rejections.js +31 -0
- package/lib/renderer/rewrite-worker.js +22 -0
- package/lib/renderer/script-rewrite.js +805 -57
- package/lib/renderer/trusted-types.js +129 -0
- package/lib/renderer/video-presentation.js +126 -0
- package/lib/renderer/workers.js +327 -0
- package/lib/screenshot-cli.js +2 -0
- package/lib/style/computed-style.js +186 -29
- package/lib/user-agent.js +14 -2
- package/package.json +4 -1
- package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
- package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
- package/patches/happy-dom/README.md +26 -0
- package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
- package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
- package/patches/ses/README.md +4 -0
- package/test/canvas-2d.test.js +55 -0
- package/test/compositor.test.js +41 -1
- package/test/computed-style.test.js +59 -1
- package/test/display-list.test.js +88 -1
- package/test/download.test.js +43 -0
- package/test/fixtures/long-video.mp4 +0 -0
- package/test/fixtures/media-sync.js +57 -0
- package/test/happy-dom-platform-adapters.test.js +25 -0
- package/test/headless-shell.test.js +23 -1
- package/test/hit-test.test.js +25 -1
- package/test/media-playback-events.test.js +22 -0
- package/test/media-source.test.js +156 -0
- package/test/media-sync.test.js +12 -0
- package/test/mobile.test.js +2 -2
- package/test/navigation.test.js +61 -0
- package/test/network-service.test.js +14 -0
- package/test/page-scripts.test.js +1203 -7
- package/test/piano-input.test.js +44 -0
- package/test/playback-buffer.test.js +31 -0
- package/test/position-audio.test.js +67 -0
- package/test/positioning.test.js +95 -1
- package/test/pulse-client.test.js +84 -0
- package/test/renderer-host.test.js +5 -2
- package/test/screenshot-cli.test.js +25 -1
- package/test/script-rewrite.test.js +111 -6
- package/test/setup.js +5 -0
- package/test/text-layout.test.js +62 -0
- package/test/user-agent.test.js +60 -4
- package/test/video-presentation.test.js +67 -0
- package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
- package/vendor/ses/dist/ses.mjs +52 -6
package/lib/renderer/bindings.js
CHANGED
|
@@ -16,6 +16,7 @@ const REFLECTED_PROPERTIES = [
|
|
|
16
16
|
"value", "checked", "disabled", "type", "name", "href", "src", "alt", "title",
|
|
17
17
|
"placeholder", "selected", "readOnly", "required", "min", "max", "step", "rel", "target",
|
|
18
18
|
"htmlFor", "lang", "dir", "tabIndex", "hidden", "defaultValue", "action", "method",
|
|
19
|
+
"controls", "autoplay", "loop", "playsInline", "preload", "poster",
|
|
19
20
|
];
|
|
20
21
|
|
|
21
22
|
/** Per-document state shared by that document's wrappers. */
|
|
@@ -23,13 +24,21 @@ export class BindingRealm {
|
|
|
23
24
|
#wrappers = new WeakMap();
|
|
24
25
|
#listeners = new WeakMap();
|
|
25
26
|
#events = new WeakMap();
|
|
27
|
+
#trustedEvents = new WeakSet();
|
|
28
|
+
// MessageEvent fields (data, origin, lastEventId, source, ports) by engine
|
|
29
|
+
// event: page values stay on this side, as CustomEvent detail does.
|
|
30
|
+
#messages = new WeakMap();
|
|
26
31
|
#liveViews = new WeakMap();
|
|
32
|
+
#ranges = new WeakMap();
|
|
27
33
|
#hooks;
|
|
28
34
|
#window;
|
|
29
35
|
eventPrototype = null;
|
|
30
36
|
elementPrototype = null;
|
|
37
|
+
/** @type {((node: object) => object) | null} the prototype for an element's wrapper */
|
|
38
|
+
elementPrototypeOf = null;
|
|
31
39
|
fragmentPrototype = null;
|
|
32
40
|
shadowRootPrototype = null;
|
|
41
|
+
rangePrototype = null;
|
|
33
42
|
textPrototype = null;
|
|
34
43
|
characterDataPrototype = null;
|
|
35
44
|
nodePrototype = null;
|
|
@@ -71,7 +80,9 @@ export class BindingRealm {
|
|
|
71
80
|
: node.nodeType === 8 ? CharacterDataBinding
|
|
72
81
|
: NodeBinding;
|
|
73
82
|
wrapper = new Binding(CONSTRUCT, node, this);
|
|
74
|
-
if (Binding === ElementBinding && this.elementPrototype)
|
|
83
|
+
if (Binding === ElementBinding && this.elementPrototype) {
|
|
84
|
+
Object.setPrototypeOf(wrapper, this.elementPrototypeOf?.(node) ?? this.elementPrototype);
|
|
85
|
+
}
|
|
75
86
|
if (Binding === FragmentBinding && this.fragmentPrototype) Object.setPrototypeOf(wrapper, this.fragmentPrototype);
|
|
76
87
|
if (Binding === ShadowRootBinding && this.shadowRootPrototype) Object.setPrototypeOf(wrapper, this.shadowRootPrototype);
|
|
77
88
|
if (Binding === TextBinding && this.textPrototype) Object.setPrototypeOf(wrapper, this.textPrototype);
|
|
@@ -107,10 +118,15 @@ export class BindingRealm {
|
|
|
107
118
|
return null;
|
|
108
119
|
},
|
|
109
120
|
};
|
|
121
|
+
// Indices and length read through; other properties a page sets are
|
|
122
|
+
// expandos kept on the target (they shadow item/namedItem, as an own
|
|
123
|
+
// property on a browser's collection does).
|
|
124
|
+
const expando = (target, property) => property !== "length" && !isIndex(property) && Object.hasOwn(target, property);
|
|
110
125
|
view = new Proxy([], {
|
|
111
126
|
get(target, property, receiver) {
|
|
112
127
|
if (property === "length") return collection.length;
|
|
113
128
|
if (isIndex(property)) return at(Number(property));
|
|
129
|
+
if (expando(target, property)) return target[property];
|
|
114
130
|
if (Object.hasOwn(methods, property)) return methods[property];
|
|
115
131
|
return Reflect.get(target, property, receiver);
|
|
116
132
|
},
|
|
@@ -121,12 +137,23 @@ export class BindingRealm {
|
|
|
121
137
|
if (isIndex(property) && Number(property) < collection.length) {
|
|
122
138
|
return { value: at(Number(property)), writable: false, enumerable: true, configurable: true };
|
|
123
139
|
}
|
|
124
|
-
return undefined;
|
|
140
|
+
return expando(target, property) ? Reflect.getOwnPropertyDescriptor(target, property) : undefined;
|
|
141
|
+
},
|
|
142
|
+
ownKeys: (target) => [...Array.from({ length: collection.length }, (_, index) => String(index)), "length",
|
|
143
|
+
...Reflect.ownKeys(target).filter((key) => expando(target, key))],
|
|
144
|
+
set(target, property, value) {
|
|
145
|
+
if (property === "length" || isIndex(property)) return false;
|
|
146
|
+
if (expando(target, property)) {
|
|
147
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, property);
|
|
148
|
+
if (!("value" in descriptor)) return descriptor.set ? (descriptor.set.call(view, value), true) : false;
|
|
149
|
+
if (!descriptor.writable) return false;
|
|
150
|
+
}
|
|
151
|
+
return Reflect.defineProperty(target, property, expando(target, property)
|
|
152
|
+
? { value } : { value, writable: true, enumerable: true, configurable: true });
|
|
125
153
|
},
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
deleteProperty: () => false,
|
|
154
|
+
defineProperty: (target, property, descriptor) =>
|
|
155
|
+
property !== "length" && !isIndex(property) && Reflect.defineProperty(target, property, descriptor),
|
|
156
|
+
deleteProperty: (target, property) => !isIndex(property) && property !== "length" && Reflect.deleteProperty(target, property),
|
|
130
157
|
});
|
|
131
158
|
this.#liveViews.set(collection, view);
|
|
132
159
|
return view;
|
|
@@ -136,13 +163,59 @@ export class BindingRealm {
|
|
|
136
163
|
return Array.from(nodes ?? [], (node) => this.wrap(node));
|
|
137
164
|
}
|
|
138
165
|
|
|
166
|
+
/** A MessageEvent for the page: an engine event carrying page-side fields. */
|
|
167
|
+
messageEvent(type, { data = null, origin = "", lastEventId = "", source = null, ports = [] } = {}, init = {}) {
|
|
168
|
+
const event = new this.#window.Event(String(type), init);
|
|
169
|
+
this.#messages.set(event, Object.freeze({ data, origin: String(origin), lastEventId: String(lastEventId), source,
|
|
170
|
+
ports: Object.freeze([...ports]) }));
|
|
171
|
+
return this.wrapEvent(event);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The engine event behind a page event, or null. */
|
|
175
|
+
unwrapEvent(value) {
|
|
176
|
+
return EventBinding.unwrap(value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Marks an event created by the browser's input path as trusted. */
|
|
180
|
+
markTrusted(value) {
|
|
181
|
+
const event = EventBinding.unwrap(value) ?? (value && typeof value === "object" ? value : null);
|
|
182
|
+
if (event) this.#trustedEvents.add(event);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
isTrusted(event) {
|
|
186
|
+
return this.#trustedEvents.has(event);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The MessageEvent fields of an engine event, if it is one. */
|
|
190
|
+
messageOf(event) {
|
|
191
|
+
return this.#messages.get(event) ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The page's Range for an engine Range (one wrapper each). */
|
|
195
|
+
wrapRange(range) {
|
|
196
|
+
if (!range) return null;
|
|
197
|
+
let wrapper = this.#ranges.get(range);
|
|
198
|
+
if (!wrapper) {
|
|
199
|
+
wrapper = new RangeBinding(CONSTRUCT, range, this);
|
|
200
|
+
if (this.rangePrototype) Object.setPrototypeOf(wrapper, this.rangePrototype);
|
|
201
|
+
this.#ranges.set(range, wrapper);
|
|
202
|
+
}
|
|
203
|
+
return wrapper;
|
|
204
|
+
}
|
|
205
|
+
|
|
139
206
|
/** One wrapper per event, so listeners see the dispatched object (and its detail). */
|
|
140
207
|
wrapEvent(event, detail = undefined) {
|
|
141
208
|
if (!event) return null;
|
|
142
209
|
let wrapper = this.#events.get(event);
|
|
143
210
|
if (!wrapper) {
|
|
144
211
|
wrapper = new EventBinding(CONSTRUCT, event, this, detail);
|
|
145
|
-
if (this.eventPrototype)
|
|
212
|
+
if (this.eventPrototype) {
|
|
213
|
+
const prototype = this.eventPrototypes?.get(event.constructor?.name) ?? this.eventPrototype;
|
|
214
|
+
Object.setPrototypeOf(wrapper, prototype);
|
|
215
|
+
// isTrusted is [LegacyUnforgeable]: an own accessor of every event.
|
|
216
|
+
const isTrusted = Object.getOwnPropertyDescriptor(EventBinding.prototype, "isTrusted");
|
|
217
|
+
Object.defineProperty(wrapper, "isTrusted", { get: isTrusted.get, enumerable: true, configurable: false });
|
|
218
|
+
}
|
|
146
219
|
this.#events.set(event, wrapper);
|
|
147
220
|
}
|
|
148
221
|
return wrapper;
|
|
@@ -172,8 +245,15 @@ export function mutableBindingPrototype(base, parent = base) {
|
|
|
172
245
|
const descriptor = Object.getOwnPropertyDescriptor(base, key);
|
|
173
246
|
descriptor.configurable = true;
|
|
174
247
|
if ("value" in descriptor) descriptor.writable = true;
|
|
248
|
+
// Web IDL attributes and operations are enumerable (class members are not).
|
|
249
|
+
if (typeof key === "string") descriptor.enumerable = true;
|
|
175
250
|
Object.defineProperty(prototype, key, descriptor);
|
|
176
251
|
}
|
|
252
|
+
// An own, writable constructor (callers set the page's): an inherited one
|
|
253
|
+
// from a hardened class would be read-only, and then a page subclass's
|
|
254
|
+
// `Sub.prototype = Object.create(Base.prototype); Sub.prototype.constructor = Sub`
|
|
255
|
+
// fails (the override mistake).
|
|
256
|
+
Object.defineProperty(prototype, "constructor", { value: base.constructor, writable: true, configurable: true });
|
|
177
257
|
return prototype;
|
|
178
258
|
}
|
|
179
259
|
|
|
@@ -204,6 +284,23 @@ function inserted(wrapper, nodes) {
|
|
|
204
284
|
}
|
|
205
285
|
|
|
206
286
|
/** A fragment empties itself on insertion, so list its children beforehand. */
|
|
287
|
+
const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
|
|
288
|
+
const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
|
|
289
|
+
|
|
290
|
+
function locateNamespace(node, prefix) {
|
|
291
|
+
if (prefix === "xml") return XML_NAMESPACE;
|
|
292
|
+
if (prefix === "xmlns") return XMLNS_NAMESPACE;
|
|
293
|
+
if (node.nodeType === 9) node = node.documentElement;
|
|
294
|
+
else if (node.nodeType !== 1) node = node.parentElement ?? node.ownerElement ?? null;
|
|
295
|
+
for (let element = node; element; element = element.parentElement) {
|
|
296
|
+
if (element.namespaceURI !== null && (element.prefix ?? null) === prefix) return element.namespaceURI;
|
|
297
|
+
const attribute = prefix === null ? element.getAttributeNS?.(XMLNS_NAMESPACE, "xmlns") ?? element.getAttribute("xmlns")
|
|
298
|
+
: element.getAttributeNS?.(XMLNS_NAMESPACE, prefix);
|
|
299
|
+
if (attribute !== null && attribute !== undefined) return attribute || null;
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
207
304
|
function insertionList(nodes) {
|
|
208
305
|
return nodes.flatMap((node) => node.nodeType === 11 ? [...node.childNodes] : [node]);
|
|
209
306
|
}
|
|
@@ -249,6 +346,19 @@ class NodeBinding {
|
|
|
249
346
|
return this.#node.nodeName;
|
|
250
347
|
}
|
|
251
348
|
|
|
349
|
+
/** The document base URL (HTML §2.4.1): the first <base href>, else the document's URL. */
|
|
350
|
+
get baseURI() {
|
|
351
|
+
const document = this.#node.nodeType === 9 ? this.#node : this.#node.ownerDocument;
|
|
352
|
+
const url = document?.URL ?? "about:blank";
|
|
353
|
+
const href = document?.querySelector?.("base[href]")?.getAttribute("href");
|
|
354
|
+
if (href == null) return url;
|
|
355
|
+
try {
|
|
356
|
+
return new URL(href, url).href;
|
|
357
|
+
} catch {
|
|
358
|
+
return url;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
252
362
|
get nodeValue() {
|
|
253
363
|
return this.#node.nodeValue ?? null;
|
|
254
364
|
}
|
|
@@ -314,6 +424,53 @@ class NodeBinding {
|
|
|
314
424
|
return node ? this.#node.contains(node) : false;
|
|
315
425
|
}
|
|
316
426
|
|
|
427
|
+
/** Where `other` is relative to this node, as DOCUMENT_POSITION_* bits (DOM §4.4). */
|
|
428
|
+
compareDocumentPosition(other) {
|
|
429
|
+
const node = nodeOf(other);
|
|
430
|
+
if (!node) throw new TypeError("Failed to execute 'compareDocumentPosition' on 'Node': parameter 1 is not of type 'Node'.");
|
|
431
|
+
return this.#node.compareDocumentPosition(node);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
getRootNode(options = undefined) {
|
|
435
|
+
return this.#realm.wrap(this.#node.getRootNode({ composed: Boolean(options?.composed) }));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
isSameNode(other) {
|
|
439
|
+
return other === this || (nodeOf(other) !== null && nodeOf(other) === this.#node);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
isEqualNode(other) {
|
|
443
|
+
const node = nodeOf(other);
|
|
444
|
+
return node ? this.#node.isEqualNode(node) : false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
normalize() {
|
|
448
|
+
this.#node.normalize();
|
|
449
|
+
this.#realm.hooks.touch(this.#node);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// DOM §4.4 "locate a namespace" and "locate a namespace prefix".
|
|
453
|
+
lookupNamespaceURI(prefix) {
|
|
454
|
+
return locateNamespace(this.#node, prefix === null || prefix === undefined || prefix === "" ? null : String(prefix));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
lookupPrefix(namespace) {
|
|
458
|
+
if (namespace === null || namespace === undefined || namespace === "") return null;
|
|
459
|
+
let element = this.#node.nodeType === 1 ? this.#node
|
|
460
|
+
: this.#node.nodeType === 9 ? this.#node.documentElement : this.#node.parentElement ?? this.#node.ownerElement ?? null;
|
|
461
|
+
for (; element; element = element.parentElement) {
|
|
462
|
+
if (element.namespaceURI === String(namespace) && element.prefix) return element.prefix;
|
|
463
|
+
for (const attribute of element.attributes ?? []) {
|
|
464
|
+
if (attribute.prefix === "xmlns" && attribute.value === String(namespace)) return attribute.localName;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
isDefaultNamespace(namespace) {
|
|
471
|
+
return locateNamespace(this.#node, null) === (namespace === undefined || namespace === "" ? null : namespace);
|
|
472
|
+
}
|
|
473
|
+
|
|
317
474
|
appendChild(child) {
|
|
318
475
|
const node = requireNode(child);
|
|
319
476
|
const list = insertionList([node]);
|
|
@@ -348,11 +505,11 @@ class NodeBinding {
|
|
|
348
505
|
return this.#realm.wrap(this.#node.cloneNode(Boolean(deep)));
|
|
349
506
|
}
|
|
350
507
|
|
|
351
|
-
addEventListener(type, listener, options) {
|
|
508
|
+
addEventListener(type, listener, options = undefined) {
|
|
352
509
|
addListener(this, this.#node, type, listener, options);
|
|
353
510
|
}
|
|
354
511
|
|
|
355
|
-
removeEventListener(type, listener, options) {
|
|
512
|
+
removeEventListener(type, listener, options = undefined) {
|
|
356
513
|
removeListener(this, this.#node, type, listener, options);
|
|
357
514
|
}
|
|
358
515
|
|
|
@@ -370,6 +527,18 @@ function requireNode(value) {
|
|
|
370
527
|
}
|
|
371
528
|
|
|
372
529
|
/** Text and comments; frameworks read comment data (React's hydration markers). */
|
|
530
|
+
// Node constants (DOM §4.4) on instances too, and the one the class lacks.
|
|
531
|
+
for (const [name, value] of Object.entries({
|
|
532
|
+
ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4, PROCESSING_INSTRUCTION_NODE: 7,
|
|
533
|
+
ENTITY_REFERENCE_NODE: 5, ENTITY_NODE: 6, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10,
|
|
534
|
+
DOCUMENT_FRAGMENT_NODE: 11, NOTATION_NODE: 12,
|
|
535
|
+
DOCUMENT_POSITION_DISCONNECTED: 1, DOCUMENT_POSITION_PRECEDING: 2, DOCUMENT_POSITION_FOLLOWING: 4,
|
|
536
|
+
DOCUMENT_POSITION_CONTAINS: 8, DOCUMENT_POSITION_CONTAINED_BY: 16, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 32,
|
|
537
|
+
})) {
|
|
538
|
+
if (!Object.hasOwn(NodeBinding, name)) Object.defineProperty(NodeBinding, name, { value, enumerable: true });
|
|
539
|
+
if (!Object.hasOwn(NodeBinding.prototype, name)) Object.defineProperty(NodeBinding.prototype, name, { value, enumerable: true });
|
|
540
|
+
}
|
|
541
|
+
|
|
373
542
|
class CharacterDataBinding extends NodeBinding {
|
|
374
543
|
get data() {
|
|
375
544
|
return nodeOf(this).data;
|
|
@@ -417,6 +586,10 @@ class ParentBinding extends NodeBinding {
|
|
|
417
586
|
return realmOf(this).live(nodeOf(this).getElementsByTagName(String(name)));
|
|
418
587
|
}
|
|
419
588
|
|
|
589
|
+
getElementsByTagNameNS(namespace, localName) {
|
|
590
|
+
return realmOf(this).live(nodeOf(this).getElementsByTagNameNS(namespace === undefined ? null : namespace, String(localName)));
|
|
591
|
+
}
|
|
592
|
+
|
|
420
593
|
getElementsByClassName(names) {
|
|
421
594
|
return realmOf(this).live(nodeOf(this).getElementsByClassName(String(names)));
|
|
422
595
|
}
|
|
@@ -476,6 +649,7 @@ class ElementBinding extends ParentBinding {
|
|
|
476
649
|
#style = null;
|
|
477
650
|
#classList = null;
|
|
478
651
|
#dataset = null;
|
|
652
|
+
#attributes = null;
|
|
479
653
|
|
|
480
654
|
get tagName() {
|
|
481
655
|
return nodeOf(this).tagName;
|
|
@@ -490,11 +664,39 @@ class ElementBinding extends ParentBinding {
|
|
|
490
664
|
return realmOf(this).wrap(nodeOf(this).shadowRoot);
|
|
491
665
|
}
|
|
492
666
|
|
|
667
|
+
// HTMLIFrameElement: its document's window and, when same-origin, its
|
|
668
|
+
// document (see messaging.js).
|
|
669
|
+
get contentWindow() {
|
|
670
|
+
const node = nodeOf(this);
|
|
671
|
+
return node.tagName === "IFRAME" ? realmOf(this).hooks.contentWindow?.(node) ?? null : undefined;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
get contentDocument() {
|
|
675
|
+
const node = nodeOf(this);
|
|
676
|
+
return node.tagName === "IFRAME" ? realmOf(this).hooks.contentDocument?.(node) ?? null : undefined;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// One property for two interfaces: HTMLTemplateElement.content (read-only,
|
|
680
|
+
// the template's fragment) and HTMLMetaElement.content (the reflected
|
|
681
|
+
// attribute, which scripts set). Other elements have none: an assignment
|
|
682
|
+
// stays on the wrapper as a plain property.
|
|
493
683
|
get content() {
|
|
494
|
-
const
|
|
684
|
+
const node = nodeOf(this);
|
|
685
|
+
if (node.tagName === "META") return node.getAttribute("content") ?? "";
|
|
686
|
+
const content = node.content;
|
|
495
687
|
return content === undefined ? undefined : realmOf(this).wrap(content);
|
|
496
688
|
}
|
|
497
689
|
|
|
690
|
+
set content(value) {
|
|
691
|
+
const node = nodeOf(this);
|
|
692
|
+
if (node.tagName === "META") {
|
|
693
|
+
node.setAttribute("content", String(value));
|
|
694
|
+
touch(this);
|
|
695
|
+
} else if (node.tagName !== "TEMPLATE") {
|
|
696
|
+
Object.defineProperty(this, "content", { value, writable: true, enumerable: true, configurable: true });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
498
700
|
// HTMLMediaElement (video and audio): the subset the renderer plays.
|
|
499
701
|
get paused() {
|
|
500
702
|
return isMedia(this) ? realmOf(this).hooks.mediaState?.(nodeOf(this)).paused ?? true : undefined;
|
|
@@ -528,6 +730,19 @@ class ElementBinding extends ParentBinding {
|
|
|
528
730
|
if (isMedia(this)) realmOf(this).hooks.pauseMedia?.(nodeOf(this));
|
|
529
731
|
}
|
|
530
732
|
|
|
733
|
+
/** Restarts loading (HTML §4.8.11.5): playback stops and starts again from the beginning when played. */
|
|
734
|
+
load() {
|
|
735
|
+
if (!isMedia(this)) return;
|
|
736
|
+
realmOf(this).hooks.pauseMedia?.(nodeOf(this));
|
|
737
|
+
void realmOf(this).hooks.seekMedia?.(nodeOf(this), 0);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
canPlayType(type) {
|
|
741
|
+
if (!isMedia(this)) return undefined;
|
|
742
|
+
// The decoder (FFmpeg) handles the common containers and codecs.
|
|
743
|
+
return /^(video|audio)\/(mp4|webm|ogg|mpeg|wav|x-wav|aac|mp3|flac)\b/i.test(String(type)) ? "maybe" : "";
|
|
744
|
+
}
|
|
745
|
+
|
|
531
746
|
get localName() {
|
|
532
747
|
return nodeOf(this).localName;
|
|
533
748
|
}
|
|
@@ -568,15 +783,57 @@ class ElementBinding extends ParentBinding {
|
|
|
568
783
|
return this.#style;
|
|
569
784
|
}
|
|
570
785
|
|
|
786
|
+
// ElementCSSInlineStyle is [PutForwards=cssText], not assignment-readonly.
|
|
787
|
+
set style(value) {
|
|
788
|
+
this.style.cssText = value;
|
|
789
|
+
}
|
|
790
|
+
|
|
571
791
|
get dataset() {
|
|
572
792
|
this.#dataset ??= datasetBinding(nodeOf(this), realmOf(this));
|
|
573
793
|
return this.#dataset;
|
|
574
794
|
}
|
|
575
795
|
|
|
796
|
+
get attributes() {
|
|
797
|
+
this.#attributes ??= attributesBinding(this);
|
|
798
|
+
return this.#attributes;
|
|
799
|
+
}
|
|
800
|
+
|
|
576
801
|
getAttribute(name) {
|
|
577
802
|
return nodeOf(this).getAttribute(String(name));
|
|
578
803
|
}
|
|
579
804
|
|
|
805
|
+
getAttributeNodeNS(_namespace, localName) {
|
|
806
|
+
return this.getAttributeNode(String(localName));
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// CSSOM View §6.1: false when the element or an ancestor has no box or is
|
|
810
|
+
// visibility: hidden (with the visibilityProperty option).
|
|
811
|
+
checkVisibility(options = undefined) {
|
|
812
|
+
const realm = realmOf(this);
|
|
813
|
+
const node = nodeOf(this);
|
|
814
|
+
if (!node.isConnected) return false;
|
|
815
|
+
for (let element = node; element; element = element.parentElement) {
|
|
816
|
+
if (realm.hooks.computedStyle?.(element)?.display === "none") return false;
|
|
817
|
+
}
|
|
818
|
+
if ((options?.visibilityProperty || options?.checkVisibilityCSS)
|
|
819
|
+
&& realm.hooks.computedStyle?.(node)?.visibility === "hidden") return false;
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Web Animations: no animations run outside CSS transitions/animations.
|
|
824
|
+
getAnimations() {
|
|
825
|
+
return [];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Pointer capture (Pointer Events §10): a pointer this element has captured.
|
|
829
|
+
hasPointerCapture(_pointerId) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
setPointerCapture(_pointerId) {}
|
|
834
|
+
|
|
835
|
+
releasePointerCapture(_pointerId) {}
|
|
836
|
+
|
|
580
837
|
getAttributeNode(name) {
|
|
581
838
|
const element = nodeOf(this);
|
|
582
839
|
const attribute = element.getAttributeNode(String(name));
|
|
@@ -623,7 +880,30 @@ class ElementBinding extends ParentBinding {
|
|
|
623
880
|
return nodeOf(this).hasAttribute(String(name));
|
|
624
881
|
}
|
|
625
882
|
|
|
626
|
-
|
|
883
|
+
hasAttributes() {
|
|
884
|
+
return nodeOf(this).getAttributeNames().length > 0;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Namespaced attributes (DOM §4.9): xlink:href and friends in SVG.
|
|
888
|
+
getAttributeNS(namespace, localName) {
|
|
889
|
+
return nodeOf(this).getAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(localName));
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
hasAttributeNS(namespace, localName) {
|
|
893
|
+
return nodeOf(this).hasAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(localName));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
setAttributeNS(namespace, qualifiedName, value) {
|
|
897
|
+
nodeOf(this).setAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(qualifiedName), String(value));
|
|
898
|
+
touch(this);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
removeAttributeNS(namespace, localName) {
|
|
902
|
+
nodeOf(this).removeAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(localName));
|
|
903
|
+
touch(this);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
toggleAttribute(name, force = undefined) {
|
|
627
907
|
const result = nodeOf(this).toggleAttribute(String(name), force === undefined ? undefined : Boolean(force));
|
|
628
908
|
touch(this);
|
|
629
909
|
return result;
|
|
@@ -770,28 +1050,104 @@ class ElementBinding extends ParentBinding {
|
|
|
770
1050
|
return Math.round(this.getBoundingClientRect().height);
|
|
771
1051
|
}
|
|
772
1052
|
|
|
1053
|
+
/**
|
|
1054
|
+
* CSSOM View §7: the nearest ancestor that is positioned (or that
|
|
1055
|
+
* contains absolutely positioned boxes, as a transformed one does), or a
|
|
1056
|
+
* td, th or table for an element that is not positioned itself, else the
|
|
1057
|
+
* body. Null for the root, the body, fixed boxes and elements without a box.
|
|
1058
|
+
*/
|
|
1059
|
+
get offsetParent() {
|
|
1060
|
+
const realm = realmOf(this);
|
|
1061
|
+
const node = nodeOf(this);
|
|
1062
|
+
const document = node.ownerDocument;
|
|
1063
|
+
if (!node.isConnected || node === document?.documentElement || node === document?.body) return null;
|
|
1064
|
+
const style = realm.hooks.computedStyle?.(node);
|
|
1065
|
+
if (!style || style.display === "none" || style.position === "fixed") return null;
|
|
1066
|
+
const positioned = style.position !== "static";
|
|
1067
|
+
for (let ancestor = node.parentElement; ancestor; ancestor = ancestor.parentElement) {
|
|
1068
|
+
if (ancestor === document.body) return realm.wrap(ancestor);
|
|
1069
|
+
const ancestorStyle = realm.hooks.computedStyle?.(ancestor);
|
|
1070
|
+
if (ancestorStyle?.display === "none") return null;
|
|
1071
|
+
if (ancestorStyle && (ancestorStyle.position !== "static" || ancestorStyle.transform)) return realm.wrap(ancestor);
|
|
1072
|
+
if (!positioned && (ancestor.tagName === "TD" || ancestor.tagName === "TH" || ancestor.tagName === "TABLE")) {
|
|
1073
|
+
return realm.wrap(ancestor);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// The border edge's distance from the offsetParent's padding edge, or
|
|
1080
|
+
// from the document's origin when that is the body or there is none.
|
|
773
1081
|
get offsetLeft() {
|
|
774
|
-
|
|
775
|
-
return Math.round(this.getBoundingClientRect().left + viewport().scrollX);
|
|
1082
|
+
return offsetCoordinate(this, "left");
|
|
776
1083
|
}
|
|
777
1084
|
|
|
778
1085
|
get offsetTop() {
|
|
779
|
-
|
|
780
|
-
return Math.round(this.getBoundingClientRect().top + viewport().scrollY);
|
|
1086
|
+
return offsetCoordinate(this, "top");
|
|
781
1087
|
}
|
|
782
1088
|
|
|
783
|
-
// The root element's client size is the viewport (CSSOM View §6)
|
|
1089
|
+
// The root element's client size is the viewport (CSSOM View §6); a
|
|
1090
|
+
// scroll container's is its padding box.
|
|
784
1091
|
get clientWidth() {
|
|
785
1092
|
const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
|
|
786
|
-
|
|
1093
|
+
if (root) return realmOf(this).hooks.viewport().width;
|
|
1094
|
+
const scroller = elementScroll(this);
|
|
1095
|
+
return scroller ? Math.round(scroller.clientWidth) : this.offsetWidth;
|
|
787
1096
|
}
|
|
788
1097
|
|
|
789
1098
|
get clientHeight() {
|
|
790
1099
|
const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
|
|
791
|
-
|
|
1100
|
+
if (root) return realmOf(this).hooks.viewport().height;
|
|
1101
|
+
const scroller = elementScroll(this);
|
|
1102
|
+
return scroller ? Math.round(scroller.clientHeight) : this.offsetHeight;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Scroll positions (CSSOM View §6): a scroll container's own; the root
|
|
1106
|
+
// element and body stand for the viewport's.
|
|
1107
|
+
get scrollTop() {
|
|
1108
|
+
if (scrollsViewport(this)) return realmOf(this).hooks.viewport().scrollY;
|
|
1109
|
+
return elementScroll(this)?.y ?? 0;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
set scrollTop(value) {
|
|
1113
|
+
this.scrollTo(this.scrollLeft, Number(value));
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
get scrollLeft() {
|
|
1117
|
+
if (scrollsViewport(this)) return realmOf(this).hooks.viewport().scrollX;
|
|
1118
|
+
return elementScroll(this)?.x ?? 0;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
set scrollLeft(value) {
|
|
1122
|
+
this.scrollTo(Number(value), this.scrollTop);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
scrollTo(x = undefined, y = undefined) {
|
|
1126
|
+
const options = typeof x === "object" && x !== null ? x : null;
|
|
1127
|
+
const left = options ? options.left ?? this.scrollLeft : Number(x);
|
|
1128
|
+
const top = options ? options.top ?? this.scrollTop : Number(y);
|
|
1129
|
+
const realm = realmOf(this);
|
|
1130
|
+
if (scrollsViewport(this)) realm.hooks.scrollTo(Number.isFinite(left) ? left : 0, Number.isFinite(top) ? top : 0);
|
|
1131
|
+
else realm.hooks.setElementScroll?.(nodeOf(this), Number.isFinite(left) ? left : 0, Number.isFinite(top) ? top : 0);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
scroll(x = undefined, y = undefined) {
|
|
1135
|
+
this.scrollTo(x, y);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
scrollBy(x = undefined, y = undefined) {
|
|
1139
|
+
const options = typeof x === "object" && x !== null ? x : null;
|
|
1140
|
+
const dx = Number(options ? options.left ?? 0 : x) || 0;
|
|
1141
|
+
const dy = Number(options ? options.top ?? 0 : y) || 0;
|
|
1142
|
+
this.scrollTo(this.scrollLeft + dx, this.scrollTop + dy);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Forms, tables and selects (undefined on other elements, as in browsers).
|
|
1146
|
+
get elements() {
|
|
1147
|
+
const node = nodeOf(this);
|
|
1148
|
+
return node.tagName === "FORM" ? realmOf(this).live(node.elements) : undefined;
|
|
792
1149
|
}
|
|
793
1150
|
|
|
794
|
-
// Tables and selects (undefined on other elements, as in browsers).
|
|
795
1151
|
get rows() {
|
|
796
1152
|
const rows = nodeOf(this).rows;
|
|
797
1153
|
return rows === undefined ? undefined : realmOf(this).wrapAll(rows);
|
|
@@ -845,26 +1201,68 @@ class ElementBinding extends ParentBinding {
|
|
|
845
1201
|
touch(this, [":state"]);
|
|
846
1202
|
}
|
|
847
1203
|
|
|
848
|
-
//
|
|
849
|
-
//
|
|
1204
|
+
// A scroll container's scroll size is how far its content reaches; other
|
|
1205
|
+
// boxes' is their own size; the root element spans at least the viewport.
|
|
850
1206
|
get scrollWidth() {
|
|
1207
|
+
const scroller = elementScroll(this);
|
|
1208
|
+
if (scroller) return Math.round(scroller.scrollWidth);
|
|
851
1209
|
const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
|
|
852
1210
|
return Math.max(this.offsetWidth, root ? realmOf(this).hooks.viewport().width : 0);
|
|
853
1211
|
}
|
|
854
1212
|
|
|
855
1213
|
get scrollHeight() {
|
|
1214
|
+
const scroller = elementScroll(this);
|
|
1215
|
+
if (scroller) return Math.round(scroller.scrollHeight);
|
|
856
1216
|
const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
|
|
857
1217
|
return Math.max(this.offsetHeight, root ? realmOf(this).hooks.viewport().height : 0);
|
|
858
1218
|
}
|
|
859
1219
|
|
|
860
1220
|
scrollIntoView() {
|
|
861
1221
|
const realm = realmOf(this);
|
|
1222
|
+
if (realm.hooks.scrollIntoView) {
|
|
1223
|
+
realm.hooks.scrollIntoView(nodeOf(this));
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
862
1226
|
const rect = this.getBoundingClientRect();
|
|
863
1227
|
const { scrollX, scrollY } = realm.hooks.viewport();
|
|
864
1228
|
realm.hooks.scrollTo(scrollX, scrollY + rect.top);
|
|
865
1229
|
}
|
|
866
1230
|
}
|
|
867
1231
|
|
|
1232
|
+
/** A scroll container's scroll state, or null (see the renderer's scrollerGeometry). */
|
|
1233
|
+
function offsetCoordinate(wrapper, side) {
|
|
1234
|
+
const realm = realmOf(wrapper);
|
|
1235
|
+
const node = nodeOf(wrapper);
|
|
1236
|
+
// CSSOM View §7: the body's offsets are zero.
|
|
1237
|
+
if (node === node.ownerDocument?.body) return 0;
|
|
1238
|
+
const rect = realm.hooks.boundsOf(node);
|
|
1239
|
+
if (!rect) return 0;
|
|
1240
|
+
const { scrollX, scrollY } = realm.hooks.viewport();
|
|
1241
|
+
const own = side === "left" ? rect.left + scrollX : rect.top + scrollY;
|
|
1242
|
+
const parent = wrapper.offsetParent;
|
|
1243
|
+
if (!parent || nodeOf(parent) === node.ownerDocument?.body) return Math.round(own);
|
|
1244
|
+
const parentNode = nodeOf(parent);
|
|
1245
|
+
const parentRect = realm.hooks.boundsOf(parentNode);
|
|
1246
|
+
if (!parentRect) return Math.round(own);
|
|
1247
|
+
const border = realm.hooks.computedStyle?.(parentNode)?.borderWidths?.[side === "left" ? 3 : 0] ?? 0;
|
|
1248
|
+
// A box scrolled inside its offsetParent keeps its layout offset.
|
|
1249
|
+
const scrolled = elementScroll(parent);
|
|
1250
|
+
const scroll = side === "left" ? scrolled?.x ?? 0 : scrolled?.y ?? 0;
|
|
1251
|
+
const origin = (side === "left" ? parentRect.left + scrollX : parentRect.top + scrollY) + border;
|
|
1252
|
+
return Math.round(own - origin + scroll);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function elementScroll(wrapper) {
|
|
1256
|
+
return realmOf(wrapper).hooks.elementScroll?.(nodeOf(wrapper)) ?? null;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** The root element and body scroll the viewport (CSSOM View §6, the scrolling element). */
|
|
1260
|
+
function scrollsViewport(wrapper) {
|
|
1261
|
+
const node = nodeOf(wrapper);
|
|
1262
|
+
const document = node.ownerDocument;
|
|
1263
|
+
return node === document?.documentElement || node === document?.body;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
868
1266
|
function isMedia(wrapper) {
|
|
869
1267
|
return wrapper.tagName === "VIDEO" || wrapper.tagName === "AUDIO";
|
|
870
1268
|
}
|
|
@@ -927,6 +1325,9 @@ for (const property of REFLECTED_PROPERTIES) {
|
|
|
927
1325
|
: typeof node[property] === "number" ? Number(value)
|
|
928
1326
|
: String(value);
|
|
929
1327
|
touch(this, [":state"]);
|
|
1328
|
+
// A media src may be a MediaSource blob URL. Attachment runs now so
|
|
1329
|
+
// sourceopen is queued after this setter returns.
|
|
1330
|
+
if (property === "src" && isMedia(this)) realmOf(this).hooks.attachMediaSrc?.(node, node.src);
|
|
930
1331
|
},
|
|
931
1332
|
});
|
|
932
1333
|
}
|
|
@@ -952,6 +1353,30 @@ for (const property of ["origin", "protocol", "username", "password", "host", "h
|
|
|
952
1353
|
}
|
|
953
1354
|
|
|
954
1355
|
class DocumentBinding extends ParentBinding {
|
|
1356
|
+
#implementation = null;
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* document.implementation (DOM §4.5.1): documents scripts build off-screen
|
|
1360
|
+
* (template parsing, sanitizers, polyfills). They share this window and are
|
|
1361
|
+
* never rendered.
|
|
1362
|
+
*/
|
|
1363
|
+
get implementation() {
|
|
1364
|
+
const realm = realmOf(this);
|
|
1365
|
+
const node = nodeOf(this);
|
|
1366
|
+
this.#implementation ??= Object.freeze({
|
|
1367
|
+
createHTMLDocument: (title = undefined) => realm.wrap(node.implementation.createHTMLDocument(
|
|
1368
|
+
title === undefined ? undefined : String(title))),
|
|
1369
|
+
createDocument: (namespace, qualifiedName, doctype = null) => realm.wrap(node.implementation.createDocument(
|
|
1370
|
+
namespace === null || namespace === undefined ? null : String(namespace),
|
|
1371
|
+
qualifiedName === null || qualifiedName === undefined ? "" : String(qualifiedName),
|
|
1372
|
+
doctype ? nodeOf(doctype) : null)),
|
|
1373
|
+
createDocumentType: (qualifiedName, publicId, systemId) => realm.wrap(node.implementation.createDocumentType(
|
|
1374
|
+
String(qualifiedName), String(publicId), String(systemId))),
|
|
1375
|
+
hasFeature: () => true,
|
|
1376
|
+
});
|
|
1377
|
+
return this.#implementation;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
955
1380
|
get documentElement() {
|
|
956
1381
|
return realmOf(this).wrap(nodeOf(this).documentElement);
|
|
957
1382
|
}
|
|
@@ -1023,6 +1448,49 @@ class DocumentBinding extends ParentBinding {
|
|
|
1023
1448
|
return realmOf(this).wrap(nodeOf(this).activeElement);
|
|
1024
1449
|
}
|
|
1025
1450
|
|
|
1451
|
+
/**
|
|
1452
|
+
* document.all is an HTMLAllCollection that is falsy and loosely equal to
|
|
1453
|
+
* undefined (HTML §7.2.6 [[IsHTMLDDA]]), which script cannot create. null
|
|
1454
|
+
* keeps what pages test, `!document.all`, `document.all == null` and
|
|
1455
|
+
* `value !== document.all` for undefined values (polymer_resin's check
|
|
1456
|
+
* before sanitizing); only typeof differs.
|
|
1457
|
+
*/
|
|
1458
|
+
get all() {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* document.styleSheets (CSSOM §6.2): one entry per <style> and loaded-or-not
|
|
1464
|
+
* <link rel=stylesheet>, in document order. Their rules are not exposed
|
|
1465
|
+
* (cssRules is empty), as for a sheet scripts may not read.
|
|
1466
|
+
*/
|
|
1467
|
+
get styleSheets() {
|
|
1468
|
+
const realm = realmOf(this);
|
|
1469
|
+
const sheets = [...nodeOf(this).querySelectorAll("style, link[rel~='stylesheet']")].map((element) => {
|
|
1470
|
+
const href = element.tagName === "LINK" ? element.getAttribute("href") : null;
|
|
1471
|
+
let resolved = null;
|
|
1472
|
+
if (href !== null) try { resolved = new URL(href, nodeOf(this).URL).href; } catch {}
|
|
1473
|
+
return Object.freeze({
|
|
1474
|
+
type: "text/css",
|
|
1475
|
+
href: resolved,
|
|
1476
|
+
title: element.getAttribute("title"),
|
|
1477
|
+
disabled: false,
|
|
1478
|
+
ownerNode: realm.wrap(element),
|
|
1479
|
+
media: Object.freeze({ mediaText: element.getAttribute("media") ?? "", length: element.hasAttribute("media") ? 1 : 0 }),
|
|
1480
|
+
cssRules: Object.freeze([]),
|
|
1481
|
+
rules: Object.freeze([]),
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
const list = [...sheets];
|
|
1485
|
+
list.item = (index) => sheets[Number(index) >>> 0] ?? null;
|
|
1486
|
+
return Object.freeze(list);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/** The topmost element painted at a viewport point (CSSOM View §5.1), or null. */
|
|
1490
|
+
elementFromPoint(x, y) {
|
|
1491
|
+
return realmOf(this).wrap(realmOf(this).hooks.elementAt?.(Number(x), Number(y)) ?? null);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1026
1494
|
get currentScript() {
|
|
1027
1495
|
return realmOf(this).wrap(realmOf(this).hooks.currentScript?.() ?? null);
|
|
1028
1496
|
}
|
|
@@ -1083,8 +1551,27 @@ class DocumentBinding extends ParentBinding {
|
|
|
1083
1551
|
return realmOf(this).wrap(nodeOf(this).createDocumentFragment());
|
|
1084
1552
|
}
|
|
1085
1553
|
|
|
1086
|
-
|
|
1087
|
-
return realmOf(this).
|
|
1554
|
+
createRange() {
|
|
1555
|
+
return realmOf(this).wrapRange(nodeOf(this).createRange());
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/** A copy of a node (from any document, a template's content say) owned by this one (DOM §4.5). */
|
|
1559
|
+
importNode(node, deep = false) {
|
|
1560
|
+
const options = typeof deep === "object" && deep !== null ? Boolean(deep.deep) : Boolean(deep);
|
|
1561
|
+
return realmOf(this).wrap(nodeOf(this).importNode(requireNode(node), options));
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/** Moves a node into this document, out of wherever it was. */
|
|
1565
|
+
adoptNode(node) {
|
|
1566
|
+
return realmOf(this).wrap(nodeOf(this).adoptNode(requireNode(node)));
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
createEvent(type) {
|
|
1570
|
+
const name = CREATE_EVENT_TYPES.get(String(type).toLowerCase());
|
|
1571
|
+
if (!name) throw new DOMException(`The provided event type ("${type}") is invalid.`, "NotSupportedError");
|
|
1572
|
+
const window = realmOf(this).window;
|
|
1573
|
+
const RealEvent = window[name] ?? window.Event;
|
|
1574
|
+
return realmOf(this).wrapEvent(new RealEvent(""));
|
|
1088
1575
|
}
|
|
1089
1576
|
|
|
1090
1577
|
hasFocus() {
|
|
@@ -1145,15 +1632,121 @@ class EventBinding {
|
|
|
1145
1632
|
}
|
|
1146
1633
|
|
|
1147
1634
|
get isTrusted() {
|
|
1148
|
-
return
|
|
1635
|
+
return this.#realm.isTrusted(this.#event);
|
|
1149
1636
|
}
|
|
1150
1637
|
|
|
1638
|
+
// Happy DOM stamps events with the engine's clock; pages see their document's timeline.
|
|
1151
1639
|
get timeStamp() {
|
|
1152
|
-
return this.#event.timeStamp;
|
|
1640
|
+
return this.#event.timeStamp - (this.#realm.hooks.timeOriginOffset?.() ?? 0);
|
|
1153
1641
|
}
|
|
1154
1642
|
|
|
1155
1643
|
get detail() {
|
|
1156
|
-
return this.#detail ?? null;
|
|
1644
|
+
return this.#detail ?? (typeof this.#event.detail === "number" ? this.#event.detail : null);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
get composed() {
|
|
1648
|
+
return Boolean(this.#event.composed);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// Legacy aliases of defaultPrevented and stopPropagation (DOM §2.2).
|
|
1652
|
+
get returnValue() {
|
|
1653
|
+
return !this.#event.defaultPrevented;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
set returnValue(value) {
|
|
1657
|
+
if (!value) this.preventDefault();
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
get cancelBubble() {
|
|
1661
|
+
return Boolean(this.#event[Symbol.for("buninu.propagationStopped")]);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
set cancelBubble(value) {
|
|
1665
|
+
if (value) {
|
|
1666
|
+
this.#event[Symbol.for("buninu.propagationStopped")] = true;
|
|
1667
|
+
this.stopPropagation();
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// UIEvent: the window for events the browser dispatches, null for constructed ones.
|
|
1672
|
+
get view() {
|
|
1673
|
+
return this.#realm.isTrusted(this.#event) ? this.#realm.hooks.targetOf?.(this.#realm.window) ?? null : null;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
get which() {
|
|
1677
|
+
if (typeof this.#event.keyCode === "number" && this.#event.keyCode) return this.#event.keyCode;
|
|
1678
|
+
return typeof this.#event.button === "number" ? this.#event.button + 1 : 0;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// MouseEvent coordinates (CSSOM View §12): page = client + scroll; x/y alias client.
|
|
1682
|
+
get pageX() {
|
|
1683
|
+
return this.clientX + (this.#realm.hooks.viewport?.().scrollX ?? 0);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
get pageY() {
|
|
1687
|
+
return this.clientY + (this.#realm.hooks.viewport?.().scrollY ?? 0);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
get x() {
|
|
1691
|
+
return this.clientX;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
get y() {
|
|
1695
|
+
return this.clientY;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
get offsetX() {
|
|
1699
|
+
return this.#event.offsetX ?? 0;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
get offsetY() {
|
|
1703
|
+
return this.#event.offsetY ?? 0;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
get movementX() {
|
|
1707
|
+
return this.#event.movementX ?? 0;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
get movementY() {
|
|
1711
|
+
return this.#event.movementY ?? 0;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
get relatedTarget() {
|
|
1715
|
+
return this.#realm.wrap(this.#event.relatedTarget ?? null) ?? null;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
getModifierState(key) {
|
|
1719
|
+
const name = String(key);
|
|
1720
|
+
return name === "Alt" ? this.altKey : name === "Control" ? this.ctrlKey : name === "Shift" ? this.shiftKey
|
|
1721
|
+
: name === "Meta" ? this.metaKey : false;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// Legacy initialisers (UI Events §B): only before dispatch, like initEvent.
|
|
1725
|
+
initUIEvent(type, bubbles = false, cancelable = false, _view = null, detail = 0) {
|
|
1726
|
+
this.initEvent(type, bubbles, cancelable);
|
|
1727
|
+
if (this.#detail === undefined) this.#detail = Number(detail) || 0;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
initMouseEvent(type, bubbles = false, cancelable = false, view = null, detail = 0) {
|
|
1731
|
+
this.initUIEvent(type, bubbles, cancelable, view, detail);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// A plain Event has no detail of its own: assigning one (Polymer's fire()
|
|
1735
|
+
// does) makes an own property, as in browsers, instead of failing on the getter.
|
|
1736
|
+
set detail(value) {
|
|
1737
|
+
Object.defineProperty(this, "detail", { value, writable: true, configurable: true, enumerable: true });
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/** Legacy initialisation of an event from document.createEvent() (DOM §2.5); not once dispatched. */
|
|
1741
|
+
initEvent(type, bubbles = false, cancelable = false) {
|
|
1742
|
+
if (this.#event.eventPhase !== 0) return;
|
|
1743
|
+
this.#event.initEvent(String(type), Boolean(bubbles), Boolean(cancelable));
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
initCustomEvent(type, bubbles = false, cancelable = false, detail = null) {
|
|
1747
|
+
if (this.#event.eventPhase !== 0) return;
|
|
1748
|
+
this.#event.initEvent(String(type), Boolean(bubbles), Boolean(cancelable));
|
|
1749
|
+
this.#detail = detail;
|
|
1157
1750
|
}
|
|
1158
1751
|
|
|
1159
1752
|
// PopStateEvent and HashChangeEvent fields; the engine only fires popstate
|
|
@@ -1180,6 +1773,9 @@ class EventBinding {
|
|
|
1180
1773
|
return this.#event.code;
|
|
1181
1774
|
}
|
|
1182
1775
|
|
|
1776
|
+
get keyCode() { return this.#event.keyCode ?? 0; }
|
|
1777
|
+
get charCode() { return this.#event.charCode ?? 0; }
|
|
1778
|
+
|
|
1183
1779
|
get clientX() {
|
|
1184
1780
|
return this.#event.clientX;
|
|
1185
1781
|
}
|
|
@@ -1237,7 +1833,26 @@ class EventBinding {
|
|
|
1237
1833
|
}
|
|
1238
1834
|
|
|
1239
1835
|
get data() {
|
|
1240
|
-
|
|
1836
|
+
const message = this.#realm.messageOf(this.#event);
|
|
1837
|
+
return message ? message.data : this.#event.data ?? null;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// MessageEvent fields; undefined on other events, as in browsers.
|
|
1841
|
+
get origin() {
|
|
1842
|
+
return this.#realm.messageOf(this.#event)?.origin;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
get lastEventId() {
|
|
1846
|
+
return this.#realm.messageOf(this.#event)?.lastEventId;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
get source() {
|
|
1850
|
+
const message = this.#realm.messageOf(this.#event);
|
|
1851
|
+
return message ? message.source : undefined;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
get ports() {
|
|
1855
|
+
return this.#realm.messageOf(this.#event)?.ports;
|
|
1241
1856
|
}
|
|
1242
1857
|
|
|
1243
1858
|
get inputType() {
|
|
@@ -1516,50 +2131,152 @@ const UNRENDERED_STYLE_PROPERTIES = new Set([
|
|
|
1516
2131
|
"viewTimelineAxis", "viewTimelineInset", "viewTimelineName",
|
|
1517
2132
|
]);
|
|
1518
2133
|
|
|
2134
|
+
/** Each element.style proxy → its element, declaration block and realm. */
|
|
2135
|
+
const styleStates = new WeakMap();
|
|
2136
|
+
|
|
2137
|
+
function styleState(self) {
|
|
2138
|
+
const state = styleStates.get(self);
|
|
2139
|
+
if (!state) throw new TypeError("Illegal invocation");
|
|
2140
|
+
return state;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
1519
2143
|
/**
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
1522
|
-
* style
|
|
2144
|
+
* CSSStyleDeclaration (CSSOM §6.6) for one realm: the operations and the
|
|
2145
|
+
* cssText/length accessors live on its prototype, as in browsers, so
|
|
2146
|
+
* element.style.setProperty is the interface's (native-looking) method.
|
|
1523
2147
|
*/
|
|
1524
|
-
function
|
|
1525
|
-
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
2148
|
+
export function cssStyleDeclarationInterface(realm) {
|
|
2149
|
+
if (realm.cssStyleDeclaration) return realm.cssStyleDeclaration;
|
|
2150
|
+
const CSSStyleDeclaration = { CSSStyleDeclaration: function () { throw new TypeError("Illegal constructor"); } }.CSSStyleDeclaration;
|
|
2151
|
+
const members = {
|
|
2152
|
+
get cssText() { return styleState(this).style.cssText; },
|
|
2153
|
+
set cssText(value) {
|
|
2154
|
+
const { style, node, realm: owner } = styleState(this);
|
|
2155
|
+
const before = style.cssText;
|
|
2156
|
+
style.cssText = String(value);
|
|
2157
|
+
if (style.cssText !== before) owner.hooks.touch(node);
|
|
2158
|
+
},
|
|
2159
|
+
get length() { return styleState(this).style.length; },
|
|
2160
|
+
get parentRule() { return null; },
|
|
2161
|
+
get cssFloat() { return styleState(this).style.getPropertyValue("float"); },
|
|
2162
|
+
set cssFloat(value) { this.setProperty("float", value); },
|
|
2163
|
+
item(index) { return styleState(this).style.item?.(Number(index) >>> 0) ?? ""; },
|
|
2164
|
+
getPropertyValue(property) { return styleState(this).style.getPropertyValue(String(property)); },
|
|
2165
|
+
getPropertyPriority(property) { return styleState(this).style.getPropertyPriority?.(String(property)) ?? ""; },
|
|
2166
|
+
setProperty(property, value, priority = "") {
|
|
2167
|
+
const { style, node, realm: owner } = styleState(this);
|
|
1531
2168
|
const before = style.cssText;
|
|
1532
2169
|
style.setProperty(String(property), value === null ? "" : String(value), String(priority));
|
|
1533
|
-
if (style.cssText !== before)
|
|
2170
|
+
if (style.cssText !== before) owner.hooks.touch(node);
|
|
1534
2171
|
},
|
|
1535
|
-
removeProperty
|
|
2172
|
+
removeProperty(property) {
|
|
2173
|
+
const { style, node, realm: owner } = styleState(this);
|
|
1536
2174
|
const previous = style.removeProperty(String(property));
|
|
1537
|
-
|
|
2175
|
+
owner.hooks.touch(node);
|
|
1538
2176
|
return previous;
|
|
1539
2177
|
},
|
|
1540
2178
|
};
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
2179
|
+
const prototype = {};
|
|
2180
|
+
for (const key of Reflect.ownKeys(members)) {
|
|
2181
|
+
Object.defineProperty(prototype, key, { ...Object.getOwnPropertyDescriptor(members, key), enumerable: true, configurable: true });
|
|
2182
|
+
}
|
|
2183
|
+
Object.defineProperty(prototype, "constructor", { value: CSSStyleDeclaration, writable: true, configurable: true });
|
|
2184
|
+
Object.defineProperty(prototype, Symbol.toStringTag, { value: "CSSStyleDeclaration", configurable: true });
|
|
2185
|
+
Object.defineProperty(CSSStyleDeclaration, "prototype", { value: prototype });
|
|
2186
|
+
realm.cssStyleDeclaration = CSSStyleDeclaration;
|
|
2187
|
+
return CSSStyleDeclaration;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/**
|
|
2191
|
+
* element.style: a proxy over a CSSStyleDeclaration that reads and writes
|
|
2192
|
+
* the real declaration block, by property name, with string values only.
|
|
2193
|
+
* Mutations go to the style attribute, which the style engine reads.
|
|
2194
|
+
*/
|
|
2195
|
+
function styleBinding(node, realm) {
|
|
2196
|
+
const style = node.style;
|
|
2197
|
+
const prototype = cssStyleDeclarationInterface(realm).prototype;
|
|
2198
|
+
const toKebab = (property) => property.startsWith("--") ? property
|
|
2199
|
+
: property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
2200
|
+
const proxy = new Proxy(Object.create(prototype), {
|
|
2201
|
+
get(target, property, receiver) {
|
|
2202
|
+
if (typeof property !== "string" || property in prototype) return Reflect.get(target, property, receiver);
|
|
2203
|
+
if (/^(0|[1-9]\d*)$/.test(property)) return style.item?.(Number(property)) ?? undefined;
|
|
1547
2204
|
return style.getPropertyValue(toKebab(property));
|
|
1548
2205
|
},
|
|
1549
|
-
set(
|
|
1550
|
-
if (typeof property !== "string") return
|
|
2206
|
+
set(target, property, value, receiver) {
|
|
2207
|
+
if (typeof property !== "string" || property in prototype) return Reflect.set(target, property, value, receiver);
|
|
1551
2208
|
const before = style.cssText;
|
|
1552
|
-
|
|
1553
|
-
else style.setProperty(toKebab(property), value === null ? "" : String(value));
|
|
2209
|
+
style.setProperty(toKebab(property), value === null ? "" : String(value));
|
|
1554
2210
|
if (style.cssText !== before) realm.hooks.touch(node);
|
|
1555
2211
|
return true;
|
|
1556
2212
|
},
|
|
1557
2213
|
has: (_target, property) => {
|
|
1558
|
-
if (typeof property !== "string") return
|
|
2214
|
+
if (typeof property !== "string") return property in prototype;
|
|
1559
2215
|
const camel = property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1560
|
-
return
|
|
1561
|
-
|
|
2216
|
+
return property in prototype || (property in style && !UNRENDERED_STYLE_PROPERTIES.has(camel));
|
|
2217
|
+
},
|
|
2218
|
+
defineProperty: () => false,
|
|
2219
|
+
deleteProperty: () => false,
|
|
2220
|
+
setPrototypeOf: () => false,
|
|
2221
|
+
});
|
|
2222
|
+
styleStates.set(proxy, { node, style, realm });
|
|
2223
|
+
return proxy;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
/**
|
|
2227
|
+
* element.attributes (DOM §4.9.1 NamedNodeMap): live, by index and by name;
|
|
2228
|
+
* its entries are the element's Attr views (see getAttributeNode).
|
|
2229
|
+
*/
|
|
2230
|
+
function attributesBinding(owner) {
|
|
2231
|
+
const names = () => nodeOf(owner).getAttributeNames();
|
|
2232
|
+
const nameAt = (index) => names()[index] ?? null;
|
|
2233
|
+
const methods = {
|
|
2234
|
+
item: (index) => {
|
|
2235
|
+
const name = nameAt(Number(index) >>> 0);
|
|
2236
|
+
return name === null ? null : owner.getAttributeNode(name);
|
|
2237
|
+
},
|
|
2238
|
+
getNamedItem: (name) => owner.getAttributeNode(String(name)),
|
|
2239
|
+
getNamedItemNS: (_namespace, localName) => owner.getAttributeNode(String(localName)),
|
|
2240
|
+
removeNamedItem(name) {
|
|
2241
|
+
const attribute = owner.getAttributeNode(String(name));
|
|
2242
|
+
if (!attribute) throw new DOMException(`No attribute named '${name}'`, "NotFoundError");
|
|
2243
|
+
const removed = { name: attribute.name, value: attribute.value, nodeType: 2, specified: true, ownerElement: null };
|
|
2244
|
+
owner.removeAttribute(attribute.name);
|
|
2245
|
+
return removed;
|
|
2246
|
+
},
|
|
2247
|
+
setNamedItem(attribute) {
|
|
2248
|
+
const previous = owner.getAttributeNode(String(attribute?.name));
|
|
2249
|
+
owner.setAttribute(String(attribute?.name), String(attribute?.value ?? ""));
|
|
2250
|
+
return previous;
|
|
2251
|
+
},
|
|
2252
|
+
*[Symbol.iterator]() {
|
|
2253
|
+
for (const name of names()) yield owner.getAttributeNode(name);
|
|
2254
|
+
},
|
|
2255
|
+
};
|
|
2256
|
+
const index = (property) => typeof property === "string" && /^(0|[1-9]\d*)$/.test(property) ? Number(property) : null;
|
|
2257
|
+
return new Proxy(Object.create(null), {
|
|
2258
|
+
get(_target, property) {
|
|
2259
|
+
if (property === "length") return names().length;
|
|
2260
|
+
if (Object.hasOwn(methods, property)) return methods[property];
|
|
2261
|
+
if (property === Symbol.toStringTag) return "NamedNodeMap";
|
|
2262
|
+
if (typeof property !== "string") return undefined;
|
|
2263
|
+
const at = index(property);
|
|
2264
|
+
if (at !== null) return methods.item(at);
|
|
2265
|
+
return nodeOf(owner).hasAttribute(property) ? owner.getAttributeNode(property) : undefined;
|
|
2266
|
+
},
|
|
2267
|
+
has(_target, property) {
|
|
2268
|
+
if (property === "length" || Object.hasOwn(methods, property)) return true;
|
|
2269
|
+
const at = index(property);
|
|
2270
|
+
return at !== null ? at < names().length : typeof property === "string" && nodeOf(owner).hasAttribute(property);
|
|
2271
|
+
},
|
|
2272
|
+
ownKeys: () => [...names().map((_name, at) => String(at)), ...names()],
|
|
2273
|
+
getOwnPropertyDescriptor(_target, property) {
|
|
2274
|
+
const at = index(property);
|
|
2275
|
+
const value = at !== null ? methods.item(at)
|
|
2276
|
+
: typeof property === "string" && nodeOf(owner).hasAttribute(property) ? owner.getAttributeNode(property) : null;
|
|
2277
|
+
return value ? { value, writable: false, enumerable: at !== null, configurable: true } : undefined;
|
|
1562
2278
|
},
|
|
2279
|
+
set: () => false,
|
|
1563
2280
|
defineProperty: () => false,
|
|
1564
2281
|
deleteProperty: () => false,
|
|
1565
2282
|
getPrototypeOf: () => null,
|
|
@@ -1601,16 +2318,38 @@ function datasetBinding(node, realm) {
|
|
|
1601
2318
|
});
|
|
1602
2319
|
}
|
|
1603
2320
|
|
|
2321
|
+
/**
|
|
2322
|
+
* Flattens addEventListener's options (DOM §2.7 "flatten more"): the
|
|
2323
|
+
* dictionary's members are read once each, in IDL order (capture, then
|
|
2324
|
+
* once, passive and signal), before anything else happens, as pages detect
|
|
2325
|
+
* support for an option by watching its getter run.
|
|
2326
|
+
*/
|
|
2327
|
+
function listenerOptions(options) {
|
|
2328
|
+
if (typeof options !== "object" || options === null) {
|
|
2329
|
+
return { capture: Boolean(options), once: false, passive: null, signal: null };
|
|
2330
|
+
}
|
|
2331
|
+
const capture = Boolean(options.capture);
|
|
2332
|
+
const once = Boolean(options.once);
|
|
2333
|
+
const passiveValue = options.passive;
|
|
2334
|
+
const signal = options.signal;
|
|
2335
|
+
if (signal !== undefined && !(signal instanceof AbortSignal)) {
|
|
2336
|
+
throw new TypeError("Failed to read the 'signal' property from 'AddEventListenerOptions': Failed to convert value to 'AbortSignal'.");
|
|
2337
|
+
}
|
|
2338
|
+
return { capture, once, passive: passiveValue === undefined ? null : Boolean(passiveValue), signal: signal ?? null };
|
|
2339
|
+
}
|
|
2340
|
+
|
|
1604
2341
|
function addListener(wrapper, target, type, listener, options) {
|
|
2342
|
+
const { capture, once, signal } = listenerOptions(options);
|
|
1605
2343
|
if (typeof listener !== "function" && (listener === null || typeof listener !== "object")) return;
|
|
2344
|
+
if (signal?.aborted) return;
|
|
1606
2345
|
const realm = realmOf(wrapper) ?? wrapper;
|
|
1607
|
-
const capture = typeof options === "boolean" ? options : Boolean(options?.capture);
|
|
1608
|
-
const once = typeof options === "object" && options !== null && Boolean(options.once);
|
|
1609
2346
|
const listeners = realm.listenerFor(target, String(type), listener, capture);
|
|
1610
2347
|
if (listeners.has(listener)) return;
|
|
2348
|
+
// An aborted signal removes the listener (DOM §2.7 "add an event listener", step 6).
|
|
2349
|
+
signal?.addEventListener("abort", () => removeListener(wrapper, target, type, listener, capture), { once: true });
|
|
1611
2350
|
const host = (event) => {
|
|
1612
2351
|
if (realm.hooks.acceptEvent && !realm.hooks.acceptEvent(event)) return;
|
|
1613
|
-
if (once) removeListener(wrapper, target, type, listener,
|
|
2352
|
+
if (once) removeListener(wrapper, target, type, listener, capture);
|
|
1614
2353
|
const binding = realm.wrapEvent(event);
|
|
1615
2354
|
const thisArg = realm.wrap(event.currentTarget) ?? wrapper;
|
|
1616
2355
|
if (typeof listener === "function") realm.hooks.call(listener, thisArg, [binding]);
|
|
@@ -1622,7 +2361,7 @@ function addListener(wrapper, target, type, listener, options) {
|
|
|
1622
2361
|
|
|
1623
2362
|
function removeListener(wrapper, target, type, listener, options) {
|
|
1624
2363
|
const realm = realmOf(wrapper) ?? wrapper;
|
|
1625
|
-
const capture = typeof options === "
|
|
2364
|
+
const capture = typeof options === "object" && options !== null ? Boolean(options.capture) : Boolean(options);
|
|
1626
2365
|
const listeners = realm.listenerFor(target, String(type), listener, capture);
|
|
1627
2366
|
const host = listeners.get(listener);
|
|
1628
2367
|
if (!host) return;
|
|
@@ -1645,16 +2384,102 @@ export function windowEventMethods(realm, pageGlobal) {
|
|
|
1645
2384
|
};
|
|
1646
2385
|
}
|
|
1647
2386
|
|
|
2387
|
+
/**
|
|
2388
|
+
* Range (DOM §5.5) over an engine Range: boundary points and the content
|
|
2389
|
+
* operations; nodes go in and out wrapped. Its client rects are those of the
|
|
2390
|
+
* smallest element containing it.
|
|
2391
|
+
*/
|
|
2392
|
+
class RangeBinding {
|
|
2393
|
+
static START_TO_START = 0;
|
|
2394
|
+
static START_TO_END = 1;
|
|
2395
|
+
static END_TO_END = 2;
|
|
2396
|
+
static END_TO_START = 3;
|
|
2397
|
+
|
|
2398
|
+
#range;
|
|
2399
|
+
#realm;
|
|
2400
|
+
|
|
2401
|
+
constructor(key, range, realm) {
|
|
2402
|
+
if (key !== CONSTRUCT) throw new TypeError("Illegal constructor");
|
|
2403
|
+
this.#range = range;
|
|
2404
|
+
this.#realm = realm;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
static rangeOf(value) {
|
|
2408
|
+
if (value === null || typeof value !== "object" || !(#range in value)) throw new TypeError("parameter is not of type 'Range'");
|
|
2409
|
+
return value.#range;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
get startContainer() { return this.#realm.wrap(this.#range.startContainer); }
|
|
2413
|
+
get startOffset() { return this.#range.startOffset; }
|
|
2414
|
+
get endContainer() { return this.#realm.wrap(this.#range.endContainer); }
|
|
2415
|
+
get endOffset() { return this.#range.endOffset; }
|
|
2416
|
+
get collapsed() { return this.#range.collapsed; }
|
|
2417
|
+
get commonAncestorContainer() { return this.#realm.wrap(this.#range.commonAncestorContainer); }
|
|
2418
|
+
|
|
2419
|
+
setStart(node, offset) { this.#range.setStart(requireNode(node), Number(offset) >>> 0); }
|
|
2420
|
+
setEnd(node, offset) { this.#range.setEnd(requireNode(node), Number(offset) >>> 0); }
|
|
2421
|
+
setStartBefore(node) { this.#range.setStartBefore(requireNode(node)); }
|
|
2422
|
+
setStartAfter(node) { this.#range.setStartAfter(requireNode(node)); }
|
|
2423
|
+
setEndBefore(node) { this.#range.setEndBefore(requireNode(node)); }
|
|
2424
|
+
setEndAfter(node) { this.#range.setEndAfter(requireNode(node)); }
|
|
2425
|
+
selectNode(node) { this.#range.selectNode(requireNode(node)); }
|
|
2426
|
+
selectNodeContents(node) { this.#range.selectNodeContents(requireNode(node)); }
|
|
2427
|
+
collapse(toStart = false) { this.#range.collapse(Boolean(toStart)); }
|
|
2428
|
+
detach() {}
|
|
2429
|
+
|
|
2430
|
+
cloneRange() { return this.#realm.wrapRange(this.#range.cloneRange()); }
|
|
2431
|
+
compareBoundaryPoints(how, other) { return this.#range.compareBoundaryPoints(Number(how), RangeBinding.rangeOf(other)); }
|
|
2432
|
+
comparePoint(node, offset) { return this.#range.comparePoint(requireNode(node), Number(offset) >>> 0); }
|
|
2433
|
+
isPointInRange(node, offset) { return this.#range.isPointInRange(requireNode(node), Number(offset) >>> 0); }
|
|
2434
|
+
intersectsNode(node) { return this.#range.intersectsNode(requireNode(node)); }
|
|
2435
|
+
|
|
2436
|
+
cloneContents() { return this.#realm.wrap(this.#range.cloneContents()); }
|
|
2437
|
+
extractContents() { return this.#realm.wrap(this.#range.extractContents()); }
|
|
2438
|
+
deleteContents() { this.#range.deleteContents(); }
|
|
2439
|
+
insertNode(node) { this.#range.insertNode(requireNode(node)); }
|
|
2440
|
+
surroundContents(node) { this.#range.surroundContents(requireNode(node)); }
|
|
2441
|
+
createContextualFragment(html) { return this.#realm.wrap(this.#range.createContextualFragment(String(html))); }
|
|
2442
|
+
|
|
2443
|
+
getBoundingClientRect() {
|
|
2444
|
+
let node = this.#range.commonAncestorContainer;
|
|
2445
|
+
if (node && node.nodeType !== 1) node = node.parentElement;
|
|
2446
|
+
const rect = node ? this.#realm.hooks.boundsOf(node) : null;
|
|
2447
|
+
return domRect(rect ?? { left: 0, top: 0, width: 0, height: 0 });
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
getClientRects() { return [this.getBoundingClientRect()]; }
|
|
2451
|
+
toString() { return this.#range.toString(); }
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/** Page-constructible Range (`new Range()`), its prototype shared by document.createRange()'s. */
|
|
2455
|
+
export function rangeConstructor(realm) {
|
|
2456
|
+
function Range() {
|
|
2457
|
+
if (!new.target) throw new TypeError("Constructor Range requires 'new'");
|
|
2458
|
+
return realm.wrapRange(new realm.window.Range());
|
|
2459
|
+
}
|
|
2460
|
+
realm.rangePrototype = mutableBindingPrototype(RangeBinding.prototype);
|
|
2461
|
+
Object.defineProperty(realm.rangePrototype, "constructor", { value: Range, writable: true, configurable: true });
|
|
2462
|
+
Range.prototype = realm.rangePrototype;
|
|
2463
|
+
for (const name of ["START_TO_START", "START_TO_END", "END_TO_END", "END_TO_START"]) {
|
|
2464
|
+
Object.defineProperty(Range, name, { value: RangeBinding[name], enumerable: true });
|
|
2465
|
+
Object.defineProperty(realm.rangePrototype, name, { value: RangeBinding[name], enumerable: true });
|
|
2466
|
+
}
|
|
2467
|
+
return Range;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
1648
2470
|
/**
|
|
1649
2471
|
* Page-constructible EventTarget (`new EventTarget()` and subclasses). Each
|
|
1650
2472
|
* instance is backed by its own Happy DOM EventTarget; `targetOf` maps that
|
|
1651
2473
|
* back to the page object for event.target and the listener's `this`.
|
|
2474
|
+
* EventTarget.prototype's methods work on every event target, as in browsers
|
|
2475
|
+
* (Node.prototype and Window.prototype inherit them): `targetOfPage` gives the
|
|
2476
|
+
* engine target behind a node or the window.
|
|
1652
2477
|
*/
|
|
1653
|
-
export function eventTargetConstructor(realm, isTarget) {
|
|
2478
|
+
export function eventTargetConstructor(realm, isTarget, targetOfPage = () => null) {
|
|
1654
2479
|
const backing = new WeakMap();
|
|
1655
2480
|
const owners = new WeakMap();
|
|
1656
2481
|
const realOf = (self) => {
|
|
1657
|
-
const real = backing.get(self);
|
|
2482
|
+
const real = backing.get(self) ?? targetOfPage(self);
|
|
1658
2483
|
if (!real) throw new TypeError("Illegal invocation");
|
|
1659
2484
|
return real;
|
|
1660
2485
|
};
|
|
@@ -1668,10 +2493,10 @@ export function eventTargetConstructor(realm, isTarget) {
|
|
|
1668
2493
|
}
|
|
1669
2494
|
EventTarget.prototype = {
|
|
1670
2495
|
constructor: EventTarget,
|
|
1671
|
-
addEventListener(type, listener, options) {
|
|
2496
|
+
addEventListener(type, listener, options = undefined) {
|
|
1672
2497
|
addListener(realm, realOf(this), type, listener, options);
|
|
1673
2498
|
},
|
|
1674
|
-
removeEventListener(type, listener, options) {
|
|
2499
|
+
removeEventListener(type, listener, options = undefined) {
|
|
1675
2500
|
removeListener(realm, realOf(this), type, listener, options);
|
|
1676
2501
|
},
|
|
1677
2502
|
dispatchEvent(event) {
|
|
@@ -1688,10 +2513,34 @@ export function eventTargetConstructor(realm, isTarget) {
|
|
|
1688
2513
|
return { EventTarget, targetOf: (real) => owners.get(real) ?? null };
|
|
1689
2514
|
}
|
|
1690
2515
|
|
|
2516
|
+
const EVENT_PHASES = Object.freeze({ NONE: 0, CAPTURING_PHASE: 1, AT_TARGET: 2, BUBBLING_PHASE: 3 });
|
|
2517
|
+
const MODIFIERS = ["altKey", "ctrlKey", "shiftKey", "metaKey", "getModifierState"];
|
|
2518
|
+
const MOUSE_MEMBERS = ["screenX", "screenY", "clientX", "clientY", "pageX", "pageY", "x", "y", "offsetX", "offsetY",
|
|
2519
|
+
"movementX", "movementY", "button", "buttons", "relatedTarget", "initMouseEvent", ...MODIFIERS];
|
|
2520
|
+
/** Event interface → [parent interface, members it owns], parents first. */
|
|
2521
|
+
const EVENT_INTERFACES = {
|
|
2522
|
+
CustomEvent: ["Event", ["detail", "initCustomEvent"]],
|
|
2523
|
+
UIEvent: ["Event", ["detail", "view", "which", "initUIEvent"]],
|
|
2524
|
+
MouseEvent: ["UIEvent", MOUSE_MEMBERS],
|
|
2525
|
+
PointerEvent: ["MouseEvent", ["pointerType"]],
|
|
2526
|
+
WheelEvent: ["MouseEvent", ["deltaX", "deltaY", "deltaMode"]],
|
|
2527
|
+
KeyboardEvent: ["UIEvent", ["key", "code", "keyCode", "charCode", "repeat", ...MODIFIERS]],
|
|
2528
|
+
FocusEvent: ["UIEvent", ["relatedTarget"]],
|
|
2529
|
+
InputEvent: ["UIEvent", ["data", "inputType"]],
|
|
2530
|
+
MessageEvent: ["Event", ["data", "origin", "lastEventId", "source", "ports"]],
|
|
2531
|
+
PopStateEvent: ["Event", ["state"]],
|
|
2532
|
+
HashChangeEvent: ["Event", ["oldURL", "newURL"]],
|
|
2533
|
+
};
|
|
2534
|
+
/** document.createEvent() interface names (DOM §4.5): legacy aliases included. */
|
|
2535
|
+
const CREATE_EVENT_TYPES = new Map([["event", "Event"], ["events", "Event"], ["htmlevents", "Event"],
|
|
2536
|
+
["customevent", "CustomEvent"], ["uievent", "UIEvent"], ["uievents", "UIEvent"], ["mouseevent", "MouseEvent"],
|
|
2537
|
+
["mouseevents", "MouseEvent"], ["keyboardevent", "KeyboardEvent"], ["focusevent", "FocusEvent"],
|
|
2538
|
+
["messageevent", "MessageEvent"], ["hashchangeevent", "HashChangeEvent"], ["popstateevent", "PopStateEvent"]]);
|
|
2539
|
+
|
|
1691
2540
|
const UI_EVENT_TYPES = ["UIEvent", "MouseEvent", "PointerEvent", "WheelEvent", "KeyboardEvent", "FocusEvent", "InputEvent"];
|
|
1692
2541
|
const UI_EVENT_FIELDS = [
|
|
1693
2542
|
...["detail", "clientX", "clientY", "screenX", "screenY", "button", "buttons", "deltaX", "deltaY", "deltaZ", "deltaMode",
|
|
1694
|
-
"pointerId", "width", "height", "pressure", "location"].map((key) => [key, Number]),
|
|
2543
|
+
"pointerId", "width", "height", "pressure", "location", "keyCode", "which", "charCode"].map((key) => [key, Number]),
|
|
1695
2544
|
...["key", "code", "data", "inputType", "pointerType"].map((key) => [key, String]),
|
|
1696
2545
|
...["ctrlKey", "altKey", "shiftKey", "metaKey", "repeat", "isComposing", "isPrimary"].map((key) => [key, Boolean]),
|
|
1697
2546
|
];
|
|
@@ -1714,8 +2563,32 @@ export function eventConstructors(realm) {
|
|
|
1714
2563
|
return realm.wrapEvent(new window.CustomEvent(String(type), init(options)), options?.detail);
|
|
1715
2564
|
}
|
|
1716
2565
|
realm.eventPrototype = mutableBindingPrototype(EventBinding.prototype);
|
|
2566
|
+
Object.defineProperty(realm.eventPrototype, "constructor", { value: Event, writable: true, configurable: true });
|
|
1717
2567
|
Event.prototype = realm.eventPrototype;
|
|
1718
|
-
|
|
2568
|
+
for (const [name, value] of Object.entries(EVENT_PHASES)) {
|
|
2569
|
+
Object.defineProperty(Event, name, { value, enumerable: true });
|
|
2570
|
+
Object.defineProperty(realm.eventPrototype, name, { value, enumerable: true });
|
|
2571
|
+
}
|
|
2572
|
+
// The event interfaces (UI Events, HTML): each its own prototype, with its
|
|
2573
|
+
// members moved off the shared binding prototype onto it.
|
|
2574
|
+
realm.eventPrototypes = new Map([["Event", realm.eventPrototype]]);
|
|
2575
|
+
const moved = new Set();
|
|
2576
|
+
for (const [name, [parentName, members]] of Object.entries(EVENT_INTERFACES)) {
|
|
2577
|
+
const prototype = Object.create(realm.eventPrototypes.get(parentName));
|
|
2578
|
+
for (const member of members) {
|
|
2579
|
+
const descriptor = Object.getOwnPropertyDescriptor(realm.eventPrototype, member);
|
|
2580
|
+
if (!descriptor) continue;
|
|
2581
|
+
Object.defineProperty(prototype, member, descriptor);
|
|
2582
|
+
moved.add(member);
|
|
2583
|
+
}
|
|
2584
|
+
Object.defineProperty(prototype, Symbol.toStringTag, { value: name, configurable: true });
|
|
2585
|
+
realm.eventPrototypes.set(name, prototype);
|
|
2586
|
+
}
|
|
2587
|
+
for (const member of moved) delete realm.eventPrototype[member];
|
|
2588
|
+
// An own property of each event instead (see RealmBindings.wrapEvent).
|
|
2589
|
+
delete realm.eventPrototype.isTrusted;
|
|
2590
|
+
CustomEvent.prototype = realm.eventPrototypes.get("CustomEvent");
|
|
2591
|
+
Object.defineProperty(CustomEvent.prototype, "constructor", { value: CustomEvent, writable: true, configurable: true });
|
|
1719
2592
|
// UI event subclasses: their init dictionaries are copied as primitives only
|
|
1720
2593
|
// (view and relatedTarget are dropped).
|
|
1721
2594
|
const uiEvents = {};
|
|
@@ -1731,10 +2604,46 @@ export function eventConstructors(realm) {
|
|
|
1731
2604
|
return realm.wrapEvent(new RealEvent(String(type), fields));
|
|
1732
2605
|
};
|
|
1733
2606
|
Object.defineProperty(Constructor, "name", { value: name });
|
|
1734
|
-
Constructor.prototype = realm.eventPrototype;
|
|
2607
|
+
Constructor.prototype = realm.eventPrototypes.get(name) ?? realm.eventPrototype;
|
|
2608
|
+
Object.defineProperty(Constructor.prototype, "constructor", { value: Constructor, writable: true, configurable: true });
|
|
1735
2609
|
uiEvents[name] = Constructor;
|
|
1736
2610
|
}
|
|
1737
|
-
|
|
2611
|
+
function MessageEvent(type, options = undefined) {
|
|
2612
|
+
if (!new.target) throw new TypeError("Constructor MessageEvent requires 'new'");
|
|
2613
|
+
return realm.messageEvent(type, {
|
|
2614
|
+
data: options?.data ?? null,
|
|
2615
|
+
origin: options?.origin ?? "",
|
|
2616
|
+
lastEventId: options?.lastEventId ?? "",
|
|
2617
|
+
source: options?.source ?? null,
|
|
2618
|
+
ports: Array.isArray(options?.ports) ? options.ports : [],
|
|
2619
|
+
}, init(options));
|
|
2620
|
+
}
|
|
2621
|
+
MessageEvent.prototype = realm.eventPrototypes.get("MessageEvent");
|
|
2622
|
+
Object.defineProperty(MessageEvent.prototype, "constructor", { value: MessageEvent, writable: true, configurable: true });
|
|
2623
|
+
// PromiseRejectionEvent (HTML §8.1.5.7): promise and reason stay page values in the binding.
|
|
2624
|
+
const rejections = new WeakMap();
|
|
2625
|
+
function PromiseRejectionEvent(type, options) {
|
|
2626
|
+
if (!new.target) throw new TypeError("Failed to construct 'PromiseRejectionEvent': Please use the 'new' operator, this DOM object constructor cannot be called as a function.");
|
|
2627
|
+
if (arguments.length < 2) {
|
|
2628
|
+
throw new TypeError(`Failed to construct 'PromiseRejectionEvent': 2 arguments required, but only ${arguments.length} present.`);
|
|
2629
|
+
}
|
|
2630
|
+
if (options?.promise === undefined) {
|
|
2631
|
+
throw new TypeError("Failed to construct 'PromiseRejectionEvent': Failed to read the 'promise' property from 'PromiseRejectionEventInit': Required member is undefined.");
|
|
2632
|
+
}
|
|
2633
|
+
const wrapper = realm.wrapEvent(new window.Event(String(type), init(options)));
|
|
2634
|
+
Object.setPrototypeOf(wrapper, PromiseRejectionEvent.prototype);
|
|
2635
|
+
rejections.set(wrapper, { promise: options.promise, reason: options.reason });
|
|
2636
|
+
return wrapper;
|
|
2637
|
+
}
|
|
2638
|
+
PromiseRejectionEvent.prototype = Object.create(realm.eventPrototype, {
|
|
2639
|
+
constructor: { value: PromiseRejectionEvent, writable: true, configurable: true },
|
|
2640
|
+
promise: { get: Object.getOwnPropertyDescriptor({ get promise() { return rejections.get(this)?.promise; } }, "promise").get,
|
|
2641
|
+
enumerable: true, configurable: true },
|
|
2642
|
+
reason: { get: Object.getOwnPropertyDescriptor({ get reason() { return rejections.get(this)?.reason; } }, "reason").get,
|
|
2643
|
+
enumerable: true, configurable: true },
|
|
2644
|
+
[Symbol.toStringTag]: { value: "PromiseRejectionEvent", configurable: true },
|
|
2645
|
+
});
|
|
2646
|
+
return { Event, CustomEvent, MessageEvent, PromiseRejectionEvent, ...uiEvents };
|
|
1738
2647
|
}
|
|
1739
2648
|
|
|
1740
2649
|
function domRect({ left, top, width, height }) {
|
|
@@ -1755,12 +2664,29 @@ function cssEscape(value) {
|
|
|
1755
2664
|
* Freezes the binding classes so no page can change the shared prototypes.
|
|
1756
2665
|
* Must run after SES lockdown() (which provides `harden`).
|
|
1757
2666
|
*/
|
|
2667
|
+
const BINDINGS = [NodeBinding, CharacterDataBinding, TextBinding, ParentBinding, FragmentBinding, ShadowRootBinding, ElementBinding,
|
|
2668
|
+
DocumentBinding, EventBinding, ClassListBinding, TreeWalkerBinding, RangeBinding];
|
|
2669
|
+
|
|
2670
|
+
// Members defined with Object.defineProperty({ get() {...} }) are named
|
|
2671
|
+
// "get"; Web IDL names an attribute's accessors "get x" / "set x" and an
|
|
2672
|
+
// operation after itself. Named here, while the functions are still mutable.
|
|
2673
|
+
for (const Binding of BINDINGS) {
|
|
2674
|
+
for (const key of Reflect.ownKeys(Binding.prototype)) {
|
|
2675
|
+
if (key === "constructor") continue;
|
|
2676
|
+
const name = typeof key === "symbol" ? `[${key.description ?? ""}]` : key;
|
|
2677
|
+
const descriptor = Object.getOwnPropertyDescriptor(Binding.prototype, key);
|
|
2678
|
+
for (const [fn, expected] of [[descriptor.get, `get ${name}`], [descriptor.set, `set ${name}`], [descriptor.value, name]]) {
|
|
2679
|
+
if (typeof fn !== "function" || fn.name === expected) continue;
|
|
2680
|
+
if (Object.getOwnPropertyDescriptor(fn, "name")?.configurable !== false) {
|
|
2681
|
+
Object.defineProperty(fn, "name", { value: expected, configurable: true });
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
1758
2687
|
export function hardenBindings() {
|
|
1759
2688
|
if (typeof harden !== "function") return;
|
|
1760
|
-
for (const Binding of
|
|
1761
|
-
DocumentBinding, EventBinding, ClassListBinding, TreeWalkerBinding]) {
|
|
1762
|
-
harden(Binding);
|
|
1763
|
-
}
|
|
2689
|
+
for (const Binding of BINDINGS) harden(Binding);
|
|
1764
2690
|
}
|
|
1765
2691
|
|
|
1766
2692
|
/** Interface objects for `instanceof` checks; all throw "Illegal constructor" when called. */
|