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/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 _package = {
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
- b[p[0]] = "";
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
- return new DataCollection();
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
- return new DataCollection();
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
- document.isFullScreen || document.webkitIsFullScreen || document.mozIsFullScreen || document.fullscreenElement ? "fullscreenenter" : "fullscreenexit"
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
- u = Math.random();
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
- randomWords: randomWords
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
- turkInfo: turkInfo,
1511
- submitToTurk: submitToTurk
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 "${sample.type}" in timeline sample parameters. Valid options for type are "custom", "with-replacement", "without-replacement", "fixed-repetitions", and "alternate-groups"`
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 _package.version;
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.",