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
package/jspsych.js DELETED
@@ -1,2796 +0,0 @@
1
- /**
2
- * jspsych.js
3
- * Josh de Leeuw
4
- *
5
- * documentation: docs.jspsych.org
6
- *
7
- **/
8
- window.jsPsych = (function() {
9
-
10
- var core = {};
11
-
12
- //
13
- // private variables
14
- //
15
-
16
- // options
17
- var opts = {};
18
- // experiment timeline
19
- var timeline;
20
- // flow control
21
- var global_trial_index = 0;
22
- var current_trial = {};
23
- var current_trial_finished = false;
24
- // target DOM element
25
- var DOM_container;
26
- var DOM_target;
27
- // time that the experiment began
28
- var exp_start_time;
29
- // is the experiment paused?
30
- var paused = false;
31
- var waiting = false;
32
- // done loading?
33
- var loaded = false;
34
- var loadfail = false;
35
- // is the page retrieved directly via file:// protocol (true) or hosted on a server (false)?
36
- var file_protocol = false;
37
-
38
- // storing a single webaudio context to prevent problems with multiple inits
39
- // of jsPsych
40
- core.webaudio_context = null;
41
- // temporary patch for Safari
42
- if (typeof window !== 'undefined' && window.hasOwnProperty('webkitAudioContext') && !window.hasOwnProperty('AudioContext')) {
43
- window.AudioContext = webkitAudioContext;
44
- }
45
- // end patch
46
- core.webaudio_context = (typeof window !== 'undefined' && typeof window.AudioContext !== 'undefined') ? new AudioContext() : null;
47
-
48
- // enumerated variables for special parameter types
49
- core.ALL_KEYS = 'allkeys';
50
- core.NO_KEYS = 'none';
51
-
52
- //
53
- // public methods
54
- //
55
-
56
- core.init = function(options) {
57
- function init() {
58
- if(typeof options.timeline === 'undefined'){
59
- console.error('No timeline declared in jsPsych.init. Cannot start experiment.')
60
- }
61
-
62
- if(options.timeline.length == 0){
63
- console.error('No trials have been added to the timeline (the timeline is an empty array). Cannot start experiment.')
64
- }
65
-
66
- // reset variables
67
- timeline = null;
68
- global_trial_index = 0;
69
- current_trial = {};
70
- current_trial_finished = false;
71
- paused = false;
72
- waiting = false;
73
- loaded = false;
74
- loadfail = false;
75
- file_protocol = false;
76
- jsPsych.data.reset();
77
-
78
- var defaults = {
79
- 'display_element': undefined,
80
- 'on_finish': function(data) {
81
- return undefined;
82
- },
83
- 'on_trial_start': function(trial) {
84
- return undefined;
85
- },
86
- 'on_trial_finish': function() {
87
- return undefined;
88
- },
89
- 'on_data_update': function(data) {
90
- return undefined;
91
- },
92
- 'on_interaction_data_update': function(data){
93
- return undefined;
94
- },
95
- 'on_close': function(){
96
- return undefined;
97
- },
98
- 'preload_images': [],
99
- 'preload_audio': [],
100
- 'preload_video': [],
101
- 'use_webaudio': true,
102
- 'exclusions': {},
103
- 'show_progress_bar': false,
104
- 'message_progress_bar': 'Completion Progress',
105
- 'auto_update_progress_bar': true,
106
- 'auto_preload': true,
107
- 'show_preload_progress_bar': true,
108
- 'max_load_time': 60000,
109
- 'max_preload_attempts': 10,
110
- 'default_iti': 0,
111
- 'minimum_valid_rt': 0,
112
- 'experiment_width': null,
113
- 'override_safe_mode': false
114
- };
115
-
116
- // detect whether page is running in browser as a local file, and if so, disable web audio and video preloading to prevent CORS issues
117
- if (window.location.protocol == 'file:' && (options.override_safe_mode === false || typeof options.override_safe_mode == 'undefined')) {
118
- options.use_webaudio = false;
119
- file_protocol = true;
120
- console.warn("jsPsych detected that it is running via the file:// protocol and not on a web server. "+
121
- "To prevent issues with cross-origin requests, Web Audio and video preloading have been disabled. "+
122
- "If you would like to override this setting, you can set 'override_safe_mode' to 'true' in jsPsych.init. "+
123
- "For more information, see: https://www.jspsych.org/overview/running-experiments");
124
- }
125
-
126
- // override default options if user specifies an option
127
- opts = Object.assign({}, defaults, options);
128
-
129
- // set DOM element where jsPsych will render content
130
- // if undefined, then jsPsych will use the <body> tag and the entire page
131
- if(typeof opts.display_element == 'undefined'){
132
- // check if there is a body element on the page
133
- var body = document.querySelector('body');
134
- if (body === null) {
135
- document.documentElement.appendChild(document.createElement('body'));
136
- }
137
- // using the full page, so we need the HTML element to
138
- // have 100% height, and body to be full width and height with
139
- // no margin
140
- document.querySelector('html').style.height = '100%';
141
- document.querySelector('body').style.margin = '0px';
142
- document.querySelector('body').style.height = '100%';
143
- document.querySelector('body').style.width = '100%';
144
- opts.display_element = document.querySelector('body');
145
- } else {
146
- // make sure that the display element exists on the page
147
- var display;
148
- if (opts.display_element instanceof Element) {
149
- var display = opts.display_element;
150
- } else {
151
- var display = document.querySelector('#' + opts.display_element);
152
- }
153
- if(display === null) {
154
- console.error('The display_element specified in jsPsych.init() does not exist in the DOM.');
155
- } else {
156
- opts.display_element = display;
157
- }
158
- }
159
- opts.display_element.innerHTML = '<div class="jspsych-content-wrapper"><div id="jspsych-content"></div></div>';
160
- DOM_container = opts.display_element;
161
- DOM_target = document.querySelector('#jspsych-content');
162
-
163
-
164
- // add tabIndex attribute to scope event listeners
165
- opts.display_element.tabIndex = 0;
166
-
167
- // add CSS class to DOM_target
168
- if(opts.display_element.className.indexOf('jspsych-display-element') == -1){
169
- opts.display_element.className += ' jspsych-display-element';
170
- }
171
- DOM_target.className += 'jspsych-content';
172
-
173
- // set experiment_width if not null
174
- if(opts.experiment_width !== null){
175
- DOM_target.style.width = opts.experiment_width + "px";
176
- }
177
-
178
- // create experiment timeline
179
- timeline = new TimelineNode({
180
- timeline: opts.timeline
181
- });
182
-
183
- // initialize audio context based on options and browser capabilities
184
- jsPsych.pluginAPI.initAudio();
185
-
186
- // below code resets event listeners that may have lingered from
187
- // a previous incomplete experiment loaded in same DOM.
188
- jsPsych.pluginAPI.reset(opts.display_element);
189
- // create keyboard event listeners
190
- jsPsych.pluginAPI.createKeyboardEventListeners(opts.display_element);
191
- // create listeners for user browser interaction
192
- jsPsych.data.createInteractionListeners();
193
-
194
- // add event for closing window
195
- window.addEventListener('beforeunload', opts.on_close);
196
-
197
- // check exclusions before continuing
198
- checkExclusions(opts.exclusions,
199
- function(){
200
- // success! user can continue...
201
- // start experiment, with or without preloading
202
- if(opts.auto_preload){
203
- jsPsych.pluginAPI.autoPreload(timeline, startExperiment, file_protocol, opts.preload_images, opts.preload_audio, opts.preload_video, opts.show_preload_progress_bar);
204
- if(opts.max_load_time > 0){
205
- setTimeout(function(){
206
- if(!loaded && !loadfail){
207
- core.loadFail();
208
- }
209
- }, opts.max_load_time);
210
- }
211
- } else {
212
- startExperiment();
213
- }
214
- },
215
- function(){
216
- // fail. incompatible user.
217
-
218
- }
219
- );
220
- };
221
-
222
- // execute init() when the document is ready
223
- if (document.readyState === "complete") {
224
- init();
225
- } else {
226
- window.addEventListener("load", init);
227
- }
228
- }
229
-
230
- core.progress = function() {
231
-
232
- var percent_complete = typeof timeline == 'undefined' ? 0 : timeline.percentComplete();
233
-
234
- var obj = {
235
- "total_trials": typeof timeline == 'undefined' ? undefined : timeline.length(),
236
- "current_trial_global": global_trial_index,
237
- "percent_complete": percent_complete
238
- };
239
-
240
- return obj;
241
- };
242
-
243
- core.startTime = function() {
244
- return exp_start_time;
245
- };
246
-
247
- core.totalTime = function() {
248
- if(typeof exp_start_time == 'undefined'){ return 0; }
249
- return (new Date()).getTime() - exp_start_time.getTime();
250
- };
251
-
252
- core.getDisplayElement = function() {
253
- return DOM_target;
254
- };
255
-
256
- core.getDisplayContainerElement = function(){
257
- return DOM_container;
258
- }
259
-
260
- core.finishTrial = function(data) {
261
-
262
- if(current_trial_finished){ return; }
263
- current_trial_finished = true;
264
-
265
- // write the data from the trial
266
- data = typeof data == 'undefined' ? {} : data;
267
- jsPsych.data.write(data);
268
-
269
- // get back the data with all of the defaults in
270
- var trial_data = jsPsych.data.get().filter({trial_index: global_trial_index});
271
-
272
- // for trial-level callbacks, we just want to pass in a reference to the values
273
- // of the DataCollection, for easy access and editing.
274
- var trial_data_values = trial_data.values()[0];
275
-
276
- // handle callback at plugin level
277
- if (typeof current_trial.on_finish === 'function') {
278
- current_trial.on_finish(trial_data_values);
279
- }
280
-
281
- // handle callback at whole-experiment level
282
- opts.on_trial_finish(trial_data_values);
283
-
284
- // after the above callbacks are complete, then the data should be finalized
285
- // for this trial. call the on_data_update handler, passing in the same
286
- // data object that just went through the trial's finish handlers.
287
- opts.on_data_update(trial_data_values);
288
-
289
- // wait for iti
290
- if (typeof current_trial.post_trial_gap === null || typeof current_trial.post_trial_gap === 'undefined') {
291
- if (opts.default_iti > 0) {
292
- setTimeout(nextTrial, opts.default_iti);
293
- } else {
294
- nextTrial();
295
- }
296
- } else {
297
- if (current_trial.post_trial_gap > 0) {
298
- setTimeout(nextTrial, current_trial.post_trial_gap);
299
- } else {
300
- nextTrial();
301
- }
302
- }
303
- }
304
-
305
- core.endExperiment = function(end_message) {
306
- timeline.end_message = end_message;
307
- timeline.end();
308
- jsPsych.pluginAPI.cancelAllKeyboardResponses();
309
- jsPsych.pluginAPI.clearAllTimeouts();
310
- core.finishTrial();
311
- }
312
-
313
- core.endCurrentTimeline = function() {
314
- timeline.endActiveNode();
315
- }
316
-
317
- core.currentTrial = function() {
318
- return current_trial;
319
- };
320
-
321
- core.initSettings = function() {
322
- return opts;
323
- };
324
-
325
- core.currentTimelineNodeID = function() {
326
- return timeline.activeID();
327
- };
328
-
329
- core.timelineVariable = function(varname, execute){
330
- if(execute){
331
- return timeline.timelineVariable(varname);
332
- } else {
333
- return function() { return timeline.timelineVariable(varname); }
334
- }
335
- }
336
-
337
- core.addNodeToEndOfTimeline = function(new_timeline, preload_callback){
338
- timeline.insert(new_timeline);
339
- if(typeof preload_callback !== 'undefined'){
340
- if(opts.auto_preload){
341
- jsPsych.pluginAPI.autoPreload(timeline, preload_callback, file_protocol);
342
- } else {
343
- preload_callback();
344
- }
345
- }
346
- }
347
-
348
- core.pauseExperiment = function(){
349
- paused = true;
350
- }
351
-
352
- core.resumeExperiment = function(){
353
- paused = false;
354
- if(waiting){
355
- waiting = false;
356
- nextTrial();
357
- }
358
- }
359
-
360
- core.loadFail = function(message){
361
- message = message || '<p>The experiment failed to load.</p>';
362
- loadfail = true;
363
- DOM_target.innerHTML = message;
364
- }
365
-
366
- function TimelineNode(parameters, parent, relativeID) {
367
-
368
- // a unique ID for this node, relative to the parent
369
- var relative_id;
370
-
371
- // store the parent for this node
372
- var parent_node;
373
-
374
- // parameters for the trial if the node contains a trial
375
- var trial_parameters;
376
-
377
- // parameters for nodes that contain timelines
378
- var timeline_parameters;
379
-
380
- // stores trial information on a node that contains a timeline
381
- // used for adding new trials
382
- var node_trial_data;
383
-
384
- // track progress through the node
385
- var progress = {
386
- current_location: -1, // where on the timeline (which timelinenode)
387
- current_variable_set: 0, // which set of variables to use from timeline_variables
388
- current_repetition: 0, // how many times through the variable set on this run of the node
389
- current_iteration: 0, // how many times this node has been revisited
390
- done: false
391
- }
392
-
393
- // reference to self
394
- var self = this;
395
-
396
- // recursively get the next trial to run.
397
- // if this node is a leaf (trial), then return the trial.
398
- // otherwise, recursively find the next trial in the child timeline.
399
- this.trial = function() {
400
- if (typeof timeline_parameters == 'undefined') {
401
- // returns a clone of the trial_parameters to
402
- // protect functions.
403
- return jsPsych.utils.deepCopy(trial_parameters);
404
- } else {
405
- if (progress.current_location >= timeline_parameters.timeline.length) {
406
- return null;
407
- } else {
408
- return timeline_parameters.timeline[progress.current_location].trial();
409
- }
410
- }
411
- }
412
-
413
- this.markCurrentTrialComplete = function() {
414
- if(typeof timeline_parameters == 'undefined'){
415
- progress.done = true;
416
- } else {
417
- timeline_parameters.timeline[progress.current_location].markCurrentTrialComplete();
418
- }
419
- }
420
-
421
- this.nextRepetiton = function() {
422
- this.setTimelineVariablesOrder();
423
- progress.current_location = -1;
424
- progress.current_variable_set = 0;
425
- progress.current_repetition++;
426
- for (var i = 0; i < timeline_parameters.timeline.length; i++) {
427
- timeline_parameters.timeline[i].reset();
428
- }
429
- }
430
-
431
- // set the order for going through the timeline variables array
432
- this.setTimelineVariablesOrder = function() {
433
-
434
- // check to make sure this node has variables
435
- if(typeof timeline_parameters === 'undefined' || typeof timeline_parameters.timeline_variables === 'undefined'){
436
- return;
437
- }
438
-
439
- var order = [];
440
- for(var i=0; i<timeline_parameters.timeline_variables.length; i++){
441
- order.push(i);
442
- }
443
-
444
- if(typeof timeline_parameters.sample !== 'undefined'){
445
- if(timeline_parameters.sample.type == 'custom'){
446
- order = timeline_parameters.sample.fn(order);
447
- } else if(timeline_parameters.sample.type == 'with-replacement'){
448
- order = jsPsych.randomization.sampleWithReplacement(order, timeline_parameters.sample.size, timeline_parameters.sample.weights);
449
- } else if(timeline_parameters.sample.type == 'without-replacement'){
450
- order = jsPsych.randomization.sampleWithoutReplacement(order, timeline_parameters.sample.size);
451
- } else if(timeline_parameters.sample.type == 'fixed-repetitions'){
452
- order = jsPsych.randomization.repeat(order, timeline_parameters.sample.size, false);
453
- } else if(timeline_parameters.sample.type == 'alternate-groups'){
454
- order = jsPsych.randomization.shuffleAlternateGroups(timeline_parameters.sample.groups, timeline_parameters.sample.randomize_group_order);
455
- } else {
456
- console.error('Invalid type in timeline sample parameters. Valid options for type are "custom", "with-replacement", "without-replacement", "fixed-repetitions", and "alternate-groups"');
457
- }
458
- }
459
-
460
- if(timeline_parameters.randomize_order) {
461
- order = jsPsych.randomization.shuffle(order);
462
- }
463
-
464
- progress.order = order;
465
- }
466
-
467
- // next variable set
468
- this.nextSet = function() {
469
- progress.current_location = -1;
470
- progress.current_variable_set++;
471
- for (var i = 0; i < timeline_parameters.timeline.length; i++) {
472
- timeline_parameters.timeline[i].reset();
473
- }
474
- }
475
-
476
- // update the current trial node to be completed
477
- // returns true if the node is complete after advance (all subnodes are also complete)
478
- // returns false otherwise
479
- this.advance = function() {
480
-
481
- // first check to see if done
482
- if (progress.done) {
483
- return true;
484
- }
485
-
486
- // if node has not started yet (progress.current_location == -1),
487
- // then try to start the node.
488
- if (progress.current_location == -1) {
489
- // check for conditonal function on nodes with timelines
490
- if (typeof timeline_parameters != 'undefined') {
491
- if (typeof timeline_parameters.conditional_function !== 'undefined') {
492
- var conditional_result = timeline_parameters.conditional_function();
493
- // if the conditional_function() returns false, then the timeline
494
- // doesn't run and is marked as complete.
495
- if (conditional_result == false) {
496
- progress.done = true;
497
- return true;
498
- }
499
- // if the conditonal_function() returns true, then the node can start
500
- else {
501
- progress.current_location = 0;
502
- }
503
- }
504
- // if there is no conditional_function, then the node can start
505
- else {
506
- progress.current_location = 0;
507
- }
508
- }
509
- // if the node does not have a timeline, then it can start
510
- progress.current_location = 0;
511
- // call advance again on this node now that it is pointing to a new location
512
- return this.advance();
513
- }
514
-
515
- // if this node has a timeline, propogate down to the current trial.
516
- if (typeof timeline_parameters !== 'undefined') {
517
-
518
- var have_node_to_run = false;
519
- // keep incrementing the location in the timeline until one of the nodes reached is incomplete
520
- while (progress.current_location < timeline_parameters.timeline.length && have_node_to_run == false) {
521
-
522
- // check to see if the node currently pointed at is done
523
- var target_complete = timeline_parameters.timeline[progress.current_location].advance();
524
- if (!target_complete) {
525
- have_node_to_run = true;
526
- return false;
527
- } else {
528
- progress.current_location++;
529
- }
530
-
531
- }
532
-
533
- // if we've reached the end of the timeline (which, if the code is here, we have)
534
- // there are a few steps to see what to do next...
535
-
536
- // first, check the timeline_variables to see if we need to loop through again
537
- // with a new set of variables
538
- if (progress.current_variable_set < progress.order.length - 1) {
539
- // reset the progress of the node to be with the new set
540
- this.nextSet();
541
- // then try to advance this node again.
542
- return this.advance();
543
- }
544
-
545
- // if we're all done with the timeline_variables, then check to see if there are more repetitions
546
- else if (progress.current_repetition < timeline_parameters.repetitions - 1) {
547
- this.nextRepetiton();
548
- return this.advance();
549
- }
550
-
551
- // if we're all done with the repetitions, check if there is a loop function.
552
- else if (typeof timeline_parameters.loop_function !== 'undefined') {
553
- if (timeline_parameters.loop_function(this.generatedData())) {
554
- this.reset();
555
- return parent_node.advance();
556
- } else {
557
- progress.done = true;
558
- return true;
559
- }
560
- }
561
-
562
- // no more loops on this timeline, we're done!
563
- else {
564
- progress.done = true;
565
- return true;
566
- }
567
-
568
- }
569
- }
570
-
571
- // check the status of the done flag
572
- this.isComplete = function() {
573
- return progress.done;
574
- }
575
-
576
- // getter method for timeline variables
577
- this.getTimelineVariableValue = function(variable_name){
578
- if(typeof timeline_parameters == 'undefined'){
579
- return undefined;
580
- }
581
- var v = timeline_parameters.timeline_variables[progress.order[progress.current_variable_set]][variable_name];
582
- return v;
583
- }
584
-
585
- // recursive upward search for timeline variables
586
- this.findTimelineVariable = function(variable_name){
587
- var v = this.getTimelineVariableValue(variable_name);
588
- if(typeof v == 'undefined'){
589
- if(typeof parent_node !== 'undefined'){
590
- return parent_node.findTimelineVariable(variable_name);
591
- } else {
592
- return undefined;
593
- }
594
- } else {
595
- return v;
596
- }
597
- }
598
-
599
- // recursive downward search for active trial to extract timeline variable
600
- this.timelineVariable = function(variable_name){
601
- if(typeof timeline_parameters == 'undefined'){
602
- return this.findTimelineVariable(variable_name);
603
- } else {
604
- // if progress.current_location is -1, then the timeline variable is being evaluated
605
- // in a function that runs prior to the trial starting, so we should treat that trial
606
- // as being the active trial for purposes of finding the value of the timeline variable
607
- var loc = Math.max(0, progress.current_location);
608
- // if loc is greater than the number of elements on this timeline, then the timeline
609
- // variable is being evaluated in a function that runs after the trial on the timeline
610
- // are complete but before advancing to the next (like a loop_function).
611
- // treat the last active trial as the active trial for this purpose.
612
- if(loc == timeline_parameters.timeline.length){
613
- loc = loc - 1;
614
- }
615
- // now find the variable
616
- return timeline_parameters.timeline[loc].timelineVariable(variable_name);
617
- }
618
- }
619
-
620
- // recursively get the number of **trials** contained in the timeline
621
- // assuming that while loops execute exactly once and if conditionals
622
- // always run
623
- this.length = function() {
624
- var length = 0;
625
- if (typeof timeline_parameters !== 'undefined') {
626
- for (var i = 0; i < timeline_parameters.timeline.length; i++) {
627
- length += timeline_parameters.timeline[i].length();
628
- }
629
- } else {
630
- return 1;
631
- }
632
- return length;
633
- }
634
-
635
- // return the percentage of trials completed, grouped at the first child level
636
- // counts a set of trials as complete when the child node is done
637
- this.percentComplete = function() {
638
- var total_trials = this.length();
639
- var completed_trials = 0;
640
- for (var i = 0; i < timeline_parameters.timeline.length; i++) {
641
- if (timeline_parameters.timeline[i].isComplete()) {
642
- completed_trials += timeline_parameters.timeline[i].length();
643
- }
644
- }
645
- return (completed_trials / total_trials * 100)
646
- }
647
-
648
- // resets the node and all subnodes to original state
649
- // but increments the current_iteration counter
650
- this.reset = function() {
651
- progress.current_location = -1;
652
- progress.current_repetition = 0;
653
- progress.current_variable_set = 0;
654
- progress.current_iteration++;
655
- progress.done = false;
656
- this.setTimelineVariablesOrder();
657
- if (typeof timeline_parameters != 'undefined') {
658
- for (var i = 0; i < timeline_parameters.timeline.length; i++) {
659
- timeline_parameters.timeline[i].reset();
660
- }
661
- }
662
-
663
- }
664
-
665
- // mark this node as finished
666
- this.end = function() {
667
- progress.done = true;
668
- }
669
-
670
- // recursively end whatever sub-node is running the current trial
671
- this.endActiveNode = function() {
672
- if (typeof timeline_parameters == 'undefined') {
673
- this.end();
674
- parent_node.end();
675
- } else {
676
- timeline_parameters.timeline[progress.current_location].endActiveNode();
677
- }
678
- }
679
-
680
- // get a unique ID associated with this node
681
- // the ID reflects the current iteration through this node.
682
- this.ID = function() {
683
- var id = "";
684
- if (typeof parent_node == 'undefined') {
685
- return "0." + progress.current_iteration;
686
- } else {
687
- id += parent_node.ID() + "-";
688
- id += relative_id + "." + progress.current_iteration;
689
- return id;
690
- }
691
- }
692
-
693
- // get the ID of the active trial
694
- this.activeID = function() {
695
- if (typeof timeline_parameters == 'undefined') {
696
- return this.ID();
697
- } else {
698
- return timeline_parameters.timeline[progress.current_location].activeID();
699
- }
700
- }
701
-
702
- // get all the data generated within this node
703
- this.generatedData = function() {
704
- return jsPsych.data.getDataByTimelineNode(this.ID());
705
- }
706
-
707
- // get all the trials of a particular type
708
- this.trialsOfType = function(type) {
709
- if (typeof timeline_parameters == 'undefined'){
710
- if (trial_parameters.type == type) {
711
- return trial_parameters;
712
- } else {
713
- return [];
714
- }
715
- } else {
716
- var trials = [];
717
- for (var i = 0; i < timeline_parameters.timeline.length; i++) {
718
- var t = timeline_parameters.timeline[i].trialsOfType(type);
719
- trials = trials.concat(t);
720
- }
721
- return trials;
722
- }
723
- }
724
-
725
- // add new trials to end of this timeline
726
- this.insert = function(parameters){
727
- if(typeof timeline_parameters == 'undefined'){
728
- console.error('Cannot add new trials to a trial-level node.');
729
- } else {
730
- timeline_parameters.timeline.push(
731
- new TimelineNode(Object.assign({}, node_trial_data, parameters), self, timeline_parameters.timeline.length)
732
- );
733
- }
734
- }
735
-
736
- // constructor
737
- var _construct = function() {
738
-
739
- // store a link to the parent of this node
740
- parent_node = parent;
741
-
742
- // create the ID for this node
743
- if (typeof parent == 'undefined') {
744
- relative_id = 0;
745
- } else {
746
- relative_id = relativeID;
747
- }
748
-
749
- // check if there is a timeline parameter
750
- // if there is, then this node has its own timeline
751
- if ((typeof parameters.timeline !== 'undefined') || (typeof jsPsych.plugins[trial_type] == 'function')) {
752
-
753
- // create timeline properties
754
- timeline_parameters = {
755
- timeline: [],
756
- loop_function: parameters.loop_function,
757
- conditional_function: parameters.conditional_function,
758
- sample: parameters.sample,
759
- randomize_order: typeof parameters.randomize_order == 'undefined' ? false : parameters.randomize_order,
760
- repetitions: typeof parameters.repetitions == 'undefined' ? 1 : parameters.repetitions,
761
- timeline_variables: typeof parameters.timeline_variables == 'undefined' ? [{}] : parameters.timeline_variables
762
- };
763
-
764
- self.setTimelineVariablesOrder();
765
-
766
- // extract all of the node level data and parameters
767
- var node_data = Object.assign({}, parameters);
768
- delete node_data.timeline;
769
- delete node_data.conditional_function;
770
- delete node_data.loop_function;
771
- delete node_data.randomize_order;
772
- delete node_data.repetitions;
773
- delete node_data.timeline_variables;
774
- delete node_data.sample;
775
- node_trial_data = node_data; // store for later...
776
-
777
- // create a TimelineNode for each element in the timeline
778
- for (var i = 0; i < parameters.timeline.length; i++) {
779
- // merge parameters
780
- var merged_parameters = Object.assign({}, node_data, parameters.timeline[i]);
781
- // merge any data from the parent node into child nodes
782
- if(typeof node_data.data == 'object' && typeof parameters.timeline[i].data == 'object'){
783
- var merged_data = Object.assign({}, node_data.data, parameters.timeline[i].data);
784
- merged_parameters.data = merged_data;
785
- }
786
- timeline_parameters.timeline.push(new TimelineNode(merged_parameters, self, i));
787
- }
788
- }
789
- // if there is no timeline parameter, then this node is a trial node
790
- else {
791
- // check to see if a valid trial type is defined
792
- var trial_type = parameters.type;
793
- if (typeof trial_type == 'undefined') {
794
- console.error('Trial level node is missing the "type" parameter. The parameters for the node are: ' + JSON.stringify(parameters));
795
- } else if ((typeof jsPsych.plugins[trial_type] == 'undefined') && (trial_type.toString().replace(/\s/g,'') != "function(){returntimeline.timelineVariable(varname);}")) {
796
- console.error('No plugin loaded for trials of type "' + trial_type + '"');
797
- }
798
- // create a deep copy of the parameters for the trial
799
- trial_parameters = Object.assign({}, parameters);
800
- }
801
-
802
- }();
803
- }
804
-
805
- function startExperiment() {
806
-
807
- loaded = true;
808
-
809
- // show progress bar if requested
810
- if (opts.show_progress_bar === true) {
811
- drawProgressBar(opts.message_progress_bar);
812
- }
813
-
814
- // record the start time
815
- exp_start_time = new Date();
816
-
817
- // begin!
818
- timeline.advance();
819
- doTrial(timeline.trial());
820
-
821
- }
822
-
823
- function finishExperiment() {
824
-
825
- if(typeof timeline.end_message !== 'undefined'){
826
- DOM_target.innerHTML = timeline.end_message;
827
- }
828
-
829
- opts.on_finish(jsPsych.data.get());
830
-
831
- }
832
-
833
- function nextTrial() {
834
- // if experiment is paused, don't do anything.
835
- if(paused) {
836
- waiting = true;
837
- return;
838
- }
839
-
840
- global_trial_index++;
841
-
842
- // advance timeline
843
- timeline.markCurrentTrialComplete();
844
- var complete = timeline.advance();
845
-
846
- // update progress bar if shown
847
- if (opts.show_progress_bar === true && opts.auto_update_progress_bar == true) {
848
- updateProgressBar();
849
- }
850
-
851
- // check if experiment is over
852
- if (complete) {
853
- finishExperiment();
854
- return;
855
- }
856
-
857
- doTrial(timeline.trial());
858
- }
859
-
860
- function doTrial(trial) {
861
-
862
- current_trial = trial;
863
- current_trial_finished = false;
864
-
865
- // process all timeline variables for this trial
866
- evaluateTimelineVariables(trial);
867
-
868
- // evaluate variables that are functions
869
- evaluateFunctionParameters(trial);
870
-
871
- // get default values for parameters
872
- setDefaultValues(trial);
873
-
874
- // call experiment wide callback
875
- opts.on_trial_start(trial);
876
-
877
- // call trial specific callback if it exists
878
- if(typeof trial.on_start == 'function'){
879
- trial.on_start(trial);
880
- }
881
-
882
- // apply the focus to the element containing the experiment.
883
- DOM_container.focus();
884
-
885
- // reset the scroll on the DOM target
886
- DOM_target.scrollTop = 0;
887
-
888
- // execute trial method
889
- jsPsych.plugins[trial.type].trial(DOM_target, trial);
890
-
891
- // call trial specific loaded callback if it exists
892
- if(typeof trial.on_load == 'function'){
893
- trial.on_load();
894
- }
895
- }
896
-
897
- function evaluateTimelineVariables(trial){
898
- var keys = Object.keys(trial);
899
-
900
- for (var i = 0; i < keys.length; i++) {
901
- // timeline variables on the root level
902
- if (typeof trial[keys[i]] == "function" && trial[keys[i]].toString().replace(/\s/g,'') == "function(){returntimeline.timelineVariable(varname);}") {
903
- trial[keys[i]] = trial[keys[i]].call();
904
- }
905
- // timeline variables that are nested in objects
906
- if (typeof trial[keys[i]] == "object" && trial[keys[i]] !== null){
907
- evaluateTimelineVariables(trial[keys[i]]);
908
- }
909
- }
910
- }
911
-
912
- function evaluateFunctionParameters(trial){
913
-
914
- // first, eval the trial type if it is a function
915
- // this lets users set the plugin type with a function
916
- if(typeof trial.type === 'function'){
917
- trial.type = trial.type.call();
918
- }
919
-
920
- // now eval the whole trial
921
-
922
- // start by getting a list of the parameters
923
- var keys = Object.keys(trial);
924
-
925
- // iterate over each parameter
926
- for (var i = 0; i < keys.length; i++) {
927
- // check to make sure parameter is not "type", since that was eval'd above.
928
- if(keys[i] !== 'type'){
929
- // 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.
930
- // the first line checks if the parameter is defined in the universalPluginParameters set
931
- // the second line checks the plugin-specific parameters
932
- if(
933
- (typeof jsPsych.plugins.universalPluginParameters[keys[i]] !== 'undefined' && jsPsych.plugins.universalPluginParameters[keys[i]].type !== jsPsych.plugins.parameterType.FUNCTION ) ||
934
- (typeof jsPsych.plugins[trial.type].info.parameters[keys[i]] !== 'undefined' && jsPsych.plugins[trial.type].info.parameters[keys[i]].type !== jsPsych.plugins.parameterType.FUNCTION)
935
- ) {
936
- if (typeof trial[keys[i]] == "function") {
937
- trial[keys[i]] = trial[keys[i]].call();
938
- }
939
- }
940
- }
941
- // add a special exception for the data parameter so we can evaluate functions. eventually this could be generalized so that any COMPLEX object type could
942
- // be evaluated at the individual parameter level.
943
- if(keys[i] == 'data'){
944
- var data_params = Object.keys(trial[keys[i]]);
945
- for(var j=0; j<data_params.length; j++){
946
- if(typeof trial[keys[i]][data_params[j]] == "function") {
947
- trial[keys[i]][data_params[j]] = trial[keys[i]][data_params[j]].call();
948
- }
949
- }
950
- }
951
- }
952
- }
953
-
954
- function setDefaultValues(trial){
955
- for(var param in jsPsych.plugins[trial.type].info.parameters){
956
- // check if parameter is complex with nested defaults
957
- if(jsPsych.plugins[trial.type].info.parameters[param].type == jsPsych.plugins.parameterType.COMPLEX){
958
- if(jsPsych.plugins[trial.type].info.parameters[param].array == true){
959
- // iterate over each entry in the array
960
- trial[param].forEach(function(ip, i){
961
- // check each parameter in the plugin description
962
- for(var p in jsPsych.plugins[trial.type].info.parameters[param].nested){
963
- if(typeof trial[param][i][p] == 'undefined' || trial[param][i][p] === null){
964
- if(typeof jsPsych.plugins[trial.type].info.parameters[param].nested[p].default == 'undefined'){
965
- console.error('You must specify a value for the '+p+' parameter (nested in the '+param+' parameter) in the '+trial.type+' plugin.');
966
- } else {
967
- trial[param][i][p] = jsPsych.plugins[trial.type].info.parameters[param].nested[p].default;
968
- }
969
- }
970
- }
971
- });
972
- }
973
- }
974
- // if it's not nested, checking is much easier and do that here:
975
- else if(typeof trial[param] == 'undefined' || trial[param] === null){
976
- if(typeof jsPsych.plugins[trial.type].info.parameters[param].default == 'undefined'){
977
- console.error('You must specify a value for the '+param+' parameter in the '+trial.type+' plugin.');
978
- } else {
979
- trial[param] = jsPsych.plugins[trial.type].info.parameters[param].default;
980
- }
981
- }
982
- }
983
- }
984
-
985
- function checkExclusions(exclusions, success, fail){
986
- var clear = true;
987
-
988
- // MINIMUM SIZE
989
- if(typeof exclusions.min_width !== 'undefined' || typeof exclusions.min_height !== 'undefined'){
990
- var mw = typeof exclusions.min_width !== 'undefined' ? exclusions.min_width : 0;
991
- var mh = typeof exclusions.min_height !== 'undefined' ? exclusions.min_height : 0;
992
- var w = window.innerWidth;
993
- var h = window.innerHeight;
994
- if(w < mw || h < mh){
995
- clear = false;
996
- var interval = setInterval(function(){
997
- var w = window.innerWidth;
998
- var h = window.innerHeight;
999
- if(w < mw || h < mh){
1000
- var msg = '<p>Your browser window is too small to complete this experiment. '+
1001
- 'Please maximize the size of your browser window. If your browser window is already maximized, '+
1002
- 'you will not be able to complete this experiment.</p>'+
1003
- '<p>The minimum width is '+mw+'px. Your current width is '+w+'px.</p>'+
1004
- '<p>The minimum height is '+mh+'px. Your current height is '+h+'px.</p>';
1005
- core.getDisplayElement().innerHTML = msg;
1006
- } else {
1007
- clearInterval(interval);
1008
- core.getDisplayElement().innerHTML = '';
1009
- checkExclusions(exclusions, success, fail);
1010
- }
1011
- }, 100);
1012
- return; // prevents checking other exclusions while this is being fixed
1013
- }
1014
- }
1015
-
1016
- // WEB AUDIO API
1017
- if(typeof exclusions.audio !== 'undefined' && exclusions.audio) {
1018
- if(window.hasOwnProperty('AudioContext') || window.hasOwnProperty('webkitAudioContext')){
1019
- // clear
1020
- } else {
1021
- clear = false;
1022
- var msg = '<p>Your browser does not support the WebAudio API, which means that you will not '+
1023
- 'be able to complete the experiment.</p><p>Browsers that support the WebAudio API include '+
1024
- 'Chrome, Firefox, Safari, and Edge.</p>';
1025
- core.getDisplayElement().innerHTML = msg;
1026
- fail();
1027
- return;
1028
- }
1029
- }
1030
-
1031
- // GO?
1032
- if(clear){ success(); }
1033
- }
1034
-
1035
- function drawProgressBar(msg) {
1036
- document.querySelector('.jspsych-display-element').insertAdjacentHTML('afterbegin',
1037
- '<div id="jspsych-progressbar-container">'+
1038
- '<span>'+
1039
- msg+
1040
- '</span>'+
1041
- '<div id="jspsych-progressbar-outer">'+
1042
- '<div id="jspsych-progressbar-inner"></div>'+
1043
- '</div></div>');
1044
- }
1045
-
1046
- function updateProgressBar() {
1047
- var progress = jsPsych.progress().percent_complete;
1048
- core.setProgressBar(progress / 100);
1049
- }
1050
-
1051
- var progress_bar_amount = 0;
1052
-
1053
- core.setProgressBar = function(proportion_complete){
1054
- proportion_complete = Math.max(Math.min(1,proportion_complete),0);
1055
- document.querySelector('#jspsych-progressbar-inner').style.width = (proportion_complete*100) + "%";
1056
- progress_bar_amount = proportion_complete;
1057
- }
1058
-
1059
- core.getProgressBarCompleted = function(){
1060
- return progress_bar_amount;
1061
- }
1062
-
1063
- //Leave a trace in the DOM that jspsych was loaded
1064
- document.documentElement.setAttribute('jspsych', 'present');
1065
-
1066
- return core;
1067
- })();
1068
-
1069
- jsPsych.plugins = (function() {
1070
-
1071
- var module = {};
1072
-
1073
- // enumerate possible parameter types for plugins
1074
- module.parameterType = {
1075
- BOOL: 0,
1076
- STRING: 1,
1077
- INT: 2,
1078
- FLOAT: 3,
1079
- FUNCTION: 4,
1080
- KEYCODE: 5,
1081
- SELECT: 6,
1082
- HTML_STRING: 7,
1083
- IMAGE: 8,
1084
- AUDIO: 9,
1085
- VIDEO: 10,
1086
- OBJECT: 11,
1087
- COMPLEX: 12
1088
- }
1089
-
1090
- module.universalPluginParameters = {
1091
- data: {
1092
- type: module.parameterType.OBJECT,
1093
- pretty_name: 'Data',
1094
- default: {},
1095
- description: 'Data to add to this trial (key-value pairs)'
1096
- },
1097
- on_start: {
1098
- type: module.parameterType.FUNCTION,
1099
- pretty_name: 'On start',
1100
- default: function() { return; },
1101
- description: 'Function to execute when trial begins'
1102
- },
1103
- on_finish: {
1104
- type: module.parameterType.FUNCTION,
1105
- pretty_name: 'On finish',
1106
- default: function() { return; },
1107
- description: 'Function to execute when trial is finished'
1108
- },
1109
- on_load: {
1110
- type: module.parameterType.FUNCTION,
1111
- pretty_name: 'On load',
1112
- default: function() { return; },
1113
- description: 'Function to execute after the trial has loaded'
1114
- },
1115
- post_trial_gap: {
1116
- type: module.parameterType.INT,
1117
- pretty_name: 'Post trial gap',
1118
- default: null,
1119
- description: 'Length of gap between the end of this trial and the start of the next trial'
1120
- }
1121
- }
1122
-
1123
- return module;
1124
- })();
1125
-
1126
- jsPsych.data = (function() {
1127
-
1128
- var module = {};
1129
-
1130
- // data storage object
1131
- var allData = DataCollection();
1132
-
1133
- // browser interaction event data
1134
- var interactionData = DataCollection();
1135
-
1136
- // data properties for all trials
1137
- var dataProperties = {};
1138
-
1139
- // cache the query_string
1140
- var query_string;
1141
-
1142
- // DataCollection
1143
- function DataCollection(data){
1144
-
1145
- var data_collection = {};
1146
-
1147
- var trials = typeof data === 'undefined' ? [] : data;
1148
-
1149
- data_collection.push = function(new_data){
1150
- trials.push(new_data);
1151
- return data_collection;
1152
- }
1153
-
1154
- data_collection.join = function(other_data_collection){
1155
- trials = trials.concat(other_data_collection.values());
1156
- return data_collection;
1157
- }
1158
-
1159
- data_collection.top = function(){
1160
- if(trials.length <= 1){
1161
- return data_collection;
1162
- } else {
1163
- return DataCollection([trials[trials.length-1]]);
1164
- }
1165
- }
1166
-
1167
- /**
1168
- * Queries the first n elements in a collection of trials.
1169
- *
1170
- * @param {number} n A positive integer of elements to return. A value of
1171
- * n that is less than 1 will throw an error.
1172
- *
1173
- * @return {Array} First n objects of a collection of trials. If fewer than
1174
- * n trials are available, the trials.length elements will
1175
- * be returned.
1176
- *
1177
- */
1178
- data_collection.first = function(n){
1179
- if (typeof n == 'undefined') { n = 1 }
1180
- if (n < 1) {
1181
- throw `You must query with a positive nonzero integer. Please use a
1182
- different value for n.`;
1183
- }
1184
- if (trials.length == 0) return DataCollection([]);
1185
- if (n > trials.length) n = trials.length;
1186
- return DataCollection(trials.slice(0, n));
1187
- }
1188
-
1189
- /**
1190
- * Queries the last n elements in a collection of trials.
1191
- *
1192
- * @param {number} n A positive integer of elements to return. A value of
1193
- * n that is less than 1 will throw an error.
1194
- *
1195
- * @return {Array} Last n objects of a collection of trials. If fewer than
1196
- * n trials are available, the trials.length elements will
1197
- * be returned.
1198
- *
1199
- */
1200
- data_collection.last = function(n) {
1201
- if (typeof n == 'undefined') { n = 1 }
1202
- if (n < 1) {
1203
- throw `You must query with a positive nonzero integer. Please use a
1204
- different value for n.`;
1205
- }
1206
- if (trials.length == 0) return DataCollection([]);
1207
- if (n > trials.length) n = trials.length;
1208
- return DataCollection(trials.slice(trials.length - n, trials.length));
1209
- }
1210
-
1211
- data_collection.values = function(){
1212
- return trials;
1213
- }
1214
-
1215
- data_collection.count = function(){
1216
- return trials.length;
1217
- }
1218
-
1219
- data_collection.readOnly = function(){
1220
- return DataCollection(jsPsych.utils.deepCopy(trials));
1221
- }
1222
-
1223
- data_collection.addToAll = function(properties){
1224
- for (var i = 0; i < trials.length; i++) {
1225
- for (var key in properties) {
1226
- trials[i][key] = properties[key];
1227
- }
1228
- }
1229
- return data_collection;
1230
- }
1231
-
1232
- data_collection.addToLast = function(properties){
1233
- if(trials.length != 0){
1234
- for (var key in properties) {
1235
- trials[trials.length-1][key] = properties[key];
1236
- }
1237
- }
1238
- return data_collection;
1239
- }
1240
-
1241
- data_collection.filter = function(filters){
1242
- // [{p1: v1, p2:v2}, {p1:v2}]
1243
- // {p1: v1}
1244
- if(!Array.isArray(filters)){
1245
- var f = jsPsych.utils.deepCopy([filters]);
1246
- } else {
1247
- var f = jsPsych.utils.deepCopy(filters);
1248
- }
1249
-
1250
- var filtered_data = [];
1251
- for(var x=0; x < trials.length; x++){
1252
- var keep = false;
1253
- for(var i=0; i<f.length; i++){
1254
- var match = true;
1255
- var keys = Object.keys(f[i]);
1256
- for(var k=0; k<keys.length; k++){
1257
- if(typeof trials[x][keys[k]] !== 'undefined' && trials[x][keys[k]] == f[i][keys[k]]){
1258
- // matches on this key!
1259
- } else {
1260
- match = false;
1261
- }
1262
- }
1263
- if(match) { keep = true; break; } // can break because each filter is OR.
1264
- }
1265
- if(keep){
1266
- filtered_data.push(trials[x]);
1267
- }
1268
- }
1269
-
1270
- var out = DataCollection(filtered_data);
1271
-
1272
- return out;
1273
- }
1274
-
1275
- data_collection.filterCustom = function(fn){
1276
- var included = [];
1277
- for(var i=0; i<trials.length; i++){
1278
- if(fn(trials[i])){
1279
- included.push(trials[i]);
1280
- }
1281
- }
1282
- return DataCollection(included);
1283
- }
1284
-
1285
- data_collection.select = function(column){
1286
- var values = [];
1287
- for(var i=0; i<trials.length; i++){
1288
- if(typeof trials[i][column] !== 'undefined'){
1289
- values.push(trials[i][column]);
1290
- }
1291
- }
1292
- var out = DataColumn();
1293
- out.values = values;
1294
- return out;
1295
- }
1296
-
1297
- data_collection.ignore = function(columns){
1298
- if(!Array.isArray(columns)){
1299
- columns = [columns];
1300
- }
1301
- var o = jsPsych.utils.deepCopy(trials);
1302
- for (var i = 0; i < o.length; i++) {
1303
- for (var j in columns) {
1304
- delete o[i][columns[j]];
1305
- }
1306
- }
1307
- return DataCollection(o);
1308
- }
1309
-
1310
- data_collection.uniqueNames = function(){
1311
- var names = [];
1312
-
1313
- for(var i=0; i<trials.length; i++){
1314
- var keys = Object.keys(trials[i]);
1315
- for(var j=0; j<keys.length; j++){
1316
- if(!names.includes(keys[j])){
1317
- names.push(keys[j]);
1318
- }
1319
- }
1320
- }
1321
-
1322
- return names;
1323
- }
1324
-
1325
- data_collection.csv = function(){
1326
- return JSON2CSV(trials);
1327
- }
1328
-
1329
- data_collection.json = function(pretty){
1330
- if(pretty){
1331
- return JSON.stringify(trials, null, '\t');
1332
- }
1333
- return JSON.stringify(trials);
1334
- }
1335
-
1336
- data_collection.localSave = function(format, filename){
1337
- var data_string;
1338
-
1339
- if (format == 'JSON' || format == 'json') {
1340
- data_string = data_collection.json();
1341
- } else if (format == 'CSV' || format == 'csv') {
1342
- data_string = data_collection.csv();
1343
- } else {
1344
- throw new Error('Invalid format specified for localSave. Must be "JSON" or "CSV".');
1345
- }
1346
-
1347
- saveTextToFile(data_string, filename);
1348
- }
1349
-
1350
- return data_collection;
1351
- }
1352
-
1353
- // DataColumn class
1354
- function DataColumn(){
1355
- var data_column = {};
1356
-
1357
- data_column.values = [];
1358
-
1359
- data_column.sum = function(){
1360
- var s = 0;
1361
- for(var i=0; i<data_column.values.length; i++){
1362
- s += data_column.values[i];
1363
- }
1364
- return s;
1365
- }
1366
-
1367
- data_column.mean = function(){
1368
- return data_column.sum() / data_column.count();
1369
- }
1370
-
1371
- data_column.median = function(){
1372
- if (data_column.values.length == 0) {return undefined};
1373
- var numbers = data_column.values.slice(0).sort(function(a,b){ return a - b; });
1374
- var middle = Math.floor(numbers.length / 2);
1375
- var isEven = numbers.length % 2 === 0;
1376
- return isEven ? (numbers[middle] + numbers[middle - 1]) / 2 : numbers[middle];
1377
- }
1378
-
1379
- data_column.min = function(){
1380
- return Math.min.apply(null, data_column.values);
1381
- }
1382
-
1383
- data_column.max = function(){
1384
- return Math.max.apply(null, data_column.values);
1385
- }
1386
-
1387
- data_column.count = function(){
1388
- return data_column.values.length;
1389
- }
1390
-
1391
- data_column.variance = function(){
1392
- var mean = data_column.mean();
1393
- var sum_square_error = 0;
1394
- for(var i=0; i<data_column.values.length; i++){
1395
- sum_square_error += Math.pow(data_column.values[i] - mean,2);
1396
- }
1397
- var mse = sum_square_error / (data_column.values.length - 1);
1398
- return mse;
1399
- }
1400
-
1401
- data_column.sd = function(){
1402
- var mse = data_column.variance();
1403
- var rmse = Math.sqrt(mse);
1404
- return rmse;
1405
- }
1406
-
1407
- data_column.frequencies = function(){
1408
- var unique = {}
1409
- for(var i=0; i<data_column.values.length; i++){
1410
- var v = data_column.values[i];
1411
- if(typeof unique[v] == 'undefined'){
1412
- unique[v] = 1;
1413
- } else {
1414
- unique[v]++;
1415
- }
1416
- }
1417
- return unique;
1418
- }
1419
-
1420
- data_column.all = function(eval_fn){
1421
- for(var i=0; i<data_column.values.length; i++){
1422
- if(!eval_fn(data_column.values[i])){
1423
- return false;
1424
- }
1425
- }
1426
- return true;
1427
- }
1428
-
1429
- data_column.subset = function(eval_fn){
1430
- var out = [];
1431
- for(var i=0; i<data_column.values.length; i++){
1432
- if(eval_fn(data_column.values[i])){
1433
- out.push(data_column.values[i]);
1434
- }
1435
- }
1436
- var o = DataColumn();
1437
- o.values = out;
1438
- return o;
1439
- }
1440
-
1441
- return data_column;
1442
- }
1443
-
1444
- module.reset = function(){
1445
- allData = DataCollection();
1446
- interactionData = DataCollection();
1447
- }
1448
-
1449
- module.get = function() {
1450
- return allData;
1451
- };
1452
-
1453
- module.getInteractionData = function() {
1454
- return interactionData;
1455
- }
1456
-
1457
- module.write = function(data_object) {
1458
-
1459
- var progress = jsPsych.progress();
1460
- var trial = jsPsych.currentTrial();
1461
-
1462
- //var trial_opt_data = typeof trial.data == 'function' ? trial.data() : trial.data;
1463
-
1464
- var default_data = {
1465
- 'trial_type': trial.type,
1466
- 'trial_index': progress.current_trial_global,
1467
- 'time_elapsed': jsPsych.totalTime(),
1468
- 'internal_node_id': jsPsych.currentTimelineNodeID()
1469
- };
1470
-
1471
- var ext_data_object = Object.assign({}, data_object, trial.data, default_data, dataProperties);
1472
-
1473
- allData.push(ext_data_object);
1474
- };
1475
-
1476
- module.addProperties = function(properties) {
1477
-
1478
- // first, add the properties to all data that's already stored
1479
- allData.addToAll(properties);
1480
-
1481
- // now add to list so that it gets appended to all future data
1482
- dataProperties = Object.assign({}, dataProperties, properties);
1483
-
1484
- };
1485
-
1486
- module.addDataToLastTrial = function(data) {
1487
- allData.addToLast(data);
1488
- }
1489
-
1490
- module.getDataByTimelineNode = function(node_id) {
1491
- var data = allData.filterCustom(function(x){
1492
- return x.internal_node_id.slice(0, node_id.length) === node_id;
1493
- });
1494
-
1495
- return data;
1496
- };
1497
-
1498
- module.getLastTrialData = function() {
1499
- return allData.top();
1500
- };
1501
-
1502
- module.getLastTimelineData = function() {
1503
- var lasttrial = module.getLastTrialData();
1504
- var node_id = lasttrial.select('internal_node_id').values[0];
1505
- if (typeof node_id === 'undefined') {
1506
- return DataCollection();
1507
- } else {
1508
- var parent_node_id = node_id.substr(0,node_id.lastIndexOf('-'));
1509
- var lastnodedata = module.getDataByTimelineNode(parent_node_id);
1510
- return lastnodedata;
1511
- }
1512
- }
1513
-
1514
- module.displayData = function(format) {
1515
- format = (typeof format === 'undefined') ? "json" : format.toLowerCase();
1516
- if (format != "json" && format != "csv") {
1517
- console.log('Invalid format declared for displayData function. Using json as default.');
1518
- format = "json";
1519
- }
1520
-
1521
- var data_string;
1522
-
1523
- if (format == 'json') {
1524
- data_string = allData.json(true); // true = pretty print with tabs
1525
- } else {
1526
- data_string = allData.csv();
1527
- }
1528
-
1529
- var display_element = jsPsych.getDisplayElement();
1530
-
1531
- display_element.innerHTML = '<pre id="jspsych-data-display"></pre>';
1532
-
1533
- document.getElementById('jspsych-data-display').textContent = data_string;
1534
- };
1535
-
1536
- module.urlVariables = function() {
1537
- if(typeof query_string == 'undefined'){
1538
- query_string = getQueryString();
1539
- }
1540
- return query_string;
1541
- }
1542
-
1543
- module.getURLVariable = function(whichvar){
1544
- if(typeof query_string == 'undefined'){
1545
- query_string = getQueryString();
1546
- }
1547
- return query_string[whichvar];
1548
- }
1549
-
1550
- module.createInteractionListeners = function(){
1551
- // blur event capture
1552
- window.addEventListener('blur', function(){
1553
- var data = {
1554
- event: 'blur',
1555
- trial: jsPsych.progress().current_trial_global,
1556
- time: jsPsych.totalTime()
1557
- };
1558
- interactionData.push(data);
1559
- jsPsych.initSettings().on_interaction_data_update(data);
1560
- });
1561
-
1562
- // focus event capture
1563
- window.addEventListener('focus', function(){
1564
- var data = {
1565
- event: 'focus',
1566
- trial: jsPsych.progress().current_trial_global,
1567
- time: jsPsych.totalTime()
1568
- };
1569
- interactionData.push(data);
1570
- jsPsych.initSettings().on_interaction_data_update(data);
1571
- });
1572
-
1573
- // fullscreen change capture
1574
- function fullscreenchange(){
1575
- var type = (document.isFullScreen || document.webkitIsFullScreen || document.mozIsFullScreen || document.fullscreenElement) ? 'fullscreenenter' : 'fullscreenexit';
1576
- var data = {
1577
- event: type,
1578
- trial: jsPsych.progress().current_trial_global,
1579
- time: jsPsych.totalTime()
1580
- };
1581
- interactionData.push(data);
1582
- jsPsych.initSettings().on_interaction_data_update(data);
1583
- }
1584
-
1585
- document.addEventListener('fullscreenchange', fullscreenchange);
1586
- document.addEventListener('mozfullscreenchange', fullscreenchange);
1587
- document.addEventListener('webkitfullscreenchange', fullscreenchange);
1588
- }
1589
-
1590
- // public methods for testing purposes. not recommended for use.
1591
- module._customInsert = function(data){
1592
- allData = DataCollection(data);
1593
- }
1594
-
1595
- module._fullreset = function(){
1596
- module.reset();
1597
- dataProperties = {};
1598
- }
1599
-
1600
- // private function to save text file on local drive
1601
- function saveTextToFile(textstr, filename) {
1602
- var blobToSave = new Blob([textstr], {
1603
- type: 'text/plain'
1604
- });
1605
- var blobURL = "";
1606
- if (typeof window.webkitURL !== 'undefined') {
1607
- blobURL = window.webkitURL.createObjectURL(blobToSave);
1608
- } else {
1609
- blobURL = window.URL.createObjectURL(blobToSave);
1610
- }
1611
-
1612
- var display_element = jsPsych.getDisplayElement();
1613
-
1614
- display_element.insertAdjacentHTML('beforeend','<a id="jspsych-download-as-text-link" style="display:none;" download="'+filename+'" href="'+blobURL+'">click to download</a>');
1615
- document.getElementById('jspsych-download-as-text-link').click();
1616
- }
1617
-
1618
- //
1619
- // A few helper functions to handle data format conversion
1620
- //
1621
-
1622
- // this function based on code suggested by StackOverflow users:
1623
- // http://stackoverflow.com/users/64741/zachary
1624
- // http://stackoverflow.com/users/317/joseph-sturtevant
1625
-
1626
- function JSON2CSV(objArray) {
1627
- var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
1628
- var line = '';
1629
- var result = '';
1630
- var columns = [];
1631
-
1632
- var i = 0;
1633
- for (var j = 0; j < array.length; j++) {
1634
- for (var key in array[j]) {
1635
- var keyString = key + "";
1636
- keyString = '"' + keyString.replace(/"/g, '""') + '",';
1637
- if (!columns.includes(key)) {
1638
- columns[i] = key;
1639
- line += keyString;
1640
- i++;
1641
- }
1642
- }
1643
- }
1644
-
1645
- line = line.slice(0, -1);
1646
- result += line + '\r\n';
1647
-
1648
- for (var i = 0; i < array.length; i++) {
1649
- var line = '';
1650
- for (var j = 0; j < columns.length; j++) {
1651
- var value = (typeof array[i][columns[j]] === 'undefined') ? '' : array[i][columns[j]];
1652
- var valueString = value + "";
1653
- line += '"' + valueString.replace(/"/g, '""') + '",';
1654
- }
1655
-
1656
- line = line.slice(0, -1);
1657
- result += line + '\r\n';
1658
- }
1659
-
1660
- return result;
1661
- }
1662
-
1663
- // this function is modified from StackOverflow:
1664
- // http://stackoverflow.com/posts/3855394
1665
-
1666
- function getQueryString() {
1667
- var a = window.location.search.substr(1).split('&');
1668
- if (a == "") return {};
1669
- var b = {};
1670
- for (var i = 0; i < a.length; ++i)
1671
- {
1672
- var p=a[i].split('=', 2);
1673
- if (p.length == 1)
1674
- b[p[0]] = "";
1675
- else
1676
- b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
1677
- }
1678
- return b;
1679
- }
1680
-
1681
- return module;
1682
-
1683
- })();
1684
-
1685
- jsPsych.turk = (function() {
1686
-
1687
- var module = {};
1688
-
1689
- // core.turkInfo gets information relevant to mechanical turk experiments. returns an object
1690
- // containing the workerID, assignmentID, and hitID, and whether or not the HIT is in
1691
- // preview mode, meaning that they haven't accepted the HIT yet.
1692
- module.turkInfo = function() {
1693
-
1694
- var turk = {};
1695
-
1696
- var param = function(url, name) {
1697
- name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
1698
- var regexS = "[\\?&]" + name + "=([^&#]*)";
1699
- var regex = new RegExp(regexS);
1700
- var results = regex.exec(url);
1701
- return (results == null) ? "" : results[1];
1702
- };
1703
-
1704
- var src = param(window.location.href, "assignmentId") ? window.location.href : document.referrer;
1705
-
1706
- var keys = ["assignmentId", "hitId", "workerId", "turkSubmitTo"];
1707
- keys.map(
1708
-
1709
- function(key) {
1710
- turk[key] = unescape(param(src, key));
1711
- });
1712
-
1713
- turk.previewMode = (turk.assignmentId == "ASSIGNMENT_ID_NOT_AVAILABLE");
1714
-
1715
- turk.outsideTurk = (!turk.previewMode && turk.hitId === "" && turk.assignmentId == "" && turk.workerId == "")
1716
-
1717
- turk_info = turk;
1718
-
1719
- return turk;
1720
-
1721
- };
1722
-
1723
- // core.submitToTurk will submit a MechanicalTurk ExternalHIT type
1724
- module.submitToTurk = function(data) {
1725
-
1726
- var turkInfo = jsPsych.turk.turkInfo();
1727
- var assignmentId = turkInfo.assignmentId;
1728
- var turkSubmitTo = turkInfo.turkSubmitTo;
1729
-
1730
- if (!assignmentId || !turkSubmitTo) return;
1731
-
1732
- var dataString = [];
1733
-
1734
- for (var key in data) {
1735
-
1736
- if (data.hasOwnProperty(key)) {
1737
- dataString.push(key + "=" + escape(data[key]));
1738
- }
1739
- }
1740
-
1741
- dataString.push("assignmentId=" + assignmentId);
1742
-
1743
- var url = turkSubmitTo + "/mturk/externalSubmit?" + dataString.join("&");
1744
-
1745
- window.location.href = url;
1746
- };
1747
-
1748
- return module;
1749
-
1750
- })();
1751
-
1752
- jsPsych.randomization = (function() {
1753
-
1754
- var module = {};
1755
-
1756
- module.repeat = function(array, repetitions, unpack) {
1757
-
1758
- var arr_isArray = Array.isArray(array);
1759
- var rep_isArray = Array.isArray(repetitions);
1760
-
1761
- // if array is not an array, then we just repeat the item
1762
- if (!arr_isArray) {
1763
- if (!rep_isArray) {
1764
- array = [array];
1765
- repetitions = [repetitions];
1766
- } else {
1767
- repetitions = [repetitions[0]];
1768
- console.log('Unclear parameters given to randomization.repeat. Multiple set sizes specified, but only one item exists to sample. Proceeding using the first set size.');
1769
- }
1770
- } else {
1771
- if (!rep_isArray) {
1772
- var reps = [];
1773
- for (var i = 0; i < array.length; i++) {
1774
- reps.push(repetitions);
1775
- }
1776
- repetitions = reps;
1777
- } else {
1778
- if (array.length != repetitions.length) {
1779
- console.warning('Unclear parameters given to randomization.repeat. Items and repetitions are unequal lengths. Behavior may not be as expected.');
1780
- // throw warning if repetitions is too short, use first rep ONLY.
1781
- if (repetitions.length < array.length) {
1782
- var reps = [];
1783
- for (var i = 0; i < array.length; i++) {
1784
- reps.push(repetitions);
1785
- }
1786
- repetitions = reps;
1787
- } else {
1788
- // throw warning if too long, and then use the first N
1789
- repetitions = repetitions.slice(0, array.length);
1790
- }
1791
- }
1792
- }
1793
- }
1794
-
1795
- // should be clear at this point to assume that array and repetitions are arrays with == length
1796
- var allsamples = [];
1797
- for (var i = 0; i < array.length; i++) {
1798
- for (var j = 0; j < repetitions[i]; j++) {
1799
- if(array[i] == null || typeof array[i] != 'object'){
1800
- allsamples.push(array[i]);
1801
- } else {
1802
- allsamples.push(Object.assign({}, array[i]));
1803
- }
1804
-
1805
- }
1806
- }
1807
-
1808
- var out = shuffle(allsamples);
1809
-
1810
- if (unpack) {
1811
- out = unpackArray(out);
1812
- }
1813
-
1814
- return out;
1815
- }
1816
-
1817
- module.shuffle = function(arr) {
1818
- if(!Array.isArray(arr)){
1819
- console.error('Argument to jsPsych.randomization.shuffle() must be an array.')
1820
- }
1821
- return shuffle(arr);
1822
- }
1823
-
1824
- module.shuffleNoRepeats = function(arr, equalityTest) {
1825
- if(!Array.isArray(arr)){
1826
- console.error('First argument to jsPsych.randomization.shuffleNoRepeats() must be an array.')
1827
- }
1828
- if(typeof equalityTest !== 'undefined' && typeof equalityTest !== 'function'){
1829
- console.error('Second argument to jsPsych.randomization.shuffleNoRepeats() must be a function.')
1830
- }
1831
- // define a default equalityTest
1832
- if (typeof equalityTest == 'undefined') {
1833
- equalityTest = function(a, b) {
1834
- if (a === b) {
1835
- return true;
1836
- } else {
1837
- return false;
1838
- }
1839
- }
1840
- }
1841
-
1842
- var random_shuffle = shuffle(arr);
1843
- for (var i = 0; i < random_shuffle.length - 1; i++) {
1844
- if (equalityTest(random_shuffle[i], random_shuffle[i + 1])) {
1845
- // neighbors are equal, pick a new random neighbor to swap (not the first or last element, to avoid edge cases)
1846
- var random_pick = Math.floor(Math.random() * (random_shuffle.length - 2)) + 1;
1847
- // test to make sure the new neighbor isn't equal to the old one
1848
- while (
1849
- equalityTest(random_shuffle[i + 1], random_shuffle[random_pick]) ||
1850
- (equalityTest(random_shuffle[i + 1], random_shuffle[random_pick + 1]) || equalityTest(random_shuffle[i + 1], random_shuffle[random_pick - 1]))
1851
- ) {
1852
- random_pick = Math.floor(Math.random() * (random_shuffle.length - 2)) + 1;
1853
- }
1854
- var new_neighbor = random_shuffle[random_pick];
1855
- random_shuffle[random_pick] = random_shuffle[i + 1];
1856
- random_shuffle[i + 1] = new_neighbor;
1857
- }
1858
- }
1859
-
1860
- return random_shuffle;
1861
- }
1862
-
1863
- module.shuffleAlternateGroups = function(arr_groups, random_group_order){
1864
- if(typeof random_group_order == 'undefined'){
1865
- random_group_order = false;
1866
- }
1867
-
1868
- var n_groups = arr_groups.length;
1869
- if(n_groups == 1){
1870
- console.warn('jsPsych.randomization.shuffleAlternateGroups was called with only one group. Defaulting to simple shuffle.');
1871
- return(module.shuffle(arr_groups[0]));
1872
- }
1873
-
1874
- var group_order = [];
1875
- for(var i=0; i<n_groups; i++){
1876
- group_order.push(i);
1877
- }
1878
- if(random_group_order){
1879
- group_order = module.shuffle(group_order);
1880
- }
1881
-
1882
- var randomized_groups = [];
1883
- var min_length = null;
1884
- for(var i=0; i<n_groups; i++){
1885
- min_length = min_length === null ? arr_groups[i].length : Math.min(min_length, arr_groups[i].length);
1886
- randomized_groups.push(module.shuffle(arr_groups[i]));
1887
- }
1888
-
1889
- var out = [];
1890
- for(var i=0; i<min_length; i++){
1891
- for(var j=0; j<group_order.length; j++){
1892
- out.push(randomized_groups[group_order[j]][i])
1893
- }
1894
- }
1895
-
1896
- return out;
1897
- }
1898
-
1899
- module.sampleWithoutReplacement = function(arr, size){
1900
- if(!Array.isArray(arr)){
1901
- console.error("First argument to jsPsych.randomization.sampleWithoutReplacement() must be an array")
1902
- }
1903
-
1904
- if (size > arr.length) {
1905
- console.error("Cannot take a sample " +
1906
- "larger than the size of the set of items to sample.");
1907
- }
1908
- return jsPsych.randomization.shuffle(arr).slice(0,size);
1909
- }
1910
-
1911
- module.sampleWithReplacement = function(arr, size, weights) {
1912
- if(!Array.isArray(arr)){
1913
- console.error("First argument to jsPsych.randomization.sampleWithReplacement() must be an array")
1914
- }
1915
-
1916
- var normalized_weights = [];
1917
- if(typeof weights !== 'undefined'){
1918
- if(weights.length !== arr.length){
1919
- console.error('The length of the weights array must equal the length of the array '+
1920
- 'to be sampled from.');
1921
- }
1922
- var weight_sum = 0;
1923
- for(var i=0; i<weights.length; i++){
1924
- weight_sum += weights[i];
1925
- }
1926
- for(var i=0; i<weights.length; i++){
1927
- normalized_weights.push( weights[i] / weight_sum );
1928
- }
1929
- } else {
1930
- for(var i=0; i<arr.length; i++){
1931
- normalized_weights.push( 1 / arr.length );
1932
- }
1933
- }
1934
-
1935
- var cumulative_weights = [normalized_weights[0]];
1936
- for(var i=1; i<normalized_weights.length; i++){
1937
- cumulative_weights.push(normalized_weights[i] + cumulative_weights[i-1]);
1938
- }
1939
-
1940
- var samp = [];
1941
- for (var i = 0; i < size; i++) {
1942
- var rnd = Math.random();
1943
- var index = 0;
1944
- while(rnd > cumulative_weights[index]) { index++; }
1945
- samp.push(arr[index]);
1946
- }
1947
- return samp;
1948
- }
1949
-
1950
- module.factorial = function(factors, repetitions, unpack) {
1951
-
1952
- var factorNames = Object.keys(factors);
1953
-
1954
- var factor_combinations = [];
1955
-
1956
- for (var i = 0; i < factors[factorNames[0]].length; i++) {
1957
- factor_combinations.push({});
1958
- factor_combinations[i][factorNames[0]] = factors[factorNames[0]][i];
1959
- }
1960
-
1961
- for (var i = 1; i < factorNames.length; i++) {
1962
- var toAdd = factors[factorNames[i]];
1963
- var n = factor_combinations.length;
1964
- for (var j = 0; j < n; j++) {
1965
- var base = factor_combinations[j];
1966
- for (var k = 0; k < toAdd.length; k++) {
1967
- var newpiece = {};
1968
- newpiece[factorNames[i]] = toAdd[k];
1969
- factor_combinations.push(Object.assign({}, base, newpiece));
1970
- }
1971
- }
1972
- factor_combinations.splice(0, n);
1973
- }
1974
-
1975
- repetitions = (typeof repetitions === 'undefined') ? 1 : repetitions;
1976
- var with_repetitions = module.repeat(factor_combinations, repetitions, unpack);
1977
-
1978
- return with_repetitions;
1979
- }
1980
-
1981
- module.randomID = function(length){
1982
- var result = '';
1983
- var length = (typeof length == 'undefined') ? 32 : length;
1984
- var chars = '0123456789abcdefghjklmnopqrstuvwxyz';
1985
- for(var i = 0; i<length; i++){
1986
- result += chars[Math.floor(Math.random() * chars.length)];
1987
- }
1988
- return result;
1989
- }
1990
-
1991
- function unpackArray(array) {
1992
-
1993
- var out = {};
1994
-
1995
- for (var i = 0; i < array.length; i++) {
1996
- var keys = Object.keys(array[i]);
1997
- for (var k = 0; k < keys.length; k++) {
1998
- if (typeof out[keys[k]] === 'undefined') {
1999
- out[keys[k]] = [];
2000
- }
2001
- out[keys[k]].push(array[i][keys[k]]);
2002
- }
2003
- }
2004
-
2005
- return out;
2006
- }
2007
-
2008
- function shuffle(array) {
2009
- var copy_array = array.slice(0);
2010
- var m = copy_array.length,
2011
- t, i;
2012
-
2013
- // While there remain elements to shuffle…
2014
- while (m) {
2015
-
2016
- // Pick a remaining element…
2017
- i = Math.floor(Math.random() * m--);
2018
-
2019
- // And swap it with the current element.
2020
- t = copy_array[m];
2021
- copy_array[m] = copy_array[i];
2022
- copy_array[i] = t;
2023
- }
2024
-
2025
- return copy_array;
2026
- }
2027
-
2028
- return module;
2029
-
2030
- })();
2031
-
2032
- jsPsych.pluginAPI = (function() {
2033
-
2034
- var module = {};
2035
-
2036
- // keyboard listeners //
2037
-
2038
- var keyboard_listeners = [];
2039
-
2040
- var held_keys = {};
2041
-
2042
- var root_keydown_listener = function(e){
2043
- for(var i=0; i<keyboard_listeners.length; i++){
2044
- keyboard_listeners[i].fn(e);
2045
- }
2046
- held_keys[e.keyCode] = true;
2047
- }
2048
- var root_keyup_listener = function(e){
2049
- held_keys[e.keyCode] = false;
2050
- }
2051
-
2052
- module.reset = function(root_element){
2053
- keyboard_listeners = [];
2054
- held_keys = {};
2055
- root_element.removeEventListener('keydown', root_keydown_listener);
2056
- root_element.removeEventListener('keyup', root_keyup_listener);
2057
- }
2058
-
2059
- module.createKeyboardEventListeners = function(root_element){
2060
- root_element.addEventListener('keydown', root_keydown_listener);
2061
- root_element.addEventListener('keyup', root_keyup_listener);
2062
- }
2063
-
2064
- module.getKeyboardResponse = function(parameters) {
2065
-
2066
- //parameters are: callback_function, valid_responses, rt_method, persist, audio_context, audio_context_start_time, allow_held_key?
2067
-
2068
- parameters.rt_method = (typeof parameters.rt_method === 'undefined') ? 'performance' : parameters.rt_method;
2069
- if (parameters.rt_method != 'performance' && parameters.rt_method != 'audio') {
2070
- console.log('Invalid RT method specified in getKeyboardResponse. Defaulting to "performance" method.');
2071
- parameters.rt_method = 'performance';
2072
- }
2073
-
2074
- var start_time;
2075
- if (parameters.rt_method == 'performance') {
2076
- start_time = performance.now();
2077
- } else if (parameters.rt_method === 'audio') {
2078
- start_time = parameters.audio_context_start_time;
2079
- }
2080
-
2081
- var listener_id;
2082
-
2083
- var listener_function = function(e) {
2084
- var key_time;
2085
- if (parameters.rt_method == 'performance') {
2086
- key_time = performance.now();
2087
- } else if (parameters.rt_method === 'audio') {
2088
- key_time = parameters.audio_context.currentTime
2089
- }
2090
- var rt = key_time - start_time;
2091
-
2092
- // overiding via parameters for testing purposes.
2093
- var minimum_valid_rt = parameters.minimum_valid_rt;
2094
- if(!minimum_valid_rt){
2095
- minimum_valid_rt = jsPsych.initSettings().minimum_valid_rt || 0;
2096
- }
2097
-
2098
- if(rt < minimum_valid_rt){
2099
- return;
2100
- }
2101
-
2102
- var valid_response = false;
2103
- if (typeof parameters.valid_responses === 'undefined' || parameters.valid_responses == jsPsych.ALL_KEYS) {
2104
- valid_response = true;
2105
- } else {
2106
- if(parameters.valid_responses != jsPsych.NO_KEYS){
2107
- for (var i = 0; i < parameters.valid_responses.length; i++) {
2108
- if (typeof parameters.valid_responses[i] == 'string') {
2109
- var kc = jsPsych.pluginAPI.convertKeyCharacterToKeyCode(parameters.valid_responses[i]);
2110
- if (typeof kc !== 'undefined') {
2111
- if (e.keyCode == kc) {
2112
- valid_response = true;
2113
- }
2114
- } else {
2115
- throw new Error('Invalid key string specified for getKeyboardResponse');
2116
- }
2117
- } else if (e.keyCode == parameters.valid_responses[i]) {
2118
- valid_response = true;
2119
- }
2120
- }
2121
- }
2122
- }
2123
- // check if key was already held down
2124
-
2125
- if (((typeof parameters.allow_held_key === 'undefined') || !parameters.allow_held_key) && valid_response) {
2126
- if (typeof held_keys[e.keyCode] !== 'undefined' && held_keys[e.keyCode] == true) {
2127
- valid_response = false;
2128
- }
2129
- }
2130
-
2131
- if (valid_response) {
2132
- // if this is a valid response, then we don't want the key event to trigger other actions
2133
- // like scrolling via the spacebar.
2134
- e.preventDefault();
2135
-
2136
- parameters.callback_function({
2137
- key: e.keyCode,
2138
- rt: rt,
2139
- });
2140
-
2141
- if (keyboard_listeners.includes(listener_id)) {
2142
-
2143
- if (!parameters.persist) {
2144
- // remove keyboard listener
2145
- module.cancelKeyboardResponse(listener_id);
2146
- }
2147
- }
2148
- }
2149
- };
2150
-
2151
- // create listener id object
2152
- listener_id = {
2153
- type: 'keydown',
2154
- fn: listener_function
2155
- };
2156
-
2157
- // add this keyboard listener to the list of listeners
2158
- keyboard_listeners.push(listener_id);
2159
-
2160
- return listener_id;
2161
-
2162
- };
2163
-
2164
- module.cancelKeyboardResponse = function(listener) {
2165
- // remove the listener from the list of listeners
2166
- if (keyboard_listeners.includes(listener)) {
2167
- keyboard_listeners.splice(keyboard_listeners.indexOf(listener), 1);
2168
- }
2169
- };
2170
-
2171
- module.cancelAllKeyboardResponses = function() {
2172
- keyboard_listeners = [];
2173
- };
2174
-
2175
- module.convertKeyCharacterToKeyCode = function(character) {
2176
- var code;
2177
- character = character.toLowerCase();
2178
- if (typeof keylookup[character] !== 'undefined') {
2179
- code = keylookup[character];
2180
- }
2181
- return code;
2182
- }
2183
-
2184
- module.convertKeyCodeToKeyCharacter = function(code){
2185
- for(var i in Object.keys(keylookup)){
2186
- if(keylookup[Object.keys(keylookup)[i]] == code){
2187
- return Object.keys(keylookup)[i];
2188
- }
2189
- }
2190
- return undefined;
2191
- }
2192
-
2193
- module.compareKeys = function(key1, key2){
2194
- // convert to numeric values no matter what
2195
- if(typeof key1 == 'string') {
2196
- key1 = module.convertKeyCharacterToKeyCode(key1);
2197
- }
2198
- if(typeof key2 == 'string') {
2199
- key2 = module.convertKeyCharacterToKeyCode(key2);
2200
- }
2201
- return key1 == key2;
2202
- }
2203
-
2204
- var keylookup = {
2205
- 'backspace': 8,
2206
- 'tab': 9,
2207
- 'enter': 13,
2208
- 'shift': 16,
2209
- 'ctrl': 17,
2210
- 'alt': 18,
2211
- 'pause': 19,
2212
- 'capslock': 20,
2213
- 'esc': 27,
2214
- 'space': 32,
2215
- 'spacebar': 32,
2216
- ' ': 32,
2217
- 'pageup': 33,
2218
- 'pagedown': 34,
2219
- 'end': 35,
2220
- 'home': 36,
2221
- 'leftarrow': 37,
2222
- 'uparrow': 38,
2223
- 'rightarrow': 39,
2224
- 'downarrow': 40,
2225
- 'insert': 45,
2226
- 'delete': 46,
2227
- '0': 48,
2228
- '1': 49,
2229
- '2': 50,
2230
- '3': 51,
2231
- '4': 52,
2232
- '5': 53,
2233
- '6': 54,
2234
- '7': 55,
2235
- '8': 56,
2236
- '9': 57,
2237
- 'a': 65,
2238
- 'b': 66,
2239
- 'c': 67,
2240
- 'd': 68,
2241
- 'e': 69,
2242
- 'f': 70,
2243
- 'g': 71,
2244
- 'h': 72,
2245
- 'i': 73,
2246
- 'j': 74,
2247
- 'k': 75,
2248
- 'l': 76,
2249
- 'm': 77,
2250
- 'n': 78,
2251
- 'o': 79,
2252
- 'p': 80,
2253
- 'q': 81,
2254
- 'r': 82,
2255
- 's': 83,
2256
- 't': 84,
2257
- 'u': 85,
2258
- 'v': 86,
2259
- 'w': 87,
2260
- 'x': 88,
2261
- 'y': 89,
2262
- 'z': 90,
2263
- '0numpad': 96,
2264
- '1numpad': 97,
2265
- '2numpad': 98,
2266
- '3numpad': 99,
2267
- '4numpad': 100,
2268
- '5numpad': 101,
2269
- '6numpad': 102,
2270
- '7numpad': 103,
2271
- '8numpad': 104,
2272
- '9numpad': 105,
2273
- 'multiply': 106,
2274
- 'plus': 107,
2275
- 'minus': 109,
2276
- 'decimal': 110,
2277
- 'divide': 111,
2278
- 'f1': 112,
2279
- 'f2': 113,
2280
- 'f3': 114,
2281
- 'f4': 115,
2282
- 'f5': 116,
2283
- 'f6': 117,
2284
- 'f7': 118,
2285
- 'f8': 119,
2286
- 'f9': 120,
2287
- 'f10': 121,
2288
- 'f11': 122,
2289
- 'f12': 123,
2290
- '=': 187,
2291
- ',': 188,
2292
- '.': 190,
2293
- '/': 191,
2294
- '`': 192,
2295
- '[': 219,
2296
- '\\': 220,
2297
- ']': 221
2298
- };
2299
-
2300
- // timeout registration
2301
-
2302
- var timeout_handlers = [];
2303
-
2304
- module.setTimeout = function(callback, delay){
2305
- var handle = setTimeout(callback, delay);
2306
- timeout_handlers.push(handle);
2307
- return handle;
2308
- }
2309
-
2310
- module.clearAllTimeouts = function(){
2311
- for(var i=0;i<timeout_handlers.length; i++){
2312
- clearTimeout(timeout_handlers[i]);
2313
- }
2314
- timeout_handlers = [];
2315
- }
2316
-
2317
- // video //
2318
- var video_buffers = {}
2319
- module.getVideoBuffer = function(videoID) {
2320
- return video_buffers[videoID]
2321
- }
2322
-
2323
- // audio //
2324
- var context = null;
2325
- var audio_buffers = [];
2326
-
2327
- module.initAudio = function(){
2328
- context = (jsPsych.initSettings().use_webaudio === true) ? jsPsych.webaudio_context : null;
2329
- }
2330
-
2331
- module.audioContext = function(){
2332
- if(context !== null){
2333
- if(context.state !== 'running'){
2334
- context.resume();
2335
- }
2336
- }
2337
- return context;
2338
- }
2339
-
2340
- module.getAudioBuffer = function(audioID) {
2341
-
2342
- if (audio_buffers[audioID] === 'tmp') {
2343
- console.error('Audio file failed to load in the time allotted.')
2344
- return;
2345
- }
2346
-
2347
- return audio_buffers[audioID];
2348
-
2349
- }
2350
-
2351
- // preloading stimuli //
2352
-
2353
- var preloads = [];
2354
-
2355
- var img_cache = {};
2356
-
2357
- module.preloadAudioFiles = function(files, callback_complete, callback_load) {
2358
-
2359
- files = jsPsych.utils.flatten(files);
2360
- files = jsPsych.utils.unique(files);
2361
-
2362
- var n_loaded = 0;
2363
- var loadfn = (typeof callback_load === 'undefined') ? function() {} : callback_load;
2364
- var finishfn = (typeof callback_complete === 'undefined') ? function() {} : callback_complete;
2365
-
2366
- if(files.length==0){
2367
- finishfn();
2368
- return;
2369
- }
2370
-
2371
- function load_audio_file_webaudio(source, count){
2372
- count = count || 1;
2373
- var request = new XMLHttpRequest();
2374
- request.open('GET', source, true);
2375
- request.responseType = 'arraybuffer';
2376
- request.onload = function() {
2377
- context.decodeAudioData(request.response, function(buffer) {
2378
- audio_buffers[source] = buffer;
2379
- n_loaded++;
2380
- loadfn(n_loaded);
2381
- if(n_loaded == files.length) {
2382
- finishfn();
2383
- }
2384
- }, function() {
2385
- console.error('Error loading audio file: ' + bufferID);
2386
- });
2387
- }
2388
- request.onerror = function(){
2389
- if(count < jsPsych.initSettings().max_preload_attempts){
2390
- setTimeout(function(){
2391
- load_audio_file_webaudio(source, count+1)
2392
- }, 200);
2393
- } else {
2394
- jsPsych.loadFail();
2395
- }
2396
- }
2397
- request.send();
2398
- }
2399
-
2400
- function load_audio_file_html5audio(source, count){
2401
- count = count || 1;
2402
- var audio = new Audio();
2403
- audio.addEventListener('canplaythrough', function handleCanPlayThrough(){
2404
- audio_buffers[source] = audio;
2405
- n_loaded++;
2406
- loadfn(n_loaded);
2407
- if(n_loaded == files.length){
2408
- finishfn();
2409
- }
2410
- audio.removeEventListener('canplaythrough', handleCanPlayThrough);
2411
- });
2412
- audio.addEventListener('error', function handleError(){
2413
- if(count < jsPsych.initSettings().max_preload_attempts){
2414
- setTimeout(function(){
2415
- load_audio_file_html5audio(source, count+1)
2416
- }, 200);
2417
- } else {
2418
- jsPsych.loadFail();
2419
- }
2420
- audio.removeEventListener('error', handleError);
2421
- });
2422
- audio.addEventListener('abort', function handleAbort(){
2423
- if(count < jsPsych.initSettings().max_preload_attempts){
2424
- setTimeout(function(){
2425
- load_audio_file_html5audio(source, count+1)
2426
- }, 200);
2427
- } else {
2428
- jsPsych.loadFail();
2429
- }
2430
- audio.removeEventListener('abort', handleAbort);
2431
- });
2432
- audio.src = source;
2433
- }
2434
-
2435
- for (var i = 0; i < files.length; i++) {
2436
- var bufferID = files[i];
2437
- if (typeof audio_buffers[bufferID] !== 'undefined') {
2438
- n_loaded++;
2439
- loadfn(n_loaded);
2440
- if(n_loaded == files.length) {
2441
- finishfn();
2442
- }
2443
- } else {
2444
- audio_buffers[bufferID] = 'tmp';
2445
- if(module.audioContext() !== null){
2446
- load_audio_file_webaudio(bufferID);
2447
- } else {
2448
- load_audio_file_html5audio(bufferID);
2449
- }
2450
- }
2451
- }
2452
-
2453
- }
2454
-
2455
- module.preloadImages = function(images, callback_complete, callback_load) {
2456
-
2457
- // flatten the images array
2458
- images = jsPsych.utils.flatten(images);
2459
- images = jsPsych.utils.unique(images);
2460
-
2461
- var n_loaded = 0;
2462
- var loadfn = (typeof callback_load === 'undefined') ? function() {} : callback_load;
2463
- var finishfn = (typeof callback_complete === 'undefined') ? function() {} : callback_complete;
2464
-
2465
- if(images.length === 0){
2466
- finishfn();
2467
- return;
2468
- }
2469
-
2470
- function preload_image(source, count){
2471
- count = count || 1;
2472
-
2473
- var img = new Image();
2474
-
2475
- img.onload = function() {
2476
- n_loaded++;
2477
- loadfn(n_loaded);
2478
- if (n_loaded === images.length) {
2479
- finishfn();
2480
- }
2481
- };
2482
-
2483
- img.onerror = function() {
2484
- if(count < jsPsych.initSettings().max_preload_attempts){
2485
- setTimeout(function(){
2486
- preload_image(source, count+1);
2487
- }, 200);
2488
- } else {
2489
- jsPsych.loadFail();
2490
- }
2491
- }
2492
-
2493
- img.src = source;
2494
-
2495
- img_cache[source] = img;
2496
- }
2497
-
2498
- for (var i = 0; i < images.length; i++) {
2499
- preload_image(images[i]);
2500
- }
2501
-
2502
- };
2503
-
2504
- module.preloadVideo = function(video, callback_complete, callback_load) {
2505
-
2506
- // flatten the images array
2507
- video = jsPsych.utils.flatten(video);
2508
- video = jsPsych.utils.unique(video);
2509
-
2510
- var n_loaded = 0;
2511
- var loadfn = !callback_load ? function() {} : callback_load;
2512
- var finishfn = !callback_complete ? function() {} : callback_complete;
2513
-
2514
- if(video.length===0){
2515
- finishfn();
2516
- return;
2517
- }
2518
-
2519
- function preload_video(source, count){
2520
- count = count || 1;
2521
- //based on option 4 here: http://dinbror.dk/blog/how-to-preload-entire-html5-video-before-play-solved/
2522
- var request = new XMLHttpRequest();
2523
- request.open('GET', source, true);
2524
- request.responseType = 'blob';
2525
- request.onload = function() {
2526
- if (this.status === 200 || this.status === 0) {
2527
- var videoBlob = this.response;
2528
- video_buffers[source] = URL.createObjectURL(videoBlob); // IE10+
2529
- n_loaded++;
2530
- loadfn(n_loaded);
2531
- if (n_loaded === video.length) {
2532
- finishfn();
2533
- }
2534
- }
2535
- };
2536
-
2537
- request.onerror = function(){
2538
- if(count < jsPsych.initSettings().max_preload_attempts){
2539
- setTimeout(function(){
2540
- preload_video(source, count+1)
2541
- }, 200);
2542
- } else {
2543
- jsPsych.loadFail();
2544
- }
2545
- }
2546
- request.send();
2547
- }
2548
-
2549
- for (var i = 0; i < video.length; i++) {
2550
- preload_video(video[i]);
2551
- }
2552
-
2553
- };
2554
-
2555
- module.registerPreload = function(plugin_name, parameter, media_type, conditional_function) {
2556
- if (['audio', 'image', 'video'].indexOf(media_type)===-1) {
2557
- console.error('Invalid media_type parameter for jsPsych.pluginAPI.registerPreload. Please check the plugin file.');
2558
- }
2559
-
2560
- var preload = {
2561
- plugin: plugin_name,
2562
- parameter: parameter,
2563
- media_type: media_type,
2564
- conditional_function: conditional_function
2565
- }
2566
-
2567
- preloads.push(preload);
2568
- }
2569
-
2570
- module.autoPreload = function(timeline, callback, file_protocol, images, audio, video, progress_bar) {
2571
- // list of items to preload
2572
- images = images || [];
2573
- audio = audio || [];
2574
- video = video || [];
2575
-
2576
- // construct list
2577
- for (var i = 0; i < preloads.length; i++) {
2578
- var type = preloads[i].plugin;
2579
- var param = preloads[i].parameter;
2580
- var media = preloads[i].media_type;
2581
- var func = preloads[i].conditional_function;
2582
- var trials = timeline.trialsOfType(type);
2583
- for (var j = 0; j < trials.length; j++) {
2584
-
2585
- if (typeof trials[j][param] == 'undefined') {
2586
- console.warn("jsPsych failed to auto preload one or more files:");
2587
- console.warn("no parameter called "+param+" in plugin "+type);
2588
- } else if (typeof trials[j][param] !== 'function') {
2589
- if ( !func || func(trials[j]) ){
2590
- if (media === 'image') {
2591
- images = images.concat(jsPsych.utils.flatten([trials[j][param]]));
2592
- } else if (media === 'audio') {
2593
- audio = audio.concat(jsPsych.utils.flatten([trials[j][param]]));
2594
- } else if (media === 'video') {
2595
- video = video.concat(jsPsych.utils.flatten([trials[j][param]]));
2596
- }
2597
- }
2598
- }
2599
- }
2600
- }
2601
-
2602
- images = jsPsych.utils.unique(jsPsych.utils.flatten(images));
2603
- audio = jsPsych.utils.unique(jsPsych.utils.flatten(audio));
2604
- video = jsPsych.utils.unique(jsPsych.utils.flatten(video));
2605
-
2606
- // remove any nulls false values
2607
- images = images.filter(function(x) { return x != false && x != null})
2608
- audio = audio.filter(function(x) { return x != false && x != null})
2609
- // prevent all video preloading (auto and manual) when file is opened directly in browser
2610
- if (file_protocol === true) {
2611
- video = [];
2612
- } else {
2613
- video = video.filter(function(x) { return x != false && x != null})
2614
- }
2615
-
2616
- var total_n = images.length + audio.length + video.length;
2617
-
2618
- var loaded = 0;
2619
-
2620
- if(progress_bar){
2621
- var pb_html = "<div id='jspsych-loading-progress-bar-container' style='height: 10px; width: 300px; background-color: #ddd; margin: auto;'>";
2622
- pb_html += "<div id='jspsych-loading-progress-bar' style='height: 10px; width: 0%; background-color: #777;'></div>";
2623
- pb_html += "</div>";
2624
- jsPsych.getDisplayElement().innerHTML = pb_html;
2625
- }
2626
-
2627
- function update_loading_progress_bar(){
2628
- loaded++;
2629
- if(progress_bar){
2630
- var percent_loaded = (loaded/total_n)*100;
2631
- var preload_progress_bar = jsPsych.getDisplayElement().querySelector('#jspsych-loading-progress-bar');
2632
- if (preload_progress_bar !== null) {
2633
- preload_progress_bar.style.width = percent_loaded+"%";
2634
- }
2635
- }
2636
- }
2637
-
2638
- // do the preloading
2639
- // first the images, then when the images are complete
2640
- // wait for the audio files to finish
2641
- module.preloadImages(images, function() {
2642
- module.preloadAudioFiles(audio, function() {
2643
- module.preloadVideo(video, function() {
2644
- callback();
2645
- }, update_loading_progress_bar);
2646
- }, update_loading_progress_bar);
2647
- }, update_loading_progress_bar);
2648
- }
2649
-
2650
- /**
2651
- * Allows communication with user hardware through our custom Google Chrome extension + native C++ program
2652
- * @param {object} mess The message to be passed to our extension, see its documentation for the expected members of this object.
2653
- * @author Daniel Rivas
2654
- *
2655
- */
2656
- module.hardware = function hardware(mess){
2657
- //since Chrome extension content-scripts do not share the javascript environment with the page script that loaded jspsych,
2658
- //we will need to use hacky methods like communicating through DOM events.
2659
- var jspsychEvt = new CustomEvent('jspsych', {detail: mess});
2660
- document.dispatchEvent(jspsychEvt);
2661
- //And voila! it will be the job of the content script injected by the extension to listen for the event and do the appropriate actions.
2662
- };
2663
-
2664
- /** {boolean} Indicates whether this instance of jspsych has opened a hardware connection through our browser extension */
2665
- module.hardwareConnected = false;
2666
-
2667
-
2668
- //it might be useful to open up a line of communication from the extension back to this page script,
2669
- //again, this will have to pass through DOM events. For now speed is of no concern so I will use jQuery
2670
- document.addEventListener("jspsych-activate", function(evt){
2671
- module.hardwareConnected = true;
2672
- })
2673
-
2674
-
2675
-
2676
- return module;
2677
- })();
2678
-
2679
- // methods used in multiple modules //
2680
- jsPsych.utils = (function() {
2681
-
2682
- var module = {};
2683
-
2684
- module.flatten = function(arr, out) {
2685
- out = (typeof out === 'undefined') ? [] : out;
2686
- for (var i = 0; i < arr.length; i++) {
2687
- if (Array.isArray(arr[i])) {
2688
- module.flatten(arr[i], out);
2689
- } else {
2690
- out.push(arr[i]);
2691
- }
2692
- }
2693
- return out;
2694
- }
2695
-
2696
- module.unique = function(arr) {
2697
- var out = [];
2698
- for (var i = 0; i < arr.length; i++) {
2699
- if (arr.indexOf(arr[i]) == i) {
2700
- out.push(arr[i]);
2701
- }
2702
- }
2703
- return out;
2704
- }
2705
-
2706
- module.deepCopy = function(obj) {
2707
- if(!obj) return obj;
2708
- var out;
2709
- if(Array.isArray(obj)){
2710
- out = [];
2711
- for(var i = 0; i<obj.length; i++){
2712
- out.push(module.deepCopy(obj[i]));
2713
- }
2714
- return out;
2715
- } else if(typeof obj === 'object'){
2716
- out = {};
2717
- for(var key in obj){
2718
- if(obj.hasOwnProperty(key)){
2719
- out[key] = module.deepCopy(obj[key]);
2720
- }
2721
- }
2722
- return out;
2723
- } else {
2724
- return obj;
2725
- }
2726
- }
2727
-
2728
- return module;
2729
- })();
2730
-
2731
- // polyfill for Object.assign to support IE
2732
- if (typeof Object.assign != 'function') {
2733
- Object.assign = function (target, varArgs) { // .length of function is 2
2734
- 'use strict';
2735
- if (target == null) { // TypeError if undefined or null
2736
- throw new TypeError('Cannot convert undefined or null to object');
2737
- }
2738
-
2739
- var to = Object(target);
2740
-
2741
- for (var index = 1; index < arguments.length; index++) {
2742
- var nextSource = arguments[index];
2743
-
2744
- if (nextSource != null) { // Skip over if undefined or null
2745
- for (var nextKey in nextSource) {
2746
- // Avoid bugs when hasOwnProperty is shadowed
2747
- if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
2748
- to[nextKey] = nextSource[nextKey];
2749
- }
2750
- }
2751
- }
2752
- }
2753
- return to;
2754
- };
2755
- }
2756
-
2757
- // polyfill for Array.includes to support IE
2758
- if (!Array.prototype.includes) {
2759
- Array.prototype.includes = function(searchElement /*, fromIndex*/) {
2760
- 'use strict';
2761
- if (this == null) {
2762
- throw new TypeError('Array.prototype.includes called on null or undefined');
2763
- }
2764
-
2765
- var O = Object(this);
2766
- var len = parseInt(O.length, 10) || 0;
2767
- if (len === 0) {
2768
- return false;
2769
- }
2770
- var n = parseInt(arguments[1], 10) || 0;
2771
- var k;
2772
- if (n >= 0) {
2773
- k = n;
2774
- } else {
2775
- k = len + n;
2776
- if (k < 0) {k = 0;}
2777
- }
2778
- var currentElement;
2779
- while (k < len) {
2780
- currentElement = O[k];
2781
- if (searchElement === currentElement ||
2782
- (searchElement !== searchElement && currentElement !== currentElement)) { // NaN !== NaN
2783
- return true;
2784
- }
2785
- k++;
2786
- }
2787
- return false;
2788
- };
2789
- }
2790
-
2791
- // polyfill for Array.isArray
2792
- if (!Array.isArray) {
2793
- Array.isArray = function(arg) {
2794
- return Object.prototype.toString.call(arg) === '[object Array]';
2795
- };
2796
- }