jspsych 7.3.4 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/css/jspsych.css +18 -8
- package/dist/index.browser.js +3097 -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 +2331 -4066
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +984 -6
- package/dist/index.js +2330 -4066
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/ExtensionManager.spec.ts +121 -0
- package/src/ExtensionManager.ts +100 -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
package/src/JsPsych.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import autoBind from "auto-bind";
|
|
2
2
|
|
|
3
3
|
import { version } from "../package.json";
|
|
4
|
-
import {
|
|
5
|
-
import { JsPsychData } from "./modules/data";
|
|
4
|
+
import { ExtensionManager, ExtensionManagerDependencies } from "./ExtensionManager";
|
|
5
|
+
import { JsPsychData, JsPsychDataDependencies } from "./modules/data";
|
|
6
6
|
import { PluginAPI, createJointPluginAPIObject } from "./modules/plugin-api";
|
|
7
|
-
import { ParameterType, universalPluginParameters } from "./modules/plugins";
|
|
8
7
|
import * as randomization from "./modules/randomization";
|
|
9
8
|
import * as turk from "./modules/turk";
|
|
10
9
|
import * as utils from "./modules/utils";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
import { ProgressBar } from "./ProgressBar";
|
|
11
|
+
import {
|
|
12
|
+
SimulationMode,
|
|
13
|
+
SimulationOptionsParameter,
|
|
14
|
+
TimelineArray,
|
|
15
|
+
TimelineDescription,
|
|
16
|
+
TimelineNodeDependencies,
|
|
17
|
+
TimelineVariable,
|
|
18
|
+
TrialResult,
|
|
19
|
+
} from "./timeline";
|
|
20
|
+
import { Timeline } from "./timeline/Timeline";
|
|
21
|
+
import { Trial } from "./timeline/Trial";
|
|
22
|
+
import { PromiseWrapper } from "./timeline/util";
|
|
16
23
|
|
|
17
24
|
export class JsPsych {
|
|
18
|
-
extensions = <any>{};
|
|
19
25
|
turk = turk;
|
|
20
26
|
randomization = randomization;
|
|
21
27
|
utils = utils;
|
|
@@ -26,75 +32,32 @@ export class JsPsych {
|
|
|
26
32
|
return version;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
35
|
+
/** Options */
|
|
36
|
+
private options: any = {};
|
|
32
37
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
*/
|
|
36
|
-
private opts: any = {};
|
|
38
|
+
/** Experiment timeline */
|
|
39
|
+
private timeline?: Timeline;
|
|
37
40
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
private timeline: TimelineNode;
|
|
42
|
-
private timelineDescription: any[];
|
|
41
|
+
/** Target DOM element */
|
|
42
|
+
private displayContainerElement: HTMLElement;
|
|
43
|
+
private displayElement: HTMLElement;
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
private current_trial: any = {};
|
|
47
|
-
private current_trial_finished = false;
|
|
48
|
-
|
|
49
|
-
// target DOM element
|
|
50
|
-
private DOM_container: HTMLElement;
|
|
51
|
-
private DOM_target: HTMLElement;
|
|
45
|
+
/** Time that the experiment began */
|
|
46
|
+
private experimentStartTime: Date;
|
|
52
47
|
|
|
53
48
|
/**
|
|
54
|
-
*
|
|
49
|
+
* Whether the page is retrieved directly via the `file://` protocol (true) or hosted on a web
|
|
50
|
+
* server (false)
|
|
55
51
|
*/
|
|
56
|
-
private
|
|
52
|
+
private isFileProtocolUsed = false;
|
|
57
53
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
*/
|
|
61
|
-
private paused = false;
|
|
62
|
-
private waiting = false;
|
|
54
|
+
/** The simulation mode (if the experiment is being simulated) */
|
|
55
|
+
private simulationMode?: SimulationMode;
|
|
63
56
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
*/
|
|
67
|
-
private file_protocol = false;
|
|
57
|
+
/** Simulation options passed in via `simulate()` */
|
|
58
|
+
private simulationOptions: Record<string, SimulationOptionsParameter>;
|
|
68
59
|
|
|
69
|
-
|
|
70
|
-
* Promise that is resolved when `finishExperiment()` is called
|
|
71
|
-
*/
|
|
72
|
-
private finished: Promise<void>;
|
|
73
|
-
private resolveFinishedPromise: () => void;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* is the experiment running in `simulate()` mode
|
|
77
|
-
*/
|
|
78
|
-
private simulation_mode: "data-only" | "visual" = null;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* simulation options passed in via `simulate()`
|
|
82
|
-
*/
|
|
83
|
-
private simulation_options;
|
|
84
|
-
|
|
85
|
-
// storing a single webaudio context to prevent problems with multiple inits
|
|
86
|
-
// of jsPsych
|
|
87
|
-
webaudio_context: AudioContext = null;
|
|
88
|
-
|
|
89
|
-
internal = {
|
|
90
|
-
/**
|
|
91
|
-
* this flag is used to determine whether we are in a scope where
|
|
92
|
-
* jsPsych.timelineVariable() should be executed immediately or
|
|
93
|
-
* whether it should return a function to access the variable later.
|
|
94
|
-
*
|
|
95
|
-
**/
|
|
96
|
-
call_immediate: false,
|
|
97
|
-
};
|
|
60
|
+
private extensionManager: ExtensionManager;
|
|
98
61
|
|
|
99
62
|
constructor(options?) {
|
|
100
63
|
// override default options if user specifies an option
|
|
@@ -107,7 +70,6 @@ export class JsPsych {
|
|
|
107
70
|
on_interaction_data_update: () => {},
|
|
108
71
|
on_close: () => {},
|
|
109
72
|
use_webaudio: true,
|
|
110
|
-
exclusions: {},
|
|
111
73
|
show_progress_bar: false,
|
|
112
74
|
message_progress_bar: "Completion Progress",
|
|
113
75
|
auto_update_progress_bar: true,
|
|
@@ -119,22 +81,18 @@ export class JsPsych {
|
|
|
119
81
|
extensions: [],
|
|
120
82
|
...options,
|
|
121
83
|
};
|
|
122
|
-
this.
|
|
84
|
+
this.options = options;
|
|
123
85
|
|
|
124
86
|
autoBind(this); // so we can pass JsPsych methods as callbacks and `this` remains the JsPsych instance
|
|
125
87
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
? new AudioContext()
|
|
129
|
-
: null;
|
|
130
|
-
|
|
131
|
-
// detect whether page is running in browser as a local file, and if so, disable web audio and video preloading to prevent CORS issues
|
|
88
|
+
// detect whether page is running in browser as a local file, and if so, disable web audio and
|
|
89
|
+
// video preloading to prevent CORS issues
|
|
132
90
|
if (
|
|
133
91
|
window.location.protocol == "file:" &&
|
|
134
92
|
(options.override_safe_mode === false || typeof options.override_safe_mode === "undefined")
|
|
135
93
|
) {
|
|
136
94
|
options.use_webaudio = false;
|
|
137
|
-
this.
|
|
95
|
+
this.isFileProtocolUsed = true;
|
|
138
96
|
console.warn(
|
|
139
97
|
"jsPsych detected that it is running via the file:// protocol and not on a web server. " +
|
|
140
98
|
"To prevent issues with cross-origin requests, Web Audio and video preloading have been disabled. " +
|
|
@@ -144,27 +102,26 @@ export class JsPsych {
|
|
|
144
102
|
}
|
|
145
103
|
|
|
146
104
|
// initialize modules
|
|
147
|
-
this.data = new JsPsychData(this);
|
|
105
|
+
this.data = new JsPsychData(this.dataDependencies);
|
|
148
106
|
this.pluginAPI = createJointPluginAPIObject(this);
|
|
149
107
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// initialize audio context based on options and browser capabilities
|
|
156
|
-
this.pluginAPI.initAudio();
|
|
108
|
+
this.extensionManager = new ExtensionManager(
|
|
109
|
+
this.extensionManagerDependencies,
|
|
110
|
+
options.extensions
|
|
111
|
+
);
|
|
157
112
|
}
|
|
158
113
|
|
|
114
|
+
private endMessage?: string;
|
|
115
|
+
|
|
159
116
|
/**
|
|
160
117
|
* Starts an experiment using the provided timeline and returns a promise that is resolved when
|
|
161
118
|
* the experiment is finished.
|
|
162
119
|
*
|
|
163
120
|
* @param timeline The timeline to be run
|
|
164
121
|
*/
|
|
165
|
-
async run(timeline:
|
|
122
|
+
async run(timeline: TimelineDescription | TimelineArray) {
|
|
166
123
|
if (typeof timeline === "undefined") {
|
|
167
|
-
console.error("No timeline declared in jsPsych.run. Cannot start experiment.");
|
|
124
|
+
console.error("No timeline declared in jsPsych.run(). Cannot start experiment.");
|
|
168
125
|
}
|
|
169
126
|
|
|
170
127
|
if (timeline.length === 0) {
|
|
@@ -174,17 +131,23 @@ export class JsPsych {
|
|
|
174
131
|
}
|
|
175
132
|
|
|
176
133
|
// create experiment timeline
|
|
177
|
-
this.
|
|
178
|
-
this.timeline = new TimelineNode(this, { timeline });
|
|
134
|
+
this.timeline = new Timeline(this.timelineDependencies, timeline);
|
|
179
135
|
|
|
180
136
|
await this.prepareDom();
|
|
181
|
-
await this.
|
|
182
|
-
await this.loadExtensions(this.opts.extensions);
|
|
137
|
+
await this.extensionManager.initializeExtensions();
|
|
183
138
|
|
|
184
139
|
document.documentElement.setAttribute("jspsych", "present");
|
|
185
140
|
|
|
186
|
-
this.
|
|
187
|
-
|
|
141
|
+
this.experimentStartTime = new Date();
|
|
142
|
+
|
|
143
|
+
await this.timeline.run();
|
|
144
|
+
await Promise.resolve(this.options.on_finish(this.data.get()));
|
|
145
|
+
|
|
146
|
+
if (this.endMessage) {
|
|
147
|
+
this.getDisplayElement().innerHTML = this.endMessage;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.data.removeInteractionListeners();
|
|
188
151
|
}
|
|
189
152
|
|
|
190
153
|
async simulate(
|
|
@@ -192,220 +155,110 @@ export class JsPsych {
|
|
|
192
155
|
simulation_mode: "data-only" | "visual" = "data-only",
|
|
193
156
|
simulation_options = {}
|
|
194
157
|
) {
|
|
195
|
-
this.
|
|
196
|
-
this.
|
|
158
|
+
this.simulationMode = simulation_mode;
|
|
159
|
+
this.simulationOptions = simulation_options;
|
|
197
160
|
await this.run(timeline);
|
|
198
161
|
}
|
|
199
162
|
|
|
163
|
+
public progressBar?: ProgressBar;
|
|
164
|
+
|
|
200
165
|
getProgress() {
|
|
201
166
|
return {
|
|
202
|
-
total_trials:
|
|
203
|
-
current_trial_global: this.
|
|
204
|
-
percent_complete:
|
|
167
|
+
total_trials: this.timeline?.getNaiveTrialCount(),
|
|
168
|
+
current_trial_global: this.timeline?.getLatestNode().index ?? 0,
|
|
169
|
+
percent_complete: this.timeline?.getNaiveProgress() * 100,
|
|
205
170
|
};
|
|
206
171
|
}
|
|
207
172
|
|
|
208
173
|
getStartTime() {
|
|
209
|
-
return this.
|
|
174
|
+
return this.experimentStartTime; // TODO This seems inconsistent, given that `getTotalTime()` returns a number, not a `Date`
|
|
210
175
|
}
|
|
211
176
|
|
|
212
177
|
getTotalTime() {
|
|
213
|
-
if (
|
|
178
|
+
if (!this.experimentStartTime) {
|
|
214
179
|
return 0;
|
|
215
180
|
}
|
|
216
|
-
return new Date().getTime() - this.
|
|
181
|
+
return new Date().getTime() - this.experimentStartTime.getTime();
|
|
217
182
|
}
|
|
218
183
|
|
|
219
184
|
getDisplayElement() {
|
|
220
|
-
return this.
|
|
185
|
+
return this.displayElement;
|
|
221
186
|
}
|
|
222
187
|
|
|
223
188
|
getDisplayContainerElement() {
|
|
224
|
-
return this.
|
|
189
|
+
return this.displayContainerElement;
|
|
225
190
|
}
|
|
226
191
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
this.current_trial_finished = true;
|
|
232
|
-
|
|
233
|
-
// remove any CSS classes that were added to the DOM via css_classes parameter
|
|
234
|
-
if (
|
|
235
|
-
typeof this.current_trial.css_classes !== "undefined" &&
|
|
236
|
-
Array.isArray(this.current_trial.css_classes)
|
|
237
|
-
) {
|
|
238
|
-
this.DOM_target.classList.remove(...this.current_trial.css_classes);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// write the data from the trial
|
|
242
|
-
this.data.write(data);
|
|
243
|
-
|
|
244
|
-
// get back the data with all of the defaults in
|
|
245
|
-
const trial_data = this.data.getLastTrialData();
|
|
246
|
-
|
|
247
|
-
// for trial-level callbacks, we just want to pass in a reference to the values
|
|
248
|
-
// of the DataCollection, for easy access and editing.
|
|
249
|
-
const trial_data_values = trial_data.values()[0];
|
|
250
|
-
|
|
251
|
-
const current_trial = this.current_trial;
|
|
252
|
-
|
|
253
|
-
if (typeof current_trial.save_trial_parameters === "object") {
|
|
254
|
-
for (const key of Object.keys(current_trial.save_trial_parameters)) {
|
|
255
|
-
const key_val = current_trial.save_trial_parameters[key];
|
|
256
|
-
if (key_val === true) {
|
|
257
|
-
if (typeof current_trial[key] === "undefined") {
|
|
258
|
-
console.warn(
|
|
259
|
-
`Invalid parameter specified in save_trial_parameters. Trial has no property called "${key}".`
|
|
260
|
-
);
|
|
261
|
-
} else if (typeof current_trial[key] === "function") {
|
|
262
|
-
trial_data_values[key] = current_trial[key].toString();
|
|
263
|
-
} else {
|
|
264
|
-
trial_data_values[key] = current_trial[key];
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (key_val === false) {
|
|
268
|
-
// we don't allow internal_node_id or trial_index to be deleted because it would break other things
|
|
269
|
-
if (key !== "internal_node_id" && key !== "trial_index") {
|
|
270
|
-
delete trial_data_values[key];
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// handle extension callbacks
|
|
277
|
-
|
|
278
|
-
const extensionCallbackResults = ((current_trial.extensions ?? []) as any[]).map((extension) =>
|
|
279
|
-
this.extensions[extension.type.info.name].on_finish(extension.params)
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
const onExtensionCallbacksFinished = () => {
|
|
283
|
-
// about to execute lots of callbacks, so switch context.
|
|
284
|
-
this.internal.call_immediate = true;
|
|
285
|
-
|
|
286
|
-
// handle callback at plugin level
|
|
287
|
-
if (typeof current_trial.on_finish === "function") {
|
|
288
|
-
current_trial.on_finish(trial_data_values);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// handle callback at whole-experiment level
|
|
292
|
-
this.opts.on_trial_finish(trial_data_values);
|
|
293
|
-
|
|
294
|
-
// after the above callbacks are complete, then the data should be finalized
|
|
295
|
-
// for this trial. call the on_data_update handler, passing in the same
|
|
296
|
-
// data object that just went through the trial's finish handlers.
|
|
297
|
-
this.opts.on_data_update(trial_data_values);
|
|
298
|
-
|
|
299
|
-
// done with callbacks
|
|
300
|
-
this.internal.call_immediate = false;
|
|
301
|
-
|
|
302
|
-
// wait for iti
|
|
303
|
-
if (this.simulation_mode === "data-only") {
|
|
304
|
-
this.nextTrial();
|
|
305
|
-
} else if (
|
|
306
|
-
typeof current_trial.post_trial_gap === null ||
|
|
307
|
-
typeof current_trial.post_trial_gap === "undefined"
|
|
308
|
-
) {
|
|
309
|
-
if (this.opts.default_iti > 0) {
|
|
310
|
-
setTimeout(this.nextTrial, this.opts.default_iti);
|
|
311
|
-
} else {
|
|
312
|
-
this.nextTrial();
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
if (current_trial.post_trial_gap > 0) {
|
|
316
|
-
setTimeout(this.nextTrial, current_trial.post_trial_gap);
|
|
317
|
-
} else {
|
|
318
|
-
this.nextTrial();
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
// Strictly using Promise.resolve to turn all values into promises would be cleaner here, but it
|
|
324
|
-
// would require user test code to make the event loop tick after every simulated key press even
|
|
325
|
-
// if there are no async `on_finish` methods. Hence, in order to avoid a breaking change, we
|
|
326
|
-
// only rely on the event loop if at least one `on_finish` method returns a promise.
|
|
327
|
-
if (extensionCallbackResults.some((result) => typeof result.then === "function")) {
|
|
328
|
-
Promise.all(
|
|
329
|
-
extensionCallbackResults.map((result) =>
|
|
330
|
-
Promise.resolve(result).then((ext_data_values) => {
|
|
331
|
-
Object.assign(trial_data_values, ext_data_values);
|
|
332
|
-
})
|
|
333
|
-
)
|
|
334
|
-
).then(onExtensionCallbacksFinished);
|
|
335
|
-
} else {
|
|
336
|
-
for (const values of extensionCallbackResults) {
|
|
337
|
-
Object.assign(trial_data_values, values);
|
|
338
|
-
}
|
|
339
|
-
onExtensionCallbacksFinished();
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
endExperiment(end_message = "", data = {}) {
|
|
344
|
-
this.timeline.end_message = end_message;
|
|
345
|
-
this.timeline.end();
|
|
192
|
+
abortExperiment(endMessage?: string, data = {}) {
|
|
193
|
+
this.endMessage = endMessage;
|
|
194
|
+
this.timeline.abort();
|
|
346
195
|
this.pluginAPI.cancelAllKeyboardResponses();
|
|
347
196
|
this.pluginAPI.clearAllTimeouts();
|
|
348
197
|
this.finishTrial(data);
|
|
349
198
|
}
|
|
350
199
|
|
|
351
|
-
|
|
352
|
-
this.timeline
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
200
|
+
abortCurrentTimeline() {
|
|
201
|
+
let currentTimeline = this.timeline?.getLatestNode();
|
|
202
|
+
if (currentTimeline instanceof Trial) {
|
|
203
|
+
currentTimeline = currentTimeline.parent;
|
|
204
|
+
}
|
|
205
|
+
if (currentTimeline instanceof Timeline) {
|
|
206
|
+
currentTimeline.abort();
|
|
207
|
+
}
|
|
357
208
|
}
|
|
358
209
|
|
|
359
|
-
|
|
360
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Aborts a named timeline. The timeline must be currently running in order to abort it.
|
|
212
|
+
*
|
|
213
|
+
* @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.
|
|
214
|
+
*/
|
|
215
|
+
abortTimelineByName(name: string): void {
|
|
216
|
+
const timeline = this.timeline?.getActiveTimelineByName(name);
|
|
217
|
+
if (timeline) {
|
|
218
|
+
timeline.abort();
|
|
219
|
+
}
|
|
361
220
|
}
|
|
362
221
|
|
|
363
|
-
|
|
364
|
-
|
|
222
|
+
getCurrentTrial() {
|
|
223
|
+
const activeNode = this.timeline?.getLatestNode();
|
|
224
|
+
if (activeNode instanceof Trial) {
|
|
225
|
+
return activeNode.description;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
365
228
|
}
|
|
366
229
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return this.timeline.timelineVariable(varname);
|
|
370
|
-
} else {
|
|
371
|
-
return {
|
|
372
|
-
timelineVariablePlaceholder: true,
|
|
373
|
-
timelineVariableFunction: () => this.timeline.timelineVariable(varname),
|
|
374
|
-
};
|
|
375
|
-
}
|
|
230
|
+
getInitSettings() {
|
|
231
|
+
return this.options;
|
|
376
232
|
}
|
|
377
233
|
|
|
378
|
-
|
|
379
|
-
return
|
|
234
|
+
timelineVariable(variableName: string) {
|
|
235
|
+
return new TimelineVariable(variableName);
|
|
380
236
|
}
|
|
381
237
|
|
|
382
|
-
|
|
383
|
-
this.timeline
|
|
238
|
+
evaluateTimelineVariable(variableName: string) {
|
|
239
|
+
return this.timeline
|
|
240
|
+
?.getLatestNode()
|
|
241
|
+
?.evaluateTimelineVariable(new TimelineVariable(variableName));
|
|
384
242
|
}
|
|
385
243
|
|
|
386
244
|
pauseExperiment() {
|
|
387
|
-
this.
|
|
245
|
+
this.timeline?.pause();
|
|
388
246
|
}
|
|
389
247
|
|
|
390
248
|
resumeExperiment() {
|
|
391
|
-
this.
|
|
392
|
-
if (this.waiting) {
|
|
393
|
-
this.waiting = false;
|
|
394
|
-
this.nextTrial();
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
loadFail(message) {
|
|
399
|
-
message = message || "<p>The experiment failed to load.</p>";
|
|
400
|
-
this.DOM_target.innerHTML = message;
|
|
249
|
+
this.timeline?.resume();
|
|
401
250
|
}
|
|
402
251
|
|
|
403
252
|
getSafeModeStatus() {
|
|
404
|
-
return this.
|
|
253
|
+
return this.isFileProtocolUsed;
|
|
405
254
|
}
|
|
406
255
|
|
|
407
256
|
getTimeline() {
|
|
408
|
-
return this.
|
|
257
|
+
return this.timeline?.description.timeline;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
get extensions() {
|
|
261
|
+
return this.extensionManager?.extensions ?? {};
|
|
409
262
|
}
|
|
410
263
|
|
|
411
264
|
private async prepareDom() {
|
|
@@ -416,24 +269,25 @@ export class JsPsych {
|
|
|
416
269
|
});
|
|
417
270
|
}
|
|
418
271
|
|
|
419
|
-
const options = this.
|
|
272
|
+
const options = this.options;
|
|
420
273
|
|
|
421
274
|
// set DOM element where jsPsych will render content
|
|
422
275
|
// if undefined, then jsPsych will use the <body> tag and the entire page
|
|
423
276
|
if (typeof options.display_element === "undefined") {
|
|
424
277
|
// check if there is a body element on the page
|
|
425
|
-
|
|
426
|
-
if (body
|
|
427
|
-
document.
|
|
278
|
+
let body = document.body;
|
|
279
|
+
if (!body) {
|
|
280
|
+
body = document.createElement("body");
|
|
281
|
+
document.documentElement.appendChild(body);
|
|
428
282
|
}
|
|
429
|
-
// using the full page, so we need the HTML element to
|
|
430
|
-
//
|
|
431
|
-
// no margin
|
|
283
|
+
// using the full page, so we need the HTML element to have 100% height, and body to be full
|
|
284
|
+
// width and height with no margin
|
|
432
285
|
document.querySelector("html").style.height = "100%";
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
286
|
+
|
|
287
|
+
body.style.margin = "0px";
|
|
288
|
+
body.style.height = "100%";
|
|
289
|
+
body.style.width = "100%";
|
|
290
|
+
options.display_element = body;
|
|
437
291
|
} else {
|
|
438
292
|
// make sure that the display element exists on the page
|
|
439
293
|
const display =
|
|
@@ -447,468 +301,119 @@ export class JsPsych {
|
|
|
447
301
|
}
|
|
448
302
|
}
|
|
449
303
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
304
|
+
const contentElement = document.createElement("div");
|
|
305
|
+
contentElement.id = "jspsych-content";
|
|
306
|
+
|
|
307
|
+
const contentWrapperElement = document.createElement("div");
|
|
308
|
+
contentWrapperElement.className = "jspsych-content-wrapper";
|
|
309
|
+
contentWrapperElement.appendChild(contentElement);
|
|
310
|
+
|
|
311
|
+
this.displayContainerElement = options.display_element;
|
|
312
|
+
this.displayContainerElement.appendChild(contentWrapperElement);
|
|
313
|
+
this.displayElement = contentElement;
|
|
454
314
|
|
|
455
315
|
// set experiment_width if not null
|
|
456
316
|
if (options.experiment_width !== null) {
|
|
457
|
-
this.
|
|
317
|
+
this.displayElement.style.width = options.experiment_width + "px";
|
|
458
318
|
}
|
|
459
319
|
|
|
460
320
|
// add tabIndex attribute to scope event listeners
|
|
461
321
|
options.display_element.tabIndex = 0;
|
|
462
322
|
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
this.DOM_target.className += "jspsych-content";
|
|
323
|
+
// Add CSS classes to container and display elements
|
|
324
|
+
this.displayContainerElement.classList.add("jspsych-display-element");
|
|
325
|
+
this.displayElement.classList.add("jspsych-content");
|
|
468
326
|
|
|
469
327
|
// create listeners for user browser interaction
|
|
470
328
|
this.data.createInteractionListeners();
|
|
471
329
|
|
|
472
330
|
// add event for closing window
|
|
473
331
|
window.addEventListener("beforeunload", options.on_close);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
private async loadExtensions(extensions) {
|
|
477
|
-
// run the .initialize method of any extensions that are in use
|
|
478
|
-
// these should return a Promise to indicate when loading is complete
|
|
479
|
-
|
|
480
|
-
try {
|
|
481
|
-
await Promise.all(
|
|
482
|
-
extensions.map((extension) =>
|
|
483
|
-
this.extensions[extension.type.info.name].initialize(extension.params || {})
|
|
484
|
-
)
|
|
485
|
-
);
|
|
486
|
-
} catch (error_message) {
|
|
487
|
-
console.error(error_message);
|
|
488
|
-
throw new Error(error_message);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
private startExperiment() {
|
|
493
|
-
this.finished = new Promise((resolve) => {
|
|
494
|
-
this.resolveFinishedPromise = resolve;
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// show progress bar if requested
|
|
498
|
-
if (this.opts.show_progress_bar === true) {
|
|
499
|
-
this.drawProgressBar(this.opts.message_progress_bar);
|
|
500
|
-
}
|
|
501
332
|
|
|
502
|
-
|
|
503
|
-
|
|
333
|
+
if (this.options.show_progress_bar) {
|
|
334
|
+
const progressBarContainer = document.createElement("div");
|
|
335
|
+
progressBarContainer.id = "jspsych-progressbar-container";
|
|
504
336
|
|
|
505
|
-
|
|
506
|
-
this.timeline.advance();
|
|
507
|
-
this.doTrial(this.timeline.trial());
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
private finishExperiment() {
|
|
511
|
-
const finish_result = this.opts.on_finish(this.data.get());
|
|
512
|
-
|
|
513
|
-
const done_handler = () => {
|
|
514
|
-
if (typeof this.timeline.end_message !== "undefined") {
|
|
515
|
-
this.DOM_target.innerHTML = this.timeline.end_message;
|
|
516
|
-
}
|
|
517
|
-
this.resolveFinishedPromise();
|
|
518
|
-
};
|
|
337
|
+
this.progressBar = new ProgressBar(progressBarContainer, this.options.message_progress_bar);
|
|
519
338
|
|
|
520
|
-
|
|
521
|
-
Promise.resolve(finish_result).then(done_handler);
|
|
522
|
-
} else {
|
|
523
|
-
done_handler();
|
|
339
|
+
this.getDisplayContainerElement().insertAdjacentElement("afterbegin", progressBarContainer);
|
|
524
340
|
}
|
|
525
341
|
}
|
|
526
342
|
|
|
527
|
-
private
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
this.waiting = true;
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
this.global_trial_index++;
|
|
535
|
-
|
|
536
|
-
// advance timeline
|
|
537
|
-
this.timeline.markCurrentTrialComplete();
|
|
538
|
-
const complete = this.timeline.advance();
|
|
539
|
-
|
|
540
|
-
// update progress bar if shown
|
|
541
|
-
if (this.opts.show_progress_bar === true && this.opts.auto_update_progress_bar === true) {
|
|
542
|
-
this.updateProgressBar();
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// check if experiment is over
|
|
546
|
-
if (complete) {
|
|
547
|
-
this.finishExperiment();
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
this.doTrial(this.timeline.trial());
|
|
343
|
+
private finishTrialPromise = new PromiseWrapper<TrialResult | void>();
|
|
344
|
+
finishTrial(data?: TrialResult) {
|
|
345
|
+
this.finishTrialPromise.resolve(data);
|
|
552
346
|
}
|
|
553
347
|
|
|
554
|
-
private
|
|
555
|
-
|
|
556
|
-
|
|
348
|
+
private timelineDependencies: TimelineNodeDependencies = {
|
|
349
|
+
onTrialStart: (trial: Trial) => {
|
|
350
|
+
this.options.on_trial_start(trial.trialObject);
|
|
557
351
|
|
|
558
|
-
|
|
559
|
-
|
|
352
|
+
// apply the focus to the element containing the experiment.
|
|
353
|
+
this.getDisplayContainerElement().focus();
|
|
354
|
+
// reset the scroll on the DOM target
|
|
355
|
+
this.getDisplayElement().scrollTop = 0;
|
|
356
|
+
},
|
|
560
357
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// instantiate the plugin for this trial
|
|
568
|
-
trial.type = {
|
|
569
|
-
// this is a hack to internally keep the old plugin object structure and prevent touching more
|
|
570
|
-
// of the core jspsych code
|
|
571
|
-
...autoBind(new trial.type(this)),
|
|
572
|
-
info: trial.type.info,
|
|
573
|
-
};
|
|
574
|
-
|
|
575
|
-
// evaluate variables that are functions
|
|
576
|
-
this.evaluateFunctionParameters(trial);
|
|
577
|
-
|
|
578
|
-
// get default values for parameters
|
|
579
|
-
this.setDefaultValues(trial);
|
|
580
|
-
|
|
581
|
-
// about to execute callbacks
|
|
582
|
-
this.internal.call_immediate = true;
|
|
583
|
-
|
|
584
|
-
// call experiment wide callback
|
|
585
|
-
this.opts.on_trial_start(trial);
|
|
586
|
-
|
|
587
|
-
// call trial specific callback if it exists
|
|
588
|
-
if (typeof trial.on_start === "function") {
|
|
589
|
-
trial.on_start(trial);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// call any on_start functions for extensions
|
|
593
|
-
if (Array.isArray(trial.extensions)) {
|
|
594
|
-
for (const extension of trial.extensions) {
|
|
595
|
-
this.extensions[extension.type.info.name].on_start(extension.params);
|
|
358
|
+
onTrialResultAvailable: (trial: Trial) => {
|
|
359
|
+
const result = trial.getResult();
|
|
360
|
+
if (result) {
|
|
361
|
+
result.time_elapsed = this.getTotalTime();
|
|
362
|
+
this.data.write(trial);
|
|
596
363
|
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// apply the focus to the element containing the experiment.
|
|
600
|
-
this.DOM_container.focus();
|
|
364
|
+
},
|
|
601
365
|
|
|
602
|
-
|
|
603
|
-
|
|
366
|
+
onTrialFinished: (trial: Trial) => {
|
|
367
|
+
const result = trial.getResult();
|
|
368
|
+
this.options.on_trial_finish(result);
|
|
604
369
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (!Array.isArray(trial.css_classes) && typeof trial.css_classes === "string") {
|
|
608
|
-
trial.css_classes = [trial.css_classes];
|
|
609
|
-
}
|
|
610
|
-
if (Array.isArray(trial.css_classes)) {
|
|
611
|
-
this.DOM_target.classList.add(...trial.css_classes);
|
|
370
|
+
if (result) {
|
|
371
|
+
this.options.on_data_update(result);
|
|
612
372
|
}
|
|
613
|
-
}
|
|
614
373
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (typeof trial.on_load === "function") {
|
|
618
|
-
trial.on_load();
|
|
374
|
+
if (this.progressBar && this.options.auto_update_progress_bar) {
|
|
375
|
+
this.progressBar.progress = this.timeline.getNaiveProgress();
|
|
619
376
|
}
|
|
377
|
+
},
|
|
620
378
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
for (const extension of trial.extensions) {
|
|
624
|
-
this.extensions[extension.type.info.name].on_load(extension.params);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
};
|
|
379
|
+
runOnStartExtensionCallbacks: (extensionsConfiguration) =>
|
|
380
|
+
this.extensionManager.onStart(extensionsConfiguration),
|
|
628
381
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
let trial_sim_opts_merged;
|
|
632
|
-
if (!this.simulation_mode) {
|
|
633
|
-
trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
|
|
634
|
-
}
|
|
635
|
-
if (this.simulation_mode) {
|
|
636
|
-
// check if the trial supports simulation
|
|
637
|
-
if (trial.type.simulate) {
|
|
638
|
-
if (!trial.simulation_options) {
|
|
639
|
-
trial_sim_opts = this.simulation_options.default;
|
|
640
|
-
}
|
|
641
|
-
if (trial.simulation_options) {
|
|
642
|
-
if (typeof trial.simulation_options == "string") {
|
|
643
|
-
if (this.simulation_options[trial.simulation_options]) {
|
|
644
|
-
trial_sim_opts = this.simulation_options[trial.simulation_options];
|
|
645
|
-
} else if (this.simulation_options.default) {
|
|
646
|
-
console.log(
|
|
647
|
-
`No matching simulation options found for "${trial.simulation_options}". Using "default" options.`
|
|
648
|
-
);
|
|
649
|
-
trial_sim_opts = this.simulation_options.default;
|
|
650
|
-
} else {
|
|
651
|
-
console.log(
|
|
652
|
-
`No matching simulation options found for "${trial.simulation_options}" and no "default" options provided. Using the default values provided by the plugin.`
|
|
653
|
-
);
|
|
654
|
-
trial_sim_opts = {};
|
|
655
|
-
}
|
|
656
|
-
} else {
|
|
657
|
-
trial_sim_opts = trial.simulation_options;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
// merge in default options that aren't overriden by the trial's simulation_options
|
|
661
|
-
// including nested parameters in the simulation_options
|
|
662
|
-
trial_sim_opts_merged = this.utils.deepMerge(
|
|
663
|
-
this.simulation_options.default,
|
|
664
|
-
trial_sim_opts
|
|
665
|
-
);
|
|
666
|
-
|
|
667
|
-
trial_sim_opts_merged = this.utils.deepCopy(trial_sim_opts_merged);
|
|
668
|
-
trial_sim_opts_merged = this.replaceFunctionsWithValues(trial_sim_opts_merged, null);
|
|
669
|
-
|
|
670
|
-
if (trial_sim_opts_merged?.simulate === false) {
|
|
671
|
-
trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
|
|
672
|
-
} else {
|
|
673
|
-
trial_complete = trial.type.simulate(
|
|
674
|
-
trial,
|
|
675
|
-
trial_sim_opts_merged?.mode || this.simulation_mode,
|
|
676
|
-
trial_sim_opts_merged,
|
|
677
|
-
load_callback
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
} else {
|
|
681
|
-
// trial doesn't have a simulate method, so just run as usual
|
|
682
|
-
trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
382
|
+
runOnLoadExtensionCallbacks: (extensionsConfiguration) =>
|
|
383
|
+
this.extensionManager.onLoad(extensionsConfiguration),
|
|
685
384
|
|
|
686
|
-
|
|
687
|
-
|
|
385
|
+
runOnFinishExtensionCallbacks: (extensionsConfiguration) =>
|
|
386
|
+
this.extensionManager.onFinish(extensionsConfiguration),
|
|
688
387
|
|
|
689
|
-
|
|
690
|
-
// so we don't need to call it here. however, if we are in simulation mode but not simulating
|
|
691
|
-
// this particular trial we need to call it.
|
|
692
|
-
if (
|
|
693
|
-
!is_promise &&
|
|
694
|
-
(!this.simulation_mode || (this.simulation_mode && trial_sim_opts_merged?.simulate === false))
|
|
695
|
-
) {
|
|
696
|
-
load_callback();
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// done with callbacks
|
|
700
|
-
this.internal.call_immediate = false;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
private evaluateTimelineVariables(trial) {
|
|
704
|
-
for (const key of Object.keys(trial)) {
|
|
705
|
-
if (
|
|
706
|
-
typeof trial[key] === "object" &&
|
|
707
|
-
trial[key] !== null &&
|
|
708
|
-
typeof trial[key].timelineVariablePlaceholder !== "undefined"
|
|
709
|
-
) {
|
|
710
|
-
trial[key] = trial[key].timelineVariableFunction();
|
|
711
|
-
}
|
|
712
|
-
// timeline variables that are nested in objects
|
|
713
|
-
if (
|
|
714
|
-
typeof trial[key] === "object" &&
|
|
715
|
-
trial[key] !== null &&
|
|
716
|
-
key !== "timeline" &&
|
|
717
|
-
key !== "timeline_variables"
|
|
718
|
-
) {
|
|
719
|
-
this.evaluateTimelineVariables(trial[key]);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
388
|
+
getSimulationMode: () => this.simulationMode,
|
|
723
389
|
|
|
724
|
-
|
|
725
|
-
// set a flag so that jsPsych.timelineVariable() is immediately executed in this context
|
|
726
|
-
this.internal.call_immediate = true;
|
|
727
|
-
|
|
728
|
-
// iterate over each parameter
|
|
729
|
-
for (const key of Object.keys(trial)) {
|
|
730
|
-
// check to make sure parameter is not "type", since that was eval'd above.
|
|
731
|
-
if (key !== "type") {
|
|
732
|
-
// this if statement is checking to see if the parameter type is expected to be a function, in which case we should NOT evaluate it.
|
|
733
|
-
// the first line checks if the parameter is defined in the universalPluginParameters set
|
|
734
|
-
// the second line checks the plugin-specific parameters
|
|
735
|
-
if (
|
|
736
|
-
typeof universalPluginParameters[key] !== "undefined" &&
|
|
737
|
-
universalPluginParameters[key].type !== ParameterType.FUNCTION
|
|
738
|
-
) {
|
|
739
|
-
trial[key] = this.replaceFunctionsWithValues(trial[key], null);
|
|
740
|
-
}
|
|
741
|
-
if (
|
|
742
|
-
typeof trial.type.info.parameters[key] !== "undefined" &&
|
|
743
|
-
trial.type.info.parameters[key].type !== ParameterType.FUNCTION
|
|
744
|
-
) {
|
|
745
|
-
trial[key] = this.replaceFunctionsWithValues(trial[key], trial.type.info.parameters[key]);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
// reset so jsPsych.timelineVariable() is no longer immediately executed
|
|
750
|
-
this.internal.call_immediate = false;
|
|
751
|
-
}
|
|
390
|
+
getGlobalSimulationOptions: () => this.simulationOptions,
|
|
752
391
|
|
|
753
|
-
|
|
754
|
-
// null typeof is 'object' (?!?!), so need to run this first!
|
|
755
|
-
if (obj === null) {
|
|
756
|
-
return obj;
|
|
757
|
-
}
|
|
758
|
-
// arrays
|
|
759
|
-
else if (Array.isArray(obj)) {
|
|
760
|
-
for (let i = 0; i < obj.length; i++) {
|
|
761
|
-
obj[i] = this.replaceFunctionsWithValues(obj[i], info);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
// objects
|
|
765
|
-
else if (typeof obj === "object") {
|
|
766
|
-
if (info === null || !info.nested) {
|
|
767
|
-
for (const key of Object.keys(obj)) {
|
|
768
|
-
if (key === "type" || key === "timeline" || key === "timeline_variables") {
|
|
769
|
-
// Ignore the object's `type` field because it contains a plugin and we do not want to
|
|
770
|
-
// call plugin functions. Also ignore `timeline` and `timeline_variables` because they
|
|
771
|
-
// are used in the `trials` parameter of the preload plugin and we don't want to actually
|
|
772
|
-
// evaluate those in that context.
|
|
773
|
-
continue;
|
|
774
|
-
}
|
|
775
|
-
obj[key] = this.replaceFunctionsWithValues(obj[key], null);
|
|
776
|
-
}
|
|
777
|
-
} else {
|
|
778
|
-
for (const key of Object.keys(obj)) {
|
|
779
|
-
if (
|
|
780
|
-
typeof info.nested[key] === "object" &&
|
|
781
|
-
info.nested[key].type !== ParameterType.FUNCTION
|
|
782
|
-
) {
|
|
783
|
-
obj[key] = this.replaceFunctionsWithValues(obj[key], info.nested[key]);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
} else if (typeof obj === "function") {
|
|
788
|
-
return obj();
|
|
789
|
-
}
|
|
790
|
-
return obj;
|
|
791
|
-
}
|
|
392
|
+
instantiatePlugin: (pluginClass) => new pluginClass(this),
|
|
792
393
|
|
|
793
|
-
|
|
794
|
-
for (const param in trial.type.info.parameters) {
|
|
795
|
-
// check if parameter is complex with nested defaults
|
|
796
|
-
if (trial.type.info.parameters[param].type === ParameterType.COMPLEX) {
|
|
797
|
-
// check if parameter is undefined and has a default value
|
|
798
|
-
if (typeof trial[param] === "undefined" && trial.type.info.parameters[param].default) {
|
|
799
|
-
trial[param] = trial.type.info.parameters[param].default;
|
|
800
|
-
}
|
|
801
|
-
// if parameter is an array, iterate over each entry after confirming that there are
|
|
802
|
-
// entries to iterate over. this is common when some parameters in a COMPLEX type have
|
|
803
|
-
// default values and others do not.
|
|
804
|
-
if (trial.type.info.parameters[param].array === true && Array.isArray(trial[param])) {
|
|
805
|
-
// iterate over each entry in the array
|
|
806
|
-
trial[param].forEach(function (ip, i) {
|
|
807
|
-
// check each parameter in the plugin description
|
|
808
|
-
for (const p in trial.type.info.parameters[param].nested) {
|
|
809
|
-
if (typeof trial[param][i][p] === "undefined" || trial[param][i][p] === null) {
|
|
810
|
-
if (typeof trial.type.info.parameters[param].nested[p].default === "undefined") {
|
|
811
|
-
console.error(
|
|
812
|
-
`You must specify a value for the ${p} parameter (nested in the ${param} parameter) in the ${trial.type.info.name} plugin.`
|
|
813
|
-
);
|
|
814
|
-
} else {
|
|
815
|
-
trial[param][i][p] = trial.type.info.parameters[param].nested[p].default;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
// if it's not nested, checking is much easier and do that here:
|
|
823
|
-
else if (typeof trial[param] === "undefined" || trial[param] === null) {
|
|
824
|
-
if (typeof trial.type.info.parameters[param].default === "undefined") {
|
|
825
|
-
console.error(
|
|
826
|
-
`You must specify a value for the ${param} parameter in the ${trial.type.info.name} plugin.`
|
|
827
|
-
);
|
|
828
|
-
} else {
|
|
829
|
-
trial[param] = trial.type.info.parameters[param].default;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
}
|
|
394
|
+
getDisplayElement: () => this.getDisplayElement(),
|
|
834
395
|
|
|
835
|
-
|
|
836
|
-
if (exclusions.min_width || exclusions.min_height || exclusions.audio) {
|
|
837
|
-
console.warn(
|
|
838
|
-
"The exclusions option in `initJsPsych()` is deprecated and will be removed in a future version. We recommend using the browser-check plugin instead. See https://www.jspsych.org/latest/plugins/browser-check/."
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
// MINIMUM SIZE
|
|
842
|
-
if (exclusions.min_width || exclusions.min_height) {
|
|
843
|
-
const mw = exclusions.min_width || 0;
|
|
844
|
-
const mh = exclusions.min_height || 0;
|
|
845
|
-
|
|
846
|
-
if (window.innerWidth < mw || window.innerHeight < mh) {
|
|
847
|
-
this.getDisplayElement().innerHTML =
|
|
848
|
-
"<p>Your browser window is too small to complete this experiment. " +
|
|
849
|
-
"Please maximize the size of your browser window. If your browser window is already maximized, " +
|
|
850
|
-
"you will not be able to complete this experiment.</p>" +
|
|
851
|
-
"<p>The minimum width is " +
|
|
852
|
-
mw +
|
|
853
|
-
"px. Your current width is " +
|
|
854
|
-
window.innerWidth +
|
|
855
|
-
"px.</p>" +
|
|
856
|
-
"<p>The minimum height is " +
|
|
857
|
-
mh +
|
|
858
|
-
"px. Your current height is " +
|
|
859
|
-
window.innerHeight +
|
|
860
|
-
"px.</p>";
|
|
861
|
-
|
|
862
|
-
// Wait for window size to increase
|
|
863
|
-
while (window.innerWidth < mw || window.innerHeight < mh) {
|
|
864
|
-
await delay(100);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
this.getDisplayElement().innerHTML = "";
|
|
868
|
-
}
|
|
869
|
-
}
|
|
396
|
+
getDefaultIti: () => this.getInitSettings().default_iti,
|
|
870
397
|
|
|
871
|
-
|
|
872
|
-
if (typeof exclusions.audio !== "undefined" && exclusions.audio) {
|
|
873
|
-
if (!window.hasOwnProperty("AudioContext") && !window.hasOwnProperty("webkitAudioContext")) {
|
|
874
|
-
this.getDisplayElement().innerHTML =
|
|
875
|
-
"<p>Your browser does not support the WebAudio API, which means that you will not " +
|
|
876
|
-
"be able to complete the experiment.</p><p>Browsers that support the WebAudio API include " +
|
|
877
|
-
"Chrome, Firefox, Safari, and Edge.</p>";
|
|
878
|
-
throw new Error();
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
398
|
+
finishTrialPromise: this.finishTrialPromise,
|
|
882
399
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
.querySelector(".jspsych-display-element")
|
|
886
|
-
.insertAdjacentHTML(
|
|
887
|
-
"afterbegin",
|
|
888
|
-
'<div id="jspsych-progressbar-container">' +
|
|
889
|
-
"<span>" +
|
|
890
|
-
msg +
|
|
891
|
-
"</span>" +
|
|
892
|
-
'<div id="jspsych-progressbar-outer">' +
|
|
893
|
-
'<div id="jspsych-progressbar-inner"></div>' +
|
|
894
|
-
"</div></div>"
|
|
895
|
-
);
|
|
896
|
-
}
|
|
400
|
+
clearAllTimeouts: () => this.pluginAPI.clearAllTimeouts(),
|
|
401
|
+
};
|
|
897
402
|
|
|
898
|
-
private
|
|
899
|
-
|
|
900
|
-
}
|
|
403
|
+
private extensionManagerDependencies: ExtensionManagerDependencies = {
|
|
404
|
+
instantiateExtension: (extensionClass) => new extensionClass(this),
|
|
405
|
+
};
|
|
901
406
|
|
|
902
|
-
private
|
|
407
|
+
private dataDependencies: JsPsychDataDependencies = {
|
|
408
|
+
getProgress: () => ({
|
|
409
|
+
time: this.getTotalTime(),
|
|
410
|
+
trial: this.timeline?.getLatestNode().index ?? 0,
|
|
411
|
+
}),
|
|
903
412
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
proportion_complete * 100 + "%";
|
|
908
|
-
this.progress_bar_amount = proportion_complete;
|
|
909
|
-
}
|
|
413
|
+
onInteractionRecordAdded: (record) => {
|
|
414
|
+
this.options.on_interaction_data_update(record);
|
|
415
|
+
},
|
|
910
416
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
417
|
+
getDisplayElement: () => this.getDisplayElement(),
|
|
418
|
+
};
|
|
914
419
|
}
|