From a9098a8e8bf66b74d76b921b1e56fffbb7aba143 Mon Sep 17 00:00:00 2001 From: JWMB Date: Tue, 27 May 2025 20:47:06 +0200 Subject: [PATCH 1/4] JsPsychConstructorOptions, including prepareDom, main_eventSource --- packages/jspsych/src/JsPsych.ts | 62 +++++++++++++++++-- packages/jspsych/src/index.ts | 5 +- .../modules/plugin-api/KeyboardListenerAPI.ts | 16 +++-- .../jspsych/src/modules/plugin-api/index.ts | 2 +- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/packages/jspsych/src/JsPsych.ts b/packages/jspsych/src/JsPsych.ts index 1a7d519174..931640c46a 100644 --- a/packages/jspsych/src/JsPsych.ts +++ b/packages/jspsych/src/JsPsych.ts @@ -4,7 +4,7 @@ import { Class } from "type-fest"; import { version } from "../package.json"; import { ExtensionManager, ExtensionManagerDependencies } from "./ExtensionManager"; -import { JsPsychData, JsPsychDataDependencies } from "./modules/data"; +import { InteractionRecord, JsPsychData, JsPsychDataDependencies } from "./modules/data"; import { JsPsychExtension } from "./modules/extensions"; import { PluginAPI, createJointPluginAPIObject } from "./modules/plugin-api"; import { JsPsychPlugin } from "./modules/plugins"; @@ -19,11 +19,46 @@ import { TimelineDescription, TimelineNodeDependencies, TimelineVariable, + TrialDescription, TrialResult, } from "./timeline"; import { Timeline } from "./timeline/Timeline"; import { Trial } from "./timeline/Trial"; import { PromiseWrapper } from "./timeline/util"; +import { DataCollection } from "./modules/data/DataCollection"; + +export type PrepareDomResult = { + displayContainerElement: HTMLElement; + displayElement: HTMLElement; + progressBar?: ProgressBar; +}; + +export type JsPsychConstructorOptions = { + display_element?: HTMLElement, + main_eventSource?: GlobalEventSource, + on_finish?: (arg: DataCollection) => void, + on_trial_start?: (arg: TrialDescription) => void, + on_trial_finish?: (arg: TrialResult) => void, + on_data_update?: (arg: TrialResult) => void, + on_interaction_data_update?: (d: InteractionRecord) => void, + on_close?: () => void, + use_webaudio?: boolean, + show_progress_bar?: boolean, + message_progress_bar?: string, + auto_update_progress_bar?: boolean, + default_iti?: number, + minimum_valid_rt?: number, + experiment_width?: number, + override_safe_mode?: boolean, + case_sensitive_responses?: boolean, + extensions?: any[], + prepareDom?: (jsPsych: JsPsych) => Promise +}; + +export interface GlobalEventSource { + addEventListener(type: K, listener: (this: Window, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: Window, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | EventListenerOptions): void; +} export class JsPsych { turk = turk; @@ -40,7 +75,7 @@ export class JsPsych { private citation: any = '__CITATIONS__'; /** Options */ - private options: any = {}; + private options: JsPsychConstructorOptions = {}; /** Experiment timeline */ private timeline?: Timeline; @@ -66,10 +101,11 @@ export class JsPsych { private extensionManager: ExtensionManager; - constructor(options?) { + constructor(options?: JsPsychConstructorOptions) { // override default options if user specifies an option options = { display_element: undefined, + main_eventSource: undefined, on_finish: () => {}, on_trial_start: () => {}, on_trial_finish: () => {}, @@ -140,10 +176,17 @@ export class JsPsych { // create experiment timeline this.timeline = new Timeline(this.timelineDependencies, timeline); - await this.prepareDom(); + if (this.options.prepareDom) { + const pdResult = await this.options.prepareDom(this); + this.displayContainerElement = pdResult.displayContainerElement; + this.displayElement = pdResult.displayElement; + this.progressBar = pdResult.progressBar; + } else { + await this.prepareDom(); + } await this.extensionManager.initializeExtensions(); - document.documentElement.setAttribute("jspsych", "present"); + if (!this.options.prepareDom) document.documentElement.setAttribute("jspsych", "present"); this.experimentStartTime = new Date(); @@ -196,6 +239,10 @@ export class JsPsych { return this.displayContainerElement; } + getMainEventSource(): GlobalEventSource { + return this.options.main_eventSource || this.displayContainerElement; + } + abortExperiment(endMessage?: string, data = {}) { this.endMessage = endMessage; this.timeline.abort(); @@ -344,7 +391,10 @@ export class JsPsych { if (display === null) { console.error("The display_element specified in initJsPsych() does not exist in the DOM."); } else { - options.display_element = display; + if (display instanceof HTMLElement) + options.display_element = display; + else + throw new Error(`display not an HTMLElement`); } } diff --git a/packages/jspsych/src/index.ts b/packages/jspsych/src/index.ts index 15e13fe7d5..7c26e4fcb6 100755 --- a/packages/jspsych/src/index.ts +++ b/packages/jspsych/src/index.ts @@ -1,6 +1,6 @@ // __rollup-babel-import-regenerator-runtime__ -import { JsPsych } from "./JsPsych"; +import { JsPsych, JsPsychConstructorOptions } from "./JsPsych"; import { MigrationError } from "./migration"; // temporary patch for Safari @@ -22,7 +22,7 @@ if ( * @param options The options to pass to the JsPsych constructor * @returns A new JsPsych instance */ -export function initJsPsych(options?) { +export function initJsPsych(options?: JsPsychConstructorOptions) { const jsPsych = new JsPsych(options); // Handle invocations of non-existent v6 methods with migration errors @@ -66,3 +66,4 @@ export type { JsPsychPlugin, PluginInfo, TrialType } from "./modules/plugins"; export { ParameterType } from "./modules/plugins"; export type { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions"; export { DataCollection } from "./modules/data/DataCollection"; +export type { TimelineDescription, TimelineArray } from "./timeline"; \ No newline at end of file diff --git a/packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts b/packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts index 6259d82ca9..43ca351a71 100644 --- a/packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts +++ b/packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts @@ -1,4 +1,5 @@ import autoBind from "auto-bind"; +import { GlobalEventSource } from "src/JsPsych"; export type KeyboardListener = (e: KeyboardEvent) => void; @@ -17,7 +18,7 @@ export interface GetKeyboardResponseOptions { export class KeyboardListenerAPI { constructor( - private getRootElement: () => Element | undefined, + private getEventSource: () => GlobalEventSource | EventTarget | undefined, private areResponsesCaseSensitive: boolean = false, private minimumValidRt = 0 ) { @@ -36,11 +37,16 @@ export class KeyboardListenerAPI { */ private registerRootListeners() { if (!this.areRootListenersRegistered) { - const rootElement = this.getRootElement(); - if (rootElement) { - rootElement.addEventListener("keydown", this.rootKeydownListener); - rootElement.addEventListener("keyup", this.rootKeyupListener); + const source = this.getEventSource(); + if (source) { + if (source instanceof Element && isNaN(parseInt(source.getAttribute("tabIndex")))) { + console.warn("non-numeric tabIndex on interactive element", source); + } + source.addEventListener("keydown", this.rootKeydownListener); + source.addEventListener("keyup", this.rootKeyupListener); this.areRootListenersRegistered = true; + } else { + console.warn("No source available for KeyboardListenerAPI", this.getEventSource); } } } diff --git a/packages/jspsych/src/modules/plugin-api/index.ts b/packages/jspsych/src/modules/plugin-api/index.ts index d09c81fa8c..eb53e0b859 100644 --- a/packages/jspsych/src/modules/plugin-api/index.ts +++ b/packages/jspsych/src/modules/plugin-api/index.ts @@ -9,7 +9,7 @@ import { TimeoutAPI } from "./TimeoutAPI"; export function createJointPluginAPIObject(jsPsych: JsPsych) { const settings = jsPsych.getInitSettings(); const keyboardListenerAPI = new KeyboardListenerAPI( - jsPsych.getDisplayContainerElement, + jsPsych.getMainEventSource, settings.case_sensitive_responses, settings.minimum_valid_rt ); From 700bd49f44d974f09f5bb26eb6f1b6b7ec6a73a0 Mon Sep 17 00:00:00 2001 From: JWMB Date: Tue, 27 May 2025 21:12:28 +0200 Subject: [PATCH 2/4] additional props on JsPsychConstructorOptions, notes to self in README --- README.md | 8 ++++++++ packages/jspsych/src/JsPsych.ts | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c3f7dd7a8..1eb0886473 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,11 @@ The project is currently managed by the core team of Josh de Leeuw ([@jodeleeuw] jsPsych was created by [Josh de Leeuw](https://www.vassar.edu/faculty/jdeleeuw). We're also grateful for the generous support from a [Mozilla Open Source Support award](https://www.mozilla.org/en-US/moss/), which funded development of the library from 2020-2022. + +## Local development +### Testing changes from a dependee project +After modifying a JsPsych package , build it with `npm run build` then run `npm link` +In the dependee project, `npm link ` + +Show all npm symlinks: `npm ls -g --depth=0 --link=true` +Unlink: run `npm unlink -g` in the directory where the link was originally created. diff --git a/packages/jspsych/src/JsPsych.ts b/packages/jspsych/src/JsPsych.ts index 931640c46a..be9bfdbda8 100644 --- a/packages/jspsych/src/JsPsych.ts +++ b/packages/jspsych/src/JsPsych.ts @@ -35,7 +35,6 @@ export type PrepareDomResult = { export type JsPsychConstructorOptions = { display_element?: HTMLElement, - main_eventSource?: GlobalEventSource, on_finish?: (arg: DataCollection) => void, on_trial_start?: (arg: TrialDescription) => void, on_trial_finish?: (arg: TrialResult) => void, @@ -52,7 +51,9 @@ export type JsPsychConstructorOptions = { override_safe_mode?: boolean, case_sensitive_responses?: boolean, extensions?: any[], - prepareDom?: (jsPsych: JsPsych) => Promise + prepareDom?: (jsPsych: JsPsych) => Promise, + main_eventSource?: GlobalEventSource, + [x: string | number | symbol]: unknown; // allow for additional unknown properties }; export interface GlobalEventSource { From 93cba500503b2ad64c7b5a784e5deed0db141185 Mon Sep 17 00:00:00 2001 From: JWMB Date: Tue, 27 May 2025 21:47:31 +0200 Subject: [PATCH 3/4] oops, "Window" leftover from copypaste --- packages/jspsych/src/JsPsych.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jspsych/src/JsPsych.ts b/packages/jspsych/src/JsPsych.ts index be9bfdbda8..79e9232cee 100644 --- a/packages/jspsych/src/JsPsych.ts +++ b/packages/jspsych/src/JsPsych.ts @@ -57,8 +57,8 @@ export type JsPsychConstructorOptions = { }; export interface GlobalEventSource { - addEventListener(type: K, listener: (this: Window, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: Window, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + addEventListener(type: K, listener: (this: GlobalEventSource, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: GlobalEventSource, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | EventListenerOptions): void; } export class JsPsych { From 9719534de696538f13e10145fa6dee30bb84b0b7 Mon Sep 17 00:00:00 2001 From: JWMB Date: Wed, 28 May 2025 07:16:02 +0200 Subject: [PATCH 4/4] adjust test for additional console.warn --- packages/jspsych/tests/data/trialparameters.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jspsych/tests/data/trialparameters.test.ts b/packages/jspsych/tests/data/trialparameters.test.ts index 9a5324f4a4..8985622fb3 100644 --- a/packages/jspsych/tests/data/trialparameters.test.ts +++ b/packages/jspsych/tests/data/trialparameters.test.ts @@ -58,7 +58,7 @@ describe("Trial parameters in the data", () => { await pressKey(" "); - expect(spy).toHaveBeenCalledTimes(4); + expect(spy).toHaveBeenCalledTimes(5); spy.mockRestore(); });