/**
* 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;
// is the experiment paused?
var paused = false;
var waiting = false;
// done loading?
var loaded = false;
// enumerated variables for special parameter types
core.ALL_KEYS = 'allkeys';
core.NO_KEYS = 'none';
//
// public methods
//
core.init = function(options) {
// reset variables
timeline = null;
global_trial_index = 0;
current_trial = {};
paused = false;
waiting = false;
loaded = false;
// 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;
},
'on_interaction_data_update': function(data){
return undefined;
},
'exclusions': {},
'show_progress_bar': false,
'auto_preload': true,
'max_load_time': 60000,
'fullscreen': false,
'default_iti': 1000
};
// override default options if user specifies an option
opts = $.extend({}, defaults, options);
// set target
opts.display_element.append('')
DOM_target = $('#jspsych-content');
// add CSS class to DOM_target
opts.display_element.addClass('jspsych-display-element')
DOM_target.addClass('jspsych-content');
// create experiment timeline
timeline = new TimelineNode({
timeline: opts.timeline
});
// create listeners for user browser interaction
jsPsych.data.createInteractionListeners();
// check exclusions before continuing
checkExclusions(opts.exclusions,
function(){
// success! user can continue...
// start experiment, with or without preloading
if(opts.auto_preload){
jsPsych.pluginAPI.autoPreload(timeline, startExperiment);
if(opts.max_load_time > 0){
setTimeout(function(){
if(!loaded){
loadFail();
}
}, opts.max_load_time);
}
} else {
startExperiment();
}
},
function(){
// fail. incompatible user.
}
);
};
core.progress = function() {
var percent_complete = typeof timeline == 'undefined' ? 0 : timeline.percentComplete();
var obj = {
"total_trials": typeof timeline == 'undefined' ? undefined : timeline.length(),
"current_trial_global": global_trial_index,
"percent_complete": percent_complete
};
return obj;
};
core.startTime = function() {
return exp_start_time;
};
core.totalTime = function() {
if(typeof exp_start_time == 'undefined'){ return 0; }
return (new Date()).getTime() - exp_start_time.getTime();
};
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(nextTrial, opts.default_iti);
} else {
nextTrial();
}
} else {
if (current_trial.timing_post_trial > 0) {
setTimeout(nextTrial, current_trial.timing_post_trial);
} else {
nextTrial();
}
}
}
core.endExperiment = function(end_message) {
timeline.end_message = end_message;
timeline.end();
jsPsych.pluginAPI.cancelAllKeyboardResponses();
jsPsych.pluginAPI.clearAllTimeouts();
core.finishTrial();
}
core.endCurrentTimeline = function() {
timeline.endActiveNode();
}
core.currentTrial = function() {
return current_trial;
};
core.initSettings = function() {
return opts;
};
core.currentTimelineNodeID = function() {
return timeline.activeID();
};
core.timelineVariable = function(varname){
return timeline.timelineVariable(varname);
}
core.addNodeToEndOfTimeline = function(new_timeline, preload_callback){
timeline.insert(new_timeline);
if(opts.auto_preload){
jsPsych.pluginAPI.autoPreload(timeline, preload_callback);
} else {
preload_callback();
}
}
core.pauseExperiment = function(){
paused = true;
}
core.resumeExperiment = function(){
paused = false;
if(waiting){
waiting = false;
nextTrial();
}
}
function TimelineNode(parameters, parent, relativeID) {
// a unique ID for this node, relative to the parent
var relative_id;
// store the parent for this node
var parent_node;
// parameters for the trial if the node contains a trial
var trial_parameters;
// parameters for nodes that contain timelines
var timeline_parameters;
// stores trial information on a node that contains a timeline
// used for adding new trials
var node_trial_data;
// track progress through the node
var progress = {
current_location: -1, // where on the timeline (which timelinenode)
current_variable_set: 0, // which set of variables to use from timeline_variables
current_repetition: 0, // how many times through the variable set on this run of the node
current_iteration: 0, // how many times this node has been revisited
done: false
}
// reference to self
var self = this;
// 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 (typeof timeline_parameters == 'undefined') {
// returns a clone of the trial_parameters to
// protect functions.
return $.extend(true, {}, trial_parameters);
} else {
if (progress.current_location >= timeline_parameters.timeline.length) {
return null;
} else {
return timeline_parameters.timeline[progress.current_location].trial();
}
}
}
this.markCurrentTrialComplete = function() {
if(typeof timeline_parameters == 'undefined'){
progress.done = true;
} else {
timeline_parameters.timeline[progress.current_location].markCurrentTrialComplete();
}
}
this.nextRepetiton = function() {
this.setTimelineVariablesOrder();
progress.current_location = -1;
progress.current_variable_set = 0;
progress.current_repetition++;
for (var i = 0; i < timeline_parameters.timeline.length; i++) {
timeline_parameters.timeline[i].reset();
}
}
// set the order for going through the timeline variables array
// TODO: this is where all the sampling options can be implemented
this.setTimelineVariablesOrder = function() {
var order = [];
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('');
setTimeout(go, 1000);
});
}
} 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!
timeline.advance();
doTrial(timeline.trial());
}
}
function finishExperiment() {
opts.on_finish(jsPsych.data.getData());
if(typeof timeline.end_message !== 'undefined'){
DOM_target.html(timeline.end_message);
}
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
function nextTrial() {
// if experiment is paused, don't do anything.
if(paused) {
waiting = true;
return;
}
global_trial_index++;
// advance timeline
timeline.markCurrentTrialComplete();
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());
}
function doTrial(trial) {
current_trial = trial;
// call experiment wide callback
opts.on_trial_start();
// check if trial has it's own display element
var display_element = DOM_target;
if(typeof trial.display_element !== 'undefined'){
display_element = trial.display_element;
}
// execute trial method
jsPsych.plugins[trial.type].trial(display_element, trial);
}
function loadFail(){
DOM_target.html('
The experiment failed to load.
');
}
function checkExclusions(exclusions, success, fail){
var clear = true;
// MINIMUM SIZE
if(typeof exclusions.min_width !== 'undefined' || typeof exclusions.min_height !== 'undefined'){
var mw = typeof exclusions.min_width !== 'undefined' ? exclusions.min_width : 0;
var mh = typeof exclusions.min_height !== 'undefined' ? exclusions.min_height : 0;
var w = window.innerWidth;
var h = window.innerHeight;
if(w < mw || h < mh){
clear = false;
var interval = setInterval(function(){
var w = window.innerWidth;
var h = window.innerHeight;
if(w < mw || h < mh){
var msg = '
Your browser window is too small to complete this experiment. '+
'Please maximize the size of your browser window. If your browser window is already maximized, '+
'you will not be able to complete this experiment.
'+
'
The minimum width is '+mw+'px. Your current width is '+w+'px.
'+
'
The minimum height is '+mh+'px. Your current height is '+h+'px.
';
core.getDisplayElement().html(msg);
} else {
clearInterval(interval);
core.getDisplayElement().empty();
checkExclusions(exclusions, success, fail);
}
}, 100);
return; // prevents checking other exclusions while this is being fixed
}
}
// GO?
if(clear){ success(); }
}
function drawProgressBar() {
$('.jspsych-display-element').prepend(
'