@vectojs/dom 0.2.0
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/dist/DOMProjection.d.ts +112 -0
- package/dist/eventBridge.d.ts +57 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +589 -0
- package/dist/index.mjs +551 -0
- package/dist/matrix.d.ts +16 -0
- package/dist/nodes.d.ts +62 -0
- package/package.json +53 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// src/eventBridge.ts
|
|
2
|
+
import { VectoJSEvent } from "@vectojs/core";
|
|
3
|
+
var gestureOwner = /* @__PURE__ */ new Map();
|
|
4
|
+
function getGestureOwner(pointerId) {
|
|
5
|
+
return gestureOwner.get(pointerId);
|
|
6
|
+
}
|
|
7
|
+
var POINTER_EVENTS = [
|
|
8
|
+
"click",
|
|
9
|
+
"pointerup",
|
|
10
|
+
"pointercancel",
|
|
11
|
+
"pointermove",
|
|
12
|
+
"wheel"
|
|
13
|
+
];
|
|
14
|
+
var PRESS_EVENTS = ["pointerdown", "mousedown", "touchstart"];
|
|
15
|
+
function attachDOMBridge(el, node, options = {}) {
|
|
16
|
+
const listeners = [];
|
|
17
|
+
let mode = options.mode ?? "selection";
|
|
18
|
+
const on = (type, handler, capture = false) => {
|
|
19
|
+
el.addEventListener(type, handler, capture);
|
|
20
|
+
listeners.push({ type, handler, capture });
|
|
21
|
+
};
|
|
22
|
+
const pressCapture = (e) => {
|
|
23
|
+
if (e.type === "pointerdown") {
|
|
24
|
+
const id = e.pointerId ?? 0;
|
|
25
|
+
if (!gestureOwner.has(id)) gestureOwner.set(id, "dom");
|
|
26
|
+
options.onGestureStart?.(node.id, id);
|
|
27
|
+
node.dispatchEvent(new VectoJSEvent("pointerdown", node, e, true, void 0, "dom"));
|
|
28
|
+
}
|
|
29
|
+
if (mode === "selection") e.stopPropagation();
|
|
30
|
+
};
|
|
31
|
+
for (const type of PRESS_EVENTS) on(type, pressCapture, true);
|
|
32
|
+
for (const type of POINTER_EVENTS) {
|
|
33
|
+
on(type, (e) => {
|
|
34
|
+
if (type === "pointerup" || type === "pointercancel") {
|
|
35
|
+
const id = e.pointerId ?? 0;
|
|
36
|
+
gestureOwner.delete(id);
|
|
37
|
+
options.onGestureEnd?.(node.id, id);
|
|
38
|
+
}
|
|
39
|
+
node.dispatchEvent(new VectoJSEvent(type, node, e, true, void 0, "dom"));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const hoverEvents = [
|
|
43
|
+
{ native: "mouseenter", vecto: "hover" },
|
|
44
|
+
{ native: "mouseleave", vecto: "pointerleave" }
|
|
45
|
+
];
|
|
46
|
+
for (const { native, vecto } of hoverEvents) {
|
|
47
|
+
on(native, (e) => {
|
|
48
|
+
node.dispatchEvent(new VectoJSEvent(vecto, node, e, false, void 0, "dom"));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
for (const type of ["focus", "blur"]) {
|
|
52
|
+
on(
|
|
53
|
+
type,
|
|
54
|
+
(e) => {
|
|
55
|
+
node.dispatchEvent(new VectoJSEvent(type, node, e, true, void 0, "dom"));
|
|
56
|
+
},
|
|
57
|
+
true
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (options.editable) {
|
|
61
|
+
const forwardEditable = (e) => {
|
|
62
|
+
const target = e.target;
|
|
63
|
+
if (target && "value" in target) options.onNativeInput?.(target.value);
|
|
64
|
+
node.dispatchEvent(new VectoJSEvent("change", node, e, true, void 0, "dom"));
|
|
65
|
+
};
|
|
66
|
+
on("input", forwardEditable);
|
|
67
|
+
on("change", forwardEditable);
|
|
68
|
+
on("compositionstart", forwardEditable);
|
|
69
|
+
on("compositionupdate", forwardEditable);
|
|
70
|
+
on("compositionend", forwardEditable);
|
|
71
|
+
}
|
|
72
|
+
let released = false;
|
|
73
|
+
return {
|
|
74
|
+
setInteractionMode(next) {
|
|
75
|
+
mode = next;
|
|
76
|
+
},
|
|
77
|
+
getInteractionMode() {
|
|
78
|
+
return mode;
|
|
79
|
+
},
|
|
80
|
+
release() {
|
|
81
|
+
if (released) return;
|
|
82
|
+
released = true;
|
|
83
|
+
for (const { type, handler, capture } of listeners) {
|
|
84
|
+
el.removeEventListener(type, handler, capture);
|
|
85
|
+
}
|
|
86
|
+
listeners.length = 0;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/matrix.ts
|
|
92
|
+
function affineToMatrix3d(m) {
|
|
93
|
+
return `matrix3d(${m.a}, ${m.b}, 0, 0, ${m.c}, ${m.d}, 0, 0, 0, 0, 1, 0, ${m.e}, ${m.f}, 0, 1)`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/DOMProjection.ts
|
|
97
|
+
var DOM_VISUAL_Z_INDEX = "9";
|
|
98
|
+
var DOM_ROOT_ATTR = "data-vecto-dom-root";
|
|
99
|
+
var POOL_CAP_PER_TAG = 8;
|
|
100
|
+
function strProp(node, key) {
|
|
101
|
+
const value = node[key];
|
|
102
|
+
return typeof value === "string" ? value : void 0;
|
|
103
|
+
}
|
|
104
|
+
function writeField(cached, key, next, apply, onWrite) {
|
|
105
|
+
const prev = cached.get(key);
|
|
106
|
+
const nextKey = next === void 0 ? "@removed" : `@value:${next}`;
|
|
107
|
+
if (prev === nextKey) return false;
|
|
108
|
+
cached.set(key, nextKey);
|
|
109
|
+
apply(next);
|
|
110
|
+
onWrite?.();
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
var textSpec = {
|
|
114
|
+
tag: "div",
|
|
115
|
+
create(_node) {
|
|
116
|
+
const el = document.createElement("div");
|
|
117
|
+
el.style.userSelect = "text";
|
|
118
|
+
return el;
|
|
119
|
+
},
|
|
120
|
+
sync(node, el, fields) {
|
|
121
|
+
fields.write("text", strProp(node, "text") ?? "", (v) => {
|
|
122
|
+
el.textContent = v ?? "";
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var buttonSpec = {
|
|
127
|
+
tag: "button",
|
|
128
|
+
create(_node) {
|
|
129
|
+
return document.createElement("button");
|
|
130
|
+
},
|
|
131
|
+
sync(node, el, fields) {
|
|
132
|
+
fields.write("label", strProp(node, "label") ?? "", (v) => {
|
|
133
|
+
el.textContent = v ?? "";
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var inputSpec = {
|
|
138
|
+
tag: "input",
|
|
139
|
+
create(_node) {
|
|
140
|
+
return document.createElement("input");
|
|
141
|
+
},
|
|
142
|
+
sync(node, el, fields) {
|
|
143
|
+
const input = el;
|
|
144
|
+
if (document.activeElement !== el) {
|
|
145
|
+
fields.write("value", strProp(node, "value") ?? "", (v) => {
|
|
146
|
+
input.value = v ?? "";
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
fields.write("placeholder", strProp(node, "placeholder"), (v) => {
|
|
150
|
+
if (v === void 0) input.removeAttribute("placeholder");
|
|
151
|
+
else input.setAttribute("placeholder", v);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var passthroughSpec = (tag) => ({
|
|
156
|
+
tag,
|
|
157
|
+
create(_node) {
|
|
158
|
+
return document.createElement(tag);
|
|
159
|
+
},
|
|
160
|
+
sync(_node, _el, _fields) {
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
var fallbackSpec = passthroughSpec("div");
|
|
164
|
+
var DOMProjection = class {
|
|
165
|
+
kind = "dom";
|
|
166
|
+
root = null;
|
|
167
|
+
sentinel = null;
|
|
168
|
+
states = /* @__PURE__ */ new Map();
|
|
169
|
+
pools = /* @__PURE__ */ new Map();
|
|
170
|
+
kinds = /* @__PURE__ */ new Map();
|
|
171
|
+
bridgeOptions;
|
|
172
|
+
/** Nodes owning a live press: `pointerId`s per node id (RFC4 §5 pin source). */
|
|
173
|
+
activeGestures = /* @__PURE__ */ new Map();
|
|
174
|
+
mountSeq = 0;
|
|
175
|
+
stats = {
|
|
176
|
+
mounts: 0,
|
|
177
|
+
unmounts: 0,
|
|
178
|
+
transformWrites: 0,
|
|
179
|
+
opacityWrites: 0,
|
|
180
|
+
sizeWrites: 0,
|
|
181
|
+
contentWrites: 0,
|
|
182
|
+
poolHits: 0,
|
|
183
|
+
poolMisses: 0
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* @param canvas - The scene's canvas. The root is appended to its
|
|
187
|
+
* `parentElement` (inserted before `a11yRoot` when present so the
|
|
188
|
+
* below-a11y layer holds even on `zIndex` ties). Without a DOM (SSR) or
|
|
189
|
+
* without a parent, the backend constructs but never goes resident — the
|
|
190
|
+
* walk then falls through to canvas paint.
|
|
191
|
+
*/
|
|
192
|
+
constructor(canvas, bridgeOptions = {}) {
|
|
193
|
+
this.bridgeOptions = bridgeOptions;
|
|
194
|
+
this.kinds.set("text", textSpec);
|
|
195
|
+
this.kinds.set("button", buttonSpec);
|
|
196
|
+
this.kinds.set("input", inputSpec);
|
|
197
|
+
this.kinds.set("container", passthroughSpec("div"));
|
|
198
|
+
this.kinds.set("transform", passthroughSpec("div"));
|
|
199
|
+
if (typeof document === "undefined") return;
|
|
200
|
+
const parent = canvas.parentElement;
|
|
201
|
+
if (!parent) return;
|
|
202
|
+
const root = document.createElement("div");
|
|
203
|
+
root.setAttribute(DOM_ROOT_ATTR, "");
|
|
204
|
+
root.style.position = "absolute";
|
|
205
|
+
root.style.top = "0";
|
|
206
|
+
root.style.left = "0";
|
|
207
|
+
root.style.width = "100%";
|
|
208
|
+
root.style.height = "100%";
|
|
209
|
+
root.style.pointerEvents = "none";
|
|
210
|
+
root.style.overflow = "hidden";
|
|
211
|
+
root.style.zIndex = DOM_VISUAL_Z_INDEX;
|
|
212
|
+
const sentinel = document.createElement("div");
|
|
213
|
+
sentinel.tabIndex = -1;
|
|
214
|
+
sentinel.setAttribute("aria-hidden", "true");
|
|
215
|
+
sentinel.style.position = "absolute";
|
|
216
|
+
sentinel.style.width = "0";
|
|
217
|
+
sentinel.style.height = "0";
|
|
218
|
+
sentinel.style.overflow = "hidden";
|
|
219
|
+
root.appendChild(sentinel);
|
|
220
|
+
this.sentinel = sentinel;
|
|
221
|
+
const a11yRoot = parent.querySelector("[data-vecto-a11y-root]");
|
|
222
|
+
if (a11yRoot) parent.insertBefore(root, a11yRoot);
|
|
223
|
+
else parent.appendChild(root);
|
|
224
|
+
this.root = root;
|
|
225
|
+
}
|
|
226
|
+
/** Register (or replace) a custom `domKind` (RFC §4.1 custom nodes). */
|
|
227
|
+
registerKind(kind, spec) {
|
|
228
|
+
this.kinds.set(kind, spec);
|
|
229
|
+
}
|
|
230
|
+
/** Telemetry counters (cumulative). */
|
|
231
|
+
getStats() {
|
|
232
|
+
return { ...this.stats };
|
|
233
|
+
}
|
|
234
|
+
/** The DOM-visual root, if constructed (null under SSR). */
|
|
235
|
+
getRoot() {
|
|
236
|
+
return this.root;
|
|
237
|
+
}
|
|
238
|
+
/** Live element for a resident node, if any. */
|
|
239
|
+
getElement(nodeId) {
|
|
240
|
+
return this.states.get(nodeId)?.el;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Whether `node` currently owns an active press on its live element
|
|
244
|
+
* (`ProjectionBackend.hasActiveGesture`, RFC4 §5). The scene consults this
|
|
245
|
+
* during `'auto'` negotiation so a node never flips backends mid-gesture.
|
|
246
|
+
*/
|
|
247
|
+
hasActiveGesture(node) {
|
|
248
|
+
return (this.activeGestures.get(node.id)?.size ?? 0) > 0;
|
|
249
|
+
}
|
|
250
|
+
/** Last ResizeObserver-measured intrinsic size, if observed. */
|
|
251
|
+
getIntrinsicSize(nodeId) {
|
|
252
|
+
const state = this.states.get(nodeId);
|
|
253
|
+
if (!state || state.intrinsicWidth === 0) return void 0;
|
|
254
|
+
return { width: state.intrinsicWidth, height: state.intrinsicHeight };
|
|
255
|
+
}
|
|
256
|
+
/** Resolve the creation spec for a node (custom kinds win; unknown = plain div). */
|
|
257
|
+
specFor(node) {
|
|
258
|
+
return this.kinds.get(node.domKind) ?? fallbackSpec;
|
|
259
|
+
}
|
|
260
|
+
mount(node) {
|
|
261
|
+
if (this.states.has(node.id) || !this.root || typeof document === "undefined") return;
|
|
262
|
+
const spec = this.specFor(node);
|
|
263
|
+
let el = this.pools.get(spec.tag)?.pop();
|
|
264
|
+
if (el) {
|
|
265
|
+
this.stats.poolHits += 1;
|
|
266
|
+
if (node.domKind === "text") el.style.userSelect = "text";
|
|
267
|
+
el.style.display = "";
|
|
268
|
+
} else {
|
|
269
|
+
this.stats.poolMisses += 1;
|
|
270
|
+
el = spec.create(node);
|
|
271
|
+
}
|
|
272
|
+
el.style.position = "absolute";
|
|
273
|
+
el.style.left = "0px";
|
|
274
|
+
el.style.top = "0px";
|
|
275
|
+
el.style.transformOrigin = "0 0";
|
|
276
|
+
el.style.pointerEvents = "auto";
|
|
277
|
+
el.setAttribute("data-vecto-id", node.id);
|
|
278
|
+
this.root.appendChild(el);
|
|
279
|
+
const bridge = attachDOMBridge(el, node, {
|
|
280
|
+
...this.bridgeOptions,
|
|
281
|
+
editable: node.domKind === "input" || this.bridgeOptions.editable,
|
|
282
|
+
onGestureStart: (nodeId, pointerId) => {
|
|
283
|
+
this.bridgeOptions.onGestureStart?.(nodeId, pointerId);
|
|
284
|
+
let owned = this.activeGestures.get(nodeId);
|
|
285
|
+
if (!owned) {
|
|
286
|
+
owned = /* @__PURE__ */ new Set();
|
|
287
|
+
this.activeGestures.set(nodeId, owned);
|
|
288
|
+
}
|
|
289
|
+
owned.add(pointerId);
|
|
290
|
+
},
|
|
291
|
+
onGestureEnd: (nodeId, pointerId) => {
|
|
292
|
+
this.bridgeOptions.onGestureEnd?.(nodeId, pointerId);
|
|
293
|
+
const owned = this.activeGestures.get(nodeId);
|
|
294
|
+
if (owned) {
|
|
295
|
+
owned.delete(pointerId);
|
|
296
|
+
if (owned.size === 0) this.activeGestures.delete(nodeId);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
onNativeInput: node.domKind === "input" ? (value) => {
|
|
300
|
+
node.value = value;
|
|
301
|
+
} : this.bridgeOptions.onNativeInput
|
|
302
|
+
});
|
|
303
|
+
const state = {
|
|
304
|
+
el,
|
|
305
|
+
bridge,
|
|
306
|
+
observer: null,
|
|
307
|
+
tag: spec.tag,
|
|
308
|
+
cached: /* @__PURE__ */ new Map(),
|
|
309
|
+
lastTransform: "",
|
|
310
|
+
lastOpacity: "",
|
|
311
|
+
lastWidth: "",
|
|
312
|
+
lastHeight: "",
|
|
313
|
+
intrinsicWidth: 0,
|
|
314
|
+
intrinsicHeight: 0
|
|
315
|
+
};
|
|
316
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
317
|
+
const observer = new ResizeObserver((entries) => {
|
|
318
|
+
const current = this.states.get(node.id);
|
|
319
|
+
if (!current) return;
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
current.intrinsicWidth = entry.contentRect.width || entry.target.offsetWidth;
|
|
322
|
+
current.intrinsicHeight = entry.contentRect.height || entry.target.offsetHeight;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
observer.observe(el);
|
|
326
|
+
state.observer = observer;
|
|
327
|
+
}
|
|
328
|
+
this.states.set(node.id, state);
|
|
329
|
+
el.style.zIndex = String(1 + this.mountSeq++);
|
|
330
|
+
node.domResident = true;
|
|
331
|
+
this.stats.mounts += 1;
|
|
332
|
+
}
|
|
333
|
+
update(node, worldMatrix) {
|
|
334
|
+
let state = this.states.get(node.id);
|
|
335
|
+
if (!state) {
|
|
336
|
+
this.mount(node);
|
|
337
|
+
state = this.states.get(node.id);
|
|
338
|
+
if (!state) return;
|
|
339
|
+
}
|
|
340
|
+
const transformStr = affineToMatrix3d(worldMatrix);
|
|
341
|
+
if (state.lastTransform !== transformStr) {
|
|
342
|
+
state.el.style.transform = transformStr;
|
|
343
|
+
state.lastTransform = transformStr;
|
|
344
|
+
this.stats.transformWrites += 1;
|
|
345
|
+
}
|
|
346
|
+
let opacity = 1;
|
|
347
|
+
let cursor = node;
|
|
348
|
+
let depth = 0;
|
|
349
|
+
while (cursor && depth < 32) {
|
|
350
|
+
opacity *= cursor.opacity;
|
|
351
|
+
cursor = cursor.parent;
|
|
352
|
+
depth += 1;
|
|
353
|
+
}
|
|
354
|
+
const opacityStr = String(opacity);
|
|
355
|
+
if (state.lastOpacity !== opacityStr) {
|
|
356
|
+
state.el.style.opacity = opacityStr;
|
|
357
|
+
state.lastOpacity = opacityStr;
|
|
358
|
+
this.stats.opacityWrites += 1;
|
|
359
|
+
}
|
|
360
|
+
if (node.width > 0 || node.height > 0) {
|
|
361
|
+
const widthStr = node.width > 0 ? `${node.width}px` : "";
|
|
362
|
+
const heightStr = node.height > 0 ? `${node.height}px` : "";
|
|
363
|
+
if (state.lastWidth !== widthStr || state.lastHeight !== heightStr) {
|
|
364
|
+
state.el.style.width = widthStr;
|
|
365
|
+
state.el.style.height = heightStr;
|
|
366
|
+
state.lastWidth = widthStr;
|
|
367
|
+
state.lastHeight = heightStr;
|
|
368
|
+
this.stats.sizeWrites += 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const spec = this.specFor(node);
|
|
372
|
+
const cached = state.cached;
|
|
373
|
+
const fields = {
|
|
374
|
+
write: (key, next, apply) => writeField(cached, key, next, apply, () => {
|
|
375
|
+
this.stats.contentWrites += 1;
|
|
376
|
+
})
|
|
377
|
+
};
|
|
378
|
+
spec.sync(node, state.el, fields);
|
|
379
|
+
}
|
|
380
|
+
unmount(node) {
|
|
381
|
+
const state = this.states.get(node.id);
|
|
382
|
+
this.activeGestures.delete(node.id);
|
|
383
|
+
node.domResident = false;
|
|
384
|
+
if (!state) return;
|
|
385
|
+
state.bridge.release();
|
|
386
|
+
if (typeof document !== "undefined") {
|
|
387
|
+
const active = document.activeElement;
|
|
388
|
+
if (active && active !== document.body && state.el.contains(active)) {
|
|
389
|
+
this.sentinel?.focus({ preventScroll: true });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
state.observer?.disconnect();
|
|
393
|
+
state.el.remove();
|
|
394
|
+
const pool = this.pools.get(state.tag) ?? [];
|
|
395
|
+
if (pool.length < POOL_CAP_PER_TAG) {
|
|
396
|
+
pool.push(state.el);
|
|
397
|
+
this.pools.set(state.tag, pool);
|
|
398
|
+
}
|
|
399
|
+
this.states.delete(node.id);
|
|
400
|
+
this.stats.unmounts += 1;
|
|
401
|
+
}
|
|
402
|
+
/** Unmount every resident node and remove the root (scene teardown). */
|
|
403
|
+
dispose() {
|
|
404
|
+
for (const id of [...this.states.keys()]) {
|
|
405
|
+
const state = this.states.get(id);
|
|
406
|
+
if (!state) continue;
|
|
407
|
+
state.bridge.release();
|
|
408
|
+
state.observer?.disconnect();
|
|
409
|
+
state.el.remove();
|
|
410
|
+
}
|
|
411
|
+
this.states.clear();
|
|
412
|
+
this.pools.clear();
|
|
413
|
+
this.activeGestures.clear();
|
|
414
|
+
this.root?.remove();
|
|
415
|
+
this.root = null;
|
|
416
|
+
this.sentinel = null;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// src/nodes.ts
|
|
421
|
+
import { Entity } from "@vectojs/core";
|
|
422
|
+
var DOMText = class extends Entity {
|
|
423
|
+
text;
|
|
424
|
+
constructor(id, text = "", width = 200, height = 24) {
|
|
425
|
+
super(id);
|
|
426
|
+
this.text = text;
|
|
427
|
+
this.width = width;
|
|
428
|
+
this.height = height;
|
|
429
|
+
this.domPolicy = "dom";
|
|
430
|
+
this.domKind = "text";
|
|
431
|
+
this.interactive = true;
|
|
432
|
+
}
|
|
433
|
+
getBounds() {
|
|
434
|
+
if (this.width <= 0 || this.height <= 0) return null;
|
|
435
|
+
return { x: 0, y: 0, width: this.width, height: this.height };
|
|
436
|
+
}
|
|
437
|
+
isPointInside(globalX, globalY) {
|
|
438
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
439
|
+
if (!local) return false;
|
|
440
|
+
return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
|
|
441
|
+
}
|
|
442
|
+
render() {
|
|
443
|
+
}
|
|
444
|
+
getA11yAttributes() {
|
|
445
|
+
return { tag: "div", role: "text", label: this.text };
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
var DOMButton = class extends Entity {
|
|
449
|
+
label;
|
|
450
|
+
constructor(id, label = "", width = 120, height = 36) {
|
|
451
|
+
super(id);
|
|
452
|
+
this.label = label;
|
|
453
|
+
this.width = width;
|
|
454
|
+
this.height = height;
|
|
455
|
+
this.domPolicy = "dom";
|
|
456
|
+
this.domKind = "button";
|
|
457
|
+
this.interactive = true;
|
|
458
|
+
}
|
|
459
|
+
getBounds() {
|
|
460
|
+
if (this.width <= 0 || this.height <= 0) return null;
|
|
461
|
+
return { x: 0, y: 0, width: this.width, height: this.height };
|
|
462
|
+
}
|
|
463
|
+
isPointInside(globalX, globalY) {
|
|
464
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
465
|
+
if (!local) return false;
|
|
466
|
+
return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
|
|
467
|
+
}
|
|
468
|
+
render() {
|
|
469
|
+
}
|
|
470
|
+
getA11yAttributes() {
|
|
471
|
+
return { tag: "button", role: "button", label: this.label };
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
var DOMInput = class extends Entity {
|
|
475
|
+
value;
|
|
476
|
+
placeholder;
|
|
477
|
+
constructor(id, value = "", width = 200, height = 32) {
|
|
478
|
+
super(id);
|
|
479
|
+
this.value = value;
|
|
480
|
+
this.width = width;
|
|
481
|
+
this.height = height;
|
|
482
|
+
this.domPolicy = "dom";
|
|
483
|
+
this.domKind = "input";
|
|
484
|
+
this.interactive = true;
|
|
485
|
+
}
|
|
486
|
+
getBounds() {
|
|
487
|
+
if (this.width <= 0 || this.height <= 0) return null;
|
|
488
|
+
return { x: 0, y: 0, width: this.width, height: this.height };
|
|
489
|
+
}
|
|
490
|
+
isPointInside(globalX, globalY) {
|
|
491
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
492
|
+
if (!local) return false;
|
|
493
|
+
return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
|
|
494
|
+
}
|
|
495
|
+
render() {
|
|
496
|
+
}
|
|
497
|
+
getA11yAttributes() {
|
|
498
|
+
return { tag: "input", role: "textbox", label: this.placeholder ?? "input", value: this.value };
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
var DOMContainer = class extends Entity {
|
|
502
|
+
constructor(id, width = 0, height = 0) {
|
|
503
|
+
super(id);
|
|
504
|
+
this.width = width;
|
|
505
|
+
this.height = height;
|
|
506
|
+
this.domPolicy = "dom";
|
|
507
|
+
this.domKind = "container";
|
|
508
|
+
this.interactive = true;
|
|
509
|
+
}
|
|
510
|
+
getBounds() {
|
|
511
|
+
if (this.width <= 0 || this.height <= 0) return null;
|
|
512
|
+
return { x: 0, y: 0, width: this.width, height: this.height };
|
|
513
|
+
}
|
|
514
|
+
isPointInside(globalX, globalY) {
|
|
515
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
516
|
+
if (!local) return false;
|
|
517
|
+
return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
|
|
518
|
+
}
|
|
519
|
+
render() {
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
var DOMTransform = class extends Entity {
|
|
523
|
+
constructor(id) {
|
|
524
|
+
super(id);
|
|
525
|
+
this.domPolicy = "dom";
|
|
526
|
+
this.domKind = "transform";
|
|
527
|
+
this.interactive = true;
|
|
528
|
+
}
|
|
529
|
+
getBounds() {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
isPointInside(_globalX, _globalY) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
render() {
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
export {
|
|
539
|
+
DOMButton,
|
|
540
|
+
DOMContainer,
|
|
541
|
+
DOMInput,
|
|
542
|
+
DOMProjection,
|
|
543
|
+
DOMText,
|
|
544
|
+
DOMTransform,
|
|
545
|
+
DOM_ROOT_ATTR,
|
|
546
|
+
DOM_VISUAL_Z_INDEX,
|
|
547
|
+
affineToMatrix3d,
|
|
548
|
+
attachDOMBridge,
|
|
549
|
+
getGestureOwner,
|
|
550
|
+
writeField
|
|
551
|
+
};
|
package/dist/matrix.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AffineTransform } from '@vectojs/core';
|
|
2
|
+
/**
|
|
3
|
+
* Embed a 2D world affine matrix as a CSS `matrix3d()` string (RFC §5 rule 1).
|
|
4
|
+
*
|
|
5
|
+
* The affine `[a c e; b d f; 0 0 1]` becomes the 4x4 column-major
|
|
6
|
+
* `matrix3d(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, e, f, 0, 1)`. One code path for
|
|
7
|
+
* 2D and camera-composed 3D scenes (uniformity preferred over the portal
|
|
8
|
+
* path's `matrix(...)`; per-frame string cost at realistic DOM-node counts is
|
|
9
|
+
* unmeasured — RFC §11 risk 4 — so this stays behind the same per-field
|
|
10
|
+
* dirty check the portals use, and steady-state frames write nothing).
|
|
11
|
+
*
|
|
12
|
+
* Correctness note: the browser renders the element (selection highlight,
|
|
13
|
+
* caret, focus ring, children) _first_ and transforms the rendered result
|
|
14
|
+
* after, which is why selection keeps its perspective shape under rotation.
|
|
15
|
+
*/
|
|
16
|
+
export declare function affineToMatrix3d(m: AffineTransform): string;
|
package/dist/nodes.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Entity, type A11yAttributes, type Bounds } from '@vectojs/core';
|
|
2
|
+
/**
|
|
3
|
+
* P1 prototype node set (RFC §3/§4.1, research P1): explicit per-node opt-in
|
|
4
|
+
* only — each class sets `domPolicy = 'dom'` in its constructor. `'auto'`
|
|
5
|
+
* switching heuristics are CTX-0601 and out of scope.
|
|
6
|
+
*
|
|
7
|
+
* All five render as no-ops on canvas: while resident, the live element owns
|
|
8
|
+
* the visuals (the Scene walk skips canvas paint for resident leaves). If the
|
|
9
|
+
* backend is absent (SSR) the walk falls through to canvas — where these paint
|
|
10
|
+
* nothing — and `getA11yAttributes` keeps the transparent-mirror fallback
|
|
11
|
+
* meaningful if the policy flips back to `'canvas'`.
|
|
12
|
+
*/
|
|
13
|
+
/** Selectable/copyable prose → `div` with `user-select: text`. */
|
|
14
|
+
export declare class DOMText extends Entity {
|
|
15
|
+
text: string;
|
|
16
|
+
constructor(id?: string, text?: string, width?: number, height?: number);
|
|
17
|
+
getBounds(): Bounds | null;
|
|
18
|
+
isPointInside(globalX: number, globalY: number): boolean;
|
|
19
|
+
render(): void;
|
|
20
|
+
getA11yAttributes(): A11yAttributes;
|
|
21
|
+
}
|
|
22
|
+
/** Native pressable control → `button`. Clicks arrive via the event bridge. */
|
|
23
|
+
export declare class DOMButton extends Entity {
|
|
24
|
+
label: string;
|
|
25
|
+
constructor(id?: string, label?: string, width?: number, height?: number);
|
|
26
|
+
getBounds(): Bounds | null;
|
|
27
|
+
isPointInside(globalX: number, globalY: number): boolean;
|
|
28
|
+
render(): void;
|
|
29
|
+
getA11yAttributes(): A11yAttributes;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Native text entry → `input`. Typing and IME composition work with zero
|
|
33
|
+
* framework key handling; the bridge syncs the element value back onto
|
|
34
|
+
* {@link value} and forwards `VectoJSEvent('change')`.
|
|
35
|
+
*/
|
|
36
|
+
export declare class DOMInput extends Entity {
|
|
37
|
+
value: string;
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
constructor(id?: string, value?: string, width?: number, height?: number);
|
|
40
|
+
getBounds(): Bounds | null;
|
|
41
|
+
isPointInside(globalX: number, globalY: number): boolean;
|
|
42
|
+
render(): void;
|
|
43
|
+
getA11yAttributes(): A11yAttributes;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Grouping node → plain `div`. Non-leaf residents keep the walk recursing so
|
|
47
|
+
* opted-in descendants stay synced; canvas-policy children paint at the same
|
|
48
|
+
* world coordinates, so mixed subtrees compose.
|
|
49
|
+
*/
|
|
50
|
+
export declare class DOMContainer extends Entity {
|
|
51
|
+
constructor(id?: string, width?: number, height?: number);
|
|
52
|
+
getBounds(): Bounds | null;
|
|
53
|
+
isPointInside(globalX: number, globalY: number): boolean;
|
|
54
|
+
render(): void;
|
|
55
|
+
}
|
|
56
|
+
/** Transform-only grouping node → pass-through `div` (position/rotation/scale). */
|
|
57
|
+
export declare class DOMTransform extends Entity {
|
|
58
|
+
constructor(id?: string);
|
|
59
|
+
getBounds(): Bounds | null;
|
|
60
|
+
isPointInside(_globalX: number, _globalY: number): boolean;
|
|
61
|
+
render(): void;
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vectojs/dom",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "DOM visual projection backend for VectoJS (RFC2 P1): selected subtrees materialize as live HTMLElements positioned by their world matrix.",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"dom",
|
|
10
|
+
"projection",
|
|
11
|
+
"accessibility",
|
|
12
|
+
"vectojs",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/vectojs/vectojs.git",
|
|
19
|
+
"directory": "packages/dom"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/vectojs/vectojs/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/vectojs/vectojs#readme",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.mjs",
|
|
33
|
+
"require": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "(cd ../core && bun run build) && tsup src/index.ts --format cjs,esm --external @vectojs/core --clean && tsc -p tsconfig.build.json",
|
|
41
|
+
"test": "vitest run"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@vectojs/core": "^1.40.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^26.4.0",
|
|
48
|
+
"esbuild": "^0.28.1",
|
|
49
|
+
"jsdom": "^30.0.1",
|
|
50
|
+
"tsup": "^8.3.5",
|
|
51
|
+
"vitest": "^4.1.11"
|
|
52
|
+
}
|
|
53
|
+
}
|