/** * jspsych.js * Josh de Leeuw * * documentation: docs.jspsych.org * **/ window.jsPsych = (function() { var core = {}; // // private variables // // options var opts = {}; // experiment timeline var timeline; // flow control var global_trial_index = 0; var current_trial = {}; var current_trial_finished = false; // target DOM element var DOM_container; 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; var loadfail = false; // is the page retrieved directly via file:// protocol (true) or hosted on a server (false)? var file_protocol = false; // storing a single webaudio context to prevent problems with multiple inits // of jsPsych core.webaudio_context = null; // temporary patch for Safari if (typeof window !== 'undefined' && window.hasOwnProperty('webkitAudioContext') && !window.hasOwnProperty('AudioContext')) { window.AudioContext = webkitAudioContext; } // end patch core.webaudio_context = (typeof window !== 'undefined' && typeof window.AudioContext !== 'undefined') ? new AudioContext() : null; // enumerated variables for special parameter types core.ALL_KEYS = 'allkeys'; core.NO_KEYS = 'none'; // // public methods // core.init = function(options) { function init() { if(typeof options.timeline === 'undefined'){ console.error('No timeline declared in jsPsych.init. Cannot start experiment.') } if(options.timeline.length == 0){ console.error('No trials have been added to the timeline (the timeline is an empty array). Cannot start experiment.') } // reset variables timeline = null; global_trial_index = 0; current_trial = {}; current_trial_finished = false; paused = false; waiting = false; loaded = false; loadfail = false; file_protocol = false; jsPsych.data.reset(); var defaults = { 'display_element': undefined, 'on_finish': function(data) { return undefined; }, 'on_trial_start': function(trial) { return undefined; }, 'on_trial_finish': function() { return undefined; }, 'on_data_update': function(data) { return undefined; }, 'on_interaction_data_update': function(data){ return undefined; }, 'on_close': function(){ return undefined; }, 'preload_images': [], 'preload_audio': [], 'preload_video': [], 'use_webaudio': true, 'exclusions': {}, 'show_progress_bar': false, 'message_progress_bar': 'Completion Progress', 'auto_update_progress_bar': true, 'auto_preload': true, 'show_preload_progress_bar': true, 'max_load_time': 60000, 'max_preload_attempts': 10, 'default_iti': 0, 'minimum_valid_rt': 0, 'experiment_width': null, 'override_safe_mode': false }; // detect whether page is running in browser as a local file, and if so, disable web audio and video preloading to prevent CORS issues if (window.location.protocol == 'file:' && (options.override_safe_mode === false || typeof options.override_safe_mode == 'undefined')) { options.use_webaudio = false; file_protocol = true; console.warn("jsPsych detected that it is running via the file:// protocol and not on a web server. "+ "To prevent issues with cross-origin requests, Web Audio and video preloading have been disabled. "+ "If you would like to override this setting, you can set 'override_safe_mode' to 'true' in jsPsych.init. "+ "For more information, see: https://www.jspsych.org/overview/running-experiments"); } // override default options if user specifies an option opts = Object.assign({}, defaults, options); // set DOM element where jsPsych will render content // if undefined, then jsPsych will use the tag and the entire page if(typeof opts.display_element == 'undefined'){ // check if there is a body element on the page var body = document.querySelector('body'); if (body === null) { document.documentElement.appendChild(document.createElement('body')); } // using the full page, so we need the HTML element to // have 100% height, and body to be full width and height with // no margin document.querySelector('html').style.height = '100%'; document.querySelector('body').style.margin = '0px'; document.querySelector('body').style.height = '100%'; document.querySelector('body').style.width = '100%'; opts.display_element = document.querySelector('body'); } else { // make sure that the display element exists on the page var display; if (opts.display_element instanceof Element) { var display = opts.display_element; } else { var display = document.querySelector('#' + opts.display_element); } if(display === null) { console.error('The display_element specified in jsPsych.init() does not exist in the DOM.'); } else { opts.display_element = display; } } opts.display_element.innerHTML = '
'; DOM_container = opts.display_element; DOM_target = document.querySelector('#jspsych-content'); // add tabIndex attribute to scope event listeners opts.display_element.tabIndex = 0; // add CSS class to DOM_target if(opts.display_element.className.indexOf('jspsych-display-element') == -1){ opts.display_element.className += ' jspsych-display-element'; } DOM_target.className += 'jspsych-content'; // set experiment_width if not null if(opts.experiment_width !== null){ DOM_target.style.width = opts.experiment_width + "px"; } // create experiment timeline timeline = new TimelineNode({ timeline: opts.timeline }); // initialize audio context based on options and browser capabilities jsPsych.pluginAPI.initAudio(); // below code resets event listeners that may have lingered from // a previous incomplete experiment loaded in same DOM. jsPsych.pluginAPI.reset(opts.display_element); // create keyboard event listeners jsPsych.pluginAPI.createKeyboardEventListeners(opts.display_element); // create listeners for user browser interaction jsPsych.data.createInteractionListeners(); // add event for closing window window.addEventListener('beforeunload', opts.on_close); // 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, file_protocol, opts.preload_images, opts.preload_audio, opts.preload_video, opts.show_preload_progress_bar); if(opts.max_load_time > 0){ setTimeout(function(){ if(!loaded && !loadfail){ core.loadFail(); } }, opts.max_load_time); } } else { startExperiment(); } }, function(){ // fail. incompatible user. } ); }; // execute init() when the document is ready if (document.readyState === "complete") { init(); } else { window.addEventListener("load", init); } } 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.getDisplayContainerElement = function(){ return DOM_container; } core.finishTrial = function(data) { if(current_trial_finished){ return; } current_trial_finished = true; // 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.get().filter({trial_index: global_trial_index}); // for trial-level callbacks, we just want to pass in a reference to the values // of the DataCollection, for easy access and editing. var trial_data_values = trial_data.values()[0]; // handle callback at plugin level if (typeof current_trial.on_finish === 'function') { current_trial.on_finish(trial_data_values); } // handle callback at whole-experiment level opts.on_trial_finish(trial_data_values); // after the above callbacks are complete, then the data should be finalized // for this trial. call the on_data_update handler, passing in the same // data object that just went through the trial's finish handlers. opts.on_data_update(trial_data_values); // wait for iti if (typeof current_trial.post_trial_gap === null || typeof current_trial.post_trial_gap === 'undefined') { if (opts.default_iti > 0) { setTimeout(nextTrial, opts.default_iti); } else { nextTrial(); } } else { if (current_trial.post_trial_gap > 0) { setTimeout(nextTrial, current_trial.post_trial_gap); } 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, execute){ if(execute){ return timeline.timelineVariable(varname); } else { return function() { return timeline.timelineVariable(varname); } } } core.addNodeToEndOfTimeline = function(new_timeline, preload_callback){ timeline.insert(new_timeline); if(typeof preload_callback !== 'undefined'){ if(opts.auto_preload){ jsPsych.pluginAPI.autoPreload(timeline, preload_callback, file_protocol); } else { preload_callback(); } } } core.pauseExperiment = function(){ paused = true; } core.resumeExperiment = function(){ paused = false; if(waiting){ waiting = false; nextTrial(); } } core.loadFail = function(message){ message = message || '

The experiment failed to load.

'; loadfail = true; DOM_target.innerHTML = message; } 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 jsPsych.utils.deepCopy(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 this.setTimelineVariablesOrder = function() { // check to make sure this node has variables if(typeof timeline_parameters === 'undefined' || typeof timeline_parameters.timeline_variables === 'undefined'){ return; } var order = []; for(var i=0; i 1, and only when on the first variable set if (typeof timeline_parameters.conditional_function !== 'undefined' && progress.current_repetition==0 && progress.current_variable_set == 0) { var conditional_result = timeline_parameters.conditional_function(); // if the conditional_function() returns false, then the timeline // doesn't run and is marked as complete. if (conditional_result == false) { progress.done = true; return true; } // // if the conditonal_function() returns true, then the node can start // else { // progress.current_location = 0; // } } // if we reach this point then the node has its own timeline and will start // so we need to check if there is an on_timeline_start function if (typeof timeline_parameters.on_timeline_start !== 'undefined'){ timeline_parameters.on_timeline_start(); } // // if there is no conditional_function, then the node can start // else { // progress.current_location = 0; // } } // if we reach this point, then either the node doesn't have a timeline of the // conditional function returned true and it can start progress.current_location = 0; // call advance again on this node now that it is pointing to a new location return this.advance(); } // if this node has a timeline, propogate down to the current trial. if (typeof timeline_parameters !== 'undefined') { var have_node_to_run = false; // keep incrementing the location in the timeline until one of the nodes reached is incomplete while (progress.current_location < timeline_parameters.timeline.length && have_node_to_run == false) { // check to see if the node currently pointed at is done var target_complete = timeline_parameters.timeline[progress.current_location].advance(); if (!target_complete) { have_node_to_run = true; return false; } else { progress.current_location++; } } // if we've reached the end of the timeline (which, if the code is here, we have) // there are a few steps to see what to do next... // first, check the timeline_variables to see if we need to loop through again // with a new set of variables if (progress.current_variable_set < progress.order.length - 1) { // reset the progress of the node to be with the new set this.nextSet(); // then try to advance this node again. return this.advance(); } // if we're all done with the timeline_variables, then check to see if there are more repetitions else if (progress.current_repetition < timeline_parameters.repetitions - 1) { this.nextRepetiton(); // check to see if there is an on_timeline_finish function if (typeof timeline_parameters.on_timeline_finish !== 'undefined'){ timeline_parameters.on_timeline_finish(); } return this.advance(); } // if we're all done with the repetitions... else { // check to see if there is an on_timeline_finish function if (typeof timeline_parameters.on_timeline_finish !== 'undefined'){ timeline_parameters.on_timeline_finish(); } // check to see if there is a loop_function if (typeof timeline_parameters.loop_function !== 'undefined') { if (timeline_parameters.loop_function(this.generatedData())) { this.reset(); return parent_node.advance(); } else { progress.done = true; return true; } } } // no more loops on this timeline, we're done! progress.done = true; return true; } } // check the status of the done flag this.isComplete = function() { return progress.done; } // getter method for timeline variables this.getTimelineVariableValue = function(variable_name){ if(typeof timeline_parameters == 'undefined'){ return undefined; } var v = timeline_parameters.timeline_variables[progress.order[progress.current_variable_set]][variable_name]; return v; } // recursive upward search for timeline variables this.findTimelineVariable = function(variable_name){ var v = this.getTimelineVariableValue(variable_name); if(typeof v == 'undefined'){ if(typeof parent_node !== 'undefined'){ return parent_node.findTimelineVariable(variable_name); } else { return undefined; } } else { return v; } } // recursive downward search for active trial to extract timeline variable this.timelineVariable = function(variable_name){ if(typeof timeline_parameters == 'undefined'){ return this.findTimelineVariable(variable_name); } else { // if progress.current_location is -1, then the timeline variable is being evaluated // in a function that runs prior to the trial starting, so we should treat that trial // as being the active trial for purposes of finding the value of the timeline variable var loc = Math.max(0, progress.current_location); // if loc is greater than the number of elements on this timeline, then the timeline // variable is being evaluated in a function that runs after the trial on the timeline // are complete but before advancing to the next (like a loop_function). // treat the last active trial as the active trial for this purpose. if(loc == timeline_parameters.timeline.length){ loc = loc - 1; } // now find the variable return timeline_parameters.timeline[loc].timelineVariable(variable_name); } } // 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 (typeof timeline_parameters !== 'undefined') { for (var i = 0; i < timeline_parameters.timeline.length; i++) { length += timeline_parameters.timeline[i].length(); } } else { return 1; } return length; } // 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_parameters.timeline.length; i++) { if (timeline_parameters.timeline[i].isComplete()) { completed_trials += timeline_parameters.timeline[i].length(); } } return (completed_trials / total_trials * 100) } // resets the node and all subnodes to original state // but increments the current_iteration counter this.reset = function() { progress.current_location = -1; progress.current_repetition = 0; progress.current_variable_set = 0; progress.current_iteration++; progress.done = false; this.setTimelineVariablesOrder(); if (typeof timeline_parameters != 'undefined') { for (var i = 0; i < timeline_parameters.timeline.length; i++) { timeline_parameters.timeline[i].reset(); } } } // mark this node as finished this.end = function() { progress.done = true; } // recursively end whatever sub-node is running the current trial this.endActiveNode = function() { if (typeof timeline_parameters == 'undefined') { this.end(); parent_node.end(); } else { timeline_parameters.timeline[progress.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." + progress.current_iteration; } else { id += parent_node.ID() + "-"; id += relative_id + "." + progress.current_iteration; return id; } } // get the ID of the active trial this.activeID = function() { if (typeof timeline_parameters == 'undefined') { return this.ID(); } else { return timeline_parameters.timeline[progress.current_location].activeID(); } } // get all the data generated within this node this.generatedData = function() { return jsPsych.data.getDataByTimelineNode(this.ID()); } // get all the trials of a particular type this.trialsOfType = function(type) { if (typeof timeline_parameters == 'undefined'){ if (trial_parameters.type == type) { return trial_parameters; } else { return []; } } else { var trials = []; for (var i = 0; i < timeline_parameters.timeline.length; i++) { var t = timeline_parameters.timeline[i].trialsOfType(type); trials = trials.concat(t); } return trials; } } // add new trials to end of this timeline this.insert = function(parameters){ if(typeof timeline_parameters == 'undefined'){ console.error('Cannot add new trials to a trial-level node.'); } else { timeline_parameters.timeline.push( new TimelineNode(Object.assign({}, node_trial_data, parameters), self, timeline_parameters.timeline.length) ); } } // 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') || (typeof jsPsych.plugins[trial_type] == 'function')) { // create timeline properties timeline_parameters = { timeline: [], loop_function: parameters.loop_function, conditional_function: parameters.conditional_function, sample: parameters.sample, 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, on_timeline_finish: parameters.on_timeline_finish, on_timeline_start: parameters.on_timeline_start, }; self.setTimelineVariablesOrder(); // extract all of the node level data and parameters // but remove all of the timeline-level specific information // since this will be used to copy things down hierarchically var node_data = Object.assign({}, 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; delete node_data.sample; delete node_data.on_timeline_start; delete node_data.on_timeline_finish; node_trial_data = node_data; // store for later... // create a TimelineNode for each element in the timeline for (var i = 0; i < parameters.timeline.length; i++) { // merge parameters var merged_parameters = Object.assign({}, node_data, parameters.timeline[i]); // merge any data from the parent node into child nodes if(typeof node_data.data == 'object' && typeof parameters.timeline[i].data == 'object'){ var merged_data = Object.assign({}, node_data.data, parameters.timeline[i].data); merged_parameters.data = merged_data; } timeline_parameters.timeline.push(new TimelineNode(merged_parameters, 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') && (trial_type.toString().replace(/\s/g,'') != "function(){returntimeline.timelineVariable(varname);}")) { console.error('No plugin loaded for trials of type "' + trial_type + '"'); } // create a deep copy of the parameters for the trial trial_parameters = Object.assign({}, parameters); } }(); } function startExperiment() { loaded = true; // show progress bar if requested if (opts.show_progress_bar === true) { drawProgressBar(opts.message_progress_bar); } // record the start time exp_start_time = new Date(); // begin! timeline.advance(); doTrial(timeline.trial()); } function finishExperiment() { if(typeof timeline.end_message !== 'undefined'){ DOM_target.innerHTML = timeline.end_message; } opts.on_finish(jsPsych.data.get()); } 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 && opts.auto_update_progress_bar == true) { updateProgressBar(); } // check if experiment is over if (complete) { finishExperiment(); return; } doTrial(timeline.trial()); } function doTrial(trial) { current_trial = trial; current_trial_finished = false; // process all timeline variables for this trial evaluateTimelineVariables(trial); // evaluate variables that are functions evaluateFunctionParameters(trial); // get default values for parameters setDefaultValues(trial); // call experiment wide callback opts.on_trial_start(trial); // call trial specific callback if it exists if(typeof trial.on_start == 'function'){ trial.on_start(trial); } // apply the focus to the element containing the experiment. DOM_container.focus(); // reset the scroll on the DOM target DOM_target.scrollTop = 0; // execute trial method jsPsych.plugins[trial.type].trial(DOM_target, trial); // call trial specific loaded callback if it exists if(typeof trial.on_load == 'function'){ trial.on_load(); } } function evaluateTimelineVariables(trial){ var keys = Object.keys(trial); for (var i = 0; i < keys.length; i++) { // timeline variables on the root level if (typeof trial[keys[i]] == "function" && trial[keys[i]].toString().replace(/\s/g,'') == "function(){returntimeline.timelineVariable(varname);}") { trial[keys[i]] = trial[keys[i]].call(); } // timeline variables that are nested in objects if (typeof trial[keys[i]] == "object" && trial[keys[i]] !== null){ evaluateTimelineVariables(trial[keys[i]]); } } } function evaluateFunctionParameters(trial){ // first, eval the trial type if it is a function // this lets users set the plugin type with a function if(typeof trial.type === 'function'){ trial.type = trial.type.call(); } // now eval the whole trial // start by getting a list of the parameters var keys = Object.keys(trial); // iterate over each parameter for (var i = 0; i < keys.length; i++) { // check to make sure parameter is not "type", since that was eval'd above. if(keys[i] !== 'type'){ // this if statement is checking to see if the parameter type is expected to be a function, in which case we should NOT evaluate it. // the first line checks if the parameter is defined in the universalPluginParameters set // the second line checks the plugin-specific parameters if( (typeof jsPsych.plugins.universalPluginParameters[keys[i]] !== 'undefined' && jsPsych.plugins.universalPluginParameters[keys[i]].type !== jsPsych.plugins.parameterType.FUNCTION ) || (typeof jsPsych.plugins[trial.type].info.parameters[keys[i]] !== 'undefined' && jsPsych.plugins[trial.type].info.parameters[keys[i]].type !== jsPsych.plugins.parameterType.FUNCTION) ) { if (typeof trial[keys[i]] == "function") { trial[keys[i]] = trial[keys[i]].call(); } } } // add a special exception for the data parameter so we can evaluate functions. eventually this could be generalized so that any COMPLEX object type could // be evaluated at the individual parameter level. if(keys[i] == 'data'){ var data_params = Object.keys(trial[keys[i]]); for(var j=0; j'+ '

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().innerHTML = msg; } else { clearInterval(interval); core.getDisplayElement().innerHTML = ''; checkExclusions(exclusions, success, fail); } }, 100); return; // prevents checking other exclusions while this is being fixed } } // WEB AUDIO API if(typeof exclusions.audio !== 'undefined' && exclusions.audio) { if(window.hasOwnProperty('AudioContext') || window.hasOwnProperty('webkitAudioContext')){ // clear } else { clear = false; var msg = '

