canvasengine 2.0.0-beta.5 → 2.0.0-beta.51

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 (172) hide show
  1. package/dist/components/Button.d.ts +185 -0
  2. package/dist/components/Button.d.ts.map +1 -0
  3. package/dist/components/Canvas.d.ts +17 -0
  4. package/dist/components/Canvas.d.ts.map +1 -0
  5. package/dist/components/Container.d.ts +86 -0
  6. package/dist/components/Container.d.ts.map +1 -0
  7. package/dist/components/DOMContainer.d.ts +98 -0
  8. package/dist/components/DOMContainer.d.ts.map +1 -0
  9. package/dist/components/DOMElement.d.ts +54 -0
  10. package/dist/components/DOMElement.d.ts.map +1 -0
  11. package/dist/components/DOMSprite.d.ts +127 -0
  12. package/dist/components/DOMSprite.d.ts.map +1 -0
  13. package/dist/components/DisplayObject.d.ts +94 -0
  14. package/dist/components/DisplayObject.d.ts.map +1 -0
  15. package/dist/components/FocusContainer.d.ts +129 -0
  16. package/dist/components/FocusContainer.d.ts.map +1 -0
  17. package/dist/components/Graphic.d.ts +64 -0
  18. package/dist/components/Graphic.d.ts.map +1 -0
  19. package/dist/components/Joystick.d.ts +36 -0
  20. package/dist/components/Joystick.d.ts.map +1 -0
  21. package/dist/components/Mesh.d.ts +208 -0
  22. package/dist/components/Mesh.d.ts.map +1 -0
  23. package/dist/components/NineSliceSprite.d.ts +16 -0
  24. package/dist/components/NineSliceSprite.d.ts.map +1 -0
  25. package/dist/components/ParticleEmitter.d.ts +4 -0
  26. package/dist/components/ParticleEmitter.d.ts.map +1 -0
  27. package/dist/components/Scene.d.ts +2 -0
  28. package/dist/components/Scene.d.ts.map +1 -0
  29. package/dist/components/Sprite.d.ts +242 -0
  30. package/dist/components/Sprite.d.ts.map +1 -0
  31. package/dist/components/Text.d.ts +25 -0
  32. package/dist/components/Text.d.ts.map +1 -0
  33. package/dist/components/TilingSprite.d.ts +17 -0
  34. package/dist/components/TilingSprite.d.ts.map +1 -0
  35. package/dist/components/Video.d.ts +14 -0
  36. package/dist/components/Video.d.ts.map +1 -0
  37. package/dist/components/Viewport.d.ts +121 -0
  38. package/dist/components/Viewport.d.ts.map +1 -0
  39. package/dist/components/index.d.ts +20 -0
  40. package/dist/components/index.d.ts.map +1 -0
  41. package/dist/components/types/DisplayObject.d.ts +106 -0
  42. package/dist/components/types/DisplayObject.d.ts.map +1 -0
  43. package/dist/components/types/MouseEvent.d.ts +4 -0
  44. package/dist/components/types/MouseEvent.d.ts.map +1 -0
  45. package/dist/components/types/Spritesheet.d.ts +248 -0
  46. package/dist/components/types/Spritesheet.d.ts.map +1 -0
  47. package/dist/components/types/index.d.ts +4 -0
  48. package/dist/components/types/index.d.ts.map +1 -0
  49. package/dist/directives/Controls.d.ts +112 -0
  50. package/dist/directives/Controls.d.ts.map +1 -0
  51. package/dist/directives/ControlsBase.d.ts +199 -0
  52. package/dist/directives/ControlsBase.d.ts.map +1 -0
  53. package/dist/directives/Drag.d.ts +69 -0
  54. package/dist/directives/Drag.d.ts.map +1 -0
  55. package/dist/directives/Flash.d.ts +116 -0
  56. package/dist/directives/Flash.d.ts.map +1 -0
  57. package/dist/directives/FocusNavigation.d.ts +52 -0
  58. package/dist/directives/FocusNavigation.d.ts.map +1 -0
  59. package/dist/directives/GamepadControls.d.ts +224 -0
  60. package/dist/directives/GamepadControls.d.ts.map +1 -0
  61. package/dist/directives/JoystickControls.d.ts +171 -0
  62. package/dist/directives/JoystickControls.d.ts.map +1 -0
  63. package/dist/directives/KeyboardControls.d.ts +219 -0
  64. package/dist/directives/KeyboardControls.d.ts.map +1 -0
  65. package/dist/directives/Scheduler.d.ts +35 -0
  66. package/dist/directives/Scheduler.d.ts.map +1 -0
  67. package/dist/directives/Shake.d.ts +98 -0
  68. package/dist/directives/Shake.d.ts.map +1 -0
  69. package/dist/directives/Sound.d.ts +25 -0
  70. package/dist/directives/Sound.d.ts.map +1 -0
  71. package/dist/directives/Transition.d.ts +10 -0
  72. package/dist/directives/Transition.d.ts.map +1 -0
  73. package/dist/directives/ViewportCull.d.ts +11 -0
  74. package/dist/directives/ViewportCull.d.ts.map +1 -0
  75. package/dist/directives/ViewportFollow.d.ts +18 -0
  76. package/dist/directives/ViewportFollow.d.ts.map +1 -0
  77. package/dist/directives/index.d.ts +13 -0
  78. package/dist/directives/index.d.ts.map +1 -0
  79. package/dist/engine/FocusManager.d.ts +174 -0
  80. package/dist/engine/FocusManager.d.ts.map +1 -0
  81. package/dist/engine/animation.d.ts +72 -0
  82. package/dist/engine/animation.d.ts.map +1 -0
  83. package/dist/engine/bootstrap.d.ts +48 -0
  84. package/dist/engine/bootstrap.d.ts.map +1 -0
  85. package/dist/engine/directive.d.ts +13 -0
  86. package/dist/engine/directive.d.ts.map +1 -0
  87. package/dist/engine/reactive.d.ts +134 -0
  88. package/dist/engine/reactive.d.ts.map +1 -0
  89. package/dist/engine/signal.d.ts +71 -0
  90. package/dist/engine/signal.d.ts.map +1 -0
  91. package/dist/engine/trigger.d.ts +54 -0
  92. package/dist/engine/trigger.d.ts.map +1 -0
  93. package/dist/engine/utils.d.ts +89 -0
  94. package/dist/engine/utils.d.ts.map +1 -0
  95. package/dist/hooks/addContext.d.ts +2 -0
  96. package/dist/hooks/addContext.d.ts.map +1 -0
  97. package/dist/hooks/useFocus.d.ts +60 -0
  98. package/dist/hooks/useFocus.d.ts.map +1 -0
  99. package/dist/hooks/useProps.d.ts +42 -0
  100. package/dist/hooks/useProps.d.ts.map +1 -0
  101. package/dist/hooks/useRef.d.ts +4 -0
  102. package/dist/hooks/useRef.d.ts.map +1 -0
  103. package/dist/index-DaGekQUW.js +2218 -0
  104. package/dist/index-DaGekQUW.js.map +1 -0
  105. package/dist/index.d.ts +19 -1099
  106. package/dist/index.d.ts.map +1 -0
  107. package/dist/index.global.js +5 -0
  108. package/dist/index.global.js.map +1 -0
  109. package/dist/index.js +11749 -2901
  110. package/dist/index.js.map +1 -1
  111. package/dist/utils/Ease.d.ts +17 -0
  112. package/dist/utils/Ease.d.ts.map +1 -0
  113. package/dist/utils/GlobalAssetLoader.d.ts +141 -0
  114. package/dist/utils/GlobalAssetLoader.d.ts.map +1 -0
  115. package/dist/utils/RadialGradient.d.ts +57 -0
  116. package/dist/utils/RadialGradient.d.ts.map +1 -0
  117. package/dist/utils/functions.d.ts +2 -0
  118. package/dist/utils/functions.d.ts.map +1 -0
  119. package/dist/utils/tabindex.d.ts +16 -0
  120. package/dist/utils/tabindex.d.ts.map +1 -0
  121. package/package.json +13 -7
  122. package/src/components/Button.ts +399 -0
  123. package/src/components/Canvas.ts +62 -46
  124. package/src/components/Container.ts +21 -2
  125. package/src/components/DOMContainer.ts +379 -0
  126. package/src/components/DOMElement.ts +556 -0
  127. package/src/components/DOMSprite.ts +1040 -0
  128. package/src/components/DisplayObject.ts +392 -201
  129. package/src/components/FocusContainer.ts +368 -0
  130. package/src/components/Graphic.ts +227 -66
  131. package/src/components/Joystick.ts +363 -0
  132. package/src/components/Mesh.ts +222 -0
  133. package/src/components/NineSliceSprite.ts +4 -1
  134. package/src/components/ParticleEmitter.ts +12 -8
  135. package/src/components/Sprite.ts +297 -31
  136. package/src/components/Text.ts +125 -18
  137. package/src/components/Video.ts +2 -2
  138. package/src/components/Viewport.ts +118 -63
  139. package/src/components/index.ts +9 -2
  140. package/src/components/types/DisplayObject.ts +41 -4
  141. package/src/components/types/Spritesheet.ts +0 -118
  142. package/src/directives/Controls.ts +254 -0
  143. package/src/directives/ControlsBase.ts +267 -0
  144. package/src/directives/Drag.ts +357 -52
  145. package/src/directives/Flash.ts +419 -0
  146. package/src/directives/FocusNavigation.ts +113 -0
  147. package/src/directives/GamepadControls.ts +537 -0
  148. package/src/directives/JoystickControls.ts +396 -0
  149. package/src/directives/KeyboardControls.ts +85 -430
  150. package/src/directives/Scheduler.ts +12 -4
  151. package/src/directives/Shake.ts +298 -0
  152. package/src/directives/Sound.ts +94 -31
  153. package/src/directives/ViewportFollow.ts +40 -9
  154. package/src/directives/index.ts +12 -6
  155. package/src/engine/FocusManager.ts +510 -0
  156. package/src/engine/animation.ts +175 -21
  157. package/src/engine/bootstrap.ts +93 -3
  158. package/src/engine/directive.ts +4 -4
  159. package/src/engine/reactive.ts +901 -161
  160. package/src/engine/signal.ts +113 -25
  161. package/src/engine/trigger.ts +34 -7
  162. package/src/engine/utils.ts +19 -3
  163. package/src/hooks/useFocus.ts +91 -0
  164. package/src/hooks/useProps.ts +1 -1
  165. package/src/index.ts +8 -2
  166. package/src/types/pixi-cull.d.ts +7 -0
  167. package/src/utils/GlobalAssetLoader.ts +257 -0
  168. package/src/utils/functions.ts +7 -0
  169. package/src/utils/tabindex.ts +70 -0
  170. package/testing/index.ts +35 -4
  171. package/tsconfig.json +18 -0
  172. package/vite.config.ts +39 -0
