jspsych 8.0.3 → 8.2.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.
- package/README.md +1 -1
- package/dist/index.browser.js +1350 -400
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +7 -6
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +251 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +28 -40
- package/dist/index.js +251 -115
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/ExtensionManager.spec.ts +1 -1
- package/src/JsPsych.ts +46 -0
- package/src/modules/extensions.ts +1 -0
- package/src/modules/plugins.ts +1 -0
- package/src/modules/randomization.ts +1 -1
- package/src/timeline/Timeline.spec.ts +105 -5
- package/src/timeline/Timeline.ts +12 -9
- package/src/timeline/Trial.spec.ts +15 -1
- package/src/timeline/Trial.ts +6 -1
package/dist/index.cjs
CHANGED
|
@@ -2,72 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
var autoBind = require('auto-bind');
|
|
4
4
|
var rw = require('random-words');
|
|
5
|
-
var seedrandom = require('seedrandom/lib/alea');
|
|
5
|
+
var seedrandom = require('seedrandom/lib/alea.js');
|
|
6
6
|
|
|
7
|
-
var
|
|
8
|
-
name: "jspsych",
|
|
9
|
-
version: "8.0.3",
|
|
10
|
-
description: "Behavioral experiments in a browser",
|
|
11
|
-
type: "module",
|
|
12
|
-
main: "dist/index.cjs",
|
|
13
|
-
exports: {
|
|
14
|
-
".": {
|
|
15
|
-
import: "./dist/index.js",
|
|
16
|
-
require: "./dist/index.cjs"
|
|
17
|
-
},
|
|
18
|
-
"./css/*": "./css/*"
|
|
19
|
-
},
|
|
20
|
-
typings: "dist/index.d.ts",
|
|
21
|
-
unpkg: "dist/index.browser.min.js",
|
|
22
|
-
files: [
|
|
23
|
-
"src",
|
|
24
|
-
"dist",
|
|
25
|
-
"css"
|
|
26
|
-
],
|
|
27
|
-
source: "src/index.ts",
|
|
28
|
-
scripts: {
|
|
29
|
-
test: "jest",
|
|
30
|
-
"test:watch": "npm test -- --watch",
|
|
31
|
-
tsc: "tsc",
|
|
32
|
-
"build:js": "rollup --config",
|
|
33
|
-
"build:styles": "webpack-cli",
|
|
34
|
-
build: "run-p build:js build:styles",
|
|
35
|
-
"build:watch": 'run-p "build:js -- --watch" "build:styles watch"',
|
|
36
|
-
prepack: "cp ../../README.md ."
|
|
37
|
-
},
|
|
38
|
-
repository: {
|
|
39
|
-
type: "git",
|
|
40
|
-
url: "git+https://github.com/jspsych/jsPsych.git",
|
|
41
|
-
directory: "packages/jspsych"
|
|
42
|
-
},
|
|
43
|
-
author: "Josh de Leeuw",
|
|
44
|
-
license: "MIT",
|
|
45
|
-
bugs: {
|
|
46
|
-
url: "https://github.com/jspsych/jsPsych/issues"
|
|
47
|
-
},
|
|
48
|
-
homepage: "https://www.jspsych.org",
|
|
49
|
-
dependencies: {
|
|
50
|
-
"auto-bind": "^4.0.0",
|
|
51
|
-
"random-words": "^1.1.1",
|
|
52
|
-
seedrandom: "^3.0.5",
|
|
53
|
-
"type-fest": "^2.9.0"
|
|
54
|
-
},
|
|
55
|
-
devDependencies: {
|
|
56
|
-
"@fontsource/open-sans": "4.5.3",
|
|
57
|
-
"@jspsych/config": "^3.0.1",
|
|
58
|
-
"@types/dom-mediacapture-record": "^1.0.11",
|
|
59
|
-
"base64-inline-loader": "^2.0.1",
|
|
60
|
-
"css-loader": "^6.6.0",
|
|
61
|
-
"mini-css-extract-plugin": "^2.5.3",
|
|
62
|
-
"npm-run-all": "^4.1.5",
|
|
63
|
-
"replace-in-file-webpack-plugin": "^1.0.6",
|
|
64
|
-
sass: "^1.43.5",
|
|
65
|
-
"sass-loader": "^12.4.0",
|
|
66
|
-
webpack: "^5.76.0",
|
|
67
|
-
"webpack-cli": "^4.9.2",
|
|
68
|
-
"webpack-remove-empty-scripts": "^0.7.2"
|
|
69
|
-
}
|
|
70
|
-
};
|
|
7
|
+
var version = "8.2.0";
|
|
71
8
|
|
|
72
9
|
class ExtensionManager {
|
|
73
10
|
constructor(dependencies, extensionsConfiguration) {
|
|
@@ -139,8 +76,7 @@ function unique(arr) {
|
|
|
139
76
|
return [...new Set(arr)];
|
|
140
77
|
}
|
|
141
78
|
function deepCopy(obj) {
|
|
142
|
-
if (!obj)
|
|
143
|
-
return obj;
|
|
79
|
+
if (!obj) return obj;
|
|
144
80
|
let out;
|
|
145
81
|
if (Array.isArray(obj)) {
|
|
146
82
|
out = [];
|
|
@@ -187,9 +123,9 @@ function deepMerge(obj1, obj2) {
|
|
|
187
123
|
|
|
188
124
|
var utils = /*#__PURE__*/Object.freeze({
|
|
189
125
|
__proto__: null,
|
|
190
|
-
unique: unique,
|
|
191
126
|
deepCopy: deepCopy,
|
|
192
|
-
deepMerge: deepMerge
|
|
127
|
+
deepMerge: deepMerge,
|
|
128
|
+
unique: unique
|
|
193
129
|
});
|
|
194
130
|
|
|
195
131
|
class DataColumn {
|
|
@@ -335,10 +271,8 @@ function getQueryString() {
|
|
|
335
271
|
const b = {};
|
|
336
272
|
for (let i = 0; i < a.length; ++i) {
|
|
337
273
|
const p = a[i].split("=", 2);
|
|
338
|
-
if (p.length == 1)
|
|
339
|
-
|
|
340
|
-
else
|
|
341
|
-
b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
|
|
274
|
+
if (p.length == 1) b[p[0]] = "";
|
|
275
|
+
else b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
|
|
342
276
|
}
|
|
343
277
|
return b;
|
|
344
278
|
}
|
|
@@ -362,26 +296,44 @@ class DataCollection {
|
|
|
362
296
|
return new DataCollection([this.trials[this.trials.length - 1]]);
|
|
363
297
|
}
|
|
364
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Queries the first n elements in a collection of trials.
|
|
301
|
+
*
|
|
302
|
+
* @param n A positive integer of elements to return. A value of
|
|
303
|
+
* n that is less than 1 will throw an error.
|
|
304
|
+
*
|
|
305
|
+
* @return First n objects of a collection of trials. If fewer than
|
|
306
|
+
* n trials are available, the trials.length elements will
|
|
307
|
+
* be returned.
|
|
308
|
+
*
|
|
309
|
+
*/
|
|
365
310
|
first(n = 1) {
|
|
366
311
|
if (n < 1) {
|
|
367
312
|
throw `You must query with a positive nonzero integer. Please use a
|
|
368
313
|
different value for n.`;
|
|
369
314
|
}
|
|
370
|
-
if (this.trials.length === 0)
|
|
371
|
-
|
|
372
|
-
if (n > this.trials.length)
|
|
373
|
-
n = this.trials.length;
|
|
315
|
+
if (this.trials.length === 0) return new DataCollection();
|
|
316
|
+
if (n > this.trials.length) n = this.trials.length;
|
|
374
317
|
return new DataCollection(this.trials.slice(0, n));
|
|
375
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Queries the last n elements in a collection of trials.
|
|
321
|
+
*
|
|
322
|
+
* @param n A positive integer of elements to return. A value of
|
|
323
|
+
* n that is less than 1 will throw an error.
|
|
324
|
+
*
|
|
325
|
+
* @return Last n objects of a collection of trials. If fewer than
|
|
326
|
+
* n trials are available, the trials.length elements will
|
|
327
|
+
* be returned.
|
|
328
|
+
*
|
|
329
|
+
*/
|
|
376
330
|
last(n = 1) {
|
|
377
331
|
if (n < 1) {
|
|
378
332
|
throw `You must query with a positive nonzero integer. Please use a
|
|
379
333
|
different value for n.`;
|
|
380
334
|
}
|
|
381
|
-
if (this.trials.length === 0)
|
|
382
|
-
|
|
383
|
-
if (n > this.trials.length)
|
|
384
|
-
n = this.trials.length;
|
|
335
|
+
if (this.trials.length === 0) return new DataCollection();
|
|
336
|
+
if (n > this.trials.length) n = this.trials.length;
|
|
385
337
|
return new DataCollection(this.trials.slice(this.trials.length - n, this.trials.length));
|
|
386
338
|
}
|
|
387
339
|
values() {
|
|
@@ -501,6 +453,7 @@ class DataCollection {
|
|
|
501
453
|
class JsPsychData {
|
|
502
454
|
constructor(dependencies) {
|
|
503
455
|
this.dependencies = dependencies;
|
|
456
|
+
/** Data properties for all trials */
|
|
504
457
|
this.dataProperties = {};
|
|
505
458
|
this.interactionListeners = {
|
|
506
459
|
blur: () => {
|
|
@@ -511,7 +464,10 @@ class JsPsychData {
|
|
|
511
464
|
},
|
|
512
465
|
fullscreenchange: () => {
|
|
513
466
|
this.addInteractionRecord(
|
|
514
|
-
|
|
467
|
+
// @ts-expect-error
|
|
468
|
+
document.isFullScreen || // @ts-expect-error
|
|
469
|
+
document.webkitIsFullScreen || // @ts-expect-error
|
|
470
|
+
document.mozIsFullScreen || document.fullscreenElement ? "fullscreenenter" : "fullscreenexit"
|
|
515
471
|
);
|
|
516
472
|
}
|
|
517
473
|
};
|
|
@@ -605,6 +561,10 @@ class KeyboardListenerAPI {
|
|
|
605
561
|
autoBind(this);
|
|
606
562
|
this.registerRootListeners();
|
|
607
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* If not previously done and `this.getRootElement()` returns an element, adds the root key
|
|
566
|
+
* listeners to that element.
|
|
567
|
+
*/
|
|
608
568
|
registerRootListeners() {
|
|
609
569
|
if (!this.areRootListenersRegistered) {
|
|
610
570
|
const rootElement = this.getRootElement();
|
|
@@ -736,8 +696,7 @@ class AudioPlayer {
|
|
|
736
696
|
if (this.audio instanceof HTMLAudioElement) {
|
|
737
697
|
this.audio.play();
|
|
738
698
|
} else {
|
|
739
|
-
if (!this.audio)
|
|
740
|
-
this.audio = this.getAudioSourceNode(this.webAudioBuffer);
|
|
699
|
+
if (!this.audio) this.audio = this.getAudioSourceNode(this.webAudioBuffer);
|
|
741
700
|
this.audio.start();
|
|
742
701
|
}
|
|
743
702
|
}
|
|
@@ -799,9 +758,12 @@ const preloadParameterTypes = [
|
|
|
799
758
|
class MediaAPI {
|
|
800
759
|
constructor(useWebaudio) {
|
|
801
760
|
this.useWebaudio = useWebaudio;
|
|
761
|
+
// video //
|
|
802
762
|
this.video_buffers = {};
|
|
763
|
+
// audio //
|
|
803
764
|
this.context = null;
|
|
804
765
|
this.audio_buffers = [];
|
|
766
|
+
// preloading stimuli //
|
|
805
767
|
this.preload_requests = [];
|
|
806
768
|
this.img_cache = {};
|
|
807
769
|
this.preloadMap = /* @__PURE__ */ new Map();
|
|
@@ -1027,12 +989,25 @@ class SimulationAPI {
|
|
|
1027
989
|
dispatchEvent(event) {
|
|
1028
990
|
this.getDisplayContainerElement().dispatchEvent(event);
|
|
1029
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* Dispatches a `keydown` event for the specified key
|
|
994
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
995
|
+
*/
|
|
1030
996
|
keyDown(key) {
|
|
1031
997
|
this.dispatchEvent(new KeyboardEvent("keydown", { key }));
|
|
1032
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* Dispatches a `keyup` event for the specified key
|
|
1001
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
1002
|
+
*/
|
|
1033
1003
|
keyUp(key) {
|
|
1034
1004
|
this.dispatchEvent(new KeyboardEvent("keyup", { key }));
|
|
1035
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Dispatches a `keydown` and `keyup` event in sequence to simulate pressing a key.
|
|
1008
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
1009
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
1010
|
+
*/
|
|
1036
1011
|
pressKey(key, delay = 0) {
|
|
1037
1012
|
if (delay > 0) {
|
|
1038
1013
|
this.setJsPsychTimeout(() => {
|
|
@@ -1044,6 +1019,11 @@ class SimulationAPI {
|
|
|
1044
1019
|
this.keyUp(key);
|
|
1045
1020
|
}
|
|
1046
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Dispatches `mousedown`, `mouseup`, and `click` events on the target element
|
|
1024
|
+
* @param target The element to click
|
|
1025
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
1026
|
+
*/
|
|
1047
1027
|
clickTarget(target, delay = 0) {
|
|
1048
1028
|
if (delay > 0) {
|
|
1049
1029
|
this.setJsPsychTimeout(() => {
|
|
@@ -1057,6 +1037,12 @@ class SimulationAPI {
|
|
|
1057
1037
|
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
1058
1038
|
}
|
|
1059
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Sets the value of a target text input
|
|
1042
|
+
* @param target A text input element to fill in
|
|
1043
|
+
* @param text Text to input
|
|
1044
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
1045
|
+
*/
|
|
1060
1046
|
fillTextInput(target, text, delay = 0) {
|
|
1061
1047
|
if (delay > 0) {
|
|
1062
1048
|
this.setJsPsychTimeout(() => {
|
|
@@ -1066,6 +1052,12 @@ class SimulationAPI {
|
|
|
1066
1052
|
target.value = text;
|
|
1067
1053
|
}
|
|
1068
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Picks a valid key from `choices`, taking into account jsPsych-specific
|
|
1057
|
+
* identifiers like "NO_KEYS" and "ALL_KEYS".
|
|
1058
|
+
* @param choices Which keys are valid.
|
|
1059
|
+
* @returns A key selected at random from the valid keys.
|
|
1060
|
+
*/
|
|
1069
1061
|
getValidKey(choices) {
|
|
1070
1062
|
const possible_keys = [
|
|
1071
1063
|
"a",
|
|
@@ -1160,11 +1152,20 @@ class TimeoutAPI {
|
|
|
1160
1152
|
constructor() {
|
|
1161
1153
|
this.timeout_handlers = [];
|
|
1162
1154
|
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Calls a function after a specified delay, in milliseconds.
|
|
1157
|
+
* @param callback The function to call after the delay.
|
|
1158
|
+
* @param delay The number of milliseconds to wait before calling the function.
|
|
1159
|
+
* @returns A handle that can be used to clear the timeout with clearTimeout.
|
|
1160
|
+
*/
|
|
1163
1161
|
setTimeout(callback, delay) {
|
|
1164
1162
|
const handle = window.setTimeout(callback, delay);
|
|
1165
1163
|
this.timeout_handlers.push(handle);
|
|
1166
1164
|
return handle;
|
|
1167
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Clears all timeouts that have been created with setTimeout.
|
|
1168
|
+
*/
|
|
1168
1169
|
clearAllTimeouts() {
|
|
1169
1170
|
for (const handler of this.timeout_handlers) {
|
|
1170
1171
|
clearTimeout(handler);
|
|
@@ -1419,10 +1420,8 @@ function randomWords(opts) {
|
|
|
1419
1420
|
}
|
|
1420
1421
|
function randn_bm() {
|
|
1421
1422
|
var u = 0, v = 0;
|
|
1422
|
-
while (u === 0)
|
|
1423
|
-
|
|
1424
|
-
while (v === 0)
|
|
1425
|
-
v = Math.random();
|
|
1423
|
+
while (u === 0) u = Math.random();
|
|
1424
|
+
while (v === 0) v = Math.random();
|
|
1426
1425
|
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
|
|
1427
1426
|
}
|
|
1428
1427
|
function unpackArray(array) {
|
|
@@ -1440,21 +1439,21 @@ function unpackArray(array) {
|
|
|
1440
1439
|
|
|
1441
1440
|
var randomization = /*#__PURE__*/Object.freeze({
|
|
1442
1441
|
__proto__: null,
|
|
1443
|
-
setSeed: setSeed,
|
|
1444
|
-
repeat: repeat,
|
|
1445
|
-
shuffle: shuffle,
|
|
1446
|
-
shuffleNoRepeats: shuffleNoRepeats,
|
|
1447
|
-
shuffleAlternateGroups: shuffleAlternateGroups,
|
|
1448
|
-
sampleWithoutReplacement: sampleWithoutReplacement,
|
|
1449
|
-
sampleWithReplacement: sampleWithReplacement,
|
|
1450
1442
|
factorial: factorial,
|
|
1451
1443
|
randomID: randomID,
|
|
1452
1444
|
randomInt: randomInt,
|
|
1445
|
+
randomWords: randomWords,
|
|
1446
|
+
repeat: repeat,
|
|
1453
1447
|
sampleBernoulli: sampleBernoulli,
|
|
1454
|
-
sampleNormal: sampleNormal,
|
|
1455
|
-
sampleExponential: sampleExponential,
|
|
1456
1448
|
sampleExGaussian: sampleExGaussian,
|
|
1457
|
-
|
|
1449
|
+
sampleExponential: sampleExponential,
|
|
1450
|
+
sampleNormal: sampleNormal,
|
|
1451
|
+
sampleWithReplacement: sampleWithReplacement,
|
|
1452
|
+
sampleWithoutReplacement: sampleWithoutReplacement,
|
|
1453
|
+
setSeed: setSeed,
|
|
1454
|
+
shuffle: shuffle,
|
|
1455
|
+
shuffleAlternateGroups: shuffleAlternateGroups,
|
|
1456
|
+
shuffleNoRepeats: shuffleNoRepeats
|
|
1458
1457
|
});
|
|
1459
1458
|
|
|
1460
1459
|
function turkInfo() {
|
|
@@ -1486,8 +1485,7 @@ function submitToTurk(data) {
|
|
|
1486
1485
|
const turk = turkInfo();
|
|
1487
1486
|
const assignmentId = turk.assignmentId;
|
|
1488
1487
|
const turkSubmitTo = turk.turkSubmitTo;
|
|
1489
|
-
if (!assignmentId || !turkSubmitTo)
|
|
1490
|
-
return;
|
|
1488
|
+
if (!assignmentId || !turkSubmitTo) return;
|
|
1491
1489
|
const form = document.createElement("form");
|
|
1492
1490
|
form.method = "POST";
|
|
1493
1491
|
form.action = turkSubmitTo + "/mturk/externalSubmit?assignmentId=" + assignmentId;
|
|
@@ -1507,8 +1505,8 @@ function submitToTurk(data) {
|
|
|
1507
1505
|
|
|
1508
1506
|
var turk = /*#__PURE__*/Object.freeze({
|
|
1509
1507
|
__proto__: null,
|
|
1510
|
-
|
|
1511
|
-
|
|
1508
|
+
submitToTurk: submitToTurk,
|
|
1509
|
+
turkInfo: turkInfo
|
|
1512
1510
|
});
|
|
1513
1511
|
|
|
1514
1512
|
class ProgressBar {
|
|
@@ -1518,6 +1516,7 @@ class ProgressBar {
|
|
|
1518
1516
|
this._progress = 0;
|
|
1519
1517
|
this.setupElements();
|
|
1520
1518
|
}
|
|
1519
|
+
/** Adds the progress bar HTML code into `this.containerElement` */
|
|
1521
1520
|
setupElements() {
|
|
1522
1521
|
this.messageSpan = document.createElement("span");
|
|
1523
1522
|
this.innerDiv = document.createElement("div");
|
|
@@ -1529,6 +1528,7 @@ class ProgressBar {
|
|
|
1529
1528
|
this.containerElement.appendChild(this.messageSpan);
|
|
1530
1529
|
this.containerElement.appendChild(outerDiv);
|
|
1531
1530
|
}
|
|
1531
|
+
/** Updates the progress bar according to `this.progress` */
|
|
1532
1532
|
update() {
|
|
1533
1533
|
this.innerDiv.style.width = this._progress * 100 + "%";
|
|
1534
1534
|
if (typeof this.message === "function") {
|
|
@@ -1537,6 +1537,10 @@ class ProgressBar {
|
|
|
1537
1537
|
this.messageSpan.innerHTML = this.message;
|
|
1538
1538
|
}
|
|
1539
1539
|
}
|
|
1540
|
+
/**
|
|
1541
|
+
* The bar's current position as a number in the closed interval [0, 1]. Set this to update the
|
|
1542
|
+
* progress bar accordingly.
|
|
1543
|
+
*/
|
|
1540
1544
|
set progress(progress) {
|
|
1541
1545
|
if (typeof progress !== "number" || progress < 0 || progress > 1) {
|
|
1542
1546
|
throw new Error("jsPsych.progressBar.progress must be a number between 0 and 1");
|
|
@@ -1686,13 +1690,36 @@ class TimelineNode {
|
|
|
1686
1690
|
getStatus() {
|
|
1687
1691
|
return this.status;
|
|
1688
1692
|
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Initializes the parameter value cache with `this.description`. To be called by subclass
|
|
1695
|
+
* constructors after setting `this.description`.
|
|
1696
|
+
*/
|
|
1689
1697
|
initializeParameterValueCache() {
|
|
1690
1698
|
this.parameterValueCache.initialize(this.description);
|
|
1691
1699
|
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Resets all cached parameter values in this timeline node and all of its parents. This is
|
|
1702
|
+
* necessary to re-evaluate function parameters and timeline variables at each new trial.
|
|
1703
|
+
*/
|
|
1692
1704
|
resetParameterValueCache() {
|
|
1693
1705
|
this.parameterValueCache.reset();
|
|
1694
1706
|
this.parent?.resetParameterValueCache();
|
|
1695
1707
|
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Retrieves a parameter value from the description of this timeline node, recursively falling
|
|
1710
|
+
* back to the description of each parent timeline node unless `recursive` is set to `false`. If
|
|
1711
|
+
* the parameter...
|
|
1712
|
+
*
|
|
1713
|
+
* * is a timeline variable, evaluates the variable and returns the result.
|
|
1714
|
+
* * is not specified, returns `undefined`.
|
|
1715
|
+
* * is a function and `evaluateFunctions` is not set to `false`, invokes the function and returns
|
|
1716
|
+
* its return value
|
|
1717
|
+
* * has previously been looked up, return the cached result of the previous lookup
|
|
1718
|
+
*
|
|
1719
|
+
* @param parameterPath The path of the respective parameter in the timeline node description. If
|
|
1720
|
+
* the path is an array, nested object properties or array items will be looked up.
|
|
1721
|
+
* @param options See {@link GetParameterValueOptions}
|
|
1722
|
+
*/
|
|
1696
1723
|
getParameterValue(parameterPath, options = {}) {
|
|
1697
1724
|
const {
|
|
1698
1725
|
evaluateFunctions = true,
|
|
@@ -1721,6 +1748,11 @@ class TimelineNode {
|
|
|
1721
1748
|
}
|
|
1722
1749
|
return result;
|
|
1723
1750
|
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Retrieves and evaluates the `data` parameter. It is different from other parameters in that
|
|
1753
|
+
* it's properties may be functions that have to be evaluated, and parent nodes' data parameter
|
|
1754
|
+
* properties are merged into the result.
|
|
1755
|
+
*/
|
|
1724
1756
|
getDataParameter() {
|
|
1725
1757
|
const data = this.getParameterValue("data", { recursive: false });
|
|
1726
1758
|
return {
|
|
@@ -1744,7 +1776,12 @@ class Trial extends TimelineNode {
|
|
|
1744
1776
|
this.initializeParameterValueCache();
|
|
1745
1777
|
this.trialObject = deepCopy(description);
|
|
1746
1778
|
this.pluginClass = this.getParameterValue("type", { evaluateFunctions: false });
|
|
1747
|
-
this.pluginInfo = this.pluginClass["info"];
|
|
1779
|
+
this.pluginInfo = this.pluginClass?.["info"];
|
|
1780
|
+
if (!this.pluginInfo) {
|
|
1781
|
+
throw new Error(
|
|
1782
|
+
"Plugin not recognized. Please provide a valid plugin using the 'type' parameter."
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1748
1785
|
if (!("version" in this.pluginInfo) && !("data" in this.pluginInfo)) {
|
|
1749
1786
|
console.warn(
|
|
1750
1787
|
this.pluginInfo["name"],
|
|
@@ -1826,10 +1863,16 @@ class Trial extends TimelineNode {
|
|
|
1826
1863
|
)
|
|
1827
1864
|
};
|
|
1828
1865
|
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Cleanup the trial by removing the display element and removing event listeners
|
|
1868
|
+
*/
|
|
1829
1869
|
cleanupTrial() {
|
|
1830
1870
|
this.dependencies.clearAllTimeouts();
|
|
1831
1871
|
this.dependencies.getDisplayElement().innerHTML = "";
|
|
1832
1872
|
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Add the CSS classes from the `css_classes` parameter to the display element
|
|
1875
|
+
*/
|
|
1833
1876
|
addCssClasses() {
|
|
1834
1877
|
const classes = this.getParameterValue("css_classes");
|
|
1835
1878
|
const classList = this.dependencies.getDisplayElement().classList;
|
|
@@ -1839,6 +1882,9 @@ class Trial extends TimelineNode {
|
|
|
1839
1882
|
classList.add(...classes);
|
|
1840
1883
|
}
|
|
1841
1884
|
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Removes the provided css classes from the display element
|
|
1887
|
+
*/
|
|
1842
1888
|
removeCssClasses() {
|
|
1843
1889
|
const classes = this.getParameterValue("css_classes");
|
|
1844
1890
|
if (classes) {
|
|
@@ -1887,6 +1933,12 @@ class Trial extends TimelineNode {
|
|
|
1887
1933
|
}
|
|
1888
1934
|
return result;
|
|
1889
1935
|
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Runs a callback function retrieved from a parameter value and returns its result.
|
|
1938
|
+
*
|
|
1939
|
+
* @param parameterName The name of the parameter to retrieve the callback function from.
|
|
1940
|
+
* @param callbackParameters The parameters (if any) to be passed to the callback function
|
|
1941
|
+
*/
|
|
1890
1942
|
runParameterCallback(parameterName, ...callbackParameters) {
|
|
1891
1943
|
const callback = this.getParameterValue(parameterName, { evaluateFunctions: false });
|
|
1892
1944
|
if (callback) {
|
|
@@ -1917,6 +1969,10 @@ class Trial extends TimelineNode {
|
|
|
1917
1969
|
}
|
|
1918
1970
|
return super.getParameterValue(parameterPath, options);
|
|
1919
1971
|
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Retrieves and evaluates the `simulation_options` parameter, considering nested properties and
|
|
1974
|
+
* global simulation options.
|
|
1975
|
+
*/
|
|
1920
1976
|
getSimulationOptions() {
|
|
1921
1977
|
const simulationOptions = this.getParameterValue("simulation_options", {
|
|
1922
1978
|
replaceResult: (result = {}) => {
|
|
@@ -1946,6 +2002,10 @@ class Trial extends TimelineNode {
|
|
|
1946
2002
|
}
|
|
1947
2003
|
return simulationOptions;
|
|
1948
2004
|
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Returns the result object of this trial or `undefined` if the result is not yet known or the
|
|
2007
|
+
* `record_data` trial parameter is `false`.
|
|
2008
|
+
*/
|
|
1949
2009
|
getResult() {
|
|
1950
2010
|
return this.getParameterValue("record_data") === false ? void 0 : this.result;
|
|
1951
2011
|
}
|
|
@@ -1953,6 +2013,12 @@ class Trial extends TimelineNode {
|
|
|
1953
2013
|
const result = this.getResult();
|
|
1954
2014
|
return result ? [result] : [];
|
|
1955
2015
|
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Checks that the parameters provided in the trial description align with the plugin's info
|
|
2018
|
+
* object, resolves missing parameter values from the parent timeline, resolves timeline variable
|
|
2019
|
+
* parameters, evaluates parameter functions if the expected parameter type is not `FUNCTION`, and
|
|
2020
|
+
* sets default values for optional parameters.
|
|
2021
|
+
*/
|
|
1956
2022
|
processParameters() {
|
|
1957
2023
|
const assignParameterValues = (parameterObject, parameterInfos, parentParameterPath = []) => {
|
|
1958
2024
|
for (const [parameterName, parameterConfig] of Object.entries(parameterInfos)) {
|
|
@@ -2040,7 +2106,8 @@ class Timeline extends TimelineNode {
|
|
|
2040
2106
|
}
|
|
2041
2107
|
for (const timelineVariableIndex of timelineVariableOrder) {
|
|
2042
2108
|
this.setCurrentTimelineVariablesByIndex(timelineVariableIndex);
|
|
2043
|
-
for (const
|
|
2109
|
+
for (const childNodeDescription of this.description.timeline) {
|
|
2110
|
+
const childNode = this.instantiateChildNode(childNodeDescription);
|
|
2044
2111
|
const previousChild = this.currentChild;
|
|
2045
2112
|
this.currentChild = childNode;
|
|
2046
2113
|
childNode.index = previousChild ? previousChild.getLatestNode().index + 1 : this.index;
|
|
@@ -2086,6 +2153,9 @@ class Timeline extends TimelineNode {
|
|
|
2086
2153
|
this.resumePromise.resolve();
|
|
2087
2154
|
}
|
|
2088
2155
|
}
|
|
2156
|
+
/**
|
|
2157
|
+
* If the timeline is running or paused, aborts the timeline after the current trial has completed
|
|
2158
|
+
*/
|
|
2089
2159
|
abort() {
|
|
2090
2160
|
if (this.status === TimelineNodeStatus.RUNNING || this.status === TimelineNodeStatus.PAUSED) {
|
|
2091
2161
|
if (this.currentChild instanceof Timeline) {
|
|
@@ -2097,12 +2167,10 @@ class Timeline extends TimelineNode {
|
|
|
2097
2167
|
}
|
|
2098
2168
|
}
|
|
2099
2169
|
}
|
|
2100
|
-
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
this.children.push(...newChildNodes);
|
|
2105
|
-
return newChildNodes;
|
|
2170
|
+
instantiateChildNode(childDescription) {
|
|
2171
|
+
const newChildNode = isTimelineDescription(childDescription) ? new Timeline(this.dependencies, childDescription, this) : new Trial(this.dependencies, childDescription, this);
|
|
2172
|
+
this.children.push(newChildNode);
|
|
2173
|
+
return newChildNode;
|
|
2106
2174
|
}
|
|
2107
2175
|
setCurrentTimelineVariablesByIndex(index) {
|
|
2108
2176
|
this.currentTimelineVariables = {
|
|
@@ -2110,6 +2178,11 @@ class Timeline extends TimelineNode {
|
|
|
2110
2178
|
...index === null ? void 0 : this.description.timeline_variables[index]
|
|
2111
2179
|
};
|
|
2112
2180
|
}
|
|
2181
|
+
/**
|
|
2182
|
+
* If the timeline has timeline variables, returns the order of `timeline_variables` array indices
|
|
2183
|
+
* to be used, according to the timeline's `sample` setting. If the timeline has no timeline
|
|
2184
|
+
* variables, returns `[null]`.
|
|
2185
|
+
*/
|
|
2113
2186
|
generateTimelineVariableOrder() {
|
|
2114
2187
|
const timelineVariableLength = this.description.timeline_variables?.length;
|
|
2115
2188
|
if (!timelineVariableLength) {
|
|
@@ -2136,7 +2209,8 @@ class Timeline extends TimelineNode {
|
|
|
2136
2209
|
break;
|
|
2137
2210
|
default:
|
|
2138
2211
|
throw new Error(
|
|
2139
|
-
`Invalid type "${
|
|
2212
|
+
`Invalid type "${// @ts-expect-error TS doesn't have a type for `sample` in this case
|
|
2213
|
+
sample.type}" in timeline sample parameters. Valid options for type are "custom", "with-replacement", "without-replacement", "fixed-repetitions", and "alternate-groups"`
|
|
2140
2214
|
);
|
|
2141
2215
|
}
|
|
2142
2216
|
}
|
|
@@ -2145,6 +2219,9 @@ class Timeline extends TimelineNode {
|
|
|
2145
2219
|
}
|
|
2146
2220
|
return order;
|
|
2147
2221
|
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Returns the current values of all timeline variables, including those from parent timelines
|
|
2224
|
+
*/
|
|
2148
2225
|
getAllTimelineVariables() {
|
|
2149
2226
|
return this.currentTimelineVariables;
|
|
2150
2227
|
}
|
|
@@ -2168,6 +2245,10 @@ class Timeline extends TimelineNode {
|
|
|
2168
2245
|
}
|
|
2169
2246
|
return results;
|
|
2170
2247
|
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Returns the naive progress of the timeline (as a fraction), without considering conditional or
|
|
2250
|
+
* loop functions.
|
|
2251
|
+
*/
|
|
2171
2252
|
getNaiveProgress() {
|
|
2172
2253
|
if (this.status === TimelineNodeStatus.PENDING) {
|
|
2173
2254
|
return 0;
|
|
@@ -2182,6 +2263,10 @@ class Timeline extends TimelineNode {
|
|
|
2182
2263
|
}
|
|
2183
2264
|
return Math.min(completedTrials / this.getNaiveTrialCount(), 1);
|
|
2184
2265
|
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Recursively computes the naive number of trials in the timeline, without considering
|
|
2268
|
+
* conditional or loop functions.
|
|
2269
|
+
*/
|
|
2185
2270
|
getNaiveTrialCount() {
|
|
2186
2271
|
const getTrialCount = (description) => {
|
|
2187
2272
|
const getTimelineArrayTrialCount = (description2) => description2.map((childDescription) => getTrialCount(childDescription)).reduce((a, b) => a + b);
|
|
@@ -2227,7 +2312,12 @@ class JsPsych {
|
|
|
2227
2312
|
this.turk = turk;
|
|
2228
2313
|
this.randomization = randomization;
|
|
2229
2314
|
this.utils = utils;
|
|
2315
|
+
/** Options */
|
|
2230
2316
|
this.options = {};
|
|
2317
|
+
/**
|
|
2318
|
+
* Whether the page is retrieved directly via the `file://` protocol (true) or hosted on a web
|
|
2319
|
+
* server (false)
|
|
2320
|
+
*/
|
|
2231
2321
|
this.isFileProtocolUsed = false;
|
|
2232
2322
|
this.finishTrialPromise = new PromiseWrapper();
|
|
2233
2323
|
this.timelineDependencies = {
|
|
@@ -2320,8 +2410,14 @@ class JsPsych {
|
|
|
2320
2410
|
);
|
|
2321
2411
|
}
|
|
2322
2412
|
version() {
|
|
2323
|
-
return
|
|
2324
|
-
}
|
|
2413
|
+
return version;
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Starts an experiment using the provided timeline and returns a promise that is resolved when
|
|
2417
|
+
* the experiment is finished.
|
|
2418
|
+
*
|
|
2419
|
+
* @param timeline The timeline to be run
|
|
2420
|
+
*/
|
|
2325
2421
|
async run(timeline) {
|
|
2326
2422
|
if (typeof timeline === "undefined") {
|
|
2327
2423
|
console.error("No timeline declared in jsPsych.run(). Cannot start experiment.");
|
|
@@ -2335,7 +2431,7 @@ class JsPsych {
|
|
|
2335
2431
|
await this.prepareDom();
|
|
2336
2432
|
await this.extensionManager.initializeExtensions();
|
|
2337
2433
|
document.documentElement.setAttribute("jspsych", "present");
|
|
2338
|
-
this.experimentStartTime = new Date();
|
|
2434
|
+
this.experimentStartTime = /* @__PURE__ */ new Date();
|
|
2339
2435
|
await this.timeline.run();
|
|
2340
2436
|
await Promise.resolve(this.options.on_finish(this.data.get()));
|
|
2341
2437
|
if (this.endMessage) {
|
|
@@ -2362,7 +2458,7 @@ class JsPsych {
|
|
|
2362
2458
|
if (!this.experimentStartTime) {
|
|
2363
2459
|
return 0;
|
|
2364
2460
|
}
|
|
2365
|
-
return new Date().getTime() - this.experimentStartTime.getTime();
|
|
2461
|
+
return (/* @__PURE__ */ new Date()).getTime() - this.experimentStartTime.getTime();
|
|
2366
2462
|
}
|
|
2367
2463
|
getDisplayElement() {
|
|
2368
2464
|
return this.displayElement;
|
|
@@ -2386,6 +2482,11 @@ class JsPsych {
|
|
|
2386
2482
|
currentTimeline.abort();
|
|
2387
2483
|
}
|
|
2388
2484
|
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Aborts a named timeline. The timeline must be currently running in order to abort it.
|
|
2487
|
+
*
|
|
2488
|
+
* @param name The name of the timeline to abort. Timelines can be given names by setting the `name` parameter in the description of the timeline.
|
|
2489
|
+
*/
|
|
2389
2490
|
abortTimelineByName(name) {
|
|
2390
2491
|
const timeline = this.timeline?.getActiveTimelineByName(name);
|
|
2391
2492
|
if (timeline) {
|
|
@@ -2420,6 +2521,40 @@ class JsPsych {
|
|
|
2420
2521
|
getTimeline() {
|
|
2421
2522
|
return this.timeline?.description.timeline;
|
|
2422
2523
|
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Prints out a string containing citations for the jsPsych library and all input plugins/extensions in the specified format.
|
|
2526
|
+
* If called without input, prints citation for jsPsych library.
|
|
2527
|
+
*
|
|
2528
|
+
* @param plugins The plugins/extensions to generate citations for. Always prints the citation for the jsPsych library at the top.
|
|
2529
|
+
* @param format The desired output citation format. Currently supports "apa" and "bibtex".
|
|
2530
|
+
* @returns String containing citations separated with newline character.
|
|
2531
|
+
*/
|
|
2532
|
+
getCitations(plugins = [], format = "apa") {
|
|
2533
|
+
const formatOptions = ["apa", "bibtex"];
|
|
2534
|
+
const jsPsychCitations = {
|
|
2535
|
+
"apa": "de Leeuw, J. R., Gilbert, R. A., & Luchterhandt, B. (2023). jsPsych: Enabling an Open-Source Collaborative Ecosystem of Behavioral Experiments. Journal of Open Source Software, 8(85), 5351. https://doi.org/10.21105/joss.05351 ",
|
|
2536
|
+
"bibtex": '@article{Leeuw2023jsPsych, author = {de Leeuw, Joshua R. and Gilbert, Rebecca A. and Luchterhandt, Bj{\\" o}rn}, journal = {Journal of Open Source Software}, doi = {10.21105/joss.05351}, issn = {2475-9066}, number = {85}, year = {2023}, month = {may 11}, pages = {5351}, publisher = {Open Journals}, title = {jsPsych: Enabling an {Open}-{Source} {Collaborative} {Ecosystem} of {Behavioral} {Experiments}}, url = {https://joss.theoj.org/papers/10.21105/joss.05351}, volume = {8}, } '
|
|
2537
|
+
};
|
|
2538
|
+
format = format.toLowerCase();
|
|
2539
|
+
if (!Array.isArray(plugins)) {
|
|
2540
|
+
throw new Error("Expected array of plugins/extensions");
|
|
2541
|
+
} else if (!formatOptions.includes(format)) {
|
|
2542
|
+
throw new Error("Unsupported citation format");
|
|
2543
|
+
} else {
|
|
2544
|
+
const jsPsychCitation = jsPsychCitations[format];
|
|
2545
|
+
const citationSet = /* @__PURE__ */ new Set([jsPsychCitation]);
|
|
2546
|
+
for (const plugin of plugins) {
|
|
2547
|
+
try {
|
|
2548
|
+
const pluginCitation = plugin["info"].citations[format];
|
|
2549
|
+
citationSet.add(pluginCitation);
|
|
2550
|
+
} catch {
|
|
2551
|
+
console.error(`${plugin} does not have citation in ${format} format.`);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
const citationList = Array.from(citationSet).join("\n");
|
|
2555
|
+
return citationList;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2423
2558
|
get extensions() {
|
|
2424
2559
|
return this.extensionManager?.extensions ?? {};
|
|
2425
2560
|
}
|
|
@@ -2520,6 +2655,7 @@ function initJsPsych(options) {
|
|
|
2520
2655
|
init: "`jsPsych.init()` was replaced by `initJsPsych()` in jsPsych v7.",
|
|
2521
2656
|
ALL_KEYS: 'jsPsych.ALL_KEYS was replaced by the "ALL_KEYS" string in jsPsych v7.',
|
|
2522
2657
|
NO_KEYS: 'jsPsych.NO_KEYS was replaced by the "NO_KEYS" string in jsPsych v7.',
|
|
2658
|
+
// Getter functions that were renamed
|
|
2523
2659
|
currentTimelineNodeID: "`currentTimelineNodeID()` was renamed to `getCurrentTimelineNodeID()` in jsPsych v7.",
|
|
2524
2660
|
progress: "`progress()` was renamed to `getProgress()` in jsPsych v7.",
|
|
2525
2661
|
startTime: "`startTime()` was renamed to `getStartTime()` in jsPsych v7.",
|