@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.5",
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",
@@ -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 { EventTarget, Event, CustomEvent } from './events.mjs';
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) { this.__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 === 'checkbox') this.checked = !this.checked;
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() { return new Event(''); }
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)); }
@@ -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
- return class extends Event {
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
- // default actions (checkbox toggle, label→control, etc.) unless prevented
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
  }
@@ -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
- const [s, ns] = process.hrtime();
184
- return s * 1000 + ns / 1e6;
184
+ return nodePerformance.now();
185
185
  }
186
186
 
187
187
  let __objUrlSeq = 0;