/** * 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 structure 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 obj = { "total_trials": timeline.length(), "current_trial_global": global_trial_index, "total_chunks": undefined, // TODO: implement this "current_chunk": undefined // TODO: implement this }; 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() { // logic to advance to next trial? // handle callback at plugin level if (typeof current_trial.on_finish === 'function') { var trial_data = jsPsych.data.getDataByTrialIndex(global_trial_index); current_trial.on_finish(trial_data); } // handle callback at whole-experiment level opts.on_trial_finish(); // 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 parseExpStructure(experiment_structure) { if (!Array.isArray(experiment_structure)) { throw new Error("Invalid experiment structure. Experiment structure must be an array"); } return createExperimentChunk({ chunk_type: 'root', timeline: experiment_structure }); } 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') { // create a TimelineNode for each element in the timeline for (var i = 0; i < parameters.timeline.length; i++) { timeline.push(new TimelineNode(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 { // 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.isComplete()) { 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.isComplete() && timeline[current_location].isComplete()) { current_location++; } if (this.isComplete()) { 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++; return true; } } // return true if the node is completely done (no more possible trials) // otherwise, return false this.isComplete = 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) { 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.reset(); } else { return true; } } else { 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 return true; } } } return false; } // 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()); } } function startExperiment(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.