buninu-browser 0.2.2 → 0.2.4

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 (118) hide show
  1. package/lib/audio/playback-buffer.js +27 -0
  2. package/lib/audio/pulse-client.js +18 -8
  3. package/lib/cdp-server.js +53 -14
  4. package/lib/happy-dom/document-base.js +33 -0
  5. package/lib/happy-dom/parser.js +20 -6
  6. package/lib/happy-dom/selector-matcher.js +2 -2
  7. package/lib/happy-dom/selector-pseudo-adapter.js +33 -0
  8. package/lib/happy-dom/svg-attribute-adapter.js +50 -0
  9. package/lib/headless-shell.js +12 -6
  10. package/lib/input/hit-test.js +67 -19
  11. package/lib/layout/text-layout.js +289 -131
  12. package/lib/media/byte-pipe.js +2 -2
  13. package/lib/media/position-audio.js +10 -1
  14. package/lib/media/source-snapshots.js +126 -0
  15. package/lib/media/video-keyframe.js +42 -0
  16. package/lib/media/webm-stream.js +106 -0
  17. package/lib/network/network-service.js +268 -120
  18. package/lib/paint/display-list.js +22 -27
  19. package/lib/paint/frame-cache.js +25 -0
  20. package/lib/paint/frame.js +42 -13
  21. package/lib/paint/paragraph-text.js +24 -0
  22. package/lib/paint/raster-readback.js +41 -0
  23. package/lib/paint/rounded-rect.js +82 -0
  24. package/lib/paint/shared-frame-pool.js +105 -0
  25. package/lib/render-tree/index.js +40 -6
  26. package/lib/render-tree/svg.js +15 -7
  27. package/lib/renderer/bindings.js +244 -82
  28. package/lib/renderer/broadcast-channel.js +76 -0
  29. package/lib/renderer/canvas-2d.js +257 -14
  30. package/lib/renderer/canvas-text.js +120 -0
  31. package/lib/renderer/compositor.js +27 -10
  32. package/lib/renderer/custom-element-reactions.js +42 -0
  33. package/lib/renderer/font-loading.js +227 -0
  34. package/lib/renderer/history.js +42 -0
  35. package/lib/renderer/host.js +36 -5
  36. package/lib/renderer/image-loading.js +27 -0
  37. package/lib/renderer/image-resources.js +65 -0
  38. package/lib/renderer/interfaces.js +28 -0
  39. package/lib/renderer/keyboard-input.js +12 -0
  40. package/lib/renderer/layout-observers.js +236 -0
  41. package/lib/renderer/main.js +33 -5
  42. package/lib/renderer/media-playback-events.js +13 -0
  43. package/lib/renderer/media-source.js +126 -55
  44. package/lib/renderer/page-realm.js +278 -147
  45. package/lib/renderer/page-renderer.js +840 -301
  46. package/lib/renderer/piano-input.js +22 -0
  47. package/lib/renderer/pointer-move.js +29 -0
  48. package/lib/renderer/prepared-script-cache.js +50 -0
  49. package/lib/renderer/rendered-text.js +108 -0
  50. package/lib/renderer/rewrite-worker.js +4 -3
  51. package/lib/renderer/script-rewrite.js +13 -2
  52. package/lib/renderer/scroll-into-view.js +28 -0
  53. package/lib/renderer/slot-assignment.js +60 -0
  54. package/lib/renderer/video-presentation.js +145 -0
  55. package/lib/renderer/workers.js +2 -2
  56. package/lib/style/computed-style.js +632 -108
  57. package/lib/style/selector-traits.js +49 -5
  58. package/package.json +1 -1
  59. package/test/broadcast-channel.test.js +26 -0
  60. package/test/canvas-2d.test.js +83 -2
  61. package/test/canvas-paint.test.js +9 -0
  62. package/test/canvas-text.test.js +41 -0
  63. package/test/cdp-stream.test.js +56 -0
  64. package/test/compositor.test.js +38 -0
  65. package/test/computed-style-binding.test.js +151 -0
  66. package/test/computed-style.test.js +279 -1
  67. package/test/display-list.test.js +26 -0
  68. package/test/document-base.test.js +87 -0
  69. package/test/dom-platform.test.js +461 -0
  70. package/test/download.test.js +8 -0
  71. package/test/fetch-lifecycle.test.js +161 -0
  72. package/test/fixtures/canvas-paint.js +117 -0
  73. package/test/fixtures/canvas-text.js +42 -0
  74. package/test/fixtures/image-lifetime.js +90 -0
  75. package/test/fixtures/media-seek-snapshot.js +77 -0
  76. package/test/fixtures/mse-audio-chirp.webm +0 -0
  77. package/test/fixtures/mse-switch-large.mp4 +0 -0
  78. package/test/fixtures/mse-switch-small.mp4 +0 -0
  79. package/test/fixtures/renderer-memory.js +110 -0
  80. package/test/fixtures/summary-style.js +28 -0
  81. package/test/font-loading.test.js +97 -0
  82. package/test/grid.test.js +58 -0
  83. package/test/headless-shell.test.js +22 -0
  84. package/test/hit-test.test.js +21 -1
  85. package/test/image-lifetime.test.js +12 -0
  86. package/test/image-resources.test.js +94 -0
  87. package/test/local-media.test.js +84 -0
  88. package/test/media-playback-events.test.js +22 -0
  89. package/test/media-seek-snapshot.test.js +11 -0
  90. package/test/media-source-config.test.js +98 -0
  91. package/test/media-source-webm-accounting.test.js +98 -0
  92. package/test/media-source-webm.test.js +108 -0
  93. package/test/navigation.test.js +1 -0
  94. package/test/network-service.test.js +388 -0
  95. package/test/page-scripts.test.js +160 -2
  96. package/test/paint-frame.test.js +103 -0
  97. package/test/paint-paragraph-cache.test.js +87 -0
  98. package/test/piano-input.test.js +44 -0
  99. package/test/playback-buffer.test.js +31 -0
  100. package/test/position-audio.test.js +24 -0
  101. package/test/positioning.test.js +22 -0
  102. package/test/prepared-script-cache.test.js +45 -0
  103. package/test/pulse-client.test.js +27 -5
  104. package/test/raster-readback.test.js +40 -0
  105. package/test/render-tree.test.js +28 -0
  106. package/test/renderer-memory.test.js +11 -0
  107. package/test/selector-traits.test.js +24 -0
  108. package/test/shared-frame-pool.test.js +63 -0
  109. package/test/style-sheets.test.js +51 -0
  110. package/test/svg.test.js +29 -0
  111. package/test/text-layout.test.js +168 -0
  112. package/test/video-keyframe.test.js +61 -0
  113. package/test/video-presentation.test.js +136 -0
  114. package/tools/browser-lab.js +78 -0
  115. package/tools/browser-soak.js +75 -0
  116. package/tools/network-reference.js +49 -0
  117. package/tools/process-memory.js +38 -0
  118. package/tools/script-benchmark.js +25 -0
