UNPKG

psytask

Version:

JavaScript Framework for Psychology task

164 lines (153 loc) 4.89 kB
import autoBind from 'auto-bind'; import type { PluginInfo, TrialType } from 'jspsych'; import { KeyboardListenerAPI } from '../../../../node_modules/jspsych/src/modules/plugin-api/KeyboardListenerAPI'; import { TimeoutAPI } from '../../../../node_modules/jspsych/src/modules/plugin-api/TimeoutAPI'; import { ParameterType } from '../../../../node_modules/jspsych/src/modules/plugins'; import type { LooseObject } from '../../types'; import { effect } from '../reactive'; import { type Component } from '../scene'; import { h, hasOwn, proxyNonKey } from '../util'; declare global { interface Window { /** Compatible with jspsych cdn */ jsPsychModule: any; } } /** * Create a scene with jsPsych Plugin * * This function provides a compatibility layer for using jsPsych plugins within * psytask. It handles the integration between jsPsych plugin API and psytask's * scene system. * * @example * * ```ts * using scene = app.scene(jsPsychStim, { * defaultProps: { * type: jsPsychHtmlKeyboardResponse, * stimulus: 'default', * choices: ['f', 'j'], * }, * }); * await scene.show({ stimulus: 'new' }); // change stimulus * ``` * * @see {@link https://www.jspsych.org/latest/plugins/ | jsPsych Plugin} */ export const jsPsychStim = function (trial: TrialType<PluginInfo>, ctx) { /** * Add jsPsychModule in CDN browser build * * @remarks * This is required for compatibility with CDN builds of jsPsych * @see https://cdn.jsdelivr.net/npm/jspsych/dist/index.browser.js */ if (process.env.NODE_ENV === 'production') { window['jsPsychModule'] ??= { ParameterType }; } let data: LooseObject; // create jsPsych DOM const content = h('div', { id: 'jspsych-content', className: 'jspsych-content', }); effect(() => { const Plugin = trial.type as Extract< TrialType<PluginInfo>['type'], new (...args: any[]) => any > & { info: PluginInfo }; if ( typeof Plugin !== 'function' || typeof Plugin.prototype === 'undefined' || typeof Plugin.info === 'undefined' ) { throw new Error( `jsPsych trial.type only supports jsPsych class plugins, but got ${Plugin}`, ); } // unsupported parameters if (process.env.NODE_ENV === 'development') { const unsupportedParams = new Set([ 'extensions', 'record_data', 'save_timeline_variables', 'save_trial_parameters', 'simulation_options', ]); for (const key in trial) { if (hasOwn(trial, key) && unsupportedParams.has(key)) { console.warn(`jsPsych trial "${key}" parameter is not supported`); } } } // set default parameters for (const key in Plugin.info.parameters) { if (!hasOwn(trial, key)) { //@ts-ignore trial[key] = Plugin.info.parameters[key]!.default; } } // mock jsPsych API const mock_jsPsychPluginAPI = [ new KeyboardListenerAPI(() => ctx.root), new TimeoutAPI(), ].reduce((api, item) => Object.assign(api, autoBind(item)), {}); const mock_jsPsych = { finishTrial(_data: LooseObject) { data = Object.assign({}, trial.data, _data); trial.on_finish?.(data); if (typeof trial.post_trial_gap === 'number') { window.setTimeout(() => ctx.close(), trial.post_trial_gap); } else { ctx.close(); } }, pluginAPI: process.env.NODE_ENV === 'production' ? mock_jsPsychPluginAPI : proxyNonKey(mock_jsPsychPluginAPI, (key) => { console.warn( `jsPsych.pluginAPI.${key.toString()} is not supported, only supports: ${Object.keys( mock_jsPsychPluginAPI, ).join(', ')}`, ); }), }; // on start trial.on_start?.(trial); // change css classes content.className = 'jspsych-content'; const classes = trial.css_classes; if (typeof classes === 'string') { content.classList.add(classes); } else if (Array.isArray(classes)) { content.classList.add(...classes); } // execute trial content.innerHTML = ''; // clear content const plugin = new Plugin( process.env.NODE_ENV === 'production' ? mock_jsPsych : proxyNonKey(mock_jsPsych, (key) => { console.warn( `jsPsych.${key.toString()} is not supported, only supports: ${Object.keys(mock_jsPsych).join(', ')}`, ); }), ); plugin.trial(content, trial, () => { trial.on_load?.(); }); }); return { node: h( 'div', { className: 'jspsych-display-element', style: { height: '100%', width: '100%' }, }, h('div', { className: 'jspsych-content-wrapper' }, content), ), data: () => data, }; } satisfies Component;