Your browser does not support the WebAudio API, which means that you will not '+ 'be able to complete the experiment.

Browsers that support the WebAudio API include '+ 'Chrome, Firefox, Safari, and Edge.

'; core.getDisplayElement().innerHTML = msg; fail(); return; } } // GO? if(clear){ success(); } } function drawProgressBar(msg) { document.querySelector('.jspsych-display-element').insertAdjacentHTML('afterbegin', '
'+ ''+ msg+ ''+ '
'+ '
'+ '
'); } function updateProgressBar() { var progress = jsPsych.progress().percent_complete; core.setProgressBar(progress / 100); } var progress_bar_amount = 0; core.setProgressBar = function(proportion_complete){ proportion_complete = Math.max(Math.min(1,proportion_complete),0); document.querySelector('#jspsych-progressbar-inner').style.width = (proportion_complete*100) + "%"; progress_bar_amount = proportion_complete; } core.getProgressBarCompleted = function(){ return progress_bar_amount; } //Leave a trace in the DOM that jspsych was loaded document.documentElement.setAttribute('jspsych', 'present'); return core; })(); jsPsych.plugins = (function() { var module = {}; // enumerate possible parameter types for plugins module.parameterType = { BOOL: 0, STRING: 1, INT: 2, FLOAT: 3, FUNCTION: 4, KEYCODE: 5, SELECT: 6, HTML_STRING: 7, IMAGE: 8, AUDIO: 9, VIDEO: 10, OBJECT: 11, COMPLEX: 12 } module.universalPluginParameters = { data: { type: module.parameterType.OBJECT, pretty_name: 'Data', default: {}, description: 'Data to add to this trial (key-value pairs)' }, on_start: { type: module.parameterType.FUNCTION, pretty_name: 'On start', default: function() { return; }, description: 'Function to execute when trial begins' }, on_finish: { type: module.parameterType.FUNCTION, pretty_name: 'On finish', default: function() { return; }, description: 'Function to execute when trial is finished' }, on_load: { type: module.parameterType.FUNCTION, pretty_name: 'On load', default: function() { return; }, description: 'Function to execute after the trial has loaded' }, post_trial_gap: { type: module.parameterType.INT, pretty_name: 'Post trial gap', default: null, description: 'Length of gap between the end of this trial and the start of the next trial' } } return module; })(); jsPsych.data = (function() { var module = {}; // data storage object var allData = DataCollection(); // browser interaction event data var interactionData = DataCollection(); // data properties for all trials var dataProperties = {}; // cache the query_string var query_string; // DataCollection function DataCollection(data){ var data_collection = {}; var trials = typeof data === 'undefined' ? [] : data; data_collection.push = function(new_data){ trials.push(new_data); return data_collection; } data_collection.join = function(other_data_collection){ trials = trials.concat(other_data_collection.values()); return data_collection; } data_collection.top = function(){ if(trials.length <= 1){ return data_collection; } else { return DataCollection([trials[trials.length-1]]); } } /** * Queries the first n elements in a collection of trials. * * @param {number} n A positive integer of elements to return. A value of * n that is less than 1 will throw an error. * * @return {Array} First n objects of a collection of trials. If fewer than * n trials are available, the trials.length elements will * be returned. * */ data_collection.first = function(n){ if (typeof n == 'undefined') { n = 1 } if (n < 1) { throw `You must query with a positive nonzero integer. Please use a different value for n.`; } if (trials.length == 0) return DataCollection([]); if (n > trials.length) n = trials.length; return DataCollection(trials.slice(0, n)); } /** * Queries the last n elements in a collection of trials. * * @param {number} n A positive integer of elements to return. A value of * n that is less than 1 will throw an error. * * @return {Array} Last n objects of a collection of trials. If fewer than * n trials are available, the trials.length elements will * be returned. * */ data_collection.last = function(n) { if (typeof n == 'undefined') { n = 1 } if (n < 1) { throw `You must query with a positive nonzero integer. Please use a different value for n.`; } if (trials.length == 0) return DataCollection([]); if (n > trials.length) n = trials.length; return DataCollection(trials.slice(trials.length - n, trials.length)); } data_collection.values = function(){ return trials; } data_collection.count = function(){ return trials.length; } data_collection.readOnly = function(){ return DataCollection(jsPsych.utils.deepCopy(trials)); } data_collection.addToAll = function(properties){ for (var i = 0; i < trials.length; i++) { for (var key in properties) { trials[i][key] = properties[key]; } } return data_collection; } data_collection.addToLast = function(properties){ if(trials.length != 0){ for (var key in properties) { trials[trials.length-1][key] = properties[key]; } } return data_collection; } data_collection.filter = function(filters){ // [{p1: v1, p2:v2}, {p1:v2}] // {p1: v1} if(!Array.isArray(filters)){ var f = jsPsych.utils.deepCopy([filters]); } else { var f = jsPsych.utils.deepCopy(filters); } var filtered_data = []; for(var x=0; x < trials.length; x++){ var keep = false; for(var i=0; i