UNPKG

psytask

Version:

JavaScript Framework for Psychology tasks

443 lines (435 loc) 12.3 kB
import { EventEmitter, MaybeGenericComponent, Scene, SceneOptions } from '@psytask/core'; export * from '@psytask/core'; import { PropertiesHyphen } from 'csstype'; type LooseObject = Record<string, unknown>; type Merge<T, U> = T extends unknown ? Omit<T, keyof U> & U : never; type Split<T extends string, S extends string> = T extends unknown ? T extends `${infer L}${S}${infer R}` ? [L, ...Split<R, S>] : [T] : never; declare global { interface ObjectConstructor { keys<T>(o: T): (keyof T & string)[]; } interface String { split<T extends string, S extends string>( this: T, separator: S, ): Split<T, S>; } interface Array<T> { map<R>(fn: <E extends T>(e: E, i: number, arr: this) => R): R[]; } interface Performance { getEntriesByType(type: 'resource'): PerformanceResourceTiming[]; } } type Serializer<T extends LooseObject = LooseObject> = { header: (row: T, rows: T[]) => string; body: (row: T, rows: T[]) => string; footer: (rows: T[]) => string; }; declare const serializers: { /** @see {@link https://www.rfc-editor.org/rfc/rfc4180 RFC-4180} */ csv: { header: (row: LooseObject) => string; body: (row: LooseObject) => string; footer: () => string; }; /** @see {@link https://www.json.org JSON} */ json: { header: () => string; body: (row: LooseObject, rows: LooseObject[]) => string; footer: () => string; }; }; declare class Collector<T extends LooseObject> extends EventEmitter<{ add: T; chunk: string; }> { #private; /** @default `data-${Date.now()}.csv` */ readonly filename: string; /** * Map of serializers by file extension * * You can add your own {@link Serializer} to this map. * * @example * * Add Markdown serializer * * ```ts * Collector.serializers['md'] = { * head: (row) => '', // generate header from the first row * body: (row) => '', // generate body from each row * tail: () => '', // generate footer * }; * using dc = new Collector('data.md'); // now you can save to Markdown file * ``` */ static readonly serializers: typeof serializers & Record<string, Serializer>; readonly rows: T[]; /** * Collect, serialize and save data. * * Built-in supports for CSV and JSON formats. You can extend this by * {@link Collector.serializers} or provide `serializer` parameter. */ constructor( /** @default `data-${Date.now()}.csv` */ filename?: string, options?: { /** @default true */ backup_on_leave?: boolean; /** * If not provided, a default {@link Serializer} based on the file * extension will be used. */ serializer?: Serializer<T>; }); /** * Add a data row. For the default serializer, object fields will be * serialized using {@link JSON.stringify}. * * @returns The total serialized data up to now. */ add(row: T): string; /** * Get the final serialized data. * * @example * * Call multiple times * * ```ts * using dc = new Collector('test.csv'); * * dc.add({ a: 1, b: 'hello' }); * dc.final() === 'a,b\n1,hello'; // true * * dc.add({ a: 2, b: 'world' }); * dc.final() === 'a,b\n1,hello\n2,world'; //true * ``` */ final(): string; /** Download final serialized data */ download(suffix?: string): void; } type CloseEventMap = HTMLElementEventMap & { [K in `mouse:${'left' | 'middle' | 'right' | 'unknown'}`]: MouseEvent; } & { [K in `key:${string}`]: KeyboardEvent; }; type ExtendedSceneOptions = { duration?: number; close_on?: keyof CloseEventMap | (keyof CloseEventMap)[]; }; declare class App<T extends { frame_ms: number; } = { frame_ms: number; }> extends EventEmitter { /** Root element of the app */ readonly root: HTMLElement; /** Data will be collected automatically */ readonly data: T & LooseObject; constructor( /** Root element of the app */ root: HTMLElement, /** Data will be collected automatically */ data: T & LooseObject); /** * Create data collector * * @example * * Basic usage * * ```ts * using dc = await app.collector('data.csv'); * dc.add({ name: 'Alice', age: 25 }); * dc.add({ name: 'Bob', age: 30 }); * dc.final(); // get final text * dc.download(); // download data.csv * ``` * * Add listeners * * ```ts * using dc = await app * .collector('data.csv') * .on('add', (row) => { * console.log('add a row', row); * }) * .on('chunk', (chunk) => { * console.log('a chunk of raw is ready', chunk); * }); * ``` * * @see {@link Collector} */ collector<T extends LooseObject>(...e: ConstructorParameters<typeof Collector<T>>): Collector<T>; /** * Create a scene * * @example * * Create text scene * * ```ts * const Component = (props: { text: string }, ctx: Scene<any>) => { * const el = document.createElement('div'); * ctx.on('show', (props) => { * el.textContent = props.text; // update element * }); * return { node: el, data: () => ({ text: el.textContent }) }; // return element and data getter * }; * * // create scene * using scene = app.scene(Component, { * defaultProps: { text: 'default text' }, // default props is required * close_on: 'key: ', // close when space is pressed * duration: 100, // auto close after 100ms * }); * // change props.text and show, then get data * const data = await scene.show({ text: 'new text' }); * ``` * * @see {@link Scene} */ scene<T extends MaybeGenericComponent>(...[component, opts]: ConstructorParameters<typeof Scene<T>> extends [ infer L, infer R extends SceneOptions<T> ] ? [ L, Pick<R, 'defaultProps' | 'adapter'> & Partial<Omit<R, 'defaultProps' | 'adapter'>> & ExtendedSceneOptions ] : never): Scene<T> & { /** Change options one-time */ config(patchOptions: Partial<ExtendedSceneOptions>): Scene<T>; }; } /** * Create app * * @example * * Basic usage * * ```ts * using app = await createApp(); * using dc = app.collector(); * using fixation = app.scene( * (props: {}) => { * const node = document.createElement('div'); * node.textContent = '+'; * return node; * }, * { * defaultProps: {}, * duration: 500, * }, * ); * ``` * * @see {@link App} {@link App.scene} */ declare const createApp: ({ root, alert_on_leave, i18n, frames_count, frame_calcer, }?: Partial<{ /** @default document.createElement('div') */ root: HTMLElement; /** @default true */ alert_on_leave: boolean; i18n: { /** Alert after leaving the page during the FPS detection */ leave_alert_on_fps: string; /** Alert after leaving the page during the task */ leave_alert_on_task: string; /** Alert before close or reload the page, not compatible with IOS */ beforeunload_alert: string; }; /** @default 10 */ frames_count: number; frame_calcer: (durations: number[]) => number; }>) => Promise<App<{ frame_ms: number; leave_count: number; }>>; /** * @example * * Custom iterable builder * * ```ts * const MyIterable = createIterableBuilder(function* (count: number) { * for (let i = 0; i < count; i++) { * const response: string = yield i + 1; // yield number; receive string * } * }); * * const myIterable = MyIterable(3); * for (const trial of myIterable) { * // set current trial response to calculate next value * myIterable.response(`Response to ${trial}`); * } * ``` */ declare const createIterableBuilder: <T extends unknown[], G extends Generator<unknown, unknown, never>, P extends unknown[] = G extends Generator<infer Val, infer Data, infer Res> ? [Val, Data, Res] : never>(gen: (...e: T) => G) => (...e: T) => { [Symbol.iterator]: () => { next(): IteratorResult<P[0], P[1]>; }; response(response: P[2]): void; readonly data: P[1]; }; /** * @example * * Basic usage * * ```ts * for (const value of RandomSampling({ * candidates: [1, 2, 3], * sample: 5, * replace: true, * })) { * console.log(value); * } * ``` */ declare const RandomSampling: <const T>(e_0: { /** Anything to be sampled */ candidates: readonly T[]; /** Size of samples @default candidates.length */ sample?: number; /** With or without replacement @default true */ replace?: boolean; }) => { [Symbol.iterator]: () => { next(): IteratorResult<T, void>; }; response(response: never): void; readonly data: void; }; /** * It will use 1-down-1-up before the first reversal. * * @example * * Basic usage * * ```ts * const staircase = StairCase({ * start: 0, * step: 1, * down: 3, * up: 1, * reversal: 3, * min: 0, * max: 3, * }); * for (const value of staircase) { * console.log(value); * // set current trial response to calculate next value * staircase.response(true); * } * // get data after iteration * const threshold = staircase.data * .filter((e) => e.reversal) * .reduce((acc, e, i, arr) => acc + e.value / arr.length, 0); * ``` */ declare const StairCase: (e_0: { /** Start value */ start: number; /** Step size */ step: number; /** Number of same trials before going down */ down: number; /** Number of same trials before going up */ up: number; /** Number of reversals */ reversals: number; /** Max number of trials */ trials?: number; /** Minimum value */ min?: number; /** Maximum value */ max?: number; }) => { [Symbol.iterator]: () => { next(): IteratorResult<number, { value: number; response: boolean; reversal: boolean; }[]>; }; response(response: boolean): void; readonly data: { value: number; response: boolean; reversal: boolean; }[]; }; /** * CSS styles builder * * @example * * Basic usage * * ```ts * const style = css({ 'background-color': 'red', 'font-size': '16px' }); * // "background-color:red;font-size:16px;" * ``` */ declare const css: (obj: PropertiesHyphen) => string; type EventType<T extends EventTarget, U = keyof T> = U extends `on${infer K}` ? K : never; /** * Add event listener and return cleanup function * * @example * * Listen to window resize event * * ```ts * const cleanup = on(window, 'resize', (e) => {}); * ``` */ 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> ? //eslint-disable-next-line @typescript-eslint/no-explicit-any -- Required for inferring event handler parameters T[P] & {} extends infer F extends (...args: any) => any ? Parameters<F>[0] : never : never) => void, options?: boolean | AddEventListenerOptions) => () => void; /** * Define default props * * @example * * Usage with component * * ```ts * const Component = (props: { a?: number; b?: string }) => { * const p = defaultProps(props, { a: 1, b: 'default' }); * // p.a is number, p.b is string * return ''; * }; * ``` */ declare const defaultProps: <T extends Record<string | symbol, unknown>, U extends Partial<T>>(props: T, defaults: U) => Merge<T, U>; /** * @example * * Basic usage * * ```ts * const durations = await detectFPS({ * leave_alert: leave_alert_on_fps, * frames_count, * }); * const average_frame_ms = * durations.reduce((a, b) => a + b) / durations.length; * ``` */ declare const detectFPS: (options: { root: HTMLElement; frames_count: number; leave_alert: string; }) => Promise<number[]>; export { App, Collector, RandomSampling, StairCase, createApp, createIterableBuilder, css, defaultProps, detectFPS, on }; export type { CloseEventMap, Serializer };