@@ -0,0 +1,556 @@
1
+ import { DOMContainer as PixiDOMContainer } from "pixi.js";
2
+ import {
3
+ createComponent,
4
+ Element,
5
+ registerComponent,
6
+ } from "../engine/reactive";
7
+ import { ComponentInstance, DisplayObject, OnHook } from "./DisplayObject";
8
+ import { ComponentFunction } from "../engine/signal";
9
+ import { DisplayObjectProps } from "./types/DisplayObject";
10
+ import { isObservable } from "../engine/utils";
11
+ import { isSignal } from "@signe/reactive";
12
+
13
+ export interface DOMElementProps extends DisplayObjectProps {
14
+ element?:
15
+ | string
16
+ | {
17
+ value: HTMLElement;
18
+ };
19
+ textContent?: string;
20
+ attrs?: Record<string, any> & {
21
+ class?:
22
+ | string
23
+ | string[]
24
+ | Record<string, boolean>
25
+ | { items?: string[] }
26
+ | { value?: string | string[] | Record<string, boolean> };
27
+ style?:
28
+ | string
29
+ | Record<string, string | number>
30
+ | { value?: string | Record<string, string | number> };
31
+ };
32
+ onBeforeDestroy?: OnHook;
33
+ }
34
+
35
+ export interface DOMContainerProps extends DOMElementProps {
36
+ element:
37
+ | string
38
+ | {
39
+ value: HTMLElement;
40
+ };
41
+ }
42
+
43
+ /**
44
+ * DOMContainer class for managing DOM elements within the canvas engine
45
+ *
46
+ * This class extends the DisplayObject functionality to handle DOM elements using
47
+ * PixiJS's native DOMContainer. It provides a bridge between the canvas rendering
48
+ * system and traditional DOM manipulation with proper transform hierarchy and visibility.
49
+ *
50
+ * The DOMContainer is especially useful for rendering standard DOM elements that handle
51
+ * user input, such as `<input>` or `<textarea>`. This is often simpler and more flexible
52
+ * than trying to implement text input directly in PixiJS.
53
+ *
54
+ * ## Form Elements with Reactive Signals
55
+ *
56
+ * For form elements (`input`, `textarea`, `select`), the component supports reactive
57
+ * two-way data binding using signals. When the `value` attribute is a signal, the
58
+ * component automatically:
59
+ * - Sets the initial value from the signal
60
+ * - Listens for `input` events and updates the signal with the new value
61
+ * - Updates the DOM element when the signal value changes programmatically
62
+ *
63
+ * ## Form Submission Handling
64
+ *
65
+ * When a `form` element has a `submit` event handler, the component automatically:
66
+ * - Prevents the default form submission behavior (stops propagation)
67
+ * - Collects all form data from input elements within the form
68
+ * - Passes both the event and the collected form data as parameters to the submit handler
69
+ * - Handles multiple values for the same field name (e.g., checkboxes with same name)
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * import { signal } from '@signe/reactive';
74
+ *
75
+ * // Basic usage with input element
76
+ * const element = document.createElement('input');
77
+ * element.type = 'text';
78
+ * element.placeholder = 'Enter text...';
79
+ *
80
+ * const domContainer = new DOMContainer({
81
+ * element,
82
+ * x: 100,
83
+ * y: 50,
84
+ * anchor: { x: 0.5, y: 0.5 }
85
+ * });
86
+ *
87
+ * // Reactive form input with signal
88
+ * const inputValue = signal('');
89
+ *
90
+ * const reactiveInput = new DOMContainer({
91
+ * element: 'input',
92
+ * attrs: {
93
+ * type: 'text',
94
+ * placeholder: 'Type something...',
95
+ * value: inputValue // Signal for two-way binding
96
+ * }
97
+ * });
98
+ *
99
+ * // The signal will automatically update when user types
100
+ * inputValue.subscribe(value => {
101
+ * console.log('Input value changed:', value);
102
+ * });
103
+ *
104
+ * // You can also update the input programmatically
105
+ * inputValue.set('New value');
106
+ *
107
+ * // Form submission with automatic data collection
108
+ * const loginForm = new DOMContainer({
109
+ * element: 'form',
110
+ * attrs: {
111
+ * submit: (event, formData) => {
112
+ * // event: the submit event (already prevented)
113
+ * // formData: object containing all form field values
114
+ * console.log('Form submitted with data:', formData);
115
+ * // Example formData: { username: 'john', password: 'secret', remember: 'on' }
116
+ * }
117
+ * },
118
+ * children: [
119
+ * new DOMContainer({
120
+ * element: 'input',
121
+ * attrs: { name: 'username', type: 'text', placeholder: 'Username' }
122
+ * }),
123
+ * new DOMContainer({
124
+ * element: 'input',
125
+ * attrs: { name: 'password', type: 'password', placeholder: 'Password' }
126
+ * }),
127
+ * new DOMContainer({
128
+ * element: 'input',
129
+ * attrs: { name: 'remember', type: 'checkbox', value: 'on' }
130
+ * }),
131
+ * new DOMContainer({
132
+ * element: 'button',
133
+ * attrs: { type: 'submit' },
134
+ * textContent: 'Login'
135
+ * })
136
+ * ]
137
+ * });
138
+ *
139
+ * // Using different class and style formats
140
+ * const containerWithClasses = new DOMContainer({
141
+ * element: 'div',
142
+ * attrs: {
143
+ * // String format: space-separated classes
144
+ * class: 'container primary-theme',
145
+ *
146
+ * // Array format: array of class names
147
+ * // class: ['container', 'primary-theme'],
148
+ *
149
+ * // Object format: conditional classes
150
+ * // class: {
151
+ * // 'container': true,
152
+ * // 'primary-theme': true,
153
+ * // 'disabled': false
154
+ * // }
155
+ *
156
+ * // String format: CSS style string
157
+ * style: 'background-color: red; padding: 10px;',
158
+ *
159
+ * // Object format: style properties
160
+ * // style: {
161
+ * // backgroundColor: 'red',
162
+ * // padding: '10px',
163
+ * // fontSize: 16
164
+ * // }
165
+ * }
166
+ * });
167
+ * ```
168
+ */
169
+ const EVENTS = [
170
+ "click",
171
+ "mouseover",
172
+ "mouseout",
173
+ "mouseenter",
174
+ "mouseleave",
175
+ "mousemove",
176
+ "mouseup",
177
+ "mousedown",
178
+ "touchstart",
179
+ "touchend",
180
+ "touchmove",
181
+ "touchcancel",
182
+ "wheel",
183
+ "scroll",
184
+ "resize",
185
+ "focus",
186
+ "blur",
187
+ "change",
188
+ "input",
189
+ "submit",
190
+ "reset",
191
+ "keydown",
192
+ "keyup",
193
+ "keypress",
194
+ "contextmenu",
195
+ "drag",
196
+ "dragend",
197
+ "dragenter",
198
+ "dragleave",
199
+ "dragover",
200
+ "drop",
201
+ "dragstart",
202
+ "select",
203
+ "selectstart",
204
+ "selectend",
205
+ "selectall",
206
+ "selectnone",
207
+ ];
208
+
209
+ export class CanvasDOMElement {
210
+ public element: HTMLElement;
211
+ private eventListeners: Map<string, (e: Event) => void> = new Map();
212
+ private onBeforeDestroy: OnHook | null = null;
213
+ private valueSignal: any = null;
214
+ private isFormElementType: boolean = false;
215
+ private classSubscriptions: Array<{ unsubscribe: () => void }> = [];
216
+ private childTextSubscriptions: Array<{ unsubscribe: () => void }> = [];
217
+
218
+ /**
219
+ * Checks if the element is a form element that supports the value attribute
220
+ * @param elementType - The element type string from props
221
+ * @returns true if the element is a form element with value support
222
+ */
223
+ private isFormElement(elementType: string): boolean {
224
+ const formElements = ["input", "textarea", "select"];
225
+ return formElements.includes(elementType.toLowerCase());
226
+ }
227
+
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) {
359
+ if (typeof props.element === "string") {
360
+ this.element = document.createElement(props.element);
361
+ this.isFormElementType = this.isFormElement(props.element);
362
+ } else {
363
+ this.element = props.element?.value;
364
+ if (!this.element) {
365
+ throw new Error("DOMElement requires a valid element.");
366
+ }
367
+ this.isFormElementType = this.isFormElement(this.element.tagName);
368
+ }
369
+ if (props.onBeforeDestroy || props["on-before-destroy"]) {
370
+ this.onBeforeDestroy =
371
+ props.onBeforeDestroy || props["on-before-destroy"];
372
+ }
373
+
374
+ for (const event of EVENTS) {
375
+ if (props.attrs?.[event]) {
376
+ const eventHandler = (e: Event) => {
377
+ // Special handling for form submit events
378
+ if (event === "submit" && this.element.tagName.toLowerCase() === "form") {
379
+ e.preventDefault(); // Stop form submission propagation
380
+
381
+ // Collect all form data
382
+ const formData = new FormData(this.element as HTMLFormElement);
383
+ const formObject: Record<string, any> = {};
384
+
385
+ // Convert FormData to plain object
386
+ formData.forEach((value, key) => {
387
+ if (formObject[key]) {
388
+ // Handle multiple values for same key (like checkboxes)
389
+ if (Array.isArray(formObject[key])) {
390
+ formObject[key].push(value);
391
+ } else {
392
+ formObject[key] = [formObject[key], value];
393
+ }
394
+ } else {
395
+ formObject[key] = value;
396
+ }
397
+ });
398
+
399
+ // Call the event handler with event and form data
400
+ props.attrs[event]?.(e, formObject);
401
+ } else {
402
+ props.attrs[event]?.(e);
403
+ }
404
+ };
405
+ this.eventListeners.set(event, eventHandler);
406
+ this.element.addEventListener(event, eventHandler, false);
407
+ }
408
+ }
409
+ if (props.children) {
410
+ this.appendChildElement(props.children);
411
+ }
412
+ this.onUpdate(props);
413
+ }
414
+
415
+ onMount(context: Element<CanvasDOMElement>) {
416
+ const props = context.propObservables;
417
+ const attrs = props.attrs as any;
418
+ // Handle form elements with signal value
419
+ if (
420
+ this.isFormElementType &&
421
+ attrs?.value &&
422
+ isSignal(attrs.value)
423
+ ) {
424
+ this.valueSignal = attrs.value;
425
+ // Set initial value from signal
426
+ (
427
+ this.element as
428
+ | HTMLInputElement
429
+ | HTMLTextAreaElement
430
+ | HTMLSelectElement
431
+ ).value = this.valueSignal();
432
+
433
+ // Listen for input events and update the signal
434
+ const inputHandler = (e: Event) => {
435
+ const target = e.target as
436
+ | HTMLInputElement
437
+ | HTMLTextAreaElement
438
+ | HTMLSelectElement;
439
+ this.valueSignal.set(target.value);
440
+ };
441
+
442
+ this.eventListeners.set("input", inputHandler);
443
+ this.element.addEventListener("input", inputHandler, false);
444
+ }
445
+ }
446
+
447
+ onUpdate(props: DOMElementProps) {
448
+ if (!this.element) return;
449
+ for (const [key, value] of Object.entries(props.attrs || {})) {
450
+ if (key === "tabindex") {
451
+ // Handle tabindex attribute
452
+ const tabindexValue = isSignal(value) ? value() : value;
453
+ if (tabindexValue !== undefined && tabindexValue !== null) {
454
+ this.element.setAttribute('tabindex', String(tabindexValue));
455
+ } else {
456
+ this.element.removeAttribute('tabindex');
457
+ }
458
+ } else if (key === "class") {
459
+ const rawClassList = value.items || value.value || value;
460
+ const classList = isSignal(rawClassList) ? rawClassList() : rawClassList;
461
+ this.applyClassList(classList);
462
+ this.syncClassSubscriptions(classList);
463
+ } else if (key === "style") {
464
+ const styleValue = value.items || value.value || value;
465
+
466
+ if (typeof styleValue === "string") {
467
+ // String: CSS style string
468
+ this.element.setAttribute("style", styleValue);
469
+ } else if (typeof styleValue === "object" && styleValue !== null) {
470
+ // Object: { property: value }
471
+ for (const [styleProp, styleVal] of Object.entries(styleValue)) {
472
+ if (styleVal !== null && styleVal !== undefined) {
473
+ (this.element.style as any)[styleProp] = styleVal;
474
+ }
475
+ }
476
+ }
477
+ } else if (key === "value" && this.isFormElementType) {
478
+ // Handle value attribute for form elements
479
+ if (isSignal(value)) {
480
+ // If it's a signal, the value is already handled in onInit
481
+ // Update the DOM element value if the signal value changed
482
+ const currentValue = (
483
+ this.element as
484
+ | HTMLInputElement
485
+ | HTMLTextAreaElement
486
+ | HTMLSelectElement
487
+ ).value;
488
+ const signalValue = value();
489
+ if (currentValue !== signalValue) {
490
+ (
491
+ this.element as
492
+ | HTMLInputElement
493
+ | HTMLTextAreaElement
494
+ | HTMLSelectElement
495
+ ).value = signalValue;
496
+ }
497
+ } else {
498
+ // If it's not a signal, set the value directly
499
+ (
500
+ this.element as
501
+ | HTMLInputElement
502
+ | HTMLTextAreaElement
503
+ | HTMLSelectElement
504
+ ).value = value;
505
+ }
506
+ } else if (!EVENTS.includes(key)) {
507
+ this.element.setAttribute(key, value);
508
+ }
509
+ }
510
+ if ("textContent" in props) {
511
+ const textContent = props.textContent;
512
+ this.element.textContent = textContent == null ? "" : String(textContent);
513
+ }
514
+ }
515
+
516
+ async onDestroy(
517
+ parent: Element<CanvasDOMElement>,
518
+ afterDestroy: () => void
519
+ ): Promise<void> {
520
+ // Remove all event listeners from the DOM element
521
+
522
+ if (this.element) {
523
+ if (this.onBeforeDestroy) {
524
+ await this.onBeforeDestroy();
525
+ }
526
+
527
+ for (const [event, handler] of this.eventListeners) {
528
+ this.element.removeEventListener(event, handler, false);
529
+ }
530
+
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 = [];
540
+
541
+ this.element.remove();
542
+
543
+ if (afterDestroy) {
544
+ afterDestroy();
545
+ }
546
+ }
547
+ }
548
+ }
549
+
550
+ export interface CanvasDOMElement extends DisplayObjectProps { }
551
+
552
+ registerComponent("DOMElement", CanvasDOMElement);
553
+
554
+ export const DOMElement: ComponentFunction<DOMContainerProps> = (props) => {
555
+ return createComponent("DOMElement", props);
556
+ };