@psytask/core 1.0.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 ADDED
File without changes
@@ -0,0 +1,201 @@
1
+ type LooseObject = Record<string, any>;
2
+ type Merge<T, U> = T extends any ? Omit<T, keyof U> & U : never;
3
+
4
+ declare const symbol: typeof Symbol.dispose;
5
+ /** {@link Disposable} event emitter, use {@link Set} to manage listeners */
6
+ declare class EventEmitter<M extends LooseObject & {
7
+ dispose?: never;
8
+ } = {}, EventMap extends {
9
+ dispose: null;
10
+ } = M & {
11
+ dispose: null;
12
+ }> implements Disposable {
13
+ readonly listeners: {
14
+ readonly [K in keyof EventMap]?: Set<(e: EventMap[K]) => void>;
15
+ };
16
+ [symbol](): void;
17
+ /** Add event listener */
18
+ on<K extends keyof EventMap>(type: K, listener: (evt: EventMap[K]) => void): this;
19
+ /** Remove event listener */
20
+ off<K extends keyof EventMap>(type: K, listener: (evt: EventMap[K]) => void): this;
21
+ /** Add one-time event listener, can not be removed manually */
22
+ once<K extends keyof EventMap>(type: K, listener: (evt: EventMap[K]) => void): this;
23
+ /** Emit event listeners */
24
+ emit<K extends keyof EventMap>(type: K, e: EventMap[K]): this;
25
+ }
26
+
27
+ declare const createShowInfo: () => {
28
+ start_time: number;
29
+ frame_times: number[];
30
+ };
31
+ type SceneShowInfo = ReturnType<typeof createShowInfo>;
32
+ type ForbiddenSceneData = {
33
+ [K in keyof SceneShowInfo]?: never;
34
+ };
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;
43
+ };
44
+ type NodeLike = string | Node;
45
+ /**
46
+ * Scene setup function, only called once when the scene is created.
47
+ *
48
+ * @param props - The reactive props to control the scene display
49
+ * @param ctx - The scene instance, can be used to manage lifecycle
50
+ * @see {@link Scene}
51
+ */
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;
57
+ };
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>;
60
+ /**
61
+ * Provide type infer for generic setup function, do nothing in runtime.
62
+ *
63
+ * @example
64
+ *
65
+ * Support generic scene setup function
66
+ *
67
+ * ```ts
68
+ * using scene = new Scene(
69
+ * generic(<T>(props: T) => ({ node: [], data: () => props })),
70
+ * //...
71
+ * );
72
+ * const data = await scene.show({ text: 'hello' });
73
+ * data; // expect: { text: string }
74
+ * ```
75
+ */
76
+ declare const generic: {
77
+ <P extends LooseObject, D extends LooseObject & ForbiddenSceneData = {}>(f: SceneSetup<P, D>): GenericSceneSetup<P, D>;
78
+ };
79
+ /** @ignore */
80
+ type MaybeGenericSceneSetup = SceneSetup | GenericSceneSetup;
81
+ /**
82
+ * Scene lifecycle and root element event name-value pairs.
83
+ *
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 |
95
+ */
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;
104
+ };
105
+ /** Scene options */
106
+ type SceneOptions<T extends MaybeGenericSceneSetup> = {
107
+ /** Root element */
108
+ root: HTMLDivElement;
109
+ /** Frame duration in milliseconds */
110
+ frame_ms: number;
111
+ /** Default props */
112
+ 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;
121
+ };
122
+ declare class Scene<T extends MaybeGenericSceneSetup> extends EventEmitter<SceneEventMap> {
123
+ #private;
124
+ /** Root element */
125
+ readonly root: HTMLDivElement;
126
+ /**
127
+ * Show the scene and change props one-time
128
+ *
129
+ * @example
130
+ *
131
+ * Basic usage
132
+ *
133
+ * ```ts
134
+ * using scene = new Scene(
135
+ * (props: { text: string }, ctx) => {
136
+ * const node = document.createElement('div');
137
+ * ctx.on('scene:show', (newProps) => {
138
+ * node.textContent = newProps.text;
139
+ * });
140
+ * return node;
141
+ * },
142
+ * {
143
+ * //...
144
+ * defaultProps: { text: 'default' },
145
+ * },
146
+ * );
147
+ * await scene.show({ text: 'new' }); // show `new`
148
+ * await scene.show(); // show `default`
149
+ * ```
150
+ *
151
+ * @function
152
+ */
153
+ show: T extends SceneSetup<infer P, infer D> ? SceneShow<P, D> : T;
154
+ /**
155
+ * @param setup - The {@link SceneSetup scene setup function}
156
+ * @param defaultOptions - Default {@link SceneOptions scene options}
157
+ */
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' */
183
+ close(): Promise<void>;
184
+ }
185
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,143 @@
1
+ /** @psytask/core v1.0.0 cubxx MIT */
2
+
3
+ // src/event-emitter.ts
4
+ var symbol = Symbol.dispose ?? Symbol.for("Symbol.dispose");
5
+
6
+ class EventEmitter {
7
+ listeners = {};
8
+ [symbol]() {
9
+ this.emit("dispose", null);
10
+ }
11
+ on(type, listener) {
12
+ (this.listeners[type] ??= new Set).add(listener);
13
+ return this;
14
+ }
15
+ off(type, listener) {
16
+ const listeners = this.listeners[type];
17
+ if (listeners) {
18
+ listeners.delete(listener);
19
+ listeners.size === 0 && delete this.listeners[type];
20
+ }
21
+ return this;
22
+ }
23
+ once(type, listener) {
24
+ const wrapper = (evt) => {
25
+ try {
26
+ listener(evt);
27
+ } finally {
28
+ this.off(type, wrapper);
29
+ }
30
+ };
31
+ this.on(type, wrapper);
32
+ return this;
33
+ }
34
+ emit(type, e) {
35
+ const listeners = this.listeners[type];
36
+ if (listeners)
37
+ for (const listener of [...listeners])
38
+ listener(e);
39
+ return this;
40
+ }
41
+ }
42
+ // ../../shared/utils.ts
43
+ var ERR = (msg) => {
44
+ throw Error(msg);
45
+ };
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));
51
+
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
61
+ };
62
+ };
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
+
72
+ class Scene extends EventEmitter {
73
+ root;
74
+ #data;
75
+ show = this.#show;
76
+ #options;
77
+ #defaultOptions;
78
+ #timer;
79
+ constructor(setup, defaultOptions) {
80
+ super();
81
+ const { root, defaultProps } = this.#defaultOptions = defaultOptions;
82
+ (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;
95
+ }
96
+ async close() {
97
+ await 0;
98
+ this.#timer?.close();
99
+ }
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)";
114
+ 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 };
136
+ }
137
+ }
138
+ export {
139
+ on,
140
+ generic,
141
+ Scene,
142
+ EventEmitter
143
+ };
@@ -0,0 +1,2 @@
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};
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@psytask/core",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "author": "cubxx",
6
+ "license": "MIT",
7
+ "description": "Core for PsyTask",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ]
18
+ }