@miaskiewicz/turbo-dom 0.1.7 → 0.1.9
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.9",
|
|
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",
|
|
@@ -17,9 +17,10 @@ import { installGlobals } from './install.mjs';
|
|
|
17
17
|
|
|
18
18
|
const environment = {
|
|
19
19
|
name: 'turbo-dom',
|
|
20
|
-
// vitest 3/4 read `viteEnvironment
|
|
20
|
+
// vitest 3/4 read `viteEnvironment` (this). vitest <3 used `transformMode: 'web'`
|
|
21
|
+
// — omitted here to avoid the v4 deprecation warning; on vitest <3 set
|
|
22
|
+
// `test.transformMode` (or add transformMode:'web' to this object) if needed.
|
|
21
23
|
viteEnvironment: 'client',
|
|
22
|
-
transformMode: 'web',
|
|
23
24
|
|
|
24
25
|
setup(global, options) {
|
|
25
26
|
const opts = (options && options.turboDom) || {};
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -573,14 +573,25 @@ export class Element extends Node {
|
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
575
|
focus() {
|
|
576
|
-
this.ownerDocument
|
|
577
|
-
|
|
578
|
-
|
|
576
|
+
const doc = this.ownerDocument;
|
|
577
|
+
const prev = doc.__active;
|
|
578
|
+
if (prev === this) return;
|
|
579
|
+
// moving focus blurs the previously-focused element first
|
|
580
|
+
if (prev && prev !== doc.body && typeof prev.dispatchEvent === 'function') {
|
|
581
|
+
doc.__active = null;
|
|
582
|
+
prev.dispatchEvent(new FocusEvent('blur', { relatedTarget: this }));
|
|
583
|
+
prev.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: this }));
|
|
584
|
+
}
|
|
585
|
+
doc.__setActive(this);
|
|
586
|
+
this.dispatchEvent(new FocusEvent('focus', { relatedTarget: prev || null }));
|
|
587
|
+
this.dispatchEvent(new FocusEvent('focusin', { bubbles: true, relatedTarget: prev || null }));
|
|
579
588
|
}
|
|
580
589
|
blur() {
|
|
581
|
-
this.ownerDocument
|
|
582
|
-
|
|
583
|
-
|
|
590
|
+
const doc = this.ownerDocument;
|
|
591
|
+
if (doc.__active !== this) return;
|
|
592
|
+
doc.__setActive(doc.body);
|
|
593
|
+
this.dispatchEvent(new FocusEvent('blur'));
|
|
594
|
+
this.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
|
|
584
595
|
}
|
|
585
596
|
getBoundingClientRect() { return zeroRect(); }
|
|
586
597
|
getClientRects() { return []; }
|
|
@@ -807,35 +818,42 @@ export class XMLSerializer {
|
|
|
807
818
|
|
|
808
819
|
// minimal inline-style CSSOM (honest: only inline + explicitly set props)
|
|
809
820
|
function makeStyle(el) {
|
|
821
|
+
// parse → { values: Map<prop,value>, prio: Map<prop,'important'|''> } (handles !important)
|
|
810
822
|
const parse = () => {
|
|
811
|
-
const
|
|
823
|
+
const values = new Map();
|
|
824
|
+
const prio = new Map();
|
|
812
825
|
for (const decl of (el.getAttribute('style') || '').split(';')) {
|
|
813
826
|
const i = decl.indexOf(':');
|
|
814
827
|
if (i === -1) continue;
|
|
815
828
|
const prop = decl.slice(0, i).trim();
|
|
816
|
-
|
|
817
|
-
if (prop)
|
|
829
|
+
let val = decl.slice(i + 1).trim();
|
|
830
|
+
if (!prop) continue;
|
|
831
|
+
const m = /\s*!\s*important\s*$/i.exec(val);
|
|
832
|
+
if (m) { val = val.slice(0, m.index).trim(); prio.set(prop, 'important'); }
|
|
833
|
+
values.set(prop, val);
|
|
818
834
|
}
|
|
819
|
-
return
|
|
835
|
+
return { values, prio };
|
|
836
|
+
};
|
|
837
|
+
const write = ({ values, prio }) => {
|
|
838
|
+
el.setAttribute('style', [...values].map(([k, v]) => `${k}: ${v}${prio.get(k) === 'important' ? ' !important' : ''}`).join('; '));
|
|
820
839
|
};
|
|
821
|
-
const write = (map) => el.setAttribute('style', [...map].map(([k, v]) => `${k}: ${v}`).join('; '));
|
|
822
840
|
return new Proxy({}, {
|
|
823
841
|
get(_t, key) {
|
|
824
|
-
if (key === 'getPropertyValue') return (p) => parse().get(p) ?? '';
|
|
825
|
-
if (key === 'getPropertyPriority') return () => '';
|
|
826
|
-
if (key === 'setProperty') return (p, v) => { const
|
|
827
|
-
if (key === 'removeProperty') return (p) => { const
|
|
828
|
-
if (key === 'item') return (i) => [...parse().keys()][i] ?? '';
|
|
829
|
-
if (key === 'length') return parse().size;
|
|
842
|
+
if (key === 'getPropertyValue') return (p) => parse().values.get(p) ?? '';
|
|
843
|
+
if (key === 'getPropertyPriority') return (p) => parse().prio.get(p) ?? '';
|
|
844
|
+
if (key === 'setProperty') return (p, v, priority) => { const s = parse(); s.values.set(p, String(v)); if (priority) s.prio.set(p, 'important'); else s.prio.delete(p); write(s); };
|
|
845
|
+
if (key === 'removeProperty') return (p) => { const s = parse(); const v = s.values.get(p) ?? ''; s.values.delete(p); s.prio.delete(p); write(s); return v; };
|
|
846
|
+
if (key === 'item') return (i) => [...parse().values.keys()][i] ?? '';
|
|
847
|
+
if (key === 'length') return parse().values.size;
|
|
830
848
|
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); }
|
|
849
|
+
if (key === 'cssFloat') return parse().values.get('float') ?? '';
|
|
850
|
+
if (key === Symbol.iterator) { const keys = [...parse().values.keys()]; return keys[Symbol.iterator].bind(keys); }
|
|
833
851
|
if (typeof key !== 'string') return undefined;
|
|
834
|
-
return parse().get(kebab(key)) ?? '';
|
|
852
|
+
return parse().values.get(kebab(key)) ?? '';
|
|
835
853
|
},
|
|
836
854
|
set(_t, key, value) {
|
|
837
855
|
if (key === 'cssText') { el.setAttribute('style', String(value)); return true; }
|
|
838
|
-
const
|
|
856
|
+
const s = parse(); s.values.set(kebab(key), String(value)); write(s); return true;
|
|
839
857
|
},
|
|
840
858
|
has(_t, key) { return typeof key === 'string'; },
|
|
841
859
|
});
|
package/src/runtime/window.mjs
CHANGED
|
@@ -31,6 +31,32 @@ class ClipboardEvent extends Event {
|
|
|
31
31
|
constructor(type, init = {}) { super(type, init); this.clipboardData = init.clipboardData ?? new DataTransfer(); }
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// FormData that preserves File/Blob identity. Node's global FormData clones
|
|
35
|
+
// entries into anonymous Blobs (losing the File reference + filename); tests that
|
|
36
|
+
// assert `fd.get('file') === file` need the original kept.
|
|
37
|
+
class TurboFormData {
|
|
38
|
+
constructor() { this.__entries = []; }
|
|
39
|
+
append(name, value, filename) { this.__entries.push([String(name), this.__wrap(value, filename)]); }
|
|
40
|
+
set(name, value, filename) { this.delete(name); this.append(name, value, filename); }
|
|
41
|
+
__wrap(value, filename) {
|
|
42
|
+
// string values coerce; File/Blob are kept by reference (optionally re-named)
|
|
43
|
+
if (value == null || typeof value !== 'object') return String(value);
|
|
44
|
+
if (filename !== undefined && globalThis.Blob && value instanceof globalThis.Blob && value.name !== filename) {
|
|
45
|
+
const F = makeFile(); return new F([value], String(filename), { type: value.type });
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
get(name) { const e = this.__entries.find((x) => x[0] === String(name)); return e ? e[1] : null; }
|
|
50
|
+
getAll(name) { return this.__entries.filter((x) => x[0] === String(name)).map((x) => x[1]); }
|
|
51
|
+
has(name) { return this.__entries.some((x) => x[0] === String(name)); }
|
|
52
|
+
delete(name) { this.__entries = this.__entries.filter((x) => x[0] !== String(name)); }
|
|
53
|
+
forEach(cb, thisArg) { for (const [k, v] of this.__entries) cb.call(thisArg, v, k, this); }
|
|
54
|
+
*entries() { for (const e of this.__entries) yield [e[0], e[1]]; }
|
|
55
|
+
*keys() { for (const e of this.__entries) yield e[0]; }
|
|
56
|
+
*values() { for (const e of this.__entries) yield e[1]; }
|
|
57
|
+
[Symbol.iterator]() { return this.entries(); }
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
// Capture host functions at module load — BEFORE any installGlobals() can shadow
|
|
35
61
|
// the bare names on globalThis (which would make these delegates call themselves).
|
|
36
62
|
const hostSetTimeout = globalThis.setTimeout;
|
|
@@ -78,7 +104,7 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
|
|
|
78
104
|
// web platform globals Node already provides
|
|
79
105
|
fetch: globalThis.fetch ? (...a) => globalThis.fetch(...a) : undefined,
|
|
80
106
|
Headers: globalThis.Headers, Request: globalThis.Request, Response: globalThis.Response,
|
|
81
|
-
FormData:
|
|
107
|
+
FormData: TurboFormData, ReadableStream: globalThis.ReadableStream,
|
|
82
108
|
crypto: globalThis.crypto, Crypto: globalThis.Crypto, SubtleCrypto: globalThis.SubtleCrypto,
|
|
83
109
|
btoa: (s) => Buffer.from(String(s), 'binary').toString('base64'),
|
|
84
110
|
atob: (s) => Buffer.from(String(s), 'base64').toString('binary'),
|
|
@@ -102,6 +128,9 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
|
|
|
102
128
|
structuredClone: (...a) => hostStructuredClone(...a),
|
|
103
129
|
getSelection: () => document.getSelection(),
|
|
104
130
|
scrollTo() {}, scroll() {}, scrollBy() {},
|
|
131
|
+
// window methods libraries/tests spy on — must exist as own props for vi.spyOn
|
|
132
|
+
open: () => null, close() {}, stop() {}, print() {}, focus() {}, blur() {},
|
|
133
|
+
moveTo() {}, moveBy() {}, resizeTo() {}, resizeBy() {},
|
|
105
134
|
alert() {}, confirm: () => false, prompt: () => null,
|
|
106
135
|
dispatchEvent: (e) => document.dispatchEvent(e),
|
|
107
136
|
addEventListener: (...a) => document.addEventListener(...a),
|
|
Binary file
|