/**
* jspsych.js
* Josh de Leeuw
*
* documentation: docs.jspsych.org
*
**/
var jsPsych = (function() {
var core = {};
//
// private variables
//
// options
var opts = {};
// experiment timeline
var timeline;
// flow control
var global_trial_index = 0;
var current_trial = {};
// target DOM element
var DOM_target;
// time that the experiment began
var exp_start_time;
//
// public methods
//
core.init = function(options) {
// reset variables
timeline = null;
global_trial_index = 0;
current_trial = {};
// check if there is a body element on the page
var default_display_element = $('body');
if (default_display_element.length === 0) {
$(document.documentElement).append($('
'));
default_display_element = $('body');
}
var defaults = {
'display_element': default_display_element,
'on_finish': function(data) {
return undefined;
},
'on_trial_start': function() {
return undefined;
},
'on_trial_finish': function() {
return undefined;
},
'on_data_update': function(data) {
return undefined;
},
'show_progress_bar': false,
'max_load_time': 30000,
'skip_load_check': false,
'fullscreen': false,
'default_iti': 1000
};
// override default options if user specifies an option
opts = $.extend({}, defaults, options);
// set target
DOM_target = opts.display_element;
// add CSS class to DOM_target
DOM_target.addClass('jspsych-display-element');
// create experiment timeline
timeline = new TimelineNode({timeline:opts.timeline});
// wait for everything to load
if (opts.skip_load_check) {
startExperiment(opts.fullscreen);
} else {
allLoaded(startExperiment, opts.max_load_time, opts.fullscreen);
}
};
core.progress = function() {
var percent_complete = timeline.percentComplete()
var obj = {
"total_trials": timeline.length(),
"current_trial_global": global_trial_index,
"percent_complete": percent_complete
};
return obj;
};
core.startTime = function() {
return exp_start_time;
};
core.totalTime = function() {
return (new Date()).getTime() - exp_start_time.getTime();
};
core.preloadImages = function(images, callback_complete, callback_load) {
// flatten the images array
images = flatten(images);
var n_loaded = 0;
var loadfn = (typeof callback_load === 'undefined') ? function() {} : callback_load;
var finishfn = (typeof callback_complete === 'undefined') ? function() {} : callback_complete;
for (var i = 0; i < images.length; i++) {
var img = new Image();
img.onload = function() {
n_loaded++;
loadfn(n_loaded);
if (n_loaded == images.length) {
finishfn();
}
};
img.src = images[i];
}
};
core.getDisplayElement = function() {
return DOM_target;
};
core.finishTrial = function(data) {
// write the data from the trial
data = typeof data == 'undefined' ? {} : data;
jsPsych.data.write(data);
// get back the data with all of the defaults in
var trial_data = jsPsych.data.getDataByTrialIndex(global_trial_index);
// handle callback at plugin level
if (typeof current_trial.on_finish === 'function') {
current_trial.on_finish(trial_data);
}
// handle callback at whole-experiment level
opts.on_trial_finish(trial_data);
// wait for iti
if (typeof current_trial.timing_post_trial == 'undefined') {
if (opts.default_iti > 0) {
setTimeout(next_trial, opts.default_iti);
} else {
next_trial();
}
} else {
if (current_trial.timing_post_trial > 0) {
setTimeout(next_trial, current_trial.timing_post_trial);
} else {
next_trial();
}
}
function next_trial() {
global_trial_index++;
// advance timeline
var complete = timeline.advance();
// update progress bar if shown
if (opts.show_progress_bar === true) {
updateProgressBar();
}
// check if experiment is over
if (complete) {
finishExperiment();
return;
}
doTrial(timeline.trial());
}
};
core.endExperiment = function() {
timeline.end();
}
core.endCurrentChunk = function() {
timeline.endActiveNode();
}
core.currentTrial = function() {
return current_trial;
};
core.initSettings = function() {
return opts;
};
core.currentChunkID = function() {
return timeline.activeID();
};
function allLoaded(callback, max_wait, fullscreen) {
var refresh_rate = 1000;
var max_wait = max_wait || 30000;
var start = (new Date()).getTime();
var interval = setInterval(function() {
if (jsPsych.pluginAPI.audioLoaded()) {
clearInterval(interval);
callback(fullscreen);
} else if ((new Date()).getTime() - max_wait > start) {
console.error('Experiment failed to load all resouces in time alloted');
}
}, refresh_rate);
}
function TimelineNode(parameters, parent, relativeID) {
// a unique ID for this node, relative to the parent
var relative_id;
// store the timeline for this node
var timeline = [];
// store the parent for this node
var parent_node;
// if there is a loop function, store it
var loop_function;
// if there is a conditional function, store it
var conditional_function;
// data for the trial if this node is a trial
var trial_data;
// flag to randomize the order of the trials
var randomize_order = false;
// keep track of progress
var current_location = 0;
var current_iteration = 0;
// flag to force the node to be finished
var done_flag = false;
// reference to self
var self = this;
// constructor
var _construct = function() {
// store a link to the parent of this node
parent_node = parent;
// create the ID for this node
if (typeof parent == 'undefined') {
relative_id = 0;
}
relative_id = relativeID;
// check if there is a timeline parameter
// if there is, then this is not a trial node
if (typeof parameters.timeline !== 'undefined') {
// extract all of the node level data and parameters
var node_data = $.extend(true, {}, parameters);
delete node_data.timeline;
delete node_data.conditional_function;
delete node_data.loop_function;
// create a TimelineNode for each element in the timeline
for (var i = 0; i < parameters.timeline.length; i++) {
timeline.push(new TimelineNode($.extend(true, {}, node_data, parameters.timeline[i]), self, i));
}
// store the loop function if it exists
if (typeof parameters.loop_function !== 'undefined') {
loop_function = parameters.loop_function;
}
// store the conditional function if it exists
if (typeof parameters.conditional_function !== 'undefined') {
conditional_function = parameters.conditional_function;
}
// flag to randomize the order of trials
if (typeof parameters.randomize_order !== 'undefined') {
randomize_order = parameters.randomize_order;
}
if (randomize_order === true) {
timeline = jsPsych.randomization.shuffle(timeline);
}
}
// if there is no timeline parameter, then this node is a trial node
else {
// check to see if a valid trial type is defined
var trial_type = parameters.type;
if(typeof trial_type == 'undefined') {
console.error('Trial level node is missing the "type" parameter. The parameters for the node are: '+JSON.stringify(parameters));
} else if(typeof jsPsych[trial_type] == 'undefined') {
console.error('No plugin loaded for trials of type "'+trial_type+'"');
}
// create a deep copy of the parameters for the trial
trial_data = $.extend(true, {}, parameters);
}
}();
// recursively get the number of **trials** contained in the timeline
// assuming that while loops execute exactly once and if conditionals
// always run
this.length = function() {
var length = 0;
if (timeline.length > 0) {
for (var i = 0; i < timeline.length; i++) {
length += timeline[i].length();
}
} else {
return 1;
}
return length;
}
// recursively get the next trial to run.
// if this node is a leaf (trial), then return the trial.
// otherwise, recursively find the next trial in the child timeline.
this.trial = function() {
if (timeline.length == 0) {
return trial_data;
} else {
if (current_location >= timeline.length) {
return null;
} else {
return timeline[current_location].trial();
}
}
}
// update the current trial node to be completed
// returns true if the node is complete after advance
// returns false otherwise
this.advance = function() {
// propogate down to the current trial, and update the current_location
// of that node (effectively ending that node)
if (timeline.length !== 0) {
if (timeline[current_location].advance()) {
// if this returns true, then the node below is complete, and we need to
// advance this node.
current_location++;
if (this.checkCompletion()) {
return true;
} else {
// we advanced the node, now we need to check if the node we advanced
// to is also complete, and keep advancing until we find a node that
// is not complete, or until this node is complete.
while (!this.checkCompletion() && timeline[current_location].checkCompletion()) {
current_location++;
}
if (this.checkCompletion()) {
return true;
} else {
return false;
}
}
} else {
// if this returns false, then the node below is not complete, and we
// don't need to do anything else here
return false;
}
} else {
// if we get here, then this is a trial node, and the node is complete
current_location++;
done_flag = true;
return true;
}
}
// return true if the node is completely done (no more possible trials)
// otherwise, return false
this.checkCompletion = function() {
// if the done_flag is true, the node is complete no matter what.
if (done_flag) {
return true;
}
// check for trial nodes
if (timeline.length == 0 && current_location > 0) {
done_flag = true;
return true;
}
// check for non-trial nodes
if (timeline.length > 0) {
// checking nodes that have reached the end of the timeline.
// if there is a loop function, evaluate it.
// otherwise, the node is done.
if (current_location >= timeline.length) {
// check if there is a loop function
if (typeof loop_function !== 'undefined') {
if (loop_function(this.generatedData())) {
this.reset();
} else {
done_flag = true;
return true;
}
} else {
done_flag = true;
return true;
}
}
// checking nodes with conditional functions
if (typeof conditional_function !== 'undefined' && current_location == 0) {
if (conditional_function()) {
// run the timeline
return false;
} else {
// skip the timeline
done_flag = true;
return true;
}
}
}
return false;
}
// check the status of the done flag
this.isComplete = function() {
return done_flag;
}
// return the percentage of trials completed, grouped at the first child level
// counts a set of trials as complete when the child node is done
this.percentComplete = function() {
var total_trials = this.length();
var completed_trials = 0;
for(var i=0; i
The experiment will launch in fullscreen mode when you click the button below.
');
$('#jspsych-fullscreen-btn').on('click', function() {
var element = document.documentElement;
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
$('#jspsych-fullscreen-btn').off('click');
DOM_target.html('');
go();
});
}
} else {
go();
}
function go() {
// show progress bar if requested
if (opts.show_progress_bar === true) {
drawProgressBar();
}
// record the start time
exp_start_time = new Date();
// begin!
doTrial(timeline.trial());
}
}
function finishExperiment() {
opts.on_finish(jsPsych.data.getData());
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
function doTrial(trial) {
current_trial = trial;
// call experiment wide callback
opts.on_trial_start();
// execute trial method
jsPsych[trial.type].trial(DOM_target, trial);
}
function drawProgressBar() {
$('body').prepend($('