canvasengine 2.0.0-beta.45 → 2.0.0-beta.46

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.
Files changed (79) hide show
  1. package/dist/components/Container.d.ts +86 -0
  2. package/dist/components/Container.d.ts.map +1 -0
  3. package/dist/components/DOMContainer.d.ts +98 -0
  4. package/dist/components/DOMContainer.d.ts.map +1 -0
  5. package/dist/components/DOMElement.d.ts +16 -5
  6. package/dist/components/DOMElement.d.ts.map +1 -1
  7. package/dist/components/DOMSprite.d.ts +108 -0
  8. package/dist/components/DOMSprite.d.ts.map +1 -0
  9. package/dist/components/DisplayObject.d.ts +94 -0
  10. package/dist/components/DisplayObject.d.ts.map +1 -0
  11. package/dist/components/FocusContainer.d.ts +129 -0
  12. package/dist/components/FocusContainer.d.ts.map +1 -0
  13. package/dist/components/Mesh.d.ts +208 -0
  14. package/dist/components/Mesh.d.ts.map +1 -0
  15. package/dist/components/Sprite.d.ts +242 -0
  16. package/dist/components/Sprite.d.ts.map +1 -0
  17. package/dist/components/Viewport.d.ts +121 -0
  18. package/dist/components/Viewport.d.ts.map +1 -0
  19. package/dist/components/index.d.ts +2 -1
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/directives/Controls.d.ts +4 -4
  22. package/dist/directives/Controls.d.ts.map +1 -1
  23. package/dist/directives/ControlsBase.d.ts +1 -0
  24. package/dist/directives/ControlsBase.d.ts.map +1 -1
  25. package/dist/directives/FocusNavigation.d.ts +4 -22
  26. package/dist/directives/FocusNavigation.d.ts.map +1 -1
  27. package/dist/directives/KeyboardControls.d.ts +1 -0
  28. package/dist/directives/KeyboardControls.d.ts.map +1 -1
  29. package/dist/directives/Scheduler.d.ts.map +1 -1
  30. package/dist/directives/Shake.d.ts +1 -0
  31. package/dist/directives/Shake.d.ts.map +1 -1
  32. package/dist/engine/FocusManager.d.ts +10 -9
  33. package/dist/engine/FocusManager.d.ts.map +1 -1
  34. package/dist/engine/bootstrap.d.ts +1 -0
  35. package/dist/engine/bootstrap.d.ts.map +1 -1
  36. package/dist/engine/directive.d.ts +1 -1
  37. package/dist/engine/directive.d.ts.map +1 -1
  38. package/dist/engine/reactive.d.ts.map +1 -1
  39. package/dist/hooks/useFocus.d.ts.map +1 -1
  40. package/dist/index-DaGekQUW.js +2218 -0
  41. package/dist/index-DaGekQUW.js.map +1 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.global.js +3 -3
  45. package/dist/index.global.js.map +1 -1
  46. package/dist/index.js +11868 -88
  47. package/dist/index.js.map +1 -1
  48. package/dist/utils/tabindex.d.ts +16 -0
  49. package/dist/utils/tabindex.d.ts.map +1 -0
  50. package/package.json +1 -1
  51. package/src/components/DOMContainer.ts +186 -1
  52. package/src/components/DOMElement.ts +164 -37
  53. package/src/components/DOMSprite.ts +759 -0
  54. package/src/components/DisplayObject.ts +33 -7
  55. package/src/components/FocusContainer.ts +22 -26
  56. package/src/components/Sprite.ts +12 -3
  57. package/src/components/Text.ts +1 -1
  58. package/src/components/Viewport.ts +5 -5
  59. package/src/components/index.ts +2 -1
  60. package/src/directives/Controls.ts +5 -5
  61. package/src/directives/ControlsBase.ts +1 -0
  62. package/src/directives/FocusNavigation.ts +8 -146
  63. package/src/directives/KeyboardControls.ts +11 -2
  64. package/src/directives/Scheduler.ts +12 -4
  65. package/src/directives/Shake.ts +9 -6
  66. package/src/engine/FocusManager.ts +44 -29
  67. package/src/engine/bootstrap.ts +5 -2
  68. package/src/engine/directive.ts +2 -2
  69. package/src/engine/reactive.ts +84 -12
  70. package/src/hooks/useFocus.ts +2 -5
  71. package/src/index.ts +2 -1
  72. package/src/types/pixi-cull.d.ts +7 -0
  73. package/src/utils/tabindex.ts +70 -0
  74. package/testing/index.ts +31 -3
  75. package/tsconfig.json +3 -2
  76. package/dist/DebugRenderer-CSxse9YI.js +0 -172
  77. package/dist/DebugRenderer-CSxse9YI.js.map +0 -1
  78. package/dist/index-DH2ZMhYm.js +0 -13276
  79. package/dist/index-DH2ZMhYm.js.map +0 -1
