jspsych 7.3.3 → 8.0.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 (58) hide show
  1. package/README.md +1 -1
  2. package/css/jspsych.css +19 -11
  3. package/dist/index.browser.js +3082 -3399
  4. package/dist/index.browser.js.map +1 -1
  5. package/dist/index.browser.min.js +6 -2
  6. package/dist/index.browser.min.js.map +1 -1
  7. package/dist/index.cjs +2464 -3327
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +990 -12
  10. package/dist/index.js +2463 -3325
  11. package/dist/index.js.map +1 -1
  12. package/package.json +6 -5
  13. package/src/ExtensionManager.spec.ts +123 -0
  14. package/src/ExtensionManager.ts +81 -0
  15. package/src/JsPsych.ts +195 -690
  16. package/src/ProgressBar.spec.ts +60 -0
  17. package/src/ProgressBar.ts +60 -0
  18. package/src/index.scss +29 -8
  19. package/src/index.ts +4 -9
  20. package/src/modules/data/DataCollection.ts +1 -1
  21. package/src/modules/data/DataColumn.ts +12 -1
  22. package/src/modules/data/index.ts +92 -103
  23. package/src/modules/extensions.ts +4 -0
  24. package/src/modules/plugin-api/AudioPlayer.ts +101 -0
  25. package/src/modules/plugin-api/KeyboardListenerAPI.ts +1 -1
  26. package/src/modules/plugin-api/MediaAPI.ts +48 -106
  27. package/src/modules/plugin-api/__mocks__/AudioPlayer.ts +38 -0
  28. package/src/modules/plugin-api/index.ts +11 -14
  29. package/src/modules/plugins.ts +26 -27
  30. package/src/modules/randomization.ts +1 -1
  31. package/src/timeline/Timeline.spec.ts +921 -0
  32. package/src/timeline/Timeline.ts +342 -0
  33. package/src/timeline/TimelineNode.ts +174 -0
  34. package/src/timeline/Trial.spec.ts +897 -0
  35. package/src/timeline/Trial.ts +419 -0
  36. package/src/timeline/index.ts +232 -0
  37. package/src/timeline/util.spec.ts +124 -0
  38. package/src/timeline/util.ts +146 -0
  39. package/dist/JsPsych.d.ts +0 -112
  40. package/dist/TimelineNode.d.ts +0 -34
  41. package/dist/migration.d.ts +0 -3
  42. package/dist/modules/data/DataCollection.d.ts +0 -46
  43. package/dist/modules/data/DataColumn.d.ts +0 -15
  44. package/dist/modules/data/index.d.ts +0 -25
  45. package/dist/modules/data/utils.d.ts +0 -3
  46. package/dist/modules/extensions.d.ts +0 -22
  47. package/dist/modules/plugin-api/HardwareAPI.d.ts +0 -15
  48. package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +0 -34
  49. package/dist/modules/plugin-api/MediaAPI.d.ts +0 -32
  50. package/dist/modules/plugin-api/SimulationAPI.d.ts +0 -44
  51. package/dist/modules/plugin-api/TimeoutAPI.d.ts +0 -17
  52. package/dist/modules/plugin-api/index.d.ts +0 -8
  53. package/dist/modules/plugins.d.ts +0 -136
  54. package/dist/modules/randomization.d.ts +0 -42
  55. package/dist/modules/turk.d.ts +0 -40
  56. package/dist/modules/utils.d.ts +0 -13
  57. package/src/TimelineNode.ts +0 -544
  58. package/src/modules/plugin-api/HardwareAPI.ts +0 -32
