canvasengine 1.3.0 → 2.0.0-beta.10
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 +1137 -0
- package/dist/index.js +3222 -0
- package/dist/index.js.map +1 -0
- package/index.d.ts +4 -0
- package/package.json +43 -17
- package/src/components/Canvas.ts +134 -0
- package/src/components/Container.ts +46 -0
- package/src/components/DisplayObject.ts +458 -0
- package/src/components/Graphic.ts +147 -0
- package/src/components/NineSliceSprite.ts +46 -0
- package/src/components/ParticleEmitter.ts +39 -0
- package/src/components/Scene.ts +6 -0
- package/src/components/Sprite.ts +514 -0
- package/src/components/Text.ts +145 -0
- package/src/components/TilingSprite.ts +39 -0
- package/src/components/Video.ts +110 -0
- package/src/components/Viewport.ts +159 -0
- package/src/components/index.ts +12 -0
- package/src/components/types/DisplayObject.ts +70 -0
- package/src/components/types/MouseEvent.ts +3 -0
- package/src/components/types/Spritesheet.ts +389 -0
- package/src/components/types/index.ts +4 -0
- package/src/directives/Drag.ts +84 -0
- package/src/directives/KeyboardControls.ts +922 -0
- package/src/directives/Scheduler.ts +101 -0
- package/src/directives/Sound.ts +91 -0
- package/src/directives/Transition.ts +45 -0
- package/src/directives/ViewportCull.ts +40 -0
- package/src/directives/ViewportFollow.ts +26 -0
- package/src/directives/index.ts +7 -0
- package/src/engine/animation.ts +149 -0
- package/src/engine/bootstrap.ts +19 -0
- package/src/engine/directive.ts +23 -0
- package/src/engine/reactive.ts +480 -0
- package/src/engine/signal.ts +138 -0
- package/src/engine/trigger.ts +96 -0
- package/src/engine/utils.ts +211 -0
- package/src/hooks/addContext.ts +6 -0
- package/src/hooks/useProps.ts +155 -0
- package/src/hooks/useRef.ts +21 -0
- package/src/index.ts +15 -0
- package/src/utils/Ease.ts +33 -0
- package/src/utils/RadialGradient.ts +115 -0
- package/testing/index.ts +11 -0
- package/.gitattributes +0 -22
- package/.npmignore +0 -163
- package/canvasengine-1.3.0.all.min.js +0 -21
- package/canvasengine.js +0 -5802
- package/core/DB.js +0 -24
- package/core/ModelServer.js +0 -348
- package/core/Users.js +0 -190
- package/core/engine-common.js +0 -952
- package/doc/cocoonjs.md +0 -36
- package/doc/doc-lang.yml +0 -43
- package/doc/doc-router.yml +0 -14
- package/doc/doc-tuto.yml +0 -9
- package/doc/doc.yml +0 -39
- package/doc/element.md +0 -37
- package/doc/entity.md +0 -90
- package/doc/extend.md +0 -47
- package/doc/get_started.md +0 -19
- package/doc/images/entity.png +0 -0
- package/doc/multitouch.md +0 -58
- package/doc/nodejs.md +0 -142
- package/doc/scene.md +0 -44
- package/doc/text.md +0 -156
- package/examples/server/client.html +0 -31
- package/examples/server/server.js +0 -16
- package/examples/tiled_server/client.html +0 -52
- package/examples/tiled_server/images/tiles_spritesheet.png +0 -0
- package/examples/tiled_server/server/map.json +0 -50
- package/examples/tiled_server/server/map.tmx +0 -16
- package/examples/tiled_server/server/server.js +0 -16
- package/extends/Animation.js +0 -910
- package/extends/Effect.js +0 -252
- package/extends/Gleed2d.js +0 -252
- package/extends/Hit.js +0 -1509
- package/extends/Input.js +0 -699
- package/extends/Marshal.js +0 -716
- package/extends/Scrolling.js +0 -388
- package/extends/Soundmanager2.js +0 -5466
- package/extends/Spritesheet.js +0 -196
- package/extends/Text.js +0 -366
- package/extends/Tiled.js +0 -403
- package/extends/Window.js +0 -575
- package/extends/gamepad.js +0 -397
- package/extends/socket.io.min.js +0 -2
- package/extends/swf/soundmanager2.swf +0 -0
- package/extends/swf/soundmanager2_debug.swf +0 -0
- package/extends/swf/soundmanager2_flash9.swf +0 -0
- package/extends/swf/soundmanager2_flash9_debug.swf +0 -0
- package/extends/swf/soundmanager2_flash_xdomain.zip +0 -0
- package/extends/workers/transition.js +0 -43
- package/index.js +0 -46
- package/license.txt +0 -19
- package/readme.md +0 -483
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { Signal, WritableArraySignal, WritableObjectSignal, isSignal } from "@signe/reactive";
|
|
2
|
+
import {
|
|
3
|
+
Observable,
|
|
4
|
+
Subject,
|
|
5
|
+
Subscription,
|
|
6
|
+
defer,
|
|
7
|
+
from,
|
|
8
|
+
map,
|
|
9
|
+
of,
|
|
10
|
+
switchMap,
|
|
11
|
+
} from "rxjs";
|
|
12
|
+
import { ComponentInstance } from "../components/DisplayObject";
|
|
13
|
+
import { Directive, applyDirective } from "./directive";
|
|
14
|
+
import { isObject, isPromise, set } from "./utils";
|
|
15
|
+
|
|
16
|
+
export interface Props {
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ArrayChange<T> = {
|
|
21
|
+
type: "add" | "remove" | "update" | "init" | "reset";
|
|
22
|
+
index?: number;
|
|
23
|
+
items: T[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ObjectChange<T> = {
|
|
27
|
+
type: "add" | "remove" | "update" | "init" | "reset";
|
|
28
|
+
key?: string;
|
|
29
|
+
value?: T;
|
|
30
|
+
items: T[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ElementObservable<T> = Observable<
|
|
34
|
+
(ArrayChange<T> | ObjectChange<T>) & {
|
|
35
|
+
value: Element | Element[];
|
|
36
|
+
}
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
type NestedSignalObjects = {
|
|
40
|
+
[Key in string]: NestedSignalObjects | Signal<any>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface Element<T = ComponentInstance> {
|
|
44
|
+
tag: string;
|
|
45
|
+
props: Props;
|
|
46
|
+
componentInstance: T;
|
|
47
|
+
propSubscriptions: Subscription[];
|
|
48
|
+
effectSubscriptions: Subscription[];
|
|
49
|
+
effectMounts: (() => void)[];
|
|
50
|
+
effectUnmounts: ((element?: Element) => void)[];
|
|
51
|
+
propObservables: NestedSignalObjects | undefined;
|
|
52
|
+
parent: Element | null;
|
|
53
|
+
context?: {
|
|
54
|
+
[key: string]: any;
|
|
55
|
+
};
|
|
56
|
+
directives: {
|
|
57
|
+
[key: string]: Directive;
|
|
58
|
+
};
|
|
59
|
+
destroy: () => void;
|
|
60
|
+
allElements: Subject<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type FlowResult = {
|
|
64
|
+
elements: Element[];
|
|
65
|
+
prev?: Element;
|
|
66
|
+
fullElements?: Element[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type FlowObservable = Observable<FlowResult>;
|
|
70
|
+
|
|
71
|
+
const components: { [key: string]: any } = {};
|
|
72
|
+
|
|
73
|
+
export const isElement = (value: any): value is Element => {
|
|
74
|
+
return (
|
|
75
|
+
value &&
|
|
76
|
+
typeof value === "object" &&
|
|
77
|
+
"tag" in value &&
|
|
78
|
+
"props" in value &&
|
|
79
|
+
"componentInstance" in value
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const isPrimitive = (value) => {
|
|
84
|
+
return (
|
|
85
|
+
typeof value === "string" ||
|
|
86
|
+
typeof value === "number" ||
|
|
87
|
+
typeof value === "boolean" ||
|
|
88
|
+
value === null ||
|
|
89
|
+
value === undefined
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function registerComponent(name, component) {
|
|
94
|
+
components[name] = component;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function destroyElement(element: Element | Element[]) {
|
|
98
|
+
if (Array.isArray(element)) {
|
|
99
|
+
element.forEach((e) => destroyElement(e));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!element) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
element.propSubscriptions.forEach((sub) => sub.unsubscribe());
|
|
106
|
+
element.effectSubscriptions.forEach((sub) => sub.unsubscribe());
|
|
107
|
+
for (let name in element.directives) {
|
|
108
|
+
element.directives[name].onDestroy?.();
|
|
109
|
+
}
|
|
110
|
+
element.componentInstance.onDestroy?.(element.parent as any);
|
|
111
|
+
element.effectUnmounts.forEach((fn) => fn?.());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates a virtual element or a representation thereof, with properties that can be dynamically updated based on BehaviorSubjects.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} tag - The tag name of the element to create.
|
|
118
|
+
* @param {Object} props - An object containing properties for the element. Each property can either be a direct value
|
|
119
|
+
* or an array where the first element is a function that returns a value based on input parameters,
|
|
120
|
+
* and the second element is an array of BehaviorSubjects. The property is updated dynamically
|
|
121
|
+
* using the combineLatest RxJS operator to wait for all BehaviorSubjects to emit.
|
|
122
|
+
* @returns {Object} An object representing the created element, including tag name and dynamic properties.
|
|
123
|
+
*/
|
|
124
|
+
export function createComponent(tag: string, props?: Props): Element {
|
|
125
|
+
if (!components[tag]) {
|
|
126
|
+
throw new Error(`Component ${tag} is not registered`);
|
|
127
|
+
}
|
|
128
|
+
const instance = new components[tag]();
|
|
129
|
+
const element: Element = {
|
|
130
|
+
tag,
|
|
131
|
+
props: {},
|
|
132
|
+
componentInstance: instance,
|
|
133
|
+
propSubscriptions: [],
|
|
134
|
+
propObservables: props,
|
|
135
|
+
parent: null,
|
|
136
|
+
directives: {},
|
|
137
|
+
effectUnmounts: [],
|
|
138
|
+
effectSubscriptions: [],
|
|
139
|
+
effectMounts: [],
|
|
140
|
+
destroy() {
|
|
141
|
+
destroyElement(this);
|
|
142
|
+
},
|
|
143
|
+
allElements: new Subject(),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Iterate over each property in the props object
|
|
147
|
+
if (props) {
|
|
148
|
+
const recursiveProps = (props, path = "") => {
|
|
149
|
+
const _set = (path, key, value) => {
|
|
150
|
+
if (path == "") {
|
|
151
|
+
element.props[key] = value;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
set(element.props, path + "." + key, value);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
Object.entries(props).forEach(([key, value]: [string, unknown]) => {
|
|
158
|
+
if (isSignal(value)) {
|
|
159
|
+
const _value = value as Signal<any>;
|
|
160
|
+
if ("dependencies" in _value && _value.dependencies.size == 0) {
|
|
161
|
+
_set(path, key, _value());
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
element.propSubscriptions.push(
|
|
165
|
+
_value.observable.subscribe((value) => {
|
|
166
|
+
_set(path, key, value);
|
|
167
|
+
if (element.directives[key]) {
|
|
168
|
+
element.directives[key].onUpdate?.(value);
|
|
169
|
+
}
|
|
170
|
+
if (key == "tick") {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
instance.onUpdate?.(
|
|
174
|
+
path == ""
|
|
175
|
+
? {
|
|
176
|
+
[key]: value,
|
|
177
|
+
}
|
|
178
|
+
: set({}, path + "." + key, value)
|
|
179
|
+
);
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
if (isObject(value) && key != "context" && !isElement(value)) {
|
|
184
|
+
recursiveProps(value, (path ? path + "." : "") + key);
|
|
185
|
+
} else {
|
|
186
|
+
_set(path, key, value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
recursiveProps(props);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
instance.onInit?.(element.props);
|
|
195
|
+
|
|
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) {
|
|
213
|
+
element.props.context = parent.props.context;
|
|
214
|
+
element.parent = parent;
|
|
215
|
+
element.componentInstance.onMount?.(element, index);
|
|
216
|
+
for (let name in element.directives) {
|
|
217
|
+
element.directives[name].onMount?.(element);
|
|
218
|
+
}
|
|
219
|
+
element.effectMounts.forEach((fn: any) => {
|
|
220
|
+
element.effectUnmounts.push(fn(element));
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
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)
|
|
229
|
+
}
|
|
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
|
+
async function createElement(parent: Element, child: Element) {
|
|
258
|
+
if (isPromise(child)) {
|
|
259
|
+
child = await child;
|
|
260
|
+
}
|
|
261
|
+
if (child instanceof Observable) {
|
|
262
|
+
child.subscribe(
|
|
263
|
+
({
|
|
264
|
+
elements: comp,
|
|
265
|
+
prev,
|
|
266
|
+
}: {
|
|
267
|
+
elements: Element[];
|
|
268
|
+
prev?: Element;
|
|
269
|
+
}) => {
|
|
270
|
+
// if prev, insert element after this
|
|
271
|
+
const components = comp.filter((c) => c !== null);
|
|
272
|
+
if (prev) {
|
|
273
|
+
components.forEach((c) => {
|
|
274
|
+
const index = parent.props.children.indexOf(prev.props.key);
|
|
275
|
+
onMount(parent, c, index + 1);
|
|
276
|
+
propagateContext(c);
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
components.forEach((component) => {
|
|
281
|
+
if (!Array.isArray(component)) {
|
|
282
|
+
onMount(parent, component);
|
|
283
|
+
propagateContext(component);
|
|
284
|
+
} else {
|
|
285
|
+
component.forEach((comp) => {
|
|
286
|
+
onMount(parent, comp);
|
|
287
|
+
propagateContext(comp);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
elementsListen.next(undefined)
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
} else {
|
|
295
|
+
onMount(parent, child);
|
|
296
|
+
await propagateContext(child);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Return the created element representation
|
|
301
|
+
return element;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Observes a BehaviorSubject containing an array or object of items and dynamically creates child elements for each item.
|
|
306
|
+
*
|
|
307
|
+
* @param {WritableArraySignal<T> | WritableObjectSignal<T>} itemsSubject - A signal that emits an array or object of items.
|
|
308
|
+
* @param {Function} createElementFn - A function that takes an item and returns an element representation.
|
|
309
|
+
* @returns {Observable} An observable that emits the list of created child elements.
|
|
310
|
+
*/
|
|
311
|
+
export function loop<T>(
|
|
312
|
+
itemsSubject: WritableArraySignal<T[]> | WritableObjectSignal<T>,
|
|
313
|
+
createElementFn: (item: T, index: number | string) => Element | null
|
|
314
|
+
): FlowObservable {
|
|
315
|
+
let elements: Element[] = [];
|
|
316
|
+
let elementMap = new Map<string | number, Element>();
|
|
317
|
+
|
|
318
|
+
return new Observable<FlowResult>(subscriber => {
|
|
319
|
+
const isArraySignal = (signal: any): signal is WritableArraySignal<T[]> =>
|
|
320
|
+
Array.isArray(signal());
|
|
321
|
+
|
|
322
|
+
const subscription = isArraySignal(itemsSubject)
|
|
323
|
+
? itemsSubject.observable.subscribe(change => {
|
|
324
|
+
if (change.type === 'init' || change.type === 'reset') {
|
|
325
|
+
elements.forEach(el => el.destroy());
|
|
326
|
+
elements = [];
|
|
327
|
+
elementMap.clear();
|
|
328
|
+
|
|
329
|
+
const items = itemsSubject();
|
|
330
|
+
if (items) {
|
|
331
|
+
items.forEach((item, index) => {
|
|
332
|
+
const element = createElementFn(item, index);
|
|
333
|
+
if (element) {
|
|
334
|
+
elements.push(element);
|
|
335
|
+
elementMap.set(index, element);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
} else if (change.type === 'add' && change.index !== undefined) {
|
|
340
|
+
const newElements = change.items.map((item, i) => {
|
|
341
|
+
const element = createElementFn(item as T, change.index! + i);
|
|
342
|
+
if (element) {
|
|
343
|
+
elementMap.set(change.index! + i, element);
|
|
344
|
+
}
|
|
345
|
+
return element;
|
|
346
|
+
}).filter((el): el is Element => el !== null);
|
|
347
|
+
|
|
348
|
+
elements.splice(change.index, 0, ...newElements);
|
|
349
|
+
} else if (change.type === 'remove' && change.index !== undefined) {
|
|
350
|
+
const removed = elements.splice(change.index, 1);
|
|
351
|
+
removed.forEach(el => {
|
|
352
|
+
el.destroy();
|
|
353
|
+
elementMap.delete(change.index!);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
subscriber.next({
|
|
358
|
+
elements: elements
|
|
359
|
+
});
|
|
360
|
+
})
|
|
361
|
+
: (itemsSubject as WritableObjectSignal<T>).observable.subscribe(change => {
|
|
362
|
+
const key = change.key as string | number
|
|
363
|
+
if (change.type === 'init' || change.type === 'reset') {
|
|
364
|
+
elements.forEach(el => el.destroy());
|
|
365
|
+
elements = [];
|
|
366
|
+
elementMap.clear();
|
|
367
|
+
|
|
368
|
+
const items = (itemsSubject as WritableObjectSignal<T>)();
|
|
369
|
+
if (items) {
|
|
370
|
+
Object.entries(items).forEach(([key, value]) => {
|
|
371
|
+
const element = createElementFn(value, key);
|
|
372
|
+
if (element) {
|
|
373
|
+
elements.push(element);
|
|
374
|
+
elementMap.set(key, element);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
} else if (change.type === 'add' && change.key && change.value !== undefined) {
|
|
379
|
+
const element = createElementFn(change.value as T, key);
|
|
380
|
+
if (element) {
|
|
381
|
+
elements.push(element);
|
|
382
|
+
elementMap.set(key, element);
|
|
383
|
+
}
|
|
384
|
+
} else if (change.type === 'remove' && change.key) {
|
|
385
|
+
const index = elements.findIndex(el => elementMap.get(key) === el);
|
|
386
|
+
if (index !== -1) {
|
|
387
|
+
const [removed] = elements.splice(index, 1);
|
|
388
|
+
removed.destroy();
|
|
389
|
+
elementMap.delete(key);
|
|
390
|
+
}
|
|
391
|
+
} else if (change.type === 'update' && change.key && change.value !== undefined) {
|
|
392
|
+
const index = elements.findIndex(el => elementMap.get(key) === el);
|
|
393
|
+
if (index !== -1) {
|
|
394
|
+
const oldElement = elements[index];
|
|
395
|
+
oldElement.destroy();
|
|
396
|
+
const newElement = createElementFn(change.value as T, key);
|
|
397
|
+
if (newElement) {
|
|
398
|
+
elements[index] = newElement;
|
|
399
|
+
elementMap.set(key, newElement);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
subscriber.next({
|
|
405
|
+
elements: elements
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return subscription;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Conditionally creates and destroys elements based on a condition signal.
|
|
415
|
+
*
|
|
416
|
+
* @param {Signal<boolean> | boolean} condition - A signal or boolean that determines whether to create an element.
|
|
417
|
+
* @param {Function} createElementFn - A function that returns an element or a promise that resolves to an element.
|
|
418
|
+
* @returns {Observable} An observable that emits the created or destroyed element.
|
|
419
|
+
*/
|
|
420
|
+
export function cond(
|
|
421
|
+
condition: Signal<boolean> | boolean,
|
|
422
|
+
createElementFn: () => Element | Promise<Element>
|
|
423
|
+
): FlowObservable {
|
|
424
|
+
let element: Element | null = null;
|
|
425
|
+
|
|
426
|
+
if (isSignal(condition)) {
|
|
427
|
+
const signalCondition = condition as WritableObjectSignal<boolean>;
|
|
428
|
+
return new Observable<{elements: Element[], type?: "init" | "remove"}>(subscriber => {
|
|
429
|
+
return signalCondition.observable.subscribe(bool => {
|
|
430
|
+
if (bool) {
|
|
431
|
+
let _el = createElementFn();
|
|
432
|
+
if (isPromise(_el)) {
|
|
433
|
+
from(_el as Promise<Element>).subscribe(el => {
|
|
434
|
+
element = el;
|
|
435
|
+
subscriber.next({
|
|
436
|
+
type: "init",
|
|
437
|
+
elements: [el],
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
} else {
|
|
441
|
+
element = _el as Element;
|
|
442
|
+
subscriber.next({
|
|
443
|
+
type: "init",
|
|
444
|
+
elements: [element],
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
} else if (element) {
|
|
448
|
+
destroyElement(element);
|
|
449
|
+
subscriber.next({
|
|
450
|
+
elements: [],
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
subscriber.next({
|
|
454
|
+
elements: [],
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
} else {
|
|
460
|
+
// Handle boolean case
|
|
461
|
+
if (condition) {
|
|
462
|
+
let _el = createElementFn();
|
|
463
|
+
if (isPromise(_el)) {
|
|
464
|
+
return from(_el as Promise<Element>).pipe(
|
|
465
|
+
map((el) => ({
|
|
466
|
+
type: "init",
|
|
467
|
+
elements: [el],
|
|
468
|
+
}))
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return of({
|
|
472
|
+
type: "init",
|
|
473
|
+
elements: [_el as Element],
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return of({
|
|
477
|
+
elements: [],
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Subscription
|
|
3
|
+
} from "rxjs";
|
|
4
|
+
import type { Element } from "./reactive";
|
|
5
|
+
import { Tick } from "../directives/Scheduler";
|
|
6
|
+
|
|
7
|
+
type MountFunction = (fn: (element: Element) => void) => void;
|
|
8
|
+
|
|
9
|
+
// Define ComponentFunction type
|
|
10
|
+
export type ComponentFunction<P = {}> = (props: P) => Element | Promise<Element>;
|
|
11
|
+
|
|
12
|
+
export let currentSubscriptionsTracker: ((subscription: Subscription) => void) | null = null;
|
|
13
|
+
export let mountTracker: MountFunction | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registers a mount function to be called when the component is mounted.
|
|
17
|
+
* To unmount the component, the function must return a function that will be called by the engine.
|
|
18
|
+
*
|
|
19
|
+
* @param {(element: Element) => void} fn - The function to be called on mount.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* mount((el) => {
|
|
23
|
+
* console.log('mounted', el);
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
* Unmount the component by returning a function:
|
|
27
|
+
* ```ts
|
|
28
|
+
* mount((el) => {
|
|
29
|
+
* console.log('mounted', el);
|
|
30
|
+
* return () => {
|
|
31
|
+
* console.log('unmounted', el);
|
|
32
|
+
* }
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function mount(fn: (element: Element) => void) {
|
|
37
|
+
mountTracker?.(fn);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Registers a tick function to be called on each tick of the component's context.
|
|
42
|
+
* @param {(tickValue: Tick, element: Element) => void} fn - The function to be called on each tick.
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* tick((tickValue, el) => {
|
|
46
|
+
* console.log('tick', tickValue, el);
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function tick(fn: (tickValue: Tick, element: Element) => void) {
|
|
51
|
+
mount((el: Element) => {
|
|
52
|
+
const { context } = el.props
|
|
53
|
+
let subscription: Subscription | undefined
|
|
54
|
+
if (context.tick) {
|
|
55
|
+
subscription = context.tick.observable.subscribe(({ value }) => {
|
|
56
|
+
fn(value, el)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
return () => {
|
|
60
|
+
subscription?.unsubscribe()
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add tracking for subscriptions and mounts, then create an element from a component function.
|
|
67
|
+
* @template C
|
|
68
|
+
* @param {C} componentFunction - The component function to create an element from.
|
|
69
|
+
* @param {Parameters<C>[0]} [props={}] - The props to pass to the component function.
|
|
70
|
+
* @param {...any[]} children - The children elements of the component.
|
|
71
|
+
* @returns {ReturnType<C>}
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const el = h(MyComponent, {
|
|
75
|
+
* x: 100,
|
|
76
|
+
* y: 100,
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* with children:
|
|
81
|
+
* ```ts
|
|
82
|
+
* const el = h(MyComponent, {
|
|
83
|
+
* x: 100,
|
|
84
|
+
* y: 100,
|
|
85
|
+
* },
|
|
86
|
+
* h(MyChildComponent, {
|
|
87
|
+
* x: 50,
|
|
88
|
+
* y: 50,
|
|
89
|
+
* }),
|
|
90
|
+
* );
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function h<C extends ComponentFunction<any>>(
|
|
94
|
+
componentFunction: C,
|
|
95
|
+
props: Parameters<C>[0] = {} as Parameters<C>[0],
|
|
96
|
+
...children: any[]
|
|
97
|
+
): ReturnType<C> {
|
|
98
|
+
const allSubscriptions = new Set<Subscription>();
|
|
99
|
+
const allMounts = new Set<MountFunction>();
|
|
100
|
+
|
|
101
|
+
currentSubscriptionsTracker = (subscription) => {
|
|
102
|
+
allSubscriptions.add(subscription);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
mountTracker = (fn: any) => {
|
|
106
|
+
allMounts.add(fn);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (children[0] instanceof Array) {
|
|
110
|
+
children = children[0]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let component = componentFunction({ ...props, children }) as Element;
|
|
114
|
+
|
|
115
|
+
if (!component) {
|
|
116
|
+
component = {} as any
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
component.effectSubscriptions = Array.from(allSubscriptions);
|
|
120
|
+
component.effectMounts = [
|
|
121
|
+
...Array.from(allMounts),
|
|
122
|
+
...((component as any).effectMounts ?? [])
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
// call mount hook for root component
|
|
126
|
+
if (component instanceof Promise) {
|
|
127
|
+
component.then((component) => {
|
|
128
|
+
if (component.props.isRoot) {
|
|
129
|
+
allMounts.forEach((fn) => fn(component));
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
currentSubscriptionsTracker = null;
|
|
135
|
+
mountTracker = null;
|
|
136
|
+
|
|
137
|
+
return component as ReturnType<C>;
|
|
138
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { effect, signal } from "@signe/reactive";
|
|
2
|
+
|
|
3
|
+
interface Listen<T = any> {
|
|
4
|
+
config: T | undefined;
|
|
5
|
+
seed: {
|
|
6
|
+
config: T | undefined;
|
|
7
|
+
value: number;
|
|
8
|
+
resolve: (value: any) => void;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Trigger<T = any> {
|
|
13
|
+
start: () => Promise<void>;
|
|
14
|
+
listen: () => Listen<T> | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks if the given argument is a Trigger object
|
|
19
|
+
* @param arg - The value to check
|
|
20
|
+
* @returns True if the argument is a Trigger object
|
|
21
|
+
*/
|
|
22
|
+
export function isTrigger(arg: any): arg is Trigger<any> {
|
|
23
|
+
return arg?.start && arg?.listen;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a new trigger that can be used to pass data between components
|
|
28
|
+
* @param globalConfig - Optional configuration data to be passed when the trigger is activated
|
|
29
|
+
* @returns A Trigger object with start and listen methods
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const myTrigger = trigger()
|
|
33
|
+
*
|
|
34
|
+
* on(myTrigger, (data) => {
|
|
35
|
+
* console.log('Triggered with data:', data)
|
|
36
|
+
* })
|
|
37
|
+
*
|
|
38
|
+
* myTrigger.start({ message: 'Hello' })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function trigger<T = any>(globalConfig?: T): Trigger<T> {
|
|
42
|
+
const _signal = signal({
|
|
43
|
+
config: globalConfig,
|
|
44
|
+
value: 0,
|
|
45
|
+
resolve: (value: any) => void 0,
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
start: (config?: T) => {
|
|
49
|
+
return new Promise((resolve: (value: any) => void) => {
|
|
50
|
+
_signal.set({
|
|
51
|
+
config: {
|
|
52
|
+
...globalConfig,
|
|
53
|
+
...config,
|
|
54
|
+
},
|
|
55
|
+
resolve,
|
|
56
|
+
value: Math.random(),
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
listen: (): Listen<T> | undefined => {
|
|
61
|
+
return {
|
|
62
|
+
config: globalConfig,
|
|
63
|
+
seed: _signal(),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Subscribes to a trigger and executes a callback when the trigger is activated
|
|
71
|
+
* @param triggerSignal - The trigger to subscribe to
|
|
72
|
+
* @param callback - Function to execute when the trigger is activated
|
|
73
|
+
* @throws Error if triggerSignal is not a valid trigger
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const click = trigger()
|
|
77
|
+
*
|
|
78
|
+
* on(click, () => {
|
|
79
|
+
* console.log('Click triggered')
|
|
80
|
+
* })
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function on(triggerSignal: any, callback: (config: any) => void | Promise<void>) {
|
|
84
|
+
if (!isTrigger(triggerSignal)) {
|
|
85
|
+
throw new Error("In 'on(arg)' must have a trigger signal type");
|
|
86
|
+
}
|
|
87
|
+
effect(() => {
|
|
88
|
+
const result = triggerSignal.listen();
|
|
89
|
+
if (result?.seed.value) {
|
|
90
|
+
const ret = callback(result?.seed.config);
|
|
91
|
+
if (ret && typeof ret.then === 'function') {
|
|
92
|
+
ret.then(result?.seed.resolve);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|