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.
Files changed (90) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +74 -9
  3. package/ai-debug.log +5537 -0
  4. package/bunfig.toml +1 -0
  5. package/buninu-browser.js +4 -0
  6. package/index.js +5 -0
  7. package/lib/audio/piano.js +24 -1
  8. package/lib/audio/playback-buffer.js +27 -0
  9. package/lib/audio/pulse-client.js +137 -19
  10. package/lib/cdp-server.js +22 -11
  11. package/lib/happy-dom/parser.js +29 -4
  12. package/lib/happy-dom/selector-pseudo-adapter.js +14 -0
  13. package/lib/headless-shell.js +69 -19
  14. package/lib/input/hit-test.js +25 -7
  15. package/lib/layout/text-layout.js +268 -85
  16. package/lib/log.js +5 -2
  17. package/lib/media/byte-pipe.js +82 -0
  18. package/lib/media/position-audio.js +42 -0
  19. package/lib/media/video-keyframe.js +29 -0
  20. package/lib/network/network-service.js +161 -14
  21. package/lib/paint/display-list.js +316 -15
  22. package/lib/paint/gradient.js +11 -5
  23. package/lib/render-tree/index.js +19 -2
  24. package/lib/render-tree/svg.js +2 -1
  25. package/lib/renderer/bindings.js +995 -69
  26. package/lib/renderer/canvas-2d.js +92 -0
  27. package/lib/renderer/compositor.js +16 -1
  28. package/lib/renderer/custom-element-reactions.js +18 -0
  29. package/lib/renderer/history.js +42 -0
  30. package/lib/renderer/host.js +166 -9
  31. package/lib/renderer/image-loading.js +27 -0
  32. package/lib/renderer/interfaces.js +215 -0
  33. package/lib/renderer/keyboard-input.js +12 -0
  34. package/lib/renderer/main.js +162 -12
  35. package/lib/renderer/media-playback-events.js +13 -0
  36. package/lib/renderer/media-source.js +819 -0
  37. package/lib/renderer/messaging.js +411 -0
  38. package/lib/renderer/native-functions.js +113 -0
  39. package/lib/renderer/page-realm.js +1408 -242
  40. package/lib/renderer/page-renderer.js +1893 -223
  41. package/lib/renderer/piano-input.js +22 -0
  42. package/lib/renderer/pointer-move.js +29 -0
  43. package/lib/renderer/rejections.js +31 -0
  44. package/lib/renderer/rewrite-worker.js +22 -0
  45. package/lib/renderer/script-rewrite.js +805 -57
  46. package/lib/renderer/trusted-types.js +129 -0
  47. package/lib/renderer/video-presentation.js +126 -0
  48. package/lib/renderer/workers.js +327 -0
  49. package/lib/screenshot-cli.js +2 -0
  50. package/lib/style/computed-style.js +186 -29
  51. package/lib/user-agent.js +14 -2
  52. package/package.json +4 -1
  53. package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
  54. package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
  55. package/patches/happy-dom/README.md +26 -0
  56. package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
  57. package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
  58. package/patches/ses/README.md +4 -0
  59. package/test/canvas-2d.test.js +55 -0
  60. package/test/compositor.test.js +41 -1
  61. package/test/computed-style.test.js +59 -1
  62. package/test/display-list.test.js +88 -1
  63. package/test/download.test.js +43 -0
  64. package/test/fixtures/long-video.mp4 +0 -0
  65. package/test/fixtures/media-sync.js +57 -0
  66. package/test/happy-dom-platform-adapters.test.js +25 -0
  67. package/test/headless-shell.test.js +23 -1
  68. package/test/hit-test.test.js +25 -1
  69. package/test/media-playback-events.test.js +22 -0
  70. package/test/media-source.test.js +156 -0
  71. package/test/media-sync.test.js +12 -0
  72. package/test/mobile.test.js +2 -2
  73. package/test/navigation.test.js +61 -0
  74. package/test/network-service.test.js +14 -0
  75. package/test/page-scripts.test.js +1203 -7
  76. package/test/piano-input.test.js +44 -0
  77. package/test/playback-buffer.test.js +31 -0
  78. package/test/position-audio.test.js +67 -0
  79. package/test/positioning.test.js +95 -1
  80. package/test/pulse-client.test.js +84 -0
  81. package/test/renderer-host.test.js +5 -2
  82. package/test/screenshot-cli.test.js +25 -1
  83. package/test/script-rewrite.test.js +111 -6
  84. package/test/setup.js +5 -0
  85. package/test/text-layout.test.js +62 -0
  86. package/test/user-agent.test.js +60 -4
  87. package/test/video-presentation.test.js +67 -0
  88. package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
  89. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
  90. package/vendor/ses/dist/ses.mjs +52 -6