@@ -0,0 +1,16 @@
1
+ import { WritableSignal } from '@signe/reactive';
2
+ export type TabindexBoundaryMode = "wrap" | "clamp" | "none";
3
+ export type TabindexBounds = {
4
+ count: () => number;
5
+ min?: number;
6
+ } | {
7
+ min: number;
8
+ max: number;
9
+ };
10
+ type TabindexNavigator = {
11
+ next: (delta: number) => void;
12
+ set: (value: number) => void;
13
+ };
14
+ export declare function createTabindexNavigator(tabindex: WritableSignal<number>, bounds: TabindexBounds, mode?: TabindexBoundaryMode): TabindexNavigator;
15
+ export {};
16
+ //# sourceMappingURL=tabindex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabindex.d.ts","sourceRoot":"","sources":["../../src/utils/tabindex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7D,MAAM,MAAM,cAAc,GACtB;IAAE,KAAK,EAAE,MAAM,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B,CAAC;AAuCF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC,EAChC,MAAM,EAAE,cAAc,EACtB,IAAI,GAAE,oBAA6B,GAClC,iBAAiB,CAenB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasengine",
3
- "version": "2.0.0-beta.45",
3
+ "version": "2.0.0-beta.46",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,13 +1,16 @@
1
1
  import { DOMContainer as PixiDOMContainer } from "pixi.js";
2
+ import { effect } from "@signe/reactive";
2
3
  import {
3
4
  createComponent,
4
5
  Element,
6
+ isElement,
5
7
  registerComponent,
6
8
  } from "../engine/reactive";
7
9
  import { ComponentInstance, DisplayObject } from "./DisplayObject";
8
10
  import { ComponentFunction, h } from "../engine/signal";
9
11
  import { DisplayObjectProps } from "./types/DisplayObject";
10
12
  import { CanvasDOMElement, DOMElement } from "./DOMElement";
13
+ import { isPercent } from "../utils/functions";
11
14
 
12
15
 
