@hypen-space/web 0.4.32 → 0.4.35
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/accessibility.js +19 -9
- package/dist/canvas/accessibility.js.map +2 -2
- package/dist/canvas/events.d.ts +7 -0
- package/dist/canvas/events.js +53 -17
- package/dist/canvas/events.js.map +3 -3
- package/dist/canvas/index.js +19 -2134
- package/dist/canvas/index.js.map +3 -11
- package/dist/canvas/input.js +19 -9
- package/dist/canvas/input.js.map +2 -2
- package/dist/canvas/layout.js +19 -9
- package/dist/canvas/layout.js.map +2 -2
- package/dist/canvas/paint.d.ts +4 -0
- package/dist/canvas/paint.js +100 -11
- package/dist/canvas/paint.js.map +3 -3
- package/dist/canvas/renderer.js +821 -707
- package/dist/canvas/renderer.js.map +7 -7
- package/dist/canvas/text.js +19 -9
- package/dist/canvas/text.js.map +2 -2
- package/dist/canvas/utils.js +19 -9
- package/dist/canvas/utils.js.map +2 -2
- package/dist/client.js +19 -3863
- package/dist/client.js.map +3 -57
- package/dist/dom/applicators/advanced-layout.js +19 -9
- package/dist/dom/applicators/advanced-layout.js.map +2 -2
- package/dist/dom/applicators/background.js +19 -9
- package/dist/dom/applicators/background.js.map +2 -2
- package/dist/dom/applicators/border.js +19 -9
- package/dist/dom/applicators/border.js.map +2 -2
- package/dist/dom/applicators/color.js +19 -9
- package/dist/dom/applicators/color.js.map +2 -2
- package/dist/dom/applicators/display.js +19 -9
- package/dist/dom/applicators/display.js.map +2 -2
- package/dist/dom/applicators/effects.js +19 -9
- package/dist/dom/applicators/effects.js.map +2 -2
- package/dist/dom/applicators/events.js +26 -10
- package/dist/dom/applicators/events.js.map +3 -3
- package/dist/dom/applicators/font.js +19 -9
- package/dist/dom/applicators/font.js.map +2 -2
- package/dist/dom/applicators/index.js +26 -10
- package/dist/dom/applicators/index.js.map +3 -3
- package/dist/dom/applicators/layout.js +19 -9
- package/dist/dom/applicators/layout.js.map +2 -2
- package/dist/dom/applicators/margin.js +19 -9
- package/dist/dom/applicators/margin.js.map +2 -2
- package/dist/dom/applicators/padding.js +19 -9
- package/dist/dom/applicators/padding.js.map +2 -2
- package/dist/dom/applicators/size.js +19 -9
- package/dist/dom/applicators/size.js.map +2 -2
- package/dist/dom/applicators/transform.js +19 -9
- package/dist/dom/applicators/transform.js.map +2 -2
- package/dist/dom/applicators/transition.js +19 -9
- package/dist/dom/applicators/transition.js.map +2 -2
- package/dist/dom/applicators/typography.js +19 -9
- package/dist/dom/applicators/typography.js.map +2 -2
- package/dist/dom/canvas/index.js +19 -9
- package/dist/dom/canvas/index.js.map +2 -2
- package/dist/dom/components/app.js +19 -9
- package/dist/dom/components/app.js.map +2 -2
- package/dist/dom/components/audio.js +19 -9
- package/dist/dom/components/audio.js.map +2 -2
- package/dist/dom/components/avatar.js +19 -9
- package/dist/dom/components/avatar.js.map +2 -2
- package/dist/dom/components/badge.js +19 -9
- package/dist/dom/components/badge.js.map +2 -2
- package/dist/dom/components/button.js +19 -9
- package/dist/dom/components/button.js.map +2 -2
- package/dist/dom/components/card.js +19 -9
- package/dist/dom/components/card.js.map +2 -2
- package/dist/dom/components/center.js +19 -9
- package/dist/dom/components/center.js.map +2 -2
- package/dist/dom/components/checkbox.js +19 -9
- package/dist/dom/components/checkbox.js.map +2 -2
- package/dist/dom/components/column.js +19 -9
- package/dist/dom/components/column.js.map +2 -2
- package/dist/dom/components/container.js +19 -9
- package/dist/dom/components/container.js.map +2 -2
- package/dist/dom/components/divider.js +19 -9
- package/dist/dom/components/divider.js.map +2 -2
- package/dist/dom/components/grid.js +19 -9
- package/dist/dom/components/grid.js.map +2 -2
- package/dist/dom/components/heading.js +19 -9
- package/dist/dom/components/heading.js.map +2 -2
- package/dist/dom/components/hypenapp.js +19 -9
- package/dist/dom/components/hypenapp.js.map +2 -2
- package/dist/dom/components/image.js +19 -9
- package/dist/dom/components/image.js.map +2 -2
- package/dist/dom/components/index.js +20 -10
- package/dist/dom/components/index.js.map +3 -3
- package/dist/dom/components/input.js +19 -9
- package/dist/dom/components/input.js.map +2 -2
- package/dist/dom/components/link.js +19 -9
- package/dist/dom/components/link.js.map +2 -2
- package/dist/dom/components/list.js +19 -9
- package/dist/dom/components/list.js.map +2 -2
- package/dist/dom/components/paragraph.js +19 -9
- package/dist/dom/components/paragraph.js.map +2 -2
- package/dist/dom/components/progressbar.js +19 -9
- package/dist/dom/components/progressbar.js.map +2 -2
- package/dist/dom/components/route.js +19 -9
- package/dist/dom/components/route.js.map +2 -2
- package/dist/dom/components/router.js +19 -9
- package/dist/dom/components/router.js.map +2 -2
- package/dist/dom/components/row.js +19 -9
- package/dist/dom/components/row.js.map +2 -2
- package/dist/dom/components/select.js +19 -9
- package/dist/dom/components/select.js.map +2 -2
- package/dist/dom/components/slider.js +19 -9
- package/dist/dom/components/slider.js.map +2 -2
- package/dist/dom/components/spacer.js +19 -9
- package/dist/dom/components/spacer.js.map +2 -2
- package/dist/dom/components/spinner.js +19 -9
- package/dist/dom/components/spinner.js.map +2 -2
- package/dist/dom/components/stack.js +19 -9
- package/dist/dom/components/stack.js.map +2 -2
- package/dist/dom/components/switch.js +19 -9
- package/dist/dom/components/switch.js.map +2 -2
- package/dist/dom/components/text.js +19 -9
- package/dist/dom/components/text.js.map +2 -2
- package/dist/dom/components/textarea.js +20 -10
- package/dist/dom/components/textarea.js.map +3 -3
- package/dist/dom/components/video.js +19 -9
- package/dist/dom/components/video.js.map +2 -2
- package/dist/dom/debug.js +19 -9
- package/dist/dom/debug.js.map +2 -2
- package/dist/dom/element-data.js +19 -9
- package/dist/dom/element-data.js.map +2 -2
- package/dist/dom/events.js +28 -18
- package/dist/dom/events.js.map +3 -3
- package/dist/dom/index.js +19 -3682
- package/dist/dom/index.js.map +3 -57
- package/dist/dom/renderer.js +31 -11
- package/dist/dom/renderer.js.map +5 -5
- package/dist/hypen.js +31 -11
- package/dist/hypen.js.map +5 -5
- package/dist/index.js +19 -5810
- package/dist/index.js.map +3 -66
- package/package.json +2 -2
- package/src/canvas/events.ts +35 -8
- package/src/canvas/paint.ts +104 -4
- package/src/dom/applicators/events.ts +11 -2
- package/src/dom/components/textarea.ts +3 -3
- package/src/dom/events.ts +14 -12
- package/src/dom/renderer.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypen-space/web",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.35",
|
|
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.35"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/bun": "latest",
|
package/src/canvas/events.ts
CHANGED
|
@@ -23,6 +23,15 @@ export class CanvasEventManager {
|
|
|
23
23
|
private focusedNode: VirtualNode | null = null;
|
|
24
24
|
private mouseDownNode: VirtualNode | null = null;
|
|
25
25
|
|
|
26
|
+
// Bound handler references for cleanup
|
|
27
|
+
private boundOnMouseMove!: (e: MouseEvent) => void;
|
|
28
|
+
private boundOnMouseDown!: (e: MouseEvent) => void;
|
|
29
|
+
private boundOnMouseUp!: (e: MouseEvent) => void;
|
|
30
|
+
private boundOnClick!: (e: MouseEvent) => void;
|
|
31
|
+
private boundOnDoubleClick!: (e: MouseEvent) => void;
|
|
32
|
+
private boundOnKeyDown!: (e: KeyboardEvent) => void;
|
|
33
|
+
private boundOnKeyUp!: (e: KeyboardEvent) => void;
|
|
34
|
+
|
|
26
35
|
constructor(canvas: HTMLCanvasElement, engine: IEngine) {
|
|
27
36
|
this.canvas = canvas;
|
|
28
37
|
this.engine = engine;
|
|
@@ -40,13 +49,21 @@ export class CanvasEventManager {
|
|
|
40
49
|
* Setup canvas event listeners
|
|
41
50
|
*/
|
|
42
51
|
private setupEventListeners(): void {
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
52
|
+
this.boundOnMouseMove = this.onMouseMove.bind(this);
|
|
53
|
+
this.boundOnMouseDown = this.onMouseDown.bind(this);
|
|
54
|
+
this.boundOnMouseUp = this.onMouseUp.bind(this);
|
|
55
|
+
this.boundOnClick = this.onClick.bind(this);
|
|
56
|
+
this.boundOnDoubleClick = this.onDoubleClick.bind(this);
|
|
57
|
+
this.boundOnKeyDown = this.onKeyDown.bind(this);
|
|
58
|
+
this.boundOnKeyUp = this.onKeyUp.bind(this);
|
|
59
|
+
|
|
60
|
+
this.canvas.addEventListener("mousemove", this.boundOnMouseMove);
|
|
61
|
+
this.canvas.addEventListener("mousedown", this.boundOnMouseDown);
|
|
62
|
+
this.canvas.addEventListener("mouseup", this.boundOnMouseUp);
|
|
63
|
+
this.canvas.addEventListener("click", this.boundOnClick);
|
|
64
|
+
this.canvas.addEventListener("dblclick", this.boundOnDoubleClick);
|
|
65
|
+
this.canvas.addEventListener("keydown", this.boundOnKeyDown);
|
|
66
|
+
this.canvas.addEventListener("keyup", this.boundOnKeyUp);
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
/**
|
|
@@ -293,7 +310,17 @@ export class CanvasEventManager {
|
|
|
293
310
|
* Cleanup
|
|
294
311
|
*/
|
|
295
312
|
destroy(): void {
|
|
296
|
-
|
|
313
|
+
this.canvas.removeEventListener("mousemove", this.boundOnMouseMove);
|
|
314
|
+
this.canvas.removeEventListener("mousedown", this.boundOnMouseDown);
|
|
315
|
+
this.canvas.removeEventListener("mouseup", this.boundOnMouseUp);
|
|
316
|
+
this.canvas.removeEventListener("click", this.boundOnClick);
|
|
317
|
+
this.canvas.removeEventListener("dblclick", this.boundOnDoubleClick);
|
|
318
|
+
this.canvas.removeEventListener("keydown", this.boundOnKeyDown);
|
|
319
|
+
this.canvas.removeEventListener("keyup", this.boundOnKeyUp);
|
|
320
|
+
this.rootNode = null;
|
|
321
|
+
this.hoveredNode = null;
|
|
322
|
+
this.focusedNode = null;
|
|
323
|
+
this.mouseDownNode = null;
|
|
297
324
|
}
|
|
298
325
|
}
|
|
299
326
|
|
package/src/canvas/paint.ts
CHANGED
|
@@ -416,6 +416,62 @@ function paintInput(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Image cache for canvas renderer
|
|
421
|
+
*/
|
|
422
|
+
const MAX_IMAGE_CACHE_SIZE = 100;
|
|
423
|
+
const MAX_FAIL_CACHE_SIZE = 200;
|
|
424
|
+
const imageCache = new Map<string, HTMLImageElement>();
|
|
425
|
+
const imagePending = new Set<string>();
|
|
426
|
+
const imageInFlight = new Map<string, HTMLImageElement>();
|
|
427
|
+
const imageFailCache = new Map<string, { failedAt: number; attempts: number }>();
|
|
428
|
+
const IMAGE_FAIL_COOLDOWN_MS = 30_000;
|
|
429
|
+
const IMAGE_MAX_ATTEMPTS = 3;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Evict oldest entries from a Map when it exceeds the given maximum size.
|
|
433
|
+
*/
|
|
434
|
+
function evictMap<V>(map: Map<string, V>, max: number): void {
|
|
435
|
+
while (map.size > max) {
|
|
436
|
+
const oldest = map.keys().next().value;
|
|
437
|
+
if (oldest === undefined) break;
|
|
438
|
+
map.delete(oldest);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Purge expired entries from imageFailCache.
|
|
444
|
+
*/
|
|
445
|
+
function purgeExpiredFailures(): void {
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
for (const [key, entry] of imageFailCache) {
|
|
448
|
+
if (entry.attempts >= IMAGE_MAX_ATTEMPTS) continue; // Keep max-attempt entries until evicted by size
|
|
449
|
+
if (now - entry.failedAt >= IMAGE_FAIL_COOLDOWN_MS) {
|
|
450
|
+
imageFailCache.delete(key);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Clear the image cache and cancel all in-flight loads, freeing resources.
|
|
457
|
+
*/
|
|
458
|
+
export function clearImageCache(): void {
|
|
459
|
+
// Cancel in-flight Image loads so their callbacks cannot repopulate caches
|
|
460
|
+
for (const img of imageInFlight.values()) {
|
|
461
|
+
img.onload = null;
|
|
462
|
+
img.onerror = null;
|
|
463
|
+
img.src = "";
|
|
464
|
+
}
|
|
465
|
+
imageInFlight.clear();
|
|
466
|
+
|
|
467
|
+
for (const img of imageCache.values()) {
|
|
468
|
+
img.src = "";
|
|
469
|
+
}
|
|
470
|
+
imageCache.clear();
|
|
471
|
+
imagePending.clear();
|
|
472
|
+
imageFailCache.clear();
|
|
473
|
+
}
|
|
474
|
+
|
|
419
475
|
/**
|
|
420
476
|
* Paint image node
|
|
421
477
|
*/
|
|
@@ -426,13 +482,19 @@ function paintImage(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
426
482
|
const src = props.src || props[0];
|
|
427
483
|
if (!src) return;
|
|
428
484
|
|
|
429
|
-
// TODO: Load and cache images
|
|
430
|
-
// For now, just draw a placeholder
|
|
431
485
|
const x = layout.x;
|
|
432
486
|
const y = layout.y;
|
|
433
487
|
const width = layout.width;
|
|
434
488
|
const height = layout.height;
|
|
435
489
|
|
|
490
|
+
const cached = imageCache.get(src);
|
|
491
|
+
if (cached && cached.complete && cached.naturalWidth > 0) {
|
|
492
|
+
// Draw the loaded image
|
|
493
|
+
ctx.drawImage(cached, x, y, width, height);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Draw placeholder while loading
|
|
436
498
|
ctx.fillStyle = "#e0e0e0";
|
|
437
499
|
ctx.fillRect(x, y, width, height);
|
|
438
500
|
|
|
@@ -440,12 +502,50 @@ function paintImage(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
440
502
|
ctx.lineWidth = 1;
|
|
441
503
|
ctx.strokeRect(x, y, width, height);
|
|
442
504
|
|
|
443
|
-
// Draw "IMG" text
|
|
444
505
|
ctx.fillStyle = "#666666";
|
|
445
506
|
ctx.font = "14px sans-serif";
|
|
446
507
|
ctx.textAlign = "center";
|
|
447
508
|
ctx.textBaseline = "middle";
|
|
448
|
-
|
|
509
|
+
|
|
510
|
+
// Check if this URL has failed recently
|
|
511
|
+
const failEntry = imageFailCache.get(src);
|
|
512
|
+
if (failEntry) {
|
|
513
|
+
if (failEntry.attempts >= IMAGE_MAX_ATTEMPTS || Date.now() - failEntry.failedAt < IMAGE_FAIL_COOLDOWN_MS) {
|
|
514
|
+
ctx.fillText("Failed", x + width / 2, y + height / 2);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
ctx.fillText("Loading...", x + width / 2, y + height / 2);
|
|
520
|
+
|
|
521
|
+
// Start loading if not already pending
|
|
522
|
+
if (!imagePending.has(src)) {
|
|
523
|
+
imagePending.add(src);
|
|
524
|
+
const canvas = ctx.canvas;
|
|
525
|
+
const img = new Image();
|
|
526
|
+
img.crossOrigin = "anonymous";
|
|
527
|
+
imageInFlight.set(src, img);
|
|
528
|
+
img.onload = () => {
|
|
529
|
+
imageInFlight.delete(src);
|
|
530
|
+
imageCache.set(src, img);
|
|
531
|
+
imagePending.delete(src);
|
|
532
|
+
imageFailCache.delete(src);
|
|
533
|
+
evictMap(imageCache, MAX_IMAGE_CACHE_SIZE);
|
|
534
|
+
// Request a repaint so the loaded image is drawn
|
|
535
|
+
canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
|
|
536
|
+
};
|
|
537
|
+
img.onerror = () => {
|
|
538
|
+
imageInFlight.delete(src);
|
|
539
|
+
imagePending.delete(src);
|
|
540
|
+
purgeExpiredFailures();
|
|
541
|
+
imageFailCache.set(src, {
|
|
542
|
+
failedAt: Date.now(),
|
|
543
|
+
attempts: (imageFailCache.get(src)?.attempts ?? 0) + 1,
|
|
544
|
+
});
|
|
545
|
+
evictMap(imageFailCache, MAX_FAIL_CACHE_SIZE);
|
|
546
|
+
};
|
|
547
|
+
img.src = src;
|
|
548
|
+
}
|
|
449
549
|
}
|
|
450
550
|
|
|
451
551
|
/**
|
|
@@ -246,10 +246,14 @@ function createEventHandler(
|
|
|
246
246
|
? options.extractPayload(event, element)
|
|
247
247
|
: extractEventData(event, element);
|
|
248
248
|
|
|
249
|
-
// Dispatch to engine
|
|
249
|
+
// Dispatch to engine, catching any async rejections
|
|
250
250
|
const engine = getEngine(element);
|
|
251
251
|
if (engine) {
|
|
252
|
-
|
|
252
|
+
try {
|
|
253
|
+
engine.dispatchAction(actionName, payload);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
log.error(`Error dispatching action "${actionName}":`, err);
|
|
256
|
+
}
|
|
253
257
|
}
|
|
254
258
|
};
|
|
255
259
|
|
|
@@ -558,6 +562,11 @@ export const eventHandlers: Record<string, ApplicatorHandler> = {
|
|
|
558
562
|
disposableListener(element, "input", listener, { passive: true })
|
|
559
563
|
);
|
|
560
564
|
} else {
|
|
565
|
+
log.warn(
|
|
566
|
+
`.bind() is not supported on element type "${element.dataset?.hypenType || element.tagName}". ` +
|
|
567
|
+
`Supported types: input, textarea, checkbox, switch, select.`
|
|
568
|
+
);
|
|
569
|
+
unregisterEvent(element, eventKey);
|
|
561
570
|
return;
|
|
562
571
|
}
|
|
563
572
|
|
|
@@ -8,14 +8,14 @@ export const textareaHandler: ComponentHandler = {
|
|
|
8
8
|
create(): HTMLElement {
|
|
9
9
|
const el = document.createElement("textarea");
|
|
10
10
|
el.dataset.hypenType = "textarea";
|
|
11
|
-
return el as
|
|
11
|
+
return el as HTMLElement;
|
|
12
12
|
},
|
|
13
13
|
|
|
14
14
|
applyProps(el: HTMLElement, props: Record<string, any>): void {
|
|
15
15
|
const textarea = el as HTMLTextAreaElement;
|
|
16
16
|
|
|
17
|
-
// Value property
|
|
18
|
-
const value = props["0"]
|
|
17
|
+
// Value property - check named prop first, then positional (consistent with Input)
|
|
18
|
+
const value = props.value ?? props["0"];
|
|
19
19
|
if (value !== undefined) {
|
|
20
20
|
textarea.value = String(value);
|
|
21
21
|
}
|
package/src/dom/events.ts
CHANGED
|
@@ -60,13 +60,20 @@ export class EventManager {
|
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
// Store listener reference
|
|
63
|
+
// Store listener reference using `domEventName` so `detach()` uses the correct name
|
|
64
64
|
let elementBindings = this.bindings.get(elementId);
|
|
65
65
|
if (!elementBindings) {
|
|
66
66
|
elementBindings = new Map();
|
|
67
67
|
this.bindings.set(elementId, elementBindings);
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
// Remove any previously attached listener for this event before overwriting,
|
|
71
|
+
// otherwise the old DOM listener leaks and fires alongside the new one.
|
|
72
|
+
const existingListener = elementBindings.get(domEventName);
|
|
73
|
+
if (existingListener) {
|
|
74
|
+
element.removeEventListener(domEventName, existingListener);
|
|
75
|
+
}
|
|
76
|
+
elementBindings.set(domEventName, listener);
|
|
70
77
|
|
|
71
78
|
// Attach to DOM
|
|
72
79
|
element.addEventListener(domEventName, listener);
|
|
@@ -77,26 +84,21 @@ export class EventManager {
|
|
|
77
84
|
dataset: element.dataset,
|
|
78
85
|
textContent: element.textContent?.substring(0, 50)
|
|
79
86
|
});
|
|
80
|
-
|
|
81
|
-
// Test: Add a simple click listener to verify DOM events work
|
|
82
|
-
if (domEventName === "click") {
|
|
83
|
-
element.addEventListener("click", (e) => {
|
|
84
|
-
log.debug(`Raw DOM click detected on ${element.tagName}`, e);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* Detach an event listener from an element
|
|
91
91
|
*/
|
|
92
92
|
detach(elementId: string, element: HTMLElement, eventName: string): void {
|
|
93
|
+
// Convert framework event name to DOM event name, matching `attach()`
|
|
94
|
+
const domEventName = eventName === "onClick" ? "click" : eventName;
|
|
93
95
|
const elementBindings = this.bindings.get(elementId);
|
|
94
96
|
if (!elementBindings) return;
|
|
95
97
|
|
|
96
|
-
const listener = elementBindings.get(
|
|
98
|
+
const listener = elementBindings.get(domEventName);
|
|
97
99
|
if (listener) {
|
|
98
|
-
element.removeEventListener(
|
|
99
|
-
elementBindings.delete(
|
|
100
|
+
element.removeEventListener(domEventName, listener);
|
|
101
|
+
elementBindings.delete(domEventName);
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
if (elementBindings.size === 0) {
|
package/src/dom/renderer.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { ComponentRegistry } from "./components/index.js";
|
|
|
15
15
|
import { ApplicatorRegistry } from "./applicators/index.js";
|
|
16
16
|
import { canvasHandler, canvasApplicators } from "./canvas/index.js";
|
|
17
17
|
import { RerenderTracker, type DebugConfig, defaultDebugConfig } from "./debug.js";
|
|
18
|
-
import { setEngine } from "./element-data.js";
|
|
18
|
+
import { setEngine, disposeHypenElement } from "./element-data.js";
|
|
19
19
|
|
|
20
20
|
// Interface for the engine that renderer needs
|
|
21
21
|
interface IEngine {
|
|
@@ -311,6 +311,9 @@ export class DOMRenderer {
|
|
|
311
311
|
const element = this.nodes.get(id);
|
|
312
312
|
if (!element) return;
|
|
313
313
|
|
|
314
|
+
// Dispose event listeners and other resources before removing from DOM
|
|
315
|
+
disposeHypenElement(element);
|
|
316
|
+
|
|
314
317
|
if (element.parentNode) {
|
|
315
318
|
element.parentNode.removeChild(element);
|
|
316
319
|
}
|
|
@@ -333,6 +336,10 @@ export class DOMRenderer {
|
|
|
333
336
|
* Clear all nodes
|
|
334
337
|
*/
|
|
335
338
|
clear(): void {
|
|
339
|
+
// Dispose all element resources before clearing
|
|
340
|
+
for (const element of this.nodes.values()) {
|
|
341
|
+
disposeHypenElement(element);
|
|
342
|
+
}
|
|
336
343
|
this.container.innerHTML = "";
|
|
337
344
|
this.nodes.clear();
|
|
338
345
|
this.rootId = null;
|