@@ -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) Object.setPrototypeOf(wrapper, 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
- ownKeys: () => [...Array.from({ length: collection.length }, (_, index) => String(index)), "length"],
127
- set: () => false,
128
- defineProperty: () => false,
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) Object.setPrototypeOf(wrapper, 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 content = nodeOf(this).content;
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
- toggleAttribute(name, force) {
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
- const { viewport } = realmOf(this).hooks;
775
- return Math.round(this.getBoundingClientRect().left + viewport().scrollX);
1082
+ return offsetCoordinate(this, "left");
776
1083
  }
777
1084
 
778
1085
  get offsetTop() {
779
- const { viewport } = realmOf(this).hooks;
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
- return root ? realmOf(this).hooks.viewport().width : this.offsetWidth;
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
- return root ? realmOf(this).hooks.viewport().height : this.offsetHeight;
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
- // Overflow inside elements is not scrollable here, so the scroll size is the
849
- // box size; the root element spans at least the viewport.
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
- createEvent(_type) {
1087
- return realmOf(this).wrapEvent(new (realmOf(this).window.Event)(""));
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 Boolean(this.#event.isTrusted);
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
- return this.#event.data ?? null;
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
- * element.style: a proxy that reads and writes the real declaration block
1521
- * with string values only. Mutations go to the style attribute, which the
1522
- * style engine reads.
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 styleBinding(node, realm) {
1525
- const style = node.style;
1526
- const toKebab = (property) => property.startsWith("--") ? property
1527
- : property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
1528
- const methods = {
1529
- getPropertyValue: (property) => style.getPropertyValue(String(property)),
1530
- setProperty: (property, value, priority = "") => {
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) realm.hooks.touch(node);
2170
+ if (style.cssText !== before) owner.hooks.touch(node);
1534
2171
  },
1535
- removeProperty: (property) => {
2172
+ removeProperty(property) {
2173
+ const { style, node, realm: owner } = styleState(this);
1536
2174
  const previous = style.removeProperty(String(property));
1537
- realm.hooks.touch(node);
2175
+ owner.hooks.touch(node);
1538
2176
  return previous;
1539
2177
  },
1540
2178
  };
1541
- return new Proxy(Object.create(null), {
1542
- get(_target, property) {
1543
- if (typeof property !== "string") return undefined;
1544
- if (Object.hasOwn(methods, property)) return methods[property];
1545
- if (property === "cssText") return style.cssText;
1546
- if (property === "length") return style.length;
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(_target, property, value) {
1550
- if (typeof property !== "string") return false;
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
- if (property === "cssText") style.cssText = String(value);
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 false;
2214
+ if (typeof property !== "string") return property in prototype;
1559
2215
  const camel = property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
1560
- return Object.hasOwn(methods, property) || property === "cssText" || property === "length"
1561
- || (property in style && !UNRENDERED_STYLE_PROPERTIES.has(camel));
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, options);
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 === "boolean" ? options : Boolean(options?.capture);
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
- CustomEvent.prototype = realm.eventPrototype;
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
- return { Event, CustomEvent, ...uiEvents };
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 [NodeBinding, CharacterDataBinding, TextBinding, ParentBinding, FragmentBinding, ShadowRootBinding, ElementBinding,
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. */