jspsych 6.2.0 → 7.1.0

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