jspsych 6.2.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -29
- package/css/jspsych.css +39 -39
- package/dist/JsPsych.d.ts +112 -0
- package/dist/TimelineNode.d.ts +34 -0
- package/dist/index.browser.js +3164 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.min.js +2 -0
- package/dist/index.browser.min.js.map +1 -0
- package/dist/index.cjs +3158 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +3152 -0
- package/dist/index.js.map +1 -0
- package/dist/migration.d.ts +3 -0
- package/dist/modules/data/DataCollection.d.ts +45 -0
- package/dist/modules/data/DataColumn.d.ts +15 -0
- package/dist/modules/data/index.d.ts +25 -0
- package/dist/modules/data/utils.d.ts +3 -0
- package/dist/modules/extensions.d.ts +22 -0
- package/dist/modules/plugin-api/HardwareAPI.d.ts +15 -0
- package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +34 -0
- package/dist/modules/plugin-api/MediaAPI.d.ts +27 -0
- package/dist/modules/plugin-api/SimulationAPI.d.ts +41 -0
- package/dist/modules/plugin-api/TimeoutAPI.d.ts +5 -0
- package/dist/modules/plugin-api/index.d.ts +8 -0
- package/dist/modules/plugins.d.ts +129 -0
- package/dist/modules/randomization.d.ts +35 -0
- package/dist/modules/turk.d.ts +40 -0
- package/dist/modules/utils.d.ts +7 -0
- package/package.json +32 -15
- package/src/JsPsych.ts +884 -0
- package/src/TimelineNode.ts +536 -0
- package/src/index.ts +71 -0
- package/src/migration.ts +37 -0
- package/src/modules/data/DataCollection.ts +198 -0
- package/src/modules/data/DataColumn.ts +86 -0
- package/src/modules/data/index.ts +174 -0
- package/src/modules/data/utils.ts +75 -0
- package/src/modules/extensions.ts +23 -0
- package/src/modules/plugin-api/HardwareAPI.ts +32 -0
- package/src/modules/plugin-api/KeyboardListenerAPI.ts +165 -0
- package/src/modules/plugin-api/MediaAPI.ts +337 -0
- package/src/modules/plugin-api/SimulationAPI.ts +181 -0
- package/src/modules/plugin-api/TimeoutAPI.ts +16 -0
- package/src/modules/plugin-api/index.ts +28 -0
- package/src/modules/plugins.ts +158 -0
- package/src/modules/randomization.ts +327 -0
- package/src/modules/turk.ts +99 -0
- package/src/modules/utils.ts +30 -0
- package/.github/workflows/jest.yml +0 -20
- package/code-of-conduct.md +0 -56
- package/contributors.md +0 -61
- package/docs/CNAME +0 -1
- package/docs/about/about.md +0 -18
- package/docs/about/contributing.md +0 -43
- package/docs/about/license.md +0 -25
- package/docs/about/support.md +0 -7
- package/docs/core_library/jspsych-core.md +0 -661
- package/docs/core_library/jspsych-data.md +0 -589
- package/docs/core_library/jspsych-pluginAPI.md +0 -510
- package/docs/core_library/jspsych-randomization.md +0 -397
- package/docs/core_library/jspsych-turk.md +0 -102
- package/docs/img/blue.png +0 -0
- package/docs/img/folder-setup.png +0 -0
- package/docs/img/folder-with-html.png +0 -0
- package/docs/img/githubreleases.jpg +0 -0
- package/docs/img/jspsych-favicon.png +0 -0
- package/docs/img/jspsych-logo-no-text-mono.svg +0 -493
- package/docs/img/jspsych-logo.jpg +0 -0
- package/docs/img/orange.png +0 -0
- package/docs/img/palmer_stim.png +0 -0
- package/docs/img/progress_bar.png +0 -0
- package/docs/img/visual_search_example.jpg +0 -0
- package/docs/index.md +0 -9
- package/docs/overview/browser-device-support.md +0 -35
- package/docs/overview/callbacks.md +0 -140
- package/docs/overview/data.md +0 -281
- package/docs/overview/exclude-browser.md +0 -32
- package/docs/overview/experiment-options.md +0 -121
- package/docs/overview/fullscreen.md +0 -36
- package/docs/overview/media-preloading.md +0 -91
- package/docs/overview/mturk.md +0 -77
- package/docs/overview/progress-bar.md +0 -110
- package/docs/overview/record-browser-interactions.md +0 -23
- package/docs/overview/running-experiments.md +0 -95
- package/docs/overview/timeline.md +0 -387
- package/docs/overview/trial.md +0 -142
- package/docs/plugins/creating-a-plugin.md +0 -79
- package/docs/plugins/jspsych-animation.md +0 -40
- package/docs/plugins/jspsych-audio-button-response.md +0 -60
- package/docs/plugins/jspsych-audio-keyboard-response.md +0 -58
- package/docs/plugins/jspsych-audio-slider-response.md +0 -53
- package/docs/plugins/jspsych-call-function.md +0 -81
- package/docs/plugins/jspsych-canvas-button-response.md +0 -66
- package/docs/plugins/jspsych-canvas-keyboard-response.md +0 -68
- package/docs/plugins/jspsych-canvas-slider-response.md +0 -89
- package/docs/plugins/jspsych-categorize-animation.md +0 -60
- package/docs/plugins/jspsych-categorize-html.md +0 -52
- package/docs/plugins/jspsych-categorize-image.md +0 -53
- package/docs/plugins/jspsych-cloze.md +0 -45
- package/docs/plugins/jspsych-external-html.md +0 -70
- package/docs/plugins/jspsych-free-sort.md +0 -55
- package/docs/plugins/jspsych-fullscreen.md +0 -57
- package/docs/plugins/jspsych-html-button-response.md +0 -42
- package/docs/plugins/jspsych-html-keyboard-response.md +0 -51
- package/docs/plugins/jspsych-html-slider-response.md +0 -45
- package/docs/plugins/jspsych-iat-html.md +0 -64
- package/docs/plugins/jspsych-iat-image.md +0 -64
- package/docs/plugins/jspsych-image-button-response.md +0 -46
- package/docs/plugins/jspsych-image-keyboard-response.md +0 -57
- package/docs/plugins/jspsych-image-slider-response.md +0 -52
- package/docs/plugins/jspsych-instructions.md +0 -58
- package/docs/plugins/jspsych-maxdiff.md +0 -42
- package/docs/plugins/jspsych-rdk.md +0 -119
- package/docs/plugins/jspsych-reconstruction.md +0 -48
- package/docs/plugins/jspsych-resize.md +0 -39
- package/docs/plugins/jspsych-same-different-html.md +0 -53
- package/docs/plugins/jspsych-same-different-image.md +0 -66
- package/docs/plugins/jspsych-serial-reaction-time-mouse.md +0 -50
- package/docs/plugins/jspsych-serial-reaction-time.md +0 -57
- package/docs/plugins/jspsych-survey-html-form.md +0 -50
- package/docs/plugins/jspsych-survey-likert.md +0 -70
- package/docs/plugins/jspsych-survey-multi-choice.md +0 -48
- package/docs/plugins/jspsych-survey-multi-select.md +0 -53
- package/docs/plugins/jspsych-survey-text.md +0 -63
- package/docs/plugins/jspsych-video-button-response.md +0 -52
- package/docs/plugins/jspsych-video-keyboard-response.md +0 -48
- package/docs/plugins/jspsych-video-slider-response.md +0 -58
- package/docs/plugins/jspsych-visual-search-circle.md +0 -52
- package/docs/plugins/jspsych-vsl-animate-occlusion.md +0 -55
- package/docs/plugins/jspsych-vsl-grid-scene.md +0 -62
- package/docs/plugins/overview.md +0 -111
- package/docs/tutorials/hello-world.md +0 -144
- package/docs/tutorials/rt-task.md +0 -1107
- package/examples/add-to-end-of-timeline.html +0 -32
- package/examples/conditional-and-loop-functions.html +0 -63
- package/examples/css/jquery-ui.css +0 -1225
- package/examples/data-add-properties.html +0 -40
- package/examples/data-as-function.html +0 -36
- package/examples/data-from-timeline.html +0 -45
- package/examples/data-from-url.html +0 -21
- package/examples/demo-flanker.html +0 -108
- package/examples/demo-simple-rt-task.html +0 -104
- package/examples/demos/demo_1.html +0 -29
- package/examples/demos/demo_2.html +0 -43
- package/examples/demos/demo_3.html +0 -58
- package/examples/display-element-to-embed-experiment.html +0 -73
- package/examples/end-active-node.html +0 -52
- package/examples/end-experiment.html +0 -43
- package/examples/exclusions.html +0 -32
- package/examples/external_html/simple_consent.html +0 -4
- package/examples/img/1.gif +0 -0
- package/examples/img/10.gif +0 -0
- package/examples/img/11.gif +0 -0
- package/examples/img/12.gif +0 -0
- package/examples/img/2.gif +0 -0
- package/examples/img/3.gif +0 -0
- package/examples/img/4.gif +0 -0
- package/examples/img/5.gif +0 -0
- package/examples/img/6.gif +0 -0
- package/examples/img/7.gif +0 -0
- package/examples/img/8.gif +0 -0
- package/examples/img/9.gif +0 -0
- package/examples/img/age/of1.jpg +0 -0
- package/examples/img/age/of2.jpg +0 -0
- package/examples/img/age/of3.jpg +0 -0
- package/examples/img/age/om1.jpg +0 -0
- package/examples/img/age/om2.jpg +0 -0
- package/examples/img/age/om3.jpg +0 -0
- package/examples/img/age/yf1.jpg +0 -0
- package/examples/img/age/yf4.jpg +0 -0
- package/examples/img/age/yf5.jpg +0 -0
- package/examples/img/age/ym2.jpg +0 -0
- package/examples/img/age/ym3.jpg +0 -0
- package/examples/img/age/ym5.jpg +0 -0
- package/examples/img/backwardN.gif +0 -0
- package/examples/img/blue.png +0 -0
- package/examples/img/con1.png +0 -0
- package/examples/img/con2.png +0 -0
- package/examples/img/fixation.gif +0 -0
- package/examples/img/happy_face_1.jpg +0 -0
- package/examples/img/happy_face_2.jpg +0 -0
- package/examples/img/happy_face_3.jpg +0 -0
- package/examples/img/happy_face_4.jpg +0 -0
- package/examples/img/inc1.png +0 -0
- package/examples/img/inc2.png +0 -0
- package/examples/img/normalN.gif +0 -0
- package/examples/img/orange.png +0 -0
- package/examples/img/redX.png +0 -0
- package/examples/img/ribbon.jpg +0 -0
- package/examples/img/sad_face_1.jpg +0 -0
- package/examples/img/sad_face_2.jpg +0 -0
- package/examples/img/sad_face_3.jpg +0 -0
- package/examples/img/sad_face_4.jpg +0 -0
- package/examples/js/snap.svg-min.js +0 -21
- package/examples/jspsych-RDK.html +0 -58
- package/examples/jspsych-animation.html +0 -33
- package/examples/jspsych-audio-button-response.html +0 -52
- package/examples/jspsych-audio-keyboard-response.html +0 -62
- package/examples/jspsych-audio-slider-response.html +0 -55
- package/examples/jspsych-call-function.html +0 -32
- package/examples/jspsych-canvas-button-response.html +0 -95
- package/examples/jspsych-canvas-keyboard-response.html +0 -78
- package/examples/jspsych-canvas-slider-response.html +0 -67
- package/examples/jspsych-categorize-animation.html +0 -46
- package/examples/jspsych-categorize-html.html +0 -38
- package/examples/jspsych-categorize-image.html +0 -38
- package/examples/jspsych-cloze.html +0 -42
- package/examples/jspsych-free-sort.html +0 -97
- package/examples/jspsych-fullscreen.html +0 -44
- package/examples/jspsych-html-button-response.html +0 -46
- package/examples/jspsych-html-keyboard-response.html +0 -42
- package/examples/jspsych-html-slider-response.html +0 -53
- package/examples/jspsych-iat.html +0 -510
- package/examples/jspsych-image-button-response.html +0 -84
- package/examples/jspsych-image-keyboard-response.html +0 -78
- package/examples/jspsych-image-slider-response.html +0 -76
- package/examples/jspsych-instructions.html +0 -37
- package/examples/jspsych-maxdiff.html +0 -33
- package/examples/jspsych-reconstruction.html +0 -43
- package/examples/jspsych-resize.html +0 -34
- package/examples/jspsych-same-different-html.html +0 -28
- package/examples/jspsych-same-different-image.html +0 -33
- package/examples/jspsych-serial-reaction-time-mouse.html +0 -98
- package/examples/jspsych-serial-reaction-time.html +0 -54
- package/examples/jspsych-survey-html-form.html +0 -33
- package/examples/jspsych-survey-likert.html +0 -42
- package/examples/jspsych-survey-multi-choice.html +0 -40
- package/examples/jspsych-survey-multi-select.html +0 -42
- package/examples/jspsych-survey-text.html +0 -34
- package/examples/jspsych-video-button-response.html +0 -57
- package/examples/jspsych-video-keyboard-response.html +0 -53
- package/examples/jspsych-video-slider-response.html +0 -55
- package/examples/jspsych-visual-search-circle.html +0 -58
- package/examples/jspsych-vsl-animate-occlusion.html +0 -29
- package/examples/jspsych-vsl-grid-scene.html +0 -41
- package/examples/lexical-decision.html +0 -132
- package/examples/manual-preloading.html +0 -53
- package/examples/pause-unpause.html +0 -33
- package/examples/progress-bar.html +0 -62
- package/examples/sound/hammer.mp3 +0 -0
- package/examples/sound/sound.mp3 +0 -0
- package/examples/sound/speech_blue.mp3 +0 -0
- package/examples/sound/speech_green.mp3 +0 -0
- package/examples/sound/speech_joke.mp3 +0 -0
- package/examples/sound/speech_red.mp3 +0 -0
- package/examples/sound/tone.mp3 +0 -0
- package/examples/timeline-variables-sampling.html +0 -50
- package/examples/timeline-variables.html +0 -55
- package/examples/video/sample_video.mp4 +0 -0
- package/jspsych.js +0 -2796
- package/license.txt +0 -21
- package/mkdocs.yml +0 -104
- package/plugins/jspsych-animation.js +0 -189
- package/plugins/jspsych-audio-button-response.js +0 -247
- package/plugins/jspsych-audio-keyboard-response.js +0 -204
- package/plugins/jspsych-audio-slider-response.js +0 -262
- package/plugins/jspsych-call-function.js +0 -58
- package/plugins/jspsych-canvas-button-response.js +0 -199
- package/plugins/jspsych-canvas-keyboard-response.js +0 -155
- package/plugins/jspsych-canvas-slider-response.js +0 -207
- package/plugins/jspsych-categorize-animation.js +0 -266
- package/plugins/jspsych-categorize-html.js +0 -220
- package/plugins/jspsych-categorize-image.js +0 -222
- package/plugins/jspsych-cloze.js +0 -112
- package/plugins/jspsych-external-html.js +0 -112
- package/plugins/jspsych-free-sort.js +0 -444
- package/plugins/jspsych-fullscreen.js +0 -104
- package/plugins/jspsych-html-button-response.js +0 -188
- package/plugins/jspsych-html-keyboard-response.js +0 -149
- package/plugins/jspsych-html-slider-response.js +0 -202
- package/plugins/jspsych-iat-html.js +0 -284
- package/plugins/jspsych-iat-image.js +0 -286
- package/plugins/jspsych-image-button-response.js +0 -311
- package/plugins/jspsych-image-keyboard-response.js +0 -247
- package/plugins/jspsych-image-slider-response.js +0 -353
- package/plugins/jspsych-instructions.js +0 -237
- package/plugins/jspsych-maxdiff.js +0 -174
- package/plugins/jspsych-rdk.js +0 -1373
- package/plugins/jspsych-reconstruction.js +0 -134
- package/plugins/jspsych-resize.js +0 -166
- package/plugins/jspsych-same-different-html.js +0 -168
- package/plugins/jspsych-same-different-image.js +0 -169
- package/plugins/jspsych-serial-reaction-time-mouse.js +0 -213
- package/plugins/jspsych-serial-reaction-time.js +0 -247
- package/plugins/jspsych-survey-html-form.js +0 -171
- package/plugins/jspsych-survey-likert.js +0 -195
- package/plugins/jspsych-survey-multi-choice.js +0 -208
- package/plugins/jspsych-survey-multi-select.js +0 -232
- package/plugins/jspsych-survey-text.js +0 -185
- package/plugins/jspsych-video-button-response.js +0 -320
- package/plugins/jspsych-video-keyboard-response.js +0 -279
- package/plugins/jspsych-video-slider-response.js +0 -351
- package/plugins/jspsych-visual-search-circle.js +0 -259
- package/plugins/jspsych-vsl-animate-occlusion.js +0 -196
- package/plugins/jspsych-vsl-grid-scene.js +0 -103
- package/plugins/template/jspsych-plugin-template.js +0 -35
- package/tests/README.md +0 -7
- package/tests/jsPsych/default-iti.test.js +0 -51
- package/tests/jsPsych/default-parameters.test.js +0 -58
- package/tests/jsPsych/endexperiment.test.js +0 -49
- package/tests/jsPsych/events.test.js +0 -369
- package/tests/jsPsych/init.test.js +0 -48
- package/tests/jsPsych/loads.test.js +0 -7
- package/tests/jsPsych/min-rt.test.js +0 -58
- package/tests/jsPsych/progressbar.test.js +0 -202
- package/tests/jsPsych/timeline-variables.test.js +0 -254
- package/tests/jsPsych/timelines.test.js +0 -498
- package/tests/jsPsych.data/datacollection.test.js +0 -116
- package/tests/jsPsych.data/datacolumn.test.js +0 -50
- package/tests/jsPsych.data/datamodule.test.js +0 -152
- package/tests/jsPsych.data/dataparameter.test.js +0 -251
- package/tests/jsPsych.data/interactions.test.js +0 -109
- package/tests/jsPsych.pluginAPI/pluginapi.test.js +0 -144
- package/tests/jsPsych.randomization/randomziation.test.js +0 -27
- package/tests/jsPsych.utils/utils.test.js +0 -58
- package/tests/media/blue.png +0 -0
- package/tests/media/orange.png +0 -0
- package/tests/media/sample_video.mp4 +0 -0
- package/tests/media/sound.mp3 +0 -0
- package/tests/plugins/plugin-animation.test.js +0 -35
- package/tests/plugins/plugin-audio-button-response.test.js +0 -15
- package/tests/plugins/plugin-audio-keyboard-response.test.js +0 -15
- package/tests/plugins/plugin-audio-slider-response.test.js +0 -15
- package/tests/plugins/plugin-call-function.test.js +0 -49
- package/tests/plugins/plugin-categorize-animation.test.js +0 -274
- package/tests/plugins/plugin-categorize-html.test.js +0 -17
- package/tests/plugins/plugin-categorize-image.test.js +0 -17
- package/tests/plugins/plugin-cloze.test.js +0 -140
- package/tests/plugins/plugin-free-sort.test.js +0 -112
- package/tests/plugins/plugin-fullscreen.test.js +0 -41
- package/tests/plugins/plugin-html-button-response.test.js +0 -161
- package/tests/plugins/plugin-html-keyboard-response.test.js +0 -139
- package/tests/plugins/plugin-html-slider-response.test.js +0 -155
- package/tests/plugins/plugin-iat-html.test.js +0 -328
- package/tests/plugins/plugin-iat-image.test.js +0 -308
- package/tests/plugins/plugin-image-button-response.test.js +0 -183
- package/tests/plugins/plugin-image-keyboard-response.test.js +0 -154
- package/tests/plugins/plugin-image-slider-response.test.js +0 -183
- package/tests/plugins/plugin-instructions.test.js +0 -66
- package/tests/plugins/plugin-maxdiff.test.js +0 -39
- package/tests/plugins/plugin-rdk.test.js +0 -17
- package/tests/plugins/plugin-reconstruction.test.js +0 -16
- package/tests/plugins/plugin-resize.test.js +0 -16
- package/tests/plugins/plugin-same-different-html.test.js +0 -17
- package/tests/plugins/plugin-same-different-image.test.js +0 -17
- package/tests/plugins/plugin-serial-reaction-time-mouse.test.js +0 -42
- package/tests/plugins/plugin-serial-reaction-time.test.js +0 -69
- package/tests/plugins/plugin-survey-html-form.test.js +0 -44
- package/tests/plugins/plugin-survey-likert.test.js +0 -48
- package/tests/plugins/plugin-survey-multi-choice.test.js +0 -48
- package/tests/plugins/plugin-survey-multi-select.test.js +0 -72
- package/tests/plugins/plugin-survey-text.test.js +0 -115
- package/tests/plugins/plugin-video-button-response.test.js +0 -35
- package/tests/plugins/plugin-video-keyboard-response.test.js +0 -35
- package/tests/plugins/plugin-video-slider-response.test.js +0 -34
- package/tests/plugins/plugin-visual-search-circle.test.js +0 -16
- package/tests/plugins/plugin-vsl-animate-occlusion.test.js +0 -16
- package/tests/plugins/plugin-vsl-grid-scene.test.js +0 -16
- package/tests/testing-utils.js +0 -13
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import autoBind from "auto-bind";
|
|
2
|
+
|
|
3
|
+
export type KeyboardListener = (e: KeyboardEvent) => void;
|
|
4
|
+
|
|
5
|
+
export type ValidResponses = string[] | "ALL_KEYS" | "NO_KEYS";
|
|
6
|
+
|
|
7
|
+
export interface GetKeyboardResponseOptions {
|
|
8
|
+
callback_function: any;
|
|
9
|
+
valid_responses?: ValidResponses;
|
|
10
|
+
rt_method?: "performance" | "audio";
|
|
11
|
+
persist?: boolean;
|
|
12
|
+
audio_context?: AudioContext;
|
|
13
|
+
audio_context_start_time?: number;
|
|
14
|
+
allow_held_key?: boolean;
|
|
15
|
+
minimum_valid_rt?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class KeyboardListenerAPI {
|
|
19
|
+
constructor(
|
|
20
|
+
private getRootElement: () => Element | undefined,
|
|
21
|
+
private areResponsesCaseSensitive: boolean = false,
|
|
22
|
+
private minimumValidRt = 0
|
|
23
|
+
) {
|
|
24
|
+
autoBind(this);
|
|
25
|
+
this.registerRootListeners();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private listeners = new Set<KeyboardListener>();
|
|
29
|
+
private heldKeys = new Set<string>();
|
|
30
|
+
|
|
31
|
+
private areRootListenersRegistered = false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* If not previously done and `this.getRootElement()` returns an element, adds the root key
|
|
35
|
+
* listeners to that element.
|
|
36
|
+
*/
|
|
37
|
+
private registerRootListeners() {
|
|
38
|
+
if (!this.areRootListenersRegistered) {
|
|
39
|
+
const rootElement = this.getRootElement();
|
|
40
|
+
if (rootElement) {
|
|
41
|
+
rootElement.addEventListener("keydown", this.rootKeydownListener);
|
|
42
|
+
rootElement.addEventListener("keyup", this.rootKeyupListener);
|
|
43
|
+
this.areRootListenersRegistered = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private rootKeydownListener(e: KeyboardEvent) {
|
|
49
|
+
// Iterate over a static copy of the listeners set because listeners might add other listeners
|
|
50
|
+
// that we do not want to be included in the loop
|
|
51
|
+
for (const listener of Array.from(this.listeners)) {
|
|
52
|
+
listener(e);
|
|
53
|
+
}
|
|
54
|
+
this.heldKeys.add(this.toLowerCaseIfInsensitive(e.key));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private toLowerCaseIfInsensitive(string: string) {
|
|
58
|
+
return this.areResponsesCaseSensitive ? string : string.toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private rootKeyupListener(e: KeyboardEvent) {
|
|
62
|
+
this.heldKeys.delete(this.toLowerCaseIfInsensitive(e.key));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private isResponseValid(validResponses: ValidResponses, allowHeldKey: boolean, key: string) {
|
|
66
|
+
// check if key was already held down
|
|
67
|
+
if (!allowHeldKey && this.heldKeys.has(key)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (validResponses === "ALL_KEYS") {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (validResponses === "NO_KEYS") {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return validResponses.includes(key);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getKeyboardResponse({
|
|
82
|
+
callback_function,
|
|
83
|
+
valid_responses = "ALL_KEYS",
|
|
84
|
+
rt_method = "performance",
|
|
85
|
+
persist,
|
|
86
|
+
audio_context,
|
|
87
|
+
audio_context_start_time,
|
|
88
|
+
allow_held_key = false,
|
|
89
|
+
minimum_valid_rt = this.minimumValidRt,
|
|
90
|
+
}: GetKeyboardResponseOptions) {
|
|
91
|
+
if (rt_method !== "performance" && rt_method !== "audio") {
|
|
92
|
+
console.log(
|
|
93
|
+
'Invalid RT method specified in getKeyboardResponse. Defaulting to "performance" method.'
|
|
94
|
+
);
|
|
95
|
+
rt_method = "performance";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const usePerformanceRt = rt_method === "performance";
|
|
99
|
+
const startTime = usePerformanceRt ? performance.now() : audio_context_start_time * 1000;
|
|
100
|
+
|
|
101
|
+
this.registerRootListeners();
|
|
102
|
+
|
|
103
|
+
if (!this.areResponsesCaseSensitive && typeof valid_responses !== "string") {
|
|
104
|
+
valid_responses = valid_responses.map((r) => r.toLowerCase());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const listener: KeyboardListener = (e) => {
|
|
108
|
+
const rt = Math.round(
|
|
109
|
+
(rt_method == "performance" ? performance.now() : audio_context.currentTime * 1000) -
|
|
110
|
+
startTime
|
|
111
|
+
);
|
|
112
|
+
if (rt < minimum_valid_rt) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const key = this.toLowerCaseIfInsensitive(e.key);
|
|
117
|
+
|
|
118
|
+
if (this.isResponseValid(valid_responses, allow_held_key, key)) {
|
|
119
|
+
// if this is a valid response, then we don't want the key event to trigger other actions
|
|
120
|
+
// like scrolling via the spacebar.
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
|
|
123
|
+
if (!persist) {
|
|
124
|
+
// remove keyboard listener if it exists
|
|
125
|
+
this.cancelKeyboardResponse(listener);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
callback_function({ key, rt });
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
this.listeners.add(listener);
|
|
133
|
+
return listener;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
cancelKeyboardResponse(listener: KeyboardListener) {
|
|
137
|
+
// remove the listener from the set of listeners if it is contained
|
|
138
|
+
this.listeners.delete(listener);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
cancelAllKeyboardResponses() {
|
|
142
|
+
this.listeners.clear();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
compareKeys(key1: string | null, key2: string | null) {
|
|
146
|
+
if (
|
|
147
|
+
(typeof key1 !== "string" && key1 !== null) ||
|
|
148
|
+
(typeof key2 !== "string" && key2 !== null)
|
|
149
|
+
) {
|
|
150
|
+
console.error(
|
|
151
|
+
"Error in jsPsych.pluginAPI.compareKeys: arguments must be key strings or null."
|
|
152
|
+
);
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (typeof key1 === "string" && typeof key2 === "string") {
|
|
157
|
+
// if both values are strings, then check whether or not letter case should be converted before comparing (case_sensitive_responses in initJsPsych)
|
|
158
|
+
return this.areResponsesCaseSensitive
|
|
159
|
+
? key1 === key2
|
|
160
|
+
: key1.toLowerCase() === key2.toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return key1 === null && key2 === null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { ParameterType } from "../../modules/plugins";
|
|
2
|
+
import { unique } from "../utils";
|
|
3
|
+
|
|
4
|
+
const preloadParameterTypes = <const>[
|
|
5
|
+
ParameterType.AUDIO,
|
|
6
|
+
ParameterType.IMAGE,
|
|
7
|
+
ParameterType.VIDEO,
|
|
8
|
+
];
|
|
9
|
+
type PreloadType = typeof preloadParameterTypes[number];
|
|
10
|
+
|
|
11
|
+
export class MediaAPI {
|
|
12
|
+
constructor(private useWebaudio: boolean, private webaudioContext?: AudioContext) {}
|
|
13
|
+
|
|
14
|
+
// video //
|
|
15
|
+
private video_buffers = {};
|
|
16
|
+
getVideoBuffer(videoID) {
|
|
17
|
+
return this.video_buffers[videoID];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// audio //
|
|
21
|
+
private context = null;
|
|
22
|
+
private audio_buffers = [];
|
|
23
|
+
|
|
24
|
+
initAudio() {
|
|
25
|
+
this.context = this.useWebaudio ? this.webaudioContext : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
audioContext() {
|
|
29
|
+
if (this.context !== null) {
|
|
30
|
+
if (this.context.state !== "running") {
|
|
31
|
+
this.context.resume();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return this.context;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getAudioBuffer(audioID) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
// check whether audio file already preloaded
|
|
40
|
+
if (
|
|
41
|
+
typeof this.audio_buffers[audioID] == "undefined" ||
|
|
42
|
+
this.audio_buffers[audioID] == "tmp"
|
|
43
|
+
) {
|
|
44
|
+
// if audio is not already loaded, try to load it
|
|
45
|
+
this.preloadAudio(
|
|
46
|
+
[audioID],
|
|
47
|
+
() => {
|
|
48
|
+
resolve(this.audio_buffers[audioID]);
|
|
49
|
+
},
|
|
50
|
+
() => {},
|
|
51
|
+
(e) => {
|
|
52
|
+
reject(e.error);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
// audio is already loaded
|
|
57
|
+
resolve(this.audio_buffers[audioID]);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// preloading stimuli //
|
|
63
|
+
private preload_requests = [];
|
|
64
|
+
|
|
65
|
+
private img_cache = {};
|
|
66
|
+
|
|
67
|
+
preloadAudio(
|
|
68
|
+
files,
|
|
69
|
+
callback_complete = () => {},
|
|
70
|
+
callback_load = (filepath) => {},
|
|
71
|
+
callback_error = (error_msg) => {}
|
|
72
|
+
) {
|
|
73
|
+
files = unique(files.flat());
|
|
74
|
+
|
|
75
|
+
let n_loaded = 0;
|
|
76
|
+
|
|
77
|
+
if (files.length == 0) {
|
|
78
|
+
callback_complete();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const load_audio_file_webaudio = (source, count = 1) => {
|
|
83
|
+
const request = new XMLHttpRequest();
|
|
84
|
+
request.open("GET", source, true);
|
|
85
|
+
request.responseType = "arraybuffer";
|
|
86
|
+
request.onload = () => {
|
|
87
|
+
this.context.decodeAudioData(
|
|
88
|
+
request.response,
|
|
89
|
+
(buffer) => {
|
|
90
|
+
this.audio_buffers[source] = buffer;
|
|
91
|
+
n_loaded++;
|
|
92
|
+
callback_load(source);
|
|
93
|
+
if (n_loaded == files.length) {
|
|
94
|
+
callback_complete();
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
(e) => {
|
|
98
|
+
callback_error({ source: source, error: e });
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
request.onerror = function (e) {
|
|
103
|
+
let err: ProgressEvent | string = e;
|
|
104
|
+
if (this.status == 404) {
|
|
105
|
+
err = "404";
|
|
106
|
+
}
|
|
107
|
+
callback_error({ source: source, error: err });
|
|
108
|
+
};
|
|
109
|
+
request.onloadend = function (e) {
|
|
110
|
+
if (this.status == 404) {
|
|
111
|
+
callback_error({ source: source, error: "404" });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
request.send();
|
|
115
|
+
this.preload_requests.push(request);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const load_audio_file_html5audio = (source, count = 1) => {
|
|
119
|
+
const audio = new Audio();
|
|
120
|
+
const handleCanPlayThrough = () => {
|
|
121
|
+
this.audio_buffers[source] = audio;
|
|
122
|
+
n_loaded++;
|
|
123
|
+
callback_load(source);
|
|
124
|
+
if (n_loaded == files.length) {
|
|
125
|
+
callback_complete();
|
|
126
|
+
}
|
|
127
|
+
audio.removeEventListener("canplaythrough", handleCanPlayThrough);
|
|
128
|
+
};
|
|
129
|
+
audio.addEventListener("canplaythrough", handleCanPlayThrough);
|
|
130
|
+
audio.addEventListener("error", function handleError(e) {
|
|
131
|
+
callback_error({ source: audio.src, error: e });
|
|
132
|
+
audio.removeEventListener("error", handleError);
|
|
133
|
+
});
|
|
134
|
+
audio.addEventListener("abort", function handleAbort(e) {
|
|
135
|
+
callback_error({ source: audio.src, error: e });
|
|
136
|
+
audio.removeEventListener("abort", handleAbort);
|
|
137
|
+
});
|
|
138
|
+
audio.src = source;
|
|
139
|
+
this.preload_requests.push(audio);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
if (typeof this.audio_buffers[file] !== "undefined") {
|
|
144
|
+
n_loaded++;
|
|
145
|
+
callback_load(file);
|
|
146
|
+
if (n_loaded == files.length) {
|
|
147
|
+
callback_complete();
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
this.audio_buffers[file] = "tmp";
|
|
151
|
+
if (this.audioContext() !== null) {
|
|
152
|
+
load_audio_file_webaudio(file);
|
|
153
|
+
} else {
|
|
154
|
+
load_audio_file_html5audio(file);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
preloadImages(
|
|
161
|
+
images,
|
|
162
|
+
callback_complete = () => {},
|
|
163
|
+
callback_load = (filepath) => {},
|
|
164
|
+
callback_error = (error_msg) => {}
|
|
165
|
+
) {
|
|
166
|
+
// flatten the images array
|
|
167
|
+
images = unique(images.flat());
|
|
168
|
+
|
|
169
|
+
var n_loaded = 0;
|
|
170
|
+
|
|
171
|
+
if (images.length === 0) {
|
|
172
|
+
callback_complete();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (var i = 0; i < images.length; i++) {
|
|
177
|
+
var img = new Image();
|
|
178
|
+
|
|
179
|
+
img.onload = function () {
|
|
180
|
+
n_loaded++;
|
|
181
|
+
callback_load(img.src);
|
|
182
|
+
if (n_loaded === images.length) {
|
|
183
|
+
callback_complete();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
img.onerror = function (e) {
|
|
188
|
+
callback_error({ source: img.src, error: e });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
img.src = images[i];
|
|
192
|
+
|
|
193
|
+
this.img_cache[images[i]] = img;
|
|
194
|
+
this.preload_requests.push(img);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
preloadVideo(
|
|
199
|
+
videos,
|
|
200
|
+
callback_complete = () => {},
|
|
201
|
+
callback_load = (filepath) => {},
|
|
202
|
+
callback_error = (error_msg) => {}
|
|
203
|
+
) {
|
|
204
|
+
// flatten the video array
|
|
205
|
+
videos = unique(videos.flat());
|
|
206
|
+
|
|
207
|
+
let n_loaded = 0;
|
|
208
|
+
|
|
209
|
+
if (videos.length === 0) {
|
|
210
|
+
callback_complete();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const video of videos) {
|
|
215
|
+
const video_buffers = this.video_buffers;
|
|
216
|
+
|
|
217
|
+
//based on option 4 here: http://dinbror.dk/blog/how-to-preload-entire-html5-video-before-play-solved/
|
|
218
|
+
const request = new XMLHttpRequest();
|
|
219
|
+
request.open("GET", video, true);
|
|
220
|
+
request.responseType = "blob";
|
|
221
|
+
request.onload = function () {
|
|
222
|
+
if (this.status === 200 || this.status === 0) {
|
|
223
|
+
const videoBlob = this.response;
|
|
224
|
+
video_buffers[video] = URL.createObjectURL(videoBlob); // IE10+
|
|
225
|
+
n_loaded++;
|
|
226
|
+
callback_load(video);
|
|
227
|
+
if (n_loaded === videos.length) {
|
|
228
|
+
callback_complete();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
request.onerror = function (e) {
|
|
233
|
+
let err: ProgressEvent | string = e;
|
|
234
|
+
if (this.status == 404) {
|
|
235
|
+
err = "404";
|
|
236
|
+
}
|
|
237
|
+
callback_error({ source: video, error: err });
|
|
238
|
+
};
|
|
239
|
+
request.onloadend = function (e) {
|
|
240
|
+
if (this.status == 404) {
|
|
241
|
+
callback_error({ source: video, error: "404" });
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
request.send();
|
|
245
|
+
this.preload_requests.push(request);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private preloadMap = new Map<string, Record<string, PreloadType>>();
|
|
250
|
+
|
|
251
|
+
getAutoPreloadList(timeline_description: any[]) {
|
|
252
|
+
/** Map each preload parameter type to a set of paths to be preloaded */
|
|
253
|
+
const preloadPaths = Object.fromEntries(
|
|
254
|
+
preloadParameterTypes.map((type) => [type, new Set<string>()])
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const traverseTimeline = (node, inheritedTrialType?) => {
|
|
258
|
+
const isTimeline = typeof node.timeline !== "undefined";
|
|
259
|
+
|
|
260
|
+
if (isTimeline) {
|
|
261
|
+
for (const childNode of node.timeline) {
|
|
262
|
+
traverseTimeline(childNode, node.type ?? inheritedTrialType);
|
|
263
|
+
}
|
|
264
|
+
} else if ((node.type ?? inheritedTrialType)?.info) {
|
|
265
|
+
// node is a trial with type.info set
|
|
266
|
+
|
|
267
|
+
// Get the plugin name and parameters object from the info object
|
|
268
|
+
const { name: pluginName, parameters } = (node.type ?? inheritedTrialType).info;
|
|
269
|
+
|
|
270
|
+
// Extract parameters to be preloaded and their types from parameter info if this has not
|
|
271
|
+
// yet been done for `pluginName`
|
|
272
|
+
if (!this.preloadMap.has(pluginName)) {
|
|
273
|
+
this.preloadMap.set(
|
|
274
|
+
pluginName,
|
|
275
|
+
Object.fromEntries(
|
|
276
|
+
Object.entries<any>(parameters)
|
|
277
|
+
// Filter out parameter entries with media types and a non-false `preload` option
|
|
278
|
+
.filter(
|
|
279
|
+
([_name, { type, preload }]) =>
|
|
280
|
+
preloadParameterTypes.includes(type) && (preload ?? true)
|
|
281
|
+
)
|
|
282
|
+
// Map each entry's value to its parameter type
|
|
283
|
+
.map(([name, { type }]) => [name, type])
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Add preload paths from this trial
|
|
289
|
+
for (const [parameterName, parameterType] of Object.entries(
|
|
290
|
+
this.preloadMap.get(pluginName)
|
|
291
|
+
)) {
|
|
292
|
+
const parameterValue = node[parameterName];
|
|
293
|
+
const elements = preloadPaths[parameterType];
|
|
294
|
+
|
|
295
|
+
if (typeof parameterValue === "string") {
|
|
296
|
+
elements.add(parameterValue);
|
|
297
|
+
} else if (Array.isArray(parameterValue)) {
|
|
298
|
+
for (const element of parameterValue.flat()) {
|
|
299
|
+
if (typeof element === "string") {
|
|
300
|
+
elements.add(element);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
traverseTimeline({ timeline: timeline_description });
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
images: [...preloadPaths[ParameterType.IMAGE]],
|
|
312
|
+
audio: [...preloadPaths[ParameterType.AUDIO]],
|
|
313
|
+
video: [...preloadPaths[ParameterType.VIDEO]],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
cancelPreloads() {
|
|
318
|
+
for (const request of this.preload_requests) {
|
|
319
|
+
request.onload = () => {};
|
|
320
|
+
request.onerror = () => {};
|
|
321
|
+
request.oncanplaythrough = () => {};
|
|
322
|
+
request.onabort = () => {};
|
|
323
|
+
}
|
|
324
|
+
this.preload_requests = [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private microphone_recorder: MediaRecorder = null;
|
|
328
|
+
|
|
329
|
+
initializeMicrophoneRecorder(stream: MediaStream) {
|
|
330
|
+
const recorder = new MediaRecorder(stream);
|
|
331
|
+
this.microphone_recorder = recorder;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
getMicrophoneRecorder(): MediaRecorder {
|
|
335
|
+
return this.microphone_recorder;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export class SimulationAPI {
|
|
2
|
+
dispatchEvent(event: Event) {
|
|
3
|
+
document.body.dispatchEvent(event);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dispatches a `keydown` event for the specified key
|
|
8
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
9
|
+
*/
|
|
10
|
+
keyDown(key: string) {
|
|
11
|
+
this.dispatchEvent(new KeyboardEvent("keydown", { key }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dispatches a `keyup` event for the specified key
|
|
16
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
17
|
+
*/
|
|
18
|
+
keyUp(key: string) {
|
|
19
|
+
this.dispatchEvent(new KeyboardEvent("keyup", { key }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dispatches a `keydown` and `keyup` event in sequence to simulate pressing a key.
|
|
24
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
25
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
26
|
+
*/
|
|
27
|
+
pressKey(key: string, delay = 0) {
|
|
28
|
+
if (delay > 0) {
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
this.keyDown(key);
|
|
31
|
+
this.keyUp(key);
|
|
32
|
+
}, delay);
|
|
33
|
+
} else {
|
|
34
|
+
this.keyDown(key);
|
|
35
|
+
this.keyUp(key);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dispatches `mousedown`, `mouseup`, and `click` events on the target element
|
|
41
|
+
* @param target The element to click
|
|
42
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
43
|
+
*/
|
|
44
|
+
clickTarget(target: Element, delay = 0) {
|
|
45
|
+
if (delay > 0) {
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
48
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
49
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
50
|
+
}, delay);
|
|
51
|
+
} else {
|
|
52
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
53
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
54
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sets the value of a target text input
|
|
60
|
+
* @param target A text input element to fill in
|
|
61
|
+
* @param text Text to input
|
|
62
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
63
|
+
*/
|
|
64
|
+
fillTextInput(target: HTMLInputElement, text: string, delay = 0) {
|
|
65
|
+
if (delay > 0) {
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
target.value = text;
|
|
68
|
+
}, delay);
|
|
69
|
+
} else {
|
|
70
|
+
target.value = text;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Picks a valid key from `choices`, taking into account jsPsych-specific
|
|
76
|
+
* identifiers like "NO_KEYS" and "ALL_KEYS".
|
|
77
|
+
* @param choices Which keys are valid.
|
|
78
|
+
* @returns A key selected at random from the valid keys.
|
|
79
|
+
*/
|
|
80
|
+
getValidKey(choices: "NO_KEYS" | "ALL_KEYS" | Array<string> | Array<Array<string>>) {
|
|
81
|
+
const possible_keys = [
|
|
82
|
+
"a",
|
|
83
|
+
"b",
|
|
84
|
+
"c",
|
|
85
|
+
"d",
|
|
86
|
+
"e",
|
|
87
|
+
"f",
|
|
88
|
+
"g",
|
|
89
|
+
"h",
|
|
90
|
+
"i",
|
|
91
|
+
"j",
|
|
92
|
+
"k",
|
|
93
|
+
"l",
|
|
94
|
+
"m",
|
|
95
|
+
"n",
|
|
96
|
+
"o",
|
|
97
|
+
"p",
|
|
98
|
+
"q",
|
|
99
|
+
"r",
|
|
100
|
+
"s",
|
|
101
|
+
"t",
|
|
102
|
+
"u",
|
|
103
|
+
"v",
|
|
104
|
+
"w",
|
|
105
|
+
"x",
|
|
106
|
+
"y",
|
|
107
|
+
"z",
|
|
108
|
+
"0",
|
|
109
|
+
"1",
|
|
110
|
+
"2",
|
|
111
|
+
"3",
|
|
112
|
+
"4",
|
|
113
|
+
"5",
|
|
114
|
+
"6",
|
|
115
|
+
"7",
|
|
116
|
+
"8",
|
|
117
|
+
"9",
|
|
118
|
+
" ",
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
let key;
|
|
122
|
+
if (choices == "NO_KEYS") {
|
|
123
|
+
key = null;
|
|
124
|
+
} else if (choices == "ALL_KEYS") {
|
|
125
|
+
key = possible_keys[Math.floor(Math.random() * possible_keys.length)];
|
|
126
|
+
} else {
|
|
127
|
+
const flat_choices = choices.flat();
|
|
128
|
+
key = flat_choices[Math.floor(Math.random() * flat_choices.length)];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return key;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
mergeSimulationData(default_data, simulation_options) {
|
|
135
|
+
// override any data with data from simulation object
|
|
136
|
+
return {
|
|
137
|
+
...default_data,
|
|
138
|
+
...simulation_options?.data,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ensureSimulationDataConsistency(trial, data) {
|
|
143
|
+
// All RTs must be rounded
|
|
144
|
+
if (data.rt) {
|
|
145
|
+
data.rt = Math.round(data.rt);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If a trial_duration and rt exist, make sure that the RT is not longer than the trial.
|
|
149
|
+
if (trial.trial_duration && data.rt && data.rt > trial.trial_duration) {
|
|
150
|
+
data.rt = null;
|
|
151
|
+
if (data.response) {
|
|
152
|
+
data.response = null;
|
|
153
|
+
}
|
|
154
|
+
if (data.correct) {
|
|
155
|
+
data.correct = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If trial.choices is NO_KEYS make sure that response and RT are null
|
|
160
|
+
if (trial.choices && trial.choices == "NO_KEYS") {
|
|
161
|
+
if (data.rt) {
|
|
162
|
+
data.rt = null;
|
|
163
|
+
}
|
|
164
|
+
if (data.response) {
|
|
165
|
+
data.response = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If response is not allowed before stimulus display complete, ensure RT
|
|
170
|
+
// is longer than display time.
|
|
171
|
+
if (trial.allow_response_before_complete) {
|
|
172
|
+
if (trial.sequence_reps && trial.frame_time) {
|
|
173
|
+
const min_time = trial.sequence_reps * trial.frame_time * trial.stimuli.length;
|
|
174
|
+
if (data.rt < min_time) {
|
|
175
|
+
data.rt = null;
|
|
176
|
+
data.response = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class TimeoutAPI {
|
|
2
|
+
private timeout_handlers = [];
|
|
3
|
+
|
|
4
|
+
setTimeout(callback, delay) {
|
|
5
|
+
const handle = window.setTimeout(callback, delay);
|
|
6
|
+
this.timeout_handlers.push(handle);
|
|
7
|
+
return handle;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
clearAllTimeouts() {
|
|
11
|
+
for (const handler of this.timeout_handlers) {
|
|
12
|
+
clearTimeout(handler);
|
|
13
|
+
}
|
|
14
|
+
this.timeout_handlers = [];
|
|
15
|
+
}
|
|
16
|
+
}
|