@miaskiewicz/turbo-dom 0.1.6 → 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
|
@@ -385,6 +385,10 @@ export class Element extends Node {
|
|
|
385
385
|
}
|
|
386
386
|
get defaultValue() { return this.getAttribute('value') ?? ''; }
|
|
387
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) : ''; }
|
|
388
392
|
|
|
389
393
|
get selectionStart() { return this.__selStart ?? null; }
|
|
390
394
|
set selectionStart(v) { this.__selStart = v; }
|
|
@@ -424,7 +428,17 @@ export class Element extends Node {
|
|
|
424
428
|
|
|
425
429
|
// option
|
|
426
430
|
get selected() { return this.__selected !== undefined ? this.__selected : this.hasAttribute('selected'); }
|
|
427
|
-
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
|
+
}
|
|
428
442
|
get defaultSelected() { return this.hasAttribute('selected'); }
|
|
429
443
|
get text() { return this.textContent; }
|
|
430
444
|
set text(v) { this.textContent = v; }
|
|
@@ -513,14 +527,43 @@ export class Element extends Node {
|
|
|
513
527
|
}
|
|
514
528
|
|
|
515
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
|
+
|
|
516
561
|
// default actions applied post-dispatch when not preventDefault'd
|
|
517
562
|
__runDefaultAction(e) {
|
|
518
563
|
if (e.type !== 'click') return;
|
|
519
564
|
if (this.localName === 'input') {
|
|
520
565
|
const t = (this.getAttribute('type') || 'text').toLowerCase();
|
|
521
|
-
if (t === '
|
|
522
|
-
else if (t === 'radio') this.checked = true;
|
|
523
|
-
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(); }
|
|
524
567
|
} else if (this.localName === 'button') {
|
|
525
568
|
const t = (this.getAttribute('type') || 'submit').toLowerCase();
|
|
526
569
|
if (t === 'submit') { const f = this.closest('form'); if (f) f.requestSubmit(); }
|
|
@@ -779,9 +822,14 @@ function makeStyle(el) {
|
|
|
779
822
|
return new Proxy({}, {
|
|
780
823
|
get(_t, key) {
|
|
781
824
|
if (key === 'getPropertyValue') return (p) => parse().get(p) ?? '';
|
|
825
|
+
if (key === 'getPropertyPriority') return () => ''; // we don't model !important
|
|
782
826
|
if (key === 'setProperty') return (p, v) => { const m = parse(); m.set(p, v); write(m); };
|
|
783
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;
|
|
784
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); }
|
|
785
833
|
if (typeof key !== 'string') return undefined;
|
|
786
834
|
return parse().get(kebab(key)) ?? '';
|
|
787
835
|
},
|
|
@@ -789,6 +837,7 @@ function makeStyle(el) {
|
|
|
789
837
|
if (key === 'cssText') { el.setAttribute('style', String(value)); return true; }
|
|
790
838
|
const m = parse(); m.set(kebab(key), String(value)); write(m); return true;
|
|
791
839
|
},
|
|
840
|
+
has(_t, key) { return typeof key === 'string'; },
|
|
792
841
|
});
|
|
793
842
|
}
|
|
794
843
|
const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
package/src/runtime/events.mjs
CHANGED
|
@@ -153,6 +153,13 @@ export class EventTarget {
|
|
|
153
153
|
event._path = this.__eventPath();
|
|
154
154
|
const path = event._path;
|
|
155
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
|
+
|
|
156
163
|
const invoke = (node, phase) => {
|
|
157
164
|
const list = node.__listeners && node.__listeners.get(event.type);
|
|
158
165
|
if (!list || list.length === 0) return;
|
|
@@ -196,7 +203,10 @@ export class EventTarget {
|
|
|
196
203
|
event.eventPhase = PHASE_NONE;
|
|
197
204
|
event.currentTarget = null;
|
|
198
205
|
|
|
199
|
-
//
|
|
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
|
|
200
210
|
if (!event.defaultPrevented && typeof this.__runDefaultAction === 'function') {
|
|
201
211
|
this.__runDefaultAction(event);
|
|
202
212
|
}
|
|
Binary file
|