jspsych 8.1.0 → 8.2.1
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 +2 -2
- package/dist/index.browser.js +1345 -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 +246 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +28 -39
- package/dist/index.js +246 -108
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/ExtensionManager.spec.ts +1 -1
- package/src/JsPsych.ts +47 -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 +2 -2
- package/src/timeline/Trial.spec.ts +16 -2
- 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.1.0",
|
|
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.1";
|
|
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)) {
|
|
@@ -2087,6 +2153,9 @@ class Timeline extends TimelineNode {
|
|
|
2087
2153
|
this.resumePromise.resolve();
|
|
2088
2154
|
}
|
|
2089
2155
|
}
|
|
2156
|
+
/**
|
|
2157
|
+
* If the timeline is running or paused, aborts the timeline after the current trial has completed
|
|
2158
|
+
*/
|
|
2090
2159
|
abort() {
|
|
2091
2160
|
if (this.status === TimelineNodeStatus.RUNNING || this.status === TimelineNodeStatus.PAUSED) {
|
|
2092
2161
|
if (this.currentChild instanceof Timeline) {
|
|
@@ -2109,6 +2178,11 @@ class Timeline extends TimelineNode {
|
|
|
2109
2178
|
...index === null ? void 0 : this.description.timeline_variables[index]
|
|
2110
2179
|
};
|
|
2111
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
|
+
*/
|
|
2112
2186
|
generateTimelineVariableOrder() {
|
|
2113
2187
|
const timelineVariableLength = this.description.timeline_variables?.length;
|
|
2114
2188
|
if (!timelineVariableLength) {
|
|
@@ -2135,7 +2209,8 @@ class Timeline extends TimelineNode {
|
|
|
2135
2209
|
break;
|
|
2136
2210
|
default:
|
|
2137
2211
|
throw new Error(
|
|
2138
|
-
`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"`
|
|
2139
2214
|
);
|
|
2140
2215
|
}
|
|
2141
2216
|
}
|
|
@@ -2144,6 +2219,9 @@ class Timeline extends TimelineNode {
|
|
|
2144
2219
|
}
|
|
2145
2220
|
return order;
|
|
2146
2221
|
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Returns the current values of all timeline variables, including those from parent timelines
|
|
2224
|
+
*/
|
|
2147
2225
|
getAllTimelineVariables() {
|
|
2148
2226
|
return this.currentTimelineVariables;
|
|
2149
2227
|
}
|
|
@@ -2167,6 +2245,10 @@ class Timeline extends TimelineNode {
|
|
|
2167
2245
|
}
|
|
2168
2246
|
return results;
|
|
2169
2247
|
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Returns the naive progress of the timeline (as a fraction), without considering conditional or
|
|
2250
|
+
* loop functions.
|
|
2251
|
+
*/
|
|
2170
2252
|
getNaiveProgress() {
|
|
2171
2253
|
if (this.status === TimelineNodeStatus.PENDING) {
|
|
2172
2254
|
return 0;
|
|
@@ -2181,6 +2263,10 @@ class Timeline extends TimelineNode {
|
|
|
2181
2263
|
}
|
|
2182
2264
|
return Math.min(completedTrials / this.getNaiveTrialCount(), 1);
|
|
2183
2265
|
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Recursively computes the naive number of trials in the timeline, without considering
|
|
2268
|
+
* conditional or loop functions.
|
|
2269
|
+
*/
|
|
2184
2270
|
getNaiveTrialCount() {
|
|
2185
2271
|
const getTrialCount = (description) => {
|
|
2186
2272
|
const getTimelineArrayTrialCount = (description2) => description2.map((childDescription) => getTrialCount(childDescription)).reduce((a, b) => a + b);
|
|
@@ -2226,7 +2312,17 @@ class JsPsych {
|
|
|
2226
2312
|
this.turk = turk;
|
|
2227
2313
|
this.randomization = randomization;
|
|
2228
2314
|
this.utils = utils;
|
|
2315
|
+
// prettier-ignore
|
|
2316
|
+
this.citation = {
|
|
2317
|
+
"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 ",
|
|
2318
|
+
"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}, } '
|
|
2319
|
+
};
|
|
2320
|
+
/** Options */
|
|
2229
2321
|
this.options = {};
|
|
2322
|
+
/**
|
|
2323
|
+
* Whether the page is retrieved directly via the `file://` protocol (true) or hosted on a web
|
|
2324
|
+
* server (false)
|
|
2325
|
+
*/
|
|
2230
2326
|
this.isFileProtocolUsed = false;
|
|
2231
2327
|
this.finishTrialPromise = new PromiseWrapper();
|
|
2232
2328
|
this.timelineDependencies = {
|
|
@@ -2319,8 +2415,14 @@ class JsPsych {
|
|
|
2319
2415
|
);
|
|
2320
2416
|
}
|
|
2321
2417
|
version() {
|
|
2322
|
-
return
|
|
2323
|
-
}
|
|
2418
|
+
return version;
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Starts an experiment using the provided timeline and returns a promise that is resolved when
|
|
2422
|
+
* the experiment is finished.
|
|
2423
|
+
*
|
|
2424
|
+
* @param timeline The timeline to be run
|
|
2425
|
+
*/
|
|
2324
2426
|
async run(timeline) {
|
|
2325
2427
|
if (typeof timeline === "undefined") {
|
|
2326
2428
|
console.error("No timeline declared in jsPsych.run(). Cannot start experiment.");
|
|
@@ -2334,7 +2436,7 @@ class JsPsych {
|
|
|
2334
2436
|
await this.prepareDom();
|
|
2335
2437
|
await this.extensionManager.initializeExtensions();
|
|
2336
2438
|
document.documentElement.setAttribute("jspsych", "present");
|
|
2337
|
-
this.experimentStartTime = new Date();
|
|
2439
|
+
this.experimentStartTime = /* @__PURE__ */ new Date();
|
|
2338
2440
|
await this.timeline.run();
|
|
2339
2441
|
await Promise.resolve(this.options.on_finish(this.data.get()));
|
|
2340
2442
|
if (this.endMessage) {
|
|
@@ -2361,7 +2463,7 @@ class JsPsych {
|
|
|
2361
2463
|
if (!this.experimentStartTime) {
|
|
2362
2464
|
return 0;
|
|
2363
2465
|
}
|
|
2364
|
-
return new Date().getTime() - this.experimentStartTime.getTime();
|
|
2466
|
+
return (/* @__PURE__ */ new Date()).getTime() - this.experimentStartTime.getTime();
|
|
2365
2467
|
}
|
|
2366
2468
|
getDisplayElement() {
|
|
2367
2469
|
return this.displayElement;
|
|
@@ -2385,6 +2487,11 @@ class JsPsych {
|
|
|
2385
2487
|
currentTimeline.abort();
|
|
2386
2488
|
}
|
|
2387
2489
|
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Aborts a named timeline. The timeline must be currently running in order to abort it.
|
|
2492
|
+
*
|
|
2493
|
+
* @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.
|
|
2494
|
+
*/
|
|
2388
2495
|
abortTimelineByName(name) {
|
|
2389
2496
|
const timeline = this.timeline?.getActiveTimelineByName(name);
|
|
2390
2497
|
if (timeline) {
|
|
@@ -2419,6 +2526,36 @@ class JsPsych {
|
|
|
2419
2526
|
getTimeline() {
|
|
2420
2527
|
return this.timeline?.description.timeline;
|
|
2421
2528
|
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Prints out a string containing citations for the jsPsych library and all input plugins/extensions in the specified format.
|
|
2531
|
+
* If called without input, prints citation for jsPsych library.
|
|
2532
|
+
*
|
|
2533
|
+
* @param plugins The plugins/extensions to generate citations for. Always prints the citation for the jsPsych library at the top.
|
|
2534
|
+
* @param format The desired output citation format. Currently supports "apa" and "bibtex".
|
|
2535
|
+
* @returns String containing citations separated with newline character.
|
|
2536
|
+
*/
|
|
2537
|
+
getCitations(plugins = [], format = "apa") {
|
|
2538
|
+
const formatOptions = ["apa", "bibtex"];
|
|
2539
|
+
format = format.toLowerCase();
|
|
2540
|
+
if (!Array.isArray(plugins)) {
|
|
2541
|
+
throw new Error("Expected array of plugins/extensions");
|
|
2542
|
+
} else if (!formatOptions.includes(format)) {
|
|
2543
|
+
throw new Error("Unsupported citation format");
|
|
2544
|
+
} else {
|
|
2545
|
+
const jsPsychCitation = this.citation[format];
|
|
2546
|
+
const citationSet = /* @__PURE__ */ new Set([jsPsychCitation]);
|
|
2547
|
+
for (const plugin of plugins) {
|
|
2548
|
+
try {
|
|
2549
|
+
const pluginCitation = plugin["info"].citations[format];
|
|
2550
|
+
citationSet.add(pluginCitation);
|
|
2551
|
+
} catch {
|
|
2552
|
+
console.error(`${plugin} does not have citation in ${format} format.`);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
const citationList = Array.from(citationSet).join("\n");
|
|
2556
|
+
return citationList;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2422
2559
|
get extensions() {
|
|
2423
2560
|
return this.extensionManager?.extensions ?? {};
|
|
2424
2561
|
}
|
|
@@ -2519,6 +2656,7 @@ function initJsPsych(options) {
|
|
|
2519
2656
|
init: "`jsPsych.init()` was replaced by `initJsPsych()` in jsPsych v7.",
|
|
2520
2657
|
ALL_KEYS: 'jsPsych.ALL_KEYS was replaced by the "ALL_KEYS" string in jsPsych v7.',
|
|
2521
2658
|
NO_KEYS: 'jsPsych.NO_KEYS was replaced by the "NO_KEYS" string in jsPsych v7.',
|
|
2659
|
+
// Getter functions that were renamed
|
|
2522
2660
|
currentTimelineNodeID: "`currentTimelineNodeID()` was renamed to `getCurrentTimelineNodeID()` in jsPsych v7.",
|
|
2523
2661
|
progress: "`progress()` was renamed to `getProgress()` in jsPsych v7.",
|
|
2524
2662
|
startTime: "`startTime()` was renamed to `getStartTime()` in jsPsych v7.",
|