13
16
  /**
@@ -107,6 +110,159 @@ const EVENTS = [
107
110
 
108
111
  export class CanvasDOMContainer extends DisplayObject(PixiDOMContainer) {
109
112
  disableLayout = true;
113
+ private canvasSizeEffect: any = null;
114
+ private static readonly DOM_ROUTING_MAP: Record<string, string> = {
115
+ Sprite: "DOMSprite",
116
+ };
117
+ private static readonly DOM_ALLOWED_TAGS = new Set([
118
+ "DOMContainer",
119
+ "DOMElement",
120
+ "DOMSprite",
121
+ ]);
122
+ private static readonly DOM_UNSUPPORTED_TAGS = new Set([
123
+ "Canvas",
124
+ "Container",
125
+ "Graphics",
126
+ "Rect",
127
+ "Circle",
128
+ "Ellipse",
129
+ "Triangle",
130
+ "Svg",
131
+ "Mesh",
132
+ "Scene",
133
+ "ParticlesEmitter",
134
+ "Sprite",
135
+ "Video",
136
+ "Text",
137
+ "TilingSprite",
138
+ "Viewport",
139
+ "NineSliceSprite",
140
+ "Button",
141
+ "Joystick",
142
+ "FocusContainer",
143
+ ]);
144
+
145
+ private hasDomContainerAncestor(): boolean {
146
+ const element = this.getElement();
147
+ let parent = element?.parent;
148
+ while (parent) {
149
+ if (parent.tag === "DOMContainer") return true;
150
+ parent = parent.parent;
151
+ }
152
+ return false;
153
+ }
154
+
155
+ private getPercentRatio(value: string): number | null {
156
+ const parsed = parseFloat(value);
157
+ if (Number.isNaN(parsed)) return null;
158
+ return parsed / 100;
159
+ }
160
+
161
+ private getCanvasSize() {
162
+ const canvasSize = this.fullProps?.context?.canvasSize;
163
+ return typeof canvasSize === "function" ? canvasSize() : canvasSize;
164
+ }
165
+
166
+ private shouldUseCanvasPercent(): boolean {
167
+ const widthProp = this.fullProps?.width;
168
+ const heightProp = this.fullProps?.height;
169
+ if (!isPercent(widthProp) && !isPercent(heightProp)) return false;
170
+ return !this.hasDomContainerAncestor();
171
+ }
172
+
173
+ private syncCanvasSizeEffect() {
174
+ const shouldTrack = this.shouldUseCanvasPercent();
175
+ if (shouldTrack && !this.canvasSizeEffect) {
176
+ const canvasSize = this.fullProps?.context?.canvasSize;
177
+ if (typeof canvasSize === "function") {
178
+ this.canvasSizeEffect = effect(() => {
179
+ canvasSize();
180
+ this.applyElementSize();
181
+ });
182
+ }
183
+ } else if (!shouldTrack && this.canvasSizeEffect) {
184
+ this.canvasSizeEffect.subscription?.unsubscribe();
185
+ this.canvasSizeEffect = null;
186
+ }
187
+ }
188
+
189
+ private applyElementSize() {
190
+ if (!this.element) return;
191
+ const widthProp = this.fullProps?.width;
192
+ const heightProp = this.fullProps?.height;
193
+ const useCanvasSize = this.shouldUseCanvasPercent();
194
+ const canvasSize = useCanvasSize ? this.getCanvasSize() : null;
195
+
196
+ if (widthProp !== undefined) {
197
+ if (isPercent(widthProp)) {
198
+ if (useCanvasSize) {
199
+ const ratio = this.getPercentRatio(widthProp);
200
+ if (ratio !== null) {
201
+ const baseWidth = (canvasSize?.width !== undefined)
202
+ ? canvasSize.width
203
+ : this.getWidth();
204
+ this.element.style.width = `${baseWidth * ratio}px`;
205
+ }
206
+ } else {
207
+ this.element.style.width = widthProp;
208
+ }
209
+ } else if (typeof widthProp === "number") {
210
+ this.element.style.width = `${widthProp}px`;
211
+ } else if (typeof widthProp === "string") {
212
+ this.element.style.width = widthProp;
213
+ }
214
+ }
215
+
216
+ if (heightProp !== undefined) {
217
+ if (isPercent(heightProp)) {
218
+ if (useCanvasSize) {
219
+ const ratio = this.getPercentRatio(heightProp);
220
+ if (ratio !== null) {
221
+ const baseHeight = (canvasSize?.height !== undefined)
222
+ ? canvasSize.height
223
+ : this.getHeight();
224
+ this.element.style.height = `${baseHeight * ratio}px`;
225
+ }
226
+ } else {
227
+ this.element.style.height = heightProp;
228
+ }
229
+ } else if (typeof heightProp === "number") {
230
+ this.element.style.height = `${heightProp}px`;
231
+ } else if (typeof heightProp === "string") {
232
+ this.element.style.height = heightProp;
233
+ }
234
+ }
235
+ }
236
+
237
+ private routeDomChildren(children: any): any {
238
+ if (!children) return children;
239
+ if (Array.isArray(children)) {
240
+ return children.map((child) => this.routeDomChildren(child));
241
+ }
242
+ if (isElement(children)) {
243
+ if (CanvasDOMContainer.DOM_ALLOWED_TAGS.has(children.tag)) {
244
+ return children;
245
+ }
246
+ const routedTag = CanvasDOMContainer.DOM_ROUTING_MAP[children.tag];
247
+ if (routedTag) {
248
+ children.propSubscriptions?.forEach((sub) => sub.unsubscribe());
249
+ children.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
250
+ children.effectUnmounts?.forEach((fn) => fn?.());
251
+ const routedProps = children.propObservables ?? children.props;
252
+ return createComponent(routedTag, routedProps);
253
+ }
254
+ if (CanvasDOMContainer.DOM_UNSUPPORTED_TAGS.has(children.tag)) {
255
+ throw new Error(
256
+ `Component ${children.tag} is not implemented for DOMContainer context yet. Only Sprite is supported.`
257
+ );
258
+ }
259
+ if (children.props?.children) {
260
+ children.props.children = this.routeDomChildren(children.props.children);
261
+ }
262
+ return children;
263
+ }
264
+ return children;
265
+ }
110
266
 
111
267
  onInit(props: any) {
112
268
  // Handle internal _scopeClass prop for scoped CSS
@@ -134,9 +290,38 @@ export class CanvasDOMContainer extends DisplayObject(PixiDOMContainer) {
134
290
  divProps.attrs = props.attrs;
135
291
  }
136
292
 
137
- const div = h(DOMElement, divProps, props.children) as unknown as Element<CanvasDOMElement>;
293
+ const routedChildren = this.routeDomChildren(props.children);
294
+ props.children = routedChildren;
295
+ const div = h(DOMElement, divProps, routedChildren) as unknown as Element<CanvasDOMElement>;
138
296
  this.element = div.componentInstance.element;
139
297
  }
298
+
299
+ async onMount(element: Element<any>, index?: number) {
300
+ await super.onMount(element, index);
301
+ this.syncCanvasSizeEffect();
302
+ this.applyElementSize();
303
+ }
304
+
305
+ onUpdate(props: any) {
306
+ super.onUpdate(props);
307
+ this.syncCanvasSizeEffect();
308
+ this.applyElementSize();
309
+ }
310
+
311
+ onLayoutComputed() {
312
+ this.applyElementSize();
313
+ }
314
+
315
+ async onDestroy(parent: Element<any>, afterDestroy?: () => void) {
316
+ const _afterDestroy = () => {
317
+ if (this.canvasSizeEffect) {
318
+ this.canvasSizeEffect.subscription?.unsubscribe();
319
+ this.canvasSizeEffect = null;
320
+ }
321
+ if (afterDestroy) afterDestroy();
322
+ };
323
+ await super.onDestroy(parent, _afterDestroy);
324
+ }
140
325
  }
141
326
 
142
327
  export interface CanvasDOMContainer extends DisplayObjectProps { }
@@ -10,8 +10,8 @@ import { DisplayObjectProps } from "./types/DisplayObject";
10
10
  import { isObservable } from "../engine/utils";
11
11
  import { isSignal } from "@signe/reactive";
12
12
 
13
- interface DOMContainerProps extends DisplayObjectProps {
14
- element:
13
+ export interface DOMElementProps extends DisplayObjectProps {
14
+ element?:
15
15
  | string
16
16
  | {
17
17
  value: HTMLElement;
@@ -32,6 +32,14 @@ interface DOMContainerProps extends DisplayObjectProps {
32
32
  onBeforeDestroy?: OnHook;
33
33
  }
34
34
 
35
+ export interface DOMContainerProps extends DOMElementProps {
36
+ element:
37
+ | string
38
+ | {
39
+ value: HTMLElement;
40
+ };
41
+ }
42
+
35
43
  /**
36
44
  * DOMContainer class for managing DOM elements within the canvas engine
37
45
  *
@@ -204,6 +212,8 @@ export class CanvasDOMElement {
204
212
  private onBeforeDestroy: OnHook | null = null;
205
213
  private valueSignal: any = null;
206
214
  private isFormElementType: boolean = false;
215
+ private classSubscriptions: Array<{ unsubscribe: () => void }> = [];
216
+ private childTextSubscriptions: Array<{ unsubscribe: () => void }> = [];
207
217
 
208
218
  /**
209
219
  * Checks if the element is a form element that supports the value attribute
@@ -215,12 +225,145 @@ export class CanvasDOMElement {
215
225
  return formElements.includes(elementType.toLowerCase());
216
226
  }
217
227
 
218
- onInit(props: DOMContainerProps) {
228
+ private collectClassTokens(value: any, tokens: string[]) {
229
+ if (!value) return;
230
+ if (isSignal(value)) {
231
+ this.collectClassTokens(value(), tokens);
232
+ return;
233
+ }
234
+ if (typeof value === "string") {
235
+ value.split(/\s+/).filter(Boolean).forEach((token) => tokens.push(token));
236
+ return;
237
+ }
238
+ if (Array.isArray(value)) {
239
+ value.forEach((item) => this.collectClassTokens(item, tokens));
240
+ return;
241
+ }
242
+ if (typeof value === "object") {
243
+ for (const [className, shouldAdd] of Object.entries(value)) {
244
+ const resolved = isSignal(shouldAdd as any) ? (shouldAdd as any)() : shouldAdd;
245
+ if (resolved) {
246
+ tokens.push(className);
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ private collectClassSignals(value: any, signals: Set<any>) {
253
+ if (!value) return;
254
+ if (isSignal(value)) {
255
+ signals.add(value);
256
+ return;
257
+ }
258
+ if (Array.isArray(value)) {
259
+ value.forEach((item) => this.collectClassSignals(item, signals));
260
+ return;
261
+ }
262
+ if (typeof value === "object") {
263
+ Object.values(value).forEach((item) =>
264
+ this.collectClassSignals(item, signals)
265
+ );
266
+ }
267
+ }
268
+
269
+ private applyClassList(classList: any) {
270
+ // Clear existing classes first
271
+ this.element.className = "";
272
+
273
+ if (typeof classList === "string") {
274
+ // String: space-separated class names
275
+ this.element.className = classList;
276
+ return;
277
+ }
278
+
279
+ const tokens: string[] = [];
280
+ this.collectClassTokens(classList, tokens);
281
+ if (tokens.length > 0) {
282
+ this.element.classList.add(...tokens);
283
+ }
284
+ }
285
+
286
+ private syncClassSubscriptions(classList: any) {
287
+ for (const sub of this.classSubscriptions) {
288
+ sub.unsubscribe();
289
+ }
290
+ this.classSubscriptions = [];
291
+
292
+ const signals = new Set<any>();
293
+ this.collectClassSignals(classList, signals);
294
+ if (signals.size === 0) return;
295
+
296
+ signals.forEach((signal) => {
297
+ if (!signal?.observable?.subscribe) return;
298
+ const sub = signal.observable.subscribe(() => {
299
+ this.applyClassList(classList);
300
+ });
301
+ this.classSubscriptions.push(sub);
302
+ });
303
+ }
304
+
305
+ private appendChildElement(child: any) {
306
+ if (child === null || child === undefined || child === false) return;
307
+
308
+ if (typeof child === "string" || typeof child === "number") {
309
+ this.element.appendChild(document.createTextNode(String(child)));
310
+ return;
311
+ }
312
+
313
+ if (isSignal(child)) {
314
+ const textNode = document.createTextNode(
315
+ child() == null ? "" : String(child())
316
+ );
317
+ this.element.appendChild(textNode);
318
+ if (child.observable?.subscribe) {
319
+ const sub = child.observable.subscribe((value: any) => {
320
+ textNode.textContent = value == null ? "" : String(value);
321
+ });
322
+ this.childTextSubscriptions.push(sub);
323
+ }
324
+ return;
325
+ }
326
+
327
+ if (isObservable(child)) {
328
+ child.subscribe((value: any) => {
329
+ if (value && typeof value === "object" && "elements" in value) {
330
+ const elements = value.elements || [];
331
+ elements.forEach((element: any) => this.appendChildElement(element));
332
+ } else if (Array.isArray(value)) {
333
+ value.forEach((element) => this.appendChildElement(element));
334
+ } else {
335
+ this.appendChildElement(value);
336
+ }
337
+ });
338
+ return;
339
+ }
340
+
341
+ if (Array.isArray(child)) {
342
+ child.forEach((item) => this.appendChildElement(item));
343
+ return;
344
+ }
345
+
346
+ const childElement = child?.componentInstance?.element;
347
+ if (childElement) {
348
+ this.element.appendChild(childElement);
349
+ return;
350
+ }
351
+
352
+ const nestedChildren = child?.props?.children;
353
+ if (nestedChildren) {
354
+ this.appendChildElement(nestedChildren);
355
+ }
356
+ }
357
+
358
+ onInit(props: DOMElementProps) {
219
359
  if (typeof props.element === "string") {
220
360
  this.element = document.createElement(props.element);
221
361
  this.isFormElementType = this.isFormElement(props.element);
222
362
  } else {
223
- this.element = props.element.value;
363
+ this.element = props.element?.value;
364
+ if (!this.element) {
365
+ throw new Error("DOMElement requires a valid element.");
366
+ }
224
367
  this.isFormElementType = this.isFormElement(this.element.tagName);
225
368
  }
226
369
  if (props.onBeforeDestroy || props["on-before-destroy"]) {
@@ -264,17 +407,7 @@ export class CanvasDOMElement {
264
407
  }
265
408
  }
266
409
  if (props.children) {
267
- for (const child of props.children) {
268
- if (isObservable(child)) {
269
- child.subscribe(({ elements }) => {
270
- for (const element of elements) {
271
- this.element.appendChild(element.componentInstance.element);
272
- }
273
- });
274
- } else {
275
- this.element.appendChild(child.componentInstance.element);
276
- }
277
- }
410
+ this.appendChildElement(props.children);
278
411
  }
279
412
  this.onUpdate(props);
280
413
  }
@@ -311,7 +444,7 @@ export class CanvasDOMElement {
311
444
  }
312
445
  }
313
446
 
314
- onUpdate(props: DOMContainerProps) {
447
+ onUpdate(props: DOMElementProps) {
315
448
  if (!this.element) return;
316
449
  for (const [key, value] of Object.entries(props.attrs || {})) {
317
450
  if (key === "tabindex") {
@@ -323,25 +456,10 @@ export class CanvasDOMElement {
323
456
  this.element.removeAttribute('tabindex');
324
457
  }
325
458
  } else if (key === "class") {
326
- const classList = value.items || value.value || value;
327
-
328
- // Clear existing classes first
329
- this.element.className = "";
330
-
331
- if (typeof classList === "string") {
332
- // String: space-separated class names
333
- this.element.className = classList;
334
- } else if (Array.isArray(classList)) {
335
- // Array: array of class names
336
- this.element.classList.add(...classList);
337
- } else if (typeof classList === "object" && classList !== null) {
338
- // Object: { className: boolean }
339
- for (const [className, shouldAdd] of Object.entries(classList)) {
340
- if (shouldAdd) {
341
- this.element.classList.add(className);
342
- }
343
- }
344
- }
459
+ const rawClassList = value.items || value.value || value;
460
+ const classList = isSignal(rawClassList) ? rawClassList() : rawClassList;
461
+ this.applyClassList(classList);
462
+ this.syncClassSubscriptions(classList);
345
463
  } else if (key === "style") {
346
464
  const styleValue = value.items || value.value || value;
347
465
 
@@ -389,8 +507,9 @@ export class CanvasDOMElement {
389
507
  this.element.setAttribute(key, value);
390
508
  }
391
509
  }
392
- if (props.textContent) {
393
- this.element.textContent = props.textContent;
510
+ if ("textContent" in props) {
511
+ const textContent = props.textContent;
512
+ this.element.textContent = textContent == null ? "" : String(textContent);
394
513
  }
395
514
  }
396
515
 
@@ -410,6 +529,14 @@ export class CanvasDOMElement {
410
529
  }
411
530
 
412
531
  this.eventListeners.clear();
532
+ for (const sub of this.classSubscriptions) {
533
+ sub.unsubscribe();
534
+ }
535
+ this.classSubscriptions = [];
536
+ for (const sub of this.childTextSubscriptions) {
537
+ sub.unsubscribe();
538
+ }
539
+ this.childTextSubscriptions = [];
413
540
 
414
541
  this.element.remove();
415
542