@@ -10,12 +10,21 @@
10
10
  *
11
11
  * Coverage is the commonly used subset of DOM, not the full WebIDL surface.
12
12
  */
13
+ import { renderedText, setRenderedText } from "./rendered-text.js";
14
+ import { assignedSlot, slotAssignedNodes, assignSlotNodes } from "./slot-assignment.js";
15
+ import { scrollIntoViewOptions, scrollAlignmentDelta } from "./scroll-into-view.js";
16
+ import { documentBaseURL } from "../happy-dom/document-base.js";
17
+ import { resetCanvas } from "./canvas-2d.js";
13
18
 
14
19
  const CONSTRUCT = Symbol("binding construction key");
20
+ const isCanvasSizeAttribute = (node, name, namespace = null) => !namespace
21
+ && node.namespaceURI === "http://www.w3.org/1999/xhtml" && node.tagName === "CANVAS"
22
+ && (name === "width" || name === "height");
15
23
  const REFLECTED_PROPERTIES = [
16
24
  "value", "checked", "disabled", "type", "name", "href", "src", "alt", "title",
17
25
  "placeholder", "selected", "readOnly", "required", "min", "max", "step", "rel", "target",
18
26
  "htmlFor", "lang", "dir", "tabIndex", "hidden", "defaultValue", "action", "method",
27
+ "controls", "autoplay", "loop", "playsInline", "preload", "poster",
19
28
  ];
20
29
 
21
30
  /** Per-document state shared by that document's wrappers. */
@@ -41,6 +50,9 @@ export class BindingRealm {
41
50
  textPrototype = null;
42
51
  characterDataPrototype = null;
43
52
  nodePrototype = null;
53
+ datasetPrototype = null;
54
+ wrapValidity = null;
55
+ documentPrototype = null;
44
56
 
45
57
  /**
46
58
  * @param {object} window the Happy DOM window of the document
@@ -87,6 +99,7 @@ export class BindingRealm {
87
99
  if (Binding === TextBinding && this.textPrototype) Object.setPrototypeOf(wrapper, this.textPrototype);
88
100
  if (Binding === CharacterDataBinding && this.characterDataPrototype) Object.setPrototypeOf(wrapper, this.characterDataPrototype);
89
101
  if (Binding === NodeBinding && this.nodePrototype) Object.setPrototypeOf(wrapper, this.nodePrototype);
102
+ if (Binding === DocumentBinding && this.documentPrototype) Object.setPrototypeOf(wrapper, this.documentPrototype);
90
103
  this.#wrappers.set(node, wrapper);
91
104
  }
92
105
  return wrapper;
@@ -209,7 +222,7 @@ export class BindingRealm {
209
222
  if (!wrapper) {
210
223
  wrapper = new EventBinding(CONSTRUCT, event, this, detail);
211
224
  if (this.eventPrototype) {
212
- const prototype = this.eventPrototypes?.get(event.constructor?.name) ?? this.eventPrototype;
225
+ const prototype = this.eventPrototypes?.get(this.#messages.has(event) ? "MessageEvent" : event.constructor?.name) ?? this.eventPrototype;
213
226
  Object.setPrototypeOf(wrapper, prototype);
214
227
  // isTrusted is [LegacyUnforgeable]: an own accessor of every event.
215
228
  const isTrusted = Object.getOwnPropertyDescriptor(EventBinding.prototype, "isTrusted");
@@ -347,15 +360,7 @@ class NodeBinding {
347
360
 
348
361
  /** The document base URL (HTML §2.4.1): the first <base href>, else the document's URL. */
349
362
  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
- }
363
+ return documentBaseURL(this.#node);
359
364
  }
360
365
 
361
366
  get nodeValue() {
@@ -553,7 +558,9 @@ class CharacterDataBinding extends NodeBinding {
553
558
  }
554
559
  }
555
560
 
556
- class TextBinding extends CharacterDataBinding {}
561
+ class TextBinding extends CharacterDataBinding {
562
+ get assignedSlot() { return realmOf(this).wrap(assignedSlot(nodeOf(this))); }
563
+ }
557
564
 
558
565
  /** Methods shared by elements, documents and fragments (ParentNode). */