@@ -1,544 +0,0 @@
1
- import { JsPsych } from "./JsPsych";
2
- import {
3
- repeat,
4
- sampleWithReplacement,
5
- sampleWithoutReplacement,
6
- shuffle,
7
- shuffleAlternateGroups,
8
- } from "./modules/randomization";
9
- import { deepCopy } from "./modules/utils";
10
-
11
- export class TimelineNode {
12
- // a unique ID for this node, relative to the parent
13
- relative_id;
14
-
15
- // store the parent for this node
16
- parent_node;
17
-
18
- // parameters for the trial if the node contains a trial
19
- trial_parameters;
20
-
21
- // parameters for nodes that contain timelines
22
- timeline_parameters;
23
-
24
- // stores trial information on a node that contains a timeline
25
- // used for adding new trials
26
- node_trial_data;
27
-
28
- // track progress through the node
29
- progress = <any>{
30
- current_location: -1, // where on the timeline (which timelinenode)
31
- current_variable_set: 0, // which set of variables to use from timeline_variables
32
- current_repetition: 0, // how many times through the variable set on this run of the node
33
- current_iteration: 0, // how many times this node has been revisited
34
- done: false,
35
- };
36
-
37
- end_message?: string;
38
-
39
- // constructor
40
- constructor(private jsPsych: JsPsych, parameters, parent?, relativeID?) {
41
- // store a link to the parent of this node
42
- this.parent_node = parent;
43
-
44
- // create the ID for this node
45
- this.relative_id = typeof parent === "undefined" ? 0 : relativeID;
46
-
47
- // check if there is a timeline parameter
48
- // if there is, then this node has its own timeline
49
- if (typeof parameters.timeline !== "undefined") {
50
- // create timeline properties
51
- this.timeline_parameters = {
52
- timeline: [],
53
- loop_function: parameters.loop_function,
54
- conditional_function: parameters.conditional_function,
55
- sample: parameters.sample,
56
- randomize_order:
57
- typeof parameters.randomize_order == "undefined" ? false : parameters.randomize_order,
58
- repetitions: typeof parameters.repetitions == "undefined" ? 1 : parameters.repetitions,
59
- timeline_variables:
60
- typeof parameters.timeline_variables == "undefined"
61
- ? [{}]
62
- : parameters.timeline_variables,
63
- on_timeline_finish: parameters.on_timeline_finish,
64
- on_timeline_start: parameters.on_timeline_start,
65
- };
66
-
67
- this.setTimelineVariablesOrder();
68
-
69
- // extract all of the node level data and parameters
70
- // but remove all of the timeline-level specific information
71
- // since this will be used to copy things down hierarchically
72
- var node_data = Object.assign({}, parameters);
73
- delete node_data.timeline;
74
- delete node_data.conditional_function;
75
- delete node_data.loop_function;
76
- delete node_data.randomize_order;
77
- delete node_data.repetitions;
78
- delete node_data.timeline_variables;
79
- delete node_data.sample;
80
- delete node_data.on_timeline_start;
81
- delete node_data.on_timeline_finish;
82
- this.node_trial_data = node_data; // store for later...
83
-
84
- // create a TimelineNode for each element in the timeline
85
- for (var i = 0; i < parameters.timeline.length; i++) {
86
- // merge parameters
87
- var merged_parameters = Object.assign({}, node_data, parameters.timeline[i]);
88
- // merge any data from the parent node into child nodes
89
- if (typeof node_data.data == "object" && typeof parameters.timeline[i].data == "object") {
90
- var merged_data = Object.assign({}, node_data.data, parameters.timeline[i].data);
91
- merged_parameters.data = merged_data;
92
- }
93
- this.timeline_parameters.timeline.push(
94
- new TimelineNode(this.jsPsych, merged_parameters, this, i)
95
- );
96
- }
97
- }
98
- // if there is no timeline parameter, then this node is a trial node
99
- else {
100
- // check to see if a valid trial type is defined
101
- if (typeof parameters.type === "undefined") {
102
- console.error(
103
- 'Trial level node is missing the "type" parameter. The parameters for the node are: ' +
104
- JSON.stringify(parameters)
105
- );
106
- }
107
- // create a deep copy of the parameters for the trial
108
- this.trial_parameters = { ...parameters };
109
- }
110
- }
111
-
112
- // recursively get the next trial to run.
113
- // if this node is a leaf (trial), then return the trial.
114
- // otherwise, recursively find the next trial in the child timeline.
115
- trial() {
116
- if (typeof this.timeline_parameters == "undefined") {
117
- // returns a clone of the trial_parameters to
118
- // protect functions.
119
- return deepCopy(this.trial_parameters);
120
- } else {
121
- if (this.progress.current_location >= this.timeline_parameters.timeline.length) {
122
- return null;
123
- } else {
124
- return this.timeline_parameters.timeline[this.progress.current_location].trial();
125
- }
126
- }
127
- }
128
-
129
- markCurrentTrialComplete() {
130
- if (typeof this.timeline_parameters === "undefined") {
131
- this.progress.done = true;
132
- } else {
133
- this.timeline_parameters.timeline[this.progress.current_location].markCurrentTrialComplete();
134
- }
135
- }
136
-
137
- nextRepetiton() {
138
- this.setTimelineVariablesOrder();
139
- this.progress.current_location = -1;
140
- this.progress.current_variable_set = 0;
141
- this.progress.current_repetition++;
142
- for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
143
- this.timeline_parameters.timeline[i].reset();
144
- }
145
- }
146
-
147
- // set the order for going through the timeline variables array
148
- setTimelineVariablesOrder() {
149
- const timeline_parameters = this.timeline_parameters;
150
-
151
- // check to make sure this node has variables
152
- if (
153
- typeof timeline_parameters === "undefined" ||
154
- typeof timeline_parameters.timeline_variables === "undefined"
155
- ) {
156
- return;
157
- }
158
-
159
- var order = [];
160
- for (var i = 0; i < timeline_parameters.timeline_variables.length; i++) {
161
- order.push(i);
162
- }
163
-
164
- if (typeof timeline_parameters.sample !== "undefined") {
165
- if (timeline_parameters.sample.type == "custom") {
166
- order = timeline_parameters.sample.fn(order);
167
- } else if (timeline_parameters.sample.type == "with-replacement") {
168
- order = sampleWithReplacement(
169
- order,
170
- timeline_parameters.sample.size,
171
- timeline_parameters.sample.weights
172
- );
173
- } else if (timeline_parameters.sample.type == "without-replacement") {
174
- order = sampleWithoutReplacement(order, timeline_parameters.sample.size);
175
- } else if (timeline_parameters.sample.type == "fixed-repetitions") {
176
- order = repeat(order, timeline_parameters.sample.size, false);
177
- } else if (timeline_parameters.sample.type == "alternate-groups") {
178
- order = shuffleAlternateGroups(
179
- timeline_parameters.sample.groups,
180
- timeline_parameters.sample.randomize_group_order
181
- );
182
- } else {
183
- console.error(
184
- 'Invalid type in timeline sample parameters. Valid options for type are "custom", "with-replacement", "without-replacement", "fixed-repetitions", and "alternate-groups"'
185
- );
186
- }
187
- }
188
-
189
- if (timeline_parameters.randomize_order) {
190
- order = shuffle(order);
191
- }
192
-
193
- this.progress.order = order;
194
- }
195
-
196
- // next variable set
197
- nextSet() {
198
- this.progress.current_location = -1;
199
- this.progress.current_variable_set++;
200
- for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
201
- this.timeline_parameters.timeline[i].reset();
202
- }
203
- }
204
-
205
- // update the current trial node to be completed
206
- // returns true if the node is complete after advance (all subnodes are also complete)
207
- // returns false otherwise
208
- advance() {
209
- const progress = this.progress;
210
- const timeline_parameters = this.timeline_parameters;
211
- const internal = this.jsPsych.internal;
212
-
213
- // first check to see if done
214
- if (progress.done) {
215
- return true;
216
- }
217
-
218
- // if node has not started yet (progress.current_location == -1),
219
- // then try to start the node.
220
- if (progress.current_location == -1) {
221
- // check for on_timeline_start and conditonal function on nodes with timelines
222
- if (typeof timeline_parameters !== "undefined") {
223
- // only run the conditional function if this is the first repetition of the timeline when
224
- // repetitions > 1, and only when on the first variable set
225
- if (
226
- typeof timeline_parameters.conditional_function !== "undefined" &&
227
- progress.current_repetition == 0 &&
228
- progress.current_variable_set == 0
229
- ) {
230
- internal.call_immediate = true;
231
- var conditional_result = timeline_parameters.conditional_function();
232
- internal.call_immediate = false;
233
- // if the conditional_function() returns false, then the timeline
234
- // doesn't run and is marked as complete.
235
- if (conditional_result == false) {
236
- progress.done = true;
237
- return true;
238
- }
239
- }
240
-
241
- // if we reach this point then the node has its own timeline and will start
242
- // so we need to check if there is an on_timeline_start function if we are on the first variable set
243
- if (
244
- typeof timeline_parameters.on_timeline_start !== "undefined" &&
245
- progress.current_variable_set == 0
246
- ) {
247
- timeline_parameters.on_timeline_start();
248
- }
249
- }
250
- // if we reach this point, then either the node doesn't have a timeline of the
251
- // conditional function returned true and it can start
252
- progress.current_location = 0;
253
- // call advance again on this node now that it is pointing to a new location
254
- return this.advance();
255
- }
256
-
257
- // if this node has a timeline, propogate down to the current trial.
258
- if (typeof timeline_parameters !== "undefined") {
259
- var have_node_to_run = false;
260
- // keep incrementing the location in the timeline until one of the nodes reached is incomplete
261
- while (
262
- progress.current_location < timeline_parameters.timeline.length &&
263
- have_node_to_run == false
264
- ) {
265
- // check to see if the node currently pointed at is done
266
- var target_complete = timeline_parameters.timeline[progress.current_location].advance();
267
- if (!target_complete) {
268
- have_node_to_run = true;
269
- return false;
270
- } else {
271
- progress.current_location++;
272
- }
273
- }
274
-
275
- // if we've reached the end of the timeline (which, if the code is here, we have)
276
-
277
- // there are a few steps to see what to do next...
278
-
279
- // first, check the timeline_variables to see if we need to loop through again
280
- // with a new set of variables
281
- if (progress.current_variable_set < progress.order.length - 1) {
282
- // reset the progress of the node to be with the new set
283
- this.nextSet();
284
- // then try to advance this node again.
285
- return this.advance();
286
- }
287
-
288
- // if we're all done with the timeline_variables, then check to see if there are more repetitions
289
- else if (progress.current_repetition < timeline_parameters.repetitions - 1) {
290
- this.nextRepetiton();
291
- // check to see if there is an on_timeline_finish function
292
- if (typeof timeline_parameters.on_timeline_finish !== "undefined") {
293
- timeline_parameters.on_timeline_finish();
294
- }
295
- return this.advance();
296
- }
297
-
298
- // if we're all done with the repetitions...
299
- else {
300
- // check to see if there is an on_timeline_finish function
301
- if (typeof timeline_parameters.on_timeline_finish !== "undefined") {
302
- timeline_parameters.on_timeline_finish();
303
- }
304
-
305
- // if we're all done with the repetitions, check if there is a loop function.
306
- if (typeof timeline_parameters.loop_function !== "undefined") {
307
- internal.call_immediate = true;
308
- if (timeline_parameters.loop_function(this.generatedData())) {
309
- this.reset();
310
- internal.call_immediate = false;
311
- return this.parent_node.advance();
312
- } else {
313
- progress.done = true;
314
- internal.call_immediate = false;
315
- return true;
316
- }
317
- }
318
- }
319
-
320
- // no more loops on this timeline, we're done!
321
- progress.done = true;
322
- return true;
323
- }
324
- }
325
-
326
- // check the status of the done flag
327
- isComplete() {
328
- return this.progress.done;
329
- }
330
-
331
- // getter method for timeline variables
332
- getTimelineVariableValue(variable_name: string) {
333
- if (typeof this.timeline_parameters == "undefined") {
334
- return undefined;
335
- }
336
- var v =
337
- this.timeline_parameters.timeline_variables[
338
- this.progress.order[this.progress.current_variable_set]
339
- ][variable_name];
340
- return v;
341
- }
342
-
343
- // recursive upward search for timeline variables
344
- findTimelineVariable(variable_name) {
345
- var v = this.getTimelineVariableValue(variable_name);
346
- if (typeof v == "undefined") {
347
- if (typeof this.parent_node !== "undefined") {
348
- return this.parent_node.findTimelineVariable(variable_name);
349
- } else {
350
- return undefined;
351
- }
352
- } else {
353
- return v;
354
- }
355
- }
356
-
357
- // recursive downward search for active trial to extract timeline variable
358
- timelineVariable(variable_name: string) {
359
- if (typeof this.timeline_parameters == "undefined") {
360
- const val = this.findTimelineVariable(variable_name);
361
- if (typeof val === "undefined") {
362
- console.warn("Timeline variable " + variable_name + " not found.");
363
- }
364
- return val;
365
- } else {
366
- // if progress.current_location is -1, then the timeline variable is being evaluated
367
- // in a function that runs prior to the trial starting, so we should treat that trial
368
- // as being the active trial for purposes of finding the value of the timeline variable
369
- var loc = Math.max(0, this.progress.current_location);
370
- // if loc is greater than the number of elements on this timeline, then the timeline
371
- // variable is being evaluated in a function that runs after the trial on the timeline
372
- // are complete but before advancing to the next (like a loop_function).
373
- // treat the last active trial as the active trial for this purpose.
374
- if (loc == this.timeline_parameters.timeline.length) {
375
- loc = loc - 1;
376
- }
377
- // now find the variable
378
- const val = this.timeline_parameters.timeline[loc].timelineVariable(variable_name);
379
- if (typeof val === "undefined") {
380
- console.warn("Timeline variable " + variable_name + " not found.");
381
- }
382
- return val;
383
- }
384
- }
385
-
386
- // recursively get all the timeline variables for this trial
387
- allTimelineVariables() {
388
- var all_tvs = this.allTimelineVariablesNames();
389
- var all_tvs_vals = <any>{};
390
- for (var i = 0; i < all_tvs.length; i++) {
391
- all_tvs_vals[all_tvs[i]] = this.timelineVariable(all_tvs[i]);
392
- }
393
- return all_tvs_vals;
394
- }
395
-
396
- // helper to get all the names at this stage.
397
- allTimelineVariablesNames(so_far = []) {
398
- if (typeof this.timeline_parameters !== "undefined") {
399
- so_far = so_far.concat(
400
- Object.keys(
401
- this.timeline_parameters.timeline_variables[
402
- this.progress.order[this.progress.current_variable_set]
403
- ]
404
- )
405
- );
406
- // if progress.current_location is -1, then the timeline variable is being evaluated
407
- // in a function that runs prior to the trial starting, so we should treat that trial
408
- // as being the active trial for purposes of finding the value of the timeline variable
409
- var loc = Math.max(0, this.progress.current_location);
410
- // if loc is greater than the number of elements on this timeline, then the timeline
411
- // variable is being evaluated in a function that runs after the trial on the timeline
412
- // are complete but before advancing to the next (like a loop_function).
413
- // treat the last active trial as the active trial for this purpose.
414
- if (loc == this.timeline_parameters.timeline.length) {
415
- loc = loc - 1;
416
- }
417
- // now find the variable
418
- return this.timeline_parameters.timeline[loc].allTimelineVariablesNames(so_far);
419
- }
420
- if (typeof this.timeline_parameters == "undefined") {
421
- return so_far;
422
- }
423
- }
424
-
425
- // recursively get the number of **trials** contained in the timeline
426
- // assuming that while loops execute exactly once and if conditionals
427
- // always run
428
- length() {
429
- var length = 0;
430
- if (typeof this.timeline_parameters !== "undefined") {
431
- for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
432
- length += this.timeline_parameters.timeline[i].length();
433
- }
434
- } else {
435
- return 1;
436
- }
437
- return length;
438
- }
439
-
440
- // return the percentage of trials completed, grouped at the first child level
441
- // counts a set of trials as complete when the child node is done
442
- percentComplete() {
443
- var total_trials = this.length();
444
- var completed_trials = 0;
445
- for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
446
- if (this.timeline_parameters.timeline[i].isComplete()) {
447
- completed_trials += this.timeline_parameters.timeline[i].length();
448
- }
449
- }
450
- return (completed_trials / total_trials) * 100;
451
- }
452
-
453
- // resets the node and all subnodes to original state
454
- // but increments the current_iteration counter
455
- reset() {
456
- this.progress.current_location = -1;
457
- this.progress.current_repetition = 0;
458
- this.progress.current_variable_set = 0;
459
- this.progress.current_iteration++;
460
- this.progress.done = false;
461
- this.setTimelineVariablesOrder();
462
- if (typeof this.timeline_parameters != "undefined") {
463
- for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
464
- this.timeline_parameters.timeline[i].reset();
465
- }
466
- }
467
- }
468
-
469
- // mark this node as finished
470
- end() {
471
- this.progress.done = true;
472
- }
473
-
474
- // recursively end whatever sub-node is running the current trial
475
- endActiveNode() {
476
- if (typeof this.timeline_parameters == "undefined") {
477
- this.end();
478
- this.parent_node.end();
479
- } else {
480
- this.timeline_parameters.timeline[this.progress.current_location].endActiveNode();
481
- }
482
- }
483
-
484
- // get a unique ID associated with this node
485
- // the ID reflects the current iteration through this node.
486
- ID() {
487
- var id = "";
488
- if (typeof this.parent_node == "undefined") {
489
- return "0." + this.progress.current_iteration;
490
- } else {
491
- id += this.parent_node.ID() + "-";
492
- id += this.relative_id + "." + this.progress.current_iteration;
493
- return id;
494
- }
495
- }
496
-
497
- // get the ID of the active trial
498
- activeID() {
499
- if (typeof this.timeline_parameters == "undefined") {
500
- return this.ID();
501
- } else {
502
- return this.timeline_parameters.timeline[this.progress.current_location].activeID();
503
- }
504
- }
505
-
506
- // get all the data generated within this node
507
- generatedData() {
508
- return this.jsPsych.data.getDataByTimelineNode(this.ID());
509
- }
510
-
511
- // get all the trials of a particular type
512
- trialsOfType(type) {
513
- if (typeof this.timeline_parameters == "undefined") {
514
- if (this.trial_parameters.type == type) {
515
- return this.trial_parameters;
516
- } else {
517
- return [];
518
- }
519
- } else {
520
- var trials = [];
521
- for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
522
- var t = this.timeline_parameters.timeline[i].trialsOfType(type);
523
- trials = trials.concat(t);
524
- }
525
- return trials;
526
- }
527
- }
528
-
529
- // add new trials to end of this timeline
530
- insert(parameters) {
531
- if (typeof this.timeline_parameters === "undefined") {
532
- console.error("Cannot add new trials to a trial-level node.");
533
- } else {
534
- this.timeline_parameters.timeline.push(
535
- new TimelineNode(
536
- this.jsPsych,
537
- { ...this.node_trial_data, ...parameters },
538
- this,
539
- this.timeline_parameters.timeline.length
540
- )
541
- );
542
- }
543
- }
544
- }
@@ -1,32 +0,0 @@
1
- export class HardwareAPI {
2
- /**
3
- * Indicates whether this instance of jspsych has opened a hardware connection through our browser
4
- * extension
5
- **/
6
- hardwareConnected = false;
7
-
8
- constructor() {
9
- //it might be useful to open up a line of communication from the extension back to this page
10
- //script, again, this will have to pass through DOM events. For now speed is of no concern so I
11
- //will use jQuery
12
- document.addEventListener("jspsych-activate", (evt) => {
13
- this.hardwareConnected = true;
14
- });
15
- }
16
-
17
- /**
18
- * Allows communication with user hardware through our custom Google Chrome extension + native C++ program
19
- * @param mess The message to be passed to our extension, see its documentation for the expected members of this object.
20
- * @author Daniel Rivas
21
- *
22
- */
23
- hardware(mess) {
24
- //since Chrome extension content-scripts do not share the javascript environment with the page
25
- //script that loaded jspsych, we will need to use hacky methods like communicating through DOM
26
- //events.
27
- const jspsychEvt = new CustomEvent("jspsych", { detail: mess });
28
- document.dispatchEvent(jspsychEvt);
29
- //And voila! it will be the job of the content script injected by the extension to listen for
30
- //the event and do the appropriate actions.
31
- }
32
- }