buninu-browser 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -0
- package/LICENSES/brotli.txt +19 -0
- package/README.md +41 -7
- package/THIRD_PARTY_NOTICES.md +26 -4
- package/buninu-browser.js +10 -2
- package/lib/audio/pcm.js +16 -0
- package/lib/audio/piano.js +109 -0
- package/lib/audio/pulse-client.js +413 -0
- package/lib/cdp-server.js +28 -7
- package/lib/document-types.js +308 -0
- package/lib/download-directory.js +28 -0
- package/lib/download.js +63 -0
- package/lib/happy-dom/selector-matcher.js +83 -0
- package/lib/headless-shell.js +165 -26
- package/lib/input/hit-test.js +57 -8
- package/lib/layout/text-layout.js +290 -41
- package/lib/network/network-service.js +177 -40
- package/lib/network/resolver.js +16 -0
- package/lib/paint/display-list.js +25 -10
- package/lib/render-tree/index.js +58 -0
- package/lib/renderer/bindings.js +543 -55
- package/lib/renderer/flags.js +8 -5
- package/lib/renderer/host.js +103 -7
- package/lib/renderer/main.js +35 -3
- package/lib/renderer/module-transform.js +30 -23
- package/lib/renderer/page-realm.js +228 -24
- package/lib/renderer/page-renderer.js +1264 -221
- package/lib/screenshot-cli.js +61 -0
- package/lib/style/computed-style.js +558 -229
- package/lib/style/selector-keys.js +302 -0
- package/lib/style/selector-traits.js +240 -0
- package/lib/user-agent.js +12 -0
- package/package.json +1 -1
- package/patches/happy-dom/0003-defer-script-events-to-renderer.patch +40 -0
- package/patches/happy-dom/0004-insert-adjacent-html-in-order.patch +19 -0
- package/patches/happy-dom/0005-invalidate-matches-on-structure-changes.patch +43 -0
- package/patches/happy-dom/README.md +12 -0
- package/patches/ses/0001-allow-regexp-own-tostring.patch +13 -0
- package/patches/ses/README.md +16 -0
- package/test/computed-style.test.js +113 -1
- package/test/document-types.test.js +111 -0
- package/test/download.test.js +192 -0
- package/test/fixtures/tone.mp3 +0 -0
- package/test/flex.test.js +11 -0
- package/test/happy-dom-matches.test.js +25 -0
- package/test/headless-shell.test.js +56 -0
- package/test/hit-test.test.js +37 -0
- package/test/log.test.js +4 -1
- package/test/network-service.test.js +67 -0
- package/test/page-scripts.test.js +437 -29
- package/test/piano.test.js +65 -0
- package/test/positioning.test.js +11 -0
- package/test/pulse-client.test.js +130 -0
- package/test/render-tree.test.js +36 -1
- package/test/resolver.test.js +12 -1
- package/test/screenshot-cli.test.js +19 -0
- package/test/selector-keys.test.js +39 -0
- package/test/selector-traits.test.js +19 -0
- package/test/text-layout.test.js +91 -1
- package/test/user-agent.test.js +42 -0
- package/vendor/happy-dom/packages/happy-dom/src/nodes/element/Element.ts +3 -6
- package/vendor/happy-dom/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +2 -20
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/QuerySelector.ts +15 -3
- package/vendor/ses/dist/ses.mjs +2 -0
package/lib/renderer/bindings.js
CHANGED
|
@@ -22,8 +22,17 @@ const REFLECTED_PROPERTIES = [
|
|
|
22
22
|
export class BindingRealm {
|
|
23
23
|
#wrappers = new WeakMap();
|
|
24
24
|
#listeners = new WeakMap();
|
|
25
|
+
#events = new WeakMap();
|
|
26
|
+
#liveViews = new WeakMap();
|
|
25
27
|
#hooks;
|
|
26
28
|
#window;
|
|
29
|
+
eventPrototype = null;
|
|
30
|
+
elementPrototype = null;
|
|
31
|
+
fragmentPrototype = null;
|
|
32
|
+
shadowRootPrototype = null;
|
|
33
|
+
textPrototype = null;
|
|
34
|
+
characterDataPrototype = null;
|
|
35
|
+
nodePrototype = null;
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* @param {object} window the Happy DOM window of the document
|
|
@@ -57,21 +66,86 @@ export class BindingRealm {
|
|
|
57
66
|
if (!wrapper) {
|
|
58
67
|
const Binding = node.nodeType === 1 ? ElementBinding
|
|
59
68
|
: node.nodeType === 9 ? DocumentBinding
|
|
60
|
-
: node.nodeType === 11 ? FragmentBinding
|
|
69
|
+
: node.nodeType === 11 ? (node instanceof this.#window.ShadowRoot ? ShadowRootBinding : FragmentBinding)
|
|
61
70
|
: node.nodeType === 3 ? TextBinding
|
|
62
|
-
:
|
|
71
|
+
: node.nodeType === 8 ? CharacterDataBinding
|
|
72
|
+
: NodeBinding;
|
|
63
73
|
wrapper = new Binding(CONSTRUCT, node, this);
|
|
74
|
+
if (Binding === ElementBinding && this.elementPrototype) Object.setPrototypeOf(wrapper, this.elementPrototype);
|
|
75
|
+
if (Binding === FragmentBinding && this.fragmentPrototype) Object.setPrototypeOf(wrapper, this.fragmentPrototype);
|
|
76
|
+
if (Binding === ShadowRootBinding && this.shadowRootPrototype) Object.setPrototypeOf(wrapper, this.shadowRootPrototype);
|
|
77
|
+
if (Binding === TextBinding && this.textPrototype) Object.setPrototypeOf(wrapper, this.textPrototype);
|
|
78
|
+
if (Binding === CharacterDataBinding && this.characterDataPrototype) Object.setPrototypeOf(wrapper, this.characterDataPrototype);
|
|
79
|
+
if (Binding === NodeBinding && this.nodePrototype) Object.setPrototypeOf(wrapper, this.nodePrototype);
|
|
64
80
|
this.#wrappers.set(node, wrapper);
|
|
65
81
|
}
|
|
66
82
|
return wrapper;
|
|
67
83
|
}
|
|
68
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A live NodeList/HTMLCollection over a Happy DOM collection (DOM §4.2.10):
|
|
87
|
+
* length and indices read through on every access, so a loop that moves
|
|
88
|
+
* nodes out (`while (list.length) fragment.appendChild(list[0])`) ends as
|
|
89
|
+
* it does in browsers. It is an Array proxy so array-style use keeps
|
|
90
|
+
* working; one view per collection keeps `el.childNodes === el.childNodes`.
|
|
91
|
+
*/
|
|
92
|
+
live(collection) {
|
|
93
|
+
if (!collection) return null;
|
|
94
|
+
let view = this.#liveViews.get(collection);
|
|
95
|
+
if (view) return view;
|
|
96
|
+
const realm = this;
|
|
97
|
+
const at = (index) => index < collection.length ? realm.wrap(collection[index] ?? collection.item?.(index)) : undefined;
|
|
98
|
+
const isIndex = (property) => typeof property === "string" && /^(?:0|[1-9]\d*)$/.test(property);
|
|
99
|
+
const methods = {
|
|
100
|
+
item: (index) => at(Number(index) >>> 0) ?? null,
|
|
101
|
+
namedItem: (name) => {
|
|
102
|
+
const key = String(name);
|
|
103
|
+
for (let index = 0; index < collection.length; index++) {
|
|
104
|
+
const node = collection[index];
|
|
105
|
+
if (node?.getAttribute?.("id") === key || node?.getAttribute?.("name") === key) return realm.wrap(node);
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
view = new Proxy([], {
|
|
111
|
+
get(target, property, receiver) {
|
|
112
|
+
if (property === "length") return collection.length;
|
|
113
|
+
if (isIndex(property)) return at(Number(property));
|
|
114
|
+
if (Object.hasOwn(methods, property)) return methods[property];
|
|
115
|
+
return Reflect.get(target, property, receiver);
|
|
116
|
+
},
|
|
117
|
+
has: (target, property) => property === "length" || Object.hasOwn(methods, property)
|
|
118
|
+
|| (isIndex(property) ? Number(property) < collection.length : Reflect.has(target, property)),
|
|
119
|
+
getOwnPropertyDescriptor(target, property) {
|
|
120
|
+
if (property === "length") return { value: collection.length, writable: true, enumerable: false, configurable: false };
|
|
121
|
+
if (isIndex(property) && Number(property) < collection.length) {
|
|
122
|
+
return { value: at(Number(property)), writable: false, enumerable: true, configurable: true };
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
},
|
|
126
|
+
ownKeys: () => [...Array.from({ length: collection.length }, (_, index) => String(index)), "length"],
|
|
127
|
+
set: () => false,
|
|
128
|
+
defineProperty: () => false,
|
|
129
|
+
deleteProperty: () => false,
|
|
130
|
+
});
|
|
131
|
+
this.#liveViews.set(collection, view);
|
|
132
|
+
return view;
|
|
133
|
+
}
|
|
134
|
+
|
|
69
135
|
wrapAll(nodes) {
|
|
70
136
|
return Array.from(nodes ?? [], (node) => this.wrap(node));
|
|
71
137
|
}
|
|
72
138
|
|
|
73
|
-
|
|
74
|
-
|
|
139
|
+
/** One wrapper per event, so listeners see the dispatched object (and its detail). */
|
|
140
|
+
wrapEvent(event, detail = undefined) {
|
|
141
|
+
if (!event) return null;
|
|
142
|
+
let wrapper = this.#events.get(event);
|
|
143
|
+
if (!wrapper) {
|
|
144
|
+
wrapper = new EventBinding(CONSTRUCT, event, this, detail);
|
|
145
|
+
if (this.eventPrototype) Object.setPrototypeOf(wrapper, this.eventPrototype);
|
|
146
|
+
this.#events.set(event, wrapper);
|
|
147
|
+
}
|
|
148
|
+
return wrapper;
|
|
75
149
|
}
|
|
76
150
|
|
|
77
151
|
/** Host listener registered on the real target for a page listener. */
|
|
@@ -90,6 +164,19 @@ export function nodeOf(value) {
|
|
|
90
164
|
return NodeBinding.unwrap(value);
|
|
91
165
|
}
|
|
92
166
|
|
|
167
|
+
/** Writable page-local facade over a frozen engine binding prototype. */
|
|
168
|
+
export function mutableBindingPrototype(base, parent = base) {
|
|
169
|
+
const prototype = Object.create(parent);
|
|
170
|
+
for (const key of Reflect.ownKeys(base)) {
|
|
171
|
+
if (key === "constructor") continue;
|
|
172
|
+
const descriptor = Object.getOwnPropertyDescriptor(base, key);
|
|
173
|
+
descriptor.configurable = true;
|
|
174
|
+
if ("value" in descriptor) descriptor.writable = true;
|
|
175
|
+
Object.defineProperty(prototype, key, descriptor);
|
|
176
|
+
}
|
|
177
|
+
return prototype;
|
|
178
|
+
}
|
|
179
|
+
|
|
93
180
|
function realmOf(wrapper) {
|
|
94
181
|
return NodeBinding.realmOf(wrapper);
|
|
95
182
|
}
|
|
@@ -101,8 +188,9 @@ function toNode(wrapper, value) {
|
|
|
101
188
|
return realmOf(wrapper).window.document.createTextNode(String(value));
|
|
102
189
|
}
|
|
103
190
|
|
|
104
|
-
|
|
105
|
-
|
|
191
|
+
/** The page changed `wrapper`'s node (state the style engine may read: attributes, checked, value). */
|
|
192
|
+
function touch(wrapper, stateFeatures = []) {
|
|
193
|
+
realmOf(wrapper).hooks.touch(NodeBinding.unwrap(wrapper), stateFeatures);
|
|
106
194
|
}
|
|
107
195
|
|
|
108
196
|
/**
|
|
@@ -111,7 +199,7 @@ function touch(wrapper) {
|
|
|
111
199
|
*/
|
|
112
200
|
function inserted(wrapper, nodes) {
|
|
113
201
|
const realm = realmOf(wrapper);
|
|
114
|
-
realm.hooks.touch();
|
|
202
|
+
realm.hooks.touch(NodeBinding.unwrap(wrapper));
|
|
115
203
|
realm.hooks.inserted?.(nodes);
|
|
116
204
|
}
|
|
117
205
|
|
|
@@ -190,7 +278,7 @@ class NodeBinding {
|
|
|
190
278
|
}
|
|
191
279
|
|
|
192
280
|
get childNodes() {
|
|
193
|
-
return this.#realm.
|
|
281
|
+
return this.#realm.live(this.#node.childNodes);
|
|
194
282
|
}
|
|
195
283
|
|
|
196
284
|
get firstChild() {
|
|
@@ -271,9 +359,7 @@ class NodeBinding {
|
|
|
271
359
|
dispatchEvent(event) {
|
|
272
360
|
const real = EventBinding.unwrap(event);
|
|
273
361
|
if (!real) throw new TypeError("dispatchEvent requires an Event");
|
|
274
|
-
|
|
275
|
-
touch(this);
|
|
276
|
-
return result;
|
|
362
|
+
return this.#node.dispatchEvent(real);
|
|
277
363
|
}
|
|
278
364
|
}
|
|
279
365
|
|
|
@@ -283,7 +369,8 @@ function requireNode(value) {
|
|
|
283
369
|
return node;
|
|
284
370
|
}
|
|
285
371
|
|
|
286
|
-
|
|
372
|
+
/** Text and comments; frameworks read comment data (React's hydration markers). */
|
|
373
|
+
class CharacterDataBinding extends NodeBinding {
|
|
287
374
|
get data() {
|
|
288
375
|
return nodeOf(this).data;
|
|
289
376
|
}
|
|
@@ -298,10 +385,12 @@ class TextBinding extends NodeBinding {
|
|
|
298
385
|
}
|
|
299
386
|
}
|
|
300
387
|
|
|
388
|
+
class TextBinding extends CharacterDataBinding {}
|
|
389
|
+
|
|
301
390
|
/** Methods shared by elements, documents and fragments (ParentNode). */
|
|
302
391
|
class ParentBinding extends NodeBinding {
|
|
303
392
|
get children() {
|
|
304
|
-
return realmOf(this).
|
|
393
|
+
return realmOf(this).live(nodeOf(this).children);
|
|
305
394
|
}
|
|
306
395
|
|
|
307
396
|
get firstElementChild() {
|
|
@@ -325,11 +414,11 @@ class ParentBinding extends NodeBinding {
|
|
|
325
414
|
}
|
|
326
415
|
|
|
327
416
|
getElementsByTagName(name) {
|
|
328
|
-
return realmOf(this).
|
|
417
|
+
return realmOf(this).live(nodeOf(this).getElementsByTagName(String(name)));
|
|
329
418
|
}
|
|
330
419
|
|
|
331
420
|
getElementsByClassName(names) {
|
|
332
|
-
return realmOf(this).
|
|
421
|
+
return realmOf(this).live(nodeOf(this).getElementsByClassName(String(names)));
|
|
333
422
|
}
|
|
334
423
|
|
|
335
424
|
append(...values) {
|
|
@@ -360,6 +449,29 @@ class FragmentBinding extends ParentBinding {
|
|
|
360
449
|
}
|
|
361
450
|
}
|
|
362
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Shadow roots work for scripts (custom elements build into them), but the
|
|
454
|
+
* renderer does not draw shadow trees yet: the host's light DOM is shown.
|
|
455
|
+
*/
|
|
456
|
+
class ShadowRootBinding extends FragmentBinding {
|
|
457
|
+
get host() {
|
|
458
|
+
return realmOf(this).wrap(nodeOf(this).host);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
get mode() {
|
|
462
|
+
return nodeOf(this).mode;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
get innerHTML() {
|
|
466
|
+
return nodeOf(this).innerHTML;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
set innerHTML(value) {
|
|
470
|
+
nodeOf(this).innerHTML = String(value);
|
|
471
|
+
touch(this);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
363
475
|
class ElementBinding extends ParentBinding {
|
|
364
476
|
#style = null;
|
|
365
477
|
#classList = null;
|
|
@@ -369,29 +481,51 @@ class ElementBinding extends ParentBinding {
|
|
|
369
481
|
return nodeOf(this).tagName;
|
|
370
482
|
}
|
|
371
483
|
|
|
484
|
+
attachShadow(init) {
|
|
485
|
+
const mode = init?.mode === "closed" ? "closed" : "open";
|
|
486
|
+
return realmOf(this).wrap(nodeOf(this).attachShadow({ mode }));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
get shadowRoot() {
|
|
490
|
+
return realmOf(this).wrap(nodeOf(this).shadowRoot);
|
|
491
|
+
}
|
|
492
|
+
|
|
372
493
|
get content() {
|
|
373
494
|
const content = nodeOf(this).content;
|
|
374
495
|
return content === undefined ? undefined : realmOf(this).wrap(content);
|
|
375
496
|
}
|
|
376
497
|
|
|
498
|
+
// HTMLMediaElement (video and audio): the subset the renderer plays.
|
|
377
499
|
get paused() {
|
|
378
|
-
return this
|
|
500
|
+
return isMedia(this) ? realmOf(this).hooks.mediaState?.(nodeOf(this)).paused ?? true : undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
get ended() {
|
|
504
|
+
return isMedia(this) ? realmOf(this).hooks.mediaState?.(nodeOf(this)).ended ?? false : undefined;
|
|
379
505
|
}
|
|
380
506
|
|
|
381
507
|
get currentTime() {
|
|
382
|
-
return this
|
|
508
|
+
return isMedia(this) ? realmOf(this).hooks.mediaState?.(nodeOf(this)).currentTime ?? 0 : undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
set currentTime(value) {
|
|
512
|
+
if (!isMedia(this)) return;
|
|
513
|
+
const seconds = Number(value);
|
|
514
|
+
if (!Number.isFinite(seconds)) throw new TypeError("The provided double value is non-finite.");
|
|
515
|
+
void realmOf(this).hooks.seekMedia?.(nodeOf(this), seconds);
|
|
383
516
|
}
|
|
384
517
|
|
|
385
518
|
get duration() {
|
|
386
|
-
return this
|
|
519
|
+
return isMedia(this) ? realmOf(this).hooks.mediaState?.(nodeOf(this)).duration ?? NaN : undefined;
|
|
387
520
|
}
|
|
388
521
|
|
|
389
522
|
play() {
|
|
390
|
-
if (this
|
|
523
|
+
if (!isMedia(this)) return undefined;
|
|
524
|
+
return Promise.resolve(realmOf(this).hooks.playMedia?.(nodeOf(this))).then(() => undefined);
|
|
391
525
|
}
|
|
392
526
|
|
|
393
527
|
pause() {
|
|
394
|
-
if (this
|
|
528
|
+
if (isMedia(this)) realmOf(this).hooks.pauseMedia?.(nodeOf(this));
|
|
395
529
|
}
|
|
396
530
|
|
|
397
531
|
get localName() {
|
|
@@ -403,8 +537,12 @@ class ElementBinding extends ParentBinding {
|
|
|
403
537
|
}
|
|
404
538
|
|
|
405
539
|
set id(value) {
|
|
406
|
-
|
|
407
|
-
|
|
540
|
+
const node = nodeOf(this);
|
|
541
|
+
const next = String(value);
|
|
542
|
+
if (node.id !== next) {
|
|
543
|
+
node.id = next;
|
|
544
|
+
touch(this);
|
|
545
|
+
}
|
|
408
546
|
}
|
|
409
547
|
|
|
410
548
|
get className() {
|
|
@@ -412,8 +550,12 @@ class ElementBinding extends ParentBinding {
|
|
|
412
550
|
}
|
|
413
551
|
|
|
414
552
|
set className(value) {
|
|
415
|
-
|
|
416
|
-
|
|
553
|
+
const node = nodeOf(this);
|
|
554
|
+
const next = String(value);
|
|
555
|
+
if (node.className !== next) {
|
|
556
|
+
node.className = next;
|
|
557
|
+
touch(this);
|
|
558
|
+
}
|
|
417
559
|
}
|
|
418
560
|
|
|
419
561
|
get classList() {
|
|
@@ -435,14 +577,46 @@ class ElementBinding extends ParentBinding {
|
|
|
435
577
|
return nodeOf(this).getAttribute(String(name));
|
|
436
578
|
}
|
|
437
579
|
|
|
580
|
+
getAttributeNode(name) {
|
|
581
|
+
const element = nodeOf(this);
|
|
582
|
+
const attribute = element.getAttributeNode(String(name));
|
|
583
|
+
if (!attribute) return null;
|
|
584
|
+
const ownerElement = this;
|
|
585
|
+
return {
|
|
586
|
+
get name() { return attribute.name; },
|
|
587
|
+
get localName() { return attribute.localName; },
|
|
588
|
+
get namespaceURI() { return attribute.namespaceURI; },
|
|
589
|
+
get prefix() { return attribute.prefix; },
|
|
590
|
+
get value() { return attribute.value; },
|
|
591
|
+
set value(value) { attribute.value = String(value); touch(ownerElement); },
|
|
592
|
+
get nodeName() { return attribute.name; },
|
|
593
|
+
get nodeValue() { return attribute.value; },
|
|
594
|
+
set nodeValue(value) { attribute.value = String(value ?? ""); touch(ownerElement); },
|
|
595
|
+
get textContent() { return attribute.value; },
|
|
596
|
+
set textContent(value) { attribute.value = String(value ?? ""); touch(ownerElement); },
|
|
597
|
+
ownerElement,
|
|
598
|
+
specified: true,
|
|
599
|
+
nodeType: 2,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
438
603
|
setAttribute(name, value) {
|
|
439
|
-
nodeOf(this)
|
|
440
|
-
|
|
604
|
+
const node = nodeOf(this);
|
|
605
|
+
const attribute = String(name);
|
|
606
|
+
const next = String(value);
|
|
607
|
+
if (node.getAttribute(attribute) !== next) {
|
|
608
|
+
node.setAttribute(attribute, next);
|
|
609
|
+
touch(this);
|
|
610
|
+
}
|
|
441
611
|
}
|
|
442
612
|
|
|
443
613
|
removeAttribute(name) {
|
|
444
|
-
nodeOf(this)
|
|
445
|
-
|
|
614
|
+
const node = nodeOf(this);
|
|
615
|
+
const attribute = String(name);
|
|
616
|
+
if (node.hasAttribute(attribute)) {
|
|
617
|
+
node.removeAttribute(attribute);
|
|
618
|
+
touch(this);
|
|
619
|
+
}
|
|
446
620
|
}
|
|
447
621
|
|
|
448
622
|
hasAttribute(name) {
|
|
@@ -543,8 +717,32 @@ class ElementBinding extends ParentBinding {
|
|
|
543
717
|
|
|
544
718
|
click() {
|
|
545
719
|
const realm = realmOf(this);
|
|
546
|
-
nodeOf(this)
|
|
547
|
-
|
|
720
|
+
const element = nodeOf(this);
|
|
721
|
+
const event = new realm.window.MouseEvent("click", { bubbles: true, cancelable: true });
|
|
722
|
+
element.dispatchEvent(event);
|
|
723
|
+
if (!event.defaultPrevented) {
|
|
724
|
+
const anchor = element.closest?.("a[href]");
|
|
725
|
+
if (anchor) realm.hooks.activateLink?.(anchor);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
submit() {
|
|
730
|
+
const form = nodeOf(this);
|
|
731
|
+
if (form.tagName === "FORM") realmOf(this).hooks.submitForm?.(form);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
requestSubmit(submitter) {
|
|
735
|
+
const realm = realmOf(this);
|
|
736
|
+
const form = nodeOf(this);
|
|
737
|
+
if (form.tagName !== "FORM") return;
|
|
738
|
+
const realSubmitter = submitter == null ? null : nodeOf(submitter);
|
|
739
|
+
if (realSubmitter && realSubmitter.form !== form) throw new DOMException("Submitter is not owned by this form", "NotFoundError");
|
|
740
|
+
if (!form.checkValidity()) return;
|
|
741
|
+
const event = new realm.window.SubmitEvent("submit", {
|
|
742
|
+
bubbles: true, cancelable: true, submitter: realSubmitter ?? form,
|
|
743
|
+
});
|
|
744
|
+
form.dispatchEvent(event);
|
|
745
|
+
if (!event.defaultPrevented) realm.hooks.submitForm?.(form, realSubmitter);
|
|
548
746
|
}
|
|
549
747
|
|
|
550
748
|
focus() {
|
|
@@ -582,12 +780,15 @@ class ElementBinding extends ParentBinding {
|
|
|
582
780
|
return Math.round(this.getBoundingClientRect().top + viewport().scrollY);
|
|
583
781
|
}
|
|
584
782
|
|
|
783
|
+
// The root element's client size is the viewport (CSSOM View §6).
|
|
585
784
|
get clientWidth() {
|
|
586
|
-
|
|
785
|
+
const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
|
|
786
|
+
return root ? realmOf(this).hooks.viewport().width : this.offsetWidth;
|
|
587
787
|
}
|
|
588
788
|
|
|
589
789
|
get clientHeight() {
|
|
590
|
-
|
|
790
|
+
const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
|
|
791
|
+
return root ? realmOf(this).hooks.viewport().height : this.offsetHeight;
|
|
591
792
|
}
|
|
592
793
|
|
|
593
794
|
// Tables and selects (undefined on other elements, as in browsers).
|
|
@@ -641,7 +842,7 @@ class ElementBinding extends ParentBinding {
|
|
|
641
842
|
set selectedIndex(value) {
|
|
642
843
|
if (nodeOf(this).tagName !== "SELECT") return;
|
|
643
844
|
nodeOf(this).selectedIndex = Number(value);
|
|
644
|
-
touch(this);
|
|
845
|
+
touch(this, [":state"]);
|
|
645
846
|
}
|
|
646
847
|
|
|
647
848
|
// Overflow inside elements is not scrollable here, so the scroll size is the
|
|
@@ -664,6 +865,10 @@ class ElementBinding extends ParentBinding {
|
|
|
664
865
|
}
|
|
665
866
|
}
|
|
666
867
|
|
|
868
|
+
function isMedia(wrapper) {
|
|
869
|
+
return wrapper.tagName === "VIDEO" || wrapper.tagName === "AUDIO";
|
|
870
|
+
}
|
|
871
|
+
|
|
667
872
|
/** on* event handler properties (element.onclick = fn, script.onload = fn). */
|
|
668
873
|
export const EVENT_HANDLER_NAMES = Object.freeze([
|
|
669
874
|
"click", "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave",
|
|
@@ -721,6 +926,26 @@ for (const property of REFLECTED_PROPERTIES) {
|
|
|
721
926
|
node[property] = typeof node[property] === "boolean" ? Boolean(value)
|
|
722
927
|
: typeof node[property] === "number" ? Number(value)
|
|
723
928
|
: String(value);
|
|
929
|
+
touch(this, [":state"]);
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// HTMLHyperlinkElementUtils: anchors and areas expose their parsed URL, not
|
|
935
|
+
// only the reflected href string. Happy DOM owns resolution against baseURI;
|
|
936
|
+
// bindings forward the primitive components without exposing the host node.
|
|
937
|
+
for (const property of ["origin", "protocol", "username", "password", "host", "hostname", "port", "pathname", "search", "hash"]) {
|
|
938
|
+
Object.defineProperty(ElementBinding.prototype, property, {
|
|
939
|
+
configurable: true,
|
|
940
|
+
enumerable: true,
|
|
941
|
+
get() {
|
|
942
|
+
const node = nodeOf(this);
|
|
943
|
+
return node.tagName === "A" || node.tagName === "AREA" ? String(node[property] ?? "") : undefined;
|
|
944
|
+
},
|
|
945
|
+
set(value) {
|
|
946
|
+
const node = nodeOf(this);
|
|
947
|
+
if (node.tagName !== "A" && node.tagName !== "AREA") return;
|
|
948
|
+
node[property] = String(value);
|
|
724
949
|
touch(this);
|
|
725
950
|
},
|
|
726
951
|
});
|
|
@@ -736,7 +961,8 @@ class DocumentBinding extends ParentBinding {
|
|
|
736
961
|
}
|
|
737
962
|
|
|
738
963
|
get body() {
|
|
739
|
-
|
|
964
|
+
const realm = realmOf(this);
|
|
965
|
+
return realm.wrap(realm.hooks.body ? realm.hooks.body() : nodeOf(this).body);
|
|
740
966
|
}
|
|
741
967
|
|
|
742
968
|
get title() {
|
|
@@ -751,6 +977,15 @@ class DocumentBinding extends ParentBinding {
|
|
|
751
977
|
return realmOf(this).hooks.readyState?.() ?? "complete";
|
|
752
978
|
}
|
|
753
979
|
|
|
980
|
+
// Page Visibility: the renderer only runs the page its client is showing.
|
|
981
|
+
get visibilityState() {
|
|
982
|
+
return "visible";
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
get hidden() {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
|
|
754
989
|
get URL() {
|
|
755
990
|
return nodeOf(this).URL;
|
|
756
991
|
}
|
|
@@ -759,6 +994,14 @@ class DocumentBinding extends ParentBinding {
|
|
|
759
994
|
return nodeOf(this).URL;
|
|
760
995
|
}
|
|
761
996
|
|
|
997
|
+
get location() {
|
|
998
|
+
return realmOf(this).hooks.pageGlobal?.()?.location ?? null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
get domain() {
|
|
1002
|
+
return new URL(nodeOf(this).URL).hostname;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
762
1005
|
get characterSet() {
|
|
763
1006
|
return "UTF-8";
|
|
764
1007
|
}
|
|
@@ -832,6 +1075,10 @@ class DocumentBinding extends ParentBinding {
|
|
|
832
1075
|
return realmOf(this).wrap(nodeOf(this).createComment(String(text)));
|
|
833
1076
|
}
|
|
834
1077
|
|
|
1078
|
+
createTreeWalker(root, whatToShow = NodeFilter.SHOW_ALL, filter = null) {
|
|
1079
|
+
return new TreeWalkerBinding(CONSTRUCT, realmOf(this), requireNode(root), whatToShow, filter);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
835
1082
|
createDocumentFragment() {
|
|
836
1083
|
return realmOf(this).wrap(nodeOf(this).createDocumentFragment());
|
|
837
1084
|
}
|
|
@@ -909,6 +1156,22 @@ class EventBinding {
|
|
|
909
1156
|
return this.#detail ?? null;
|
|
910
1157
|
}
|
|
911
1158
|
|
|
1159
|
+
// PopStateEvent and HashChangeEvent fields; the engine only fires popstate
|
|
1160
|
+
// with a null state, so no engine object reaches the page.
|
|
1161
|
+
get state() {
|
|
1162
|
+
if (!("state" in this.#event)) return undefined;
|
|
1163
|
+
const state = this.#event.state;
|
|
1164
|
+
return state !== null && typeof state === "object" ? null : state ?? null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
get oldURL() {
|
|
1168
|
+
return typeof this.#event.oldURL === "string" ? this.#event.oldURL : undefined;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
get newURL() {
|
|
1172
|
+
return typeof this.#event.newURL === "string" ? this.#event.newURL : undefined;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
912
1175
|
get key() {
|
|
913
1176
|
return this.#event.key;
|
|
914
1177
|
}
|
|
@@ -1002,6 +1265,159 @@ class EventBinding {
|
|
|
1002
1265
|
}
|
|
1003
1266
|
}
|
|
1004
1267
|
|
|
1268
|
+
export const NodeFilter = Object.freeze({
|
|
1269
|
+
FILTER_ACCEPT: 1, FILTER_REJECT: 2, FILTER_SKIP: 3,
|
|
1270
|
+
SHOW_ALL: 0xffffffff, SHOW_ELEMENT: 0x1, SHOW_ATTRIBUTE: 0x2, SHOW_TEXT: 0x4, SHOW_CDATA_SECTION: 0x8,
|
|
1271
|
+
SHOW_PROCESSING_INSTRUCTION: 0x40, SHOW_COMMENT: 0x80, SHOW_DOCUMENT: 0x100, SHOW_DOCUMENT_TYPE: 0x200,
|
|
1272
|
+
SHOW_DOCUMENT_FRAGMENT: 0x400,
|
|
1273
|
+
});
|
|
1274
|
+
const { FILTER_ACCEPT, FILTER_REJECT, FILTER_SKIP } = NodeFilter;
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* DOM §6.2 TreeWalker over the real nodes. The page's filter (a function or
|
|
1278
|
+
* an object with acceptNode) sees node wrappers, and its exceptions reach the
|
|
1279
|
+
* caller as the spec says.
|
|
1280
|
+
*/
|
|
1281
|
+
class TreeWalkerBinding {
|
|
1282
|
+
#realm;
|
|
1283
|
+
#root;
|
|
1284
|
+
#current;
|
|
1285
|
+
#whatToShow;
|
|
1286
|
+
#filter;
|
|
1287
|
+
|
|
1288
|
+
constructor(key, realm, root, whatToShow, filter) {
|
|
1289
|
+
if (key !== CONSTRUCT) throw new TypeError("Illegal constructor");
|
|
1290
|
+
this.#realm = realm;
|
|
1291
|
+
this.#root = root;
|
|
1292
|
+
this.#current = root;
|
|
1293
|
+
this.#whatToShow = Number(whatToShow) >>> 0;
|
|
1294
|
+
this.#filter = filter ?? null;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
get root() { return this.#realm.wrap(this.#root); }
|
|
1298
|
+
get whatToShow() { return this.#whatToShow; }
|
|
1299
|
+
get filter() { return this.#filter; }
|
|
1300
|
+
get currentNode() { return this.#realm.wrap(this.#current); }
|
|
1301
|
+
set currentNode(value) { this.#current = requireNode(value); }
|
|
1302
|
+
|
|
1303
|
+
#accept(node) {
|
|
1304
|
+
if (!(this.#whatToShow & (1 << (node.nodeType - 1)))) return FILTER_SKIP;
|
|
1305
|
+
const filter = this.#filter;
|
|
1306
|
+
if (filter === null) return FILTER_ACCEPT;
|
|
1307
|
+
const wrapper = this.#realm.wrap(node);
|
|
1308
|
+
const result = typeof filter === "function"
|
|
1309
|
+
? Reflect.apply(filter, undefined, [wrapper])
|
|
1310
|
+
: Reflect.apply(filter.acceptNode, filter, [wrapper]);
|
|
1311
|
+
return Number(result);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
#move(node) {
|
|
1315
|
+
this.#current = node;
|
|
1316
|
+
return this.#realm.wrap(node);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
parentNode() {
|
|
1320
|
+
let node = this.#current;
|
|
1321
|
+
while (node && node !== this.#root) {
|
|
1322
|
+
node = node.parentNode;
|
|
1323
|
+
if (node && this.#accept(node) === FILTER_ACCEPT) return this.#move(node);
|
|
1324
|
+
}
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
firstChild() { return this.#children(true); }
|
|
1329
|
+
lastChild() { return this.#children(false); }
|
|
1330
|
+
nextSibling() { return this.#siblings(true); }
|
|
1331
|
+
previousSibling() { return this.#siblings(false); }
|
|
1332
|
+
|
|
1333
|
+
#children(first) {
|
|
1334
|
+
let node = first ? this.#current.firstChild : this.#current.lastChild;
|
|
1335
|
+
while (node) {
|
|
1336
|
+
const result = this.#accept(node);
|
|
1337
|
+
if (result === FILTER_ACCEPT) return this.#move(node);
|
|
1338
|
+
if (result === FILTER_SKIP) {
|
|
1339
|
+
const child = first ? node.firstChild : node.lastChild;
|
|
1340
|
+
if (child) {
|
|
1341
|
+
node = child;
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
while (node) {
|
|
1346
|
+
const sibling = first ? node.nextSibling : node.previousSibling;
|
|
1347
|
+
if (sibling) {
|
|
1348
|
+
node = sibling;
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
const parent = node.parentNode;
|
|
1352
|
+
if (!parent || parent === this.#root || parent === this.#current) return null;
|
|
1353
|
+
node = parent;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
#siblings(next) {
|
|
1360
|
+
let node = this.#current;
|
|
1361
|
+
if (node === this.#root) return null;
|
|
1362
|
+
for (;;) {
|
|
1363
|
+
let sibling = next ? node.nextSibling : node.previousSibling;
|
|
1364
|
+
while (sibling) {
|
|
1365
|
+
node = sibling;
|
|
1366
|
+
const result = this.#accept(node);
|
|
1367
|
+
if (result === FILTER_ACCEPT) return this.#move(node);
|
|
1368
|
+
sibling = next ? node.firstChild : node.lastChild;
|
|
1369
|
+
if (result === FILTER_REJECT || !sibling) sibling = next ? node.nextSibling : node.previousSibling;
|
|
1370
|
+
}
|
|
1371
|
+
node = node.parentNode;
|
|
1372
|
+
if (!node || node === this.#root) return null;
|
|
1373
|
+
if (this.#accept(node) === FILTER_ACCEPT) return null;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
previousNode() {
|
|
1378
|
+
let node = this.#current;
|
|
1379
|
+
while (node !== this.#root) {
|
|
1380
|
+
let sibling = node.previousSibling;
|
|
1381
|
+
while (sibling) {
|
|
1382
|
+
node = sibling;
|
|
1383
|
+
let result = this.#accept(node);
|
|
1384
|
+
while (result !== FILTER_REJECT && node.lastChild) {
|
|
1385
|
+
node = node.lastChild;
|
|
1386
|
+
result = this.#accept(node);
|
|
1387
|
+
}
|
|
1388
|
+
if (result === FILTER_ACCEPT) return this.#move(node);
|
|
1389
|
+
sibling = node.previousSibling;
|
|
1390
|
+
}
|
|
1391
|
+
if (node === this.#root || !node.parentNode) return null;
|
|
1392
|
+
node = node.parentNode;
|
|
1393
|
+
if (this.#accept(node) === FILTER_ACCEPT) return this.#move(node);
|
|
1394
|
+
}
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
nextNode() {
|
|
1399
|
+
let node = this.#current;
|
|
1400
|
+
let result = FILTER_ACCEPT;
|
|
1401
|
+
for (;;) {
|
|
1402
|
+
while (result !== FILTER_REJECT && node.firstChild) {
|
|
1403
|
+
node = node.firstChild;
|
|
1404
|
+
result = this.#accept(node);
|
|
1405
|
+
if (result === FILTER_ACCEPT) return this.#move(node);
|
|
1406
|
+
}
|
|
1407
|
+
let sibling = null;
|
|
1408
|
+
for (let temporary = node; temporary; temporary = temporary.parentNode) {
|
|
1409
|
+
if (temporary === this.#root) return null;
|
|
1410
|
+
sibling = temporary.nextSibling;
|
|
1411
|
+
if (sibling) break;
|
|
1412
|
+
}
|
|
1413
|
+
if (!sibling) return null;
|
|
1414
|
+
node = sibling;
|
|
1415
|
+
result = this.#accept(node);
|
|
1416
|
+
if (result === FILTER_ACCEPT) return this.#move(node);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1005
1421
|
class ClassListBinding {
|
|
1006
1422
|
#node;
|
|
1007
1423
|
#realm;
|
|
@@ -1029,30 +1445,35 @@ class ClassListBinding {
|
|
|
1029
1445
|
}
|
|
1030
1446
|
|
|
1031
1447
|
add(...tokens) {
|
|
1448
|
+
const before = this.#node.className;
|
|
1032
1449
|
this.#node.classList.add(...tokens.map(String));
|
|
1033
|
-
this.#realm.hooks.touch();
|
|
1450
|
+
if (this.#node.className !== before) this.#realm.hooks.touch(this.#node);
|
|
1034
1451
|
}
|
|
1035
1452
|
|
|
1036
1453
|
remove(...tokens) {
|
|
1454
|
+
const before = this.#node.className;
|
|
1037
1455
|
this.#node.classList.remove(...tokens.map(String));
|
|
1038
|
-
this.#realm.hooks.touch();
|
|
1456
|
+
if (this.#node.className !== before) this.#realm.hooks.touch(this.#node);
|
|
1039
1457
|
}
|
|
1040
1458
|
|
|
1041
1459
|
toggle(token, force) {
|
|
1042
1460
|
const result = this.#node.classList.toggle(String(token), force === undefined ? undefined : Boolean(force));
|
|
1043
|
-
this.#realm.hooks.touch();
|
|
1461
|
+
this.#realm.hooks.touch(this.#node);
|
|
1044
1462
|
return result;
|
|
1045
1463
|
}
|
|
1046
1464
|
|
|
1047
1465
|
replace(oldToken, newToken) {
|
|
1048
1466
|
const result = this.#node.classList.replace(String(oldToken), String(newToken));
|
|
1049
|
-
this.#realm.hooks.touch();
|
|
1467
|
+
this.#realm.hooks.touch(this.#node);
|
|
1050
1468
|
return result;
|
|
1051
1469
|
}
|
|
1052
1470
|
|
|
1053
1471
|
set value(value) {
|
|
1054
|
-
|
|
1055
|
-
this.#
|
|
1472
|
+
const next = String(value);
|
|
1473
|
+
if (this.#node.className !== next) {
|
|
1474
|
+
this.#node.className = next;
|
|
1475
|
+
this.#realm.hooks.touch(this.#node);
|
|
1476
|
+
}
|
|
1056
1477
|
}
|
|
1057
1478
|
|
|
1058
1479
|
forEach(callback, thisArg) {
|
|
@@ -1081,6 +1502,20 @@ class ClassListBinding {
|
|
|
1081
1502
|
}
|
|
1082
1503
|
}
|
|
1083
1504
|
|
|
1505
|
+
/**
|
|
1506
|
+
* Properties Happy DOM stores but the renderer does not implement. `in` says
|
|
1507
|
+
* no for them, so feature detection (`"anchorName" in el.style`) takes the
|
|
1508
|
+
* page's fallback path instead of relying on layout that never happens.
|
|
1509
|
+
*/
|
|
1510
|
+
const UNRENDERED_STYLE_PROPERTIES = new Set([
|
|
1511
|
+
// CSS Anchor Positioning
|
|
1512
|
+
"anchorName", "anchorScope", "positionAnchor", "positionArea", "insetArea", "positionTry",
|
|
1513
|
+
"positionTryFallbacks", "positionTryOrder", "positionVisibility",
|
|
1514
|
+
// Scroll-driven animations
|
|
1515
|
+
"animationTimeline", "scrollTimeline", "scrollTimelineAxis", "scrollTimelineName", "viewTimeline",
|
|
1516
|
+
"viewTimelineAxis", "viewTimelineInset", "viewTimelineName",
|
|
1517
|
+
]);
|
|
1518
|
+
|
|
1084
1519
|
/**
|
|
1085
1520
|
* element.style: a proxy that reads and writes the real declaration block
|
|
1086
1521
|
* with string values only. Mutations go to the style attribute, which the
|
|
@@ -1093,12 +1528,13 @@ function styleBinding(node, realm) {
|
|
|
1093
1528
|
const methods = {
|
|
1094
1529
|
getPropertyValue: (property) => style.getPropertyValue(String(property)),
|
|
1095
1530
|
setProperty: (property, value, priority = "") => {
|
|
1531
|
+
const before = style.cssText;
|
|
1096
1532
|
style.setProperty(String(property), value === null ? "" : String(value), String(priority));
|
|
1097
|
-
realm.hooks.touch();
|
|
1533
|
+
if (style.cssText !== before) realm.hooks.touch(node);
|
|
1098
1534
|
},
|
|
1099
1535
|
removeProperty: (property) => {
|
|
1100
1536
|
const previous = style.removeProperty(String(property));
|
|
1101
|
-
realm.hooks.touch();
|
|
1537
|
+
realm.hooks.touch(node);
|
|
1102
1538
|
return previous;
|
|
1103
1539
|
},
|
|
1104
1540
|
};
|
|
@@ -1112,12 +1548,18 @@ function styleBinding(node, realm) {
|
|
|
1112
1548
|
},
|
|
1113
1549
|
set(_target, property, value) {
|
|
1114
1550
|
if (typeof property !== "string") return false;
|
|
1551
|
+
const before = style.cssText;
|
|
1115
1552
|
if (property === "cssText") style.cssText = String(value);
|
|
1116
1553
|
else style.setProperty(toKebab(property), value === null ? "" : String(value));
|
|
1117
|
-
realm.hooks.touch();
|
|
1554
|
+
if (style.cssText !== before) realm.hooks.touch(node);
|
|
1118
1555
|
return true;
|
|
1119
1556
|
},
|
|
1120
|
-
has: (_target, property) =>
|
|
1557
|
+
has: (_target, property) => {
|
|
1558
|
+
if (typeof property !== "string") return false;
|
|
1559
|
+
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));
|
|
1562
|
+
},
|
|
1121
1563
|
defineProperty: () => false,
|
|
1122
1564
|
deleteProperty: () => false,
|
|
1123
1565
|
getPrototypeOf: () => null,
|
|
@@ -1136,14 +1578,14 @@ function datasetBinding(node, realm) {
|
|
|
1136
1578
|
set(_target, property, value) {
|
|
1137
1579
|
if (typeof property !== "string") return false;
|
|
1138
1580
|
node.setAttribute(attribute(property), String(value));
|
|
1139
|
-
realm.hooks.touch();
|
|
1581
|
+
realm.hooks.touch(node);
|
|
1140
1582
|
return true;
|
|
1141
1583
|
},
|
|
1142
1584
|
has: (_target, property) => typeof property === "string" && node.hasAttribute(attribute(property)),
|
|
1143
1585
|
deleteProperty(_target, property) {
|
|
1144
1586
|
if (typeof property !== "string") return false;
|
|
1145
1587
|
node.removeAttribute(attribute(property));
|
|
1146
|
-
realm.hooks.touch();
|
|
1588
|
+
realm.hooks.touch(node);
|
|
1147
1589
|
return true;
|
|
1148
1590
|
},
|
|
1149
1591
|
ownKeys: () => node.getAttributeNames()
|
|
@@ -1203,6 +1645,49 @@ export function windowEventMethods(realm, pageGlobal) {
|
|
|
1203
1645
|
};
|
|
1204
1646
|
}
|
|
1205
1647
|
|
|
1648
|
+
/**
|
|
1649
|
+
* Page-constructible EventTarget (`new EventTarget()` and subclasses). Each
|
|
1650
|
+
* instance is backed by its own Happy DOM EventTarget; `targetOf` maps that
|
|
1651
|
+
* back to the page object for event.target and the listener's `this`.
|
|
1652
|
+
*/
|
|
1653
|
+
export function eventTargetConstructor(realm, isTarget) {
|
|
1654
|
+
const backing = new WeakMap();
|
|
1655
|
+
const owners = new WeakMap();
|
|
1656
|
+
const realOf = (self) => {
|
|
1657
|
+
const real = backing.get(self);
|
|
1658
|
+
if (!real) throw new TypeError("Illegal invocation");
|
|
1659
|
+
return real;
|
|
1660
|
+
};
|
|
1661
|
+
function EventTarget() {
|
|
1662
|
+
if (!new.target) throw new TypeError("Constructor EventTarget requires 'new'");
|
|
1663
|
+
const self = Object.create(new.target.prototype);
|
|
1664
|
+
const real = new realm.window.EventTarget();
|
|
1665
|
+
backing.set(self, real);
|
|
1666
|
+
owners.set(real, self);
|
|
1667
|
+
return self;
|
|
1668
|
+
}
|
|
1669
|
+
EventTarget.prototype = {
|
|
1670
|
+
constructor: EventTarget,
|
|
1671
|
+
addEventListener(type, listener, options) {
|
|
1672
|
+
addListener(realm, realOf(this), type, listener, options);
|
|
1673
|
+
},
|
|
1674
|
+
removeEventListener(type, listener, options) {
|
|
1675
|
+
removeListener(realm, realOf(this), type, listener, options);
|
|
1676
|
+
},
|
|
1677
|
+
dispatchEvent(event) {
|
|
1678
|
+
const real = EventBinding.unwrap(event);
|
|
1679
|
+
if (!real) throw new TypeError("dispatchEvent requires an Event");
|
|
1680
|
+
return realOf(this).dispatchEvent(real);
|
|
1681
|
+
},
|
|
1682
|
+
};
|
|
1683
|
+
// Nodes and the window are event targets too, though not built from this constructor.
|
|
1684
|
+
Object.defineProperty(EventTarget, Symbol.hasInstance, {
|
|
1685
|
+
value: (value) => backing.has(value) || isTarget(value)
|
|
1686
|
+
|| Function.prototype[Symbol.hasInstance].call(EventTarget, value),
|
|
1687
|
+
});
|
|
1688
|
+
return { EventTarget, targetOf: (real) => owners.get(real) ?? null };
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1206
1691
|
const UI_EVENT_TYPES = ["UIEvent", "MouseEvent", "PointerEvent", "WheelEvent", "KeyboardEvent", "FocusEvent", "InputEvent"];
|
|
1207
1692
|
const UI_EVENT_FIELDS = [
|
|
1208
1693
|
...["detail", "clientX", "clientY", "screenX", "screenY", "button", "buttons", "deltaX", "deltaY", "deltaZ", "deltaMode",
|
|
@@ -1221,15 +1706,16 @@ export function eventConstructors(realm) {
|
|
|
1221
1706
|
});
|
|
1222
1707
|
function Event(type, options = undefined) {
|
|
1223
1708
|
if (!new.target) throw new TypeError("Constructor Event requires 'new'");
|
|
1224
|
-
return
|
|
1709
|
+
return realm.wrapEvent(new window.Event(String(type), init(options)));
|
|
1225
1710
|
}
|
|
1226
1711
|
function CustomEvent(type, options = undefined) {
|
|
1227
1712
|
if (!new.target) throw new TypeError("Constructor CustomEvent requires 'new'");
|
|
1228
1713
|
// detail stays in the binding: page values never enter Happy DOM.
|
|
1229
|
-
return
|
|
1714
|
+
return realm.wrapEvent(new window.CustomEvent(String(type), init(options)), options?.detail);
|
|
1230
1715
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1716
|
+
realm.eventPrototype = mutableBindingPrototype(EventBinding.prototype);
|
|
1717
|
+
Event.prototype = realm.eventPrototype;
|
|
1718
|
+
CustomEvent.prototype = realm.eventPrototype;
|
|
1233
1719
|
// UI event subclasses: their init dictionaries are copied as primitives only
|
|
1234
1720
|
// (view and relatedTarget are dropped).
|
|
1235
1721
|
const uiEvents = {};
|
|
@@ -1242,10 +1728,10 @@ export function eventConstructors(realm) {
|
|
|
1242
1728
|
for (const [key, coerce] of UI_EVENT_FIELDS) {
|
|
1243
1729
|
if (options?.[key] !== undefined) fields[key] = coerce(options[key]);
|
|
1244
1730
|
}
|
|
1245
|
-
return
|
|
1731
|
+
return realm.wrapEvent(new RealEvent(String(type), fields));
|
|
1246
1732
|
};
|
|
1247
1733
|
Object.defineProperty(Constructor, "name", { value: name });
|
|
1248
|
-
Constructor.prototype =
|
|
1734
|
+
Constructor.prototype = realm.eventPrototype;
|
|
1249
1735
|
uiEvents[name] = Constructor;
|
|
1250
1736
|
}
|
|
1251
1737
|
return { Event, CustomEvent, ...uiEvents };
|
|
@@ -1271,8 +1757,8 @@ function cssEscape(value) {
|
|
|
1271
1757
|
*/
|
|
1272
1758
|
export function hardenBindings() {
|
|
1273
1759
|
if (typeof harden !== "function") return;
|
|
1274
|
-
for (const Binding of [NodeBinding, TextBinding, ParentBinding, FragmentBinding, ElementBinding,
|
|
1275
|
-
DocumentBinding, EventBinding, ClassListBinding]) {
|
|
1760
|
+
for (const Binding of [NodeBinding, CharacterDataBinding, TextBinding, ParentBinding, FragmentBinding, ShadowRootBinding, ElementBinding,
|
|
1761
|
+
DocumentBinding, EventBinding, ClassListBinding, TreeWalkerBinding]) {
|
|
1276
1762
|
harden(Binding);
|
|
1277
1763
|
}
|
|
1278
1764
|
}
|
|
@@ -1280,12 +1766,14 @@ export function hardenBindings() {
|
|
|
1280
1766
|
/** Interface objects for `instanceof` checks; all throw "Illegal constructor" when called. */
|
|
1281
1767
|
export const INTERFACES = Object.freeze({
|
|
1282
1768
|
Node: NodeBinding,
|
|
1283
|
-
CharacterData:
|
|
1769
|
+
CharacterData: CharacterDataBinding,
|
|
1284
1770
|
Text: TextBinding,
|
|
1285
1771
|
Element: ElementBinding,
|
|
1286
1772
|
HTMLElement: ElementBinding,
|
|
1287
1773
|
Document: DocumentBinding,
|
|
1288
1774
|
HTMLDocument: DocumentBinding,
|
|
1289
1775
|
DocumentFragment: FragmentBinding,
|
|
1776
|
+
ShadowRoot: ShadowRootBinding,
|
|
1777
|
+
TreeWalker: TreeWalkerBinding,
|
|
1290
1778
|
DOMTokenList: ClassListBinding,
|
|
1291
1779
|
});
|