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.
Files changed (401) hide show
  1. package/README.md +36 -37
  2. package/css/jspsych.css +39 -39
  3. package/dist/JsPsych.d.ts +112 -0
  4. package/dist/TimelineNode.d.ts +34 -0
  5. package/dist/index.browser.js +3171 -0
  6. package/dist/index.browser.js.map +1 -0
  7. package/dist/index.browser.min.js +2 -0
  8. package/dist/index.browser.min.js.map +1 -0
  9. package/dist/index.cjs +3165 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.d.ts +11 -0
  12. package/dist/index.js +3159 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/migration.d.ts +3 -0
  15. package/dist/modules/data/DataCollection.d.ts +45 -0
  16. package/dist/modules/data/DataColumn.d.ts +15 -0
  17. package/dist/modules/data/index.d.ts +25 -0
  18. package/dist/modules/data/utils.d.ts +3 -0
  19. package/dist/modules/extensions.d.ts +22 -0
  20. package/dist/modules/plugin-api/HardwareAPI.d.ts +15 -0
  21. package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +34 -0
  22. package/dist/modules/plugin-api/MediaAPI.d.ts +27 -0
  23. package/dist/modules/plugin-api/SimulationAPI.d.ts +41 -0
  24. package/dist/modules/plugin-api/TimeoutAPI.d.ts +5 -0
  25. package/dist/modules/plugin-api/index.d.ts +8 -0
  26. package/dist/modules/plugins.d.ts +136 -0
  27. package/dist/modules/randomization.d.ts +35 -0
  28. package/dist/modules/turk.d.ts +40 -0
  29. package/dist/modules/utils.d.ts +7 -0
  30. package/package.json +32 -15
  31. package/src/JsPsych.ts +884 -0
  32. package/src/TimelineNode.ts +536 -0
  33. package/src/index.ts +71 -0
  34. package/src/migration.ts +37 -0
  35. package/src/modules/data/DataCollection.ts +198 -0
  36. package/src/modules/data/DataColumn.ts +86 -0
  37. package/src/modules/data/index.ts +174 -0
  38. package/src/modules/data/utils.ts +75 -0
  39. package/src/modules/extensions.ts +23 -0
  40. package/src/modules/plugin-api/HardwareAPI.ts +32 -0
  41. package/src/modules/plugin-api/KeyboardListenerAPI.ts +165 -0
  42. package/src/modules/plugin-api/MediaAPI.ts +337 -0
  43. package/src/modules/plugin-api/SimulationAPI.ts +181 -0
  44. package/src/modules/plugin-api/TimeoutAPI.ts +16 -0
  45. package/src/modules/plugin-api/index.ts +28 -0
  46. package/src/modules/plugins.ts +165 -0
  47. package/src/modules/randomization.ts +327 -0
  48. package/src/modules/turk.ts +99 -0
  49. package/src/modules/utils.ts +30 -0
  50. package/.github/workflows/jest.yml +0 -20
  51. package/code-of-conduct.md +0 -56
  52. package/contributors.md +0 -61
  53. package/docs/CNAME +0 -1
  54. package/docs/about/about.md +0 -18
  55. package/docs/about/contributing.md +0 -43
  56. package/docs/about/license.md +0 -25
  57. package/docs/about/support.md +0 -7
  58. package/docs/core_library/jspsych-core.md +0 -719
  59. package/docs/core_library/jspsych-data.md +0 -587
  60. package/docs/core_library/jspsych-pluginAPI.md +0 -624
  61. package/docs/core_library/jspsych-randomization.md +0 -389
  62. package/docs/core_library/jspsych-turk.md +0 -98
  63. package/docs/extensions/extensions.md +0 -83
  64. package/docs/extensions/jspsych-ext-webgazer.md +0 -137
  65. package/docs/img/blue.png +0 -0
  66. package/docs/img/devtools-change-css.png +0 -0
  67. package/docs/img/devtools-css-errors.png +0 -0
  68. package/docs/img/devtools-inspect-element.png +0 -0
  69. package/docs/img/folder-setup.png +0 -0
  70. package/docs/img/folder-with-html.png +0 -0
  71. package/docs/img/githubreleases.jpg +0 -0
  72. package/docs/img/jspsych-favicon.png +0 -0
  73. package/docs/img/jspsych-logo-no-text-mono.svg +0 -493
  74. package/docs/img/jspsych-logo.jpg +0 -0
  75. package/docs/img/orange.png +0 -0
  76. package/docs/img/palmer_stim.png +0 -0
  77. package/docs/img/progress_bar.png +0 -0
  78. package/docs/img/prolific-study-completion.png +0 -0
  79. package/docs/img/prolific-study-link.png +0 -0
  80. package/docs/img/visual_search_example.jpg +0 -0
  81. package/docs/index.md +0 -9
  82. package/docs/overview/browser-device-support.md +0 -35
  83. package/docs/overview/callbacks.md +0 -180
  84. package/docs/overview/data.md +0 -281
  85. package/docs/overview/dynamic-parameters.md +0 -147
  86. package/docs/overview/exclude-browser.md +0 -32
  87. package/docs/overview/experiment-options.md +0 -149
  88. package/docs/overview/eye-tracking.md +0 -271
  89. package/docs/overview/fullscreen.md +0 -36
  90. package/docs/overview/media-preloading.md +0 -369
  91. package/docs/overview/mturk.md +0 -77
  92. package/docs/overview/plugins.md +0 -320
  93. package/docs/overview/progress-bar.md +0 -110
  94. package/docs/overview/prolific.md +0 -78
  95. package/docs/overview/record-browser-interactions.md +0 -23
  96. package/docs/overview/running-experiments.md +0 -95
  97. package/docs/overview/style.md +0 -293
  98. package/docs/overview/timeline.md +0 -457
  99. package/docs/plugins/jspsych-animation.md +0 -40
  100. package/docs/plugins/jspsych-audio-button-response.md +0 -60
  101. package/docs/plugins/jspsych-audio-keyboard-response.md +0 -58
  102. package/docs/plugins/jspsych-audio-slider-response.md +0 -53
  103. package/docs/plugins/jspsych-call-function.md +0 -81
  104. package/docs/plugins/jspsych-canvas-button-response.md +0 -66
  105. package/docs/plugins/jspsych-canvas-keyboard-response.md +0 -68
  106. package/docs/plugins/jspsych-canvas-slider-response.md +0 -89
  107. package/docs/plugins/jspsych-categorize-animation.md +0 -60
  108. package/docs/plugins/jspsych-categorize-html.md +0 -53
  109. package/docs/plugins/jspsych-categorize-image.md +0 -53
  110. package/docs/plugins/jspsych-cloze.md +0 -45
  111. package/docs/plugins/jspsych-external-html.md +0 -70
  112. package/docs/plugins/jspsych-free-sort.md +0 -56
  113. package/docs/plugins/jspsych-fullscreen.md +0 -57
  114. package/docs/plugins/jspsych-html-button-response.md +0 -42
  115. package/docs/plugins/jspsych-html-keyboard-response.md +0 -51
  116. package/docs/plugins/jspsych-html-slider-response.md +0 -45
  117. package/docs/plugins/jspsych-iat-html.md +0 -64
  118. package/docs/plugins/jspsych-iat-image.md +0 -64
  119. package/docs/plugins/jspsych-image-button-response.md +0 -48
  120. package/docs/plugins/jspsych-image-keyboard-response.md +0 -58
  121. package/docs/plugins/jspsych-image-slider-response.md +0 -54
  122. package/docs/plugins/jspsych-instructions.md +0 -58
  123. package/docs/plugins/jspsych-maxdiff.md +0 -41
  124. package/docs/plugins/jspsych-preload.md +0 -128
  125. package/docs/plugins/jspsych-rdk.md +0 -119
  126. package/docs/plugins/jspsych-reconstruction.md +0 -48
  127. package/docs/plugins/jspsych-resize.md +0 -39
  128. package/docs/plugins/jspsych-same-different-html.md +0 -53
  129. package/docs/plugins/jspsych-same-different-image.md +0 -66
  130. package/docs/plugins/jspsych-serial-reaction-time-mouse.md +0 -52
  131. package/docs/plugins/jspsych-serial-reaction-time.md +0 -57
  132. package/docs/plugins/jspsych-survey-html-form.md +0 -50
  133. package/docs/plugins/jspsych-survey-likert.md +0 -70
  134. package/docs/plugins/jspsych-survey-multi-choice.md +0 -48
  135. package/docs/plugins/jspsych-survey-multi-select.md +0 -53
  136. package/docs/plugins/jspsych-survey-text.md +0 -63
  137. package/docs/plugins/jspsych-video-button-response.md +0 -54
  138. package/docs/plugins/jspsych-video-keyboard-response.md +0 -50
  139. package/docs/plugins/jspsych-video-slider-response.md +0 -60
  140. package/docs/plugins/jspsych-virtual-chinrest.md +0 -105
  141. package/docs/plugins/jspsych-visual-search-circle.md +0 -52
  142. package/docs/plugins/jspsych-vsl-animate-occlusion.md +0 -55
  143. package/docs/plugins/jspsych-vsl-grid-scene.md +0 -62
  144. package/docs/plugins/jspsych-webgazer-calibrate.md +0 -61
  145. package/docs/plugins/jspsych-webgazer-init-camera.md +0 -30
  146. package/docs/plugins/jspsych-webgazer-validate.md +0 -44
  147. package/docs/plugins/list-of-plugins.md +0 -54
  148. package/docs/tutorials/hello-world.md +0 -162
  149. package/docs/tutorials/rt-task.md +0 -1334
  150. package/docs/tutorials/video-tutorials.md +0 -11
  151. package/examples/add-to-end-of-timeline.html +0 -38
  152. package/examples/case-sensitive-responses.html +0 -45
  153. package/examples/conditional-and-loop-functions.html +0 -64
  154. package/examples/css/jquery-ui.css +0 -1225
  155. package/examples/css-classes-parameter.html +0 -145
  156. package/examples/data-add-properties.html +0 -44
  157. package/examples/data-as-function.html +0 -39
  158. package/examples/data-from-timeline.html +0 -52
  159. package/examples/data-from-url.html +0 -21
  160. package/examples/demo-flanker.html +0 -117
  161. package/examples/demo-simple-rt-task.html +0 -120
  162. package/examples/demos/demo_1.html +0 -35
  163. package/examples/demos/demo_2.html +0 -50
  164. package/examples/demos/demo_3.html +0 -63
  165. package/examples/display-element-to-embed-experiment.html +0 -79
  166. package/examples/end-active-node.html +0 -52
  167. package/examples/end-experiment.html +0 -45
  168. package/examples/exclusions.html +0 -32
  169. package/examples/external_html/simple_consent.html +0 -4
  170. package/examples/img/1.gif +0 -0
  171. package/examples/img/10.gif +0 -0
  172. package/examples/img/11.gif +0 -0
  173. package/examples/img/12.gif +0 -0
  174. package/examples/img/2.gif +0 -0
  175. package/examples/img/3.gif +0 -0
  176. package/examples/img/4.gif +0 -0
  177. package/examples/img/5.gif +0 -0
  178. package/examples/img/6.gif +0 -0
  179. package/examples/img/7.gif +0 -0
  180. package/examples/img/8.gif +0 -0
  181. package/examples/img/9.gif +0 -0
  182. package/examples/img/age/of1.jpg +0 -0
  183. package/examples/img/age/of2.jpg +0 -0
  184. package/examples/img/age/of3.jpg +0 -0
  185. package/examples/img/age/om1.jpg +0 -0
  186. package/examples/img/age/om2.jpg +0 -0
  187. package/examples/img/age/om3.jpg +0 -0
  188. package/examples/img/age/yf1.jpg +0 -0
  189. package/examples/img/age/yf4.jpg +0 -0
  190. package/examples/img/age/yf5.jpg +0 -0
  191. package/examples/img/age/ym2.jpg +0 -0
  192. package/examples/img/age/ym3.jpg +0 -0
  193. package/examples/img/age/ym5.jpg +0 -0
  194. package/examples/img/backwardN.gif +0 -0
  195. package/examples/img/blue.png +0 -0
  196. package/examples/img/card.png +0 -0
  197. package/examples/img/con1.png +0 -0
  198. package/examples/img/con2.png +0 -0
  199. package/examples/img/fixation.gif +0 -0
  200. package/examples/img/happy_face_1.jpg +0 -0
  201. package/examples/img/happy_face_2.jpg +0 -0
  202. package/examples/img/happy_face_3.jpg +0 -0
  203. package/examples/img/happy_face_4.jpg +0 -0
  204. package/examples/img/inc1.png +0 -0
  205. package/examples/img/inc2.png +0 -0
  206. package/examples/img/normalN.gif +0 -0
  207. package/examples/img/orange.png +0 -0
  208. package/examples/img/redX.png +0 -0
  209. package/examples/img/ribbon.jpg +0 -0
  210. package/examples/img/sad_face_1.jpg +0 -0
  211. package/examples/img/sad_face_2.jpg +0 -0
  212. package/examples/img/sad_face_3.jpg +0 -0
  213. package/examples/img/sad_face_4.jpg +0 -0
  214. package/examples/js/snap.svg-min.js +0 -21
  215. package/examples/js/webgazer/ridgeWorker.mjs +0 -135
  216. package/examples/js/webgazer/webgazer.js +0 -88909
  217. package/examples/js/webgazer/worker_scripts/mat.js +0 -306
  218. package/examples/js/webgazer/worker_scripts/util.js +0 -398
  219. package/examples/jspsych-RDK.html +0 -58
  220. package/examples/jspsych-animation.html +0 -39
  221. package/examples/jspsych-audio-button-response.html +0 -58
  222. package/examples/jspsych-audio-keyboard-response.html +0 -68
  223. package/examples/jspsych-audio-slider-response.html +0 -61
  224. package/examples/jspsych-call-function.html +0 -32
  225. package/examples/jspsych-canvas-button-response.html +0 -95
  226. package/examples/jspsych-canvas-keyboard-response.html +0 -78
  227. package/examples/jspsych-canvas-slider-response.html +0 -67
  228. package/examples/jspsych-categorize-animation.html +0 -49
  229. package/examples/jspsych-categorize-html.html +0 -33
  230. package/examples/jspsych-categorize-image.html +0 -44
  231. package/examples/jspsych-cloze.html +0 -37
  232. package/examples/jspsych-free-sort.html +0 -109
  233. package/examples/jspsych-fullscreen.html +0 -45
  234. package/examples/jspsych-html-button-response.html +0 -43
  235. package/examples/jspsych-html-keyboard-response.html +0 -42
  236. package/examples/jspsych-html-slider-response.html +0 -53
  237. package/examples/jspsych-iat.html +0 -520
  238. package/examples/jspsych-image-button-response.html +0 -91
  239. package/examples/jspsych-image-keyboard-response.html +0 -85
  240. package/examples/jspsych-image-slider-response.html +0 -85
  241. package/examples/jspsych-instructions.html +0 -37
  242. package/examples/jspsych-maxdiff.html +0 -33
  243. package/examples/jspsych-preload.html +0 -140
  244. package/examples/jspsych-reconstruction.html +0 -43
  245. package/examples/jspsych-resize.html +0 -34
  246. package/examples/jspsych-same-different-html.html +0 -28
  247. package/examples/jspsych-same-different-image.html +0 -39
  248. package/examples/jspsych-serial-reaction-time-mouse.html +0 -98
  249. package/examples/jspsych-serial-reaction-time.html +0 -54
  250. package/examples/jspsych-survey-html-form.html +0 -33
  251. package/examples/jspsych-survey-likert.html +0 -42
  252. package/examples/jspsych-survey-multi-choice.html +0 -40
  253. package/examples/jspsych-survey-multi-select.html +0 -42
  254. package/examples/jspsych-survey-text.html +0 -34
  255. package/examples/jspsych-video-button-response.html +0 -65
  256. package/examples/jspsych-video-keyboard-response.html +0 -61
  257. package/examples/jspsych-video-slider-response.html +0 -63
  258. package/examples/jspsych-virtual-chinrest.html +0 -69
  259. package/examples/jspsych-visual-search-circle.html +0 -64
  260. package/examples/jspsych-vsl-animate-occlusion.html +0 -35
  261. package/examples/jspsych-vsl-grid-scene.html +0 -47
  262. package/examples/lexical-decision.html +0 -134
  263. package/examples/manual-preloading.html +0 -59
  264. package/examples/pause-unpause.html +0 -33
  265. package/examples/progress-bar.html +0 -68
  266. package/examples/save-trial-parameters.html +0 -98
  267. package/examples/sound/hammer.mp3 +0 -0
  268. package/examples/sound/sound.mp3 +0 -0
  269. package/examples/sound/speech_blue.mp3 +0 -0
  270. package/examples/sound/speech_green.mp3 +0 -0
  271. package/examples/sound/speech_joke.mp3 +0 -0
  272. package/examples/sound/speech_red.mp3 +0 -0
  273. package/examples/sound/tone.mp3 +0 -0
  274. package/examples/timeline-variables-sampling.html +0 -50
  275. package/examples/timeline-variables.html +0 -64
  276. package/examples/video/sample_video.mp4 +0 -0
  277. package/examples/webgazer.html +0 -174
  278. package/examples/webgazer_audio.html +0 -90
  279. package/examples/webgazer_image.html +0 -60
  280. package/extensions/jspsych-ext-webgazer.js +0 -265
  281. package/jspsych.js +0 -3023
  282. package/license.txt +0 -21
  283. package/mkdocs.yml +0 -118
  284. package/plugins/jspsych-animation.js +0 -189
  285. package/plugins/jspsych-audio-button-response.js +0 -269
  286. package/plugins/jspsych-audio-keyboard-response.js +0 -209
  287. package/plugins/jspsych-audio-slider-response.js +0 -278
  288. package/plugins/jspsych-call-function.js +0 -58
  289. package/plugins/jspsych-canvas-button-response.js +0 -199
  290. package/plugins/jspsych-canvas-keyboard-response.js +0 -155
  291. package/plugins/jspsych-canvas-slider-response.js +0 -207
  292. package/plugins/jspsych-categorize-animation.js +0 -266
  293. package/plugins/jspsych-categorize-html.js +0 -220
  294. package/plugins/jspsych-categorize-image.js +0 -222
  295. package/plugins/jspsych-cloze.js +0 -112
  296. package/plugins/jspsych-external-html.js +0 -112
  297. package/plugins/jspsych-free-sort.js +0 -478
  298. package/plugins/jspsych-fullscreen.js +0 -106
  299. package/plugins/jspsych-html-button-response.js +0 -188
  300. package/plugins/jspsych-html-keyboard-response.js +0 -149
  301. package/plugins/jspsych-html-slider-response.js +0 -202
  302. package/plugins/jspsych-iat-html.js +0 -284
  303. package/plugins/jspsych-iat-image.js +0 -286
  304. package/plugins/jspsych-image-button-response.js +0 -327
  305. package/plugins/jspsych-image-keyboard-response.js +0 -263
  306. package/plugins/jspsych-image-slider-response.js +0 -369
  307. package/plugins/jspsych-instructions.js +0 -237
  308. package/plugins/jspsych-maxdiff.js +0 -173
  309. package/plugins/jspsych-preload.js +0 -345
  310. package/plugins/jspsych-rdk.js +0 -1373
  311. package/plugins/jspsych-reconstruction.js +0 -134
  312. package/plugins/jspsych-resize.js +0 -166
  313. package/plugins/jspsych-same-different-html.js +0 -168
  314. package/plugins/jspsych-same-different-image.js +0 -169
  315. package/plugins/jspsych-serial-reaction-time-mouse.js +0 -212
  316. package/plugins/jspsych-serial-reaction-time.js +0 -247
  317. package/plugins/jspsych-survey-html-form.js +0 -171
  318. package/plugins/jspsych-survey-likert.js +0 -195
  319. package/plugins/jspsych-survey-multi-choice.js +0 -208
  320. package/plugins/jspsych-survey-multi-select.js +0 -232
  321. package/plugins/jspsych-survey-text.js +0 -185
  322. package/plugins/jspsych-video-button-response.js +0 -335
  323. package/plugins/jspsych-video-keyboard-response.js +0 -279
  324. package/plugins/jspsych-video-slider-response.js +0 -351
  325. package/plugins/jspsych-virtual-chinrest.js +0 -471
  326. package/plugins/jspsych-visual-search-circle.js +0 -259
  327. package/plugins/jspsych-vsl-animate-occlusion.js +0 -196
  328. package/plugins/jspsych-vsl-grid-scene.js +0 -103
  329. package/plugins/jspsych-webgazer-calibrate.js +0 -161
  330. package/plugins/jspsych-webgazer-init-camera.js +0 -139
  331. package/plugins/jspsych-webgazer-validate.js +0 -314
  332. package/plugins/template/jspsych-plugin-template.js +0 -35
  333. package/tests/README.md +0 -7
  334. package/tests/jsPsych/case-sensitive-responses.test.js +0 -53
  335. package/tests/jsPsych/css-classes-parameter.test.js +0 -107
  336. package/tests/jsPsych/default-iti.test.js +0 -51
  337. package/tests/jsPsych/default-parameters.test.js +0 -58
  338. package/tests/jsPsych/endexperiment.test.js +0 -49
  339. package/tests/jsPsych/events.test.js +0 -606
  340. package/tests/jsPsych/functions-as-parameters.test.js +0 -210
  341. package/tests/jsPsych/init.test.js +0 -48
  342. package/tests/jsPsych/loads.test.js +0 -7
  343. package/tests/jsPsych/min-rt.test.js +0 -58
  344. package/tests/jsPsych/progressbar.test.js +0 -202
  345. package/tests/jsPsych/timeline-variables.test.js +0 -531
  346. package/tests/jsPsych/timelines.test.js +0 -569
  347. package/tests/jsPsych.data/data-csv-conversion.test.js +0 -85
  348. package/tests/jsPsych.data/data-json-conversion.test.js +0 -120
  349. package/tests/jsPsych.data/datacollection.test.js +0 -117
  350. package/tests/jsPsych.data/datacolumn.test.js +0 -50
  351. package/tests/jsPsych.data/datamodule.test.js +0 -152
  352. package/tests/jsPsych.data/dataparameter.test.js +0 -251
  353. package/tests/jsPsych.data/interactions.test.js +0 -109
  354. package/tests/jsPsych.data/trialparameters.test.js +0 -175
  355. package/tests/jsPsych.extensions/extensions.test.js +0 -207
  356. package/tests/jsPsych.extensions/test-extension.js +0 -42
  357. package/tests/jsPsych.pluginAPI/pluginapi.test.js +0 -365
  358. package/tests/jsPsych.pluginAPI/preloads.test.js +0 -43
  359. package/tests/jsPsych.randomization/randomziation.test.js +0 -27
  360. package/tests/jsPsych.utils/utils.test.js +0 -58
  361. package/tests/plugins/plugin-animation.test.js +0 -34
  362. package/tests/plugins/plugin-audio-button-response.test.js +0 -15
  363. package/tests/plugins/plugin-audio-keyboard-response.test.js +0 -15
  364. package/tests/plugins/plugin-audio-slider-response.test.js +0 -15
  365. package/tests/plugins/plugin-call-function.test.js +0 -49
  366. package/tests/plugins/plugin-categorize-animation.test.js +0 -263
  367. package/tests/plugins/plugin-categorize-html.test.js +0 -17
  368. package/tests/plugins/plugin-categorize-image.test.js +0 -17
  369. package/tests/plugins/plugin-cloze.test.js +0 -157
  370. package/tests/plugins/plugin-free-sort.test.js +0 -106
  371. package/tests/plugins/plugin-fullscreen.test.js +0 -41
  372. package/tests/plugins/plugin-html-button-response.test.js +0 -161
  373. package/tests/plugins/plugin-html-keyboard-response.test.js +0 -139
  374. package/tests/plugins/plugin-html-slider-response.test.js +0 -155
  375. package/tests/plugins/plugin-iat-html.test.js +0 -299
  376. package/tests/plugins/plugin-iat-image.test.js +0 -298
  377. package/tests/plugins/plugin-image-button-response.test.js +0 -174
  378. package/tests/plugins/plugin-image-keyboard-response.test.js +0 -147
  379. package/tests/plugins/plugin-image-slider-response.test.js +0 -174
  380. package/tests/plugins/plugin-instructions.test.js +0 -85
  381. package/tests/plugins/plugin-maxdiff.test.js +0 -39
  382. package/tests/plugins/plugin-preload.test.js +0 -916
  383. package/tests/plugins/plugin-rdk.test.js +0 -61
  384. package/tests/plugins/plugin-reconstruction.test.js +0 -16
  385. package/tests/plugins/plugin-resize.test.js +0 -16
  386. package/tests/plugins/plugin-same-different-html.test.js +0 -17
  387. package/tests/plugins/plugin-same-different-image.test.js +0 -17
  388. package/tests/plugins/plugin-serial-reaction-time-mouse.test.js +0 -42
  389. package/tests/plugins/plugin-serial-reaction-time.test.js +0 -109
  390. package/tests/plugins/plugin-survey-html-form.test.js +0 -44
  391. package/tests/plugins/plugin-survey-likert.test.js +0 -48
  392. package/tests/plugins/plugin-survey-multi-choice.test.js +0 -47
  393. package/tests/plugins/plugin-survey-multi-select.test.js +0 -71
  394. package/tests/plugins/plugin-survey-text.test.js +0 -115
  395. package/tests/plugins/plugin-video-button-response.test.js +0 -32
  396. package/tests/plugins/plugin-video-keyboard-response.test.js +0 -32
  397. package/tests/plugins/plugin-video-slider-response.test.js +0 -31
  398. package/tests/plugins/plugin-visual-search-circle.test.js +0 -16
  399. package/tests/plugins/plugin-vsl-animate-occlusion.test.js +0 -16
  400. package/tests/plugins/plugin-vsl-grid-scene.test.js +0 -16
  401. 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
+ }