canvasengine 2.0.0-beta.44 → 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.
- package/dist/components/Container.d.ts +86 -0
- package/dist/components/Container.d.ts.map +1 -0
- package/dist/components/DOMContainer.d.ts +98 -0
- package/dist/components/DOMContainer.d.ts.map +1 -0
- package/dist/components/DOMElement.d.ts +16 -5
- package/dist/components/DOMElement.d.ts.map +1 -1
- package/dist/components/DOMSprite.d.ts +108 -0
- package/dist/components/DOMSprite.d.ts.map +1 -0
- package/dist/components/DisplayObject.d.ts +94 -0
- package/dist/components/DisplayObject.d.ts.map +1 -0
- package/dist/components/FocusContainer.d.ts +129 -0
- package/dist/components/FocusContainer.d.ts.map +1 -0
- package/dist/components/Mesh.d.ts +208 -0
- package/dist/components/Mesh.d.ts.map +1 -0
- package/dist/components/Sprite.d.ts +242 -0
- package/dist/components/Sprite.d.ts.map +1 -0
- package/dist/components/Viewport.d.ts +121 -0
- package/dist/components/Viewport.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/directives/Controls.d.ts +4 -4
- package/dist/directives/Controls.d.ts.map +1 -1
- package/dist/directives/ControlsBase.d.ts +1 -0
- package/dist/directives/ControlsBase.d.ts.map +1 -1
- package/dist/directives/FocusNavigation.d.ts +4 -22
- package/dist/directives/FocusNavigation.d.ts.map +1 -1
- package/dist/directives/KeyboardControls.d.ts +1 -0
- package/dist/directives/KeyboardControls.d.ts.map +1 -1
- package/dist/directives/Scheduler.d.ts.map +1 -1
- package/dist/directives/Shake.d.ts +1 -0
- package/dist/directives/Shake.d.ts.map +1 -1
- package/dist/engine/FocusManager.d.ts +10 -9
- package/dist/engine/FocusManager.d.ts.map +1 -1
- package/dist/engine/bootstrap.d.ts +1 -0
- package/dist/engine/bootstrap.d.ts.map +1 -1
- package/dist/engine/directive.d.ts +1 -1
- package/dist/engine/directive.d.ts.map +1 -1
- package/dist/engine/reactive.d.ts +10 -0
- package/dist/engine/reactive.d.ts.map +1 -1
- package/dist/engine/signal.d.ts +19 -19
- package/dist/engine/signal.d.ts.map +1 -1
- package/dist/hooks/useFocus.d.ts.map +1 -1
- package/dist/index-DaGekQUW.js +2218 -0
- package/dist/index-DaGekQUW.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.js +3 -3
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +11868 -86
- package/dist/index.js.map +1 -1
- package/dist/utils/tabindex.d.ts +16 -0
- package/dist/utils/tabindex.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/DOMContainer.ts +186 -1
- package/src/components/DOMElement.ts +164 -37
- package/src/components/DOMSprite.ts +759 -0
- package/src/components/DisplayObject.ts +33 -7
- package/src/components/FocusContainer.ts +22 -26
- package/src/components/Sprite.ts +12 -3
- package/src/components/Text.ts +1 -1
- package/src/components/Viewport.ts +5 -5
- package/src/components/index.ts +2 -1
- package/src/directives/Controls.ts +5 -5
- package/src/directives/ControlsBase.ts +1 -0
- package/src/directives/FocusNavigation.ts +8 -146
- package/src/directives/KeyboardControls.ts +11 -2
- package/src/directives/Scheduler.ts +12 -4
- package/src/directives/Shake.ts +9 -6
- package/src/engine/FocusManager.ts +44 -29
- package/src/engine/bootstrap.ts +5 -2
- package/src/engine/directive.ts +2 -2
- package/src/engine/reactive.ts +137 -27
- package/src/engine/signal.ts +80 -23
- package/src/hooks/useFocus.ts +2 -5
- package/src/index.ts +2 -1
- package/src/types/pixi-cull.d.ts +7 -0
- package/src/utils/tabindex.ts +70 -0
- package/testing/index.ts +31 -3
- package/tsconfig.json +3 -2
- package/dist/DebugRenderer-P5sZ-0Tq.js +0 -172
- package/dist/DebugRenderer-P5sZ-0Tq.js.map +0 -1
- package/dist/index-VPoz4ufu.js +0 -13252
- package/dist/index-VPoz4ufu.js.map +0 -1
package/src/directives/Shake.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Container, Point } from 'pixi.js';
|
|
2
2
|
import { Directive, registerDirective } from '../engine/directive';
|
|
3
3
|
import { Element } from '../engine/reactive';
|
|
4
|
-
import { effect } from '@signe/reactive';
|
|
4
|
+
import { effect, isSignal } from '@signe/reactive';
|
|
5
5
|
import { on, isTrigger, Trigger } from '../engine/trigger';
|
|
6
6
|
import { useProps } from '../hooks/useProps';
|
|
7
7
|
import { SignalOrPrimitive } from '../components/types';
|
|
@@ -128,6 +128,10 @@ export class Shake extends Directive {
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
private resolveSignalValue<T>(value: SignalOrPrimitive<T>): T {
|
|
132
|
+
return (isSignal(value as any) ? (value as any)() : value) as T;
|
|
133
|
+
}
|
|
134
|
+
|
|
131
135
|
/**
|
|
132
136
|
* Performs the shake animation using animatedSignal
|
|
133
137
|
* @param data - Optional data passed from the trigger that can override default options
|
|
@@ -139,10 +143,10 @@ export class Shake extends Directive {
|
|
|
139
143
|
const shakeProps = this.shakeProps;
|
|
140
144
|
|
|
141
145
|
// Use data from trigger to override defaults if provided
|
|
142
|
-
const intensity = data?.intensity ?? shakeProps.intensity
|
|
143
|
-
const duration = data?.duration ?? shakeProps.duration
|
|
144
|
-
const frequency = data?.frequency ?? shakeProps.frequency
|
|
145
|
-
const direction = data?.direction ?? shakeProps.direction
|
|
146
|
+
const intensity = data?.intensity ?? this.resolveSignalValue(shakeProps.intensity);
|
|
147
|
+
const duration = data?.duration ?? this.resolveSignalValue(shakeProps.duration);
|
|
148
|
+
const frequency = data?.frequency ?? this.resolveSignalValue(shakeProps.frequency);
|
|
149
|
+
const direction = data?.direction ?? this.resolveSignalValue(shakeProps.direction);
|
|
146
150
|
|
|
147
151
|
// Stop any existing animation and clean up
|
|
148
152
|
if (this.positionEffect) {
|
|
@@ -292,4 +296,3 @@ export class Shake extends Directive {
|
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
registerDirective('shake', Shake);
|
|
295
|
-
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isSignal, signal, Signal } from "@signe/reactive";
|
|
1
|
+
import { isSignal, signal, Signal, WritableSignal, WritableObjectSignal } from "@signe/reactive";
|
|
2
2
|
import { Element, isElementFrozen } from "./reactive";
|
|
3
3
|
import { CanvasViewport } from "../components/Viewport";
|
|
4
4
|
import { SignalOrPrimitive } from "../components/types";
|
|
@@ -21,19 +21,20 @@ export interface ScrollOptions {
|
|
|
21
21
|
/**
|
|
22
22
|
* Data structure for a focus container
|
|
23
23
|
*/
|
|
24
|
+
type WritableElementSignal = WritableSignal<Element | null> | WritableObjectSignal<Element | null>;
|
|
25
|
+
|
|
24
26
|
interface FocusContainerData {
|
|
25
27
|
id: string;
|
|
26
|
-
element?: Element
|
|
27
|
-
focusables: Map<number, Element
|
|
28
|
-
currentIndex:
|
|
29
|
-
focusedElement:
|
|
28
|
+
element?: Element<any>;
|
|
29
|
+
focusables: Map<number, Element<any>>;
|
|
30
|
+
currentIndex: WritableSignal<number | null>;
|
|
31
|
+
focusedElement: WritableElementSignal;
|
|
30
32
|
onFocusChange?: (index: number, element: Element | null) => void;
|
|
31
33
|
autoScroll?: boolean | ScrollOptions;
|
|
32
34
|
viewport?: CanvasViewport;
|
|
33
|
-
|
|
34
|
-
lastNavigateTime?: number;
|
|
35
|
-
tabindex?: SignalOrPrimitive<number>;
|
|
35
|
+
tabindex?: SignalOrPrimitive<number> | null;
|
|
36
36
|
tabindexSubscription?: any;
|
|
37
|
+
pendingIndex?: number;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -89,7 +90,7 @@ export class FocusManager {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
setTabindex(id: string, tabindex
|
|
93
|
+
setTabindex(id: string, tabindex?: SignalOrPrimitive<number> | null): void {
|
|
93
94
|
const container = this.containers.get(id);
|
|
94
95
|
if (!container) return;
|
|
95
96
|
|
|
@@ -98,10 +99,20 @@ export class FocusManager {
|
|
|
98
99
|
container.tabindexSubscription.unsubscribe();
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
if (tabindex === undefined || tabindex === null) {
|
|
103
|
+
container.tabindex = undefined;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
container.tabindex = tabindex;
|
|
102
108
|
|
|
109
|
+
const currentTabindex = isSignal(tabindex) ? (tabindex as Signal<number>)() : tabindex;
|
|
110
|
+
if (typeof currentTabindex === "number") {
|
|
111
|
+
this.setIndex(id, currentTabindex);
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
if (isSignal(tabindex)) {
|
|
104
|
-
container.tabindexSubscription = (tabindex as Signal<number>).observable.subscribe((value: any) => {
|
|
115
|
+
container.tabindexSubscription = ((tabindex as Signal<number>).observable as any).subscribe((value: any) => {
|
|
105
116
|
if (value !== null && value !== container.currentIndex()) {
|
|
106
117
|
this.setIndex(id, value);
|
|
107
118
|
}
|
|
@@ -136,8 +147,9 @@ export class FocusManager {
|
|
|
136
147
|
|
|
137
148
|
// If this is the index we are supposed to be at, set it now
|
|
138
149
|
const currentTabindex = isSignal(container.tabindex) ? (container.tabindex as Signal<number>)() : container.tabindex;
|
|
139
|
-
if (currentTabindex === index && container.currentIndex() === null) {
|
|
140
|
-
|
|
150
|
+
if (container.pendingIndex === index || (currentTabindex === index && container.currentIndex() === null)) {
|
|
151
|
+
container.pendingIndex = undefined;
|
|
152
|
+
this.applyFocus(container, containerId, index, element);
|
|
141
153
|
}
|
|
142
154
|
}
|
|
143
155
|
|
|
@@ -170,16 +182,6 @@ export class FocusManager {
|
|
|
170
182
|
return;
|
|
171
183
|
}
|
|
172
184
|
|
|
173
|
-
// Handle throttling
|
|
174
|
-
if (container.throttle) {
|
|
175
|
-
const now = Date.now();
|
|
176
|
-
const lastTime = container.lastNavigateTime || 0;
|
|
177
|
-
if (now - lastTime < container.throttle) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
container.lastNavigateTime = now;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
185
|
const currentIndex = container.currentIndex();
|
|
184
186
|
const focusableIndices = Array.from(container.focusables.keys()).sort((a, b) => a - b);
|
|
185
187
|
|
|
@@ -211,7 +213,7 @@ export class FocusManager {
|
|
|
211
213
|
|
|
212
214
|
if (newIndex !== null) {
|
|
213
215
|
const tabindex = container.tabindex;
|
|
214
|
-
if (isSignal(tabindex)) {
|
|
216
|
+
if (isSignal(tabindex) && typeof (tabindex as any).set === "function") {
|
|
215
217
|
(tabindex as any).set(newIndex);
|
|
216
218
|
} else {
|
|
217
219
|
this.setIndex(containerId, newIndex);
|
|
@@ -231,16 +233,24 @@ export class FocusManager {
|
|
|
231
233
|
|
|
232
234
|
const element = container.focusables.get(index);
|
|
233
235
|
if (!element) {
|
|
234
|
-
|
|
236
|
+
container.pendingIndex = index;
|
|
235
237
|
return;
|
|
236
238
|
}
|
|
239
|
+
this.applyFocus(container, containerId, index, element);
|
|
240
|
+
}
|
|
237
241
|
|
|
242
|
+
private applyFocus(
|
|
243
|
+
container: FocusContainerData,
|
|
244
|
+
containerId: string,
|
|
245
|
+
index: number,
|
|
246
|
+
element: Element
|
|
247
|
+
): void {
|
|
238
248
|
container.currentIndex.set(index);
|
|
239
249
|
container.focusedElement.set(element);
|
|
240
250
|
|
|
241
251
|
// Sync back to tabindex signal if it exists
|
|
242
252
|
const tabindex = container.tabindex;
|
|
243
|
-
if (isSignal(tabindex) && (tabindex as any)() !== index) {
|
|
253
|
+
if (isSignal(tabindex) && (tabindex as any)() !== index && typeof (tabindex as any).set === "function") {
|
|
244
254
|
(tabindex as any).set(index);
|
|
245
255
|
}
|
|
246
256
|
|
|
@@ -308,7 +318,7 @@ export class FocusManager {
|
|
|
308
318
|
*/
|
|
309
319
|
getFocusedElementSignal(containerId: string): Signal<Element | null> | null {
|
|
310
320
|
const container = this.containers.get(containerId);
|
|
311
|
-
return container ? container.focusedElement : null;
|
|
321
|
+
return container ? (container.focusedElement as unknown as Signal<Element | null>) : null;
|
|
312
322
|
}
|
|
313
323
|
|
|
314
324
|
/**
|
|
@@ -345,10 +355,16 @@ export class FocusManager {
|
|
|
345
355
|
}
|
|
346
356
|
|
|
347
357
|
// Get local bounds
|
|
348
|
-
const localBounds = instance.getLocalBounds();
|
|
358
|
+
const localBounds = instance.getLocalBounds?.();
|
|
359
|
+
if (!localBounds) {
|
|
360
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
361
|
+
}
|
|
349
362
|
|
|
350
363
|
// Get global position
|
|
351
|
-
const globalPos = instance.getGlobalPosition();
|
|
364
|
+
const globalPos = instance.getGlobalPosition?.();
|
|
365
|
+
if (!globalPos) {
|
|
366
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
367
|
+
}
|
|
352
368
|
|
|
353
369
|
return {
|
|
354
370
|
x: globalPos.x,
|
|
@@ -492,4 +508,3 @@ export class FocusManager {
|
|
|
492
508
|
|
|
493
509
|
// Export singleton instance
|
|
494
510
|
export const focusManager = FocusManager.getInstance();
|
|
495
|
-
|
package/src/engine/bootstrap.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import '@pixi/layout';
|
|
2
1
|
import { Application, ApplicationOptions } from "pixi.js";
|
|
3
2
|
import { ComponentFunction, h } from "./signal";
|
|
4
3
|
import { useProps } from '../hooks/useProps';
|
|
@@ -31,6 +30,7 @@ export interface BootstrapOptions extends ApplicationOptions {
|
|
|
31
30
|
[name: string]: any; // ComponentClass
|
|
32
31
|
};
|
|
33
32
|
autoRegister?: boolean; // true by default if components is not provided
|
|
33
|
+
enableLayout?: boolean; // true by default
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -63,7 +63,10 @@ export interface BootstrapOptions extends ApplicationOptions {
|
|
|
63
63
|
*/
|
|
64
64
|
export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: ComponentFunction<any>, options?: BootstrapOptions) => {
|
|
65
65
|
// Extract component registration options
|
|
66
|
-
const { components, autoRegister, ...appOptions } = options ?? {};
|
|
66
|
+
const { components, autoRegister, enableLayout, ...appOptions } = options ?? {};
|
|
67
|
+
if (enableLayout !== false) {
|
|
68
|
+
await import('@pixi/layout');
|
|
69
|
+
}
|
|
67
70
|
|
|
68
71
|
// Handle component registration
|
|
69
72
|
if (components) {
|
package/src/engine/directive.ts
CHANGED
|
@@ -13,11 +13,11 @@ export function registerDirective(name: string, directive: any) {
|
|
|
13
13
|
directives[name] = directive
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function applyDirective(element: Element
|
|
16
|
+
export function applyDirective(element: Element<any>, directiveName: string) {
|
|
17
17
|
if (!directives[directiveName]) {
|
|
18
18
|
return null
|
|
19
19
|
}
|
|
20
20
|
const directive = new directives[directiveName]()
|
|
21
21
|
directive.onInit?.(element)
|
|
22
22
|
return directive
|
|
23
|
-
}
|
|
23
|
+
}
|
package/src/engine/reactive.ts
CHANGED
|
@@ -81,6 +81,72 @@ export const isPrimitive = (value) => {
|
|
|
81
81
|
);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
const DOM_ROUTING_MAP: Record<string, string> = {
|
|
85
|
+
Sprite: "DOMSprite",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const DOM_ALLOWED_TAGS = new Set(["DOMContainer", "DOMElement", "DOMSprite"]);
|
|
89
|
+
const DOM_UNSUPPORTED_TAGS = new Set([
|
|
90
|
+
"Canvas",
|
|
91
|
+
"Container",
|
|
92
|
+
"Graphics",
|
|
93
|
+
"Rect",
|
|
94
|
+
"Circle",
|
|
95
|
+
"Ellipse",
|
|
96
|
+
"Triangle",
|
|
97
|
+
"Svg",
|
|
98
|
+
"Mesh",
|
|
99
|
+
"Scene",
|
|
100
|
+
"ParticlesEmitter",
|
|
101
|
+
"Sprite",
|
|
102
|
+
"Video",
|
|
103
|
+
"Text",
|
|
104
|
+
"TilingSprite",
|
|
105
|
+
"Viewport",
|
|
106
|
+
"NineSliceSprite",
|
|
107
|
+
"Button",
|
|
108
|
+
"Joystick",
|
|
109
|
+
"FocusContainer",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const hasDomAncestor = (element: Element | null): boolean => {
|
|
113
|
+
let current = element;
|
|
114
|
+
while (current) {
|
|
115
|
+
if (current.tag === "DOMContainer" || current.tag === "DOMElement") {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
current = current.parent;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const cleanupElementForRouting = (element: Element) => {
|
|
124
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
125
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
126
|
+
element.effectUnmounts?.forEach((fn) => fn?.());
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const routeDomComponent = (parent: Element, child: Element): Element => {
|
|
130
|
+
if (!hasDomAncestor(parent)) {
|
|
131
|
+
return child;
|
|
132
|
+
}
|
|
133
|
+
if (DOM_ALLOWED_TAGS.has(child.tag)) {
|
|
134
|
+
return child;
|
|
135
|
+
}
|
|
136
|
+
const routedTag = DOM_ROUTING_MAP[child.tag];
|
|
137
|
+
if (routedTag) {
|
|
138
|
+
cleanupElementForRouting(child);
|
|
139
|
+
const routedProps = child.propObservables ?? child.props;
|
|
140
|
+
return createComponent(routedTag, routedProps);
|
|
141
|
+
}
|
|
142
|
+
if (DOM_UNSUPPORTED_TAGS.has(child.tag)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Component ${child.tag} is not implemented for DOMContainer context yet. Only Sprite is supported.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return child;
|
|
148
|
+
};
|
|
149
|
+
|
|
84
150
|
export function registerComponent(name, component) {
|
|
85
151
|
components[name] = component;
|
|
86
152
|
}
|
|
@@ -120,6 +186,54 @@ export function registerAllComponents() {
|
|
|
120
186
|
componentsRegistered = true;
|
|
121
187
|
}
|
|
122
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Checks if all dependencies are ready (not undefined).
|
|
191
|
+
* Handles signals synchronously and promises asynchronously.
|
|
192
|
+
* For reactive signals, sets up subscriptions to mount when all become ready.
|
|
193
|
+
*
|
|
194
|
+
* @param deps - Array of signals, promises, or direct values
|
|
195
|
+
* @returns Promise<boolean> - true if all dependencies are ready
|
|
196
|
+
*/
|
|
197
|
+
export async function checkDependencies(
|
|
198
|
+
deps: any[]
|
|
199
|
+
): Promise<boolean> {
|
|
200
|
+
const values = await Promise.all(
|
|
201
|
+
deps.map(async (dep) => {
|
|
202
|
+
if (isSignal(dep)) {
|
|
203
|
+
return dep(); // Read current signal value
|
|
204
|
+
} else if (isPromise(dep)) {
|
|
205
|
+
return await dep; // Await promise resolution
|
|
206
|
+
}
|
|
207
|
+
return dep; // Direct value
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
return values.every((v) => v !== undefined);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function waitForDependencies(deps: any[]): Promise<void> {
|
|
214
|
+
return new Promise(async (resolve) => {
|
|
215
|
+
const ready = await checkDependencies(deps);
|
|
216
|
+
if (ready) {
|
|
217
|
+
resolve();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const signalDeps = deps.filter((dep) => isSignal(dep));
|
|
222
|
+
if (signalDeps.length === 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const signalObservables = signalDeps.map((sig) => sig.observable);
|
|
227
|
+
const subscription = combineLatest(signalObservables).subscribe(async () => {
|
|
228
|
+
const allReady = await checkDependencies(deps);
|
|
229
|
+
if (allReady) {
|
|
230
|
+
subscription.unsubscribe();
|
|
231
|
+
resolve();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
123
237
|
/**
|
|
124
238
|
* Checks if an element is currently frozen.
|
|
125
239
|
* An element is frozen when the `freeze` prop is set to `true` (either as a boolean or Signal<boolean>),
|
|
@@ -376,22 +490,12 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
376
490
|
* @param deps - Array of signals, promises, or direct values
|
|
377
491
|
* @returns Promise<boolean> - true if all dependencies are ready
|
|
378
492
|
*/
|
|
379
|
-
async function checkDependencies(
|
|
380
|
-
deps: any[]
|
|
381
|
-
): Promise<boolean> {
|
|
382
|
-
const values = await Promise.all(
|
|
383
|
-
deps.map(async (dep) => {
|
|
384
|
-
if (isSignal(dep)) {
|
|
385
|
-
return dep(); // Read current signal value
|
|
386
|
-
} else if (isPromise(dep)) {
|
|
387
|
-
return await dep; // Await promise resolution
|
|
388
|
-
}
|
|
389
|
-
return dep; // Direct value
|
|
390
|
-
})
|
|
391
|
-
);
|
|
392
|
-
return values.every((v) => v !== undefined);
|
|
393
|
-
}
|
|
394
493
|
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Sets up subscriptions to reactive signal dependencies.
|
|
497
|
+
* When all signals become defined, mounts the component.
|
|
498
|
+
*/
|
|
395
499
|
/**
|
|
396
500
|
* Sets up subscriptions to reactive signal dependencies.
|
|
397
501
|
* When all signals become defined, mounts the component.
|
|
@@ -571,8 +675,9 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
571
675
|
// Handle observable component recursively
|
|
572
676
|
await createElement(parent, c);
|
|
573
677
|
} else if (isElement(c)) {
|
|
574
|
-
|
|
575
|
-
|
|
678
|
+
const routed = routeDomComponent(parent, c);
|
|
679
|
+
onMount(parent, routed, index + 1);
|
|
680
|
+
propagateContext(routed);
|
|
576
681
|
}
|
|
577
682
|
});
|
|
578
683
|
return;
|
|
@@ -583,8 +688,9 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
583
688
|
// Handle observable component recursively
|
|
584
689
|
await createElement(parent, component);
|
|
585
690
|
} else if (isElement(component)) {
|
|
586
|
-
|
|
587
|
-
|
|
691
|
+
const routed = routeDomComponent(parent, component);
|
|
692
|
+
onMount(parent, routed);
|
|
693
|
+
propagateContext(routed);
|
|
588
694
|
}
|
|
589
695
|
} else {
|
|
590
696
|
component.forEach(async (comp) => {
|
|
@@ -592,16 +698,18 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
592
698
|
// Handle observable component recursively
|
|
593
699
|
await createElement(parent, comp);
|
|
594
700
|
} else if (isElement(comp)) {
|
|
595
|
-
|
|
596
|
-
|
|
701
|
+
const routed = routeDomComponent(parent, comp);
|
|
702
|
+
onMount(parent, routed);
|
|
703
|
+
propagateContext(routed);
|
|
597
704
|
}
|
|
598
705
|
});
|
|
599
706
|
}
|
|
600
707
|
});
|
|
601
708
|
} else if (isElement(value)) {
|
|
602
709
|
// Handle direct Element emission
|
|
603
|
-
|
|
604
|
-
|
|
710
|
+
const routed = routeDomComponent(parent, value);
|
|
711
|
+
onMount(parent, routed);
|
|
712
|
+
propagateContext(routed);
|
|
605
713
|
} else if (Array.isArray(value)) {
|
|
606
714
|
// Handle array of elements (which can also be observables)
|
|
607
715
|
value.forEach(async (element) => {
|
|
@@ -609,8 +717,9 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
609
717
|
// Handle observable element recursively
|
|
610
718
|
await createElement(parent, element);
|
|
611
719
|
} else if (isElement(element)) {
|
|
612
|
-
|
|
613
|
-
|
|
720
|
+
const routed = routeDomComponent(parent, element);
|
|
721
|
+
onMount(parent, routed);
|
|
722
|
+
propagateContext(routed);
|
|
614
723
|
}
|
|
615
724
|
});
|
|
616
725
|
}
|
|
@@ -621,8 +730,9 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
621
730
|
// Store subscription for cleanup
|
|
622
731
|
parent.effectSubscriptions.push(subscription);
|
|
623
732
|
} else if (isElement(child)) {
|
|
624
|
-
|
|
625
|
-
|
|
733
|
+
const routed = routeDomComponent(parent, child);
|
|
734
|
+
onMount(parent, routed);
|
|
735
|
+
await propagateContext(routed);
|
|
626
736
|
}
|
|
627
737
|
}
|
|
628
738
|
|
package/src/engine/signal.ts
CHANGED
|
@@ -2,8 +2,10 @@ import {
|
|
|
2
2
|
Observable,
|
|
3
3
|
Subscription
|
|
4
4
|
} from "rxjs";
|
|
5
|
+
import { isSignal } from "@signe/reactive";
|
|
5
6
|
import type { Element } from "./reactive";
|
|
6
|
-
import { isElementFrozen } from "./reactive";
|
|
7
|
+
import { isElementFrozen, waitForDependencies } from "./reactive";
|
|
8
|
+
import { isPromise } from "./utils";
|
|
7
9
|
import { Tick } from "../directives/Scheduler";
|
|
8
10
|
import { Container } from "../components";
|
|
9
11
|
|
|
@@ -22,16 +24,16 @@ export let mountTracker: MountFunction | null = null;
|
|
|
22
24
|
* @param {(element: Element) => void} fn - The function to be called on mount.
|
|
23
25
|
* @example
|
|
24
26
|
* ```ts
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
+
* mount((el) => {
|
|
28
|
+
* console.log('mounted', el);
|
|
27
29
|
* });
|
|
28
30
|
* ```
|
|
29
31
|
* Unmount the component by returning a function:
|
|
30
32
|
* ```ts
|
|
31
|
-
|
|
32
|
-
*
|
|
33
|
+
* mount((el) => {
|
|
34
|
+
* console.log('mounted', el);
|
|
33
35
|
* return () => {
|
|
34
|
-
*
|
|
36
|
+
* console.log('unmounted', el);
|
|
35
37
|
* }
|
|
36
38
|
* });
|
|
37
39
|
* ```
|
|
@@ -45,8 +47,8 @@ export function mount(fn: (element: Element) => void) {
|
|
|
45
47
|
* @param {(tickValue: Tick, element: Element) => void} fn - The function to be called on each tick.
|
|
46
48
|
* @example
|
|
47
49
|
* ```ts
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
+
* tick((tickValue, el) => {
|
|
51
|
+
* console.log('tick', tickValue, el);
|
|
50
52
|
* });
|
|
51
53
|
* ```
|
|
52
54
|
*/
|
|
@@ -78,29 +80,29 @@ export function tick(fn: (tickValue: Tick, element: Element) => void) {
|
|
|
78
80
|
* @returns {ReturnType<C>}
|
|
79
81
|
* @example
|
|
80
82
|
* ```ts
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
* const el = h(MyComponent, {
|
|
84
|
+
* x: 100,
|
|
85
|
+
* y: 100,
|
|
86
|
+
* });
|
|
85
87
|
* ```
|
|
86
88
|
*
|
|
87
89
|
* with children:
|
|
88
90
|
* ```ts
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
* const el = h(MyComponent, {
|
|
92
|
+
* x: 100,
|
|
93
|
+
* y: 100,
|
|
94
|
+
* },
|
|
95
|
+
* h(MyChildComponent, {
|
|
96
|
+
* x: 50,
|
|
97
|
+
* y: 50,
|
|
98
|
+
* }),
|
|
97
99
|
* );
|
|
98
100
|
* ```
|
|
99
101
|
*/
|
|
100
|
-
|
|
102
|
+
function _h<C extends ComponentFunction<any>>(
|
|
101
103
|
componentFunction: C | Element,
|
|
102
104
|
props: Parameters<C>[0] = {} as Parameters<C>[0],
|
|
103
|
-
|
|
105
|
+
children: any[]
|
|
104
106
|
): ReturnType<C> {
|
|
105
107
|
const allSubscriptions = new Set<Subscription>();
|
|
106
108
|
const allMounts = new Set<MountFunction>();
|
|
@@ -124,7 +126,7 @@ export function h<C extends ComponentFunction<any>>(
|
|
|
124
126
|
component = componentFunction[0]
|
|
125
127
|
}
|
|
126
128
|
else {
|
|
127
|
-
component =
|
|
129
|
+
component = _h(Container, {}, componentFunction) as Element
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
else if ('tag' in componentFunction) {
|
|
@@ -167,3 +169,58 @@ export function h<C extends ComponentFunction<any>>(
|
|
|
167
169
|
|
|
168
170
|
return component as ReturnType<C>;
|
|
169
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Add tracking for subscriptions and mounts, then create an element from a component function.
|
|
175
|
+
* @template C
|
|
176
|
+
* @param {C} componentFunction - The component function to create an element from.
|
|
177
|
+
* @param {Parameters<C>[0]} [props={}] - The props to pass to the component function.
|
|
178
|
+
* @param {...any[]} children - The children elements of the component.
|
|
179
|
+
* @returns {ReturnType<C>}
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* const el = h(MyComponent, {
|
|
183
|
+
* x: 100,
|
|
184
|
+
* y: 100,
|
|
185
|
+
* });
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* with children:
|
|
189
|
+
* ```ts
|
|
190
|
+
* const el = h(MyComponent, {
|
|
191
|
+
* x: 100,
|
|
192
|
+
* y: 100,
|
|
193
|
+
* },
|
|
194
|
+
* h(MyChildComponent, {
|
|
195
|
+
* x: 50,
|
|
196
|
+
* y: 50,
|
|
197
|
+
* }),
|
|
198
|
+
* );
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function h<C extends ComponentFunction<any>>(
|
|
202
|
+
componentFunction: C | Element,
|
|
203
|
+
props: Parameters<C>[0] = {} as Parameters<C>[0],
|
|
204
|
+
...children: any[]
|
|
205
|
+
): ReturnType<C> {
|
|
206
|
+
if (props?.dependencies) {
|
|
207
|
+
const hasPromise = props.dependencies.some(isPromise);
|
|
208
|
+
if (!hasPromise) {
|
|
209
|
+
const allReady = props.dependencies.every(dep => {
|
|
210
|
+
if (isSignal(dep)) return dep() !== undefined;
|
|
211
|
+
return dep !== undefined;
|
|
212
|
+
});
|
|
213
|
+
if (allReady) {
|
|
214
|
+
return _h(componentFunction, props, children);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return new Observable(subscriber => {
|
|
219
|
+
waitForDependencies(props.dependencies).then(() => {
|
|
220
|
+
const el = _h(componentFunction, props, children);
|
|
221
|
+
subscriber.next(el);
|
|
222
|
+
});
|
|
223
|
+
}) as any;
|
|
224
|
+
}
|
|
225
|
+
return _h(componentFunction, props, children);
|
|
226
|
+
}
|
package/src/hooks/useFocus.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function useFocusChange(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Set up reactive effect
|
|
81
|
-
const
|
|
81
|
+
const effectResult = effect(() => {
|
|
82
82
|
const index = indexSignal();
|
|
83
83
|
const element = elementSignal();
|
|
84
84
|
callback(index, element);
|
|
@@ -86,9 +86,6 @@ export function useFocusChange(
|
|
|
86
86
|
|
|
87
87
|
// Return cleanup function
|
|
88
88
|
return () => {
|
|
89
|
-
|
|
90
|
-
subscription.unsubscribe();
|
|
91
|
-
}
|
|
89
|
+
effectResult.subscription?.unsubscribe();
|
|
92
90
|
};
|
|
93
91
|
}
|
|
94
|
-
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,8 @@ export { useProps, useDefineProps } from './hooks/useProps'
|
|
|
14
14
|
export { useFocusIndex, useFocusedElement, useFocusChange } from './hooks/useFocus'
|
|
15
15
|
export * from './utils/Ease'
|
|
16
16
|
export * from './utils/RadialGradient'
|
|
17
|
+
export * from './utils/tabindex'
|
|
17
18
|
export * from './components/DisplayObject'
|
|
18
19
|
export { isObservable } from 'rxjs'
|
|
19
20
|
export * as Utils from './engine/utils'
|
|
20
|
-
export * as Howl from 'howler'
|
|
21
|
+
export * as Howl from 'howler'
|