canvasengine 2.0.0-beta.20 → 2.0.0-beta.22
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.d.ts +10 -6
- package/dist/index.js +140 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DOMContainer.ts +6 -112
- package/src/components/DOMElement.ts +421 -0
- package/src/components/DisplayObject.ts +1 -1
- package/src/components/index.ts +2 -1
- package/src/engine/bootstrap.ts +10 -2
- package/src/engine/reactive.ts +20 -7
- package/src/engine/utils.ts +5 -0
package/package.json
CHANGED
|
@@ -5,30 +5,10 @@ import {
|
|
|
5
5
|
registerComponent,
|
|
6
6
|
} from "../engine/reactive";
|
|
7
7
|
import { ComponentInstance, DisplayObject } from "./DisplayObject";
|
|
8
|
-
import { ComponentFunction } from "../engine/signal";
|
|
8
|
+
import { ComponentFunction, h } from "../engine/signal";
|
|
9
9
|
import { DisplayObjectProps } from "./types/DisplayObject";
|
|
10
|
+
import { CanvasDOMElement, DOMElement } from "./DOMElement";
|
|
10
11
|
|
|
11
|
-
interface DOMContainerProps extends DisplayObjectProps {
|
|
12
|
-
element:
|
|
13
|
-
| string
|
|
14
|
-
| {
|
|
15
|
-
value: HTMLElement;
|
|
16
|
-
};
|
|
17
|
-
textContent?: string;
|
|
18
|
-
attrs?: Record<string, any> & {
|
|
19
|
-
class?:
|
|
20
|
-
| string
|
|
21
|
-
| string[]
|
|
22
|
-
| Record<string, boolean>
|
|
23
|
-
| { items?: string[] }
|
|
24
|
-
| { value?: string | string[] | Record<string, boolean> };
|
|
25
|
-
style?:
|
|
26
|
-
| string
|
|
27
|
-
| Record<string, string | number>
|
|
28
|
-
| { value?: string | Record<string, string | number> };
|
|
29
|
-
};
|
|
30
|
-
sortableChildren?: boolean;
|
|
31
|
-
}
|
|
32
12
|
|
|
33
13
|
/**
|
|
34
14
|
* DOMContainer class for managing DOM elements within the canvas engine
|
|
@@ -127,96 +107,10 @@ const EVENTS = [
|
|
|
127
107
|
|
|
128
108
|
export class CanvasDOMContainer extends DisplayObject(PixiDOMContainer) {
|
|
129
109
|
disableLayout = true;
|
|
130
|
-
private eventListeners: Map<string, (e: Event) => void> = new Map();
|
|
131
|
-
|
|
132
|
-
onInit(props: DOMContainerProps) {
|
|
133
|
-
super.onInit(props);
|
|
134
|
-
if (props.element === undefined) {
|
|
135
|
-
throw new Error("DOMContainer: element is required");
|
|
136
|
-
}
|
|
137
|
-
if (typeof props.element === "string") {
|
|
138
|
-
this.element = document.createElement(props.element);
|
|
139
|
-
} else {
|
|
140
|
-
this.element = props.element.value;
|
|
141
|
-
}
|
|
142
|
-
for (const event of EVENTS) {
|
|
143
|
-
if (props.attrs?.[event]) {
|
|
144
|
-
const eventHandler = (e: Event) => {
|
|
145
|
-
props.attrs[event]?.(e);
|
|
146
|
-
};
|
|
147
|
-
this.eventListeners.set(event, eventHandler);
|
|
148
|
-
this.element.addEventListener(event, eventHandler, false);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
onUpdate(props: DOMContainerProps) {
|
|
154
|
-
super.onUpdate(props);
|
|
155
|
-
|
|
156
|
-
for (const [key, value] of Object.entries(props.attrs || {})) {
|
|
157
|
-
if (key === "class") {
|
|
158
|
-
const classList = value.items || value.value || value;
|
|
159
|
-
|
|
160
|
-
// Clear existing classes first
|
|
161
|
-
this.element.className = "";
|
|
162
|
-
|
|
163
|
-
if (typeof classList === "string") {
|
|
164
|
-
// String: space-separated class names
|
|
165
|
-
this.element.className = classList;
|
|
166
|
-
} else if (Array.isArray(classList)) {
|
|
167
|
-
// Array: array of class names
|
|
168
|
-
this.element.classList.add(...classList);
|
|
169
|
-
} else if (typeof classList === "object" && classList !== null) {
|
|
170
|
-
// Object: { className: boolean }
|
|
171
|
-
for (const [className, shouldAdd] of Object.entries(classList)) {
|
|
172
|
-
if (shouldAdd) {
|
|
173
|
-
this.element.classList.add(className);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} else if (key === "style") {
|
|
178
|
-
const styleValue = value.items || value.value || value;
|
|
179
|
-
|
|
180
|
-
if (typeof styleValue === "string") {
|
|
181
|
-
// String: CSS style string
|
|
182
|
-
this.element.setAttribute("style", styleValue);
|
|
183
|
-
} else if (typeof styleValue === "object" && styleValue !== null) {
|
|
184
|
-
// Object: { property: value }
|
|
185
|
-
for (const [styleProp, styleVal] of Object.entries(styleValue)) {
|
|
186
|
-
if (styleVal !== null && styleVal !== undefined) {
|
|
187
|
-
(this.element.style as any)[styleProp] = styleVal;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
} else if (!EVENTS.includes(key)) {
|
|
192
|
-
this.element.setAttribute(key, value);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (props.textContent) {
|
|
196
|
-
this.element.textContent = props.textContent;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (props.sortableChildren !== undefined) {
|
|
200
|
-
this.sortableChildren = props.sortableChildren;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async onDestroy(
|
|
205
|
-
parent: Element<ComponentInstance>,
|
|
206
|
-
afterDestroy: () => void
|
|
207
|
-
): Promise<void> {
|
|
208
|
-
// Remove all event listeners from the DOM element
|
|
209
|
-
if (this.element) {
|
|
210
|
-
for (const [event, handler] of this.eventListeners) {
|
|
211
|
-
this.element.removeEventListener(event, handler, false);
|
|
212
|
-
}
|
|
213
|
-
this.eventListeners.clear();
|
|
214
|
-
}
|
|
215
110
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
await super.onDestroy(parent, _afterDestroyCallback);
|
|
111
|
+
onInit(props: any) {
|
|
112
|
+
const div = h(DOMElement, { element: "div" }, props.children) as unknown as Element<CanvasDOMElement>;
|
|
113
|
+
this.element = div.componentInstance.element;
|
|
220
114
|
}
|
|
221
115
|
}
|
|
222
116
|
|
|
@@ -224,6 +118,6 @@ export interface CanvasDOMContainer extends DisplayObjectProps {}
|
|
|
224
118
|
|
|
225
119
|
registerComponent("DOMContainer", CanvasDOMContainer);
|
|
226
120
|
|
|
227
|
-
export const DOMContainer: ComponentFunction<
|
|
121
|
+
export const DOMContainer: ComponentFunction<any> = (props) => {
|
|
228
122
|
return createComponent("DOMContainer", props);
|
|
229
123
|
};
|
|
@@ -0,0 +1,421 @@
|
|
|
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
|
+
interface DOMContainerProps 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
|
+
/**
|
|
36
|
+
* DOMContainer class for managing DOM elements within the canvas engine
|
|
37
|
+
*
|
|
38
|
+
* This class extends the DisplayObject functionality to handle DOM elements using
|
|
39
|
+
* PixiJS's native DOMContainer. It provides a bridge between the canvas rendering
|
|
40
|
+
* system and traditional DOM manipulation with proper transform hierarchy and visibility.
|
|
41
|
+
*
|
|
42
|
+
* The DOMContainer is especially useful for rendering standard DOM elements that handle
|
|
43
|
+
* user input, such as `<input>` or `<textarea>`. This is often simpler and more flexible
|
|
44
|
+
* than trying to implement text input directly in PixiJS.
|
|
45
|
+
*
|
|
46
|
+
* ## Form Elements with Reactive Signals
|
|
47
|
+
*
|
|
48
|
+
* For form elements (`input`, `textarea`, `select`), the component supports reactive
|
|
49
|
+
* two-way data binding using signals. When the `value` attribute is a signal, the
|
|
50
|
+
* component automatically:
|
|
51
|
+
* - Sets the initial value from the signal
|
|
52
|
+
* - Listens for `input` events and updates the signal with the new value
|
|
53
|
+
* - Updates the DOM element when the signal value changes programmatically
|
|
54
|
+
*
|
|
55
|
+
* ## Form Submission Handling
|
|
56
|
+
*
|
|
57
|
+
* When a `form` element has a `submit` event handler, the component automatically:
|
|
58
|
+
* - Prevents the default form submission behavior (stops propagation)
|
|
59
|
+
* - Collects all form data from input elements within the form
|
|
60
|
+
* - Passes both the event and the collected form data as parameters to the submit handler
|
|
61
|
+
* - Handles multiple values for the same field name (e.g., checkboxes with same name)
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* import { signal } from '@signe/reactive';
|
|
66
|
+
*
|
|
67
|
+
* // Basic usage with input element
|
|
68
|
+
* const element = document.createElement('input');
|
|
69
|
+
* element.type = 'text';
|
|
70
|
+
* element.placeholder = 'Enter text...';
|
|
71
|
+
*
|
|
72
|
+
* const domContainer = new DOMContainer({
|
|
73
|
+
* element,
|
|
74
|
+
* x: 100,
|
|
75
|
+
* y: 50,
|
|
76
|
+
* anchor: { x: 0.5, y: 0.5 }
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* // Reactive form input with signal
|
|
80
|
+
* const inputValue = signal('');
|
|
81
|
+
*
|
|
82
|
+
* const reactiveInput = new DOMContainer({
|
|
83
|
+
* element: 'input',
|
|
84
|
+
* attrs: {
|
|
85
|
+
* type: 'text',
|
|
86
|
+
* placeholder: 'Type something...',
|
|
87
|
+
* value: inputValue // Signal for two-way binding
|
|
88
|
+
* }
|
|
89
|
+
* });
|
|
90
|
+
*
|
|
91
|
+
* // The signal will automatically update when user types
|
|
92
|
+
* inputValue.subscribe(value => {
|
|
93
|
+
* console.log('Input value changed:', value);
|
|
94
|
+
* });
|
|
95
|
+
*
|
|
96
|
+
* // You can also update the input programmatically
|
|
97
|
+
* inputValue.set('New value');
|
|
98
|
+
*
|
|
99
|
+
* // Form submission with automatic data collection
|
|
100
|
+
* const loginForm = new DOMContainer({
|
|
101
|
+
* element: 'form',
|
|
102
|
+
* attrs: {
|
|
103
|
+
* submit: (event, formData) => {
|
|
104
|
+
* // event: the submit event (already prevented)
|
|
105
|
+
* // formData: object containing all form field values
|
|
106
|
+
* console.log('Form submitted with data:', formData);
|
|
107
|
+
* // Example formData: { username: 'john', password: 'secret', remember: 'on' }
|
|
108
|
+
* }
|
|
109
|
+
* },
|
|
110
|
+
* children: [
|
|
111
|
+
* new DOMContainer({
|
|
112
|
+
* element: 'input',
|
|
113
|
+
* attrs: { name: 'username', type: 'text', placeholder: 'Username' }
|
|
114
|
+
* }),
|
|
115
|
+
* new DOMContainer({
|
|
116
|
+
* element: 'input',
|
|
117
|
+
* attrs: { name: 'password', type: 'password', placeholder: 'Password' }
|
|
118
|
+
* }),
|
|
119
|
+
* new DOMContainer({
|
|
120
|
+
* element: 'input',
|
|
121
|
+
* attrs: { name: 'remember', type: 'checkbox', value: 'on' }
|
|
122
|
+
* }),
|
|
123
|
+
* new DOMContainer({
|
|
124
|
+
* element: 'button',
|
|
125
|
+
* attrs: { type: 'submit' },
|
|
126
|
+
* textContent: 'Login'
|
|
127
|
+
* })
|
|
128
|
+
* ]
|
|
129
|
+
* });
|
|
130
|
+
*
|
|
131
|
+
* // Using different class and style formats
|
|
132
|
+
* const containerWithClasses = new DOMContainer({
|
|
133
|
+
* element: 'div',
|
|
134
|
+
* attrs: {
|
|
135
|
+
* // String format: space-separated classes
|
|
136
|
+
* class: 'container primary-theme',
|
|
137
|
+
*
|
|
138
|
+
* // Array format: array of class names
|
|
139
|
+
* // class: ['container', 'primary-theme'],
|
|
140
|
+
*
|
|
141
|
+
* // Object format: conditional classes
|
|
142
|
+
* // class: {
|
|
143
|
+
* // 'container': true,
|
|
144
|
+
* // 'primary-theme': true,
|
|
145
|
+
* // 'disabled': false
|
|
146
|
+
* // }
|
|
147
|
+
*
|
|
148
|
+
* // String format: CSS style string
|
|
149
|
+
* style: 'background-color: red; padding: 10px;',
|
|
150
|
+
*
|
|
151
|
+
* // Object format: style properties
|
|
152
|
+
* // style: {
|
|
153
|
+
* // backgroundColor: 'red',
|
|
154
|
+
* // padding: '10px',
|
|
155
|
+
* // fontSize: 16
|
|
156
|
+
* // }
|
|
157
|
+
* }
|
|
158
|
+
* });
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
const EVENTS = [
|
|
162
|
+
"click",
|
|
163
|
+
"mouseover",
|
|
164
|
+
"mouseout",
|
|
165
|
+
"mouseenter",
|
|
166
|
+
"mouseleave",
|
|
167
|
+
"mousemove",
|
|
168
|
+
"mouseup",
|
|
169
|
+
"mousedown",
|
|
170
|
+
"touchstart",
|
|
171
|
+
"touchend",
|
|
172
|
+
"touchmove",
|
|
173
|
+
"touchcancel",
|
|
174
|
+
"wheel",
|
|
175
|
+
"scroll",
|
|
176
|
+
"resize",
|
|
177
|
+
"focus",
|
|
178
|
+
"blur",
|
|
179
|
+
"change",
|
|
180
|
+
"input",
|
|
181
|
+
"submit",
|
|
182
|
+
"reset",
|
|
183
|
+
"keydown",
|
|
184
|
+
"keyup",
|
|
185
|
+
"keypress",
|
|
186
|
+
"contextmenu",
|
|
187
|
+
"drag",
|
|
188
|
+
"dragend",
|
|
189
|
+
"dragenter",
|
|
190
|
+
"dragleave",
|
|
191
|
+
"dragover",
|
|
192
|
+
"drop",
|
|
193
|
+
"dragstart",
|
|
194
|
+
"select",
|
|
195
|
+
"selectstart",
|
|
196
|
+
"selectend",
|
|
197
|
+
"selectall",
|
|
198
|
+
"selectnone",
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
export class CanvasDOMElement {
|
|
202
|
+
public element: HTMLElement;
|
|
203
|
+
private eventListeners: Map<string, (e: Event) => void> = new Map();
|
|
204
|
+
private onBeforeDestroy: OnHook | null = null;
|
|
205
|
+
private valueSignal: any = null;
|
|
206
|
+
private isFormElementType: boolean = false;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Checks if the element is a form element that supports the value attribute
|
|
210
|
+
* @param elementType - The element type string from props
|
|
211
|
+
* @returns true if the element is a form element with value support
|
|
212
|
+
*/
|
|
213
|
+
private isFormElement(elementType: string): boolean {
|
|
214
|
+
const formElements = ["input", "textarea", "select"];
|
|
215
|
+
return formElements.includes(elementType.toLowerCase());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onInit(props: DOMContainerProps) {
|
|
219
|
+
if (typeof props.element === "string") {
|
|
220
|
+
this.element = document.createElement(props.element);
|
|
221
|
+
this.isFormElementType = this.isFormElement(props.element);
|
|
222
|
+
} else {
|
|
223
|
+
this.element = props.element.value;
|
|
224
|
+
this.isFormElementType = this.isFormElement(this.element.tagName);
|
|
225
|
+
}
|
|
226
|
+
if (props.onBeforeDestroy || props["on-before-destroy"]) {
|
|
227
|
+
this.onBeforeDestroy =
|
|
228
|
+
props.onBeforeDestroy || props["on-before-destroy"];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const event of EVENTS) {
|
|
232
|
+
if (props.attrs?.[event]) {
|
|
233
|
+
const eventHandler = (e: Event) => {
|
|
234
|
+
// Special handling for form submit events
|
|
235
|
+
if (event === "submit" && this.element.tagName.toLowerCase() === "form") {
|
|
236
|
+
e.preventDefault(); // Stop form submission propagation
|
|
237
|
+
|
|
238
|
+
// Collect all form data
|
|
239
|
+
const formData = new FormData(this.element as HTMLFormElement);
|
|
240
|
+
const formObject: Record<string, any> = {};
|
|
241
|
+
|
|
242
|
+
// Convert FormData to plain object
|
|
243
|
+
formData.forEach((value, key) => {
|
|
244
|
+
if (formObject[key]) {
|
|
245
|
+
// Handle multiple values for same key (like checkboxes)
|
|
246
|
+
if (Array.isArray(formObject[key])) {
|
|
247
|
+
formObject[key].push(value);
|
|
248
|
+
} else {
|
|
249
|
+
formObject[key] = [formObject[key], value];
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
formObject[key] = value;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Call the event handler with event and form data
|
|
257
|
+
props.attrs[event]?.(e, formObject);
|
|
258
|
+
} else {
|
|
259
|
+
props.attrs[event]?.(e);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
this.eventListeners.set(event, eventHandler);
|
|
263
|
+
this.element.addEventListener(event, eventHandler, false);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
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
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.onUpdate(props);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
onMount(context: Element<CanvasDOMElement>) {
|
|
283
|
+
const props = context.propObservables;
|
|
284
|
+
const attrs = props.attrs as any;
|
|
285
|
+
// Handle form elements with signal value
|
|
286
|
+
if (
|
|
287
|
+
this.isFormElementType &&
|
|
288
|
+
attrs?.value &&
|
|
289
|
+
isSignal(attrs.value)
|
|
290
|
+
) {
|
|
291
|
+
this.valueSignal = attrs.value;
|
|
292
|
+
// Set initial value from signal
|
|
293
|
+
(
|
|
294
|
+
this.element as
|
|
295
|
+
| HTMLInputElement
|
|
296
|
+
| HTMLTextAreaElement
|
|
297
|
+
| HTMLSelectElement
|
|
298
|
+
).value = this.valueSignal();
|
|
299
|
+
|
|
300
|
+
// Listen for input events and update the signal
|
|
301
|
+
const inputHandler = (e: Event) => {
|
|
302
|
+
const target = e.target as
|
|
303
|
+
| HTMLInputElement
|
|
304
|
+
| HTMLTextAreaElement
|
|
305
|
+
| HTMLSelectElement;
|
|
306
|
+
this.valueSignal.set(target.value);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
this.eventListeners.set("input", inputHandler);
|
|
310
|
+
this.element.addEventListener("input", inputHandler, false);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
onUpdate(props: DOMContainerProps) {
|
|
315
|
+
if (!this.element) return;
|
|
316
|
+
for (const [key, value] of Object.entries(props.attrs || {})) {
|
|
317
|
+
if (key === "class") {
|
|
318
|
+
const classList = value.items || value.value || value;
|
|
319
|
+
|
|
320
|
+
// Clear existing classes first
|
|
321
|
+
this.element.className = "";
|
|
322
|
+
|
|
323
|
+
if (typeof classList === "string") {
|
|
324
|
+
// String: space-separated class names
|
|
325
|
+
this.element.className = classList;
|
|
326
|
+
} else if (Array.isArray(classList)) {
|
|
327
|
+
// Array: array of class names
|
|
328
|
+
this.element.classList.add(...classList);
|
|
329
|
+
} else if (typeof classList === "object" && classList !== null) {
|
|
330
|
+
// Object: { className: boolean }
|
|
331
|
+
for (const [className, shouldAdd] of Object.entries(classList)) {
|
|
332
|
+
if (shouldAdd) {
|
|
333
|
+
this.element.classList.add(className);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else if (key === "style") {
|
|
338
|
+
const styleValue = value.items || value.value || value;
|
|
339
|
+
|
|
340
|
+
if (typeof styleValue === "string") {
|
|
341
|
+
// String: CSS style string
|
|
342
|
+
this.element.setAttribute("style", styleValue);
|
|
343
|
+
} else if (typeof styleValue === "object" && styleValue !== null) {
|
|
344
|
+
// Object: { property: value }
|
|
345
|
+
for (const [styleProp, styleVal] of Object.entries(styleValue)) {
|
|
346
|
+
if (styleVal !== null && styleVal !== undefined) {
|
|
347
|
+
(this.element.style as any)[styleProp] = styleVal;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} else if (key === "value" && this.isFormElementType) {
|
|
352
|
+
// Handle value attribute for form elements
|
|
353
|
+
if (isSignal(value)) {
|
|
354
|
+
// If it's a signal, the value is already handled in onInit
|
|
355
|
+
// Update the DOM element value if the signal value changed
|
|
356
|
+
const currentValue = (
|
|
357
|
+
this.element as
|
|
358
|
+
| HTMLInputElement
|
|
359
|
+
| HTMLTextAreaElement
|
|
360
|
+
| HTMLSelectElement
|
|
361
|
+
).value;
|
|
362
|
+
const signalValue = value();
|
|
363
|
+
if (currentValue !== signalValue) {
|
|
364
|
+
(
|
|
365
|
+
this.element as
|
|
366
|
+
| HTMLInputElement
|
|
367
|
+
| HTMLTextAreaElement
|
|
368
|
+
| HTMLSelectElement
|
|
369
|
+
).value = signalValue;
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
// If it's not a signal, set the value directly
|
|
373
|
+
(
|
|
374
|
+
this.element as
|
|
375
|
+
| HTMLInputElement
|
|
376
|
+
| HTMLTextAreaElement
|
|
377
|
+
| HTMLSelectElement
|
|
378
|
+
).value = value;
|
|
379
|
+
}
|
|
380
|
+
} else if (!EVENTS.includes(key)) {
|
|
381
|
+
this.element.setAttribute(key, value);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (props.textContent) {
|
|
385
|
+
this.element.textContent = props.textContent;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async onDestroy(
|
|
390
|
+
parent: Element<CanvasDOMElement>,
|
|
391
|
+
afterDestroy: () => void
|
|
392
|
+
): Promise<void> {
|
|
393
|
+
// Remove all event listeners from the DOM element
|
|
394
|
+
|
|
395
|
+
if (this.element) {
|
|
396
|
+
if (this.onBeforeDestroy) {
|
|
397
|
+
await this.onBeforeDestroy();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const [event, handler] of this.eventListeners) {
|
|
401
|
+
this.element.removeEventListener(event, handler, false);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.eventListeners.clear();
|
|
405
|
+
|
|
406
|
+
this.element.remove();
|
|
407
|
+
|
|
408
|
+
if (afterDestroy) {
|
|
409
|
+
afterDestroy();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export interface CanvasDOMElement extends DisplayObjectProps {}
|
|
416
|
+
|
|
417
|
+
registerComponent("DOMElement", CanvasDOMElement);
|
|
418
|
+
|
|
419
|
+
export const DOMElement: ComponentFunction<DOMContainerProps> = (props) => {
|
|
420
|
+
return createComponent("DOMElement", props);
|
|
421
|
+
};
|
|
@@ -96,7 +96,7 @@ export const EVENTS = [
|
|
|
96
96
|
"wheelcapture",
|
|
97
97
|
];
|
|
98
98
|
|
|
99
|
-
type OnHook = (() => void) | (() => Promise<void> | void);
|
|
99
|
+
export type OnHook = (() => void) | (() => Promise<void> | void);
|
|
100
100
|
|
|
101
101
|
export function DisplayObject(extendClass) {
|
|
102
102
|
return class DisplayObject extends extendClass {
|
package/src/components/index.ts
CHANGED
|
@@ -11,4 +11,5 @@ export { TilingSprite } from './TilingSprite'
|
|
|
11
11
|
export { Viewport } from './Viewport'
|
|
12
12
|
export { NineSliceSprite } from './NineSliceSprite'
|
|
13
13
|
export { type ComponentInstance } from './DisplayObject'
|
|
14
|
-
export { DOMContainer } from './DOMContainer'
|
|
14
|
+
export { DOMContainer } from './DOMContainer'
|
|
15
|
+
export { DOMElement } from './DOMElement'
|
package/src/engine/bootstrap.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import '@pixi/layout';
|
|
2
|
-
import { Application } from "pixi.js";
|
|
2
|
+
import { Application, ApplicationOptions } from "pixi.js";
|
|
3
3
|
import { ComponentFunction, h } from "./signal";
|
|
4
|
+
import { useProps } from '../hooks/useProps';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Bootstraps a canvas element and renders it to the DOM.
|
|
@@ -10,12 +11,13 @@ import { ComponentFunction, h } from "./signal";
|
|
|
10
11
|
* @returns A Promise that resolves to the rendered canvas element.
|
|
11
12
|
* @throws {Error} If the provided element is not a Canvas component.
|
|
12
13
|
*/
|
|
13
|
-
export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: ComponentFunction<any
|
|
14
|
+
export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: ComponentFunction<any>, options?: ApplicationOptions) => {
|
|
14
15
|
|
|
15
16
|
const app = new Application();
|
|
16
17
|
await app.init({
|
|
17
18
|
resizeTo: rootElement,
|
|
18
19
|
autoStart: false,
|
|
20
|
+
...(options ?? {})
|
|
19
21
|
});
|
|
20
22
|
const canvasElement = await h(canvas);
|
|
21
23
|
if (canvasElement.tag != 'Canvas') {
|
|
@@ -23,6 +25,12 @@ export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: C
|
|
|
23
25
|
}
|
|
24
26
|
(canvasElement as any).render(rootElement, app);
|
|
25
27
|
|
|
28
|
+
const { backgroundColor } = useProps(canvasElement.props, {
|
|
29
|
+
backgroundColor: 'black'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.renderer.background.color = backgroundColor()
|
|
33
|
+
|
|
26
34
|
return {
|
|
27
35
|
canvasElement,
|
|
28
36
|
app
|
package/src/engine/reactive.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
from,
|
|
8
8
|
map,
|
|
9
9
|
of,
|
|
10
|
+
share,
|
|
10
11
|
switchMap,
|
|
11
12
|
} from "rxjs";
|
|
12
13
|
import { ComponentInstance } from "../components/DisplayObject";
|
|
@@ -83,14 +84,26 @@ function destroyElement(element: Element | Element[]) {
|
|
|
83
84
|
if (!element) {
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
87
|
+
if (element.props?.children) {
|
|
88
|
+
for (let child of element.props.children) {
|
|
89
|
+
destroyElement(child)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
86
92
|
for (let name in element.directives) {
|
|
87
93
|
element.directives[name].onDestroy?.(element);
|
|
88
94
|
}
|
|
89
|
-
element.componentInstance.onDestroy
|
|
90
|
-
element.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
if (element.componentInstance && element.componentInstance.onDestroy) {
|
|
96
|
+
element.componentInstance.onDestroy(element.parent as any, () => {
|
|
97
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
98
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
99
|
+
element.effectUnmounts?.forEach((fn) => fn?.());
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
// If componentInstance is undefined or doesn't have onDestroy, still clean up subscriptions
|
|
103
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
104
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
105
|
+
element.effectUnmounts?.forEach((fn) => fn?.());
|
|
106
|
+
}
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
/**
|
|
@@ -489,7 +502,7 @@ export function cond(
|
|
|
489
502
|
createElementFn: () => Element | Promise<Element>
|
|
490
503
|
): FlowObservable {
|
|
491
504
|
let element: Element | null = null;
|
|
492
|
-
|
|
505
|
+
|
|
493
506
|
if (isSignal(condition)) {
|
|
494
507
|
const signalCondition = condition as WritableObjectSignal<boolean>;
|
|
495
508
|
return new Observable<{elements: Element[], type?: "init" | "remove"}>(subscriber => {
|
|
@@ -522,7 +535,7 @@ export function cond(
|
|
|
522
535
|
});
|
|
523
536
|
}
|
|
524
537
|
});
|
|
525
|
-
})
|
|
538
|
+
}).pipe(share())
|
|
526
539
|
} else {
|
|
527
540
|
// Handle boolean case
|
|
528
541
|
if (condition) {
|
package/src/engine/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ObservablePoint } from "pixi.js"
|
|
2
|
+
import { Observable } from "rxjs"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Checks if code is running in a browser environment
|
|
@@ -101,6 +102,10 @@ export function isObject(val: unknown): boolean {
|
|
|
101
102
|
return typeof val == 'object' && val != null && !Array.isArray(val) && val.constructor === Object
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
export function isObservable(val: unknown): boolean {
|
|
106
|
+
return val instanceof Observable
|
|
107
|
+
}
|
|
108
|
+
|
|
104
109
|
/**
|
|
105
110
|
* Sets a value in an object using a dot notation path
|
|
106
111
|
* @param {Record<string, any>} obj - Target object
|