Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <somepackage>`

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.
63 changes: 57 additions & 6 deletions packages/jspsych/src/JsPsych.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,11 +19,47 @@ 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,
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<PrepareDomResult>,
main_eventSource?: GlobalEventSource,
[x: string | number | symbol]: unknown; // allow for additional unknown properties
};

export interface GlobalEventSource {
addEventListener<K extends keyof GlobalEventHandlersEventMap>(type: K, listener: (this: GlobalEventSource, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof GlobalEventHandlersEventMap>(type: K, listener: (this: GlobalEventSource, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
}

export class JsPsych {
turk = turk;
Expand All @@ -40,7 +76,7 @@ export class JsPsych {
private citation: any = '__CITATIONS__';

/** Options */
private options: any = {};
private options: JsPsychConstructorOptions = {};

/** Experiment timeline */
private timeline?: Timeline;
Expand All @@ -66,10 +102,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: () => {},
Expand Down Expand Up @@ -140,10 +177,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();

Expand Down Expand Up @@ -196,6 +240,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();
Expand Down Expand Up @@ -344,7 +392,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`);
}
}

Expand Down
5 changes: 3 additions & 2 deletions packages/jspsych/src/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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";
16 changes: 11 additions & 5 deletions packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import autoBind from "auto-bind";
import { GlobalEventSource } from "src/JsPsych";

export type KeyboardListener = (e: KeyboardEvent) => void;

Expand All @@ -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
) {
Expand All @@ -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);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/jspsych/src/modules/plugin-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
2 changes: 1 addition & 1 deletion packages/jspsych/tests/data/trialparameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe("Trial parameters in the data", () => {

await pressKey(" ");

expect(spy).toHaveBeenCalledTimes(4);
expect(spy).toHaveBeenCalledTimes(5);
spy.mockRestore();
});

Expand Down