@psytask/core 1.0.0 → 1.1.0

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/README.md CHANGED
@@ -0,0 +1 @@
1
+ [PsyTask](https://github.com/bluebonesx/psytask) scene and event emitter model.
package/dist/index.d.ts CHANGED
@@ -1,68 +1,116 @@
1
- type LooseObject = Record<string, any>;
2
- type Merge<T, U> = T extends any ? Omit<T, keyof U> & U : never;
1
+ type LooseObject = Record<string, unknown>;
2
+ type Merge<T, U> = T extends unknown ? Omit<T, keyof U> & U : never;
3
+ type Split<T extends string, S extends string> = T extends unknown
4
+ ? T extends `${infer L}${S}${infer R}`
5
+ ? [L, ...Split<R, S>]
6
+ : [T]
7
+ : never;
8
+
9
+ declare global {
10
+ interface ObjectConstructor {
11
+ keys<T>(o: T): (keyof T & string)[];
12
+ }
13
+ interface String {
14
+ split<T extends string, S extends string>(
15
+ this: T,
16
+ separator: S,
17
+ ): Split<T, S>;
18
+ }
19
+ interface Array<T> {
20
+ map<R>(fn: <E extends T>(e: E, i: number, arr: this) => R): R[];
21
+ }
22
+ interface Performance {
23
+ getEntriesByType(type: 'resource'): PerformanceResourceTiming[];
24
+ }
25
+ }
3
26
 
4
27
  declare const symbol: typeof Symbol.dispose;
28
+ type EventMap<T extends LooseObject> = T & {
29
+ dispose: undefined;
30
+ };
5
31
  /** {@link Disposable} event emitter, use {@link Set} to manage listeners */
6
- declare class EventEmitter<M extends LooseObject & {
32
+ declare class EventEmitter<T extends LooseObject & {
7
33
  dispose?: never;
8
- } = {}, EventMap extends {
9
- dispose: null;
10
- } = M & {
11
- dispose: null;
12
- }> implements Disposable {
34
+ } = {}> implements Disposable {
13
35
  readonly listeners: {
14
- readonly [K in keyof EventMap]?: Set<(e: EventMap[K]) => void>;
36
+ [K in keyof EventMap<T>]?: Set<(e: EventMap<T>[K]) => void>;
15
37
  };
16
38
  [symbol](): void;
17
39
  /** Add event listener */
18
- on<K extends keyof EventMap>(type: K, listener: (evt: EventMap[K]) => void): this;
40
+ on<K extends keyof EventMap<T>>(type: K, listener: (evt: EventMap<T>[K]) => void): this;
19
41
  /** Remove event listener */
20
- off<K extends keyof EventMap>(type: K, listener: (evt: EventMap[K]) => void): this;
42
+ off<K extends keyof EventMap<T>>(type: K, listener: (evt: EventMap<T>[K]) => void): this;
21
43
  /** Add one-time event listener, can not be removed manually */
22
- once<K extends keyof EventMap>(type: K, listener: (evt: EventMap[K]) => void): this;
44
+ once<K extends keyof EventMap<T>>(type: K, listener: (evt: EventMap<T>[K]) => void): this;
23
45
  /** Emit event listeners */
24
- emit<K extends keyof EventMap>(type: K, e: EventMap[K]): this;
46
+ emit<K extends keyof EventMap<T>>(type: K, ...[evt]: EventMap<T>[K] extends undefined ? [evt?: EventMap<T>[K]] : [evt: EventMap<T>[K]]): this;
25
47
  }
26
48
 
27
- declare const createShowInfo: () => {
28
- start_time: number;
29
- frame_times: number[];
49
+ type TimerRecords = number[];
50
+ type Timer = {
51
+ start(onFrame?: (records: TimerRecords) => void): Promise<TimerRecords>;
52
+ stop(): void;
30
53
  };
31
- type SceneShowInfo = ReturnType<typeof createShowInfo>;
32
- type ForbiddenSceneData = {
33
- [K in keyof SceneShowInfo]?: never;
54
+ /**
55
+ * ## Render logic
56
+ *
57
+ * ```text
58
+ * rAF(scene_1.show) -> render -> vsync -> rAF[scene_1.start_time] -> ... ->
59
+ * rAF(scene_2.show) -> ...
60
+ * ```
61
+ *
62
+ * ## Closing condition
63
+ *
64
+ * | symbol/expression | description |
65
+ * | :---------------------: | ------------------- |
66
+ * | t | current frame time |
67
+ * | t_0 | start frame time |
68
+ * | D | duration |
69
+ * | \delta | next frame duration |
70
+ * | e = t - t_0 - D | duration error |
71
+ * | \|e\| <= \|e + \delta\| | closing condition |
72
+ *
73
+ * Inference:
74
+ *
75
+ * ```text
76
+ * For |e| <= |e + \delta|, given that \delta > 0
77
+ * if e >= 0 then e <= e + \delta -> true
78
+ * if e < 0 then -e <= |e + \delta|
79
+ * if e + \delta >= 0 then -e <= e + \delta -> e >= -\delta / 2
80
+ * if e + \delta < 0 then -e <= -e - \delta -> false
81
+ * ```
82
+ */
83
+ declare const createTimer: (shouldStop: (records: TimerRecords) => boolean) => Timer;
84
+ type NodeLike = string | Node;
85
+ type BuiltinData = {
86
+ frame_times: TimerRecords;
34
87
  };
35
- type SceneTimerCreator = (options: {
36
- frame_ms: number;
37
- duration?: number;
38
- onStart: (time: number) => void;
39
- onFrame: (time: number) => void;
40
- }) => {
41
- promise: Promise<void>;
42
- close: () => void;
88
+ type ForbiddenData = {
89
+ [K in keyof BuiltinData]?: never;
43
90
  };
44
- type NodeLike = string | Node;
45
91
  /**
46
- * Scene setup function, only called once when the scene is created.
92
+ * Only called once when the scene is created.
47
93
  *
48
94
  * @param props - The reactive props to control the scene display
49
- * @param ctx - The scene instance, can be used to manage lifecycle
50
95
  * @see {@link Scene}
51
96
  */
52
- type SceneSetup<P extends LooseObject = any, D extends LooseObject = LooseObject & ForbiddenSceneData> = (props: P, ctx: Scene<any>) => NodeLike | NodeLike[] | {
53
- /** The node(s) appended to the root element of scene */
54
- node: NodeLike | NodeLike[];
55
- /** Data getter to get data from elements */
56
- data: () => D;
97
+ type Component<P extends LooseObject = any, D extends LooseObject = LooseObject & ForbiddenData> = {
98
+ (props: P): NodeLike | NodeLike[] | {
99
+ /** The node(s) appended to the root element of scene */
100
+ node: NodeLike | NodeLike[];
101
+ /** Data getter to get data from elements */
102
+ data: () => D;
103
+ };
57
104
  };
58
- type SceneShow<P extends LooseObject = any, D extends LooseObject = LooseObject & ForbiddenSceneData> = (patchProps?: Partial<P>) => Promise<Merge<D, SceneShowInfo>>;
59
- type GenericSceneSetup<P extends LooseObject = any, D extends LooseObject = LooseObject & ForbiddenSceneData> = SceneShow<P, D>;
105
+ type SceneShow<P extends LooseObject = LooseObject, D extends LooseObject = LooseObject & ForbiddenData> = (patchProps?: Partial<P>) => Promise<Merge<D, BuiltinData>>;
106
+ /** Same with {@link SceneShow} */
107
+ type GenericComponent<P extends LooseObject = LooseObject, D extends LooseObject = LooseObject & ForbiddenData> = SceneShow<P, D>;
60
108
  /**
61
- * Provide type infer for generic setup function, do nothing in runtime.
109
+ * Provide type infer for generic component, do nothing in runtime.
62
110
  *
63
111
  * @example
64
112
  *
65
- * Support generic scene setup function
113
+ * Support generic component
66
114
  *
67
115
  * ```ts
68
116
  * using scene = new Scene(
@@ -74,128 +122,126 @@ type GenericSceneSetup<P extends LooseObject = any, D extends LooseObject = Loos
74
122
  * ```
75
123
  */
76
124
  declare const generic: {
77
- <P extends LooseObject, D extends LooseObject & ForbiddenSceneData = {}>(f: SceneSetup<P, D>): GenericSceneSetup<P, D>;
125
+ <P extends LooseObject, D extends LooseObject & ForbiddenData = {}>(f: Component<P, D>): GenericComponent<P, D>;
78
126
  };
79
127
  /** @ignore */
80
- type MaybeGenericSceneSetup = SceneSetup | GenericSceneSetup;
128
+ type MaybeGenericComponent<P extends LooseObject = any, D extends LooseObject & ForbiddenData = {}> = Component<P, D> | GenericComponent<P, D>;
129
+ type ComponentAdapter = {
130
+ /** Wrap a component with reactive props */
131
+ wrap: <T extends Component>(component: T) => T;
132
+ /** Render a component with default props and provided scene */
133
+ render: <T extends Component>(component: T, defaultProps: Parameters<T>[0],
134
+ /** If not provided, it will use current scene */
135
+ ctx?: Scene<Component>) => {
136
+ props: Parameters<T>[0];
137
+ } & (ReturnType<T> extends infer R ? R extends {
138
+ node: infer N;
139
+ data: infer D;
140
+ } ? {
141
+ nodes: N extends NodeLike ? [N] : N;
142
+ data: D;
143
+ } : {
144
+ nodes: R extends NodeLike ? [R] : R;
145
+ data: undefined;
146
+ } : never);
147
+ };
148
+ declare const createComponentAdapter: (reactive: <T extends LooseObject>(obj: T) => T) => ComponentAdapter;
81
149
  /**
82
- * Scene lifecycle and root element event name-value pairs.
150
+ * Lifecycle hooks.
83
151
  *
84
- * | name | trigger timing |
85
- * | --------------------- | -------------------------------------------------------------------------------------------------------- |
86
- * | scene:show | the scene is shown |
87
- * | scene:frame | on each frame when the scene is shown |
88
- * | scene:close | the scene is closed |
89
- * | mouse:left | the left mouse button is pressed |
90
- * | mouse:middle | the middle mouse button is pressed |
91
- * | mouse:right | the right mouse button is pressed |
92
- * | mouse:unknown | an unknown mouse button is pressed |
93
- * | key:\<key\> | a {@link https://developer.mozilla.org/docs/Web/API/UI_Events/Keyboard_event_key_values key} is pressed |
94
- * | \<HTMLElement-Event\> | an {@link https://developer.mozilla.org/docs/Web/API/HTMLElement#events html element event} is triggered |
152
+ * | name | trigger timing |
153
+ * | ----- | ------------------------------------- |
154
+ * | show | the scene is shown |
155
+ * | frame | on each frame when the scene is shown |
156
+ * | close | the scene is closed |
95
157
  */
96
- type SceneEventMap = HTMLElementEventMap & {
97
- 'scene:show': LooseObject;
98
- 'scene:frame': number;
99
- 'scene:close': null;
100
- } & {
101
- [K in `mouse:${'left' | 'middle' | 'right' | 'unknown'}`]: MouseEvent;
102
- } & {
103
- [K in `key:${string}`]: KeyboardEvent;
158
+ type SceneEventMap = {
159
+ show: undefined;
160
+ frame: number;
161
+ close: undefined;
104
162
  };
163
+ /**
164
+ * @example
165
+ *
166
+ * Must be called on the top scope of component
167
+ *
168
+ * ```ts
169
+ * const Component = (props: {}) => {
170
+ * const ctx = getCurrentScene();
171
+ * return '';
172
+ * };
173
+ * ```
174
+ *
175
+ * DO NOT call on other place
176
+ *
177
+ * ```ts
178
+ * const Component = (props: {}) => {
179
+ * const fn = () => {
180
+ * const ctx = getCurrentScene(); // WRONG!
181
+ * };
182
+ * return '';
183
+ * };
184
+ * ```
185
+ */
186
+ declare const getCurrentScene: () => Scene<MaybeGenericComponent<any, {}>>;
105
187
  /** Scene options */
106
- type SceneOptions<T extends MaybeGenericSceneSetup> = {
188
+ type SceneOptions<T extends MaybeGenericComponent> = {
107
189
  /** Root element */
108
190
  root: HTMLDivElement;
109
- /** Frame duration in milliseconds */
110
- frame_ms: number;
111
191
  /** Default props */
112
192
  defaultProps: Parameters<T>[0];
113
- /** Scene duration in milliseconds */
114
- duration?: number;
115
- /** Close on specific {@link SceneEventMap events} */
116
- close_on?: keyof SceneEventMap | (keyof SceneEventMap)[];
117
- /** Whether to record frame times */
118
- record_frame_times?: boolean;
119
- /** Control display timing */
120
- createTimer?: SceneTimerCreator;
193
+ /** Control show timing */
194
+ timer: () => Timer;
195
+ /** Component adapter */
196
+ adapter: ComponentAdapter;
121
197
  };
122
- declare class Scene<T extends MaybeGenericSceneSetup> extends EventEmitter<SceneEventMap> {
198
+ declare class Scene<T extends MaybeGenericComponent> extends EventEmitter<SceneEventMap> {
123
199
  #private;
124
- /** Root element */
200
+ readonly options: SceneOptions<T>;
125
201
  readonly root: HTMLDivElement;
202
+ readonly data: T extends MaybeGenericComponent<infer P, infer D> ? () => D : undefined;
126
203
  /**
127
- * Show the scene and change props one-time
204
+ * Show DOM and update props one-time
128
205
  *
129
206
  * @example
130
207
  *
131
- * Basic usage
208
+ * Integrate with `@vue/reactivity`
132
209
  *
133
210
  * ```ts
211
+ * import { reactive, effect } from '@vue/reactivity';
212
+ *
134
213
  * using scene = new Scene(
135
- * (props: { text: string }, ctx) => {
214
+ * (props: { text: string }) => {
136
215
  * const node = document.createElement('div');
137
- * ctx.on('scene:show', (newProps) => {
138
- * node.textContent = newProps.text;
216
+ * effect(() => {
217
+ * node.textContent = props.text; // auto update when props.text changes
139
218
  * });
140
219
  * return node;
141
220
  * },
142
221
  * {
143
- * //...
222
+ * root: document.createElement('div'),
144
223
  * defaultProps: { text: 'default' },
224
+ * adapter: createComponentAdapter(reactive),
225
+ * timer: () => createTimer((records) => records.length > 100), // show 100 frames
145
226
  * },
146
227
  * );
228
+ * document.body.appendChild(scene.root);
229
+ *
147
230
  * await scene.show({ text: 'new' }); // show `new`
148
231
  * await scene.show(); // show `default`
149
232
  * ```
150
233
  *
151
234
  * @function
152
235
  */
153
- show: T extends SceneSetup<infer P, infer D> ? SceneShow<P, D> : T;
236
+ show: T extends Component<infer P, infer D> ? SceneShow<P, D> : T;
154
237
  /**
155
- * @param setup - The {@link SceneSetup scene setup function}
156
- * @param defaultOptions - Default {@link SceneOptions scene options}
238
+ * @param component - {@link Component}
239
+ * @param options - {@link SceneOptions}
157
240
  */
158
- constructor(setup: T, defaultOptions: SceneOptions<T>);
159
- /**
160
- * Override default options one-time
161
- *
162
- * @example
163
- *
164
- * Change duration temporarily
165
- *
166
- * ```ts
167
- * using scene = new Scene(() => '', {
168
- * root: document.appendChild(document.createElement('div')),
169
- * frame_ms: 16.67,
170
- * defaultProps: {},
171
- * duration: 100,
172
- * });
173
- * await scene.config({ duration: 200 }).show(); // show 200ms
174
- * await scene.show(); // show 100ms
175
- * ```
176
- */
177
- config(patchOptions: {
178
- [K in {
179
- [K in keyof SceneOptions<T>]-?: undefined extends SceneOptions<T>[K] ? K : never;
180
- }[keyof SceneOptions<T>]]?: SceneOptions<T>[K];
181
- }): this;
182
- /** Add a microtask to close the scene. It is useful when close in 'scene:show' */
241
+ constructor(component: T, options: SceneOptions<T>);
242
+ /** Add a microtask to close the scene. It is useful when close in 'show' */
183
243
  close(): Promise<void>;
184
244
  }
185
245
 
186
- type EventType<T extends EventTarget, U = keyof T> = U extends `on${infer K}` ? K : never;
187
- /**
188
- * Add event listener and return cleanup function
189
- *
190
- * @example
191
- *
192
- * Listen to window resize event
193
- *
194
- * ```ts
195
- * const cleanup = on(window, 'resize', (e) => {});
196
- * ```
197
- */
198
- declare const on: <T extends EventTarget, K extends EventType<T>>(target: T, type: K, listener: (ev: `on${K}` extends infer P extends Extract<keyof T, string> ? T[P] & {} extends infer F extends (...args: any) => any ? Parameters<F>[0] : never : never) => void, options?: boolean | AddEventListenerOptions) => () => void;
199
-
200
- export { EventEmitter, Scene, generic, on };
201
- export type { MaybeGenericSceneSetup, NodeLike, SceneEventMap, SceneOptions, SceneSetup, SceneTimerCreator };
246
+ export { EventEmitter, Scene, createComponentAdapter, createTimer, generic, getCurrentScene };
247
+ export type { Component, ComponentAdapter, MaybeGenericComponent, NodeLike, SceneEventMap, SceneOptions, Timer, TimerRecords };
package/dist/index.js CHANGED
@@ -1,17 +1,16 @@
1
- /** @psytask/core v1.0.0 cubxx MIT */
2
-
3
- // src/event-emitter.ts
4
- var symbol = Symbol.dispose ?? Symbol.for("Symbol.dispose");
5
-
1
+ /** @psytask/core v1.1.0 cubxx MIT */
2
+ const symbol = Symbol.dispose ?? /* @__PURE__ */ Symbol.for("Symbol.dispose");
6
3
  class EventEmitter {
7
4
  listeners = {};
8
5
  [symbol]() {
9
- this.emit("dispose", null);
6
+ this.emit("dispose");
10
7
  }
8
+ /** Add event listener */
11
9
  on(type, listener) {
12
- (this.listeners[type] ??= new Set).add(listener);
10
+ (this.listeners[type] ??= /* @__PURE__ */ new Set()).add(listener);
13
11
  return this;
14
12
  }
13
+ /** Remove event listener */
15
14
  off(type, listener) {
16
15
  const listeners = this.listeners[type];
17
16
  if (listeners) {
@@ -20,6 +19,7 @@ class EventEmitter {
20
19
  }
21
20
  return this;
22
21
  }
22
+ /** Add one-time event listener, can not be removed manually */
23
23
  once(type, listener) {
24
24
  const wrapper = (evt) => {
25
25
  try {
@@ -31,113 +31,147 @@ class EventEmitter {
31
31
  this.on(type, wrapper);
32
32
  return this;
33
33
  }
34
- emit(type, e) {
34
+ /** Emit event listeners */
35
+ emit(type, ...[evt]) {
35
36
  const listeners = this.listeners[type];
36
- if (listeners)
37
- for (const listener of [...listeners])
38
- listener(e);
37
+ if (listeners) for (const listener of [...listeners]) listener(evt);
39
38
  return this;
40
39
  }
41
40
  }
42
- // ../../shared/utils.ts
43
- var ERR = (msg) => {
41
+
42
+ const ERR = (msg) => {
44
43
  throw Error(msg);
45
44
  };
46
- var rAF = requestAnimationFrame;
47
- var array_normalize = (e) => Array.isArray(e) ? e : [e];
48
-
49
- // src/utils.ts
50
- var on = (target, type, listener, options) => (target.addEventListener(type, listener, options), () => target.removeEventListener(type, listener, options));
45
+ const rAF = requestAnimationFrame;
46
+ const $Object = Object;
47
+ const modify = (a, b) => $Object.assign(a, b);
48
+ const doc = document;
49
+ new Proxy(
50
+ {},
51
+ {
52
+ get: (_, tag) => (props) => modify(doc.createElement(tag), props)
53
+ }
54
+ );
55
+ const array_normalize = (e) => Array.isArray(e) ? e : [e];
51
56
 
52
- // src/scene.ts
53
- var createShowInfo = () => ({ start_time: NaN, frame_times: [] });
54
- var createRAFTimer = (opts) => {
55
- let start_time, close;
56
- const frame = (last_time) => (opts.onFrame(last_time), typeof opts.duration === "number" && last_time - start_time >= opts.duration - opts.frame_ms * 1.5 ? close() : handle = rAF(frame));
57
- let handle = rAF((last_time) => (opts.onStart(last_time), frame(start_time = last_time)));
58
- return {
59
- promise: new Promise((resolve) => close = () => (cancelAnimationFrame(handle), resolve())),
60
- close
57
+ const createTimer = (shouldStop) => {
58
+ const timer = {
59
+ start: (cb) => new Promise((resolve) => {
60
+ timer.stop = () => (cancelAnimationFrame(handle), resolve(records));
61
+ const records = [];
62
+ const frame = (time) => {
63
+ records.push(time);
64
+ cb?.(records);
65
+ shouldStop(records) ? timer.stop() : handle = rAF(frame);
66
+ };
67
+ let handle = rAF(frame);
68
+ }),
69
+ stop() {
70
+ }
61
71
  };
72
+ return timer;
62
73
  };
63
- var generic = (f) => f;
64
- var mouseSuffixs = ["left", "middle", "right"];
65
- var prefix2type = {
66
- scene: 0,
67
- dispose: 0,
68
- key: "keydown",
69
- mouse: "mousedown"
70
- };
71
-
74
+ const generic = (f) => (
75
+ //@ts-expect-error impl generic component
76
+ f
77
+ );
78
+ const createComponentAdapter = (reactive) => ({
79
+ wrap: (component) => ((props) => component(reactive(props))),
80
+ render: (component, defaultProps, ctx) => {
81
+ const props = reactive(defaultProps);
82
+ ctx && sceneStack.push(ctx);
83
+ const instanceOrNode = component(props);
84
+ ctx && sceneStack.pop();
85
+ let data;
86
+ const nodes = array_normalize(
87
+ typeof instanceOrNode !== "string" && "node" in instanceOrNode ? (data = instanceOrNode.data, instanceOrNode.node) : instanceOrNode
88
+ );
89
+ return { props, nodes, data };
90
+ }
91
+ });
92
+ const sceneStack = [];
93
+ const getCurrentScene = () => sceneStack[sceneStack.length - 1] ?? ERR("Not found current scene");
72
94
  class Scene extends EventEmitter {
73
- root;
74
- #data;
75
- show = this.#show;
76
- #options;
77
- #defaultOptions;
78
- #timer;
79
- constructor(setup, defaultOptions) {
95
+ /**
96
+ * @param component - {@link Component}
97
+ * @param options - {@link SceneOptions}
98
+ */
99
+ constructor(component, options) {
80
100
  super();
81
- const { root, defaultProps } = this.#defaultOptions = defaultOptions;
101
+ this.options = options;
102
+ const { root, adapter, defaultProps, timer } = options;
82
103
  (this.root = root).tabIndex = -1;
83
- const reset = () => {
84
- root.style.transform = "scale(0)";
85
- this.#options = defaultOptions;
86
- this.#timer = undefined;
87
- };
88
- reset();
89
- const metaOrNode = setup(defaultProps, this.on("scene:close", reset).on("dispose", () => root.remove()));
90
- root.append(...array_normalize(typeof metaOrNode === "object" && "node" in metaOrNode ? (this.#data = metaOrNode.data, metaOrNode.node) : metaOrNode));
91
- }
92
- config(patchOptions) {
93
- this.#options = { ...this.#defaultOptions, ...patchOptions };
94
- return this;
104
+ root.style.scale = "0";
105
+ const { props, nodes, data } = adapter.render(
106
+ component,
107
+ { ...defaultProps },
108
+ this.on("dispose", () => root.remove())
109
+ );
110
+ this.#timer = timer();
111
+ this.#props = props, this.data = data;
112
+ root.append(...nodes);
95
113
  }
114
+ root;
115
+ #timer;
116
+ #props;
117
+ data;
118
+ /**
119
+ * Show DOM and update props one-time
120
+ *
121
+ * @example
122
+ *
123
+ * Integrate with `@vue/reactivity`
124
+ *
125
+ * ```ts
126
+ * import { reactive, effect } from '@vue/reactivity';
127
+ *
128
+ * using scene = new Scene(
129
+ * (props: { text: string }) => {
130
+ * const node = document.createElement('div');
131
+ * effect(() => {
132
+ * node.textContent = props.text; // auto update when props.text changes
133
+ * });
134
+ * return node;
135
+ * },
136
+ * {
137
+ * root: document.createElement('div'),
138
+ * defaultProps: { text: 'default' },
139
+ * adapter: createComponentAdapter(reactive),
140
+ * timer: () => createTimer((records) => records.length > 100), // show 100 frames
141
+ * },
142
+ * );
143
+ * document.body.appendChild(scene.root);
144
+ *
145
+ * await scene.show({ text: 'new' }); // show `new`
146
+ * await scene.show(); // show `default`
147
+ * ```
148
+ *
149
+ * @function
150
+ */
151
+ //@ts-expect-error impl generic component
152
+ show = this.#show;
153
+ /** Add a microtask to close the scene. It is useful when close in 'show' */
96
154
  async close() {
97
155
  await 0;
98
- this.#timer?.close();
156
+ this.#timer.stop();
99
157
  }
100
- async#show(patchProps) {
101
- if (this.#timer)
102
- ERR("Scene is showing");
103
- const {
104
- root,
105
- frame_ms,
106
- createTimer = createRAFTimer,
107
- defaultProps,
108
- duration,
109
- close_on,
110
- record_frame_times
111
- } = this.#options;
112
- this.emit("scene:show", { ...defaultProps, ...patchProps });
113
- root.style.transform = "scale(1)";
158
+ async #show(patchProps) {
159
+ const { root, defaultProps } = this.options;
160
+ const newProps = { ...defaultProps, ...patchProps };
161
+ for (const key of Object.keys({ ...this.#props, ...newProps }))
162
+ this.#props[key] = newProps[key];
163
+ this.emit("show");
164
+ root.style.scale = "1";
114
165
  root.focus();
115
- if (typeof close_on !== "undefined") {
116
- const close = () => this.close();
117
- array_normalize(close_on).map((type) => this.on(type, close).once("scene:close", () => this.off(type, close)));
118
- }
119
- let hasMouseType = 0, hasKeyType = 0;
120
- const cleanups = Object.keys(this.listeners).map((type) => {
121
- const DOM_type = prefix2type[type.split(":", 1)[0]] ?? type;
122
- return DOM_type === "keydown" ? !hasKeyType++ && on(root, DOM_type, (e) => this.emit(`key:${e.key}`, e).emit(DOM_type, e)) : DOM_type === "mousedown" ? !hasMouseType++ && on(root, DOM_type, (e) => this.emit(`mouse:${mouseSuffixs[e.button] ?? "unknown"}`, e).emit(DOM_type, e)) : DOM_type && on(root, DOM_type, (e) => this.emit(type, e));
123
- });
124
- this.once("scene:close", () => cleanups.map((fn) => fn && fn()));
125
- const showInfo = createShowInfo();
126
- await (this.#timer = createTimer({
127
- frame_ms,
128
- duration,
129
- onStart: (time) => showInfo.start_time = time,
130
- onFrame: (time) => {
131
- record_frame_times && showInfo.frame_times.push(time);
132
- this.emit("scene:frame", time);
133
- }
134
- })).promise;
135
- return { ...this.emit("scene:close", null).#data?.(), ...showInfo };
166
+ const records = await this.#timer.start(
167
+ (records2) => this.emit("frame", records2[records2.length - 1])
168
+ );
169
+ root.style.scale = "0";
170
+ return {
171
+ ...this.emit("close").data?.(),
172
+ frame_times: records
173
+ };
136
174
  }
137
175
  }
138
- export {
139
- on,
140
- generic,
141
- Scene,
142
- EventEmitter
143
- };
176
+
177
+ export { EventEmitter, Scene, createComponentAdapter, createTimer, generic, getCurrentScene };
package/dist/index.min.js CHANGED
@@ -1,2 +1 @@
1
- /** @psytask/core v1.0.0 cubxx MIT */
2
- var V=Symbol.dispose??Symbol.for("Symbol.dispose");class P{listeners={};[V](){this.emit("dispose",null)}on(T,c){return(this.listeners[T]??=new Set).add(c),this}off(T,c){let x=this.listeners[T];if(x)x.delete(c),x.size===0&&delete this.listeners[T];return this}once(T,c){let x=(k)=>{try{c(k)}finally{this.off(T,x)}};return this.on(T,x),this}emit(T,c){let x=this.listeners[T];if(x)for(let k of[...x])k(c);return this}}var A=(T)=>{throw Error(T)},r=requestAnimationFrame;var C=(T)=>Array.isArray(T)?T:[T];var E=(T,c,x,k)=>(T.addEventListener(c,x,k),()=>T.removeEventListener(c,x,k));var B=()=>({start_time:NaN,frame_times:[]}),F=(T)=>{let c,x,k=(b)=>(T.onFrame(b),typeof T.duration==="number"&&b-c>=T.duration-T.frame_ms*1.5?x():U=r(k)),U=r((b)=>(T.onStart(b),k(c=b)));return{promise:new Promise((b)=>x=()=>(cancelAnimationFrame(U),b())),close:x}},d=(T)=>T,G=["left","middle","right"],J={scene:0,dispose:0,key:"keydown",mouse:"mousedown"};class Q extends P{root;#c;show=this.#j;#x;#b;#T;constructor(T,c){super();let{root:x,defaultProps:k}=this.#b=c;(this.root=x).tabIndex=-1;let U=()=>{x.style.transform="scale(0)",this.#x=c,this.#T=void 0};U();let b=T(k,this.on("scene:close",U).on("dispose",()=>x.remove()));x.append(...C(typeof b==="object"&&"node"in b?(this.#c=b.data,b.node):b))}config(T){return this.#x={...this.#b,...T},this}async close(){await 0,this.#T?.close()}async#j(T){if(this.#T)A("Scene is showing");let{root:c,frame_ms:x,createTimer:k=F,defaultProps:U,duration:b,close_on:H,record_frame_times:L}=this.#x;if(this.emit("scene:show",{...U,...T}),c.style.transform="scale(1)",c.focus(),typeof H<"u"){let j=()=>this.close();C(H).map((K)=>this.on(K,j).once("scene:close",()=>this.off(K,j)))}let z=0,$=0,q=Object.keys(this.listeners).map((j)=>{let K=J[j.split(":",1)[0]]??j;return K==="keydown"?!$++&&E(c,K,(n)=>this.emit(`key:${n.key}`,n).emit(K,n)):K==="mousedown"?!z++&&E(c,K,(n)=>this.emit(`mouse:${G[n.button]??"unknown"}`,n).emit(K,n)):K&&E(c,K,(n)=>this.emit(j,n))});this.once("scene:close",()=>q.map((j)=>j&&j()));let g=B();return await(this.#T=k({frame_ms:x,duration:b,onStart:(j)=>g.start_time=j,onFrame:(j)=>{L&&g.frame_times.push(j),this.emit("scene:frame",j)}})).promise,{...this.emit("scene:close",null).#c?.(),...g}}}export{E as on,d as generic,Q as Scene,P as EventEmitter};
1
+ const m=Symbol.dispose??Symbol.for("Symbol.dispose");class l{listeners={};[m](){this.emit("dispose")}on(t,s){return(this.listeners[t]??=new Set).add(s),this}off(t,s){const e=this.listeners[t];return e&&(e.delete(s),e.size===0&&delete this.listeners[t]),this}once(t,s){const e=n=>{try{s(n)}finally{this.off(t,e)}};return this.on(t,e),this}emit(t,...[s]){const e=this.listeners[t];if(e)for(const n of[...e])n(s);return this}}const f=o=>{throw Error(o)},h=requestAnimationFrame,u=Object,y=(o,t)=>u.assign(o,t),w=document;new Proxy({},{get:(o,t)=>s=>y(w.createElement(t),s)});const b=o=>Array.isArray(o)?o:[o],g=o=>{const t={start:s=>new Promise(e=>{t.stop=()=>(cancelAnimationFrame(r),e(n));const n=[],i=c=>{n.push(c),s?.(n),o(n)?t.stop():r=h(i)};let r=h(i)}),stop(){}};return t},S=o=>o,A=o=>({wrap:t=>(s=>t(o(s))),render:(t,s,e)=>{const n=o(s);e&&a.push(e);const i=t(n);e&&a.pop();let r;const c=b(typeof i!="string"&&"node"in i?(r=i.data,i.node):i);return{props:n,nodes:c,data:r}}}),a=[],E=()=>a[a.length-1]??f("Not found current scene");class P extends l{constructor(t,s){super(),this.options=s;const{root:e,adapter:n,defaultProps:i,timer:r}=s;(this.root=e).tabIndex=-1,e.style.scale="0";const{props:c,nodes:d,data:p}=n.render(t,{...i},this.on("dispose",()=>e.remove()));this.#t=r(),this.#s=c,this.data=p,e.append(...d)}root;#t;#s;data;show=this.#e;async close(){await 0,this.#t.stop()}async#e(t){const{root:s,defaultProps:e}=this.options,n={...e,...t};for(const r of Object.keys({...this.#s,...n}))this.#s[r]=n[r];this.emit("show"),s.style.scale="1",s.focus();const i=await this.#t.start(r=>this.emit("frame",r[r.length-1]));return s.style.scale="0",{...this.emit("close").data?.(),frame_times:i}}}export{l as EventEmitter,P as Scene,A as createComponentAdapter,g as createTimer,S as generic,E as getCurrentScene};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@psytask/core",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "author": "cubxx",
6
6
  "license": "MIT",