canvasengine 2.0.0-beta.3 → 2.0.0-beta.31
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/DebugRenderer-DrlzuIVv.js +172 -0
- package/dist/DebugRenderer-DrlzuIVv.js.map +1 -0
- package/dist/components/Button.d.ts +136 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Canvas.d.ts +18 -0
- package/dist/components/Canvas.d.ts.map +1 -0
- package/dist/components/Container.d.ts +80 -0
- package/dist/components/Container.d.ts.map +1 -0
- package/dist/components/DOMContainer.d.ts +77 -0
- package/dist/components/DOMContainer.d.ts.map +1 -0
- package/dist/components/DOMElement.d.ts +44 -0
- package/dist/components/DOMElement.d.ts.map +1 -0
- package/dist/components/DisplayObject.d.ts +82 -0
- package/dist/components/DisplayObject.d.ts.map +1 -0
- package/dist/components/Graphic.d.ts +65 -0
- package/dist/components/Graphic.d.ts.map +1 -0
- package/dist/components/Mesh.d.ts +202 -0
- package/dist/components/Mesh.d.ts.map +1 -0
- package/dist/components/NineSliceSprite.d.ts +17 -0
- package/dist/components/NineSliceSprite.d.ts.map +1 -0
- package/dist/components/ParticleEmitter.d.ts +5 -0
- package/dist/components/ParticleEmitter.d.ts.map +1 -0
- package/dist/components/Scene.d.ts +2 -0
- package/dist/components/Scene.d.ts.map +1 -0
- package/dist/components/Sprite.d.ts +174 -0
- package/dist/components/Sprite.d.ts.map +1 -0
- package/dist/components/Text.d.ts +21 -0
- package/dist/components/Text.d.ts.map +1 -0
- package/dist/components/TilingSprite.d.ts +18 -0
- package/dist/components/TilingSprite.d.ts.map +1 -0
- package/dist/components/Video.d.ts +15 -0
- package/dist/components/Video.d.ts.map +1 -0
- package/dist/components/Viewport.d.ts +106 -0
- package/dist/components/Viewport.d.ts.map +1 -0
- package/dist/components/index.d.ts +17 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/types/DisplayObject.d.ts +106 -0
- package/dist/components/types/DisplayObject.d.ts.map +1 -0
- package/dist/components/types/MouseEvent.d.ts +4 -0
- package/dist/components/types/MouseEvent.d.ts.map +1 -0
- package/dist/components/types/Spritesheet.d.ts +366 -0
- package/dist/components/types/Spritesheet.d.ts.map +1 -0
- package/dist/components/types/index.d.ts +5 -0
- package/dist/components/types/index.d.ts.map +1 -0
- package/dist/directives/Drag.d.ts +70 -0
- package/dist/directives/Drag.d.ts.map +1 -0
- package/dist/directives/KeyboardControls.d.ts +530 -0
- package/dist/directives/KeyboardControls.d.ts.map +1 -0
- package/dist/directives/Scheduler.d.ts +36 -0
- package/dist/directives/Scheduler.d.ts.map +1 -0
- package/dist/directives/Sound.d.ts +26 -0
- package/dist/directives/Sound.d.ts.map +1 -0
- package/dist/directives/Transition.d.ts +11 -0
- package/dist/directives/Transition.d.ts.map +1 -0
- package/dist/directives/ViewportCull.d.ts +12 -0
- package/dist/directives/ViewportCull.d.ts.map +1 -0
- package/dist/directives/ViewportFollow.d.ts +19 -0
- package/dist/directives/ViewportFollow.d.ts.map +1 -0
- package/dist/directives/index.d.ts +2 -0
- package/dist/directives/index.d.ts.map +1 -0
- package/dist/engine/animation.d.ts +59 -0
- package/dist/engine/animation.d.ts.map +1 -0
- package/dist/engine/bootstrap.d.ts +16 -0
- package/dist/engine/bootstrap.d.ts.map +1 -0
- package/dist/engine/directive.d.ts +14 -0
- package/dist/engine/directive.d.ts.map +1 -0
- package/dist/engine/reactive.d.ts +95 -0
- package/dist/engine/reactive.d.ts.map +1 -0
- package/dist/engine/signal.d.ts +72 -0
- package/dist/engine/signal.d.ts.map +1 -0
- package/dist/engine/trigger.d.ts +51 -0
- package/dist/engine/trigger.d.ts.map +1 -0
- package/dist/engine/utils.d.ts +90 -0
- package/dist/engine/utils.d.ts.map +1 -0
- package/dist/hooks/addContext.d.ts +2 -0
- package/dist/hooks/addContext.d.ts.map +1 -0
- package/dist/hooks/useProps.d.ts +42 -0
- package/dist/hooks/useProps.d.ts.map +1 -0
- package/dist/hooks/useRef.d.ts +5 -0
- package/dist/hooks/useRef.d.ts.map +1 -0
- package/dist/index-DNDNQN-q.js +11088 -0
- package/dist/index-DNDNQN-q.js.map +1 -0
- package/dist/index.d.ts +15 -919
- package/dist/index.d.ts.map +1 -0
- package/dist/index.global.js +29 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +63 -2950
- package/dist/index.js.map +1 -1
- package/dist/utils/Ease.d.ts +17 -0
- package/dist/utils/Ease.d.ts.map +1 -0
- package/dist/utils/RadialGradient.d.ts +58 -0
- package/dist/utils/RadialGradient.d.ts.map +1 -0
- package/dist/utils/functions.d.ts +2 -0
- package/dist/utils/functions.d.ts.map +1 -0
- package/index.d.ts +4 -0
- package/package.json +12 -7
- package/src/components/Button.ts +269 -0
- package/src/components/Canvas.ts +53 -45
- package/src/components/Container.ts +2 -2
- package/src/components/DOMContainer.ts +123 -0
- package/src/components/DOMElement.ts +421 -0
- package/src/components/DisplayObject.ts +283 -190
- package/src/components/Graphic.ts +200 -34
- package/src/components/Mesh.ts +222 -0
- package/src/components/NineSliceSprite.ts +4 -1
- package/src/components/ParticleEmitter.ts +12 -8
- package/src/components/Sprite.ts +92 -22
- package/src/components/Text.ts +34 -14
- package/src/components/Video.ts +110 -0
- package/src/components/Viewport.ts +59 -43
- package/src/components/index.ts +7 -2
- package/src/components/types/DisplayObject.ts +30 -0
- package/src/directives/Drag.ts +357 -52
- package/src/directives/KeyboardControls.ts +3 -1
- package/src/directives/Sound.ts +94 -31
- package/src/directives/ViewportFollow.ts +35 -7
- package/src/engine/animation.ts +41 -5
- package/src/engine/bootstrap.ts +23 -3
- package/src/engine/directive.ts +2 -2
- package/src/engine/reactive.ts +542 -170
- package/src/engine/signal.ts +22 -2
- package/src/engine/trigger.ts +65 -9
- package/src/engine/utils.ts +97 -9
- package/src/hooks/useProps.ts +1 -1
- package/src/index.ts +4 -1
- package/src/utils/RadialGradient.ts +29 -0
- package/src/utils/functions.ts +7 -0
- package/testing/index.ts +12 -0
- package/tsconfig.json +17 -0
- package/vite.config.ts +39 -0
package/src/engine/reactive.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Signal, WritableArraySignal, isSignal } from "@signe/reactive";
|
|
1
|
+
import { ArrayChange, ObjectChange, Signal, WritableArraySignal, WritableObjectSignal, isComputed, isSignal, signal, computed } from "@signe/reactive";
|
|
2
2
|
import {
|
|
3
3
|
Observable,
|
|
4
4
|
Subject,
|
|
@@ -7,7 +7,13 @@ import {
|
|
|
7
7
|
from,
|
|
8
8
|
map,
|
|
9
9
|
of,
|
|
10
|
+
share,
|
|
10
11
|
switchMap,
|
|
12
|
+
debounceTime,
|
|
13
|
+
distinctUntilChanged,
|
|
14
|
+
bufferTime,
|
|
15
|
+
filter,
|
|
16
|
+
throttleTime,
|
|
11
17
|
} from "rxjs";
|
|
12
18
|
import { ComponentInstance } from "../components/DisplayObject";
|
|
13
19
|
import { Directive, applyDirective } from "./directive";
|
|
@@ -17,18 +23,6 @@ export interface Props {
|
|
|
17
23
|
[key: string]: any;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
export type ArrayChange<T> = {
|
|
21
|
-
type: "add" | "remove" | "update" | "init" | "reset";
|
|
22
|
-
index?: number;
|
|
23
|
-
items: T[];
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
type ElementObservable<T> = Observable<
|
|
27
|
-
ArrayChange<T> & {
|
|
28
|
-
value: Element | Element[];
|
|
29
|
-
}
|
|
30
|
-
>;
|
|
31
|
-
|
|
32
26
|
type NestedSignalObjects = {
|
|
33
27
|
[Key in string]: NestedSignalObjects | Signal<any>;
|
|
34
28
|
};
|
|
@@ -53,10 +47,13 @@ export interface Element<T = ComponentInstance> {
|
|
|
53
47
|
allElements: Subject<void>;
|
|
54
48
|
}
|
|
55
49
|
|
|
56
|
-
type
|
|
50
|
+
type FlowResult = {
|
|
57
51
|
elements: Element[];
|
|
58
52
|
prev?: Element;
|
|
59
|
-
|
|
53
|
+
fullElements?: Element[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type FlowObservable = Observable<FlowResult>;
|
|
60
57
|
|
|
61
58
|
const components: { [key: string]: any } = {};
|
|
62
59
|
|
|
@@ -92,13 +89,26 @@ function destroyElement(element: Element | Element[]) {
|
|
|
92
89
|
if (!element) {
|
|
93
90
|
return;
|
|
94
91
|
}
|
|
95
|
-
element.
|
|
96
|
-
|
|
92
|
+
if (element.props?.children) {
|
|
93
|
+
for (let child of element.props.children) {
|
|
94
|
+
destroyElement(child)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
97
|
for (let name in element.directives) {
|
|
98
|
-
element.directives[name].onDestroy?.();
|
|
98
|
+
element.directives[name].onDestroy?.(element);
|
|
99
|
+
}
|
|
100
|
+
if (element.componentInstance && element.componentInstance.onDestroy) {
|
|
101
|
+
element.componentInstance.onDestroy(element.parent as any, () => {
|
|
102
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
103
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
104
|
+
element.effectUnmounts?.forEach((fn) => fn?.());
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
// If componentInstance is undefined or doesn't have onDestroy, still clean up subscriptions
|
|
108
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
109
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
110
|
+
element.effectUnmounts?.forEach((fn) => fn?.());
|
|
99
111
|
}
|
|
100
|
-
element.componentInstance.onDestroy?.(element.parent as any);
|
|
101
|
-
element.effectUnmounts.forEach((fn) => fn?.());
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
/**
|
|
@@ -155,7 +165,7 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
155
165
|
_value.observable.subscribe((value) => {
|
|
156
166
|
_set(path, key, value);
|
|
157
167
|
if (element.directives[key]) {
|
|
158
|
-
element.directives[key].onUpdate?.(value);
|
|
168
|
+
element.directives[key].onUpdate?.(value, element);
|
|
159
169
|
}
|
|
160
170
|
if (key == "tick") {
|
|
161
171
|
return
|
|
@@ -182,9 +192,24 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
instance.onInit?.(element.props);
|
|
185
|
-
instance.onUpdate?.(element.props);
|
|
186
195
|
|
|
187
|
-
const
|
|
196
|
+
const elementsListen = new Subject<any>()
|
|
197
|
+
|
|
198
|
+
if (props?.isRoot) {
|
|
199
|
+
element.allElements = elementsListen
|
|
200
|
+
element.props.context.rootElement = element;
|
|
201
|
+
element.componentInstance.onMount?.(element);
|
|
202
|
+
propagateContext(element);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (props) {
|
|
206
|
+
for (let key in props) {
|
|
207
|
+
const directive = applyDirective(element, key);
|
|
208
|
+
if (directive) element.directives[key] = directive;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function onMount(parent: Element, element: Element, index?: number) {
|
|
188
213
|
element.props.context = parent.props.context;
|
|
189
214
|
element.parent = parent;
|
|
190
215
|
element.componentInstance.onMount?.(element, index);
|
|
@@ -196,68 +221,151 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
196
221
|
});
|
|
197
222
|
};
|
|
198
223
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (!element.props.children) {
|
|
205
|
-
return;
|
|
224
|
+
async function propagateContext(element) {
|
|
225
|
+
if (element.props.attach) {
|
|
226
|
+
const isReactiveAttach = isSignal(element.propObservables?.attach)
|
|
227
|
+
if (!isReactiveAttach) {
|
|
228
|
+
element.props.children.push(element.props.attach)
|
|
206
229
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
230
|
+
else {
|
|
231
|
+
await new Promise((resolve) => {
|
|
232
|
+
let lastElement = null
|
|
233
|
+
element.propSubscriptions.push(element.propObservables.attach.observable.subscribe(async (args) => {
|
|
234
|
+
const value = args?.value ?? args
|
|
235
|
+
if (!value) {
|
|
236
|
+
throw new Error(`attach in ${element.tag} is undefined or null, add a component`)
|
|
237
|
+
}
|
|
238
|
+
if (lastElement) {
|
|
239
|
+
destroyElement(lastElement)
|
|
240
|
+
}
|
|
241
|
+
lastElement = value
|
|
242
|
+
await createElement(element, value)
|
|
243
|
+
resolve(undefined)
|
|
244
|
+
}))
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!element.props.children) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
for (let child of element.props.children) {
|
|
252
|
+
if (!child) continue;
|
|
253
|
+
await createElement(element, child)
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Creates and mounts a child element to a parent element.
|
|
259
|
+
* Handles different types of children: Elements, Promises resolving to Elements, and Observables.
|
|
260
|
+
*
|
|
261
|
+
* @description This function is designed to handle reactive child components that can be:
|
|
262
|
+
* - Direct Element instances
|
|
263
|
+
* - Promises that resolve to Elements (for async components)
|
|
264
|
+
* - Observables that emit Elements, arrays of Elements, or FlowObservable results
|
|
265
|
+
* - Nested observables within arrays or FlowObservable results (handled recursively)
|
|
266
|
+
*
|
|
267
|
+
* For Observables, it subscribes to the stream and automatically mounts/unmounts elements
|
|
268
|
+
* as they are emitted. The function handles nested observables recursively, ensuring that
|
|
269
|
+
* observables within arrays or FlowObservable results are also properly subscribed to.
|
|
270
|
+
* All subscriptions are stored in the parent's effectSubscriptions for automatic cleanup.
|
|
271
|
+
*
|
|
272
|
+
* @param {Element} parent - The parent element to mount the child to
|
|
273
|
+
* @param {Element | Observable<any> | Promise<Element>} child - The child to create and mount
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* // Direct element
|
|
278
|
+
* await createElement(parent, childElement);
|
|
279
|
+
*
|
|
280
|
+
* // Observable of elements (from cond, loop, etc.)
|
|
281
|
+
* await createElement(parent, cond(signal(visible), () => h(Container)));
|
|
282
|
+
*
|
|
283
|
+
* // Observable that emits arrays containing other observables
|
|
284
|
+
* await createElement(parent, observableOfObservables);
|
|
285
|
+
*
|
|
286
|
+
* // Promise resolving to element
|
|
287
|
+
* await createElement(parent, import('./MyComponent').then(mod => h(mod.default)));
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
async function createElement(parent: Element, child: Element | Observable<any> | Promise<Element>) {
|
|
291
|
+
if (isPromise(child)) {
|
|
292
|
+
child = await child;
|
|
293
|
+
}
|
|
294
|
+
if (child instanceof Observable) {
|
|
295
|
+
// Subscribe to the observable and handle the emitted values
|
|
296
|
+
const subscription = child.subscribe(
|
|
297
|
+
(value: any) => {
|
|
298
|
+
// Handle different types of observable emissions
|
|
299
|
+
if (value && typeof value === 'object' && 'elements' in value) {
|
|
300
|
+
// Handle FlowObservable result (from loop, cond, etc.)
|
|
301
|
+
const {
|
|
215
302
|
elements: comp,
|
|
216
303
|
prev,
|
|
217
304
|
}: {
|
|
218
305
|
elements: Element[];
|
|
219
306
|
prev?: Element;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
307
|
+
} = value;
|
|
308
|
+
|
|
309
|
+
const components = comp.filter((c) => c !== null);
|
|
310
|
+
if (prev) {
|
|
311
|
+
components.forEach(async (c) => {
|
|
312
|
+
const index = parent.props.children.indexOf(prev.props.key);
|
|
313
|
+
if (c instanceof Observable) {
|
|
314
|
+
// Handle observable component recursively
|
|
315
|
+
await createElement(parent, c);
|
|
316
|
+
} else if (isElement(c)) {
|
|
317
|
+
onMount(parent, c, index + 1);
|
|
318
|
+
propagateContext(c);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
components.forEach(async (component) => {
|
|
324
|
+
if (!Array.isArray(component)) {
|
|
325
|
+
if (component instanceof Observable) {
|
|
326
|
+
// Handle observable component recursively
|
|
327
|
+
await createElement(parent, component);
|
|
328
|
+
} else if (isElement(component)) {
|
|
329
|
+
onMount(parent, component);
|
|
330
|
+
propagateContext(component);
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
component.forEach(async (comp) => {
|
|
334
|
+
if (comp instanceof Observable) {
|
|
335
|
+
// Handle observable component recursively
|
|
336
|
+
await createElement(parent, comp);
|
|
337
|
+
} else if (isElement(comp)) {
|
|
338
|
+
onMount(parent, comp);
|
|
339
|
+
propagateContext(comp);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
} else if (isElement(value)) {
|
|
345
|
+
// Handle direct Element emission
|
|
346
|
+
onMount(parent, value);
|
|
347
|
+
propagateContext(value);
|
|
348
|
+
} else if (Array.isArray(value)) {
|
|
349
|
+
// Handle array of elements (which can also be observables)
|
|
350
|
+
value.forEach(async (element) => {
|
|
351
|
+
if (element instanceof Observable) {
|
|
352
|
+
// Handle observable element recursively
|
|
353
|
+
await createElement(parent, element);
|
|
354
|
+
} else if (isElement(element)) {
|
|
355
|
+
onMount(parent, element);
|
|
356
|
+
propagateContext(element);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
elementsListen.next(undefined);
|
|
248
361
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (props) {
|
|
258
|
-
for (let key in props) {
|
|
259
|
-
const directive = applyDirective(element, key);
|
|
260
|
-
if (directive) element.directives[key] = directive;
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Store subscription for cleanup
|
|
365
|
+
parent.effectSubscriptions.push(subscription);
|
|
366
|
+
} else if (isElement(child)) {
|
|
367
|
+
onMount(parent, child);
|
|
368
|
+
await propagateContext(child);
|
|
261
369
|
}
|
|
262
370
|
}
|
|
263
371
|
|
|
@@ -266,114 +374,378 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
266
374
|
}
|
|
267
375
|
|
|
268
376
|
/**
|
|
269
|
-
* Observes a BehaviorSubject containing an array of items and dynamically creates child elements for each item.
|
|
377
|
+
* Observes a BehaviorSubject containing an array or object of items and dynamically creates child elements for each item.
|
|
270
378
|
*
|
|
271
|
-
* @param {
|
|
379
|
+
* @param {WritableArraySignal<T> | WritableObjectSignal<T>} itemsSubject - A signal that emits an array or object of items.
|
|
272
380
|
* @param {Function} createElementFn - A function that takes an item and returns an element representation.
|
|
273
381
|
* @returns {Observable} An observable that emits the list of created child elements.
|
|
274
382
|
*/
|
|
275
|
-
export function loop<T
|
|
276
|
-
itemsSubject:
|
|
277
|
-
createElementFn: (item:
|
|
383
|
+
export function loop<T>(
|
|
384
|
+
itemsSubject: any,
|
|
385
|
+
createElementFn: (item: T, index: number | string) => Element | null
|
|
278
386
|
): FlowObservable {
|
|
279
|
-
let elements: Element[] = [];
|
|
280
387
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
};
|
|
388
|
+
if (isComputed(itemsSubject) && itemsSubject.dependencies.size == 0) {
|
|
389
|
+
itemsSubject = signal(itemsSubject());
|
|
390
|
+
}
|
|
391
|
+
else if (!isSignal(itemsSubject)) {
|
|
392
|
+
itemsSubject = signal(itemsSubject);
|
|
393
|
+
}
|
|
288
394
|
|
|
289
395
|
return defer(() => {
|
|
290
|
-
let
|
|
291
|
-
let
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
396
|
+
let elements: Element[] = [];
|
|
397
|
+
let elementMap = new Map<string | number, Element>();
|
|
398
|
+
let isFirstSubscription = true;
|
|
399
|
+
|
|
400
|
+
const isArraySignal = (signal: any): signal is WritableArraySignal<T[]> =>
|
|
401
|
+
Array.isArray(signal());
|
|
402
|
+
|
|
403
|
+
return new Observable<FlowResult>(subscriber => {
|
|
404
|
+
const subscription = isArraySignal(itemsSubject)
|
|
405
|
+
? itemsSubject.observable.subscribe(change => {
|
|
406
|
+
if (isFirstSubscription) {
|
|
407
|
+
isFirstSubscription = false;
|
|
408
|
+
elements.forEach(el => el.destroy());
|
|
409
|
+
elements = [];
|
|
410
|
+
elementMap.clear();
|
|
411
|
+
|
|
412
|
+
const items = itemsSubject();
|
|
413
|
+
if (items) {
|
|
414
|
+
items.forEach((item, index) => {
|
|
415
|
+
const element = createElementFn(item, index);
|
|
416
|
+
if (element) {
|
|
417
|
+
elements.push(element);
|
|
418
|
+
elementMap.set(index, element);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
subscriber.next({
|
|
423
|
+
elements: [...elements]
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (change.type === 'init' || change.type === 'reset') {
|
|
429
|
+
elements.forEach(el => destroyElement(el));
|
|
430
|
+
elements = [];
|
|
431
|
+
elementMap.clear();
|
|
432
|
+
|
|
433
|
+
const items = itemsSubject();
|
|
434
|
+
if (items) {
|
|
435
|
+
items.forEach((item, index) => {
|
|
436
|
+
const element = createElementFn(item, index);
|
|
437
|
+
if (element) {
|
|
438
|
+
elements.push(element);
|
|
439
|
+
elementMap.set(index, element);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
} else if (change.type === 'add' && change.index !== undefined) {
|
|
444
|
+
const newElements = change.items.map((item, i) => {
|
|
445
|
+
const element = createElementFn(item as T, change.index! + i);
|
|
446
|
+
if (element) {
|
|
447
|
+
elementMap.set(change.index! + i, element);
|
|
448
|
+
}
|
|
449
|
+
return element;
|
|
450
|
+
}).filter((el): el is Element => el !== null);
|
|
451
|
+
|
|
452
|
+
elements.splice(change.index, 0, ...newElements);
|
|
453
|
+
} else if (change.type === 'remove' && change.index !== undefined) {
|
|
454
|
+
const removed = elements.splice(change.index, 1);
|
|
455
|
+
removed.forEach(el => {
|
|
456
|
+
destroyElement(el)
|
|
457
|
+
elementMap.delete(change.index!);
|
|
458
|
+
});
|
|
459
|
+
} else if (change.type === 'update' && change.index !== undefined && change.items.length === 1) {
|
|
460
|
+
const index = change.index;
|
|
461
|
+
const newItem = change.items[0];
|
|
462
|
+
|
|
463
|
+
// Check if the previous item at this index was effectively undefined or non-existent
|
|
464
|
+
if (index >= elements.length || elements[index] === undefined || !elementMap.has(index)) {
|
|
465
|
+
// Treat as add operation
|
|
466
|
+
const newElement = createElementFn(newItem as T, index);
|
|
467
|
+
if (newElement) {
|
|
468
|
+
elements.splice(index, 0, newElement); // Insert at the correct index
|
|
469
|
+
elementMap.set(index, newElement);
|
|
470
|
+
// Adjust indices in elementMap for subsequent elements might be needed if map relied on exact indices
|
|
471
|
+
// This simple implementation assumes keys are stable or createElementFn handles context correctly
|
|
472
|
+
} else {
|
|
473
|
+
console.warn(`Element creation returned null for index ${index} during add-like update.`);
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
// Treat as a standard update operation
|
|
477
|
+
const oldElement = elements[index];
|
|
478
|
+
destroyElement(oldElement)
|
|
479
|
+
const newElement = createElementFn(newItem as T, index);
|
|
480
|
+
if (newElement) {
|
|
481
|
+
elements[index] = newElement;
|
|
482
|
+
elementMap.set(index, newElement);
|
|
483
|
+
} else {
|
|
484
|
+
// Handle case where new element creation returns null
|
|
485
|
+
elements.splice(index, 1);
|
|
486
|
+
elementMap.delete(index);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
subscriber.next({
|
|
492
|
+
elements: [...elements] // Create a new array to ensure change detection
|
|
313
493
|
});
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
494
|
+
})
|
|
495
|
+
: (itemsSubject as WritableObjectSignal<T>).observable.subscribe(change => {
|
|
496
|
+
const key = change.key as string | number
|
|
497
|
+
if (isFirstSubscription) {
|
|
498
|
+
isFirstSubscription = false;
|
|
499
|
+
elements.forEach(el => destroyElement(el));
|
|
500
|
+
elements = [];
|
|
501
|
+
elementMap.clear();
|
|
502
|
+
|
|
503
|
+
const items = (itemsSubject as WritableObjectSignal<T>)();
|
|
504
|
+
if (items) {
|
|
505
|
+
Object.entries(items).forEach(([key, value]) => {
|
|
506
|
+
const element = createElementFn(value, key);
|
|
507
|
+
if (element) {
|
|
508
|
+
elements.push(element);
|
|
509
|
+
elementMap.set(key, element);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
subscriber.next({
|
|
514
|
+
elements: [...elements]
|
|
515
|
+
});
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (change.type === 'init' || change.type === 'reset') {
|
|
520
|
+
elements.forEach(el => destroyElement(el));
|
|
521
|
+
elements = [];
|
|
522
|
+
elementMap.clear();
|
|
523
|
+
|
|
524
|
+
const items = (itemsSubject as WritableObjectSignal<T>)();
|
|
525
|
+
if (items) {
|
|
526
|
+
Object.entries(items).forEach(([key, value]) => {
|
|
527
|
+
const element = createElementFn(value, key);
|
|
528
|
+
if (element) {
|
|
529
|
+
elements.push(element);
|
|
530
|
+
elementMap.set(key, element);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
} else if (change.type === 'add' && change.key && change.value !== undefined) {
|
|
535
|
+
const element = createElementFn(change.value as T, key);
|
|
536
|
+
if (element) {
|
|
537
|
+
elements.push(element);
|
|
538
|
+
elementMap.set(key, element);
|
|
539
|
+
}
|
|
540
|
+
} else if (change.type === 'remove' && change.key) {
|
|
541
|
+
const index = elements.findIndex(el => elementMap.get(key) === el);
|
|
542
|
+
if (index !== -1) {
|
|
543
|
+
const [removed] = elements.splice(index, 1);
|
|
544
|
+
destroyElement(removed)
|
|
545
|
+
elementMap.delete(key);
|
|
546
|
+
}
|
|
547
|
+
} else if (change.type === 'update' && change.key && change.value !== undefined) {
|
|
548
|
+
const index = elements.findIndex(el => elementMap.get(key) === el);
|
|
549
|
+
if (index !== -1) {
|
|
550
|
+
const oldElement = elements[index];
|
|
551
|
+
destroyElement(oldElement)
|
|
552
|
+
const newElement = createElementFn(change.value as T, key);
|
|
553
|
+
if (newElement) {
|
|
554
|
+
elements[index] = newElement;
|
|
555
|
+
elementMap.set(key, newElement);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
subscriber.next({
|
|
561
|
+
elements: [...elements] // Create a new array to ensure change detection
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
return subscription;
|
|
566
|
+
});
|
|
343
567
|
});
|
|
344
568
|
}
|
|
345
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Conditionally creates and destroys elements based on condition signals with support for else if and else.
|
|
572
|
+
*
|
|
573
|
+
* @description This function creates conditional rendering with support for multiple conditions (if/else if/else pattern).
|
|
574
|
+
* It evaluates conditions in order and renders the first matching condition's element.
|
|
575
|
+
* The function maintains full reactivity with signals and ensures proper cleanup of elements.
|
|
576
|
+
*
|
|
577
|
+
* @param {Signal<boolean> | boolean | (() => boolean)} condition - A signal, boolean, or function that determines whether to create an element.
|
|
578
|
+
* @param {Function} createElementFn - A function that returns an element or a promise that resolves to an element.
|
|
579
|
+
* @param {...Array} additionalConditions - Additional conditions for else if and else cases.
|
|
580
|
+
* Can be:
|
|
581
|
+
* - A function for else case: `() => Element | Promise<Element>`
|
|
582
|
+
* - An array for else if case: `[Signal<boolean> | boolean | (() => boolean), () => Element | Promise<Element>]`
|
|
583
|
+
* @returns {Observable} An observable that emits the created element based on the matching condition.
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```typescript
|
|
587
|
+
* // Simple if/else
|
|
588
|
+
* cond(
|
|
589
|
+
* signal(isVisible),
|
|
590
|
+
* () => h(Container),
|
|
591
|
+
* () => h(Text, { text: 'Hidden' }) // else
|
|
592
|
+
* );
|
|
593
|
+
*
|
|
594
|
+
* // Multiple else if + else
|
|
595
|
+
* cond(
|
|
596
|
+
* signal(status === 'loading'),
|
|
597
|
+
* () => h(LoadingSpinner),
|
|
598
|
+
* [signal(status === 'error'), () => h(ErrorMessage)], // else if
|
|
599
|
+
* [signal(status === 'success'), () => h(SuccessMessage)], // else if
|
|
600
|
+
* () => h(DefaultMessage) // else
|
|
601
|
+
* );
|
|
602
|
+
* ```
|
|
603
|
+
*/
|
|
346
604
|
export function cond(
|
|
347
|
-
condition: Signal,
|
|
348
|
-
createElementFn: () => Element | Promise<Element
|
|
605
|
+
condition: Signal<boolean> | boolean | (() => boolean),
|
|
606
|
+
createElementFn: () => Element | Promise<Element>,
|
|
607
|
+
...additionalConditions: Array<
|
|
608
|
+
| (() => Element | Promise<Element>) // else final
|
|
609
|
+
| [Signal<boolean> | boolean | (() => boolean), () => Element | Promise<Element>] // else if
|
|
610
|
+
>
|
|
349
611
|
): FlowObservable {
|
|
350
|
-
let
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
612
|
+
let currentElement: Element | null = null;
|
|
613
|
+
let currentConditionIndex = -1;
|
|
614
|
+
|
|
615
|
+
// Parse additional conditions
|
|
616
|
+
const elseIfConditions: Array<{
|
|
617
|
+
condition: Signal<boolean>;
|
|
618
|
+
elementFn: () => Element | Promise<Element>;
|
|
619
|
+
}> = [];
|
|
620
|
+
let elseElementFn: (() => Element | Promise<Element>) | null = null;
|
|
621
|
+
|
|
622
|
+
// Convert function conditions to computed signals
|
|
623
|
+
const convertConditionToSignal = (cond: Signal<boolean> | boolean | (() => boolean)): Signal<boolean> => {
|
|
624
|
+
if (isSignal(cond)) {
|
|
625
|
+
return cond as Signal<boolean>;
|
|
626
|
+
} else if (typeof cond === 'function') {
|
|
627
|
+
return computed(cond as () => boolean);
|
|
628
|
+
} else {
|
|
629
|
+
return signal(cond as boolean);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// Process additional conditions
|
|
634
|
+
for (const param of additionalConditions) {
|
|
635
|
+
if (Array.isArray(param)) {
|
|
636
|
+
// else if case: [condition, elementFn]
|
|
637
|
+
elseIfConditions.push({
|
|
638
|
+
condition: convertConditionToSignal(param[0]),
|
|
639
|
+
elementFn: param[1],
|
|
640
|
+
});
|
|
641
|
+
} else if (typeof param === 'function') {
|
|
642
|
+
// else case: elementFn (should be the last one)
|
|
643
|
+
elseElementFn = param;
|
|
644
|
+
break; // Stop processing after else
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Collect all conditions with their element functions
|
|
649
|
+
const allConditions = [
|
|
650
|
+
{ condition: convertConditionToSignal(condition), elementFn: createElementFn },
|
|
651
|
+
...elseIfConditions,
|
|
652
|
+
];
|
|
653
|
+
|
|
654
|
+
// All conditions are now signals, so we always use the reactive path
|
|
655
|
+
return new Observable<{elements: Element[], type?: "init" | "remove"}>(subscriber => {
|
|
656
|
+
const subscriptions: Subscription[] = [];
|
|
657
|
+
|
|
658
|
+
const evaluateConditions = () => {
|
|
659
|
+
// Find the first matching condition
|
|
660
|
+
let matchingIndex = -1;
|
|
661
|
+
for (let i = 0; i < allConditions.length; i++) {
|
|
662
|
+
const condition = allConditions[i].condition;
|
|
663
|
+
const conditionValue = condition();
|
|
664
|
+
|
|
665
|
+
if (conditionValue) {
|
|
666
|
+
matchingIndex = i;
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// If no condition matches and we have an else, use else
|
|
672
|
+
const shouldUseElse = matchingIndex === -1 && elseElementFn;
|
|
673
|
+
const newConditionIndex = shouldUseElse ? -2 : matchingIndex; // -2 for else, -1 for nothing
|
|
674
|
+
|
|
675
|
+
// Only update if the condition changed
|
|
676
|
+
if (newConditionIndex !== currentConditionIndex) {
|
|
677
|
+
// Destroy current element if it exists
|
|
678
|
+
if (currentElement) {
|
|
679
|
+
destroyElement(currentElement);
|
|
680
|
+
currentElement = null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
currentConditionIndex = newConditionIndex;
|
|
684
|
+
|
|
685
|
+
if (shouldUseElse) {
|
|
686
|
+
// Render else element
|
|
687
|
+
let _el = elseElementFn!();
|
|
688
|
+
if (isPromise(_el)) {
|
|
689
|
+
from(_el as Promise<Element>).subscribe(el => {
|
|
690
|
+
currentElement = el;
|
|
691
|
+
subscriber.next({
|
|
360
692
|
type: "init",
|
|
361
693
|
elements: [el],
|
|
362
|
-
};
|
|
363
|
-
})
|
|
364
|
-
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
} else {
|
|
697
|
+
currentElement = _el as Element;
|
|
698
|
+
subscriber.next({
|
|
699
|
+
type: "init",
|
|
700
|
+
elements: [currentElement],
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
} else if (matchingIndex >= 0) {
|
|
704
|
+
// Render matching condition element
|
|
705
|
+
let _el = allConditions[matchingIndex].elementFn();
|
|
706
|
+
if (isPromise(_el)) {
|
|
707
|
+
from(_el as Promise<Element>).subscribe(el => {
|
|
708
|
+
currentElement = el;
|
|
709
|
+
subscriber.next({
|
|
710
|
+
type: "init",
|
|
711
|
+
elements: [el],
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
} else {
|
|
715
|
+
currentElement = _el as Element;
|
|
716
|
+
subscriber.next({
|
|
717
|
+
type: "init",
|
|
718
|
+
elements: [currentElement],
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
// No matching condition and no else
|
|
723
|
+
subscriber.next({
|
|
724
|
+
elements: [],
|
|
725
|
+
});
|
|
365
726
|
}
|
|
366
|
-
element = _el as Element;
|
|
367
|
-
return of({
|
|
368
|
-
type: "init",
|
|
369
|
-
elements: [element],
|
|
370
|
-
});
|
|
371
|
-
} else if (element) {
|
|
372
|
-
destroyElement(element);
|
|
373
727
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
})
|
|
378
|
-
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// Subscribe to all signal conditions
|
|
731
|
+
allConditions.forEach(({ condition }) => {
|
|
732
|
+
const signalCondition = condition as WritableObjectSignal<boolean>;
|
|
733
|
+
subscriptions.push(
|
|
734
|
+
signalCondition.observable.subscribe(() => {
|
|
735
|
+
evaluateConditions();
|
|
736
|
+
})
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// Initial evaluation
|
|
741
|
+
evaluateConditions();
|
|
742
|
+
|
|
743
|
+
// Return cleanup function
|
|
744
|
+
return () => {
|
|
745
|
+
subscriptions.forEach(sub => sub.unsubscribe());
|
|
746
|
+
if (currentElement) {
|
|
747
|
+
destroyElement(currentElement);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}).pipe(share());
|
|
379
751
|
}
|