/** * 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, 'auto_preload': true, '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 }); // preloading if(opts.auto_preload){ jsPsych.pluginAPI.autoPreload(timeline, startExperiment); } else { startExperiment(); } }; 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.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 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.plugins[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() { // first check to see if this node is done if(done_flag){ return true; } // 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 < timeline.length; i++) { if (timeline[i].isComplete()) { completed_trials += timeline[i].length(); } } return (completed_trials / total_trials * 100) } // reset the location pointer to the start of the timeline, and reset all the // child nodes on the timeline. this.reset = function() { current_location = 0; done_flag = false; for (var i = 0; i < timeline.length; i++) { timeline[i].reset(); } current_iteration++; if (randomize_order === true) { timeline = jsPsych.randomization.shuffle(timeline); } } // mark this node as finished this.end = function() { done_flag = true; } // recursively end whatever sub-node is running the current trial this.endActiveNode = function() { if (timeline.length == 0) { this.end(); parent_node.end(); } else { timeline[current_location].endActiveNode(); } } // get a unique ID associated with this node // the ID reflects the current iteration through this node. this.ID = function() { var id = ""; if (typeof parent_node == 'undefined') { return "0." + current_iteration; } else { id += parent_node.ID() + "-"; id += relative_id + "." + current_iteration; return id; } } // get the ID of the active trial this.activeID = function() { if (timeline.length == 0) { return this.ID(); } else { return timeline[current_location].activeID(); } } // get all the data generated within this node this.generatedData = function() { return jsPsych.data.getTrialsFromChunk(this.ID()); } // get all the trials of a particular type this.trialsOfType = function(type) { if (timeline.length == 0) { if (trial_data.type == type) { return trial_data; } else { return []; } } else { var trials = []; for (var i = 0; i < timeline.length; i++) { var t = timeline[i].trialsOfType(type); trials = trials.concat(t); } return trials; } } } function startExperiment() { var fullscreen = opts.fullscreen; // fullscreen setup if (fullscreen) { // check if keys are allowed in fullscreen mode var keyboardNotAllowed = typeof Element !== 'undefined' && 'ALLOW_KEYBOARD_INPUT' in Element; if (keyboardNotAllowed) { go(); } else { DOM_target.append('The experiment will launch in fullscreen mode when you click the button below.