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.js
CHANGED
|
@@ -67,7 +67,7 @@ var autoBind = (self, {include, exclude} = {}) => {
|
|
|
67
67
|
return self;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
var version = "7.
|
|
70
|
+
var version = "7.3.1";
|
|
71
71
|
|
|
72
72
|
class MigrationError extends Error {
|
|
73
73
|
constructor(message = "The global `jsPsych` variable is no longer available in jsPsych v7.") {
|
|
@@ -839,8 +839,13 @@ class MediaAPI {
|
|
|
839
839
|
this.img_cache = {};
|
|
840
840
|
this.preloadMap = new Map();
|
|
841
841
|
this.microphone_recorder = null;
|
|
842
|
+
this.camera_stream = null;
|
|
843
|
+
this.camera_recorder = null;
|
|
842
844
|
}
|
|
843
845
|
getVideoBuffer(videoID) {
|
|
846
|
+
if (videoID.startsWith("blob:")) {
|
|
847
|
+
this.video_buffers[videoID] = videoID;
|
|
848
|
+
}
|
|
844
849
|
return this.video_buffers[videoID];
|
|
845
850
|
}
|
|
846
851
|
initAudio() {
|
|
@@ -895,15 +900,15 @@ class MediaAPI {
|
|
|
895
900
|
callback_error({ source: source, error: e });
|
|
896
901
|
});
|
|
897
902
|
};
|
|
898
|
-
request.onerror =
|
|
903
|
+
request.onerror = (e) => {
|
|
899
904
|
let err = e;
|
|
900
|
-
if (
|
|
905
|
+
if (request.status == 404) {
|
|
901
906
|
err = "404";
|
|
902
907
|
}
|
|
903
908
|
callback_error({ source: source, error: err });
|
|
904
909
|
};
|
|
905
|
-
request.onloadend =
|
|
906
|
-
if (
|
|
910
|
+
request.onloadend = (e) => {
|
|
911
|
+
if (request.status == 404) {
|
|
907
912
|
callback_error({ source: source, error: "404" });
|
|
908
913
|
}
|
|
909
914
|
};
|
|
@@ -960,20 +965,21 @@ class MediaAPI {
|
|
|
960
965
|
callback_complete();
|
|
961
966
|
return;
|
|
962
967
|
}
|
|
963
|
-
for (
|
|
964
|
-
|
|
965
|
-
|
|
968
|
+
for (let i = 0; i < images.length; i++) {
|
|
969
|
+
const img = new Image();
|
|
970
|
+
const src = images[i];
|
|
971
|
+
img.onload = () => {
|
|
966
972
|
n_loaded++;
|
|
967
|
-
callback_load(
|
|
973
|
+
callback_load(src);
|
|
968
974
|
if (n_loaded === images.length) {
|
|
969
975
|
callback_complete();
|
|
970
976
|
}
|
|
971
977
|
};
|
|
972
|
-
img.onerror =
|
|
973
|
-
callback_error({ source:
|
|
978
|
+
img.onerror = (e) => {
|
|
979
|
+
callback_error({ source: src, error: e });
|
|
974
980
|
};
|
|
975
|
-
img.src =
|
|
976
|
-
this.img_cache[
|
|
981
|
+
img.src = src;
|
|
982
|
+
this.img_cache[src] = img;
|
|
977
983
|
this.preload_requests.push(img);
|
|
978
984
|
}
|
|
979
985
|
}
|
|
@@ -991,9 +997,9 @@ class MediaAPI {
|
|
|
991
997
|
const request = new XMLHttpRequest();
|
|
992
998
|
request.open("GET", video, true);
|
|
993
999
|
request.responseType = "blob";
|
|
994
|
-
request.onload =
|
|
995
|
-
if (
|
|
996
|
-
const videoBlob =
|
|
1000
|
+
request.onload = () => {
|
|
1001
|
+
if (request.status === 200 || request.status === 0) {
|
|
1002
|
+
const videoBlob = request.response;
|
|
997
1003
|
video_buffers[video] = URL.createObjectURL(videoBlob); // IE10+
|
|
998
1004
|
n_loaded++;
|
|
999
1005
|
callback_load(video);
|
|
@@ -1002,15 +1008,15 @@ class MediaAPI {
|
|
|
1002
1008
|
}
|
|
1003
1009
|
}
|
|
1004
1010
|
};
|
|
1005
|
-
request.onerror =
|
|
1011
|
+
request.onerror = (e) => {
|
|
1006
1012
|
let err = e;
|
|
1007
|
-
if (
|
|
1013
|
+
if (request.status == 404) {
|
|
1008
1014
|
err = "404";
|
|
1009
1015
|
}
|
|
1010
1016
|
callback_error({ source: video, error: err });
|
|
1011
1017
|
};
|
|
1012
|
-
request.onloadend =
|
|
1013
|
-
if (
|
|
1018
|
+
request.onloadend = (e) => {
|
|
1019
|
+
if (request.status == 404) {
|
|
1014
1020
|
callback_error({ source: video, error: "404" });
|
|
1015
1021
|
}
|
|
1016
1022
|
};
|
|
@@ -1082,6 +1088,17 @@ class MediaAPI {
|
|
|
1082
1088
|
getMicrophoneRecorder() {
|
|
1083
1089
|
return this.microphone_recorder;
|
|
1084
1090
|
}
|
|
1091
|
+
initializeCameraRecorder(stream, opts) {
|
|
1092
|
+
this.camera_stream = stream;
|
|
1093
|
+
const recorder = new MediaRecorder(stream, opts);
|
|
1094
|
+
this.camera_recorder = recorder;
|
|
1095
|
+
}
|
|
1096
|
+
getCameraStream() {
|
|
1097
|
+
return this.camera_stream;
|
|
1098
|
+
}
|
|
1099
|
+
getCameraRecorder() {
|
|
1100
|
+
return this.camera_recorder;
|
|
1101
|
+
}
|
|
1085
1102
|
}
|
|
1086
1103
|
|
|
1087
1104
|
class SimulationAPI {
|
|
@@ -1856,7 +1873,8 @@ function shuffleNoRepeats(arr, equalityTest) {
|
|
|
1856
1873
|
// test to make sure the new neighbor isn't equal to the old one
|
|
1857
1874
|
while (equalityTest(random_shuffle[i + 1], random_shuffle[random_pick]) ||
|
|
1858
1875
|
equalityTest(random_shuffle[i + 1], random_shuffle[random_pick + 1]) ||
|
|
1859
|
-
equalityTest(random_shuffle[i + 1], random_shuffle[random_pick - 1])
|
|
1876
|
+
equalityTest(random_shuffle[i + 1], random_shuffle[random_pick - 1]) ||
|
|
1877
|
+
equalityTest(random_shuffle[i], random_shuffle[random_pick])) {
|
|
1860
1878
|
random_pick = Math.floor(Math.random() * (random_shuffle.length - 2)) + 1;
|
|
1861
1879
|
}
|
|
1862
1880
|
const new_neighbor = random_shuffle[random_pick];
|
|
@@ -2406,7 +2424,11 @@ class TimelineNode {
|
|
|
2406
2424
|
// recursive downward search for active trial to extract timeline variable
|
|
2407
2425
|
timelineVariable(variable_name) {
|
|
2408
2426
|
if (typeof this.timeline_parameters == "undefined") {
|
|
2409
|
-
|
|
2427
|
+
const val = this.findTimelineVariable(variable_name);
|
|
2428
|
+
if (typeof val === "undefined") {
|
|
2429
|
+
console.warn("Timeline variable " + variable_name + " not found.");
|
|
2430
|
+
}
|
|
2431
|
+
return val;
|
|
2410
2432
|
}
|
|
2411
2433
|
else {
|
|
2412
2434
|
// if progress.current_location is -1, then the timeline variable is being evaluated
|
|
@@ -2421,7 +2443,11 @@ class TimelineNode {
|
|
|
2421
2443
|
loc = loc - 1;
|
|
2422
2444
|
}
|
|
2423
2445
|
// now find the variable
|
|
2424
|
-
|
|
2446
|
+
const val = this.timeline_parameters.timeline[loc].timelineVariable(variable_name);
|
|
2447
|
+
if (typeof val === "undefined") {
|
|
2448
|
+
console.warn("Timeline variable " + variable_name + " not found.");
|
|
2449
|
+
}
|
|
2450
|
+
return val;
|
|
2425
2451
|
}
|
|
2426
2452
|
}
|
|
2427
2453
|
// recursively get all the timeline variables for this trial
|
|
@@ -2699,6 +2725,7 @@ class JsPsych {
|
|
|
2699
2725
|
return this.DOM_container;
|
|
2700
2726
|
}
|
|
2701
2727
|
finishTrial(data = {}) {
|
|
2728
|
+
var _a;
|
|
2702
2729
|
if (this.current_trial_finished) {
|
|
2703
2730
|
return;
|
|
2704
2731
|
}
|
|
@@ -2739,46 +2766,58 @@ class JsPsych {
|
|
|
2739
2766
|
}
|
|
2740
2767
|
}
|
|
2741
2768
|
// handle extension callbacks
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2769
|
+
const extensionCallbackResults = ((_a = current_trial.extensions) !== null && _a !== void 0 ? _a : []).map((extension) => this.extensions[extension.type.info.name].on_finish(extension.params));
|
|
2770
|
+
const onExtensionCallbacksFinished = () => {
|
|
2771
|
+
// about to execute lots of callbacks, so switch context.
|
|
2772
|
+
this.internal.call_immediate = true;
|
|
2773
|
+
// handle callback at plugin level
|
|
2774
|
+
if (typeof current_trial.on_finish === "function") {
|
|
2775
|
+
current_trial.on_finish(trial_data_values);
|
|
2776
|
+
}
|
|
2777
|
+
// handle callback at whole-experiment level
|
|
2778
|
+
this.opts.on_trial_finish(trial_data_values);
|
|
2779
|
+
// after the above callbacks are complete, then the data should be finalized
|
|
2780
|
+
// for this trial. call the on_data_update handler, passing in the same
|
|
2781
|
+
// data object that just went through the trial's finish handlers.
|
|
2782
|
+
this.opts.on_data_update(trial_data_values);
|
|
2783
|
+
// done with callbacks
|
|
2784
|
+
this.internal.call_immediate = false;
|
|
2785
|
+
// wait for iti
|
|
2786
|
+
if (this.simulation_mode === "data-only") {
|
|
2787
|
+
this.nextTrial();
|
|
2746
2788
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
this.opts.on_trial_finish(trial_data_values);
|
|
2756
|
-
// after the above callbacks are complete, then the data should be finalized
|
|
2757
|
-
// for this trial. call the on_data_update handler, passing in the same
|
|
2758
|
-
// data object that just went through the trial's finish handlers.
|
|
2759
|
-
this.opts.on_data_update(trial_data_values);
|
|
2760
|
-
// done with callbacks
|
|
2761
|
-
this.internal.call_immediate = false;
|
|
2762
|
-
// wait for iti
|
|
2763
|
-
if (this.simulation_mode === "data-only") {
|
|
2764
|
-
this.nextTrial();
|
|
2765
|
-
}
|
|
2766
|
-
else if (typeof current_trial.post_trial_gap === null ||
|
|
2767
|
-
typeof current_trial.post_trial_gap === "undefined") {
|
|
2768
|
-
if (this.opts.default_iti > 0) {
|
|
2769
|
-
setTimeout(this.nextTrial, this.opts.default_iti);
|
|
2789
|
+
else if (typeof current_trial.post_trial_gap === null ||
|
|
2790
|
+
typeof current_trial.post_trial_gap === "undefined") {
|
|
2791
|
+
if (this.opts.default_iti > 0) {
|
|
2792
|
+
setTimeout(this.nextTrial, this.opts.default_iti);
|
|
2793
|
+
}
|
|
2794
|
+
else {
|
|
2795
|
+
this.nextTrial();
|
|
2796
|
+
}
|
|
2770
2797
|
}
|
|
2771
2798
|
else {
|
|
2772
|
-
|
|
2799
|
+
if (current_trial.post_trial_gap > 0) {
|
|
2800
|
+
setTimeout(this.nextTrial, current_trial.post_trial_gap);
|
|
2801
|
+
}
|
|
2802
|
+
else {
|
|
2803
|
+
this.nextTrial();
|
|
2804
|
+
}
|
|
2773
2805
|
}
|
|
2806
|
+
};
|
|
2807
|
+
// Strictly using Promise.resolve to turn all values into promises would be cleaner here, but it
|
|
2808
|
+
// would require user test code to make the event loop tick after every simulated key press even
|
|
2809
|
+
// if there are no async `on_finish` methods. Hence, in order to avoid a breaking change, we
|
|
2810
|
+
// only rely on the event loop if at least one `on_finish` method returns a promise.
|
|
2811
|
+
if (extensionCallbackResults.some((result) => typeof result.then === "function")) {
|
|
2812
|
+
Promise.all(extensionCallbackResults.map((result) => Promise.resolve(result).then((ext_data_values) => {
|
|
2813
|
+
Object.assign(trial_data_values, ext_data_values);
|
|
2814
|
+
}))).then(onExtensionCallbacksFinished);
|
|
2774
2815
|
}
|
|
2775
2816
|
else {
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
}
|
|
2779
|
-
else {
|
|
2780
|
-
this.nextTrial();
|
|
2817
|
+
for (const values of extensionCallbackResults) {
|
|
2818
|
+
Object.assign(trial_data_values, values);
|
|
2781
2819
|
}
|
|
2820
|
+
onExtensionCallbacksFinished();
|
|
2782
2821
|
}
|
|
2783
2822
|
}
|
|
2784
2823
|
endExperiment(end_message = "", data = {}) {
|
|
@@ -3066,16 +3105,16 @@ class JsPsych {
|
|
|
3066
3105
|
}
|
|
3067
3106
|
evaluateTimelineVariables(trial) {
|
|
3068
3107
|
for (const key of Object.keys(trial)) {
|
|
3069
|
-
// timeline variables on the root level
|
|
3070
3108
|
if (typeof trial[key] === "object" &&
|
|
3071
3109
|
trial[key] !== null &&
|
|
3072
3110
|
typeof trial[key].timelineVariablePlaceholder !== "undefined") {
|
|
3073
|
-
|
|
3074
|
-
"function(){returntimeline.timelineVariable(varname);}"
|
|
3075
|
-
)*/ trial[key] = trial[key].timelineVariableFunction();
|
|
3111
|
+
trial[key] = trial[key].timelineVariableFunction();
|
|
3076
3112
|
}
|
|
3077
3113
|
// timeline variables that are nested in objects
|
|
3078
|
-
if (typeof trial[key] === "object" &&
|
|
3114
|
+
if (typeof trial[key] === "object" &&
|
|
3115
|
+
trial[key] !== null &&
|
|
3116
|
+
key !== "timeline" &&
|
|
3117
|
+
key !== "timeline_variables") {
|
|
3079
3118
|
this.evaluateTimelineVariables(trial[key]);
|
|
3080
3119
|
}
|
|
3081
3120
|
}
|
|
@@ -3118,9 +3157,11 @@ class JsPsych {
|
|
|
3118
3157
|
else if (typeof obj === "object") {
|
|
3119
3158
|
if (info === null || !info.nested) {
|
|
3120
3159
|
for (const key of Object.keys(obj)) {
|
|
3121
|
-
if (key === "type") {
|
|
3160
|
+
if (key === "type" || key === "timeline" || key === "timeline_variables") {
|
|
3122
3161
|
// Ignore the object's `type` field because it contains a plugin and we do not want to
|
|
3123
|
-
// call plugin functions
|
|
3162
|
+
// call plugin functions. Also ignore `timeline` and `timeline_variables` because they
|
|
3163
|
+
// are used in the `trials` parameter of the preload plugin and we don't want to actually
|
|
3164
|
+
// evaluate those in that context.
|
|
3124
3165
|
continue;
|
|
3125
3166
|
}
|
|
3126
3167
|
obj[key] = this.replaceFunctionsWithValues(obj[key], null);
|