jspsych 7.2.3 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.browser.js +82 -43
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +1 -1
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +82 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +82 -43
- package/dist/index.js.map +1 -1
- package/dist/modules/extensions.d.ts +1 -1
- package/dist/modules/plugin-api/MediaAPI.d.ts +6 -1
- package/package.json +1 -1
- package/src/JsPsych.ts +65 -47
- package/src/TimelineNode.ts +10 -2
- package/src/modules/extensions.ts +1 -1
- package/src/modules/plugin-api/MediaAPI.ts +21 -1
|
@@ -18,5 +18,5 @@ export interface JsPsychExtension {
|
|
|
18
18
|
* Called at the end of the trial.
|
|
19
19
|
* @returns Data to append to the trial's data object.
|
|
20
20
|
*/
|
|
21
|
-
on_finish(params?: Record<string, any>): Record<string, any
|
|
21
|
+
on_finish(params?: Record<string, any>): Record<string, any> | Promise<Record<string, any>>;
|
|
22
22
|
}
|
|
@@ -3,7 +3,7 @@ export declare class MediaAPI {
|
|
|
3
3
|
private webaudioContext?;
|
|
4
4
|
constructor(useWebaudio: boolean, webaudioContext?: AudioContext);
|
|
5
5
|
private video_buffers;
|
|
6
|
-
getVideoBuffer(videoID:
|
|
6
|
+
getVideoBuffer(videoID: string): any;
|
|
7
7
|
private context;
|
|
8
8
|
private audio_buffers;
|
|
9
9
|
initAudio(): void;
|
|
@@ -24,4 +24,9 @@ export declare class MediaAPI {
|
|
|
24
24
|
private microphone_recorder;
|
|
25
25
|
initializeMicrophoneRecorder(stream: MediaStream): void;
|
|
26
26
|
getMicrophoneRecorder(): MediaRecorder;
|
|
27
|
+
private camera_stream;
|
|
28
|
+
private camera_recorder;
|
|
29
|
+
initializeCameraRecorder(stream: MediaStream, opts?: MediaRecorderOptions): void;
|
|
30
|
+
getCameraStream(): MediaStream;
|
|
31
|
+
getCameraRecorder(): MediaRecorder;
|
|
27
32
|
}
|
package/package.json
CHANGED
package/src/JsPsych.ts
CHANGED
|
@@ -272,53 +272,71 @@ export class JsPsych {
|
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
|
+
|
|
275
276
|
// handle extension callbacks
|
|
276
|
-
if (Array.isArray(current_trial.extensions)) {
|
|
277
|
-
for (const extension of current_trial.extensions) {
|
|
278
|
-
const ext_data_values = this.extensions[extension.type.info.name].on_finish(
|
|
279
|
-
extension.params
|
|
280
|
-
);
|
|
281
|
-
Object.assign(trial_data_values, ext_data_values);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
277
|
|
|
285
|
-
|
|
286
|
-
|
|
278
|
+
const extensionCallbackResults = ((current_trial.extensions ?? []) as any[]).map((extension) =>
|
|
279
|
+
this.extensions[extension.type.info.name].on_finish(extension.params)
|
|
280
|
+
);
|
|
287
281
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
282
|
+
const onExtensionCallbacksFinished = () => {
|
|
283
|
+
// about to execute lots of callbacks, so switch context.
|
|
284
|
+
this.internal.call_immediate = true;
|
|
292
285
|
|
|
293
|
-
|
|
294
|
-
|
|
286
|
+
// handle callback at plugin level
|
|
287
|
+
if (typeof current_trial.on_finish === "function") {
|
|
288
|
+
current_trial.on_finish(trial_data_values);
|
|
289
|
+
}
|
|
295
290
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// data object that just went through the trial's finish handlers.
|
|
299
|
-
this.opts.on_data_update(trial_data_values);
|
|
291
|
+
// handle callback at whole-experiment level
|
|
292
|
+
this.opts.on_trial_finish(trial_data_values);
|
|
300
293
|
|
|
301
|
-
|
|
302
|
-
|
|
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);
|
|
303
298
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
typeof current_trial.post_trial_gap === "undefined"
|
|
310
|
-
) {
|
|
311
|
-
if (this.opts.default_iti > 0) {
|
|
312
|
-
setTimeout(this.nextTrial, this.opts.default_iti);
|
|
313
|
-
} else {
|
|
299
|
+
// done with callbacks
|
|
300
|
+
this.internal.call_immediate = false;
|
|
301
|
+
|
|
302
|
+
// wait for iti
|
|
303
|
+
if (this.simulation_mode === "data-only") {
|
|
314
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
|
+
}
|
|
315
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);
|
|
316
335
|
} else {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
} else {
|
|
320
|
-
this.nextTrial();
|
|
336
|
+
for (const values of extensionCallbackResults) {
|
|
337
|
+
Object.assign(trial_data_values, values);
|
|
321
338
|
}
|
|
339
|
+
onExtensionCallbacksFinished();
|
|
322
340
|
}
|
|
323
341
|
}
|
|
324
342
|
|
|
@@ -671,22 +689,20 @@ export class JsPsych {
|
|
|
671
689
|
|
|
672
690
|
private evaluateTimelineVariables(trial) {
|
|
673
691
|
for (const key of Object.keys(trial)) {
|
|
674
|
-
if (key === "type") {
|
|
675
|
-
// skip the `type` parameter as it contains a plugin
|
|
676
|
-
//continue;
|
|
677
|
-
}
|
|
678
|
-
// timeline variables on the root level
|
|
679
692
|
if (
|
|
680
693
|
typeof trial[key] === "object" &&
|
|
681
694
|
trial[key] !== null &&
|
|
682
695
|
typeof trial[key].timelineVariablePlaceholder !== "undefined"
|
|
683
696
|
) {
|
|
684
|
-
|
|
685
|
-
"function(){returntimeline.timelineVariable(varname);}"
|
|
686
|
-
)*/ trial[key] = trial[key].timelineVariableFunction();
|
|
697
|
+
trial[key] = trial[key].timelineVariableFunction();
|
|
687
698
|
}
|
|
688
699
|
// timeline variables that are nested in objects
|
|
689
|
-
if (
|
|
700
|
+
if (
|
|
701
|
+
typeof trial[key] === "object" &&
|
|
702
|
+
trial[key] !== null &&
|
|
703
|
+
key !== "timeline" &&
|
|
704
|
+
key !== "timeline_variables"
|
|
705
|
+
) {
|
|
690
706
|
this.evaluateTimelineVariables(trial[key]);
|
|
691
707
|
}
|
|
692
708
|
}
|
|
@@ -736,9 +752,11 @@ export class JsPsych {
|
|
|
736
752
|
else if (typeof obj === "object") {
|
|
737
753
|
if (info === null || !info.nested) {
|
|
738
754
|
for (const key of Object.keys(obj)) {
|
|
739
|
-
if (key === "type") {
|
|
755
|
+
if (key === "type" || key === "timeline" || key === "timeline_variables") {
|
|
740
756
|
// Ignore the object's `type` field because it contains a plugin and we do not want to
|
|
741
|
-
// call plugin functions
|
|
757
|
+
// call plugin functions. Also ignore `timeline` and `timeline_variables` because they
|
|
758
|
+
// are used in the `trials` parameter of the preload plugin and we don't want to actually
|
|
759
|
+
// evaluate those in that context.
|
|
742
760
|
continue;
|
|
743
761
|
}
|
|
744
762
|
obj[key] = this.replaceFunctionsWithValues(obj[key], null);
|
package/src/TimelineNode.ts
CHANGED
|
@@ -357,7 +357,11 @@ export class TimelineNode {
|
|
|
357
357
|
// recursive downward search for active trial to extract timeline variable
|
|
358
358
|
timelineVariable(variable_name: string) {
|
|
359
359
|
if (typeof this.timeline_parameters == "undefined") {
|
|
360
|
-
|
|
360
|
+
const val = this.findTimelineVariable(variable_name);
|
|
361
|
+
if (typeof val === "undefined") {
|
|
362
|
+
console.warn("Timeline variable " + variable_name + " not found.");
|
|
363
|
+
}
|
|
364
|
+
return val;
|
|
361
365
|
} else {
|
|
362
366
|
// if progress.current_location is -1, then the timeline variable is being evaluated
|
|
363
367
|
// in a function that runs prior to the trial starting, so we should treat that trial
|
|
@@ -371,7 +375,11 @@ export class TimelineNode {
|
|
|
371
375
|
loc = loc - 1;
|
|
372
376
|
}
|
|
373
377
|
// now find the variable
|
|
374
|
-
|
|
378
|
+
const val = this.timeline_parameters.timeline[loc].timelineVariable(variable_name);
|
|
379
|
+
if (typeof val === "undefined") {
|
|
380
|
+
console.warn("Timeline variable " + variable_name + " not found.");
|
|
381
|
+
}
|
|
382
|
+
return val;
|
|
375
383
|
}
|
|
376
384
|
}
|
|
377
385
|
|
|
@@ -19,5 +19,5 @@ export interface JsPsychExtension {
|
|
|
19
19
|
* Called at the end of the trial.
|
|
20
20
|
* @returns Data to append to the trial's data object.
|
|
21
21
|
*/
|
|
22
|
-
on_finish(params?: Record<string, any>): Record<string, any
|
|
22
|
+
on_finish(params?: Record<string, any>): Record<string, any> | Promise<Record<string, any>>;
|
|
23
23
|
}
|
|
@@ -13,7 +13,10 @@ export class MediaAPI {
|
|
|
13
13
|
|
|
14
14
|
// video //
|
|
15
15
|
private video_buffers = {};
|
|
16
|
-
getVideoBuffer(videoID) {
|
|
16
|
+
getVideoBuffer(videoID: string) {
|
|
17
|
+
if (videoID.startsWith("blob:")) {
|
|
18
|
+
this.video_buffers[videoID] = videoID;
|
|
19
|
+
}
|
|
17
20
|
return this.video_buffers[videoID];
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -334,4 +337,21 @@ export class MediaAPI {
|
|
|
334
337
|
getMicrophoneRecorder(): MediaRecorder {
|
|
335
338
|
return this.microphone_recorder;
|
|
336
339
|
}
|
|
340
|
+
|
|
341
|
+
private camera_stream: MediaStream = null;
|
|
342
|
+
private camera_recorder: MediaRecorder = null;
|
|
343
|
+
|
|
344
|
+
initializeCameraRecorder(stream: MediaStream, opts?: MediaRecorderOptions) {
|
|
345
|
+
this.camera_stream = stream;
|
|
346
|
+
const recorder = new MediaRecorder(stream, opts);
|
|
347
|
+
this.camera_recorder = recorder;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
getCameraStream(): MediaStream {
|
|
351
|
+
return this.camera_stream;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
getCameraRecorder(): MediaRecorder {
|
|
355
|
+
return this.camera_recorder;
|
|
356
|
+
}
|
|
337
357
|
}
|