@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/index.js ADDED
@@ -0,0 +1,589 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DOMButton: () => DOMButton,
24
+ DOMContainer: () => DOMContainer,
25
+ DOMInput: () => DOMInput,
26
+ DOMProjection: () => DOMProjection,
27
+ DOMText: () => DOMText,
28
+ DOMTransform: () => DOMTransform,
29
+ DOM_ROOT_ATTR: () => DOM_ROOT_ATTR,
30
+ DOM_VISUAL_Z_INDEX: () => DOM_VISUAL_Z_INDEX,
31
+ affineToMatrix3d: () => affineToMatrix3d,
32
+ attachDOMBridge: () => attachDOMBridge,
33
+ getGestureOwner: () => getGestureOwner,
34
+ writeField: () => writeField
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/eventBridge.ts
39
+ var import_core = require("@vectojs/core");
40
+ var gestureOwner = /* @__PURE__ */ new Map();
41
+ function getGestureOwner(pointerId) {
42
+ return gestureOwner.get(pointerId);
43
+ }
44
+ var POINTER_EVENTS = [
45
+ "click",
46
+ "pointerup",
47
+ "pointercancel",
48
+ "pointermove",
49
+ "wheel"
50
+ ];
51
+ var PRESS_EVENTS = ["pointerdown", "mousedown", "touchstart"];
52
+ function attachDOMBridge(el, node, options = {}) {
53
+ const listeners = [];
54
+ let mode = options.mode ?? "selection";
55
+ const on = (type, handler, capture = false) => {
56
+ el.addEventListener(type, handler, capture);
57
+ listeners.push({ type, handler, capture });
58
+ };
59
+ const pressCapture = (e) => {
60
+ if (e.type === "pointerdown") {
61
+ const id = e.pointerId ?? 0;
62
+ if (!gestureOwner.has(id)) gestureOwner.set(id, "dom");
63
+ options.onGestureStart?.(node.id, id);
64
+ node.dispatchEvent(new import_core.VectoJSEvent("pointerdown", node, e, true, void 0, "dom"));
65
+ }
66
+ if (mode === "selection") e.stopPropagation();
67
+ };
68
+ for (const type of PRESS_EVENTS) on(type, pressCapture, true);
69
+ for (const type of POINTER_EVENTS) {
70
+ on(type, (e) => {
71
+ if (type === "pointerup" || type === "pointercancel") {
72
+ const id = e.pointerId ?? 0;
73
+ gestureOwner.delete(id);
74
+ options.onGestureEnd?.(node.id, id);
75
+ }
76
+ node.dispatchEvent(new import_core.VectoJSEvent(type, node, e, true, void 0, "dom"));
77
+ });
78
+ }
79
+ const hoverEvents = [
80
+ { native: "mouseenter", vecto: "hover" },
81
+ { native: "mouseleave", vecto: "pointerleave" }
82
+ ];
83
+ for (const { native, vecto } of hoverEvents) {
84
+ on(native, (e) => {
85
+ node.dispatchEvent(new import_core.VectoJSEvent(vecto, node, e, false, void 0, "dom"));
86
+ });
87
+ }
88
+ for (const type of ["focus", "blur"]) {
89
+ on(
90
+ type,
91
+ (e) => {
92
+ node.dispatchEvent(new import_core.VectoJSEvent(type, node, e, true, void 0, "dom"));
93
+ },
94
+ true
95
+ );
96
+ }
97
+ if (options.editable) {
98
+ const forwardEditable = (e) => {
99
+ const target = e.target;
100
+ if (target && "value" in target) options.onNativeInput?.(target.value);
101
+ node.dispatchEvent(new import_core.VectoJSEvent("change", node, e, true, void 0, "dom"));
102
+ };
103
+ on("input", forwardEditable);
104
+ on("change", forwardEditable);
105
+ on("compositionstart", forwardEditable);
106
+ on("compositionupdate", forwardEditable);
107
+ on("compositionend", forwardEditable);
108
+ }
109
+ let released = false;
110
+ return {
111
+ setInteractionMode(next) {
112
+ mode = next;
113
+ },
114
+ getInteractionMode() {
115
+ return mode;
116
+ },
117
+ release() {
118
+ if (released) return;
119
+ released = true;
120
+ for (const { type, handler, capture } of listeners) {
121
+ el.removeEventListener(type, handler, capture);
122
+ }
123
+ listeners.length = 0;
124
+ }
125
+ };
126
+ }
127
+
128
+ // src/matrix.ts
129
+ function affineToMatrix3d(m) {
130
+ return `matrix3d(${m.a}, ${m.b}, 0, 0, ${m.c}, ${m.d}, 0, 0, 0, 0, 1, 0, ${m.e}, ${m.f}, 0, 1)`;
131
+ }
132
+
133
+ // src/DOMProjection.ts
134
+ var DOM_VISUAL_Z_INDEX = "9";
135
+ var DOM_ROOT_ATTR = "data-vecto-dom-root";
136
+ var POOL_CAP_PER_TAG = 8;
137
+ function strProp(node, key) {
138
+ const value = node[key];
139
+ return typeof value === "string" ? value : void 0;
140
+ }
141
+ function writeField(cached, key, next, apply, onWrite) {
142
+ const prev = cached.get(key);
143
+ const nextKey = next === void 0 ? "@removed" : `@value:${next}`;
144
+ if (prev === nextKey) return false;
145
+ cached.set(key, nextKey);
146
+ apply(next);
147
+ onWrite?.();
148
+ return true;
149
+ }
150
+ var textSpec = {
151
+ tag: "div",
152
+ create(_node) {
153
+ const el = document.createElement("div");
154
+ el.style.userSelect = "text";
155
+ return el;
156
+ },
157
+ sync(node, el, fields) {
158
+ fields.write("text", strProp(node, "text") ?? "", (v) => {
159
+ el.textContent = v ?? "";
160
+ });
161
+ }
162
+ };
163
+ var buttonSpec = {
164
+ tag: "button",
165
+ create(_node) {
166
+ return document.createElement("button");
167
+ },
168
+ sync(node, el, fields) {
169
+ fields.write("label", strProp(node, "label") ?? "", (v) => {
170
+ el.textContent = v ?? "";
171
+ });
172
+ }
173
+ };
174
+ var inputSpec = {
175
+ tag: "input",
176
+ create(_node) {
177
+ return document.createElement("input");
178
+ },
179
+ sync(node, el, fields) {
180
+ const input = el;
181
+ if (document.activeElement !== el) {
182
+ fields.write("value", strProp(node, "value") ?? "", (v) => {
183
+ input.value = v ?? "";
184
+ });
185
+ }
186
+ fields.write("placeholder", strProp(node, "placeholder"), (v) => {
187
+ if (v === void 0) input.removeAttribute("placeholder");
188
+ else input.setAttribute("placeholder", v);
189
+ });
190
+ }
191
+ };
192
+ var passthroughSpec = (tag) => ({
193
+ tag,
194
+ create(_node) {
195
+ return document.createElement(tag);
196
+ },
197
+ sync(_node, _el, _fields) {
198
+ }
199
+ });
200
+ var fallbackSpec = passthroughSpec("div");
201
+ var DOMProjection = class {
202
+ kind = "dom";
203
+ root = null;
204
+ sentinel = null;
205
+ states = /* @__PURE__ */ new Map();
206
+ pools = /* @__PURE__ */ new Map();
207
+ kinds = /* @__PURE__ */ new Map();
208
+ bridgeOptions;
209
+ /** Nodes owning a live press: `pointerId`s per node id (RFC4 §5 pin source). */
210
+ activeGestures = /* @__PURE__ */ new Map();
211
+ mountSeq = 0;
212
+ stats = {
213
+ mounts: 0,
214
+ unmounts: 0,
215
+ transformWrites: 0,
216
+ opacityWrites: 0,
217
+ sizeWrites: 0,
218
+ contentWrites: 0,
219
+ poolHits: 0,
220
+ poolMisses: 0
221
+ };
222
+ /**
223
+ * @param canvas - The scene's canvas. The root is appended to its
224
+ * `parentElement` (inserted before `a11yRoot` when present so the
225
+ * below-a11y layer holds even on `zIndex` ties). Without a DOM (SSR) or
226
+ * without a parent, the backend constructs but never goes resident — the
227
+ * walk then falls through to canvas paint.
228
+ */
229
+ constructor(canvas, bridgeOptions = {}) {
230
+ this.bridgeOptions = bridgeOptions;
231
+ this.kinds.set("text", textSpec);
232
+ this.kinds.set("button", buttonSpec);
233
+ this.kinds.set("input", inputSpec);
234
+ this.kinds.set("container", passthroughSpec("div"));
235
+ this.kinds.set("transform", passthroughSpec("div"));
236
+ if (typeof document === "undefined") return;
237
+ const parent = canvas.parentElement;
238
+ if (!parent) return;
239
+ const root = document.createElement("div");
240
+ root.setAttribute(DOM_ROOT_ATTR, "");
241
+ root.style.position = "absolute";
242
+ root.style.top = "0";
243
+ root.style.left = "0";
244
+ root.style.width = "100%";
245
+ root.style.height = "100%";
246
+ root.style.pointerEvents = "none";
247
+ root.style.overflow = "hidden";
248
+ root.style.zIndex = DOM_VISUAL_Z_INDEX;
249
+ const sentinel = document.createElement("div");
250
+ sentinel.tabIndex = -1;
251
+ sentinel.setAttribute("aria-hidden", "true");
252
+ sentinel.style.position = "absolute";
253
+ sentinel.style.width = "0";
254
+ sentinel.style.height = "0";
255
+ sentinel.style.overflow = "hidden";
256
+ root.appendChild(sentinel);
257
+ this.sentinel = sentinel;
258
+ const a11yRoot = parent.querySelector("[data-vecto-a11y-root]");
259
+ if (a11yRoot) parent.insertBefore(root, a11yRoot);
260
+ else parent.appendChild(root);
261
+ this.root = root;
262
+ }
263
+ /** Register (or replace) a custom `domKind` (RFC §4.1 custom nodes). */
264
+ registerKind(kind, spec) {
265
+ this.kinds.set(kind, spec);
266
+ }
267
+ /** Telemetry counters (cumulative). */
268
+ getStats() {
269
+ return { ...this.stats };
270
+ }
271
+ /** The DOM-visual root, if constructed (null under SSR). */
272
+ getRoot() {
273
+ return this.root;
274
+ }
275
+ /** Live element for a resident node, if any. */
276
+ getElement(nodeId) {
277
+ return this.states.get(nodeId)?.el;
278
+ }
279
+ /**
280
+ * Whether `node` currently owns an active press on its live element
281
+ * (`ProjectionBackend.hasActiveGesture`, RFC4 §5). The scene consults this
282
+ * during `'auto'` negotiation so a node never flips backends mid-gesture.
283
+ */
284
+ hasActiveGesture(node) {
285
+ return (this.activeGestures.get(node.id)?.size ?? 0) > 0;
286
+ }
287
+ /** Last ResizeObserver-measured intrinsic size, if observed. */
288
+ getIntrinsicSize(nodeId) {
289
+ const state = this.states.get(nodeId);
290
+ if (!state || state.intrinsicWidth === 0) return void 0;
291
+ return { width: state.intrinsicWidth, height: state.intrinsicHeight };
292
+ }
293
+ /** Resolve the creation spec for a node (custom kinds win; unknown = plain div). */
294
+ specFor(node) {
295
+ return this.kinds.get(node.domKind) ?? fallbackSpec;
296
+ }
297
+ mount(node) {
298
+ if (this.states.has(node.id) || !this.root || typeof document === "undefined") return;
299
+ const spec = this.specFor(node);
300
+ let el = this.pools.get(spec.tag)?.pop();
301
+ if (el) {
302
+ this.stats.poolHits += 1;
303
+ if (node.domKind === "text") el.style.userSelect = "text";
304
+ el.style.display = "";
305
+ } else {
306
+ this.stats.poolMisses += 1;
307
+ el = spec.create(node);
308
+ }
309
+ el.style.position = "absolute";
310
+ el.style.left = "0px";
311
+ el.style.top = "0px";
312
+ el.style.transformOrigin = "0 0";
313
+ el.style.pointerEvents = "auto";
314
+ el.setAttribute("data-vecto-id", node.id);
315
+ this.root.appendChild(el);
316
+ const bridge = attachDOMBridge(el, node, {
317
+ ...this.bridgeOptions,
318
+ editable: node.domKind === "input" || this.bridgeOptions.editable,
319
+ onGestureStart: (nodeId, pointerId) => {
320
+ this.bridgeOptions.onGestureStart?.(nodeId, pointerId);
321
+ let owned = this.activeGestures.get(nodeId);
322
+ if (!owned) {
323
+ owned = /* @__PURE__ */ new Set();
324
+ this.activeGestures.set(nodeId, owned);
325
+ }
326
+ owned.add(pointerId);
327
+ },
328
+ onGestureEnd: (nodeId, pointerId) => {
329
+ this.bridgeOptions.onGestureEnd?.(nodeId, pointerId);
330
+ const owned = this.activeGestures.get(nodeId);
331
+ if (owned) {
332
+ owned.delete(pointerId);
333
+ if (owned.size === 0) this.activeGestures.delete(nodeId);
334
+ }
335
+ },
336
+ onNativeInput: node.domKind === "input" ? (value) => {
337
+ node.value = value;
338
+ } : this.bridgeOptions.onNativeInput
339
+ });
340
+ const state = {
341
+ el,
342
+ bridge,
343
+ observer: null,
344
+ tag: spec.tag,
345
+ cached: /* @__PURE__ */ new Map(),
346
+ lastTransform: "",
347
+ lastOpacity: "",
348
+ lastWidth: "",
349
+ lastHeight: "",
350
+ intrinsicWidth: 0,
351
+ intrinsicHeight: 0
352
+ };
353
+ if (typeof ResizeObserver !== "undefined") {
354
+ const observer = new ResizeObserver((entries) => {
355
+ const current = this.states.get(node.id);
356
+ if (!current) return;
357
+ for (const entry of entries) {
358
+ current.intrinsicWidth = entry.contentRect.width || entry.target.offsetWidth;
359
+ current.intrinsicHeight = entry.contentRect.height || entry.target.offsetHeight;
360
+ }
361
+ });
362
+ observer.observe(el);
363
+ state.observer = observer;
364
+ }
365
+ this.states.set(node.id, state);
366
+ el.style.zIndex = String(1 + this.mountSeq++);
367
+ node.domResident = true;
368
+ this.stats.mounts += 1;
369
+ }
370
+ update(node, worldMatrix) {
371
+ let state = this.states.get(node.id);
372
+ if (!state) {
373
+ this.mount(node);
374
+ state = this.states.get(node.id);
375
+ if (!state) return;
376
+ }
377
+ const transformStr = affineToMatrix3d(worldMatrix);
378
+ if (state.lastTransform !== transformStr) {
379
+ state.el.style.transform = transformStr;
380
+ state.lastTransform = transformStr;
381
+ this.stats.transformWrites += 1;
382
+ }
383
+ let opacity = 1;
384
+ let cursor = node;
385
+ let depth = 0;
386
+ while (cursor && depth < 32) {
387
+ opacity *= cursor.opacity;
388
+ cursor = cursor.parent;
389
+ depth += 1;
390
+ }
391
+ const opacityStr = String(opacity);
392
+ if (state.lastOpacity !== opacityStr) {
393
+ state.el.style.opacity = opacityStr;
394
+ state.lastOpacity = opacityStr;
395
+ this.stats.opacityWrites += 1;
396
+ }
397
+ if (node.width > 0 || node.height > 0) {
398
+ const widthStr = node.width > 0 ? `${node.width}px` : "";
399
+ const heightStr = node.height > 0 ? `${node.height}px` : "";
400
+ if (state.lastWidth !== widthStr || state.lastHeight !== heightStr) {
401
+ state.el.style.width = widthStr;
402
+ state.el.style.height = heightStr;
403
+ state.lastWidth = widthStr;
404
+ state.lastHeight = heightStr;
405
+ this.stats.sizeWrites += 1;
406
+ }
407
+ }
408
+ const spec = this.specFor(node);
409
+ const cached = state.cached;
410
+ const fields = {
411
+ write: (key, next, apply) => writeField(cached, key, next, apply, () => {
412
+ this.stats.contentWrites += 1;
413
+ })
414
+ };
415
+ spec.sync(node, state.el, fields);
416
+ }
417
+ unmount(node) {
418
+ const state = this.states.get(node.id);
419
+ this.activeGestures.delete(node.id);
420
+ node.domResident = false;
421
+ if (!state) return;
422
+ state.bridge.release();
423
+ if (typeof document !== "undefined") {
424
+ const active = document.activeElement;
425
+ if (active && active !== document.body && state.el.contains(active)) {
426
+ this.sentinel?.focus({ preventScroll: true });
427
+ }
428
+ }
429
+ state.observer?.disconnect();
430
+ state.el.remove();
431
+ const pool = this.pools.get(state.tag) ?? [];
432
+ if (pool.length < POOL_CAP_PER_TAG) {
433
+ pool.push(state.el);
434
+ this.pools.set(state.tag, pool);
435
+ }
436
+ this.states.delete(node.id);
437
+ this.stats.unmounts += 1;
438
+ }
439
+ /** Unmount every resident node and remove the root (scene teardown). */
440
+ dispose() {
441
+ for (const id of [...this.states.keys()]) {
442
+ const state = this.states.get(id);
443
+ if (!state) continue;
444
+ state.bridge.release();
445
+ state.observer?.disconnect();
446
+ state.el.remove();
447
+ }
448
+ this.states.clear();
449
+ this.pools.clear();
450
+ this.activeGestures.clear();
451
+ this.root?.remove();
452
+ this.root = null;
453
+ this.sentinel = null;
454
+ }
455
+ };
456
+
457
+ // src/nodes.ts
458
+ var import_core2 = require("@vectojs/core");
459
+ var DOMText = class extends import_core2.Entity {
460
+ text;
461
+ constructor(id, text = "", width = 200, height = 24) {
462
+ super(id);
463
+ this.text = text;
464
+ this.width = width;
465
+ this.height = height;
466
+ this.domPolicy = "dom";
467
+ this.domKind = "text";
468
+ this.interactive = true;
469
+ }
470
+ getBounds() {
471
+ if (this.width <= 0 || this.height <= 0) return null;
472
+ return { x: 0, y: 0, width: this.width, height: this.height };
473
+ }
474
+ isPointInside(globalX, globalY) {
475
+ const local = this.worldToLocal(globalX, globalY);
476
+ if (!local) return false;
477
+ return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
478
+ }
479
+ render() {
480
+ }
481
+ getA11yAttributes() {
482
+ return { tag: "div", role: "text", label: this.text };
483
+ }
484
+ };
485
+ var DOMButton = class extends import_core2.Entity {
486
+ label;
487
+ constructor(id, label = "", width = 120, height = 36) {
488
+ super(id);
489
+ this.label = label;
490
+ this.width = width;
491
+ this.height = height;
492
+ this.domPolicy = "dom";
493
+ this.domKind = "button";
494
+ this.interactive = true;
495
+ }
496
+ getBounds() {
497
+ if (this.width <= 0 || this.height <= 0) return null;
498
+ return { x: 0, y: 0, width: this.width, height: this.height };
499
+ }
500
+ isPointInside(globalX, globalY) {
501
+ const local = this.worldToLocal(globalX, globalY);
502
+ if (!local) return false;
503
+ return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
504
+ }
505
+ render() {
506
+ }
507
+ getA11yAttributes() {
508
+ return { tag: "button", role: "button", label: this.label };
509
+ }
510
+ };
511
+ var DOMInput = class extends import_core2.Entity {
512
+ value;
513
+ placeholder;
514
+ constructor(id, value = "", width = 200, height = 32) {
515
+ super(id);
516
+ this.value = value;
517
+ this.width = width;
518
+ this.height = height;
519
+ this.domPolicy = "dom";
520
+ this.domKind = "input";
521
+ this.interactive = true;
522
+ }
523
+ getBounds() {
524
+ if (this.width <= 0 || this.height <= 0) return null;
525
+ return { x: 0, y: 0, width: this.width, height: this.height };
526
+ }
527
+ isPointInside(globalX, globalY) {
528
+ const local = this.worldToLocal(globalX, globalY);
529
+ if (!local) return false;
530
+ return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
531
+ }
532
+ render() {
533
+ }
534
+ getA11yAttributes() {
535
+ return { tag: "input", role: "textbox", label: this.placeholder ?? "input", value: this.value };
536
+ }
537
+ };
538
+ var DOMContainer = class extends import_core2.Entity {
539
+ constructor(id, width = 0, height = 0) {
540
+ super(id);
541
+ this.width = width;
542
+ this.height = height;
543
+ this.domPolicy = "dom";
544
+ this.domKind = "container";
545
+ this.interactive = true;
546
+ }
547
+ getBounds() {
548
+ if (this.width <= 0 || this.height <= 0) return null;
549
+ return { x: 0, y: 0, width: this.width, height: this.height };
550
+ }
551
+ isPointInside(globalX, globalY) {
552
+ const local = this.worldToLocal(globalX, globalY);
553
+ if (!local) return false;
554
+ return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
555
+ }
556
+ render() {
557
+ }
558
+ };
559
+ var DOMTransform = class extends import_core2.Entity {
560
+ constructor(id) {
561
+ super(id);
562
+ this.domPolicy = "dom";
563
+ this.domKind = "transform";
564
+ this.interactive = true;
565
+ }
566
+ getBounds() {
567
+ return null;
568
+ }
569
+ isPointInside(_globalX, _globalY) {
570
+ return false;
571
+ }
572
+ render() {
573
+ }
574
+ };
575
+ // Annotate the CommonJS export names for ESM import in node:
576
+ 0 && (module.exports = {
577
+ DOMButton,
578
+ DOMContainer,
579
+ DOMInput,
580
+ DOMProjection,
581
+ DOMText,
582
+ DOMTransform,
583
+ DOM_ROOT_ATTR,
584
+ DOM_VISUAL_Z_INDEX,
585
+ affineToMatrix3d,
586
+ attachDOMBridge,
587
+ getGestureOwner,
588
+ writeField
589
+ });