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