559
566
  class ParentBinding extends NodeBinding {
@@ -634,6 +641,9 @@ class ShadowRootBinding extends FragmentBinding {
634
641
  return nodeOf(this).mode;
635
642
  }
636
643
 
644
+ get delegatesFocus() { return nodeOf(this).delegatesFocus; }
645
+ get slotAssignment() { return nodeOf(this).slotAssignment; }
646
+
637
647
  get innerHTML() {
638
648
  return nodeOf(this).innerHTML;
639
649
  }
@@ -649,20 +659,49 @@ class ElementBinding extends ParentBinding {
649
659
  #classList = null;
650
660
  #dataset = null;
651
661
  #attributes = null;
662
+ #validity = null;
663
+ #clickInProgress = false;
652
664
 
653
665
  get tagName() {
654
666
  return nodeOf(this).tagName;
655
667
  }
656
668
 
657
669
  attachShadow(init) {
658
- const mode = init?.mode === "closed" ? "closed" : "open";
659
- return realmOf(this).wrap(nodeOf(this).attachShadow({ mode }));
670
+ const mode = String(init?.mode);
671
+ if (mode !== "open" && mode !== "closed") throw new TypeError("Shadow root mode must be open or closed");
672
+ const slotAssignment = String(init?.slotAssignment ?? "named");
673
+ if (slotAssignment !== "named" && slotAssignment !== "manual") throw new TypeError("Invalid slot assignment mode");
674
+ // Happy DOM spells this internal option delegateFocus (without the s).
675
+ return realmOf(this).wrap(nodeOf(this).attachShadow({ mode, slotAssignment, delegateFocus: Boolean(init?.delegatesFocus) }));
660
676
  }
661
677
 
662
678
  get shadowRoot() {
663
679
  return realmOf(this).wrap(nodeOf(this).shadowRoot);
664
680
  }
665
681
 
682
+ get assignedSlot() { return realmOf(this).wrap(assignedSlot(nodeOf(this))); }
683
+
684
+ get slot() { return nodeOf(this).getAttribute("slot") ?? ""; }
685
+ set slot(value) { this.setAttribute("slot", String(value)); }
686
+
687
+ assignedNodes(options = {}) {
688
+ const realm = realmOf(this);
689
+ return slotAssignedNodes(nodeOf(this), Boolean(options?.flatten)).map(node => realm.wrap(node));
690
+ }
691
+
692
+ assignedElements(options = {}) {
693
+ const realm = realmOf(this);
694
+ return slotAssignedNodes(nodeOf(this), Boolean(options?.flatten)).filter(node => node.nodeType === 1).map(node => realm.wrap(node));
695
+ }
696
+
697
+ assign(...nodes) {
698
+ const realm = realmOf(this);
699
+ assignSlotNodes(nodeOf(this), nodes.map(node => nodeOf(node)), slot => {
700
+ queueMicrotask(() => slot.dispatchEvent(new realm.window.Event("slotchange", { bubbles: true })));
701
+ });
702
+ touch(this);
703
+ }
704
+
666
705
  // HTMLIFrameElement: its document's window and, when same-origin, its
667
706
  // document (see messaging.js).
668
707
  get contentWindow() {
@@ -746,6 +785,9 @@ class ElementBinding extends ParentBinding {
746
785
  return nodeOf(this).localName;
747
786
  }
748
787
 
788
+ get namespaceURI() { return nodeOf(this).namespaceURI; }
789
+ get prefix() { return nodeOf(this).prefix; }
790
+
749
791
  get id() {
750
792
  return nodeOf(this).id;
751
793
  }
@@ -782,6 +824,11 @@ class ElementBinding extends ParentBinding {
782
824
  return this.#style;
783
825
  }
784
826
 
827
+ // ElementCSSInlineStyle is [PutForwards=cssText], not assignment-readonly.
828
+ set style(value) {
829
+ this.style.cssText = value;
830
+ }
831
+
785
832
  get dataset() {
786
833
  this.#dataset ??= datasetBinding(nodeOf(this), realmOf(this));
787
834
  return this.#dataset;
@@ -833,18 +880,23 @@ class ElementBinding extends ParentBinding {
833
880
  const attribute = element.getAttributeNode(String(name));
834
881
  if (!attribute) return null;
835
882
  const ownerElement = this;
883
+ const setValue = value => {
884
+ attribute.value = String(value);
885
+ if (attribute.ownerElement === element && isCanvasSizeAttribute(element, attribute.localName, attribute.namespaceURI)) resetCanvas(element);
886
+ touch(ownerElement);
887
+ };
836
888
  return {
837
889
  get name() { return attribute.name; },
838
890
  get localName() { return attribute.localName; },
839
891
  get namespaceURI() { return attribute.namespaceURI; },
840
892
  get prefix() { return attribute.prefix; },
841
893
  get value() { return attribute.value; },
842
- set value(value) { attribute.value = String(value); touch(ownerElement); },
894
+ set value(value) { setValue(value); },
843
895
  get nodeName() { return attribute.name; },
844
896
  get nodeValue() { return attribute.value; },
845
- set nodeValue(value) { attribute.value = String(value ?? ""); touch(ownerElement); },
897
+ set nodeValue(value) { setValue(value ?? ""); },
846
898
  get textContent() { return attribute.value; },
847
- set textContent(value) { attribute.value = String(value ?? ""); touch(ownerElement); },
899
+ set textContent(value) { setValue(value ?? ""); },
848
900
  ownerElement,
849
901
  specified: true,
850
902
  nodeType: 2,
@@ -855,8 +907,10 @@ class ElementBinding extends ParentBinding {
855
907
  const node = nodeOf(this);
856
908
  const attribute = String(name);
857
909
  const next = String(value);
858
- if (node.getAttribute(attribute) !== next) {
910
+ const resetsCanvas = isCanvasSizeAttribute(node, attribute.toLowerCase());
911
+ if (node.getAttribute(attribute) !== next || resetsCanvas) {
859
912
  node.setAttribute(attribute, next);
913
+ if (resetsCanvas) resetCanvas(node);
860
914
  touch(this);
861
915
  }
862
916
  }
@@ -866,6 +920,7 @@ class ElementBinding extends ParentBinding {
866
920
  const attribute = String(name);
867
921
  if (node.hasAttribute(attribute)) {
868
922
  node.removeAttribute(attribute);
923
+ if (isCanvasSizeAttribute(node, attribute.toLowerCase())) resetCanvas(node);
869
924
  touch(this);
870
925
  }
871
926
  }
@@ -888,17 +943,25 @@ class ElementBinding extends ParentBinding {
888
943
  }
889
944
 
890
945
  setAttributeNS(namespace, qualifiedName, value) {
891
- nodeOf(this).setAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(qualifiedName), String(value));
946
+ const node = nodeOf(this), ns = namespace === undefined ? null : namespace && String(namespace), name = String(qualifiedName);
947
+ node.setAttributeNS(ns, name, String(value));
948
+ if (isCanvasSizeAttribute(node, name, ns)) resetCanvas(node);
892
949
  touch(this);
893
950
  }
894
951
 
895
952
  removeAttributeNS(namespace, localName) {
896
- nodeOf(this).removeAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(localName));
953
+ const node = nodeOf(this), ns = namespace === undefined ? null : namespace && String(namespace), name = String(localName);
954
+ const existed = node.hasAttributeNS(ns, name);
955
+ node.removeAttributeNS(ns, name);
956
+ if (existed && isCanvasSizeAttribute(node, name, ns)) resetCanvas(node);
897
957
  touch(this);
898
958
  }
899
959
 
900
960
  toggleAttribute(name, force = undefined) {
901
- const result = nodeOf(this).toggleAttribute(String(name), force === undefined ? undefined : Boolean(force));
961
+ const node = nodeOf(this), attribute = String(name);
962
+ const existed = node.hasAttribute(attribute);
963
+ const result = node.toggleAttribute(attribute, force === undefined ? undefined : Boolean(force));
964
+ if (existed !== result && isCanvasSizeAttribute(node, attribute.toLowerCase())) resetCanvas(node);
902
965
  touch(this);
903
966
  return result;
904
967
  }
@@ -922,11 +985,13 @@ class ElementBinding extends ParentBinding {
922
985
  }
923
986
 
924
987
  get innerText() {
925
- return nodeOf(this).textContent;
988
+ const node = nodeOf(this);
989
+ const realm = realmOf(this);
990
+ return node.ownerDocument !== realm.window.document ? node.textContent : renderedText(node, realm.hooks.computedStyle);
926
991
  }
927
992
 
928
993
  set innerText(value) {
929
- nodeOf(this).textContent = String(value);
994
+ setRenderedText(nodeOf(this), value);
930
995
  touch(this);
931
996
  }
932
997
 
@@ -992,11 +1057,18 @@ class ElementBinding extends ParentBinding {
992
1057
  click() {
993
1058
  const realm = realmOf(this);
994
1059
  const element = nodeOf(this);
995
- const event = new realm.window.MouseEvent("click", { bubbles: true, cancelable: true });
996
- element.dispatchEvent(event);
997
- if (!event.defaultPrevented) {
998
- const anchor = element.closest?.("a[href]");
999
- if (anchor) realm.hooks.activateLink?.(anchor);
1060
+ if (this.#clickInProgress || element.matches(":disabled")) return;
1061
+ const anchor = element.closest?.("a[href]");
1062
+ const Event = realm.window.PointerEvent ?? realm.window.MouseEvent;
1063
+ const event = new Event("click", { bubbles: true, cancelable: true, composed: true });
1064
+ const activationState = realm.hooks.activationState?.(element);
1065
+ this.#clickInProgress = true;
1066
+ try {
1067
+ element.dispatchEvent(event);
1068
+ if (!event.defaultPrevented && anchor?.hasAttribute("href")) realm.hooks.activateLink?.(anchor);
1069
+ } finally {
1070
+ this.#clickInProgress = false;
1071
+ realm.hooks.activationChanged?.(activationState);
1000
1072
  }
1001
1073
  }
1002
1074
 
@@ -1010,21 +1082,42 @@ class ElementBinding extends ParentBinding {
1010
1082
  const form = nodeOf(this);
1011
1083
  if (form.tagName !== "FORM") return;
1012
1084
  const realSubmitter = submitter == null ? null : nodeOf(submitter);
1085
+ if (realSubmitter && !((realSubmitter.tagName === "BUTTON" && realSubmitter.type === "submit")
1086
+ || (realSubmitter.tagName === "INPUT" && ["submit", "image"].includes(realSubmitter.type)))) {
1087
+ throw new TypeError("The submitter must be a submit button");
1088
+ }
1013
1089
  if (realSubmitter && realSubmitter.form !== form) throw new DOMException("Submitter is not owned by this form", "NotFoundError");
1014
- if (!form.checkValidity()) return;
1090
+ if (!form.noValidate && !realSubmitter?.formNoValidate && !form.checkValidity()) return;
1015
1091
  const event = new realm.window.SubmitEvent("submit", {
1016
- bubbles: true, cancelable: true, submitter: realSubmitter ?? form,
1092
+ bubbles: true, cancelable: true, submitter: realSubmitter,
1017
1093
  });
1018
1094
  form.dispatchEvent(event);
1019
1095
  if (!event.defaultPrevented) realm.hooks.submitForm?.(form, realSubmitter);
1020
1096
  }
1021
1097
 
1098
+ get validity() { return this.#validity ??= realmOf(this).wrapValidity(nodeOf(this).validity); }
1099
+
1100
+ setCustomValidity(message) {
1101
+ if (!arguments.length) throw new TypeError("setCustomValidity requires a message");
1102
+ nodeOf(this).setCustomValidity(String(message));
1103
+ touch(this, [":state"]);
1104
+ }
1105
+
1106
+ checkValidity() { return nodeOf(this).checkValidity(); }
1107
+ reportValidity() { return nodeOf(this).reportValidity(); }
1108
+
1022
1109
  focus() {
1023
- nodeOf(this).focus();
1110
+ const node = nodeOf(this);
1111
+ const previous = node.ownerDocument.activeElement;
1112
+ node.focus();
1113
+ if (node.ownerDocument.activeElement !== previous) touch(this, [":focus", ":focus-visible"]);
1024
1114
  }
1025
1115
 
1026
1116
  blur() {
1027
- nodeOf(this).blur();
1117
+ const node = nodeOf(this);
1118
+ const previous = node.ownerDocument.activeElement;
1119
+ node.blur();
1120
+ if (node.ownerDocument.activeElement !== previous) touch(this, [":focus", ":focus-visible"]);
1028
1121
  }
1029
1122
 
1030
1123
  getBoundingClientRect() {
@@ -1033,7 +1126,10 @@ class ElementBinding extends ParentBinding {
1033
1126
  }
1034
1127
 
1035
1128
  getClientRects() {
1036
- return [this.getBoundingClientRect()];
1129
+ const realm = realmOf(this), node = nodeOf(this);
1130
+ if (!node.isConnected || realm.hooks.computedStyle?.(node)?.display === "contents") return [];
1131
+ const rect = realm.hooks.boundsOf(node);
1132
+ return rect ? [domRect(rect)] : [];
1037
1133
  }
1038
1134
 
1039
1135
  get offsetWidth() {
@@ -1211,15 +1307,17 @@ class ElementBinding extends ParentBinding {
1211
1307
  return Math.max(this.offsetHeight, root ? realmOf(this).hooks.viewport().height : 0);
1212
1308
  }
1213
1309
 
1214
- scrollIntoView() {
1310
+ scrollIntoView(options = undefined) {
1215
1311
  const realm = realmOf(this);
1312
+ const normalized = scrollIntoViewOptions(options);
1216
1313
  if (realm.hooks.scrollIntoView) {
1217
- realm.hooks.scrollIntoView(nodeOf(this));
1314
+ realm.hooks.scrollIntoView(nodeOf(this), normalized);
1218
1315
  return;
1219
1316
  }
1220
1317
  const rect = this.getBoundingClientRect();
1221
- const { scrollX, scrollY } = realm.hooks.viewport();
1222
- realm.hooks.scrollTo(scrollX, scrollY + rect.top);
1318
+ const { scrollX, scrollY, width, height } = realm.hooks.viewport();
1319
+ realm.hooks.scrollTo(scrollX + scrollAlignmentDelta(rect.left, rect.width, width, normalized.inline),
1320
+ scrollY + scrollAlignmentDelta(rect.top, rect.height, height, normalized.block));
1223
1321
  }
1224
1322
  }
1225
1323
 
@@ -1266,7 +1364,7 @@ export const EVENT_HANDLER_NAMES = Object.freeze([
1266
1364
  "click", "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave",
1267
1365
  "keydown", "keyup", "keypress", "beforeinput", "input", "change", "submit", "reset", "focus", "blur", "load", "error",
1268
1366
  "abort", "scroll", "resize", "wheel", "contextmenu", "touchstart", "touchend", "touchmove",
1269
- "pointerdown", "pointerup", "pointermove", "animationend", "transitionend", "readystatechange",
1367
+ "pointerdown", "pointerup", "pointermove", "animationend", "transitionend", "readystatechange", "slotchange", "toggle", "invalid",
1270
1368
  ]);
1271
1369
 
1272
1370
  /**
@@ -1310,7 +1408,16 @@ for (const property of REFLECTED_PROPERTIES) {
1310
1408
  configurable: true,
1311
1409
  enumerable: true,
1312
1410
  get() {
1313
- const value = nodeOf(this)[property];
1411
+ const node = nodeOf(this);
1412
+ const value = node[property];
1413
+ if (typeof value === "string" && ["src", "href", "action", "poster"].includes(property)) {
1414
+ const attribute = node.getAttribute(property);
1415
+ if (property === "action" && !attribute) return node.ownerDocument.URL;
1416
+ if (attribute !== null) {
1417
+ try { return new URL(attribute, node.tagName === "BASE" ? node.ownerDocument.URL : documentBaseURL(node)).href; }
1418
+ catch { return attribute; }
1419
+ }
1420
+ }
1314
1421
  return value === null || typeof value !== "object" ? value : String(value);
1315
1422
  },
1316
1423
  set(value) {
@@ -1358,12 +1465,32 @@ class DocumentBinding extends ParentBinding {
1358
1465
  const realm = realmOf(this);
1359
1466
  const node = nodeOf(this);
1360
1467
  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)),
1468
+ createHTMLDocument: (title = undefined) => {
1469
+ const created = node.implementation.createHTMLDocument();
1470
+ const html = created.createElement("html");
1471
+ const head = created.createElement("head");
1472
+ const body = created.createElement("body");
1473
+ created.replaceChildren(created.implementation.createDocumentType("html", "", ""), html);
1474
+ html.append(head, body);
1475
+ if (title !== undefined) {
1476
+ const titleElement = created.createElement("title");
1477
+ titleElement.textContent = String(title);
1478
+ head.appendChild(titleElement);
1479
+ }
1480
+ return realm.wrap(created);
1481
+ },
1482
+ createDocument: (namespace, qualifiedName, doctype = null) => {
1483
+ const created = new realm.window.XMLDocument();
1484
+ const uri = namespace === null || namespace === undefined ? null : String(namespace);
1485
+ const name = qualifiedName === null || qualifiedName === undefined ? "" : String(qualifiedName);
1486
+ if (doctype !== null) created.appendChild(requireNode(doctype));
1487
+ if (name) created.appendChild(created.createElementNS(uri, name));
1488
+ Object.defineProperty(created, "contentType", {
1489
+ value: uri === "http://www.w3.org/1999/xhtml" ? "application/xhtml+xml"
1490
+ : uri === "http://www.w3.org/2000/svg" ? "image/svg+xml" : "application/xml", configurable: true,
1491
+ });
1492
+ return realm.wrap(created);
1493
+ },
1367
1494
  createDocumentType: (qualifiedName, publicId, systemId) => realm.wrap(node.implementation.createDocumentType(
1368
1495
  String(qualifiedName), String(publicId), String(systemId))),
1369
1496
  hasFeature: () => true,
@@ -1375,13 +1502,16 @@ class DocumentBinding extends ParentBinding {
1375
1502
  return realmOf(this).wrap(nodeOf(this).documentElement);
1376
1503
  }
1377
1504
 
1505
+ get doctype() { return realmOf(this).wrap(nodeOf(this).doctype); }
1506
+
1378
1507
  get head() {
1379
1508
  return realmOf(this).wrap(nodeOf(this).head);
1380
1509
  }
1381
1510
 
1382
1511
  get body() {
1383
1512
  const realm = realmOf(this);
1384
- return realm.wrap(realm.hooks.body ? realm.hooks.body() : nodeOf(this).body);
1513
+ const node = nodeOf(this);
1514
+ return realm.wrap(node === realm.window.document && realm.hooks.body ? realm.hooks.body() : node.body);
1385
1515
  }
1386
1516
 
1387
1517
  get title() {
@@ -1393,7 +1523,8 @@ class DocumentBinding extends ParentBinding {
1393
1523
  }
1394
1524
 
1395
1525
  get readyState() {
1396
- return realmOf(this).hooks.readyState?.() ?? "complete";
1526
+ const realm = realmOf(this);
1527
+ return nodeOf(this) === realm.window.document ? realm.hooks.readyState?.() ?? "complete" : "complete";
1397
1528
  }
1398
1529
 
1399
1530
  // Page Visibility: the renderer only runs the page its client is showing.
@@ -1413,8 +1544,11 @@ class DocumentBinding extends ParentBinding {
1413
1544
  return nodeOf(this).URL;
1414
1545
  }
1415
1546
 
1547
+ get referrer() { return nodeOf(this).referrer ?? ""; }
1548
+
1416
1549
  get location() {
1417
- return realmOf(this).hooks.pageGlobal?.()?.location ?? null;
1550
+ const realm = realmOf(this);
1551
+ return nodeOf(this) === realm.window.document ? realm.hooks.pageGlobal?.()?.location ?? null : null;
1418
1552
  }
1419
1553
 
1420
1554
  get domain() {
@@ -1425,17 +1559,21 @@ class DocumentBinding extends ParentBinding {
1425
1559
  return "UTF-8";
1426
1560
  }
1427
1561
 
1562
+ get contentType() { return nodeOf(this).contentType; }
1563
+
1428
1564
  get compatMode() {
1429
1565
  return "CSS1Compat";
1430
1566
  }
1431
1567
 
1432
1568
  /** Non-HttpOnly cookies from the controller's jar; writes go back to the jar. */
1433
1569
  get cookie() {
1434
- return realmOf(this).hooks.documentCookie?.() ?? "";
1570
+ const realm = realmOf(this);
1571
+ return nodeOf(this) === realm.window.document ? realm.hooks.documentCookie?.() ?? "" : "";
1435
1572
  }
1436
1573
 
1437
1574
  set cookie(value) {
1438
- realmOf(this).hooks.setDocumentCookie?.(String(value));
1575
+ const realm = realmOf(this);
1576
+ if (nodeOf(this) === realm.window.document) realm.hooks.setDocumentCookie?.(String(value));
1439
1577
  }
1440
1578
 
1441
1579
  get activeElement() {
@@ -1463,7 +1601,7 @@ class DocumentBinding extends ParentBinding {
1463
1601
  const sheets = [...nodeOf(this).querySelectorAll("style, link[rel~='stylesheet']")].map((element) => {
1464
1602
  const href = element.tagName === "LINK" ? element.getAttribute("href") : null;
1465
1603
  let resolved = null;
1466
- if (href !== null) try { resolved = new URL(href, nodeOf(this).URL).href; } catch {}
1604
+ if (href !== null) try { resolved = new URL(href, element.baseURI || nodeOf(this).URL).href; } catch {}
1467
1605
  return Object.freeze({
1468
1606
  type: "text/css",
1469
1607
  href: resolved,
@@ -1486,11 +1624,13 @@ class DocumentBinding extends ParentBinding {
1486
1624
  }
1487
1625
 
1488
1626
  get currentScript() {
1489
- return realmOf(this).wrap(realmOf(this).hooks.currentScript?.() ?? null);
1627
+ const realm = realmOf(this);
1628
+ return nodeOf(this) === realm.window.document ? realm.wrap(realm.hooks.currentScript?.() ?? null) : null;
1490
1629
  }
1491
1630
 
1492
1631
  get defaultView() {
1493
- return realmOf(this).hooks.pageGlobal?.() ?? null;
1632
+ const realm = realmOf(this);
1633
+ return nodeOf(this) === realm.window.document ? realm.hooks.pageGlobal?.() ?? null : null;
1494
1634
  }
1495
1635
 
1496
1636
  get scripts() {
@@ -1709,6 +1849,8 @@ class EventBinding {
1709
1849
  return this.#realm.wrap(this.#event.relatedTarget ?? null) ?? null;
1710
1850
  }
1711
1851
 
1852
+ get submitter() { return this.#realm.wrap(this.#event.submitter ?? null); }
1853
+
1712
1854
  getModifierState(key) {
1713
1855
  const name = String(key);
1714
1856
  return name === "Alt" ? this.altKey : name === "Control" ? this.ctrlKey : name === "Shift" ? this.shiftKey
@@ -1725,12 +1867,6 @@ class EventBinding {
1725
1867
  this.initUIEvent(type, bubbles, cancelable, view, detail);
1726
1868
  }
1727
1869
 
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
1870
  /** Legacy initialisation of an event from document.createEvent() (DOM §2.5); not once dispatched. */
1735
1871
  initEvent(type, bubbles = false, cancelable = false) {
1736
1872
  if (this.#event.eventPhase !== 0) return;
@@ -1767,6 +1903,9 @@ class EventBinding {
1767
1903
  return this.#event.code;
1768
1904
  }
1769
1905
 
1906
+ get keyCode() { return this.#event.keyCode ?? 0; }
1907
+ get charCode() { return this.#event.charCode ?? 0; }
1908
+
1770
1909
  get clientX() {
1771
1910
  return this.#event.clientX;
1772
1911
  }
@@ -2278,34 +2417,45 @@ function attributesBinding(owner) {
2278
2417
  /** element.dataset: data-* attributes as camelCase string properties. */
2279
2418
  function datasetBinding(node, realm) {
2280
2419
  const attribute = (property) => `data-${property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
2281
- return new Proxy(Object.create(null), {
2282
- get(_target, property) {
2283
- if (typeof property !== "string") return undefined;
2284
- return node.getAttribute(attribute(property)) ?? undefined;
2285
- },
2286
- set(_target, property, value) {
2287
- if (typeof property !== "string") return false;
2288
- node.setAttribute(attribute(property), String(value));
2420
+ const supported = (property) => typeof property === "string" && !/-[a-z]/.test(property)
2421
+ && node.hasAttribute(attribute(property));
2422
+ const set = (target, property, value) => {
2423
+ if (typeof property !== "string") return Reflect.set(target, property, value);
2424
+ if (/-[a-z]/.test(property)) throw new DOMException("The property name must not contain a hyphen followed by a lowercase letter.", "SyntaxError");
2425
+ const name = attribute(property);
2426
+ const next = String(value);
2427
+ if (node.getAttribute(name) !== next) {
2428
+ node.setAttribute(name, next);
2289
2429
  realm.hooks.touch(node);
2290
- return true;
2430
+ }
2431
+ return true;
2432
+ };
2433
+ return new Proxy(Object.create(realm.datasetPrototype), {
2434
+ get(target, property, receiver) {
2435
+ return supported(property) ? node.getAttribute(attribute(property)) : Reflect.get(target, property, receiver);
2291
2436
  },
2292
- has: (_target, property) => typeof property === "string" && node.hasAttribute(attribute(property)),
2293
- deleteProperty(_target, property) {
2294
- if (typeof property !== "string") return false;
2437
+ set,
2438
+ has: (target, property) => supported(property) || Reflect.has(target, property),
2439
+ deleteProperty(target, property) {
2440
+ if (!supported(property)) return Reflect.deleteProperty(target, property);
2295
2441
  node.removeAttribute(attribute(property));
2296
2442
  realm.hooks.touch(node);
2297
2443
  return true;
2298
2444
  },
2299
- ownKeys: () => node.getAttributeNames()
2445
+ ownKeys: (target) => [...node.getAttributeNames()
2300
2446
  .filter((name) => name.startsWith("data-"))
2301
- .map((name) => name.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())),
2302
- getOwnPropertyDescriptor(_target, property) {
2303
- const value = typeof property === "string" ? node.getAttribute(attribute(property)) : null;
2304
- return value === null ? undefined : { value, writable: true, enumerable: true, configurable: true };
2447
+ .map((name) => name.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())), ...Reflect.ownKeys(target)],
2448
+ getOwnPropertyDescriptor(target, property) {
2449
+ return supported(property)
2450
+ ? { value: node.getAttribute(attribute(property)), writable: true, enumerable: true, configurable: true }
2451
+ : Reflect.getOwnPropertyDescriptor(target, property);
2305
2452
  },
2306
- defineProperty: () => false,
2307
- getPrototypeOf: () => null,
2308
- setPrototypeOf: () => false,
2453
+ defineProperty(target, property, descriptor) {
2454
+ if (typeof property !== "string") return Reflect.defineProperty(target, property, descriptor);
2455
+ if (!("value" in descriptor) || descriptor.configurable === false) return false;
2456
+ return set(target, property, descriptor.value);
2457
+ },
2458
+ preventExtensions: () => false,
2309
2459
  });
2310
2460
  }
2311
2461
 
@@ -2515,12 +2665,13 @@ const EVENT_INTERFACES = {
2515
2665
  MouseEvent: ["UIEvent", MOUSE_MEMBERS],
2516
2666
  PointerEvent: ["MouseEvent", ["pointerType"]],
2517
2667
  WheelEvent: ["MouseEvent", ["deltaX", "deltaY", "deltaMode"]],
2518
- KeyboardEvent: ["UIEvent", ["key", "code", "repeat", ...MODIFIERS]],
2668
+ KeyboardEvent: ["UIEvent", ["key", "code", "keyCode", "charCode", "repeat", ...MODIFIERS]],
2519
2669
  FocusEvent: ["UIEvent", ["relatedTarget"]],
2520
2670
  InputEvent: ["UIEvent", ["data", "inputType"]],
2521
2671
  MessageEvent: ["Event", ["data", "origin", "lastEventId", "source", "ports"]],
2522
2672
  PopStateEvent: ["Event", ["state"]],
2523
2673
  HashChangeEvent: ["Event", ["oldURL", "newURL"]],
2674
+ SubmitEvent: ["Event", ["submitter"]],
2524
2675
  };
2525
2676
  /** document.createEvent() interface names (DOM §4.5): legacy aliases included. */
2526
2677
  const CREATE_EVENT_TYPES = new Map([["event", "Event"], ["events", "Event"], ["htmlevents", "Event"],
@@ -2531,7 +2682,7 @@ const CREATE_EVENT_TYPES = new Map([["event", "Event"], ["events", "Event"], ["h
2531
2682
  const UI_EVENT_TYPES = ["UIEvent", "MouseEvent", "PointerEvent", "WheelEvent", "KeyboardEvent", "FocusEvent", "InputEvent"];
2532
2683
  const UI_EVENT_FIELDS = [
2533
2684
  ...["detail", "clientX", "clientY", "screenX", "screenY", "button", "buttons", "deltaX", "deltaY", "deltaZ", "deltaMode",
2534
- "pointerId", "width", "height", "pressure", "location"].map((key) => [key, Number]),
2685
+ "pointerId", "width", "height", "pressure", "location", "keyCode", "which", "charCode"].map((key) => [key, Number]),
2535
2686
  ...["key", "code", "data", "inputType", "pointerType"].map((key) => [key, String]),
2536
2687
  ...["ctrlKey", "altKey", "shiftKey", "metaKey", "repeat", "isComposing", "isPrimary"].map((key) => [key, Boolean]),
2537
2688
  ];
@@ -2553,7 +2704,10 @@ export function eventConstructors(realm) {
2553
2704
  // detail stays in the binding: page values never enter Happy DOM.
2554
2705
  return realm.wrapEvent(new window.CustomEvent(String(type), init(options)), options?.detail);
2555
2706
  }
2556
- realm.eventPrototype = mutableBindingPrototype(EventBinding.prototype);
2707
+ // Members moved to UI event subclasses must not remain inherited through
2708
+ // the frozen engine binding. Plain Events allow expandos such as React's
2709
+ // afterblur.relatedTarget, and Event.prototype directly inherits Object.
2710
+ realm.eventPrototype = mutableBindingPrototype(EventBinding.prototype, Object.prototype);
2557
2711
  Object.defineProperty(realm.eventPrototype, "constructor", { value: Event, writable: true, configurable: true });
2558
2712
  Event.prototype = realm.eventPrototype;
2559
2713
  for (const [name, value] of Object.entries(EVENT_PHASES)) {
@@ -2611,6 +2765,14 @@ export function eventConstructors(realm) {
2611
2765
  }
2612
2766
  MessageEvent.prototype = realm.eventPrototypes.get("MessageEvent");
2613
2767
  Object.defineProperty(MessageEvent.prototype, "constructor", { value: MessageEvent, writable: true, configurable: true });
2768
+ function SubmitEvent(type, options = undefined) {
2769
+ if (!new.target) throw new TypeError("Constructor SubmitEvent requires 'new'");
2770
+ const submitter = options?.submitter == null ? null : nodeOf(options.submitter);
2771
+ if (submitter && submitter.nodeType !== 1) throw new TypeError("The submitter must be an element");
2772
+ return realm.wrapEvent(new window.SubmitEvent(String(type), { ...init(options), submitter }));
2773
+ }
2774
+ SubmitEvent.prototype = realm.eventPrototypes.get("SubmitEvent");
2775
+ Object.defineProperty(SubmitEvent.prototype, "constructor", { value: SubmitEvent, writable: true, configurable: true });
2614
2776
  // PromiseRejectionEvent (HTML §8.1.5.7): promise and reason stay page values in the binding.
2615
2777
  const rejections = new WeakMap();
2616
2778
  function PromiseRejectionEvent(type, options) {
@@ -2634,7 +2796,7 @@ export function eventConstructors(realm) {
2634
2796
  enumerable: true, configurable: true },
2635
2797
  [Symbol.toStringTag]: { value: "PromiseRejectionEvent", configurable: true },
2636
2798
  });
2637
- return { Event, CustomEvent, MessageEvent, PromiseRejectionEvent, ...uiEvents };
2799
+ return { Event, CustomEvent, MessageEvent, SubmitEvent, PromiseRejectionEvent, ...uiEvents };
2638
2800
  }
2639
2801
 
2640
2802
  function domRect({ left, top, width, height }) {