@miaskiewicz/turbo-dom 0.1.5 → 0.1.7
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miaskiewicz/turbo-dom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Faster, more spec-correct DOM for test runners — native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
// across the boundary — a buffer-backed read and an owned read are indistinguishable.
|
|
8
8
|
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
EventTarget, Event, CustomEvent,
|
|
12
|
+
UIEvent, MouseEvent, KeyboardEvent, FocusEvent,
|
|
13
|
+
} from './events.mjs';
|
|
11
14
|
import { liveNodeList, liveHTMLCollection } from './collections.mjs';
|
|
12
15
|
import { matchesSelector, querySelector as qsel, querySelectorAll as qselAll } from './selectors.mjs';
|
|
13
16
|
import { serializeInner, serializeOuter } from './html-serialize.mjs';
|
|
@@ -382,6 +385,10 @@ export class Element extends Node {
|
|
|
382
385
|
}
|
|
383
386
|
get defaultValue() { return this.getAttribute('value') ?? ''; }
|
|
384
387
|
set defaultValue(v) { this.setAttribute('value', v); }
|
|
388
|
+
get valueAsNumber() { const v = this.value; return v === '' || v == null ? NaN : Number(v); }
|
|
389
|
+
set valueAsNumber(n) { this.value = String(n); }
|
|
390
|
+
get valueAsDate() { const v = this.value; const d = v ? new Date(v) : null; return d && !isNaN(d) ? d : null; }
|
|
391
|
+
set valueAsDate(d) { this.value = d instanceof Date ? d.toISOString().slice(0, 10) : ''; }
|
|
385
392
|
|
|
386
393
|
get selectionStart() { return this.__selStart ?? null; }
|
|
387
394
|
set selectionStart(v) { this.__selStart = v; }
|
|
@@ -421,7 +428,17 @@ export class Element extends Node {
|
|
|
421
428
|
|
|
422
429
|
// option
|
|
423
430
|
get selected() { return this.__selected !== undefined ? this.__selected : this.hasAttribute('selected'); }
|
|
424
|
-
set selected(x) {
|
|
431
|
+
set selected(x) {
|
|
432
|
+
this.__selected = !!x;
|
|
433
|
+
// single-select: selecting one option deselects the others (exclusive)
|
|
434
|
+
if (x && this.localName === 'option') {
|
|
435
|
+
let sel = this.parentNode;
|
|
436
|
+
while (sel && sel.localName !== 'select') sel = sel.parentNode;
|
|
437
|
+
if (sel && !sel.multiple) {
|
|
438
|
+
for (const o of sel.getElementsByTagName('option')) if (o !== this) o.__selected = false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
425
442
|
get defaultSelected() { return this.hasAttribute('selected'); }
|
|
426
443
|
get text() { return this.textContent; }
|
|
427
444
|
set text(v) { this.textContent = v; }
|
|
@@ -510,14 +527,43 @@ export class Element extends Node {
|
|
|
510
527
|
}
|
|
511
528
|
|
|
512
529
|
click() { this.dispatchEvent(new Event('click', { bubbles: true, cancelable: true })); }
|
|
530
|
+
// pre-click activation (runs BEFORE click listeners; undone if preventDefault)
|
|
531
|
+
__preClickActivation() {
|
|
532
|
+
if (this.localName !== 'input') return null;
|
|
533
|
+
const t = (this.getAttribute('type') || 'text').toLowerCase();
|
|
534
|
+
// Write the internal field directly — NOT via the `checked` setter, which
|
|
535
|
+
// React wraps with its value-tracker. Going through the setter would update
|
|
536
|
+
// React's tracked value too, hiding the change and suppressing onChange.
|
|
537
|
+
if (t === 'checkbox') {
|
|
538
|
+
const old = this.checked; this.__checked = !old;
|
|
539
|
+
return { undo: () => { this.__checked = old; } };
|
|
540
|
+
}
|
|
541
|
+
if (t === 'radio') {
|
|
542
|
+
if (this.checked) return null;
|
|
543
|
+
const group = this.__radioGroup();
|
|
544
|
+
const prev = group.find((r) => r.checked) || null;
|
|
545
|
+
group.forEach((r) => { r.__checked = false; });
|
|
546
|
+
this.__checked = true;
|
|
547
|
+
return { undo: () => { this.__checked = false; if (prev) prev.__checked = true; } };
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
__radioGroup() {
|
|
552
|
+
const name = this.getAttribute('name');
|
|
553
|
+
let root = this; while (root.parentNode) root = root.parentNode;
|
|
554
|
+
const form = this.closest && this.closest('form');
|
|
555
|
+
const scope = form || root;
|
|
556
|
+
if (!name) return [this];
|
|
557
|
+
return (scope.getElementsByTagName ? Array.from(scope.getElementsByTagName('input')) : [])
|
|
558
|
+
.filter((i) => i.localName === 'input' && (i.getAttribute('type') || '').toLowerCase() === 'radio' && i.getAttribute('name') === name);
|
|
559
|
+
}
|
|
560
|
+
|
|
513
561
|
// default actions applied post-dispatch when not preventDefault'd
|
|
514
562
|
__runDefaultAction(e) {
|
|
515
563
|
if (e.type !== 'click') return;
|
|
516
564
|
if (this.localName === 'input') {
|
|
517
565
|
const t = (this.getAttribute('type') || 'text').toLowerCase();
|
|
518
|
-
if (t === '
|
|
519
|
-
else if (t === 'radio') this.checked = true;
|
|
520
|
-
else if (t === 'submit') { const f = this.closest('form'); if (f) f.requestSubmit(); }
|
|
566
|
+
if (t === 'submit') { const f = this.closest('form'); if (f) f.requestSubmit(); }
|
|
521
567
|
} else if (this.localName === 'button') {
|
|
522
568
|
const t = (this.getAttribute('type') || 'submit').toLowerCase();
|
|
523
569
|
if (t === 'submit') { const f = this.closest('form'); if (f) f.requestSubmit(); }
|
|
@@ -776,9 +822,14 @@ function makeStyle(el) {
|
|
|
776
822
|
return new Proxy({}, {
|
|
777
823
|
get(_t, key) {
|
|
778
824
|
if (key === 'getPropertyValue') return (p) => parse().get(p) ?? '';
|
|
825
|
+
if (key === 'getPropertyPriority') return () => ''; // we don't model !important
|
|
779
826
|
if (key === 'setProperty') return (p, v) => { const m = parse(); m.set(p, v); write(m); };
|
|
780
827
|
if (key === 'removeProperty') return (p) => { const m = parse(); const v = m.get(p) ?? ''; m.delete(p); write(m); return v; };
|
|
828
|
+
if (key === 'item') return (i) => [...parse().keys()][i] ?? '';
|
|
829
|
+
if (key === 'length') return parse().size;
|
|
781
830
|
if (key === 'cssText') return el.getAttribute('style') || '';
|
|
831
|
+
if (key === 'cssFloat') return parse().get('float') ?? '';
|
|
832
|
+
if (key === Symbol.iterator) { const keys = [...parse().keys()]; return keys[Symbol.iterator].bind(keys); }
|
|
782
833
|
if (typeof key !== 'string') return undefined;
|
|
783
834
|
return parse().get(kebab(key)) ?? '';
|
|
784
835
|
},
|
|
@@ -786,6 +837,7 @@ function makeStyle(el) {
|
|
|
786
837
|
if (key === 'cssText') { el.setAttribute('style', String(value)); return true; }
|
|
787
838
|
const m = parse(); m.set(kebab(key), String(value)); write(m); return true;
|
|
788
839
|
},
|
|
840
|
+
has(_t, key) { return typeof key === 'string'; },
|
|
789
841
|
});
|
|
790
842
|
}
|
|
791
843
|
const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
@@ -988,7 +1040,16 @@ export class Document extends Node {
|
|
|
988
1040
|
createTextNode(data) { return new Text(this, String(data)); }
|
|
989
1041
|
createComment(data) { return new Comment(this, String(data)); }
|
|
990
1042
|
createDocumentFragment() { return new DocumentFragment(this); }
|
|
991
|
-
createEvent(
|
|
1043
|
+
createEvent(type = 'Event') {
|
|
1044
|
+
switch (String(type)) {
|
|
1045
|
+
case 'CustomEvent': return new CustomEvent('');
|
|
1046
|
+
case 'MouseEvent': case 'MouseEvents': return new MouseEvent('');
|
|
1047
|
+
case 'KeyboardEvent': case 'KeyEvents': return new KeyboardEvent('');
|
|
1048
|
+
case 'UIEvent': case 'UIEvents': return new UIEvent('');
|
|
1049
|
+
case 'FocusEvent': return new FocusEvent('');
|
|
1050
|
+
default: return new Event('');
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
992
1053
|
createRange() { return new Range(this); }
|
|
993
1054
|
createAttribute(name) { return { name, value: '', ownerElement: null }; }
|
|
994
1055
|
createComment(data) { return new Comment(this, String(data)); }
|
package/src/runtime/events.mjs
CHANGED
|
@@ -41,6 +41,13 @@ export class Event {
|
|
|
41
41
|
set returnValue(v) { if (v === false) this.preventDefault(); }
|
|
42
42
|
|
|
43
43
|
composedPath() { return this._path.slice(); }
|
|
44
|
+
|
|
45
|
+
// legacy init — react-dom's dev rethrow path uses createEvent('Event')+initEvent
|
|
46
|
+
initEvent(type, bubbles = false, cancelable = false) {
|
|
47
|
+
this.type = String(type);
|
|
48
|
+
this.bubbles = !!bubbles;
|
|
49
|
+
this.cancelable = !!cancelable;
|
|
50
|
+
}
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export class CustomEvent extends Event {
|
|
@@ -48,6 +55,10 @@ export class CustomEvent extends Event {
|
|
|
48
55
|
super(type, init);
|
|
49
56
|
this.detail = init.detail ?? null;
|
|
50
57
|
}
|
|
58
|
+
initCustomEvent(type, bubbles = false, cancelable = false, detail = null) {
|
|
59
|
+
this.initEvent(type, bubbles, cancelable);
|
|
60
|
+
this.detail = detail;
|
|
61
|
+
}
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
// Typed events. Real libraries (RTL/user-event) construct these by name; we copy
|
|
@@ -68,12 +79,19 @@ const TYPED_DEFAULTS = {
|
|
|
68
79
|
|
|
69
80
|
function makeTyped(name) {
|
|
70
81
|
const defaults = TYPED_DEFAULTS[name];
|
|
71
|
-
|
|
82
|
+
const cls = class extends Event {
|
|
72
83
|
constructor(type, init = {}) {
|
|
73
84
|
super(type, init);
|
|
74
85
|
Object.assign(this, defaults, init);
|
|
75
86
|
}
|
|
76
87
|
};
|
|
88
|
+
// legacy initMouseEvent/initKeyboardEvent/initUIEvent(type, bubbles, cancelable, ...rest)
|
|
89
|
+
cls.prototype['init' + name] = function (type, bubbles = false, cancelable = false, ...rest) {
|
|
90
|
+
this.initEvent(type, bubbles, cancelable);
|
|
91
|
+
// view is the common 4th arg for UI/Mouse/Keyboard events; ignore the rest's exact slots
|
|
92
|
+
if (rest.length) this.view = rest[0];
|
|
93
|
+
};
|
|
94
|
+
return cls;
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
export const UIEvent = makeTyped('UIEvent');
|
|
@@ -135,6 +153,13 @@ export class EventTarget {
|
|
|
135
153
|
event._path = this.__eventPath();
|
|
136
154
|
const path = event._path;
|
|
137
155
|
|
|
156
|
+
// pre-click activation (WHATWG): checkbox/radio toggle BEFORE click listeners
|
|
157
|
+
// run, so React's change detection sees the new value. Undone if preventDefault.
|
|
158
|
+
let activation = null;
|
|
159
|
+
if (event.type === 'click' && typeof this.__preClickActivation === 'function') {
|
|
160
|
+
activation = this.__preClickActivation();
|
|
161
|
+
}
|
|
162
|
+
|
|
138
163
|
const invoke = (node, phase) => {
|
|
139
164
|
const list = node.__listeners && node.__listeners.get(event.type);
|
|
140
165
|
if (!list || list.length === 0) return;
|
|
@@ -178,7 +203,10 @@ export class EventTarget {
|
|
|
178
203
|
event.eventPhase = PHASE_NONE;
|
|
179
204
|
event.currentTarget = null;
|
|
180
205
|
|
|
181
|
-
//
|
|
206
|
+
// canceled activation: undo the pre-click toggle if default was prevented
|
|
207
|
+
if (activation && event.defaultPrevented) activation.undo();
|
|
208
|
+
|
|
209
|
+
// remaining default actions (label→control, form submit) unless prevented
|
|
182
210
|
if (!event.defaultPrevented && typeof this.__runDefaultAction === 'function') {
|
|
183
211
|
this.__runDefaultAction(event);
|
|
184
212
|
}
|
package/src/runtime/window.mjs
CHANGED
|
@@ -179,9 +179,9 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
|
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
import { performance as nodePerformance } from 'node:perf_hooks';
|
|
182
183
|
function performanceNow() {
|
|
183
|
-
|
|
184
|
-
return s * 1000 + ns / 1e6;
|
|
184
|
+
return nodePerformance.now();
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
let __objUrlSeq = 0;
|
|
Binary file
|