jsPsych/packages/jspsych/src/modules/plugin-api/SimulationAPI.ts
Josh de Leeuw 522aa2cdbf
Adding a simulation mode (#2287)
Implements simulation mode in the core library, supporting API features, and plugin support in most plugins.
2021-11-23 15:12:30 -05:00

182 lines
4.6 KiB
TypeScript

export class SimulationAPI {
dispatchEvent(event: Event) {
document.body.dispatchEvent(event);
}
/**
* Dispatches a `keydown` event for the specified key
* @param key Character code (`.key` property) for the key to press.
*/
keyDown(key: string) {
this.dispatchEvent(new KeyboardEvent("keydown", { key }));
}
/**
* Dispatches a `keyup` event for the specified key
* @param key Character code (`.key` property) for the key to press.
*/
keyUp(key: string) {
this.dispatchEvent(new KeyboardEvent("keyup", { key }));
}
/**
* Dispatches a `keydown` and `keyup` event in sequence to simulate pressing a key.
* @param key Character code (`.key` property) for the key to press.
* @param delay Length of time to wait (ms) before executing action
*/
pressKey(key: string, delay = 0) {
if (delay > 0) {
setTimeout(() => {
this.keyDown(key);
this.keyUp(key);
}, delay);
} else {
this.keyDown(key);
this.keyUp(key);
}
}
/**
* Dispatches `mousedown`, `mouseup`, and `click` events on the target element
* @param target The element to click
* @param delay Length of time to wait (ms) before executing action
*/
clickTarget(target: Element, delay = 0) {
if (delay > 0) {
setTimeout(() => {
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}, delay);
} else {
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
}
/**
* Sets the value of a target text input
* @param target A text input element to fill in
* @param text Text to input
* @param delay Length of time to wait (ms) before executing action
*/
fillTextInput(target: HTMLInputElement, text: string, delay = 0) {
if (delay > 0) {
setTimeout(() => {
target.value = text;
}, delay);
} else {
target.value = text;
}
}
/**
* Picks a valid key from `choices`, taking into account jsPsych-specific
* identifiers like "NO_KEYS" and "ALL_KEYS".
* @param choices Which keys are valid.
* @returns A key selected at random from the valid keys.
*/
getValidKey(choices: "NO_KEYS" | "ALL_KEYS" | Array<string> | Array<Array<string>>) {
const possible_keys = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
" ",
];
let key;
if (choices == "NO_KEYS") {
key = null;
} else if (choices == "ALL_KEYS") {
key = possible_keys[Math.floor(Math.random() * possible_keys.length)];
} else {
const flat_choices = choices.flat();
key = flat_choices[Math.floor(Math.random() * flat_choices.length)];
}
return key;
}
mergeSimulationData(default_data, simulation_options) {
// override any data with data from simulation object
return {
...default_data,
...simulation_options?.data,
};
}
ensureSimulationDataConsistency(trial, data) {
// All RTs must be rounded
if (data.rt) {
data.rt = Math.round(data.rt);
}
// If a trial_duration and rt exist, make sure that the RT is not longer than the trial.
if (trial.trial_duration && data.rt && data.rt > trial.trial_duration) {
data.rt = null;
if (data.response) {
data.response = null;
}
if (data.correct) {
data.correct = false;
}
}
// If trial.choices is NO_KEYS make sure that response and RT are null
if (trial.choices && trial.choices == "NO_KEYS") {
if (data.rt) {
data.rt = null;
}
if (data.response) {
data.response = null;
}
}
// If response is not allowed before stimulus display complete, ensure RT
// is longer than display time.
if (trial.allow_response_before_complete) {
if (trial.sequence_reps && trial.frame_time) {
const min_time = trial.sequence_reps * trial.frame_time * trial.stimuli.length;
if (data.rt < min_time) {
data.rt = null;
data.response = null;
}
}
}
}
}