jspsych
Version:
Behavioral experiments in a browser
1 lines • 272 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../../../node_modules/tslib/tslib.es6.js","../../../node_modules/auto-bind/index.js","../src/migration.ts","../src/modules/utils.ts","../src/modules/data/DataColumn.ts","../src/modules/data/utils.ts","../src/modules/data/DataCollection.ts","../src/modules/data/index.ts","../src/modules/plugin-api/HardwareAPI.ts","../src/modules/plugin-api/KeyboardListenerAPI.ts","../src/modules/plugins.ts","../src/modules/plugin-api/MediaAPI.ts","../src/modules/plugin-api/SimulationAPI.ts","../src/modules/plugin-api/TimeoutAPI.ts","../src/modules/plugin-api/index.ts","../../../node_modules/random-words/index.js","../../../node_modules/seedrandom/lib/alea.js","../src/modules/randomization.ts","../src/modules/turk.ts","../src/TimelineNode.ts","../src/JsPsych.ts","../src/index.ts"],"sourcesContent":["/******************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n var desc = Object.getOwnPropertyDescriptor(m, k);\r\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\r\n desc = { enumerable: true, get: function() { return m[k]; } };\r\n }\r\n Object.defineProperty(o, k2, desc);\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n\r\nexport function __classPrivateFieldIn(state, receiver) {\r\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\r\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\r\n}\r\n","'use strict';\n\n// Gets all non-builtin properties up the prototype chain\nconst getAllProperties = object => {\n\tconst properties = new Set();\n\n\tdo {\n\t\tfor (const key of Reflect.ownKeys(object)) {\n\t\t\tproperties.add([object, key]);\n\t\t}\n\t} while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);\n\n\treturn properties;\n};\n\nmodule.exports = (self, {include, exclude} = {}) => {\n\tconst filter = key => {\n\t\tconst match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key);\n\n\t\tif (include) {\n\t\t\treturn include.some(match);\n\t\t}\n\n\t\tif (exclude) {\n\t\t\treturn !exclude.some(match);\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tfor (const [object, key] of getAllProperties(self.constructor.prototype)) {\n\t\tif (key === 'constructor' || !filter(key)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst descriptor = Reflect.getOwnPropertyDescriptor(object, key);\n\t\tif (descriptor && typeof descriptor.value === 'function') {\n\t\t\tself[key] = self[key].bind(self);\n\t\t}\n\t}\n\n\treturn self;\n};\n","export class MigrationError extends Error {\n constructor(message = \"The global `jsPsych` variable is no longer available in jsPsych v7.\") {\n super(\n `${message} Please follow the migration guide at https://www.jspsych.org/7.0/support/migration-v7/ to update your experiment.`\n );\n this.name = \"MigrationError\";\n }\n}\n\n// Define a global jsPsych object to handle invocations on it with migration errors\n(window as any).jsPsych = {\n get init() {\n throw new MigrationError(\"`jsPsych.init()` was replaced by `initJsPsych()` in jsPsych v7.\");\n },\n\n get data() {\n throw new MigrationError();\n },\n get randomization() {\n throw new MigrationError();\n },\n get turk() {\n throw new MigrationError();\n },\n get pluginAPI() {\n throw new MigrationError();\n },\n\n get ALL_KEYS() {\n throw new MigrationError(\n 'jsPsych.ALL_KEYS was replaced by the \"ALL_KEYS\" string in jsPsych v7.'\n );\n },\n get NO_KEYS() {\n throw new MigrationError('jsPsych.NO_KEYS was replaced by the \"NO_KEYS\" string in jsPsych v7.');\n },\n};\n","/**\n * Finds all of the unique items in an array.\n * @param arr The array to extract unique values from\n * @returns An array with one copy of each unique item in `arr`\n */\nexport function unique(arr: Array<any>) {\n return [...new Set(arr)];\n}\n\nexport function deepCopy(obj) {\n if (!obj) return obj;\n let out;\n if (Array.isArray(obj)) {\n out = [];\n for (const x of obj) {\n out.push(deepCopy(x));\n }\n return out;\n } else if (typeof obj === \"object\" && obj !== null) {\n out = {};\n for (const key in obj) {\n if (obj.hasOwnProperty(key)) {\n out[key] = deepCopy(obj[key]);\n }\n }\n return out;\n } else {\n return obj;\n }\n}\n\n/**\n * Merges two objects, recursively.\n * @param obj1 Object to merge\n * @param obj2 Object to merge\n */\nexport function deepMerge(obj1: any, obj2: any): any {\n let merged = {};\n for (const key in obj1) {\n if (obj1.hasOwnProperty(key)) {\n if (typeof obj1[key] === \"object\" && obj2.hasOwnProperty(key)) {\n merged[key] = deepMerge(obj1[key], obj2[key]);\n } else {\n merged[key] = obj1[key];\n }\n }\n }\n for (const key in obj2) {\n if (obj2.hasOwnProperty(key)) {\n if (!merged.hasOwnProperty(key)) {\n merged[key] = obj2[key];\n } else if (typeof obj2[key] === \"object\") {\n merged[key] = deepMerge(merged[key], obj2[key]);\n } else {\n merged[key] = obj2[key];\n }\n }\n }\n\n return merged;\n}\n","export class DataColumn {\n constructor(public values = []) {}\n\n sum() {\n let s = 0;\n for (const v of this.values) {\n s += v;\n }\n return s;\n }\n\n mean() {\n return this.sum() / this.count();\n }\n\n median() {\n if (this.values.length === 0) {\n return undefined;\n }\n const numbers = this.values.slice(0).sort(function (a, b) {\n return a - b;\n });\n const middle = Math.floor(numbers.length / 2);\n const isEven = numbers.length % 2 === 0;\n return isEven ? (numbers[middle] + numbers[middle - 1]) / 2 : numbers[middle];\n }\n\n min() {\n return Math.min.apply(null, this.values);\n }\n\n max() {\n return Math.max.apply(null, this.values);\n }\n\n count() {\n return this.values.length;\n }\n\n variance() {\n const mean = this.mean();\n let sum_square_error = 0;\n for (const x of this.values) {\n sum_square_error += Math.pow(x - mean, 2);\n }\n const mse = sum_square_error / (this.values.length - 1);\n return mse;\n }\n\n sd() {\n const mse = this.variance();\n const rmse = Math.sqrt(mse);\n return rmse;\n }\n\n frequencies() {\n const unique = {};\n for (const x of this.values) {\n if (typeof unique[x] === \"undefined\") {\n unique[x] = 1;\n } else {\n unique[x]++;\n }\n }\n return unique;\n }\n\n all(eval_fn) {\n for (const x of this.values) {\n if (!eval_fn(x)) {\n return false;\n }\n }\n return true;\n }\n\n subset(eval_fn) {\n const out = [];\n for (const x of this.values) {\n if (eval_fn(x)) {\n out.push(x);\n }\n }\n return new DataColumn(out);\n }\n}\n","// private function to save text file on local drive\nexport function saveTextToFile(textstr: string, filename: string) {\n const blobToSave = new Blob([textstr], {\n type: \"text/plain\",\n });\n let blobURL = \"\";\n if (typeof window.webkitURL !== \"undefined\") {\n blobURL = window.webkitURL.createObjectURL(blobToSave);\n } else {\n blobURL = window.URL.createObjectURL(blobToSave);\n }\n\n const link = document.createElement(\"a\");\n link.id = \"jspsych-download-as-text-link\";\n link.style.display = \"none\";\n link.download = filename;\n link.href = blobURL;\n link.click();\n}\n\n// this function based on code suggested by StackOverflow users:\n// http://stackoverflow.com/users/64741/zachary\n// http://stackoverflow.com/users/317/joseph-sturtevant\n\nexport function JSON2CSV(objArray) {\n const array = typeof objArray != \"object\" ? JSON.parse(objArray) : objArray;\n let line = \"\";\n let result = \"\";\n const columns = [];\n\n for (const row of array) {\n for (const key in row) {\n let keyString = key + \"\";\n keyString = '\"' + keyString.replace(/\"/g, '\"\"') + '\",';\n if (!columns.includes(key)) {\n columns.push(key);\n line += keyString;\n }\n }\n }\n\n line = line.slice(0, -1); // removes last comma\n result += line + \"\\r\\n\";\n\n for (const row of array) {\n line = \"\";\n for (const col of columns) {\n let value = typeof row[col] === \"undefined\" ? \"\" : row[col];\n if (typeof value == \"object\") {\n value = JSON.stringify(value);\n }\n const valueString = value + \"\";\n line += '\"' + valueString.replace(/\"/g, '\"\"') + '\",';\n }\n\n line = line.slice(0, -1);\n result += line + \"\\r\\n\";\n }\n\n return result;\n}\n\n// this function is modified from StackOverflow:\n// http://stackoverflow.com/posts/3855394\n\nexport function getQueryString() {\n const a = window.location.search.substr(1).split(\"&\");\n const b = {};\n for (let i = 0; i < a.length; ++i) {\n const p = a[i].split(\"=\", 2);\n if (p.length == 1) b[p[0]] = \"\";\n else b[p[0]] = decodeURIComponent(p[1].replace(/\\+/g, \" \"));\n }\n return b;\n}\n","import { deepCopy } from \"../utils\";\nimport { DataColumn } from \"./DataColumn\";\nimport { JSON2CSV, saveTextToFile } from \"./utils\";\n\nexport class DataCollection {\n private trials: any[];\n\n constructor(data = []) {\n this.trials = data;\n }\n\n push(new_data) {\n this.trials.push(new_data);\n return this;\n }\n\n join(other_data_collection: DataCollection) {\n this.trials = this.trials.concat(other_data_collection.values());\n return this;\n }\n\n top() {\n if (this.trials.length <= 1) {\n return this;\n } else {\n return new DataCollection([this.trials[this.trials.length - 1]]);\n }\n }\n\n /**\n * Queries the first n elements in a collection of trials.\n *\n * @param n A positive integer of elements to return. A value of\n * n that is less than 1 will throw an error.\n *\n * @return First n objects of a collection of trials. If fewer than\n * n trials are available, the trials.length elements will\n * be returned.\n *\n */\n first(n = 1) {\n if (n < 1) {\n throw `You must query with a positive nonzero integer. Please use a\n different value for n.`;\n }\n if (this.trials.length === 0) return new DataCollection();\n if (n > this.trials.length) n = this.trials.length;\n return new DataCollection(this.trials.slice(0, n));\n }\n\n /**\n * Queries the last n elements in a collection of trials.\n *\n * @param n A positive integer of elements to return. A value of\n * n that is less than 1 will throw an error.\n *\n * @return Last n objects of a collection of trials. If fewer than\n * n trials are available, the trials.length elements will\n * be returned.\n *\n */\n last(n = 1) {\n if (n < 1) {\n throw `You must query with a positive nonzero integer. Please use a\n different value for n.`;\n }\n if (this.trials.length === 0) return new DataCollection();\n if (n > this.trials.length) n = this.trials.length;\n return new DataCollection(this.trials.slice(this.trials.length - n, this.trials.length));\n }\n\n values() {\n return this.trials;\n }\n\n count() {\n return this.trials.length;\n }\n\n readOnly() {\n return new DataCollection(deepCopy(this.trials));\n }\n\n addToAll(properties) {\n for (const trial of this.trials) {\n Object.assign(trial, properties);\n }\n return this;\n }\n\n addToLast(properties) {\n if (this.trials.length != 0) {\n Object.assign(this.trials[this.trials.length - 1], properties);\n }\n return this;\n }\n\n filter(filters) {\n // [{p1: v1, p2:v2}, {p1:v2}]\n // {p1: v1}\n let f;\n if (!Array.isArray(filters)) {\n f = deepCopy([filters]);\n } else {\n f = deepCopy(filters);\n }\n\n const filtered_data = [];\n for (const trial of this.trials) {\n let keep = false;\n for (const filter of f) {\n let match = true;\n for (const key of Object.keys(filter)) {\n if (typeof trial[key] !== \"undefined\" && trial[key] === filter[key]) {\n // matches on this key!\n } else {\n match = false;\n }\n }\n if (match) {\n keep = true;\n break;\n } // can break because each filter is OR.\n }\n if (keep) {\n filtered_data.push(trial);\n }\n }\n\n return new DataCollection(filtered_data);\n }\n\n filterCustom(fn) {\n return new DataCollection(this.trials.filter(fn));\n }\n\n filterColumns(columns: Array<string>) {\n return new DataCollection(\n this.trials.map((trial) =>\n Object.fromEntries(columns.filter((key) => key in trial).map((key) => [key, trial[key]]))\n )\n );\n }\n\n select(column) {\n const values = [];\n for (const trial of this.trials) {\n if (typeof trial[column] !== \"undefined\") {\n values.push(trial[column]);\n }\n }\n return new DataColumn(values);\n }\n\n ignore(columns) {\n if (!Array.isArray(columns)) {\n columns = [columns];\n }\n const o = deepCopy(this.trials);\n for (const trial of o) {\n for (const delete_key of columns) {\n delete trial[delete_key];\n }\n }\n return new DataCollection(o);\n }\n\n uniqueNames() {\n const names = [];\n\n for (const trial of this.trials) {\n for (const key of Object.keys(trial)) {\n if (!names.includes(key)) {\n names.push(key);\n }\n }\n }\n\n return names;\n }\n\n csv() {\n return JSON2CSV(this.trials);\n }\n\n json(pretty = false) {\n if (pretty) {\n return JSON.stringify(this.trials, null, \"\\t\");\n }\n return JSON.stringify(this.trials);\n }\n\n localSave(format, filename) {\n format = format.toLowerCase();\n let data_string;\n if (format === \"json\") {\n data_string = this.json();\n } else if (format === \"csv\") {\n data_string = this.csv();\n } else {\n throw new Error('Invalid format specified for localSave. Must be \"json\" or \"csv\".');\n }\n\n saveTextToFile(data_string, filename);\n }\n}\n","import { JsPsych } from \"../../JsPsych\";\nimport { DataCollection } from \"./DataCollection\";\nimport { getQueryString } from \"./utils\";\n\nexport class JsPsychData {\n // data storage object\n private allData: DataCollection;\n\n // browser interaction event data\n private interactionData: DataCollection;\n\n // data properties for all trials\n private dataProperties = {};\n\n // cache the query_string\n private query_string;\n\n constructor(private jsPsych: JsPsych) {\n this.reset();\n }\n\n reset() {\n this.allData = new DataCollection();\n this.interactionData = new DataCollection();\n }\n\n get() {\n return this.allData;\n }\n\n getInteractionData() {\n return this.interactionData;\n }\n\n write(data_object) {\n const progress = this.jsPsych.getProgress();\n const trial = this.jsPsych.getCurrentTrial();\n\n //var trial_opt_data = typeof trial.data == 'function' ? trial.data() : trial.data;\n\n const default_data = {\n trial_type: trial.type.info.name,\n trial_index: progress.current_trial_global,\n time_elapsed: this.jsPsych.getTotalTime(),\n internal_node_id: this.jsPsych.getCurrentTimelineNodeID(),\n };\n\n this.allData.push({\n ...data_object,\n ...trial.data,\n ...default_data,\n ...this.dataProperties,\n });\n }\n\n addProperties(properties) {\n // first, add the properties to all data that's already stored\n this.allData.addToAll(properties);\n\n // now add to list so that it gets appended to all future data\n this.dataProperties = Object.assign({}, this.dataProperties, properties);\n }\n\n addDataToLastTrial(data) {\n this.allData.addToLast(data);\n }\n\n getDataByTimelineNode(node_id) {\n return this.allData.filterCustom(\n (x) => x.internal_node_id.slice(0, node_id.length) === node_id\n );\n }\n\n getLastTrialData() {\n return this.allData.top();\n }\n\n getLastTimelineData() {\n const lasttrial = this.getLastTrialData();\n const node_id = lasttrial.select(\"internal_node_id\").values[0];\n if (typeof node_id === \"undefined\") {\n return new DataCollection();\n } else {\n const parent_node_id = node_id.substr(0, node_id.lastIndexOf(\"-\"));\n const lastnodedata = this.getDataByTimelineNode(parent_node_id);\n return lastnodedata;\n }\n }\n\n displayData(format = \"json\") {\n format = format.toLowerCase();\n if (format != \"json\" && format != \"csv\") {\n console.log(\"Invalid format declared for displayData function. Using json as default.\");\n format = \"json\";\n }\n\n const data_string = format === \"json\" ? this.allData.json(true) : this.allData.csv();\n\n const display_element = this.jsPsych.getDisplayElement();\n\n display_element.innerHTML = '<pre id=\"jspsych-data-display\"></pre>';\n\n document.getElementById(\"jspsych-data-display\").textContent = data_string;\n }\n\n urlVariables() {\n if (typeof this.query_string == \"undefined\") {\n this.query_string = getQueryString();\n }\n return this.query_string;\n }\n\n getURLVariable(whichvar) {\n return this.urlVariables()[whichvar];\n }\n\n createInteractionListeners() {\n // blur event capture\n window.addEventListener(\"blur\", () => {\n const data = {\n event: \"blur\",\n trial: this.jsPsych.getProgress().current_trial_global,\n time: this.jsPsych.getTotalTime(),\n };\n this.interactionData.push(data);\n this.jsPsych.getInitSettings().on_interaction_data_update(data);\n });\n\n // focus event capture\n window.addEventListener(\"focus\", () => {\n const data = {\n event: \"focus\",\n trial: this.jsPsych.getProgress().current_trial_global,\n time: this.jsPsych.getTotalTime(),\n };\n this.interactionData.push(data);\n this.jsPsych.getInitSettings().on_interaction_data_update(data);\n });\n\n // fullscreen change capture\n const fullscreenchange = () => {\n const data = {\n event:\n // @ts-expect-error\n document.isFullScreen ||\n // @ts-expect-error\n document.webkitIsFullScreen ||\n // @ts-expect-error\n document.mozIsFullScreen ||\n document.fullscreenElement\n ? \"fullscreenenter\"\n : \"fullscreenexit\",\n trial: this.jsPsych.getProgress().current_trial_global,\n time: this.jsPsych.getTotalTime(),\n };\n this.interactionData.push(data);\n this.jsPsych.getInitSettings().on_interaction_data_update(data);\n };\n\n document.addEventListener(\"fullscreenchange\", fullscreenchange);\n document.addEventListener(\"mozfullscreenchange\", fullscreenchange);\n document.addEventListener(\"webkitfullscreenchange\", fullscreenchange);\n }\n\n // public methods for testing purposes. not recommended for use.\n _customInsert(data) {\n this.allData = new DataCollection(data);\n }\n\n _fullreset() {\n this.reset();\n this.dataProperties = {};\n }\n}\n","export class HardwareAPI {\n /**\n * Indicates whether this instance of jspsych has opened a hardware connection through our browser\n * extension\n **/\n hardwareConnected = false;\n\n constructor() {\n //it might be useful to open up a line of communication from the extension back to this page\n //script, again, this will have to pass through DOM events. For now speed is of no concern so I\n //will use jQuery\n document.addEventListener(\"jspsych-activate\", (evt) => {\n this.hardwareConnected = true;\n });\n }\n\n /**\n * Allows communication with user hardware through our custom Google Chrome extension + native C++ program\n * @param\t\tmess\tThe message to be passed to our extension, see its documentation for the expected members of this object.\n * @author\tDaniel Rivas\n *\n */\n hardware(mess) {\n //since Chrome extension content-scripts do not share the javascript environment with the page\n //script that loaded jspsych, we will need to use hacky methods like communicating through DOM\n //events.\n const jspsychEvt = new CustomEvent(\"jspsych\", { detail: mess });\n document.dispatchEvent(jspsychEvt);\n //And voila! it will be the job of the content script injected by the extension to listen for\n //the event and do the appropriate actions.\n }\n}\n","import autoBind from \"auto-bind\";\n\nexport type KeyboardListener = (e: KeyboardEvent) => void;\n\nexport type ValidResponses = string[] | \"ALL_KEYS\" | \"NO_KEYS\";\n\nexport interface GetKeyboardResponseOptions {\n callback_function: any;\n valid_responses?: ValidResponses;\n rt_method?: \"performance\" | \"audio\";\n persist?: boolean;\n audio_context?: AudioContext;\n audio_context_start_time?: number;\n allow_held_key?: boolean;\n minimum_valid_rt?: number;\n}\n\nexport class KeyboardListenerAPI {\n constructor(\n private getRootElement: () => Element | undefined,\n private areResponsesCaseSensitive: boolean = false,\n private minimumValidRt = 0\n ) {\n autoBind(this);\n this.registerRootListeners();\n }\n\n private listeners = new Set<KeyboardListener>();\n private heldKeys = new Set<string>();\n\n private areRootListenersRegistered = false;\n\n /**\n * If not previously done and `this.getRootElement()` returns an element, adds the root key\n * listeners to that element.\n */\n private registerRootListeners() {\n if (!this.areRootListenersRegistered) {\n const rootElement = this.getRootElement();\n if (rootElement) {\n rootElement.addEventListener(\"keydown\", this.rootKeydownListener);\n rootElement.addEventListener(\"keyup\", this.rootKeyupListener);\n this.areRootListenersRegistered = true;\n }\n }\n }\n\n private rootKeydownListener(e: KeyboardEvent) {\n // Iterate over a static copy of the listeners set because listeners might add other listeners\n // that we do not want to be included in the loop\n for (const listener of Array.from(this.listeners)) {\n listener(e);\n }\n this.heldKeys.add(this.toLowerCaseIfInsensitive(e.key));\n }\n\n private toLowerCaseIfInsensitive(string: string) {\n return this.areResponsesCaseSensitive ? string : string.toLowerCase();\n }\n\n private rootKeyupListener(e: KeyboardEvent) {\n this.heldKeys.delete(this.toLowerCaseIfInsensitive(e.key));\n }\n\n private isResponseValid(validResponses: ValidResponses, allowHeldKey: boolean, key: string) {\n // check if key was already held down\n if (!allowHeldKey && this.heldKeys.has(key)) {\n return false;\n }\n\n if (validResponses === \"ALL_KEYS\") {\n return true;\n }\n if (validResponses === \"NO_KEYS\") {\n return false;\n }\n\n return validResponses.includes(key);\n }\n\n getKeyboardResponse({\n callback_function,\n valid_responses = \"ALL_KEYS\",\n rt_method = \"performance\",\n persist,\n audio_context,\n audio_context_start_time,\n allow_held_key = false,\n minimum_valid_rt = this.minimumValidRt,\n }: GetKeyboardResponseOptions) {\n if (rt_method !== \"performance\" && rt_method !== \"audio\") {\n console.log(\n 'Invalid RT method specified in getKeyboardResponse. Defaulting to \"performance\" method.'\n );\n rt_method = \"performance\";\n }\n\n const usePerformanceRt = rt_method === \"performance\";\n const startTime = usePerformanceRt ? performance.now() : audio_context_start_time * 1000;\n\n this.registerRootListeners();\n\n if (!this.areResponsesCaseSensitive && typeof valid_responses !== \"string\") {\n valid_responses = valid_responses.map((r) => r.toLowerCase());\n }\n\n const listener: KeyboardListener = (e) => {\n const rt = Math.round(\n (rt_method == \"performance\" ? performance.now() : audio_context.currentTime * 1000) -\n startTime\n );\n if (rt < minimum_valid_rt) {\n return;\n }\n\n const key = this.toLowerCaseIfInsensitive(e.key);\n\n if (this.isResponseValid(valid_responses, allow_held_key, key)) {\n // if this is a valid response, then we don't want the key event to trigger other actions\n // like scrolling via the spacebar.\n e.preventDefault();\n\n if (!persist) {\n // remove keyboard listener if it exists\n this.cancelKeyboardResponse(listener);\n }\n\n callback_function({ key, rt });\n }\n };\n\n this.listeners.add(listener);\n return listener;\n }\n\n cancelKeyboardResponse(listener: KeyboardListener) {\n // remove the listener from the set of listeners if it is contained\n this.listeners.delete(listener);\n }\n\n cancelAllKeyboardResponses() {\n this.listeners.clear();\n }\n\n compareKeys(key1: string | null, key2: string | null) {\n if (\n (typeof key1 !== \"string\" && key1 !== null) ||\n (typeof key2 !== \"string\" && key2 !== null)\n ) {\n console.error(\n \"Error in jsPsych.pluginAPI.compareKeys: arguments must be key strings or null.\"\n );\n return undefined;\n }\n\n if (typeof key1 === \"string\" && typeof key2 === \"string\") {\n // if both values are strings, then check whether or not letter case should be converted before comparing (case_sensitive_responses in initJsPsych)\n return this.areResponsesCaseSensitive\n ? key1 === key2\n : key1.toLowerCase() === key2.toLowerCase();\n }\n\n return key1 === null && key2 === null;\n }\n}\n","/**\nFlatten the type output to improve type hints shown in editors.\nBorrowed from type-fest\n*/\ntype Simplify<T> = { [KeyType in keyof T]: T[KeyType] };\n\n/**\nCreate a type that makes the given keys required. The remaining keys are kept as is.\nBorrowed from type-fest\n*/\ntype SetRequired<BaseType, Keys extends keyof BaseType> = Simplify<\n Omit<BaseType, Keys> & Required<Pick<BaseType, Keys>>\n>;\n\n/**\n * Parameter types for plugins\n */\nexport enum ParameterType {\n BOOL,\n STRING,\n INT,\n FLOAT,\n FUNCTION,\n KEY,\n KEYS,\n SELECT,\n HTML_STRING,\n IMAGE,\n AUDIO,\n VIDEO,\n OBJECT,\n COMPLEX,\n TIMELINE,\n}\n\ntype ParameterTypeMap = {\n [ParameterType.BOOL]: boolean;\n [ParameterType.STRING]: string;\n [ParameterType.INT]: number;\n [ParameterType.FLOAT]: number;\n [ParameterType.FUNCTION]: (...args: any[]) => any;\n [ParameterType.KEY]: string;\n [ParameterType.KEYS]: string[] | \"ALL_KEYS\" | \"NO_KEYS\";\n [ParameterType.SELECT]: any;\n [ParameterType.HTML_STRING]: string;\n [ParameterType.IMAGE]: string;\n [ParameterType.AUDIO]: string;\n [ParameterType.VIDEO]: string;\n [ParameterType.OBJECT]: object;\n [ParameterType.COMPLEX]: any;\n [ParameterType.TIMELINE]: any;\n};\n\nexport interface ParameterInfo {\n type: ParameterType;\n array?: boolean;\n pretty_name?: string;\n default?: any;\n preload?: boolean;\n}\n\nexport interface ParameterInfos {\n [key: string]: ParameterInfo;\n}\n\ntype InferredParameter<I extends ParameterInfo> = I[\"array\"] extends true\n ? Array<ParameterTypeMap[I[\"type\"]]>\n : ParameterTypeMap[I[\"type\"]];\n\ntype RequiredParameterNames<I extends ParameterInfos> = {\n [K in keyof I]: I[K][\"default\"] extends undefined ? K : never;\n}[keyof I];\n\ntype InferredParameters<I extends ParameterInfos> = SetRequired<\n {\n [Property in keyof I]?: InferredParameter<I[Property]>;\n },\n RequiredParameterNames<I>\n>;\n\nexport const universalPluginParameters = <const>{\n /**\n * Data to add to this trial (key-value pairs)\n */\n data: {\n type: ParameterType.OBJECT,\n pretty_name: \"Data\",\n default: {},\n },\n /**\n * Function to execute when trial begins\n */\n on_start: {\n type: ParameterType.FUNCTION,\n pretty_name: \"On start\",\n default: function () {\n return;\n },\n },\n /**\n * Function to execute when trial is finished\n */\n on_finish: {\n type: ParameterType.FUNCTION,\n pretty_name: \"On finish\",\n default: function () {\n return;\n },\n },\n /**\n * Function to execute after the trial has loaded\n */\n on_load: {\n type: ParameterType.FUNCTION,\n pretty_name: \"On load\",\n default: function () {\n return;\n },\n },\n /**\n * Length of gap between the end of this trial and the start of the next trial\n */\n post_trial_gap: {\n type: ParameterType.INT,\n pretty_name: \"Post trial gap\",\n default: null,\n },\n /**\n * A list of CSS classes to add to the jsPsych display element for the duration of this trial\n */\n css_classes: {\n type: ParameterType.STRING,\n pretty_name: \"Custom CSS classes\",\n default: null,\n },\n /**\n * Options to control simulation mode for the trial.\n */\n simulation_options: {\n type: ParameterType.COMPLEX,\n default: null,\n },\n};\n\nexport type UniversalPluginParameters = InferredParameters<typeof universalPluginParameters>;\n\nexport interface PluginInfo {\n name: string;\n parameters: {\n [key: string]: ParameterInfo;\n };\n}\n\nexport interface JsPsychPlugin<I extends PluginInfo> {\n trial(\n display_element: HTMLElement,\n trial: TrialType<I>,\n on_load?: () => void\n ): void | Promise<any>;\n}\n\nexport type TrialType<I extends PluginInfo> = InferredParameters<I[\"parameters\"]> &\n UniversalPluginParameters;\n\nexport type PluginParameters<I extends PluginInfo> = InferredParameters<I[\"parameters\"]>;\n","import { ParameterType } from \"../../modules/plugins\";\nimport { unique } from \"../utils\";\n\nconst preloadParameterTypes = <const>[\n ParameterType.AUDIO,\n ParameterType.IMAGE,\n ParameterType.VIDEO,\n];\ntype PreloadType = typeof preloadParameterTypes[number];\n\nexport class MediaAPI {\n constructor(private useWebaudio: boolean, private webaudioContext?: AudioContext) {}\n\n // video //\n private video_buffers = {};\n getVideoBuffer(videoID: string) {\n if (videoID.startsWith(\"blob:\")) {\n this.video_buffers[videoID] = videoID;\n }\n return this.video_buffers[videoID];\n }\n\n // audio //\n private context = null;\n private audio_buffers = [];\n\n initAudio() {\n this.context = this.useWebaudio ? this.webaudioContext : null;\n }\n\n audioContext() {\n if (this.context !== null) {\n if (this.context.state !== \"running\") {\n this.context.resume();\n }\n }\n return this.context;\n }\n\n getAudioBuffer(audioID) {\n return new Promise((resolve, reject) => {\n // check whether audio file already preloaded\n if (\n typeof this.audio_buffers[audioID] == \"undefined\" ||\n this.audio_buffers[audioID] == \"tmp\"\n ) {\n // if audio is not already loaded, try to load it\n this.preloadAudio(\n [audioID],\n () => {\n resolve(this.audio_buffers[audioID]);\n },\n () => {},\n (e) => {\n reject(e.error);\n }\n );\n } else {\n // audio is already loaded\n resolve(this.audio_buffers[audioID]);\n }\n });\n }\n\n // preloading stimuli //\n private preload_requests = [];\n\n private img_cache = {};\n\n preloadAudio(\n files,\n callback_complete = () => {},\n callback_load = (filepath) => {},\n callback_error = (error_msg) => {}\n ) {\n files = unique(files.flat());\n\n let n_loaded = 0;\n\n if (files.length == 0) {\n callback_complete();\n return;\n }\n\n const load_audio_file_webaudio = (source, count = 1) => {\n const request = new XMLHttpRequest();\n request.open(\"GET\", source, true);\n request.responseType = \"arraybuffer\";\n request.onload = () => {\n this.context.decodeAudioData(\n request.response,\n (buffer) => {\n this.audio_buffers[source] = buffer;\n n_loaded++;\n callback_load(source);\n if (n_loaded == files.length) {\n callback_complete();\n }\n },\n (e) => {\n callback_error({ source: source, error: e });\n }\n );\n };\n request.onerror = (e) => {\n let err: ProgressEvent | string = e;\n if (request.status == 404) {\n err = \"404\";\n }\n callback_error({ source: source, error: err });\n };\n request.onloadend = (e) => {\n if (request.status == 404) {\n callback_error({ source: source, error: \"404\" });\n }\n };\n request.send();\n this.preload_requests.push(request);\n };\n\n const load_audio_file_html5audio = (source, count = 1) => {\n const audio = new Audio();\n const handleCanPlayThrough = () => {\n this.audio_buffers[source] = audio;\n n_loaded++;\n callback_load(source);\n if (n_loaded == files.length) {\n callback_complete();\n }\n audio.removeEventListener(\"canplaythrough\", handleCanPlayThrough);\n };\n audio.addEventListener(\"canplaythrough\", handleCanPlayThrough);\n audio.addEventListener(\"error\", function handleError(e) {\n callback_error({ source: audio.src, error: e });\n audio.removeEventListener(\"error\", handleError);\n });\n audio.addEventListener(\"abort\", function handleAbort(e) {\n callback_error({ source: audio.src, error: e });\n audio.removeEventListener(\"abort\", handleAbort);\n });\n audio.src = source;\n this.preload_requests.push(audio);\n };\n\n for (const file of files) {\n if (typeof this.audio_buffers[file] !== \"undefined\") {\n n_loaded++;\n callback_load(file);\n if (n_loaded == files.length) {\n callback_complete();\n }\n } else {\n this.audio_buffers[file] = \"tmp\";\n if (this.audioContext() !== null) {\n load_audio_file_webaudio(file);\n } else {\n load_audio_file_html5audio(file);\n }\n }\n }\n }\n\n preloadImages(\n images,\n callback_complete = () => {},\n callback_load = (filepath) => {},\n callback_error = (error_msg) => {}\n ) {\n // flatten the images array\n images = unique(images.flat());\n\n var n_loaded = 0;\n\n if (images.length === 0) {\n callback_complete();\n return;\n }\n\n for (let i = 0; i < images.length; i++) {\n const img = new Image();\n const src = images[i];\n img.onload = () => {\n n_loaded++;\n callback_load(src);\n if (n_loaded === images.length) {\n callback_complete();\n }\n };\n\n img.onerror = (e) => {\n callback_error({ source: src, error: e });\n };\n\n img.src = src;\n\n this.img_cache[src] = img;\n this.preload_requests.push(img);\n }\n }\n\n preloadVideo(\n videos,\n callback_complete = () => {},\n callback_load = (filepath) => {},\n callback_error = (error_msg) => {}\n ) {\n // flatten the video array\n videos = unique(videos.flat());\n\n let n_loaded = 0;\n\n if (videos.length === 0) {\n callback_complete();\n return;\n }\n\n for (const video of videos) {\n const video_buffers = this.video_buffers;\n\n //based on option 4 here: http://dinbror.dk/blog/how-to-preload-entire-html5-video-before-play-solved/\n const request = new XMLHttpRequest();\n request.open(\"GET\", video, true);\n request.responseType = \"blob\";\n request.onload = () => {\n if (request.status === 200 || request.status === 0) {\n const videoBlob = request.response;\n video_buffers[video] = URL.createObjectURL(videoBlob); // IE10+\n n_loaded++;\n callback_load(video);\n if (n_loaded === videos.length) {\n callback_complete();\n }\n }\n };\n request.onerror = (e) => {\n let err: ProgressEvent | string = e;\n if (request.status == 404) {\n err = \"404\";\n }\n callback_error({ source: video, error: err });\n };\n request.onloadend = (e) => {\n if (request.status == 404) {\n callback_error({ source: video, error: \"404\" });\n }\n };\n request.send();\n this.preload_requests.push(request);\n }\n }\n\n private preloadMap = new Map<string, Record<string, PreloadType>>();\n\n getAutoPreloadList(timeline_description: any[]) {\n /** Map each preload parameter type to a set of paths to be preloaded */\n const preloadPaths = Object.fromEntries(\n preloadParameterTypes.map((type) => [type, new Set<string>()])\n );\n\n const traverseTimeline = (node, inheritedTrialType?) => {\n const isTimeline = typeof node.timeline !== \"undefined\";\n\n if (isTimeline) {\n for (const childNode of node.timeline) {\n traverseTimeline(childNode, node.type ?? inheritedTrialType);\n }\n } else if ((node.type ?? inheritedTrialType)?.info) {\n // node is a trial with type.info set\n\n // Get the plugin name and parameters object from the info object\n const { name: pluginName, parameters } = (node.type ?? inheritedTrialType).info;\n\n // Extract parameters to be preloaded and their types from parame