/** * 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 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()); } }; core.endExperiment = function(end_message) { timeline.end_message = end_message; timeline.end(); } 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); } 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; // 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: -1, // 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; // 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; } else { relative_id = relativeID; } // check if there is a timeline parameter // if there is, then this node has its own timeline if (typeof parameters.timeline !== 'undefined') { // create timeline properties timeline_parameters = { timeline: [], loop_function: parameters.loop_function, conditional_function: parameters.conditional_function, randomize_order: typeof parameters.randomize_order == 'undefined' ? false : parameters.randomize_order, repetitions: typeof parameters.repetitions == 'undefined' ? 1 : parameters.repetitions, timeline_variables: typeof parameters.timeline_variables == 'undefined' ? [{}] : parameters.timeline_variables }; // 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; delete node_data.randomize_order; delete node_data.repetitions; delete node_data.timeline_variables; // create a TimelineNode for each element in the timeline for (var i = 0; i < parameters.timeline.length; i++) { timeline_parameters.timeline.push(new TimelineNode($.extend(true, {}, node_data, parameters.timeline[i]), self, i)); } } // 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_parameters = $.extend(true, {}, parameters); } }(); // 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(); } } } // start this timeline // moves it from 'waiting' status (progress.current_location == -1) // to 'active' status (progress.current_location > -1) // and performs any sampling/randomization needed this.start = function() { // check if there is a conditional function if(typeof timeline_parameters != 'undefined'){ if(typeof timeline_parameters.conditional_function !== 'undefined'){ var conditional_result = timeline_parameters.conditional_function(); if(conditional_result == false){ return false; } } } // set up order for progressing through the timeline variables if(typeof timeline_parameters !== 'undefined') { this.nextRepetiton(); } // set the location to the head of the timeline progress.current_location = 0; return true; } this.markCurrentTrialComplete = function() { if(typeof timeline_parameters == 'undefined'){ progress.done = true; } else { timeline_parameters.timeline[progress.current_location].markCurrentTrialComplete(); } } // set the order for going through the timeline variables array // TODO: this is where all the sampling options can be implemented this.nextRepetiton = function() { var order = []; for(var i=0; iThe experiment will launch in fullscreen mode when you click the button below.