jspsych 6.3.1 → 7.1.2
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 +36 -37
- 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 +3171 -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 +3165 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +3159 -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 +136 -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 +165 -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 -719
- package/docs/core_library/jspsych-data.md +0 -587
- package/docs/core_library/jspsych-pluginAPI.md +0 -624
- package/docs/core_library/jspsych-randomization.md +0 -389
- package/docs/core_library/jspsych-turk.md +0 -98
- package/docs/extensions/extensions.md +0 -83
- package/docs/extensions/jspsych-ext-webgazer.md +0 -137
- package/docs/img/blue.png +0 -0
- package/docs/img/devtools-change-css.png +0 -0
- package/docs/img/devtools-css-errors.png +0 -0
- package/docs/img/devtools-inspect-element.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/prolific-study-completion.png +0 -0
- package/docs/img/prolific-study-link.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 -180
- package/docs/overview/data.md +0 -281
- package/docs/overview/dynamic-parameters.md +0 -147
- package/docs/overview/exclude-browser.md +0 -32
- package/docs/overview/experiment-options.md +0 -149
- package/docs/overview/eye-tracking.md +0 -271
- package/docs/overview/fullscreen.md +0 -36
- package/docs/overview/media-preloading.md +0 -369
- package/docs/overview/mturk.md +0 -77
- package/docs/overview/plugins.md +0 -320
- package/docs/overview/progress-bar.md +0 -110
- package/docs/overview/prolific.md +0 -78
- package/docs/overview/record-browser-interactions.md +0 -23
- package/docs/overview/running-experiments.md +0 -95
- package/docs/overview/style.md +0 -293
- package/docs/overview/timeline.md +0 -457
- 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 -53
- 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 -56
- 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 -48
- package/docs/plugins/jspsych-image-keyboard-response.md +0 -58
- package/docs/plugins/jspsych-image-slider-response.md +0 -54
- package/docs/plugins/jspsych-instructions.md +0 -58
- package/docs/plugins/jspsych-maxdiff.md +0 -41
- package/docs/plugins/jspsych-preload.md +0 -128
- 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 -52
- 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 -54
- package/docs/plugins/jspsych-video-keyboard-response.md +0 -50
- package/docs/plugins/jspsych-video-slider-response.md +0 -60
- package/docs/plugins/jspsych-virtual-chinrest.md +0 -105
- 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/jspsych-webgazer-calibrate.md +0 -61
- package/docs/plugins/jspsych-webgazer-init-camera.md +0 -30
- package/docs/plugins/jspsych-webgazer-validate.md +0 -44
- package/docs/plugins/list-of-plugins.md +0 -54
- package/docs/tutorials/hello-world.md +0 -162
- package/docs/tutorials/rt-task.md +0 -1334
- package/docs/tutorials/video-tutorials.md +0 -11
- package/examples/add-to-end-of-timeline.html +0 -38
- package/examples/case-sensitive-responses.html +0 -45
- package/examples/conditional-and-loop-functions.html +0 -64
- package/examples/css/jquery-ui.css +0 -1225
- package/examples/css-classes-parameter.html +0 -145
- package/examples/data-add-properties.html +0 -44
- package/examples/data-as-function.html +0 -39
- package/examples/data-from-timeline.html +0 -52
- package/examples/data-from-url.html +0 -21
- package/examples/demo-flanker.html +0 -117
- package/examples/demo-simple-rt-task.html +0 -120
- package/examples/demos/demo_1.html +0 -35
- package/examples/demos/demo_2.html +0 -50
- package/examples/demos/demo_3.html +0 -63
- package/examples/display-element-to-embed-experiment.html +0 -79
- package/examples/end-active-node.html +0 -52
- package/examples/end-experiment.html +0 -45
- 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/card.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/js/webgazer/ridgeWorker.mjs +0 -135
- package/examples/js/webgazer/webgazer.js +0 -88909
- package/examples/js/webgazer/worker_scripts/mat.js +0 -306
- package/examples/js/webgazer/worker_scripts/util.js +0 -398
- package/examples/jspsych-RDK.html +0 -58
- package/examples/jspsych-animation.html +0 -39
- package/examples/jspsych-audio-button-response.html +0 -58
- package/examples/jspsych-audio-keyboard-response.html +0 -68
- package/examples/jspsych-audio-slider-response.html +0 -61
- 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 -49
- package/examples/jspsych-categorize-html.html +0 -33
- package/examples/jspsych-categorize-image.html +0 -44
- package/examples/jspsych-cloze.html +0 -37
- package/examples/jspsych-free-sort.html +0 -109
- package/examples/jspsych-fullscreen.html +0 -45
- package/examples/jspsych-html-button-response.html +0 -43
- package/examples/jspsych-html-keyboard-response.html +0 -42
- package/examples/jspsych-html-slider-response.html +0 -53
- package/examples/jspsych-iat.html +0 -520
- package/examples/jspsych-image-button-response.html +0 -91
- package/examples/jspsych-image-keyboard-response.html +0 -85
- package/examples/jspsych-image-slider-response.html +0 -85
- package/examples/jspsych-instructions.html +0 -37
- package/examples/jspsych-maxdiff.html +0 -33
- package/examples/jspsych-preload.html +0 -140
- 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 -39
- 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 -65
- package/examples/jspsych-video-keyboard-response.html +0 -61
- package/examples/jspsych-video-slider-response.html +0 -63
- package/examples/jspsych-virtual-chinrest.html +0 -69
- package/examples/jspsych-visual-search-circle.html +0 -64
- package/examples/jspsych-vsl-animate-occlusion.html +0 -35
- package/examples/jspsych-vsl-grid-scene.html +0 -47
- package/examples/lexical-decision.html +0 -134
- package/examples/manual-preloading.html +0 -59
- package/examples/pause-unpause.html +0 -33
- package/examples/progress-bar.html +0 -68
- package/examples/save-trial-parameters.html +0 -98
- 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 -64
- package/examples/video/sample_video.mp4 +0 -0
- package/examples/webgazer.html +0 -174
- package/examples/webgazer_audio.html +0 -90
- package/examples/webgazer_image.html +0 -60
- package/extensions/jspsych-ext-webgazer.js +0 -265
- package/jspsych.js +0 -3023
- package/license.txt +0 -21
- package/mkdocs.yml +0 -118
- package/plugins/jspsych-animation.js +0 -189
- package/plugins/jspsych-audio-button-response.js +0 -269
- package/plugins/jspsych-audio-keyboard-response.js +0 -209
- package/plugins/jspsych-audio-slider-response.js +0 -278
- 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 -478
- package/plugins/jspsych-fullscreen.js +0 -106
- 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 -327
- package/plugins/jspsych-image-keyboard-response.js +0 -263
- package/plugins/jspsych-image-slider-response.js +0 -369
- package/plugins/jspsych-instructions.js +0 -237
- package/plugins/jspsych-maxdiff.js +0 -173
- package/plugins/jspsych-preload.js +0 -345
- 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 -212
- 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 -335
- package/plugins/jspsych-video-keyboard-response.js +0 -279
- package/plugins/jspsych-video-slider-response.js +0 -351
- package/plugins/jspsych-virtual-chinrest.js +0 -471
- 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/jspsych-webgazer-calibrate.js +0 -161
- package/plugins/jspsych-webgazer-init-camera.js +0 -139
- package/plugins/jspsych-webgazer-validate.js +0 -314
- package/plugins/template/jspsych-plugin-template.js +0 -35
- package/tests/README.md +0 -7
- package/tests/jsPsych/case-sensitive-responses.test.js +0 -53
- package/tests/jsPsych/css-classes-parameter.test.js +0 -107
- 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 -606
- package/tests/jsPsych/functions-as-parameters.test.js +0 -210
- 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 -531
- package/tests/jsPsych/timelines.test.js +0 -569
- package/tests/jsPsych.data/data-csv-conversion.test.js +0 -85
- package/tests/jsPsych.data/data-json-conversion.test.js +0 -120
- package/tests/jsPsych.data/datacollection.test.js +0 -117
- 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.data/trialparameters.test.js +0 -175
- package/tests/jsPsych.extensions/extensions.test.js +0 -207
- package/tests/jsPsych.extensions/test-extension.js +0 -42
- package/tests/jsPsych.pluginAPI/pluginapi.test.js +0 -365
- package/tests/jsPsych.pluginAPI/preloads.test.js +0 -43
- package/tests/jsPsych.randomization/randomziation.test.js +0 -27
- package/tests/jsPsych.utils/utils.test.js +0 -58
- package/tests/plugins/plugin-animation.test.js +0 -34
- 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 -263
- 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 -157
- package/tests/plugins/plugin-free-sort.test.js +0 -106
- 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 -299
- package/tests/plugins/plugin-iat-image.test.js +0 -298
- package/tests/plugins/plugin-image-button-response.test.js +0 -174
- package/tests/plugins/plugin-image-keyboard-response.test.js +0 -147
- package/tests/plugins/plugin-image-slider-response.test.js +0 -174
- package/tests/plugins/plugin-instructions.test.js +0 -85
- package/tests/plugins/plugin-maxdiff.test.js +0 -39
- package/tests/plugins/plugin-preload.test.js +0 -916
- package/tests/plugins/plugin-rdk.test.js +0 -61
- 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 -109
- 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 -47
- package/tests/plugins/plugin-survey-multi-select.test.js +0 -71
- package/tests/plugins/plugin-survey-text.test.js +0 -115
- package/tests/plugins/plugin-video-button-response.test.js +0 -32
- package/tests/plugins/plugin-video-keyboard-response.test.js +0 -32
- package/tests/plugins/plugin-video-slider-response.test.js +0 -31
- 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
package/src/JsPsych.ts
ADDED
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import autoBind from "auto-bind";
|
|
2
|
+
|
|
3
|
+
import { version } from "../package.json";
|
|
4
|
+
import { MigrationError } from "./migration";
|
|
5
|
+
import { JsPsychData } from "./modules/data";
|
|
6
|
+
import { PluginAPI, createJointPluginAPIObject } from "./modules/plugin-api";
|
|
7
|
+
import { ParameterType, universalPluginParameters } from "./modules/plugins";
|
|
8
|
+
import * as randomization from "./modules/randomization";
|
|
9
|
+
import * as turk from "./modules/turk";
|
|
10
|
+
import * as utils from "./modules/utils";
|
|
11
|
+
import { TimelineNode } from "./TimelineNode";
|
|
12
|
+
|
|
13
|
+
function delay(ms: number) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class JsPsych {
|
|
18
|
+
extensions = <any>{};
|
|
19
|
+
turk = turk;
|
|
20
|
+
randomization = randomization;
|
|
21
|
+
utils = utils;
|
|
22
|
+
data: JsPsychData;
|
|
23
|
+
pluginAPI: PluginAPI;
|
|
24
|
+
|
|
25
|
+
version() {
|
|
26
|
+
return version;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//
|
|
30
|
+
// private variables
|
|
31
|
+
//
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* options
|
|
35
|
+
*/
|
|
36
|
+
private opts: any = {};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* experiment timeline
|
|
40
|
+
*/
|
|
41
|
+
private timeline: TimelineNode;
|
|
42
|
+
private timelineDescription: any[];
|
|
43
|
+
|
|
44
|
+
// flow control
|
|
45
|
+
private global_trial_index = 0;
|
|
46
|
+
private current_trial: any = {};
|
|
47
|
+
private current_trial_finished = false;
|
|
48
|
+
|
|
49
|
+
// target DOM element
|
|
50
|
+
private DOM_container: HTMLElement;
|
|
51
|
+
private DOM_target: HTMLElement;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* time that the experiment began
|
|
55
|
+
*/
|
|
56
|
+
private exp_start_time;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* is the experiment paused?
|
|
60
|
+
*/
|
|
61
|
+
private paused = false;
|
|
62
|
+
private waiting = false;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* is the page retrieved directly via file:// protocol (true) or hosted on a server (false)?
|
|
66
|
+
*/
|
|
67
|
+
private file_protocol = false;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Promise that is resolved when `finishExperiment()` is called
|
|
71
|
+
*/
|
|
72
|
+
private finished: Promise<void>;
|
|
73
|
+
private resolveFinishedPromise: () => void;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* is the experiment running in `simulate()` mode
|
|
77
|
+
*/
|
|
78
|
+
private simulation_mode: "data-only" | "visual" = null;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* simulation options passed in via `simulate()`
|
|
82
|
+
*/
|
|
83
|
+
private simulation_options;
|
|
84
|
+
|
|
85
|
+
// storing a single webaudio context to prevent problems with multiple inits
|
|
86
|
+
// of jsPsych
|
|
87
|
+
webaudio_context: AudioContext = null;
|
|
88
|
+
|
|
89
|
+
internal = {
|
|
90
|
+
/**
|
|
91
|
+
* this flag is used to determine whether we are in a scope where
|
|
92
|
+
* jsPsych.timelineVariable() should be executed immediately or
|
|
93
|
+
* whether it should return a function to access the variable later.
|
|
94
|
+
*
|
|
95
|
+
**/
|
|
96
|
+
call_immediate: false,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
constructor(options?) {
|
|
100
|
+
// override default options if user specifies an option
|
|
101
|
+
options = {
|
|
102
|
+
display_element: undefined,
|
|
103
|
+
on_finish: () => {},
|
|
104
|
+
on_trial_start: () => {},
|
|
105
|
+
on_trial_finish: () => {},
|
|
106
|
+
on_data_update: () => {},
|
|
107
|
+
on_interaction_data_update: () => {},
|
|
108
|
+
on_close: () => {},
|
|
109
|
+
use_webaudio: true,
|
|
110
|
+
exclusions: {},
|
|
111
|
+
show_progress_bar: false,
|
|
112
|
+
message_progress_bar: "Completion Progress",
|
|
113
|
+
auto_update_progress_bar: true,
|
|
114
|
+
default_iti: 0,
|
|
115
|
+
minimum_valid_rt: 0,
|
|
116
|
+
experiment_width: null,
|
|
117
|
+
override_safe_mode: false,
|
|
118
|
+
case_sensitive_responses: false,
|
|
119
|
+
extensions: [],
|
|
120
|
+
...options,
|
|
121
|
+
};
|
|
122
|
+
this.opts = options;
|
|
123
|
+
|
|
124
|
+
autoBind(this); // so we can pass JsPsych methods as callbacks and `this` remains the JsPsych instance
|
|
125
|
+
|
|
126
|
+
this.webaudio_context =
|
|
127
|
+
typeof window !== "undefined" && typeof window.AudioContext !== "undefined"
|
|
128
|
+
? new AudioContext()
|
|
129
|
+
: null;
|
|
130
|
+
|
|
131
|
+
// detect whether page is running in browser as a local file, and if so, disable web audio and video preloading to prevent CORS issues
|
|
132
|
+
if (
|
|
133
|
+
window.location.protocol == "file:" &&
|
|
134
|
+
(options.override_safe_mode === false || typeof options.override_safe_mode === "undefined")
|
|
135
|
+
) {
|
|
136
|
+
options.use_webaudio = false;
|
|
137
|
+
this.file_protocol = true;
|
|
138
|
+
console.warn(
|
|
139
|
+
"jsPsych detected that it is running via the file:// protocol and not on a web server. " +
|
|
140
|
+
"To prevent issues with cross-origin requests, Web Audio and video preloading have been disabled. " +
|
|
141
|
+
"If you would like to override this setting, you can set 'override_safe_mode' to 'true' in initJsPsych. " +
|
|
142
|
+
"For more information, see: https://www.jspsych.org/overview/running-experiments"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// initialize modules
|
|
147
|
+
this.data = new JsPsychData(this);
|
|
148
|
+
this.pluginAPI = createJointPluginAPIObject(this);
|
|
149
|
+
|
|
150
|
+
// create instances of extensions
|
|
151
|
+
for (const extension of options.extensions) {
|
|
152
|
+
this.extensions[extension.type.info.name] = new extension.type(this);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// initialize audio context based on options and browser capabilities
|
|
156
|
+
this.pluginAPI.initAudio();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Starts an experiment using the provided timeline and returns a promise that is resolved when
|
|
161
|
+
* the experiment is finished.
|
|
162
|
+
*
|
|
163
|
+
* @param timeline The timeline to be run
|
|
164
|
+
*/
|
|
165
|
+
async run(timeline: any[]) {
|
|
166
|
+
if (typeof timeline === "undefined") {
|
|
167
|
+
console.error("No timeline declared in jsPsych.run. Cannot start experiment.");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (timeline.length === 0) {
|
|
171
|
+
console.error(
|
|
172
|
+
"No trials have been added to the timeline (the timeline is an empty array). Cannot start experiment."
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// create experiment timeline
|
|
177
|
+
this.timelineDescription = timeline;
|
|
178
|
+
this.timeline = new TimelineNode(this, { timeline });
|
|
179
|
+
|
|
180
|
+
await this.prepareDom();
|
|
181
|
+
await this.checkExclusions(this.opts.exclusions);
|
|
182
|
+
await this.loadExtensions(this.opts.extensions);
|
|
183
|
+
|
|
184
|
+
document.documentElement.setAttribute("jspsych", "present");
|
|
185
|
+
|
|
186
|
+
this.startExperiment();
|
|
187
|
+
await this.finished;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async simulate(
|
|
191
|
+
timeline: any[],
|
|
192
|
+
simulation_mode: "data-only" | "visual" = "data-only",
|
|
193
|
+
simulation_options = {}
|
|
194
|
+
) {
|
|
195
|
+
this.simulation_mode = simulation_mode;
|
|
196
|
+
this.simulation_options = simulation_options;
|
|
197
|
+
await this.run(timeline);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getProgress() {
|
|
201
|
+
return {
|
|
202
|
+
total_trials: typeof this.timeline === "undefined" ? undefined : this.timeline.length(),
|
|
203
|
+
current_trial_global: this.global_trial_index,
|
|
204
|
+
percent_complete: typeof this.timeline === "undefined" ? 0 : this.timeline.percentComplete(),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getStartTime() {
|
|
209
|
+
return this.exp_start_time;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getTotalTime() {
|
|
213
|
+
if (typeof this.exp_start_time === "undefined") {
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
return new Date().getTime() - this.exp_start_time.getTime();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getDisplayElement() {
|
|
220
|
+
return this.DOM_target;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getDisplayContainerElement() {
|
|
224
|
+
return this.DOM_container;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
finishTrial(data = {}) {
|
|
228
|
+
if (this.current_trial_finished) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.current_trial_finished = true;
|
|
232
|
+
|
|
233
|
+
// remove any CSS classes that were added to the DOM via css_classes parameter
|
|
234
|
+
if (
|
|
235
|
+
typeof this.current_trial.css_classes !== "undefined" &&
|
|
236
|
+
Array.isArray(this.current_trial.css_classes)
|
|
237
|
+
) {
|
|
238
|
+
this.DOM_target.classList.remove(...this.current_trial.css_classes);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// write the data from the trial
|
|
242
|
+
this.data.write(data);
|
|
243
|
+
|
|
244
|
+
// get back the data with all of the defaults in
|
|
245
|
+
const trial_data = this.data.get().filter({ trial_index: this.global_trial_index });
|
|
246
|
+
|
|
247
|
+
// for trial-level callbacks, we just want to pass in a reference to the values
|
|
248
|
+
// of the DataCollection, for easy access and editing.
|
|
249
|
+
const trial_data_values = trial_data.values()[0];
|
|
250
|
+
|
|
251
|
+
const current_trial = this.current_trial;
|
|
252
|
+
|
|
253
|
+
if (typeof current_trial.save_trial_parameters === "object") {
|
|
254
|
+
for (const key of Object.keys(current_trial.save_trial_parameters)) {
|
|
255
|
+
const key_val = current_trial.save_trial_parameters[key];
|
|
256
|
+
if (key_val === true) {
|
|
257
|
+
if (typeof current_trial[key] === "undefined") {
|
|
258
|
+
console.warn(
|
|
259
|
+
`Invalid parameter specified in save_trial_parameters. Trial has no property called "${key}".`
|
|
260
|
+
);
|
|
261
|
+
} else if (typeof current_trial[key] === "function") {
|
|
262
|
+
trial_data_values[key] = current_trial[key].toString();
|
|
263
|
+
} else {
|
|
264
|
+
trial_data_values[key] = current_trial[key];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (key_val === false) {
|
|
268
|
+
// we don't allow internal_node_id or trial_index to be deleted because it would break other things
|
|
269
|
+
if (key !== "internal_node_id" && key !== "trial_index") {
|
|
270
|
+
delete trial_data_values[key];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// handle extension callbacks
|
|
276
|
+
if (Array.isArray(current_trial.extensions)) {
|
|
277
|
+
for (const extension of current_trial.extensions) {
|
|
278
|
+
const ext_data_values = this.extensions[extension.type.info.name].on_finish(
|
|
279
|
+
extension.params
|
|
280
|
+
);
|
|
281
|
+
Object.assign(trial_data_values, ext_data_values);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// about to execute lots of callbacks, so switch context.
|
|
286
|
+
this.internal.call_immediate = true;
|
|
287
|
+
|
|
288
|
+
// handle callback at plugin level
|
|
289
|
+
if (typeof current_trial.on_finish === "function") {
|
|
290
|
+
current_trial.on_finish(trial_data_values);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// handle callback at whole-experiment level
|
|
294
|
+
this.opts.on_trial_finish(trial_data_values);
|
|
295
|
+
|
|
296
|
+
// after the above callbacks are complete, then the data should be finalized
|
|
297
|
+
// for this trial. call the on_data_update handler, passing in the same
|
|
298
|
+
// data object that just went through the trial's finish handlers.
|
|
299
|
+
this.opts.on_data_update(trial_data_values);
|
|
300
|
+
|
|
301
|
+
// done with callbacks
|
|
302
|
+
this.internal.call_immediate = false;
|
|
303
|
+
|
|
304
|
+
// wait for iti
|
|
305
|
+
if (
|
|
306
|
+
typeof current_trial.post_trial_gap === null ||
|
|
307
|
+
typeof current_trial.post_trial_gap === "undefined"
|
|
308
|
+
) {
|
|
309
|
+
if (this.opts.default_iti > 0) {
|
|
310
|
+
setTimeout(this.nextTrial, this.opts.default_iti);
|
|
311
|
+
} else {
|
|
312
|
+
this.nextTrial();
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
if (current_trial.post_trial_gap > 0) {
|
|
316
|
+
setTimeout(this.nextTrial, current_trial.post_trial_gap);
|
|
317
|
+
} else {
|
|
318
|
+
this.nextTrial();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
endExperiment(end_message = "", data = {}) {
|
|
324
|
+
this.timeline.end_message = end_message;
|
|
325
|
+
this.timeline.end();
|
|
326
|
+
this.pluginAPI.cancelAllKeyboardResponses();
|
|
327
|
+
this.pluginAPI.clearAllTimeouts();
|
|
328
|
+
this.finishTrial(data);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
endCurrentTimeline() {
|
|
332
|
+
this.timeline.endActiveNode();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
getCurrentTrial() {
|
|
336
|
+
return this.current_trial;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
getInitSettings() {
|
|
340
|
+
return this.opts;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getCurrentTimelineNodeID() {
|
|
344
|
+
return this.timeline.activeID();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
timelineVariable(varname: string, immediate = false) {
|
|
348
|
+
if (this.internal.call_immediate || immediate === true) {
|
|
349
|
+
return this.timeline.timelineVariable(varname);
|
|
350
|
+
} else {
|
|
351
|
+
return {
|
|
352
|
+
timelineVariablePlaceholder: true,
|
|
353
|
+
timelineVariableFunction: () => this.timeline.timelineVariable(varname),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getAllTimelineVariables() {
|
|
359
|
+
return this.timeline.allTimelineVariables();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
addNodeToEndOfTimeline(new_timeline, preload_callback?) {
|
|
363
|
+
this.timeline.insert(new_timeline);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
pauseExperiment() {
|
|
367
|
+
this.paused = true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
resumeExperiment() {
|
|
371
|
+
this.paused = false;
|
|
372
|
+
if (this.waiting) {
|
|
373
|
+
this.waiting = false;
|
|
374
|
+
this.nextTrial();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
loadFail(message) {
|
|
379
|
+
message = message || "<p>The experiment failed to load.</p>";
|
|
380
|
+
this.DOM_target.innerHTML = message;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
getSafeModeStatus() {
|
|
384
|
+
return this.file_protocol;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
getTimeline() {
|
|
388
|
+
return this.timelineDescription;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private async prepareDom() {
|
|
392
|
+
// Wait until the document is ready
|
|
393
|
+
if (document.readyState !== "complete") {
|
|
394
|
+
await new Promise((resolve) => {
|
|
395
|
+
window.addEventListener("load", resolve);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const options = this.opts;
|
|
400
|
+
|
|
401
|
+
// set DOM element where jsPsych will render content
|
|
402
|
+
// if undefined, then jsPsych will use the <body> tag and the entire page
|
|
403
|
+
if (typeof options.display_element === "undefined") {
|
|
404
|
+
// check if there is a body element on the page
|
|
405
|
+
const body = document.querySelector("body");
|
|
406
|
+
if (body === null) {
|
|
407
|
+
document.documentElement.appendChild(document.createElement("body"));
|
|
408
|
+
}
|
|
409
|
+
// using the full page, so we need the HTML element to
|
|
410
|
+
// have 100% height, and body to be full width and height with
|
|
411
|
+
// no margin
|
|
412
|
+
document.querySelector("html").style.height = "100%";
|
|
413
|
+
document.querySelector("body").style.margin = "0px";
|
|
414
|
+
document.querySelector("body").style.height = "100%";
|
|
415
|
+
document.querySelector("body").style.width = "100%";
|
|
416
|
+
options.display_element = document.querySelector("body");
|
|
417
|
+
} else {
|
|
418
|
+
// make sure that the display element exists on the page
|
|
419
|
+
const display =
|
|
420
|
+
options.display_element instanceof Element
|
|
421
|
+
? options.display_element
|
|
422
|
+
: document.querySelector("#" + options.display_element);
|
|
423
|
+
if (display === null) {
|
|
424
|
+
console.error("The display_element specified in initJsPsych() does not exist in the DOM.");
|
|
425
|
+
} else {
|
|
426
|
+
options.display_element = display;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
options.display_element.innerHTML =
|
|
431
|
+
'<div class="jspsych-content-wrapper"><div id="jspsych-content"></div></div>';
|
|
432
|
+
this.DOM_container = options.display_element;
|
|
433
|
+
this.DOM_target = document.querySelector("#jspsych-content");
|
|
434
|
+
|
|
435
|
+
// set experiment_width if not null
|
|
436
|
+
if (options.experiment_width !== null) {
|
|
437
|
+
this.DOM_target.style.width = options.experiment_width + "px";
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// add tabIndex attribute to scope event listeners
|
|
441
|
+
options.display_element.tabIndex = 0;
|
|
442
|
+
|
|
443
|
+
// add CSS class to DOM_target
|
|
444
|
+
if (options.display_element.className.indexOf("jspsych-display-element") === -1) {
|
|
445
|
+
options.display_element.className += " jspsych-display-element";
|
|
446
|
+
}
|
|
447
|
+
this.DOM_target.className += "jspsych-content";
|
|
448
|
+
|
|
449
|
+
// create listeners for user browser interaction
|
|
450
|
+
this.data.createInteractionListeners();
|
|
451
|
+
|
|
452
|
+
// add event for closing window
|
|
453
|
+
window.addEventListener("beforeunload", options.on_close);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private async loadExtensions(extensions) {
|
|
457
|
+
// run the .initialize method of any extensions that are in use
|
|
458
|
+
// these should return a Promise to indicate when loading is complete
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
await Promise.all(
|
|
462
|
+
extensions.map((extension) =>
|
|
463
|
+
this.extensions[extension.type.info.name].initialize(extension.params || {})
|
|
464
|
+
)
|
|
465
|
+
);
|
|
466
|
+
} catch (error_message) {
|
|
467
|
+
console.error(error_message);
|
|
468
|
+
throw new Error(error_message);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private startExperiment() {
|
|
473
|
+
this.finished = new Promise((resolve) => {
|
|
474
|
+
this.resolveFinishedPromise = resolve;
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// show progress bar if requested
|
|
478
|
+
if (this.opts.show_progress_bar === true) {
|
|
479
|
+
this.drawProgressBar(this.opts.message_progress_bar);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// record the start time
|
|
483
|
+
this.exp_start_time = new Date();
|
|
484
|
+
|
|
485
|
+
// begin!
|
|
486
|
+
this.timeline.advance();
|
|
487
|
+
this.doTrial(this.timeline.trial());
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private finishExperiment() {
|
|
491
|
+
const finish_result = this.opts.on_finish(this.data.get());
|
|
492
|
+
|
|
493
|
+
const done_handler = () => {
|
|
494
|
+
if (typeof this.timeline.end_message !== "undefined") {
|
|
495
|
+
this.DOM_target.innerHTML = this.timeline.end_message;
|
|
496
|
+
}
|
|
497
|
+
this.resolveFinishedPromise();
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
if (finish_result) {
|
|
501
|
+
Promise.resolve(finish_result).then(done_handler);
|
|
502
|
+
} else {
|
|
503
|
+
done_handler();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private nextTrial() {
|
|
508
|
+
// if experiment is paused, don't do anything.
|
|
509
|
+
if (this.paused) {
|
|
510
|
+
this.waiting = true;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.global_trial_index++;
|
|
515
|
+
|
|
516
|
+
// advance timeline
|
|
517
|
+
this.timeline.markCurrentTrialComplete();
|
|
518
|
+
const complete = this.timeline.advance();
|
|
519
|
+
|
|
520
|
+
// update progress bar if shown
|
|
521
|
+
if (this.opts.show_progress_bar === true && this.opts.auto_update_progress_bar === true) {
|
|
522
|
+
this.updateProgressBar();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// check if experiment is over
|
|
526
|
+
if (complete) {
|
|
527
|
+
this.finishExperiment();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this.doTrial(this.timeline.trial());
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private doTrial(trial) {
|
|
535
|
+
this.current_trial = trial;
|
|
536
|
+
this.current_trial_finished = false;
|
|
537
|
+
|
|
538
|
+
// process all timeline variables for this trial
|
|
539
|
+
this.evaluateTimelineVariables(trial);
|
|
540
|
+
|
|
541
|
+
if (typeof trial.type === "string") {
|
|
542
|
+
throw new MigrationError(
|
|
543
|
+
"A string was provided as the trial's `type` parameter. Since jsPsych v7, the `type` parameter needs to be a plugin object."
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// instantiate the plugin for this trial
|
|
548
|
+
trial.type = {
|
|
549
|
+
// this is a hack to internally keep the old plugin object structure and prevent touching more
|
|
550
|
+
// of the core jspsych code
|
|
551
|
+
...autoBind(new trial.type(this)),
|
|
552
|
+
info: trial.type.info,
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// evaluate variables that are functions
|
|
556
|
+
this.evaluateFunctionParameters(trial);
|
|
557
|
+
|
|
558
|
+
// get default values for parameters
|
|
559
|
+
this.setDefaultValues(trial);
|
|
560
|
+
|
|
561
|
+
// about to execute callbacks
|
|
562
|
+
this.internal.call_immediate = true;
|
|
563
|
+
|
|
564
|
+
// call experiment wide callback
|
|
565
|
+
this.opts.on_trial_start(trial);
|
|
566
|
+
|
|
567
|
+
// call trial specific callback if it exists
|
|
568
|
+
if (typeof trial.on_start === "function") {
|
|
569
|
+
trial.on_start(trial);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// call any on_start functions for extensions
|
|
573
|
+
if (Array.isArray(trial.extensions)) {
|
|
574
|
+
for (const extension of trial.extensions) {
|
|
575
|
+
this.extensions[extension.type.info.name].on_start(extension.params);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// apply the focus to the element containing the experiment.
|
|
580
|
+
this.DOM_container.focus();
|
|
581
|
+
|
|
582
|
+
// reset the scroll on the DOM target
|
|
583
|
+
this.DOM_target.scrollTop = 0;
|
|
584
|
+
|
|
585
|
+
// add CSS classes to the DOM_target if they exist in trial.css_classes
|
|
586
|
+
if (typeof trial.css_classes !== "undefined") {
|
|
587
|
+
if (!Array.isArray(trial.css_classes) && typeof trial.css_classes === "string") {
|
|
588
|
+
trial.css_classes = [trial.css_classes];
|
|
589
|
+
}
|
|
590
|
+
if (Array.isArray(trial.css_classes)) {
|
|
591
|
+
this.DOM_target.classList.add(...trial.css_classes);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// setup on_load event callback
|
|
596
|
+
const load_callback = () => {
|
|
597
|
+
if (typeof trial.on_load === "function") {
|
|
598
|
+
trial.on_load();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// call any on_load functions for extensions
|
|
602
|
+
if (Array.isArray(trial.extensions)) {
|
|
603
|
+
for (const extension of trial.extensions) {
|
|
604
|
+
this.extensions[extension.type.info.name].on_load(extension.params);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
let trial_complete;
|
|
610
|
+
if (!this.simulation_mode) {
|
|
611
|
+
trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
|
|
612
|
+
}
|
|
613
|
+
if (this.simulation_mode) {
|
|
614
|
+
// check if the trial supports simulation
|
|
615
|
+
if (trial.type.simulate) {
|
|
616
|
+
let trial_sim_opts;
|
|
617
|
+
if (!trial.simulation_options) {
|
|
618
|
+
trial_sim_opts = this.simulation_options.default;
|
|
619
|
+
}
|
|
620
|
+
if (trial.simulation_options) {
|
|
621
|
+
if (typeof trial.simulation_options == "string") {
|
|
622
|
+
if (this.simulation_options[trial.simulation_options]) {
|
|
623
|
+
trial_sim_opts = this.simulation_options[trial.simulation_options];
|
|
624
|
+
} else if (this.simulation_options.default) {
|
|
625
|
+
console.log(
|
|
626
|
+
`No matching simulation options found for "${trial.simulation_options}". Using "default" options.`
|
|
627
|
+
);
|
|
628
|
+
trial_sim_opts = this.simulation_options.default;
|
|
629
|
+
} else {
|
|
630
|
+
console.log(
|
|
631
|
+
`No matching simulation options found for "${trial.simulation_options}" and no "default" options provided. Using the default values provided by the plugin.`
|
|
632
|
+
);
|
|
633
|
+
trial_sim_opts = {};
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
trial_sim_opts = trial.simulation_options;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
trial_sim_opts = this.utils.deepCopy(trial_sim_opts);
|
|
640
|
+
trial_sim_opts = this.replaceFunctionsWithValues(trial_sim_opts, null);
|
|
641
|
+
|
|
642
|
+
if (trial_sim_opts?.simulate === false) {
|
|
643
|
+
trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
|
|
644
|
+
} else {
|
|
645
|
+
trial_complete = trial.type.simulate(
|
|
646
|
+
trial,
|
|
647
|
+
trial_sim_opts?.mode || this.simulation_mode,
|
|
648
|
+
trial_sim_opts,
|
|
649
|
+
load_callback
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
// trial doesn't have a simulate method, so just run as usual
|
|
654
|
+
trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// see if trial_complete is a Promise by looking for .then() function
|
|
659
|
+
const is_promise = trial_complete && typeof trial_complete.then == "function";
|
|
660
|
+
|
|
661
|
+
// in simulation mode we let the simulate function call the load_callback always.
|
|
662
|
+
if (!is_promise && !this.simulation_mode) {
|
|
663
|
+
load_callback();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// done with callbacks
|
|
667
|
+
this.internal.call_immediate = false;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private evaluateTimelineVariables(trial) {
|
|
671
|
+
for (const key of Object.keys(trial)) {
|
|
672
|
+
if (key === "type") {
|
|
673
|
+
// skip the `type` parameter as it contains a plugin
|
|
674
|
+
//continue;
|
|
675
|
+
}
|
|
676
|
+
// timeline variables on the root level
|
|
677
|
+
if (
|
|
678
|
+
typeof trial[key] === "object" &&
|
|
679
|
+
trial[key] !== null &&
|
|
680
|
+
typeof trial[key].timelineVariablePlaceholder !== "undefined"
|
|
681
|
+
) {
|
|
682
|
+
/*trial[key].toString().replace(/\s/g, "") ==
|
|
683
|
+
"function(){returntimeline.timelineVariable(varname);}"
|
|
684
|
+
)*/ trial[key] = trial[key].timelineVariableFunction();
|
|
685
|
+
}
|
|
686
|
+
// timeline variables that are nested in objects
|
|
687
|
+
if (typeof trial[key] === "object" && trial[key] !== null) {
|
|
688
|
+
this.evaluateTimelineVariables(trial[key]);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private evaluateFunctionParameters(trial) {
|
|
694
|
+
// set a flag so that jsPsych.timelineVariable() is immediately executed in this context
|
|
695
|
+
this.internal.call_immediate = true;
|
|
696
|
+
|
|
697
|
+
// iterate over each parameter
|
|
698
|
+
for (const key of Object.keys(trial)) {
|
|
699
|
+
// check to make sure parameter is not "type", since that was eval'd above.
|
|
700
|
+
if (key !== "type") {
|
|
701
|
+
// this if statement is checking to see if the parameter type is expected to be a function, in which case we should NOT evaluate it.
|
|
702
|
+
// the first line checks if the parameter is defined in the universalPluginParameters set
|
|
703
|
+
// the second line checks the plugin-specific parameters
|
|
704
|
+
if (
|
|
705
|
+
typeof universalPluginParameters[key] !== "undefined" &&
|
|
706
|
+
universalPluginParameters[key].type !== ParameterType.FUNCTION
|
|
707
|
+
) {
|
|
708
|
+
trial[key] = this.replaceFunctionsWithValues(trial[key], null);
|
|
709
|
+
}
|
|
710
|
+
if (
|
|
711
|
+
typeof trial.type.info.parameters[key] !== "undefined" &&
|
|
712
|
+
trial.type.info.parameters[key].type !== ParameterType.FUNCTION
|
|
713
|
+
) {
|
|
714
|
+
trial[key] = this.replaceFunctionsWithValues(trial[key], trial.type.info.parameters[key]);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// reset so jsPsych.timelineVariable() is no longer immediately executed
|
|
719
|
+
this.internal.call_immediate = false;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private replaceFunctionsWithValues(obj, info) {
|
|
723
|
+
// null typeof is 'object' (?!?!), so need to run this first!
|
|
724
|
+
if (obj === null) {
|
|
725
|
+
return obj;
|
|
726
|
+
}
|
|
727
|
+
// arrays
|
|
728
|
+
else if (Array.isArray(obj)) {
|
|
729
|
+
for (let i = 0; i < obj.length; i++) {
|
|
730
|
+
obj[i] = this.replaceFunctionsWithValues(obj[i], info);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// objects
|
|
734
|
+
else if (typeof obj === "object") {
|
|
735
|
+
if (info === null || !info.nested) {
|
|
736
|
+
for (const key of Object.keys(obj)) {
|
|
737
|
+
if (key === "type") {
|
|
738
|
+
// Ignore the object's `type` field because it contains a plugin and we do not want to
|
|
739
|
+
// call plugin functions
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
obj[key] = this.replaceFunctionsWithValues(obj[key], null);
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
for (const key of Object.keys(obj)) {
|
|
746
|
+
if (
|
|
747
|
+
typeof info.nested[key] === "object" &&
|
|
748
|
+
info.nested[key].type !== ParameterType.FUNCTION
|
|
749
|
+
) {
|
|
750
|
+
obj[key] = this.replaceFunctionsWithValues(obj[key], info.nested[key]);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} else if (typeof obj === "function") {
|
|
755
|
+
return obj();
|
|
756
|
+
}
|
|
757
|
+
return obj;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private setDefaultValues(trial) {
|
|
761
|
+
for (const param in trial.type.info.parameters) {
|
|
762
|
+
// check if parameter is complex with nested defaults
|
|
763
|
+
if (trial.type.info.parameters[param].type === ParameterType.COMPLEX) {
|
|
764
|
+
if (trial.type.info.parameters[param].array === true) {
|
|
765
|
+
// iterate over each entry in the array
|
|
766
|
+
trial[param].forEach(function (ip, i) {
|
|
767
|
+
// check each parameter in the plugin description
|
|
768
|
+
for (const p in trial.type.info.parameters[param].nested) {
|
|
769
|
+
if (typeof trial[param][i][p] === "undefined" || trial[param][i][p] === null) {
|
|
770
|
+
if (typeof trial.type.info.parameters[param].nested[p].default === "undefined") {
|
|
771
|
+
console.error(
|
|
772
|
+
"You must specify a value for the " +
|
|
773
|
+
p +
|
|
774
|
+
" parameter (nested in the " +
|
|
775
|
+
param +
|
|
776
|
+
" parameter) in the " +
|
|
777
|
+
trial.type +
|
|
778
|
+
" plugin."
|
|
779
|
+
);
|
|
780
|
+
} else {
|
|
781
|
+
trial[param][i][p] = trial.type.info.parameters[param].nested[p].default;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// if it's not nested, checking is much easier and do that here:
|
|
789
|
+
else if (typeof trial[param] === "undefined" || trial[param] === null) {
|
|
790
|
+
if (typeof trial.type.info.parameters[param].default === "undefined") {
|
|
791
|
+
console.error(
|
|
792
|
+
"You must specify a value for the " +
|
|
793
|
+
param +
|
|
794
|
+
" parameter in the " +
|
|
795
|
+
trial.type.info.name +
|
|
796
|
+
" plugin."
|
|
797
|
+
);
|
|
798
|
+
} else {
|
|
799
|
+
trial[param] = trial.type.info.parameters[param].default;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private async checkExclusions(exclusions) {
|
|
806
|
+
if (exclusions.min_width || exclusions.min_height || exclusions.audio) {
|
|
807
|
+
console.warn(
|
|
808
|
+
"The exclusions option in `initJsPsych()` is deprecated and will be removed in a future version. We recommend using the browser-check plugin instead. See https://www.jspsych.org/latest/plugins/browser-check/."
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
// MINIMUM SIZE
|
|
812
|
+
if (exclusions.min_width || exclusions.min_height) {
|
|
813
|
+
const mw = exclusions.min_width || 0;
|
|
814
|
+
const mh = exclusions.min_height || 0;
|
|
815
|
+
|
|
816
|
+
if (window.innerWidth < mw || window.innerHeight < mh) {
|
|
817
|
+
this.getDisplayElement().innerHTML =
|
|
818
|
+
"<p>Your browser window is too small to complete this experiment. " +
|
|
819
|
+
"Please maximize the size of your browser window. If your browser window is already maximized, " +
|
|
820
|
+
"you will not be able to complete this experiment.</p>" +
|
|
821
|
+
"<p>The minimum width is " +
|
|
822
|
+
mw +
|
|
823
|
+
"px. Your current width is " +
|
|
824
|
+
window.innerWidth +
|
|
825
|
+
"px.</p>" +
|
|
826
|
+
"<p>The minimum height is " +
|
|
827
|
+
mh +
|
|
828
|
+
"px. Your current height is " +
|
|
829
|
+
window.innerHeight +
|
|
830
|
+
"px.</p>";
|
|
831
|
+
|
|
832
|
+
// Wait for window size to increase
|
|
833
|
+
while (window.innerWidth < mw || window.innerHeight < mh) {
|
|
834
|
+
await delay(100);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
this.getDisplayElement().innerHTML = "";
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// WEB AUDIO API
|
|
842
|
+
if (typeof exclusions.audio !== "undefined" && exclusions.audio) {
|
|
843
|
+
if (!window.hasOwnProperty("AudioContext") && !window.hasOwnProperty("webkitAudioContext")) {
|
|
844
|
+
this.getDisplayElement().innerHTML =
|
|
845
|
+
"<p>Your browser does not support the WebAudio API, which means that you will not " +
|
|
846
|
+
"be able to complete the experiment.</p><p>Browsers that support the WebAudio API include " +
|
|
847
|
+
"Chrome, Firefox, Safari, and Edge.</p>";
|
|
848
|
+
throw new Error();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private drawProgressBar(msg) {
|
|
854
|
+
document
|
|
855
|
+
.querySelector(".jspsych-display-element")
|
|
856
|
+
.insertAdjacentHTML(
|
|
857
|
+
"afterbegin",
|
|
858
|
+
'<div id="jspsych-progressbar-container">' +
|
|
859
|
+
"<span>" +
|
|
860
|
+
msg +
|
|
861
|
+
"</span>" +
|
|
862
|
+
'<div id="jspsych-progressbar-outer">' +
|
|
863
|
+
'<div id="jspsych-progressbar-inner"></div>' +
|
|
864
|
+
"</div></div>"
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
private updateProgressBar() {
|
|
869
|
+
this.setProgressBar(this.getProgress().percent_complete / 100);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
private progress_bar_amount = 0;
|
|
873
|
+
|
|
874
|
+
setProgressBar(proportion_complete) {
|
|
875
|
+
proportion_complete = Math.max(Math.min(1, proportion_complete), 0);
|
|
876
|
+
document.querySelector<HTMLElement>("#jspsych-progressbar-inner").style.width =
|
|
877
|
+
proportion_complete * 100 + "%";
|
|
878
|
+
this.progress_bar_amount = proportion_complete;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
getProgressBarCompleted() {
|
|
882
|
+
return this.progress_bar_amount;
|
|
883
|
+
}
|
|
884
|
+
}
|