@hypen-space/web 0.4.3 → 0.4.11
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/canvas/index.js +30 -2
- package/dist/canvas/index.js.map +4 -4
- package/dist/canvas/input.d.ts +8 -1
- package/dist/canvas/input.js +14 -2
- package/dist/canvas/input.js.map +3 -3
- package/dist/canvas/renderer.d.ts +4 -0
- package/dist/canvas/renderer.js +30 -2
- package/dist/canvas/renderer.js.map +4 -4
- package/dist/client.js +71 -26
- package/dist/client.js.map +6 -6
- package/dist/dom/applicators/advanced-layout.js +1 -4
- package/dist/dom/applicators/advanced-layout.js.map +3 -3
- package/dist/dom/applicators/events.js +53 -2
- package/dist/dom/applicators/events.js.map +3 -3
- package/dist/dom/applicators/index.js +53 -5
- package/dist/dom/applicators/index.js.map +4 -4
- package/dist/dom/index.js +66 -8
- package/dist/dom/index.js.map +5 -5
- package/dist/dom/renderer.d.ts +7 -3
- package/dist/dom/renderer.js +66 -8
- package/dist/dom/renderer.js.map +5 -5
- package/dist/hypen.js +71 -26
- package/dist/hypen.js.map +6 -6
- package/dist/index.js +95 -9
- package/dist/index.js.map +7 -7
- package/package.json +2 -2
- package/src/canvas/input.ts +23 -1
- package/src/canvas/renderer.ts +23 -0
- package/src/dom/applicators/advanced-layout.ts +0 -4
- package/src/dom/applicators/events.ts +71 -0
- package/src/dom/renderer.ts +18 -4
- package/src/hypen.ts +7 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypen-space/web",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.11",
|
|
4
4
|
"description": "Hypen web renderers - DOM and Canvas rendering for browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"clean": "rm -rf dist"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@hypen-space/core": "^0.4.
|
|
59
|
+
"@hypen-space/core": "^0.4.11"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/bun": "latest",
|
package/src/canvas/input.ts
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
import type { VirtualNode, Rectangle } from "./types.js";
|
|
8
8
|
import { getAbsoluteBounds } from "./utils.js";
|
|
9
9
|
|
|
10
|
+
/** Minimal engine interface for dispatching bind actions */
|
|
11
|
+
interface BindEngine {
|
|
12
|
+
dispatchAction(name: string, payload?: unknown): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* Input Overlay Manager
|
|
12
17
|
*/
|
|
@@ -15,6 +20,8 @@ export class InputOverlay {
|
|
|
15
20
|
private overlay: HTMLInputElement | HTMLTextAreaElement | null = null;
|
|
16
21
|
private focusedNode: VirtualNode | null = null;
|
|
17
22
|
private onChangeCallback: ((value: string) => void) | null = null;
|
|
23
|
+
private bindPath: string | null = null;
|
|
24
|
+
private engine: BindEngine | null = null;
|
|
18
25
|
|
|
19
26
|
constructor(container: HTMLElement | null) {
|
|
20
27
|
this.container = container || ({} as HTMLElement);
|
|
@@ -26,13 +33,18 @@ export class InputOverlay {
|
|
|
26
33
|
showInput(
|
|
27
34
|
node: VirtualNode,
|
|
28
35
|
canvasBounds: DOMRect,
|
|
29
|
-
onChange: (value: string) => void
|
|
36
|
+
onChange: (value: string) => void,
|
|
37
|
+
engine?: BindEngine | null
|
|
30
38
|
): void {
|
|
31
39
|
// Skip if document is not available (non-browser environment)
|
|
32
40
|
if (typeof document === "undefined") return;
|
|
33
41
|
|
|
34
42
|
this.hideInput();
|
|
35
43
|
|
|
44
|
+
// Store bind path and engine for two-way binding
|
|
45
|
+
this.bindPath = (node.props.bind as string) || null;
|
|
46
|
+
this.engine = engine || null;
|
|
47
|
+
|
|
36
48
|
const bounds = getAbsoluteBounds(node);
|
|
37
49
|
if (!bounds) return;
|
|
38
50
|
|
|
@@ -71,6 +83,8 @@ export class InputOverlay {
|
|
|
71
83
|
}
|
|
72
84
|
this.focusedNode = null;
|
|
73
85
|
this.onChangeCallback = null;
|
|
86
|
+
this.bindPath = null;
|
|
87
|
+
this.engine = null;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
/**
|
|
@@ -164,6 +178,14 @@ export class InputOverlay {
|
|
|
164
178
|
|
|
165
179
|
const value = this.overlay.value;
|
|
166
180
|
this.onChangeCallback(value);
|
|
181
|
+
|
|
182
|
+
// Dispatch __hypen_bind for two-way binding
|
|
183
|
+
if (this.bindPath && this.engine) {
|
|
184
|
+
this.engine.dispatchAction("__hypen_bind", {
|
|
185
|
+
path: this.bindPath,
|
|
186
|
+
value,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
167
189
|
}
|
|
168
190
|
|
|
169
191
|
/**
|
package/src/canvas/renderer.ts
CHANGED
|
@@ -141,6 +141,10 @@ export class CanvasRenderer implements Renderer {
|
|
|
141
141
|
this.onSetProp(patch.id!, patch.name!, patch.value);
|
|
142
142
|
break;
|
|
143
143
|
|
|
144
|
+
case "removeProp":
|
|
145
|
+
this.onRemoveProp(patch.id!, patch.name!);
|
|
146
|
+
break;
|
|
147
|
+
|
|
144
148
|
case "setText":
|
|
145
149
|
this.onSetText(patch.id!, patch.text!);
|
|
146
150
|
break;
|
|
@@ -202,6 +206,25 @@ export class CanvasRenderer implements Renderer {
|
|
|
202
206
|
this.accessibilityLayer.updateNode(node);
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Remove a property from a node
|
|
211
|
+
*/
|
|
212
|
+
private onRemoveProp(id: string, name: string): void {
|
|
213
|
+
const node = this.nodes.get(id);
|
|
214
|
+
if (!node) return;
|
|
215
|
+
|
|
216
|
+
delete node.props[name];
|
|
217
|
+
|
|
218
|
+
if (name === "visible") {
|
|
219
|
+
node.visible = true;
|
|
220
|
+
}
|
|
221
|
+
if (name === "opacity") {
|
|
222
|
+
node.opacity = 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.accessibilityLayer.updateNode(node);
|
|
226
|
+
}
|
|
227
|
+
|
|
205
228
|
/**
|
|
206
229
|
* Set text on node
|
|
207
230
|
*/
|
|
@@ -23,10 +23,6 @@ export const advancedLayoutHandlers: Record<string, ApplicatorHandler> = {
|
|
|
23
23
|
el.style.alignContent = String(value);
|
|
24
24
|
},
|
|
25
25
|
|
|
26
|
-
alignSelf: (el, value) => {
|
|
27
|
-
el.style.alignSelf = String(value);
|
|
28
|
-
},
|
|
29
|
-
|
|
30
26
|
order: (el, value) => {
|
|
31
27
|
el.style.order = String(value);
|
|
32
28
|
},
|
|
@@ -492,4 +492,75 @@ export const eventHandlers: Record<string, ApplicatorHandler> = {
|
|
|
492
492
|
// Mouse hover events
|
|
493
493
|
onMouseEnter: createEventHandler("mouseenter", { extractPayload: mousePayload }),
|
|
494
494
|
onMouseLeave: createEventHandler("mouseleave", { extractPayload: mousePayload }),
|
|
495
|
+
|
|
496
|
+
// Two-way binding for .bind(@state.x)
|
|
497
|
+
bind: ((element: HTMLElement, value: unknown) => {
|
|
498
|
+
const bindPath = typeof value === "string" ? value : null;
|
|
499
|
+
if (!bindPath) return;
|
|
500
|
+
|
|
501
|
+
const disposables = getElementDisposables(element);
|
|
502
|
+
const eventKey = `bind:${bindPath}`;
|
|
503
|
+
if (getRegisteredEvents(element).has(eventKey)) return;
|
|
504
|
+
registerEvent(element, eventKey);
|
|
505
|
+
|
|
506
|
+
// Determine the target element, event type, and value extractor based on component type
|
|
507
|
+
const hypenType = element.dataset?.hypenType;
|
|
508
|
+
|
|
509
|
+
if (hypenType === "checkbox" || hypenType === "switch") {
|
|
510
|
+
// Checkbox/Switch: wrapper <label> containing <input type="checkbox">
|
|
511
|
+
const input = element.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
|
|
512
|
+
if (!input) return;
|
|
513
|
+
|
|
514
|
+
const listener = () => {
|
|
515
|
+
const engine = getEngine(element);
|
|
516
|
+
if (engine) {
|
|
517
|
+
engine.dispatchAction("__hypen_bind", {
|
|
518
|
+
path: bindPath,
|
|
519
|
+
value: input.checked,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
disposables.add(
|
|
525
|
+
disposableListener(input, "change", listener, { passive: true })
|
|
526
|
+
);
|
|
527
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
528
|
+
// Select: listen to change event, read .value
|
|
529
|
+
const listener = () => {
|
|
530
|
+
const engine = getEngine(element);
|
|
531
|
+
if (engine) {
|
|
532
|
+
engine.dispatchAction("__hypen_bind", {
|
|
533
|
+
path: bindPath,
|
|
534
|
+
value: element.value,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
disposables.add(
|
|
540
|
+
disposableListener(element, "change", listener, { passive: true })
|
|
541
|
+
);
|
|
542
|
+
} else if (
|
|
543
|
+
element instanceof HTMLInputElement ||
|
|
544
|
+
element instanceof HTMLTextAreaElement
|
|
545
|
+
) {
|
|
546
|
+
// Input/Textarea: listen to input event, read .value
|
|
547
|
+
const listener = () => {
|
|
548
|
+
const engine = getEngine(element);
|
|
549
|
+
if (engine) {
|
|
550
|
+
engine.dispatchAction("__hypen_bind", {
|
|
551
|
+
path: bindPath,
|
|
552
|
+
value: element.value,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
disposables.add(
|
|
558
|
+
disposableListener(element, "input", listener, { passive: true })
|
|
559
|
+
);
|
|
560
|
+
} else {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
disposables.addCallback(() => unregisterEvent(element, eventKey));
|
|
565
|
+
}) as ApplicatorHandler,
|
|
495
566
|
};
|
package/src/dom/renderer.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Patch } from "@hypen-space/core/types";
|
|
8
|
-
import type { HypenModuleInstance
|
|
8
|
+
import type { HypenModuleInstance } from "@hypen-space/core/app";
|
|
9
9
|
import type { HypenRouter } from "@hypen-space/core/router";
|
|
10
10
|
import type { HypenGlobalContext } from "@hypen-space/core/context";
|
|
11
11
|
import { frameworkLoggers } from "@hypen-space/core/logger";
|
|
@@ -30,7 +30,7 @@ export class DOMRenderer {
|
|
|
30
30
|
private applicators: ApplicatorRegistry;
|
|
31
31
|
private engine: IEngine;
|
|
32
32
|
private currentState: Record<string, any> = {};
|
|
33
|
-
private
|
|
33
|
+
private router: HypenRouter | null = null;
|
|
34
34
|
private globalContext: HypenGlobalContext | null = null;
|
|
35
35
|
private componentInstances = new Map<string, HypenModuleInstance>();
|
|
36
36
|
private debugTracker: RerenderTracker;
|
|
@@ -52,8 +52,8 @@ export class DOMRenderer {
|
|
|
52
52
|
/**
|
|
53
53
|
* Set router and global context for component composition
|
|
54
54
|
*/
|
|
55
|
-
setContext(
|
|
56
|
-
this.
|
|
55
|
+
setContext(router: HypenRouter | null, globalContext: HypenGlobalContext): void {
|
|
56
|
+
this.router = router;
|
|
57
57
|
this.globalContext = globalContext;
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -144,6 +144,9 @@ export class DOMRenderer {
|
|
|
144
144
|
case "setProp":
|
|
145
145
|
this.onSetProp(id!, patch.name!, patch.value);
|
|
146
146
|
break;
|
|
147
|
+
case "removeProp":
|
|
148
|
+
this.onRemoveProp(id!, patch.name!);
|
|
149
|
+
break;
|
|
147
150
|
case "setText":
|
|
148
151
|
this.onSetText(id!, patch.text!);
|
|
149
152
|
break;
|
|
@@ -239,6 +242,17 @@ export class DOMRenderer {
|
|
|
239
242
|
this.applicators.apply(element, name, value);
|
|
240
243
|
}
|
|
241
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Remove a property from an element
|
|
247
|
+
*/
|
|
248
|
+
private onRemoveProp(id: string, name: string): void {
|
|
249
|
+
const element = this.nodes.get(id);
|
|
250
|
+
if (!element) return;
|
|
251
|
+
|
|
252
|
+
this.debugTracker.trackRerender(id, element, `removeProp:${name}`);
|
|
253
|
+
this.applicators.apply(element, name, undefined);
|
|
254
|
+
}
|
|
255
|
+
|
|
242
256
|
/**
|
|
243
257
|
* Set text content
|
|
244
258
|
*/
|
package/src/hypen.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { Engine } from "@hypen-space/core/engine/browser";
|
|
8
8
|
import { HypenModuleInstance } from "@hypen-space/core/app";
|
|
9
|
-
import type {
|
|
9
|
+
import type { HypenModuleDefinition } from "@hypen-space/core/app";
|
|
10
10
|
import { HypenRouter } from "@hypen-space/core/router";
|
|
11
11
|
import { HypenGlobalContext } from "@hypen-space/core/context";
|
|
12
12
|
import { componentLoader } from "@hypen-space/core/loader";
|
|
@@ -65,8 +65,7 @@ export class Hypen {
|
|
|
65
65
|
componentLoader.register("Route", Route, "");
|
|
66
66
|
componentLoader.register("Link", Link, "");
|
|
67
67
|
|
|
68
|
-
// Store
|
|
69
|
-
(this.globalContext as any).__router = this.router;
|
|
68
|
+
// Store hypen engine instance in global context for built-in component access
|
|
70
69
|
(this.globalContext as any).__hypenEngine = this;
|
|
71
70
|
}
|
|
72
71
|
|
|
@@ -150,14 +149,8 @@ export class Hypen {
|
|
|
150
149
|
this.renderer!.applyPatches(patches);
|
|
151
150
|
});
|
|
152
151
|
|
|
153
|
-
// Create router context
|
|
154
|
-
const routerContext: RouterContext = {
|
|
155
|
-
root: this.router,
|
|
156
|
-
current: this.router,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
152
|
// Set context on renderer for component composition
|
|
160
|
-
this.renderer.setContext(
|
|
153
|
+
this.renderer.setContext(this.router, this.globalContext);
|
|
161
154
|
|
|
162
155
|
// Extract module ID from component name or .id() applicator
|
|
163
156
|
const moduleId = this.extractModuleId(componentName, component.template);
|
|
@@ -166,7 +159,7 @@ export class Hypen {
|
|
|
166
159
|
this.moduleInstance = new HypenModuleInstance(
|
|
167
160
|
this.engine,
|
|
168
161
|
component.module,
|
|
169
|
-
|
|
162
|
+
this.router,
|
|
170
163
|
this.globalContext
|
|
171
164
|
);
|
|
172
165
|
|
|
@@ -305,15 +298,10 @@ export class Hypen {
|
|
|
305
298
|
if (component.module && !this.moduleInstances.has(componentName)) {
|
|
306
299
|
const moduleId = this.extractModuleId(componentName, template);
|
|
307
300
|
|
|
308
|
-
const routerContext: RouterContext = {
|
|
309
|
-
root: this.router,
|
|
310
|
-
current: this.router,
|
|
311
|
-
};
|
|
312
|
-
|
|
313
301
|
const moduleInstance = new HypenModuleInstance(
|
|
314
302
|
this.engine,
|
|
315
303
|
component.module,
|
|
316
|
-
|
|
304
|
+
this.router,
|
|
317
305
|
this.globalContext
|
|
318
306
|
);
|
|
319
307
|
|
|
@@ -366,17 +354,12 @@ export class Hypen {
|
|
|
366
354
|
* Create module instances for all components that have state
|
|
367
355
|
*/
|
|
368
356
|
private createNestedModuleInstances(): void {
|
|
369
|
-
const routerContext: RouterContext = {
|
|
370
|
-
root: this.router,
|
|
371
|
-
current: this.router,
|
|
372
|
-
};
|
|
373
|
-
|
|
374
357
|
// Create built-in Router module instance
|
|
375
358
|
if (Router && !this.moduleInstances.has("Router")) {
|
|
376
359
|
const routerInstance = new HypenModuleInstance(
|
|
377
360
|
this.engine!,
|
|
378
361
|
Router,
|
|
379
|
-
|
|
362
|
+
this.router,
|
|
380
363
|
this.globalContext
|
|
381
364
|
);
|
|
382
365
|
this.globalContext.registerModule("Router", routerInstance);
|
|
@@ -408,7 +391,7 @@ export class Hypen {
|
|
|
408
391
|
const moduleInstance = new HypenModuleInstance(
|
|
409
392
|
this.engine!,
|
|
410
393
|
comp.module,
|
|
411
|
-
|
|
394
|
+
this.router,
|
|
412
395
|
this.globalContext
|
|
413
396
|
);
|
|
414
397
|
|