jspsych 7.3.4 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/css/jspsych.css +18 -8
- package/dist/index.browser.js +3080 -4286
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +6 -2
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +2314 -4066
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +984 -6
- package/dist/index.js +2313 -4066
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/ExtensionManager.spec.ts +123 -0
- package/src/ExtensionManager.ts +81 -0
- package/src/JsPsych.ts +195 -690
- package/src/ProgressBar.spec.ts +60 -0
- package/src/ProgressBar.ts +60 -0
- package/src/index.scss +29 -8
- package/src/index.ts +4 -9
- package/src/modules/data/DataCollection.ts +1 -1
- package/src/modules/data/DataColumn.ts +12 -1
- package/src/modules/data/index.ts +92 -103
- package/src/modules/extensions.ts +4 -0
- package/src/modules/plugin-api/AudioPlayer.ts +101 -0
- package/src/modules/plugin-api/KeyboardListenerAPI.ts +1 -1
- package/src/modules/plugin-api/MediaAPI.ts +48 -106
- package/src/modules/plugin-api/__mocks__/AudioPlayer.ts +38 -0
- package/src/modules/plugin-api/index.ts +11 -14
- package/src/modules/plugins.ts +26 -27
- package/src/modules/randomization.ts +1 -1
- package/src/timeline/Timeline.spec.ts +921 -0
- package/src/timeline/Timeline.ts +342 -0
- package/src/timeline/TimelineNode.ts +174 -0
- package/src/timeline/Trial.spec.ts +897 -0
- package/src/timeline/Trial.ts +419 -0
- package/src/timeline/index.ts +232 -0
- package/src/timeline/util.spec.ts +124 -0
- package/src/timeline/util.ts +146 -0
- package/dist/JsPsych.d.ts +0 -112
- package/dist/TimelineNode.d.ts +0 -34
- package/dist/migration.d.ts +0 -3
- package/dist/modules/data/DataCollection.d.ts +0 -46
- package/dist/modules/data/DataColumn.d.ts +0 -15
- package/dist/modules/data/index.d.ts +0 -25
- package/dist/modules/data/utils.d.ts +0 -3
- package/dist/modules/extensions.d.ts +0 -22
- package/dist/modules/plugin-api/HardwareAPI.d.ts +0 -15
- package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +0 -34
- package/dist/modules/plugin-api/MediaAPI.d.ts +0 -32
- package/dist/modules/plugin-api/SimulationAPI.d.ts +0 -44
- package/dist/modules/plugin-api/TimeoutAPI.d.ts +0 -17
- package/dist/modules/plugin-api/index.d.ts +0 -8
- package/dist/modules/plugins.d.ts +0 -136
- package/dist/modules/randomization.d.ts +0 -42
- package/dist/modules/turk.d.ts +0 -40
- package/dist/modules/utils.d.ts +0 -13
- package/src/TimelineNode.ts +0 -544
- package/src/modules/plugin-api/HardwareAPI.ts +0 -32
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { Class } from "type-fest";
|
|
2
|
+
|
|
3
|
+
import { ParameterInfos } from "../modules/plugins";
|
|
4
|
+
import { JsPsychPlugin, ParameterType, PluginInfo } from "../modules/plugins";
|
|
5
|
+
import { deepCopy, deepMerge } from "../modules/utils";
|
|
6
|
+
import { Timeline } from "./Timeline";
|
|
7
|
+
import { GetParameterValueOptions, TimelineNode } from "./TimelineNode";
|
|
8
|
+
import { delay, isPromise, parameterPathArrayToString } from "./util";
|
|
9
|
+
import {
|
|
10
|
+
SimulationOptions,
|
|
11
|
+
TimelineNodeDependencies,
|
|
12
|
+
TimelineNodeStatus,
|
|
13
|
+
TimelineVariable,
|
|
14
|
+
TrialDescription,
|
|
15
|
+
TrialResult,
|
|
16
|
+
timelineDescriptionKeys,
|
|
17
|
+
} from ".";
|
|
18
|
+
|
|
19
|
+
export class Trial extends TimelineNode {
|
|
20
|
+
public readonly pluginClass: Class<JsPsychPlugin<any>>;
|
|
21
|
+
public pluginInstance: JsPsychPlugin<any>;
|
|
22
|
+
public trialObject?: TrialDescription;
|
|
23
|
+
public index?: number;
|
|
24
|
+
|
|
25
|
+
private result: TrialResult;
|
|
26
|
+
private readonly pluginInfo: PluginInfo;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
dependencies: TimelineNodeDependencies,
|
|
30
|
+
public readonly description: TrialDescription,
|
|
31
|
+
public readonly parent: Timeline
|
|
32
|
+
) {
|
|
33
|
+
super(dependencies);
|
|
34
|
+
this.initializeParameterValueCache();
|
|
35
|
+
|
|
36
|
+
this.trialObject = deepCopy(description);
|
|
37
|
+
this.pluginClass = this.getParameterValue("type", { evaluateFunctions: false });
|
|
38
|
+
this.pluginInfo = this.pluginClass["info"];
|
|
39
|
+
|
|
40
|
+
if (!("version" in this.pluginInfo) && !("data" in this.pluginInfo)) {
|
|
41
|
+
console.warn(
|
|
42
|
+
this.pluginInfo["name"],
|
|
43
|
+
"is missing the 'version' and 'data' fields. Please update plugin as 'version' and 'data' will be required in v9. See https://www.jspsych.org/latest/developers/plugin-development/ for more details."
|
|
44
|
+
);
|
|
45
|
+
} else if (!("version" in this.pluginInfo)) {
|
|
46
|
+
console.warn(
|
|
47
|
+
this.pluginInfo["name"],
|
|
48
|
+
"is missing the 'version' field. Please update plugin as 'version' will be required in v9. See https://www.jspsych.org/latest/developers/plugin-development/ for more details."
|
|
49
|
+
);
|
|
50
|
+
} else if (!("data" in this.pluginInfo)) {
|
|
51
|
+
console.warn(
|
|
52
|
+
this.pluginInfo["name"],
|
|
53
|
+
"is missing the 'data' field. Please update plugin as 'data' will be required in v9. See https://www.jspsych.org/latest/developers/plugin-development/ for more details."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public async run() {
|
|
59
|
+
this.status = TimelineNodeStatus.RUNNING;
|
|
60
|
+
this.processParameters();
|
|
61
|
+
|
|
62
|
+
this.onStart();
|
|
63
|
+
this.addCssClasses();
|
|
64
|
+
|
|
65
|
+
this.pluginInstance = this.dependencies.instantiatePlugin(this.pluginClass);
|
|
66
|
+
|
|
67
|
+
this.result = this.processResult(await this.executeTrial());
|
|
68
|
+
|
|
69
|
+
this.dependencies.onTrialResultAvailable(this);
|
|
70
|
+
|
|
71
|
+
this.status = TimelineNodeStatus.COMPLETED;
|
|
72
|
+
|
|
73
|
+
await this.onFinish();
|
|
74
|
+
this.removeCssClasses();
|
|
75
|
+
|
|
76
|
+
const gap = this.getParameterValue("post_trial_gap") ?? this.dependencies.getDefaultIti();
|
|
77
|
+
if (gap !== 0 && this.dependencies.getSimulationMode() !== "data-only") {
|
|
78
|
+
await delay(gap);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.resetParameterValueCache();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async executeTrial() {
|
|
85
|
+
const trialPromise = this.dependencies.finishTrialPromise.get();
|
|
86
|
+
|
|
87
|
+
/** Used as a way to figure out if `finishTrial()` has ben called without awaiting `trialPromise` */
|
|
88
|
+
let hasTrialPromiseBeenResolved = false;
|
|
89
|
+
trialPromise.then(() => {
|
|
90
|
+
hasTrialPromiseBeenResolved = true;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const { trialReturnValue, hasTrialBeenSimulated } = this.invokeTrialMethod();
|
|
94
|
+
|
|
95
|
+
// Wait until the trial has completed and grab result data
|
|
96
|
+
let result: TrialResult | void;
|
|
97
|
+
if (isPromise(trialReturnValue)) {
|
|
98
|
+
result = await Promise.race([trialReturnValue, trialPromise]);
|
|
99
|
+
|
|
100
|
+
// If `finishTrial()` was called, use the result provided to it. This may happen although
|
|
101
|
+
// `trialReturnValue` won the race ("run-to-completion").
|
|
102
|
+
if (hasTrialPromiseBeenResolved) {
|
|
103
|
+
result = await trialPromise;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
// The `simulate` method always invokes `onLoad()`, so we don't call `onLoad()` when the trial
|
|
107
|
+
// has been simulated
|
|
108
|
+
if (!hasTrialBeenSimulated) {
|
|
109
|
+
this.onLoad();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
result = await trialPromise;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The trial has finished, time to clean up.
|
|
116
|
+
this.cleanupTrial();
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private invokeTrialMethod(): {
|
|
122
|
+
trialReturnValue: void | Promise<void | TrialResult>;
|
|
123
|
+
hasTrialBeenSimulated: boolean;
|
|
124
|
+
} {
|
|
125
|
+
const globalSimulationMode = this.dependencies.getSimulationMode();
|
|
126
|
+
|
|
127
|
+
if (globalSimulationMode && typeof this.pluginInstance.simulate === "function") {
|
|
128
|
+
const simulationOptions = this.getSimulationOptions();
|
|
129
|
+
|
|
130
|
+
if (simulationOptions.simulate !== false) {
|
|
131
|
+
return {
|
|
132
|
+
hasTrialBeenSimulated: true,
|
|
133
|
+
trialReturnValue: this.pluginInstance.simulate(
|
|
134
|
+
this.trialObject,
|
|
135
|
+
simulationOptions.mode ?? globalSimulationMode,
|
|
136
|
+
simulationOptions,
|
|
137
|
+
this.onLoad
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
hasTrialBeenSimulated: false,
|
|
145
|
+
trialReturnValue: this.pluginInstance.trial(
|
|
146
|
+
this.dependencies.getDisplayElement(),
|
|
147
|
+
this.trialObject,
|
|
148
|
+
this.onLoad
|
|
149
|
+
),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Cleanup the trial by removing the display element and removing event listeners
|
|
155
|
+
*/
|
|
156
|
+
private cleanupTrial() {
|
|
157
|
+
this.dependencies.clearAllTimeouts();
|
|
158
|
+
this.dependencies.getDisplayElement().innerHTML = "";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Add the CSS classes from the `css_classes` parameter to the display element
|
|
163
|
+
*/
|
|
164
|
+
private addCssClasses() {
|
|
165
|
+
const classes: string | string[] = this.getParameterValue("css_classes");
|
|
166
|
+
const classList = this.dependencies.getDisplayElement().classList;
|
|
167
|
+
if (typeof classes === "string") {
|
|
168
|
+
classList.add(classes);
|
|
169
|
+
} else if (Array.isArray(classes)) {
|
|
170
|
+
classList.add(...classes);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Removes the provided css classes from the display element
|
|
176
|
+
*/
|
|
177
|
+
private removeCssClasses() {
|
|
178
|
+
const classes = this.getParameterValue("css_classes");
|
|
179
|
+
if (classes) {
|
|
180
|
+
this.dependencies
|
|
181
|
+
.getDisplayElement()
|
|
182
|
+
.classList.remove(...(typeof classes === "string" ? [classes] : classes));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private processResult(result: TrialResult | void) {
|
|
187
|
+
if (!result) {
|
|
188
|
+
result = {};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const [parameterName, shouldParameterBeIncluded] of Object.entries(
|
|
192
|
+
this.getParameterValue("save_trial_parameters") ?? {}
|
|
193
|
+
)) {
|
|
194
|
+
if (this.pluginInfo.parameters[parameterName]) {
|
|
195
|
+
if (shouldParameterBeIncluded && !Object.hasOwn(result, parameterName)) {
|
|
196
|
+
let parameterValue = this.trialObject[parameterName];
|
|
197
|
+
if (typeof parameterValue === "function") {
|
|
198
|
+
parameterValue = parameterValue.toString();
|
|
199
|
+
}
|
|
200
|
+
result[parameterName] = parameterValue;
|
|
201
|
+
} else if (!shouldParameterBeIncluded && Object.hasOwn(result, parameterName)) {
|
|
202
|
+
delete result[parameterName];
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
console.warn(
|
|
206
|
+
`Non-existent parameter "${parameterName}" specified in save_trial_parameters.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
result = {
|
|
212
|
+
...this.getDataParameter(),
|
|
213
|
+
...result,
|
|
214
|
+
trial_type: this.pluginInfo.name,
|
|
215
|
+
trial_index: this.index,
|
|
216
|
+
plugin_version: this.pluginInfo["version"] ? this.pluginInfo["version"] : null,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Add timeline variables to the result according to the `save_timeline_variables` parameter
|
|
220
|
+
const saveTimelineVariables = this.getParameterValue("save_timeline_variables");
|
|
221
|
+
if (saveTimelineVariables === true) {
|
|
222
|
+
result.timeline_variables = { ...this.parent.getAllTimelineVariables() };
|
|
223
|
+
} else if (Array.isArray(saveTimelineVariables)) {
|
|
224
|
+
result.timeline_variables = Object.fromEntries(
|
|
225
|
+
Object.entries(this.parent.getAllTimelineVariables()).filter(([key, _]) =>
|
|
226
|
+
saveTimelineVariables.includes(key)
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Runs a callback function retrieved from a parameter value and returns its result.
|
|
236
|
+
*
|
|
237
|
+
* @param parameterName The name of the parameter to retrieve the callback function from.
|
|
238
|
+
* @param callbackParameters The parameters (if any) to be passed to the callback function
|
|
239
|
+
*/
|
|
240
|
+
private runParameterCallback(parameterName: string, ...callbackParameters: unknown[]) {
|
|
241
|
+
const callback = this.getParameterValue(parameterName, { evaluateFunctions: false });
|
|
242
|
+
if (callback) {
|
|
243
|
+
return callback(...callbackParameters);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private onStart() {
|
|
248
|
+
this.dependencies.onTrialStart(this);
|
|
249
|
+
this.runParameterCallback("on_start", this.trialObject);
|
|
250
|
+
this.dependencies.runOnStartExtensionCallbacks(this.getParameterValue("extensions"));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private onLoad = () => {
|
|
254
|
+
this.runParameterCallback("on_load");
|
|
255
|
+
this.dependencies.runOnLoadExtensionCallbacks(this.getParameterValue("extensions"));
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
private async onFinish() {
|
|
259
|
+
const extensionResults = await this.dependencies.runOnFinishExtensionCallbacks(
|
|
260
|
+
this.getParameterValue("extensions")
|
|
261
|
+
);
|
|
262
|
+
Object.assign(this.result, extensionResults);
|
|
263
|
+
|
|
264
|
+
await Promise.resolve(this.runParameterCallback("on_finish", this.getResult()));
|
|
265
|
+
|
|
266
|
+
this.dependencies.onTrialFinished(this);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public evaluateTimelineVariable(variable: TimelineVariable) {
|
|
270
|
+
// Timeline variable values are specified at the timeline level, not at the trial level, hence
|
|
271
|
+
// deferring to the parent timeline here
|
|
272
|
+
return this.parent?.evaluateTimelineVariable(variable);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public getParameterValue(
|
|
276
|
+
parameterPath: string | string[],
|
|
277
|
+
options: GetParameterValueOptions = {}
|
|
278
|
+
) {
|
|
279
|
+
// Disable recursion for timeline description keys
|
|
280
|
+
if (
|
|
281
|
+
timelineDescriptionKeys.includes(
|
|
282
|
+
typeof parameterPath === "string" ? parameterPath : parameterPath[0]
|
|
283
|
+
)
|
|
284
|
+
) {
|
|
285
|
+
options.recursive = false;
|
|
286
|
+
}
|
|
287
|
+
return super.getParameterValue(parameterPath, options);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Retrieves and evaluates the `simulation_options` parameter, considering nested properties and
|
|
292
|
+
* global simulation options.
|
|
293
|
+
*/
|
|
294
|
+
public getSimulationOptions() {
|
|
295
|
+
const simulationOptions: SimulationOptions = this.getParameterValue("simulation_options", {
|
|
296
|
+
replaceResult: (result = {}) => {
|
|
297
|
+
if (typeof result === "string") {
|
|
298
|
+
// Look up the global simulation options by their key
|
|
299
|
+
const globalSimulationOptions = this.dependencies.getGlobalSimulationOptions();
|
|
300
|
+
result = globalSimulationOptions[result] ?? globalSimulationOptions["default"] ?? {};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return deepMerge(
|
|
304
|
+
deepCopy(this.dependencies.getGlobalSimulationOptions().default),
|
|
305
|
+
deepCopy(result)
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (typeof simulationOptions === "undefined") {
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
simulationOptions.mode = this.getParameterValue(["simulation_options", "mode"]);
|
|
315
|
+
simulationOptions.simulate = this.getParameterValue(["simulation_options", "simulate"]);
|
|
316
|
+
simulationOptions.data = this.getParameterValue(["simulation_options", "data"]);
|
|
317
|
+
|
|
318
|
+
if (typeof simulationOptions.data === "object") {
|
|
319
|
+
simulationOptions.data = Object.fromEntries(
|
|
320
|
+
Object.keys(simulationOptions.data).map((key) => [
|
|
321
|
+
key,
|
|
322
|
+
this.getParameterValue(["simulation_options", "data", key]),
|
|
323
|
+
])
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return simulationOptions;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Returns the result object of this trial or `undefined` if the result is not yet known or the
|
|
332
|
+
* `record_data` trial parameter is `false`.
|
|
333
|
+
*/
|
|
334
|
+
public getResult() {
|
|
335
|
+
return this.getParameterValue("record_data") === false ? undefined : this.result;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
public getResults() {
|
|
339
|
+
const result = this.getResult();
|
|
340
|
+
return result ? [result] : [];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Checks that the parameters provided in the trial description align with the plugin's info
|
|
345
|
+
* object, resolves missing parameter values from the parent timeline, resolves timeline variable
|
|
346
|
+
* parameters, evaluates parameter functions if the expected parameter type is not `FUNCTION`, and
|
|
347
|
+
* sets default values for optional parameters.
|
|
348
|
+
*/
|
|
349
|
+
private processParameters() {
|
|
350
|
+
const assignParameterValues = (
|
|
351
|
+
parameterObject: Record<string, any>,
|
|
352
|
+
parameterInfos: ParameterInfos,
|
|
353
|
+
parentParameterPath: string[] = []
|
|
354
|
+
) => {
|
|
355
|
+
for (const [parameterName, parameterConfig] of Object.entries(parameterInfos)) {
|
|
356
|
+
const parameterPath = [...parentParameterPath, parameterName];
|
|
357
|
+
|
|
358
|
+
let parameterValue = this.getParameterValue(parameterPath, {
|
|
359
|
+
evaluateFunctions: parameterConfig.type !== ParameterType.FUNCTION,
|
|
360
|
+
replaceResult: (originalResult) => {
|
|
361
|
+
if (typeof originalResult === "undefined") {
|
|
362
|
+
if (typeof parameterConfig.default === "undefined") {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`You must specify a value for the "${parameterPathArrayToString(
|
|
365
|
+
parameterPath
|
|
366
|
+
)}" parameter in the "${this.pluginInfo.name}" plugin.`
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
return parameterConfig.default;
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
return originalResult;
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (parameterConfig.array && !Array.isArray(parameterValue)) {
|
|
378
|
+
const parameterPathString = parameterPathArrayToString(parameterPath);
|
|
379
|
+
throw new Error(
|
|
380
|
+
`A non-array value (\`${parameterValue}\`) was provided for the array parameter "${parameterPathString}" in the "${this.pluginInfo.name}" plugin. Please make sure that "${parameterPathString}" is an array.`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (parameterConfig.type === ParameterType.COMPLEX && parameterConfig.nested) {
|
|
385
|
+
// Assign parameter values according to the `nested` schema
|
|
386
|
+
if (parameterConfig.array) {
|
|
387
|
+
// ...for each nested array element
|
|
388
|
+
parameterValue = parameterValue.map((_, arrayIndex) => {
|
|
389
|
+
const arrayElementPath = [...parameterPath, arrayIndex.toString()];
|
|
390
|
+
const arrayElementValue = this.getParameterValue(arrayElementPath);
|
|
391
|
+
assignParameterValues(arrayElementValue, parameterConfig.nested, arrayElementPath);
|
|
392
|
+
return arrayElementValue;
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
// ...for the nested object
|
|
396
|
+
assignParameterValues(parameterValue, parameterConfig.nested, parameterPath);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
parameterObject[parameterName] = parameterValue;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const trialObject = deepCopy(this.description);
|
|
405
|
+
assignParameterValues(trialObject, this.pluginInfo.parameters);
|
|
406
|
+
this.trialObject = trialObject;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
public getLatestNode() {
|
|
410
|
+
return this;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
public getActiveTimelineByName(name: string): Timeline | undefined {
|
|
414
|
+
// This returns undefined because the function is looking
|
|
415
|
+
// for a timeline. If we get to this point, then none
|
|
416
|
+
// of the parent nodes match the name.
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Class } from "type-fest";
|
|
2
|
+
|
|
3
|
+
import { JsPsychExtension } from "../modules/extensions";
|
|
4
|
+
import { JsPsychPlugin, PluginInfo } from "../modules/plugins";
|
|
5
|
+
import { Trial } from "./Trial";
|
|
6
|
+
import { PromiseWrapper } from "./util";
|
|
7
|
+
|
|
8
|
+
export class TimelineVariable {
|
|
9
|
+
constructor(public readonly name: string) {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type Parameter<T> = T | (() => T) | TimelineVariable;
|
|
13
|
+
|
|
14
|
+
export type TrialExtensionsConfiguration = Array<{
|
|
15
|
+
type: Class<JsPsychExtension>;
|
|
16
|
+
params?: Record<string, any>;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type SimulationMode = "visual" | "data-only";
|
|
20
|
+
|
|
21
|
+
export type SimulationOptions = {
|
|
22
|
+
data?: Record<string, any>;
|
|
23
|
+
mode?: SimulationMode;
|
|
24
|
+
simulate?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SimulationOptionsParameter = Parameter<{
|
|
28
|
+
data?: Parameter<Record<string, Parameter<any>>>;
|
|
29
|
+
mode?: Parameter<SimulationMode>;
|
|
30
|
+
simulate?: Parameter<boolean>;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
export interface TrialDescription extends Record<string, any> {
|
|
34
|
+
type: Parameter<Class<JsPsychPlugin<any>>>;
|
|
35
|
+
|
|
36
|
+
/** https://www.jspsych.org/latest/overview/plugins/#the-post_trial_gap-iti-parameter */
|
|
37
|
+
post_trial_gap?: Parameter<number>;
|
|
38
|
+
|
|
39
|
+
/** https://www.jspsych.org/latest/overview/plugins/#the-save_trial_parameters-parameter */
|
|
40
|
+
save_trial_parameters?: Parameter<Record<string, boolean>>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether to include the values of timeline variables under a `timeline_variables` key. Can be
|
|
44
|
+
* `true` to save the values of all timeline variables, or an array of timeline variable names to
|
|
45
|
+
* only save specific timeline variables. Defaults to `false`.
|
|
46
|
+
*/
|
|
47
|
+
save_timeline_variables?: Parameter<boolean | string[]>;
|
|
48
|
+
|
|
49
|
+
/** https://www.jspsych.org/latest/overview/style/#using-the-css_classes-trial-parameter */
|
|
50
|
+
css_classes?: Parameter<string | string[]>;
|
|
51
|
+
|
|
52
|
+
/** https://www.jspsych.org/latest/overview/simulation/#controlling-simulation-mode-with-simulation_options */
|
|
53
|
+
simulation_options?: SimulationOptionsParameter | string;
|
|
54
|
+
|
|
55
|
+
/** https://www.jspsych.org/latest/overview/extensions/ */
|
|
56
|
+
extensions?: Parameter<TrialExtensionsConfiguration>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether to record the data of this trial. Defaults to `true`.
|
|
60
|
+
*/
|
|
61
|
+
record_data?: Parameter<boolean>;
|
|
62
|
+
|
|
63
|
+
// Events
|
|
64
|
+
|
|
65
|
+
/** https://www.jspsych.org/latest/overview/events/#on_start-trial */
|
|
66
|
+
on_start?: (trial: any) => void;
|
|
67
|
+
|
|
68
|
+
/** https://www.jspsych.org/latest/overview/events/#on_load */
|
|
69
|
+
on_load?: () => void;
|
|
70
|
+
|
|
71
|
+
/** https://www.jspsych.org/latest/overview/events/#on_finish-trial */
|
|
72
|
+
on_finish?: (data: any) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** https://www.jspsych.org/latest/overview/timeline/#sampling-methods */
|
|
76
|
+
export type SampleOptions =
|
|
77
|
+
| { type: "with-replacement"; size: number; weights?: number[] }
|
|
78
|
+
| { type: "without-replacement"; size: number }
|
|
79
|
+
| { type: "fixed-repetitions"; size: number }
|
|
80
|
+
| { type: "alternate-groups"; groups: number[][]; randomize_group_order?: boolean }
|
|
81
|
+
| { type: "custom"; fn: (ids: number[]) => number[] };
|
|
82
|
+
|
|
83
|
+
export type TimelineArray = Array<TimelineDescription | TrialDescription | TimelineArray>;
|
|
84
|
+
|
|
85
|
+
export interface TimelineDescription extends Record<string, any> {
|
|
86
|
+
timeline: TimelineArray;
|
|
87
|
+
timeline_variables?: Record<string, any>[];
|
|
88
|
+
|
|
89
|
+
name?: string;
|
|
90
|
+
|
|
91
|
+
// Control flow
|
|
92
|
+
|
|
93
|
+
/** https://www.jspsych.org/latest/overview/timeline/#repeating-a-set-of-trials */
|
|
94
|
+
repetitions?: number;
|
|
95
|
+
|
|
96
|
+
/** https://www.jspsych.org/latest/overview/timeline/#looping-timelines */
|
|
97
|
+
loop_function?: (data: any) => boolean;
|
|
98
|
+
|
|
99
|
+
/** https://www.jspsych.org/latest/overview/timeline/#conditional-timelines */
|
|
100
|
+
conditional_function?: () => boolean;
|
|
101
|
+
|
|
102
|
+
// Randomization
|
|
103
|
+
|
|
104
|
+
/** https://www.jspsych.org/latest/overview/timeline/#random-orders-of-trials */
|
|
105
|
+
randomize_order?: boolean;
|
|
106
|
+
|
|
107
|
+
/** https://www.jspsych.org/latest/overview/timeline/#sampling-methods */
|
|
108
|
+
sample?: SampleOptions;
|
|
109
|
+
|
|
110
|
+
// Events
|
|
111
|
+
|
|
112
|
+
/** https://www.jspsych.org/latest/overview/events/#on_timeline_start */
|
|
113
|
+
on_timeline_start?: () => void;
|
|
114
|
+
|
|
115
|
+
/** https://www.jspsych.org/latest/overview/events/#on_timeline_finish */
|
|
116
|
+
on_timeline_finish?: () => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const timelineDescriptionKeys = [
|
|
120
|
+
"timeline",
|
|
121
|
+
"timeline_variables",
|
|
122
|
+
"name",
|
|
123
|
+
"repetitions",
|
|
124
|
+
"loop_function",
|
|
125
|
+
"conditional_function",
|
|
126
|
+
"randomize_order",
|
|
127
|
+
"sample",
|
|
128
|
+
"on_timeline_start",
|
|
129
|
+
"on_timeline_finish",
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
export function isTrialDescription(
|
|
133
|
+
description: TrialDescription | TimelineDescription
|
|
134
|
+
): description is TrialDescription {
|
|
135
|
+
return !isTimelineDescription(description);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function isTimelineDescription(
|
|
139
|
+
description: TrialDescription | TimelineDescription | TimelineArray
|
|
140
|
+
): description is TimelineDescription | TimelineArray {
|
|
141
|
+
return Boolean((description as TimelineDescription).timeline) || Array.isArray(description);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export enum TimelineNodeStatus {
|
|
145
|
+
PENDING,
|
|
146
|
+
RUNNING,
|
|
147
|
+
PAUSED,
|
|
148
|
+
COMPLETED,
|
|
149
|
+
ABORTED,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Functions and options needed by `TimelineNode`s, provided by the `JsPsych` instance. This
|
|
154
|
+
* approach allows to keep the public `JsPsych` API slim and decouples the `JsPsych` and timeline
|
|
155
|
+
* node classes, simplifying unit testing.
|
|
156
|
+
*/
|
|
157
|
+
export interface TimelineNodeDependencies {
|
|
158
|
+
/**
|
|
159
|
+
* Called at the start of a trial, prior to invoking the plugin's trial method.
|
|
160
|
+
*/
|
|
161
|
+
onTrialStart: (trial: Trial) => void;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Called when a trial's result data is available, before invoking `onTrialFinished()`.
|
|
165
|
+
*/
|
|
166
|
+
onTrialResultAvailable: (trial: Trial) => void;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Called after a trial has finished.
|
|
170
|
+
*/
|
|
171
|
+
onTrialFinished: (trial: Trial) => void;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Invoke `on_start` extension callbacks according to `extensionsConfiguration`
|
|
175
|
+
*/
|
|
176
|
+
runOnStartExtensionCallbacks(extensionsConfiguration: TrialExtensionsConfiguration): void;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Invoke `on_load` extension callbacks according to `extensionsConfiguration`
|
|
180
|
+
*/
|
|
181
|
+
runOnLoadExtensionCallbacks(extensionsConfiguration: TrialExtensionsConfiguration): void;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Invoke `on_finish` extension callbacks according to `extensionsConfiguration` and return a
|
|
185
|
+
* joint extensions result object
|
|
186
|
+
*/
|
|
187
|
+
runOnFinishExtensionCallbacks(
|
|
188
|
+
extensionsConfiguration: TrialExtensionsConfiguration
|
|
189
|
+
): Promise<Record<string, any>>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Returns the simulation mode or `undefined`, if the experiment is not running in simulation
|
|
193
|
+
* mode.
|
|
194
|
+
*/
|
|
195
|
+
getSimulationMode(): SimulationMode | undefined;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns the global simulation options as passed to `jsPsych.simulate()`
|
|
199
|
+
*/
|
|
200
|
+
getGlobalSimulationOptions(): Record<string, SimulationOptionsParameter>;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Given a plugin class, create a new instance of it and return it.
|
|
204
|
+
*/
|
|
205
|
+
instantiatePlugin: <Info extends PluginInfo>(
|
|
206
|
+
pluginClass: Class<JsPsychPlugin<Info>>
|
|
207
|
+
) => JsPsychPlugin<Info>;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Return JsPsych's display element so it can be provided to plugins
|
|
211
|
+
*/
|
|
212
|
+
getDisplayElement: () => HTMLElement;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Return the default inter-trial interval as provided to `initJsPsych()`
|
|
216
|
+
*/
|
|
217
|
+
getDefaultIti: () => number;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A `PromiseWrapper` whose promise is resolved with result data whenever `jsPsych.finishTrial()`
|
|
221
|
+
* is called.
|
|
222
|
+
*/
|
|
223
|
+
finishTrialPromise: PromiseWrapper<TrialResult | void>;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Clear all of the timeouts
|
|
227
|
+
*/
|
|
228
|
+
clearAllTimeouts: () => void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export type TrialResult = Record<string, any>;
|
|
232
|
+
export type TrialResults = Array<Record<string, any>>;
|