jspsych 7.2.3 → 7.3.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/dist/index.browser.js +104 -63
- 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 +104 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +104 -63
- 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 +41 -21
- package/src/modules/randomization.ts +3 -1
package/dist/index.cjs
CHANGED
|
@@ -71,7 +71,7 @@ var autoBind = (self, {include, exclude} = {}) => {
|
|
|
71
71
|
return self;
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
var version = "7.
|
|
74
|
+
var version = "7.3.1";
|
|
75
75
|
|
|
76
76
|
class MigrationError extends Error {
|
|
77
77
|
constructor(message = "The global `jsPsych` variable is no longer available in jsPsych v7.") {
|
|
@@ -843,8 +843,13 @@ class MediaAPI {
|
|
|
843
843
|
this.img_cache = {};
|
|
844
844
|
this.preloadMap = new Map();
|
|
845
845
|
this.microphone_recorder = null;
|
|
846
|
+
this.camera_stream = null;
|
|
847
|
+
this.camera_recorder = null;
|
|
846
848
|
}
|
|
847
849
|
getVideoBuffer(videoID) {
|
|
850
|
+
if (videoID.startsWith("blob:")) {
|
|
851
|
+
this.video_buffers[videoID] = videoID;
|
|
852
|
+
}
|
|
848
853
|
return this.video_buffers[videoID];
|
|
849
854
|
}
|
|
850
855
|
initAudio() {
|
|
@@ -899,15 +904,15 @@ class MediaAPI {
|
|
|
899
904
|
callback_error({ source: source, error: e });
|
|
900
905
|
});
|
|
901
906
|
};
|
|
902
|
-
request.onerror =
|
|
907
|
+
request.onerror = (e) => {
|
|
903
908
|
let err = e;
|
|
904
|
-
if (
|
|
909
|
+
if (request.status == 404) {
|
|
905
910
|
err = "404";
|
|
906
911
|
}
|
|
907
912
|
callback_error({ source: source, error: err });
|
|
908
913
|
};
|
|
909
|
-
request.onloadend =
|
|
910
|
-
if (
|
|
914
|
+
request.onloadend = (e) => {
|
|
915
|
+
if (request.status == 404) {
|
|
911
916
|
callback_error({ source: source, error: "404" });
|
|
912
917
|
}
|
|
913
918
|
};
|
|
@@ -964,20 +969,21 @@ class MediaAPI {
|
|
|
964
969
|
callback_complete();
|
|
965
970
|
return;
|
|
966
971
|
}
|
|
967
|
-
for (
|
|
968
|
-
|
|
969
|
-
|
|
972
|
+
for (let i = 0; i < images.length; i++) {
|
|
973
|
+
const img = new Image();
|
|
974
|
+
const src = images[i];
|
|
975
|
+
img.onload = () => {
|
|
970
976
|
n_loaded++;
|
|
971
|
-
callback_load(
|
|
977
|
+
callback_load(src);
|
|
972
978
|
if (n_loaded === images.length) {
|
|
973
979
|
callback_complete();
|
|
974
980
|
}
|
|
975
981
|
};
|
|
976
|
-
img.onerror =
|
|
977
|
-
callback_error({ source:
|
|
982
|
+
img.onerror = (e) => {
|
|
983
|
+
callback_error({ source: src, error: e });
|
|
978
984
|
};
|
|
979
|
-
img.src =
|
|
980
|
-
this.img_cache[
|
|
985
|
+
img.src = src;
|
|
986
|
+
this.img_cache[src] = img;
|
|
981
987
|
this.preload_requests.push(img);
|
|
982
988
|
}
|
|
983
989
|
}
|
|
@@ -995,9 +1001,9 @@ class MediaAPI {
|
|
|
995
1001
|
const request = new XMLHttpRequest();
|
|
996
1002
|
request.open("GET", video, true);
|
|
997
1003
|
request.responseType = "blob";
|
|
998
|
-
request.onload =
|
|
999
|
-
if (
|
|
1000
|
-
const videoBlob =
|
|
1004
|
+
request.onload = () => {
|
|
1005
|
+
if (request.status === 200 || request.status === 0) {
|
|
1006
|
+
const videoBlob = request.response;
|
|
1001
1007
|
video_buffers[video] = URL.createObjectURL(videoBlob); // IE10+
|
|
1002
1008
|
n_loaded++;
|
|
1003
1009
|
callback_load(video);
|
|
@@ -1006,15 +1012,15 @@ class MediaAPI {
|
|
|
1006
1012
|
}
|
|
1007
1013
|
}
|
|
1008
1014
|
};
|
|
1009
|
-
request.onerror =
|
|
1015
|
+
request.onerror = (e) => {
|
|
1010
1016
|
let err = e;
|
|
1011
|
-
if (
|
|
1017
|
+
if (request.status == 404) {
|
|
1012
1018
|
err = "404";
|
|
1013
1019
|
}
|
|
1014
1020
|
callback_error({ source: video, error: err });
|
|
1015
1021
|
};
|
|
1016
|
-
request.onloadend =
|
|
1017
|
-
if (
|
|
1022
|
+
request.onloadend = (e) => {
|
|
1023
|
+
if (request.status == 404) {
|
|
1018
1024
|
callback_error({ source: video, error: "404" });
|
|
1019
1025
|
}
|
|
1020
1026
|
};
|
|
@@ -1086,6 +1092,17 @@ class MediaAPI {
|
|
|
1086
1092
|
getMicrophoneRecorder() {
|
|
1087
1093
|
return this.microphone_recorder;
|
|
1088
1094
|
}
|
|
1095
|
+
initializeCameraRecorder(stream, opts) {
|
|
1096
|
+
this.camera_stream = stream;
|
|
1097
|
+
const recorder = new MediaRecorder(stream, opts);
|
|
1098
|
+
this.camera_recorder = recorder;
|
|
1099
|
+
}
|
|
1100
|
+
getCameraStream() {
|
|
1101
|
+
return this.camera_stream;
|
|
1102
|
+
}
|
|
1103
|
+
getCameraRecorder() {
|
|
1104
|
+
return this.camera_recorder;
|
|
1105
|
+
}
|
|
1089
1106
|
}
|
|
1090
1107
|
|
|
1091
1108
|
class SimulationAPI {
|
|
@@ -1860,7 +1877,8 @@ function shuffleNoRepeats(arr, equalityTest) {
|
|
|
1860
1877
|
// test to make sure the new neighbor isn't equal to the old one
|
|
1861
1878
|
while (equalityTest(random_shuffle[i + 1], random_shuffle[random_pick]) ||
|
|
1862
1879
|
equalityTest(random_shuffle[i + 1], random_shuffle[random_pick + 1]) ||
|
|
1863
|
-
equalityTest(random_shuffle[i + 1], random_shuffle[random_pick - 1])
|
|
1880
|
+
equalityTest(random_shuffle[i + 1], random_shuffle[random_pick - 1]) ||
|
|
1881
|
+
equalityTest(random_shuffle[i], random_shuffle[random_pick])) {
|
|
1864
1882
|
random_pick = Math.floor(Math.random() * (random_shuffle.length - 2)) + 1;
|
|
1865
1883
|
}
|
|
1866
1884
|
const new_neighbor = random_shuffle[random_pick];
|
|
@@ -2410,7 +2428,11 @@ class TimelineNode {
|
|
|
2410
2428
|
// recursive downward search for active trial to extract timeline variable
|
|
2411
2429
|
timelineVariable(variable_name) {
|
|
2412
2430
|
if (typeof this.timeline_parameters == "undefined") {
|
|
2413
|
-
|
|
2431
|
+
const val = this.findTimelineVariable(variable_name);
|
|
2432
|
+
if (typeof val === "undefined") {
|
|
2433
|
+
console.warn("Timeline variable " + variable_name + " not found.");
|
|
2434
|
+
}
|
|
2435
|
+
return val;
|
|
2414
2436
|
}
|
|
2415
2437
|
else {
|
|
2416
2438
|
// if progress.current_location is -1, then the timeline variable is being evaluated
|
|
@@ -2425,7 +2447,11 @@ class TimelineNode {
|
|
|
2425
2447
|
loc = loc - 1;
|
|
2426
2448
|
}
|
|
2427
2449
|
// now find the variable
|
|
2428
|
-
|
|
2450
|
+
const val = this.timeline_parameters.timeline[loc].timelineVariable(variable_name);
|
|
2451
|
+
if (typeof val === "undefined") {
|
|
2452
|
+
console.warn("Timeline variable " + variable_name + " not found.");
|
|
2453
|
+
}
|
|
2454
|
+
return val;
|
|
2429
2455
|
}
|
|
2430
2456
|
}
|
|
2431
2457
|
// recursively get all the timeline variables for this trial
|
|
@@ -2703,6 +2729,7 @@ class JsPsych {
|
|
|
2703
2729
|
return this.DOM_container;
|
|
2704
2730
|
}
|
|
2705
2731
|
finishTrial(data = {}) {
|
|
2732
|
+
var _a;
|
|
2706
2733
|
if (this.current_trial_finished) {
|
|
2707
2734
|
return;
|
|
2708
2735
|
}
|
|
@@ -2743,46 +2770,58 @@ class JsPsych {
|
|
|
2743
2770
|
}
|
|
2744
2771
|
}
|
|
2745
2772
|
// handle extension callbacks
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2773
|
+
const extensionCallbackResults = ((_a = current_trial.extensions) !== null && _a !== void 0 ? _a : []).map((extension) => this.extensions[extension.type.info.name].on_finish(extension.params));
|
|
2774
|
+
const onExtensionCallbacksFinished = () => {
|
|
2775
|
+
// about to execute lots of callbacks, so switch context.
|
|
2776
|
+
this.internal.call_immediate = true;
|
|
2777
|
+
// handle callback at plugin level
|
|
2778
|
+
if (typeof current_trial.on_finish === "function") {
|
|
2779
|
+
current_trial.on_finish(trial_data_values);
|
|
2780
|
+
}
|
|
2781
|
+
// handle callback at whole-experiment level
|
|
2782
|
+
this.opts.on_trial_finish(trial_data_values);
|
|
2783
|
+
// after the above callbacks are complete, then the data should be finalized
|
|
2784
|
+
// for this trial. call the on_data_update handler, passing in the same
|
|
2785
|
+
// data object that just went through the trial's finish handlers.
|
|
2786
|
+
this.opts.on_data_update(trial_data_values);
|
|
2787
|
+
// done with callbacks
|
|
2788
|
+
this.internal.call_immediate = false;
|
|
2789
|
+
// wait for iti
|
|
2790
|
+
if (this.simulation_mode === "data-only") {
|
|
2791
|
+
this.nextTrial();
|
|
2750
2792
|
}
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
this.opts.on_trial_finish(trial_data_values);
|
|
2760
|
-
// after the above callbacks are complete, then the data should be finalized
|
|
2761
|
-
// for this trial. call the on_data_update handler, passing in the same
|
|
2762
|
-
// data object that just went through the trial's finish handlers.
|
|
2763
|
-
this.opts.on_data_update(trial_data_values);
|
|
2764
|
-
// done with callbacks
|
|
2765
|
-
this.internal.call_immediate = false;
|
|
2766
|
-
// wait for iti
|
|
2767
|
-
if (this.simulation_mode === "data-only") {
|
|
2768
|
-
this.nextTrial();
|
|
2769
|
-
}
|
|
2770
|
-
else if (typeof current_trial.post_trial_gap === null ||
|
|
2771
|
-
typeof current_trial.post_trial_gap === "undefined") {
|
|
2772
|
-
if (this.opts.default_iti > 0) {
|
|
2773
|
-
setTimeout(this.nextTrial, this.opts.default_iti);
|
|
2793
|
+
else if (typeof current_trial.post_trial_gap === null ||
|
|
2794
|
+
typeof current_trial.post_trial_gap === "undefined") {
|
|
2795
|
+
if (this.opts.default_iti > 0) {
|
|
2796
|
+
setTimeout(this.nextTrial, this.opts.default_iti);
|
|
2797
|
+
}
|
|
2798
|
+
else {
|
|
2799
|
+
this.nextTrial();
|
|
2800
|
+
}
|
|
2774
2801
|
}
|
|
2775
2802
|
else {
|
|
2776
|
-
|
|
2803
|
+
if (current_trial.post_trial_gap > 0) {
|
|
2804
|
+
setTimeout(this.nextTrial, current_trial.post_trial_gap);
|
|
2805
|
+
}
|
|
2806
|
+
else {
|
|
2807
|
+
this.nextTrial();
|
|
2808
|
+
}
|
|
2777
2809
|
}
|
|
2810
|
+
};
|
|
2811
|
+
// Strictly using Promise.resolve to turn all values into promises would be cleaner here, but it
|
|
2812
|
+
// would require user test code to make the event loop tick after every simulated key press even
|
|
2813
|
+
// if there are no async `on_finish` methods. Hence, in order to avoid a breaking change, we
|
|
2814
|
+
// only rely on the event loop if at least one `on_finish` method returns a promise.
|
|
2815
|
+
if (extensionCallbackResults.some((result) => typeof result.then === "function")) {
|
|
2816
|
+
Promise.all(extensionCallbackResults.map((result) => Promise.resolve(result).then((ext_data_values) => {
|
|
2817
|
+
Object.assign(trial_data_values, ext_data_values);
|
|
2818
|
+
}))).then(onExtensionCallbacksFinished);
|
|
2778
2819
|
}
|
|
2779
2820
|
else {
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
}
|
|
2783
|
-
else {
|
|
2784
|
-
this.nextTrial();
|
|
2821
|
+
for (const values of extensionCallbackResults) {
|
|
2822
|
+
Object.assign(trial_data_values, values);
|
|
2785
2823
|
}
|
|
2824
|
+
onExtensionCallbacksFinished();
|
|
2786
2825
|
}
|
|
2787
2826
|
}
|
|
2788
2827
|
endExperiment(end_message = "", data = {}) {
|
|
@@ -3070,16 +3109,16 @@ class JsPsych {
|
|
|
3070
3109
|
}
|
|
3071
3110
|
evaluateTimelineVariables(trial) {
|
|
3072
3111
|
for (const key of Object.keys(trial)) {
|
|
3073
|
-
// timeline variables on the root level
|
|
3074
3112
|
if (typeof trial[key] === "object" &&
|
|
3075
3113
|
trial[key] !== null &&
|
|
3076
3114
|
typeof trial[key].timelineVariablePlaceholder !== "undefined") {
|
|
3077
|
-
|
|
3078
|
-
"function(){returntimeline.timelineVariable(varname);}"
|
|
3079
|
-
)*/ trial[key] = trial[key].timelineVariableFunction();
|
|
3115
|
+
trial[key] = trial[key].timelineVariableFunction();
|
|
3080
3116
|
}
|
|
3081
3117
|
// timeline variables that are nested in objects
|
|
3082
|
-
if (typeof trial[key] === "object" &&
|
|
3118
|
+
if (typeof trial[key] === "object" &&
|
|
3119
|
+
trial[key] !== null &&
|
|
3120
|
+
key !== "timeline" &&
|
|
3121
|
+
key !== "timeline_variables") {
|
|
3083
3122
|
this.evaluateTimelineVariables(trial[key]);
|
|
3084
3123
|
}
|
|
3085
3124
|
}
|
|
@@ -3122,9 +3161,11 @@ class JsPsych {
|
|
|
3122
3161
|
else if (typeof obj === "object") {
|
|
3123
3162
|
if (info === null || !info.nested) {
|
|
3124
3163
|
for (const key of Object.keys(obj)) {
|
|
3125
|
-
if (key === "type") {
|
|
3164
|
+
if (key === "type" || key === "timeline" || key === "timeline_variables") {
|
|
3126
3165
|
// Ignore the object's `type` field because it contains a plugin and we do not want to
|
|
3127
|
-
// call plugin functions
|
|
3166
|
+
// call plugin functions. Also ignore `timeline` and `timeline_variables` because they
|
|
3167
|
+
// are used in the `trials` parameter of the preload plugin and we don't want to actually
|
|
3168
|
+
// evaluate those in that context.
|
|
3128
3169
|
continue;
|
|
3129
3170
|
}
|
|
3130
3171
|
obj[key] = this.replaceFunctionsWithValues(obj[key], null);
|