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