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