buninu-browser 0.2.3 → 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.
- package/lib/cdp-server.js +47 -14
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +15 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +9 -6
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +213 -91
- package/lib/media/byte-pipe.js +2 -2
- package/lib/media/position-audio.js +10 -1
- package/lib/media/source-snapshots.js +126 -0
- package/lib/media/video-keyframe.js +17 -4
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +8 -28
- package/lib/paint/frame-cache.js +25 -0
- package/lib/paint/frame.js +42 -13
- package/lib/paint/paragraph-text.js +24 -0
- package/lib/paint/raster-readback.js +41 -0
- package/lib/paint/rounded-rect.js +82 -0
- package/lib/paint/shared-frame-pool.js +105 -0
- package/lib/render-tree/index.js +38 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +233 -80
- package/lib/renderer/broadcast-channel.js +76 -0
- package/lib/renderer/canvas-2d.js +257 -14
- package/lib/renderer/canvas-text.js +120 -0
- package/lib/renderer/compositor.js +13 -11
- package/lib/renderer/custom-element-reactions.js +25 -1
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/host.js +28 -4
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +30 -4
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +270 -98
- package/lib/renderer/page-renderer.js +647 -235
- package/lib/renderer/prepared-script-cache.js +50 -0
- package/lib/renderer/rendered-text.js +108 -0
- package/lib/renderer/rewrite-worker.js +4 -3
- package/lib/renderer/script-rewrite.js +13 -2
- package/lib/renderer/scroll-into-view.js +28 -0
- package/lib/renderer/slot-assignment.js +60 -0
- package/lib/renderer/video-presentation.js +23 -4
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +611 -104
- package/lib/style/selector-traits.js +49 -5
- package/package.json +1 -1
- package/test/broadcast-channel.test.js +26 -0
- package/test/canvas-2d.test.js +83 -2
- package/test/canvas-paint.test.js +9 -0
- package/test/canvas-text.test.js +41 -0
- package/test/cdp-stream.test.js +56 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +258 -1
- package/test/display-list.test.js +12 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/fetch-lifecycle.test.js +161 -0
- package/test/fixtures/canvas-paint.js +117 -0
- package/test/fixtures/canvas-text.js +42 -0
- package/test/fixtures/image-lifetime.js +90 -0
- package/test/fixtures/media-seek-snapshot.js +77 -0
- package/test/fixtures/mse-audio-chirp.webm +0 -0
- package/test/fixtures/mse-switch-large.mp4 +0 -0
- package/test/fixtures/mse-switch-small.mp4 +0 -0
- package/test/fixtures/renderer-memory.js +110 -0
- package/test/fixtures/summary-style.js +28 -0
- package/test/font-loading.test.js +97 -0
- package/test/grid.test.js +58 -0
- package/test/hit-test.test.js +21 -1
- package/test/image-lifetime.test.js +12 -0
- package/test/image-resources.test.js +94 -0
- package/test/local-media.test.js +84 -0
- package/test/media-seek-snapshot.test.js +11 -0
- package/test/media-source-config.test.js +98 -0
- package/test/media-source-webm-accounting.test.js +98 -0
- package/test/media-source-webm.test.js +108 -0
- package/test/navigation.test.js +1 -0
- package/test/network-service.test.js +388 -0
- package/test/page-scripts.test.js +1 -1
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/position-audio.test.js +24 -0
- package/test/positioning.test.js +22 -0
- package/test/prepared-script-cache.test.js +45 -0
- package/test/pulse-client.test.js +10 -3
- package/test/raster-readback.test.js +40 -0
- package/test/render-tree.test.js +28 -0
- package/test/renderer-memory.test.js +11 -0
- package/test/selector-traits.test.js +24 -0
- package/test/shared-frame-pool.test.js +63 -0
- package/test/style-sheets.test.js +51 -0
- package/test/svg.test.js +29 -0
- package/test/text-layout.test.js +106 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +69 -0
- package/tools/browser-lab.js +78 -0
- package/tools/browser-soak.js +75 -0
- package/tools/network-reference.js +49 -0
- package/tools/process-memory.js +38 -0
- package/tools/script-benchmark.js +25 -0
- package/ai-debug.log +0 -5537
package/lib/renderer/bindings.js
CHANGED
|
@@ -10,8 +10,16 @@
|
|
|
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",
|
|
@@ -42,6 +50,9 @@ export class BindingRealm {
|
|
|
42
50
|
textPrototype = null;
|
|
43
51
|
characterDataPrototype = null;
|
|
44
52
|
nodePrototype = null;
|
|
53
|
+
datasetPrototype = null;
|
|
54
|
+
wrapValidity = null;
|
|
55
|
+
documentPrototype = null;
|
|
45
56
|
|
|
46
57
|
/**
|
|
47
58
|
* @param {object} window the Happy DOM window of the document
|
|
@@ -88,6 +99,7 @@ export class BindingRealm {
|
|
|
88
99
|
if (Binding === TextBinding && this.textPrototype) Object.setPrototypeOf(wrapper, this.textPrototype);
|
|
89
100
|
if (Binding === CharacterDataBinding && this.characterDataPrototype) Object.setPrototypeOf(wrapper, this.characterDataPrototype);
|
|
90
101
|
if (Binding === NodeBinding && this.nodePrototype) Object.setPrototypeOf(wrapper, this.nodePrototype);
|
|
102
|
+
if (Binding === DocumentBinding && this.documentPrototype) Object.setPrototypeOf(wrapper, this.documentPrototype);
|
|
91
103
|
this.#wrappers.set(node, wrapper);
|
|
92
104
|
}
|
|
93
105
|
return wrapper;
|
|
@@ -210,7 +222,7 @@ export class BindingRealm {
|
|
|
210
222
|
if (!wrapper) {
|
|
211
223
|
wrapper = new EventBinding(CONSTRUCT, event, this, detail);
|
|
212
224
|
if (this.eventPrototype) {
|
|
213
|
-
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;
|
|
214
226
|
Object.setPrototypeOf(wrapper, prototype);
|
|
215
227
|
// isTrusted is [LegacyUnforgeable]: an own accessor of every event.
|
|
216
228
|
const isTrusted = Object.getOwnPropertyDescriptor(EventBinding.prototype, "isTrusted");
|
|
@@ -348,15 +360,7 @@ class NodeBinding {
|
|
|
348
360
|
|
|
349
361
|
/** The document base URL (HTML §2.4.1): the first <base href>, else the document's URL. */
|
|
350
362
|
get baseURI() {
|
|
351
|
-
|
|
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
|
-
}
|
|
363
|
+
return documentBaseURL(this.#node);
|
|
360
364
|
}
|
|
361
365
|
|
|
362
366
|
get nodeValue() {
|
|
@@ -554,7 +558,9 @@ class CharacterDataBinding extends NodeBinding {
|
|
|
554
558
|
}
|
|
555
559
|
}
|
|
556
560
|
|
|
557
|
-
class TextBinding extends CharacterDataBinding {
|
|
561
|
+
class TextBinding extends CharacterDataBinding {
|
|
562
|
+
get assignedSlot() { return realmOf(this).wrap(assignedSlot(nodeOf(this))); }
|
|
563
|
+
}
|
|
558
564
|
|
|
559
565
|
/** Methods shared by elements, documents and fragments (ParentNode). */
|
|
560
566
|
class ParentBinding extends NodeBinding {
|
|
@@ -635,6 +641,9 @@ class ShadowRootBinding extends FragmentBinding {
|
|
|
635
641
|
return nodeOf(this).mode;
|
|
636
642
|
}
|
|
637
643
|
|
|
644
|
+
get delegatesFocus() { return nodeOf(this).delegatesFocus; }
|
|
645
|
+
get slotAssignment() { return nodeOf(this).slotAssignment; }
|
|
646
|
+
|
|
638
647
|
get innerHTML() {
|
|
639
648
|
return nodeOf(this).innerHTML;
|
|
640
649
|
}
|
|
@@ -650,20 +659,49 @@ class ElementBinding extends ParentBinding {
|
|
|
650
659
|
#classList = null;
|
|
651
660
|
#dataset = null;
|
|
652
661
|
#attributes = null;
|
|
662
|
+
#validity = null;
|
|
663
|
+
#clickInProgress = false;
|
|
653
664
|
|
|
654
665
|
get tagName() {
|
|
655
666
|
return nodeOf(this).tagName;
|
|
656
667
|
}
|
|
657
668
|
|
|
658
669
|
attachShadow(init) {
|
|
659
|
-
const mode = init?.mode
|
|
660
|
-
|
|
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) }));
|
|
661
676
|
}
|
|
662
677
|
|
|
663
678
|
get shadowRoot() {
|
|
664
679
|
return realmOf(this).wrap(nodeOf(this).shadowRoot);
|
|
665
680
|
}
|
|
666
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
|
+
|
|
667
705
|
// HTMLIFrameElement: its document's window and, when same-origin, its
|
|
668
706
|
// document (see messaging.js).
|
|
669
707
|
get contentWindow() {
|
|
@@ -747,6 +785,9 @@ class ElementBinding extends ParentBinding {
|
|
|
747
785
|
return nodeOf(this).localName;
|
|
748
786
|
}
|
|
749
787
|
|
|
788
|
+
get namespaceURI() { return nodeOf(this).namespaceURI; }
|
|
789
|
+
get prefix() { return nodeOf(this).prefix; }
|
|
790
|
+
|
|
750
791
|
get id() {
|
|
751
792
|
return nodeOf(this).id;
|
|
752
793
|
}
|
|
@@ -839,18 +880,23 @@ class ElementBinding extends ParentBinding {
|
|
|
839
880
|
const attribute = element.getAttributeNode(String(name));
|
|
840
881
|
if (!attribute) return null;
|
|
841
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
|
+
};
|
|
842
888
|
return {
|
|
843
889
|
get name() { return attribute.name; },
|
|
844
890
|
get localName() { return attribute.localName; },
|
|
845
891
|
get namespaceURI() { return attribute.namespaceURI; },
|
|
846
892
|
get prefix() { return attribute.prefix; },
|
|
847
893
|
get value() { return attribute.value; },
|
|
848
|
-
set value(value) {
|
|
894
|
+
set value(value) { setValue(value); },
|
|
849
895
|
get nodeName() { return attribute.name; },
|
|
850
896
|
get nodeValue() { return attribute.value; },
|
|
851
|
-
set nodeValue(value) {
|
|
897
|
+
set nodeValue(value) { setValue(value ?? ""); },
|
|
852
898
|
get textContent() { return attribute.value; },
|
|
853
|
-
set textContent(value) {
|
|
899
|
+
set textContent(value) { setValue(value ?? ""); },
|
|
854
900
|
ownerElement,
|
|
855
901
|
specified: true,
|
|
856
902
|
nodeType: 2,
|
|
@@ -861,8 +907,10 @@ class ElementBinding extends ParentBinding {
|
|
|
861
907
|
const node = nodeOf(this);
|
|
862
908
|
const attribute = String(name);
|
|
863
909
|
const next = String(value);
|
|
864
|
-
|
|
910
|
+
const resetsCanvas = isCanvasSizeAttribute(node, attribute.toLowerCase());
|
|
911
|
+
if (node.getAttribute(attribute) !== next || resetsCanvas) {
|
|
865
912
|
node.setAttribute(attribute, next);
|
|
913
|
+
if (resetsCanvas) resetCanvas(node);
|
|
866
914
|
touch(this);
|
|
867
915
|
}
|
|
868
916
|
}
|
|
@@ -872,6 +920,7 @@ class ElementBinding extends ParentBinding {
|
|
|
872
920
|
const attribute = String(name);
|
|
873
921
|
if (node.hasAttribute(attribute)) {
|
|
874
922
|
node.removeAttribute(attribute);
|
|
923
|
+
if (isCanvasSizeAttribute(node, attribute.toLowerCase())) resetCanvas(node);
|
|
875
924
|
touch(this);
|
|
876
925
|
}
|
|
877
926
|
}
|
|
@@ -894,17 +943,25 @@ class ElementBinding extends ParentBinding {
|
|
|
894
943
|
}
|
|
895
944
|
|
|
896
945
|
setAttributeNS(namespace, qualifiedName, value) {
|
|
897
|
-
nodeOf(this)
|
|
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);
|
|
898
949
|
touch(this);
|
|
899
950
|
}
|
|
900
951
|
|
|
901
952
|
removeAttributeNS(namespace, localName) {
|
|
902
|
-
nodeOf(this)
|
|
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);
|
|
903
957
|
touch(this);
|
|
904
958
|
}
|
|
905
959
|
|
|
906
960
|
toggleAttribute(name, force = undefined) {
|
|
907
|
-
const
|
|
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);
|
|
908
965
|
touch(this);
|
|
909
966
|
return result;
|
|
910
967
|
}
|
|
@@ -928,11 +985,13 @@ class ElementBinding extends ParentBinding {
|
|
|
928
985
|
}
|
|
929
986
|
|
|
930
987
|
get innerText() {
|
|
931
|
-
|
|
988
|
+
const node = nodeOf(this);
|
|
989
|
+
const realm = realmOf(this);
|
|
990
|
+
return node.ownerDocument !== realm.window.document ? node.textContent : renderedText(node, realm.hooks.computedStyle);
|
|
932
991
|
}
|
|
933
992
|
|
|
934
993
|
set innerText(value) {
|
|
935
|
-
nodeOf(this)
|
|
994
|
+
setRenderedText(nodeOf(this), value);
|
|
936
995
|
touch(this);
|
|
937
996
|
}
|
|
938
997
|
|
|
@@ -998,11 +1057,18 @@ class ElementBinding extends ParentBinding {
|
|
|
998
1057
|
click() {
|
|
999
1058
|
const realm = realmOf(this);
|
|
1000
1059
|
const element = nodeOf(this);
|
|
1001
|
-
|
|
1002
|
-
element.
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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);
|
|
1006
1072
|
}
|
|
1007
1073
|
}
|
|
1008
1074
|
|
|
@@ -1016,21 +1082,42 @@ class ElementBinding extends ParentBinding {
|
|
|
1016
1082
|
const form = nodeOf(this);
|
|
1017
1083
|
if (form.tagName !== "FORM") return;
|
|
1018
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
|
+
}
|
|
1019
1089
|
if (realSubmitter && realSubmitter.form !== form) throw new DOMException("Submitter is not owned by this form", "NotFoundError");
|
|
1020
|
-
if (!form.checkValidity()) return;
|
|
1090
|
+
if (!form.noValidate && !realSubmitter?.formNoValidate && !form.checkValidity()) return;
|
|
1021
1091
|
const event = new realm.window.SubmitEvent("submit", {
|
|
1022
|
-
bubbles: true, cancelable: true, submitter: realSubmitter
|
|
1092
|
+
bubbles: true, cancelable: true, submitter: realSubmitter,
|
|
1023
1093
|
});
|
|
1024
1094
|
form.dispatchEvent(event);
|
|
1025
1095
|
if (!event.defaultPrevented) realm.hooks.submitForm?.(form, realSubmitter);
|
|
1026
1096
|
}
|
|
1027
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
|
+
|
|
1028
1109
|
focus() {
|
|
1029
|
-
nodeOf(this)
|
|
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"]);
|
|
1030
1114
|
}
|
|
1031
1115
|
|
|
1032
1116
|
blur() {
|
|
1033
|
-
nodeOf(this)
|
|
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"]);
|
|
1034
1121
|
}
|
|
1035
1122
|
|
|
1036
1123
|
getBoundingClientRect() {
|
|
@@ -1039,7 +1126,10 @@ class ElementBinding extends ParentBinding {
|
|
|
1039
1126
|
}
|
|
1040
1127
|
|
|
1041
1128
|
getClientRects() {
|
|
1042
|
-
|
|
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)] : [];
|
|
1043
1133
|
}
|
|
1044
1134
|
|
|
1045
1135
|
get offsetWidth() {
|
|
@@ -1217,15 +1307,17 @@ class ElementBinding extends ParentBinding {
|
|
|
1217
1307
|
return Math.max(this.offsetHeight, root ? realmOf(this).hooks.viewport().height : 0);
|
|
1218
1308
|
}
|
|
1219
1309
|
|
|
1220
|
-
scrollIntoView() {
|
|
1310
|
+
scrollIntoView(options = undefined) {
|
|
1221
1311
|
const realm = realmOf(this);
|
|
1312
|
+
const normalized = scrollIntoViewOptions(options);
|
|
1222
1313
|
if (realm.hooks.scrollIntoView) {
|
|
1223
|
-
realm.hooks.scrollIntoView(nodeOf(this));
|
|
1314
|
+
realm.hooks.scrollIntoView(nodeOf(this), normalized);
|
|
1224
1315
|
return;
|
|
1225
1316
|
}
|
|
1226
1317
|
const rect = this.getBoundingClientRect();
|
|
1227
|
-
const { scrollX, scrollY } = realm.hooks.viewport();
|
|
1228
|
-
realm.hooks.scrollTo(scrollX
|
|
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));
|
|
1229
1321
|
}
|
|
1230
1322
|
}
|
|
1231
1323
|
|
|
@@ -1272,7 +1364,7 @@ export const EVENT_HANDLER_NAMES = Object.freeze([
|
|
|
1272
1364
|
"click", "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave",
|
|
1273
1365
|
"keydown", "keyup", "keypress", "beforeinput", "input", "change", "submit", "reset", "focus", "blur", "load", "error",
|
|
1274
1366
|
"abort", "scroll", "resize", "wheel", "contextmenu", "touchstart", "touchend", "touchmove",
|
|
1275
|
-
"pointerdown", "pointerup", "pointermove", "animationend", "transitionend", "readystatechange",
|
|
1367
|
+
"pointerdown", "pointerup", "pointermove", "animationend", "transitionend", "readystatechange", "slotchange", "toggle", "invalid",
|
|
1276
1368
|
]);
|
|
1277
1369
|
|
|
1278
1370
|
/**
|
|
@@ -1316,7 +1408,16 @@ for (const property of REFLECTED_PROPERTIES) {
|
|
|
1316
1408
|
configurable: true,
|
|
1317
1409
|
enumerable: true,
|
|
1318
1410
|
get() {
|
|
1319
|
-
const
|
|
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
|
+
}
|
|
1320
1421
|
return value === null || typeof value !== "object" ? value : String(value);
|
|
1321
1422
|
},
|
|
1322
1423
|
set(value) {
|
|
@@ -1364,12 +1465,32 @@ class DocumentBinding extends ParentBinding {
|
|
|
1364
1465
|
const realm = realmOf(this);
|
|
1365
1466
|
const node = nodeOf(this);
|
|
1366
1467
|
this.#implementation ??= Object.freeze({
|
|
1367
|
-
createHTMLDocument: (title = undefined) =>
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
+
},
|
|
1373
1494
|
createDocumentType: (qualifiedName, publicId, systemId) => realm.wrap(node.implementation.createDocumentType(
|
|
1374
1495
|
String(qualifiedName), String(publicId), String(systemId))),
|
|
1375
1496
|
hasFeature: () => true,
|
|
@@ -1381,13 +1502,16 @@ class DocumentBinding extends ParentBinding {
|
|
|
1381
1502
|
return realmOf(this).wrap(nodeOf(this).documentElement);
|
|
1382
1503
|
}
|
|
1383
1504
|
|
|
1505
|
+
get doctype() { return realmOf(this).wrap(nodeOf(this).doctype); }
|
|
1506
|
+
|
|
1384
1507
|
get head() {
|
|
1385
1508
|
return realmOf(this).wrap(nodeOf(this).head);
|
|
1386
1509
|
}
|
|
1387
1510
|
|
|
1388
1511
|
get body() {
|
|
1389
1512
|
const realm = realmOf(this);
|
|
1390
|
-
|
|
1513
|
+
const node = nodeOf(this);
|
|
1514
|
+
return realm.wrap(node === realm.window.document && realm.hooks.body ? realm.hooks.body() : node.body);
|
|
1391
1515
|
}
|
|
1392
1516
|
|
|
1393
1517
|
get title() {
|
|
@@ -1399,7 +1523,8 @@ class DocumentBinding extends ParentBinding {
|
|
|
1399
1523
|
}
|
|
1400
1524
|
|
|
1401
1525
|
get readyState() {
|
|
1402
|
-
|
|
1526
|
+
const realm = realmOf(this);
|
|
1527
|
+
return nodeOf(this) === realm.window.document ? realm.hooks.readyState?.() ?? "complete" : "complete";
|
|
1403
1528
|
}
|
|
1404
1529
|
|
|
1405
1530
|
// Page Visibility: the renderer only runs the page its client is showing.
|
|
@@ -1419,8 +1544,11 @@ class DocumentBinding extends ParentBinding {
|
|
|
1419
1544
|
return nodeOf(this).URL;
|
|
1420
1545
|
}
|
|
1421
1546
|
|
|
1547
|
+
get referrer() { return nodeOf(this).referrer ?? ""; }
|
|
1548
|
+
|
|
1422
1549
|
get location() {
|
|
1423
|
-
|
|
1550
|
+
const realm = realmOf(this);
|
|
1551
|
+
return nodeOf(this) === realm.window.document ? realm.hooks.pageGlobal?.()?.location ?? null : null;
|
|
1424
1552
|
}
|
|
1425
1553
|
|
|
1426
1554
|
get domain() {
|
|
@@ -1431,17 +1559,21 @@ class DocumentBinding extends ParentBinding {
|
|
|
1431
1559
|
return "UTF-8";
|
|
1432
1560
|
}
|
|
1433
1561
|
|
|
1562
|
+
get contentType() { return nodeOf(this).contentType; }
|
|
1563
|
+
|
|
1434
1564
|
get compatMode() {
|
|
1435
1565
|
return "CSS1Compat";
|
|
1436
1566
|
}
|
|
1437
1567
|
|
|
1438
1568
|
/** Non-HttpOnly cookies from the controller's jar; writes go back to the jar. */
|
|
1439
1569
|
get cookie() {
|
|
1440
|
-
|
|
1570
|
+
const realm = realmOf(this);
|
|
1571
|
+
return nodeOf(this) === realm.window.document ? realm.hooks.documentCookie?.() ?? "" : "";
|
|
1441
1572
|
}
|
|
1442
1573
|
|
|
1443
1574
|
set cookie(value) {
|
|
1444
|
-
realmOf(this)
|
|
1575
|
+
const realm = realmOf(this);
|
|
1576
|
+
if (nodeOf(this) === realm.window.document) realm.hooks.setDocumentCookie?.(String(value));
|
|
1445
1577
|
}
|
|
1446
1578
|
|
|
1447
1579
|
get activeElement() {
|
|
@@ -1469,7 +1601,7 @@ class DocumentBinding extends ParentBinding {
|
|
|
1469
1601
|
const sheets = [...nodeOf(this).querySelectorAll("style, link[rel~='stylesheet']")].map((element) => {
|
|
1470
1602
|
const href = element.tagName === "LINK" ? element.getAttribute("href") : null;
|
|
1471
1603
|
let resolved = null;
|
|
1472
|
-
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 {}
|
|
1473
1605
|
return Object.freeze({
|
|
1474
1606
|
type: "text/css",
|
|
1475
1607
|
href: resolved,
|
|
@@ -1492,11 +1624,13 @@ class DocumentBinding extends ParentBinding {
|
|
|
1492
1624
|
}
|
|
1493
1625
|
|
|
1494
1626
|
get currentScript() {
|
|
1495
|
-
|
|
1627
|
+
const realm = realmOf(this);
|
|
1628
|
+
return nodeOf(this) === realm.window.document ? realm.wrap(realm.hooks.currentScript?.() ?? null) : null;
|
|
1496
1629
|
}
|
|
1497
1630
|
|
|
1498
1631
|
get defaultView() {
|
|
1499
|
-
|
|
1632
|
+
const realm = realmOf(this);
|
|
1633
|
+
return nodeOf(this) === realm.window.document ? realm.hooks.pageGlobal?.() ?? null : null;
|
|
1500
1634
|
}
|
|
1501
1635
|
|
|
1502
1636
|
get scripts() {
|
|
@@ -1715,6 +1849,8 @@ class EventBinding {
|
|
|
1715
1849
|
return this.#realm.wrap(this.#event.relatedTarget ?? null) ?? null;
|
|
1716
1850
|
}
|
|
1717
1851
|
|
|
1852
|
+
get submitter() { return this.#realm.wrap(this.#event.submitter ?? null); }
|
|
1853
|
+
|
|
1718
1854
|
getModifierState(key) {
|
|
1719
1855
|
const name = String(key);
|
|
1720
1856
|
return name === "Alt" ? this.altKey : name === "Control" ? this.ctrlKey : name === "Shift" ? this.shiftKey
|
|
@@ -1731,12 +1867,6 @@ class EventBinding {
|
|
|
1731
1867
|
this.initUIEvent(type, bubbles, cancelable, view, detail);
|
|
1732
1868
|
}
|
|
1733
1869
|
|
|
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
1870
|
/** Legacy initialisation of an event from document.createEvent() (DOM §2.5); not once dispatched. */
|
|
1741
1871
|
initEvent(type, bubbles = false, cancelable = false) {
|
|
1742
1872
|
if (this.#event.eventPhase !== 0) return;
|
|
@@ -2287,34 +2417,45 @@ function attributesBinding(owner) {
|
|
|
2287
2417
|
/** element.dataset: data-* attributes as camelCase string properties. */
|
|
2288
2418
|
function datasetBinding(node, realm) {
|
|
2289
2419
|
const attribute = (property) => `data-${property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
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);
|
|
2298
2429
|
realm.hooks.touch(node);
|
|
2299
|
-
|
|
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);
|
|
2300
2436
|
},
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
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);
|
|
2304
2441
|
node.removeAttribute(attribute(property));
|
|
2305
2442
|
realm.hooks.touch(node);
|
|
2306
2443
|
return true;
|
|
2307
2444
|
},
|
|
2308
|
-
ownKeys: () => node.getAttributeNames()
|
|
2445
|
+
ownKeys: (target) => [...node.getAttributeNames()
|
|
2309
2446
|
.filter((name) => name.startsWith("data-"))
|
|
2310
|
-
.map((name) => name.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())),
|
|
2311
|
-
getOwnPropertyDescriptor(
|
|
2312
|
-
|
|
2313
|
-
|
|
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);
|
|
2314
2452
|
},
|
|
2315
|
-
defineProperty
|
|
2316
|
-
|
|
2317
|
-
|
|
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,
|
|
2318
2459
|
});
|
|
2319
2460
|
}
|
|
2320
2461
|
|
|
@@ -2530,6 +2671,7 @@ const EVENT_INTERFACES = {
|
|
|
2530
2671
|
MessageEvent: ["Event", ["data", "origin", "lastEventId", "source", "ports"]],
|
|
2531
2672
|
PopStateEvent: ["Event", ["state"]],
|
|
2532
2673
|
HashChangeEvent: ["Event", ["oldURL", "newURL"]],
|
|
2674
|
+
SubmitEvent: ["Event", ["submitter"]],
|
|
2533
2675
|
};
|
|
2534
2676
|
/** document.createEvent() interface names (DOM §4.5): legacy aliases included. */
|
|
2535
2677
|
const CREATE_EVENT_TYPES = new Map([["event", "Event"], ["events", "Event"], ["htmlevents", "Event"],
|
|
@@ -2562,7 +2704,10 @@ export function eventConstructors(realm) {
|
|
|
2562
2704
|
// detail stays in the binding: page values never enter Happy DOM.
|
|
2563
2705
|
return realm.wrapEvent(new window.CustomEvent(String(type), init(options)), options?.detail);
|
|
2564
2706
|
}
|
|
2565
|
-
|
|
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);
|
|
2566
2711
|
Object.defineProperty(realm.eventPrototype, "constructor", { value: Event, writable: true, configurable: true });
|
|
2567
2712
|
Event.prototype = realm.eventPrototype;
|
|
2568
2713
|
for (const [name, value] of Object.entries(EVENT_PHASES)) {
|
|
@@ -2620,6 +2765,14 @@ export function eventConstructors(realm) {
|
|
|
2620
2765
|
}
|
|
2621
2766
|
MessageEvent.prototype = realm.eventPrototypes.get("MessageEvent");
|
|
2622
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 });
|
|
2623
2776
|
// PromiseRejectionEvent (HTML §8.1.5.7): promise and reason stay page values in the binding.
|
|
2624
2777
|
const rejections = new WeakMap();
|
|
2625
2778
|
function PromiseRejectionEvent(type, options) {
|
|
@@ -2643,7 +2796,7 @@ export function eventConstructors(realm) {
|
|
|
2643
2796
|
enumerable: true, configurable: true },
|
|
2644
2797
|
[Symbol.toStringTag]: { value: "PromiseRejectionEvent", configurable: true },
|
|
2645
2798
|
});
|
|
2646
|
-
return { Event, CustomEvent, MessageEvent, PromiseRejectionEvent, ...uiEvents };
|
|
2799
|
+
return { Event, CustomEvent, MessageEvent, SubmitEvent, PromiseRejectionEvent, ...uiEvents };
|
|
2647
2800
|
}
|
|
2648
2801
|
|
|
2649
2802
|
function domRect({ left, top, width, height }) {
|