From b80872642f1011e21b1b2e219c90399c743a6a4c Mon Sep 17 00:00:00 2001 From: Sam Zorowitz Date: Wed, 16 Dec 2020 10:10:02 -0500 Subject: [PATCH] version 1.1 (MTurk) (#49) --- README.rst | 8 +- app/__init__.py | 2 +- .../plugins/jspsych-free-sort.js | 194 -------- .../plugins/jspsych-image-button-response.js | 224 --------- .../plugins/jspsych-image-slider-response.js | 227 --------- .../css/jspsych.css | 85 +++- .../jspsych.js | 443 +++++++++-------- .../license.txt | 0 .../plugins/jspsych-animation.js | 51 +- .../plugins/jspsych-audio-button-response.js | 59 ++- .../jspsych-audio-keyboard-response.js | 71 ++- .../plugins/jspsych-audio-slider-response.js | 82 +++- .../plugins/jspsych-call-function.js | 0 .../plugins/jspsych-canvas-button-response.js | 199 ++++++++ .../jspsych-canvas-keyboard-response.js} | 85 ++-- .../plugins/jspsych-canvas-slider-response.js | 207 ++++++++ .../plugins/jspsych-categorize-animation.js | 76 ++- .../plugins/jspsych-categorize-html.js | 0 .../plugins/jspsych-categorize-image.js | 0 .../plugins/jspsych-cloze.js | 6 +- .../plugins/jspsych-external-html.js | 0 .../plugins/jspsych-free-sort.js | 444 ++++++++++++++++++ .../plugins/jspsych-fullscreen.js | 0 .../plugins/jspsych-html-button-response.js | 2 +- .../plugins/jspsych-html-keyboard-response.js | 0 .../plugins/jspsych-html-slider-response.js | 31 +- .../plugins/jspsych-iat-html.js | 0 .../plugins/jspsych-iat-image.js | 0 .../plugins/jspsych-image-button-response.js | 311 ++++++++++++ .../jspsych-image-keyboard-response.js | 247 ++++++++++ .../plugins/jspsych-image-slider-response.js | 353 ++++++++++++++ .../plugins/jspsych-instructions.js | 8 +- .../jspsych-6.2.0/plugins/jspsych-maxdiff.js | 174 +++++++ .../plugins/jspsych-rdk.js | 11 +- .../plugins/jspsych-reconstruction.js | 0 .../plugins/jspsych-resize.js | 0 .../plugins/jspsych-same-different-html.js | 10 +- .../plugins/jspsych-same-different-image.js | 10 +- .../jspsych-serial-reaction-time-mouse.js | 2 +- .../plugins/jspsych-serial-reaction-time.js | 0 .../plugins/jspsych-survey-html-form.js | 31 +- .../plugins/jspsych-survey-likert.js | 17 +- .../plugins/jspsych-survey-multi-choice.js | 16 +- .../plugins/jspsych-survey-multi-select.js | 13 +- .../plugins/jspsych-survey-text.js | 13 +- .../plugins/jspsych-video-button-response.js | 77 ++- .../jspsych-video-keyboard-response.js | 75 ++- .../plugins/jspsych-video-slider-response.js | 116 +++-- .../plugins/jspsych-visual-search-circle.js | 0 .../plugins/jspsych-vsl-animate-occlusion.js | 0 .../plugins/jspsych-vsl-grid-scene.js | 0 .../template/jspsych-plugin-template.js | 0 app/templates/experiment.html | 6 +- monitor | 30 -- 54 files changed, 2916 insertions(+), 1100 deletions(-) delete mode 100755 app/static/lib/jspsych-6.1.0/plugins/jspsych-free-sort.js delete mode 100755 app/static/lib/jspsych-6.1.0/plugins/jspsych-image-button-response.js delete mode 100755 app/static/lib/jspsych-6.1.0/plugins/jspsych-image-slider-response.js rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/css/jspsych.css (54%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/jspsych.js (84%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/license.txt (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-animation.js (66%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-audio-button-response.js (75%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-audio-keyboard-response.js (68%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-audio-slider-response.js (62%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-call-function.js (100%) mode change 100755 => 100644 create mode 100644 app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-button-response.js rename app/static/lib/{jspsych-6.1.0/plugins/jspsych-image-keyboard-response.js => jspsych-6.2.0/plugins/jspsych-canvas-keyboard-response.js} (57%) mode change 100755 => 100644 create mode 100644 app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-slider-response.js rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-categorize-animation.js (63%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-categorize-html.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-categorize-image.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-cloze.js (94%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-external-html.js (100%) mode change 100755 => 100644 create mode 100644 app/static/lib/jspsych-6.2.0/plugins/jspsych-free-sort.js rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-fullscreen.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-html-button-response.js (96%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-html-keyboard-response.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-html-slider-response.js (80%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-iat-html.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-iat-image.js (100%) mode change 100755 => 100644 create mode 100644 app/static/lib/jspsych-6.2.0/plugins/jspsych-image-button-response.js create mode 100644 app/static/lib/jspsych-6.2.0/plugins/jspsych-image-keyboard-response.js create mode 100644 app/static/lib/jspsych-6.2.0/plugins/jspsych-image-slider-response.js rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-instructions.js (91%) mode change 100755 => 100644 create mode 100644 app/static/lib/jspsych-6.2.0/plugins/jspsych-maxdiff.js rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-rdk.js (97%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-reconstruction.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-resize.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-same-different-html.js (91%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-same-different-image.js (91%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-serial-reaction-time-mouse.js (96%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-serial-reaction-time.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-survey-html-form.js (77%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-survey-likert.js (87%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-survey-multi-choice.js (90%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-survey-multi-select.js (93%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-survey-text.js (90%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-video-button-response.js (73%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-video-keyboard-response.js (68%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-video-slider-response.js (64%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-visual-search-circle.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-vsl-animate-occlusion.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/jspsych-vsl-grid-scene.js (100%) mode change 100755 => 100644 rename app/static/lib/{jspsych-6.1.0 => jspsych-6.2.0}/plugins/template/jspsych-plugin-template.js (100%) mode change 100755 => 100644 delete mode 100644 monitor diff --git a/README.rst b/README.rst index 9707b368..c4fd0951 100644 --- a/README.rst +++ b/README.rst @@ -21,10 +21,10 @@ The following is the minimal set of commands needed to get started with NivTurk cd nivturk pip install -r requirements.txt gunicorn -b 0.0.0.0:9000 -w 4 app:app - -Wiki -^^^^ + +Documentation +^^^^^^^^^^^^^ For details on how to serve your experiment, how the code is organized, and how data is stored, please see the -`Wiki `_. +`Documentation `_. diff --git a/app/__init__.py b/app/__init__.py index a2f8243a..9992ac32 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,7 +3,7 @@ from app import consent, alert, experiment, complete, error from .io import write_metadata from .utils import gen_code -__version__ = '1.0' +__version__ = '1.1' ## Define root directory. ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-free-sort.js b/app/static/lib/jspsych-6.1.0/plugins/jspsych-free-sort.js deleted file mode 100755 index 2eaffaeb..00000000 --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-free-sort.js +++ /dev/null @@ -1,194 +0,0 @@ -/** - * jspsych-free-sort - * plugin for drag-and-drop sorting of a collection of images - * Josh de Leeuw - * - * documentation: docs.jspsych.org - */ - - -jsPsych.plugins['free-sort'] = (function() { - - var plugin = {}; - - jsPsych.pluginAPI.registerPreload('free-sort', 'stimuli', 'image'); - - plugin.info = { - name: 'free-sort', - description: '', - parameters: { - stimuli: { - type: jsPsych.plugins.parameterType.STRING, - pretty_name: 'Stimuli', - default: undefined, - array: true, - description: 'Images to be displayed.' - }, - stim_height: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Stimulus height', - default: 100, - description: 'Height of images in pixels.' - }, - stim_width: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Stimulus width', - default: 100, - description: 'Width of images in pixels' - }, - sort_area_height: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Sort area height', - default: 800, - description: 'The height of the container that subjects can move the stimuli in.' - }, - sort_area_width: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Sort area width', - default: 800, - description: 'The width of the container that subjects can move the stimuli in.' - }, - prompt: { - type: jsPsych.plugins.parameterType.STRING, - pretty_name: 'Prompt', - default: null, - description: 'It can be used to provide a reminder about the action the subject is supposed to take.' - }, - prompt_location: { - type: jsPsych.plugins.parameterType.SELECT, - pretty_name: 'Prompt location', - options: ['above','below'], - default: 'above', - description: 'Indicates whether to show prompt "above" or "below" the sorting area.' - }, - button_label: { - type: jsPsych.plugins.parameterType.STRING, - pretty_name: 'Button label', - default: 'Continue', - description: 'The text that appears on the button to continue to the next trial.' - } - } - } - - plugin.trial = function(display_element, trial) { - - var start_time = performance.now(); - - var html = ""; - // check if there is a prompt and if it is shown above - if (trial.prompt !== null && trial.prompt_location == "above") { - html += trial.prompt; - } - - html += '
'; - - // check if prompt exists and if it is shown below - if (trial.prompt !== null && trial.prompt_location == "below") { - html += trial.prompt; - } - - display_element.innerHTML = html; - - // store initial location data - var init_locations = []; - - for (var i = 0; i < trial.stimuli.length; i++) { - var coords = random_coordinate(trial.sort_area_width - trial.stim_width, trial.sort_area_height - trial.stim_height); - - display_element.querySelector("#jspsych-free-sort-arena").innerHTML += ''+ - ''; - - init_locations.push({ - "src": trial.stimuli[i], - "x": coords.x, - "y": coords.y - }); - } - - display_element.innerHTML += ''; - - var maxz = 1; - - var moves = []; - - var draggables = display_element.querySelectorAll('.jspsych-free-sort-draggable'); - - for(var i=0;i%choice%', - array: true, - description: 'The html of the button. Can create own style.' - }, - prompt: { - type: jsPsych.plugins.parameterType.STRING, - pretty_name: 'Prompt', - default: null, - description: 'Any content here will be displayed under the button.' - }, - stimulus_duration: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Stimulus duration', - default: null, - description: 'How long to hide the stimulus.' - }, - trial_duration: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Trial duration', - default: null, - description: 'How long to show the trial.' - }, - margin_vertical: { - type: jsPsych.plugins.parameterType.STRING, - pretty_name: 'Margin vertical', - default: '0px', - description: 'The vertical margin of the button.' - }, - margin_horizontal: { - type: jsPsych.plugins.parameterType.STRING, - pretty_name: 'Margin horizontal', - default: '8px', - description: 'The horizontal margin of the button.' - }, - response_ends_trial: { - type: jsPsych.plugins.parameterType.BOOL, - pretty_name: 'Response ends trial', - default: true, - description: 'If true, then trial will end when user responds.' - }, - } - } - - plugin.trial = function(display_element, trial) { - - // display stimulus - var html = ''; - - //display buttons - var buttons = []; - if (Array.isArray(trial.button_html)) { - if (trial.button_html.length == trial.choices.length) { - buttons = trial.button_html; - } else { - console.error('Error in image-button-response plugin. The length of the button_html array does not equal the length of the choices array'); - } - } else { - for (var i = 0; i < trial.choices.length; i++) { - buttons.push(trial.button_html); - } - } - html += '
'; - - for (var i = 0; i < trial.choices.length; i++) { - var str = buttons[i].replace(/%choice%/g, trial.choices[i]); - html += '
'+str+'
'; - } - html += '
'; - - //show prompt if there is one - if (trial.prompt !== null) { - html += trial.prompt; - } - - display_element.innerHTML = html; - - // start timing - var start_time = performance.now(); - - for (var i = 0; i < trial.choices.length; i++) { - display_element.querySelector('#jspsych-image-button-response-button-' + i).addEventListener('click', function(e){ - var choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility - after_response(choice); - }); - } - - // store response - var response = { - rt: null, - button: null - }; - - // function to handle responses by the subject - function after_response(choice) { - - // measure rt - var end_time = performance.now(); - var rt = end_time - start_time; - response.button = choice; - response.rt = rt; - - // after a valid response, the stimulus will have the CSS class 'responded' - // which can be used to provide visual feedback that a response was recorded - display_element.querySelector('#jspsych-image-button-response-stimulus').className += ' responded'; - - // disable all the buttons after a response - var btns = document.querySelectorAll('.jspsych-image-button-response-button button'); - for(var i=0; i'; - html += ''; - html += '
'; - html += ''; - html += '
' - for(var j=0; j < trial.labels.length; j++){ - var width = 100/(trial.labels.length-1); - var left_offset = (j * (100 /(trial.labels.length - 1))) - (width/2); - html += '
'; - html += ''+trial.labels[j]+''; - html += '
' - } - html += '
'; - html += '
'; - html += ''; - - if (trial.prompt !== null){ - html += trial.prompt; - } - - // add submit button - html += ''; - - display_element.innerHTML = html; - - var response = { - rt: null, - response: null - }; - - if(trial.require_movement){ - display_element.querySelector('#jspsych-image-slider-response-response').addEventListener('change', function(){ - display_element.querySelector('#jspsych-image-slider-response-next').disabled = false; - }) - } - - display_element.querySelector('#jspsych-image-slider-response-next').addEventListener('click', function() { - // measure response time - var endTime = performance.now(); - response.rt = endTime - startTime; - response.response = display_element.querySelector('#jspsych-image-slider-response-response').value; - - if(trial.response_ends_trial){ - end_trial(); - } else { - display_element.querySelector('#jspsych-image-slider-response-next').disabled = true; - } - - }); - - function end_trial(){ - - jsPsych.pluginAPI.clearAllTimeouts(); - - // save data - var trialdata = { - "rt": response.rt, - "response": response.response - }; - - display_element.innerHTML = ''; - - // next trial - jsPsych.finishTrial(trialdata); - } - - if (trial.stimulus_duration !== null) { - jsPsych.pluginAPI.setTimeout(function() { - display_element.querySelector('#jspsych-image-slider-response-stimulus').style.visibility = 'hidden'; - }, trial.stimulus_duration); - } - - // end trial if trial_duration is set - if (trial.trial_duration !== null) { - jsPsych.pluginAPI.setTimeout(function() { - end_trial(); - }, trial.trial_duration); - } - - var startTime = performance.now(); - }; - - return plugin; -})(); diff --git a/app/static/lib/jspsych-6.1.0/css/jspsych.css b/app/static/lib/jspsych-6.2.0/css/jspsych.css old mode 100755 new mode 100644 similarity index 54% rename from app/static/lib/jspsych-6.1.0/css/jspsych.css rename to app/static/lib/jspsych-6.2.0/css/jspsych.css index 8e05821b..3b6d1774 --- a/app/static/lib/jspsych-6.1.0/css/jspsych.css +++ b/app/static/lib/jspsych-6.2.0/css/jspsych.css @@ -76,9 +76,17 @@ border-color: #ccc; } -.jspsych-btn:hover { +/* only apply the hover style on devices with a mouse/pointer that can hover - issue #977 */ +@media (hover: hover) { + .jspsych-btn:hover { + background-color: #ddd; + border-color: #aaa; + } +} + +.jspsych-btn:active { background-color: #ddd; - border-color: #aaa; + border-color:#000000; } .jspsych-btn:disabled { @@ -88,6 +96,79 @@ cursor: not-allowed; } +/* custom style for input[type="range] (slider) to improve alignment between positions and labels */ + +.jspsych-slider { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + width: 100%; + background: transparent; +} +.jspsych-slider:focus { + outline: none; +} +/* track */ +.jspsych-slider::-webkit-slider-runnable-track { + appearance: none; + -webkit-appearance: none; + width: 100%; + height: 8px; + cursor: pointer; + background: #eee; + box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; + border-radius: 2px; + border: 1px solid #aaa; +} +.jspsych-slider::-moz-range-track { + appearance: none; + width: 100%; + height: 8px; + cursor: pointer; + background: #eee; + box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; + border-radius: 2px; + border: 1px solid #aaa; +} +.jspsych-slider::-ms-track { + appearance: none; + width: 99%; + height: 14px; + cursor: pointer; + background: #eee; + box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; + border-radius: 2px; + border: 1px solid #aaa; +} +/* thumb */ +.jspsych-slider::-webkit-slider-thumb { + border: 1px solid #666; + height: 24px; + width: 15px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; + -webkit-appearance: none; + margin-top: -9px; +} +.jspsych-slider::-moz-range-thumb { + border: 1px solid #666; + height: 24px; + width: 15px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; +} +.jspsych-slider::-ms-thumb { + border: 1px solid #666; + height: 20px; + width: 15px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; + margin-top: -2px; +} + /* jsPsych progress bar */ #jspsych-progressbar-container { diff --git a/app/static/lib/jspsych-6.1.0/jspsych.js b/app/static/lib/jspsych-6.2.0/jspsych.js old mode 100755 new mode 100644 similarity index 84% rename from app/static/lib/jspsych-6.1.0/jspsych.js rename to app/static/lib/jspsych-6.2.0/jspsych.js index 9fb24104..d8787697 --- a/app/static/lib/jspsych-6.1.0/jspsych.js +++ b/app/static/lib/jspsych-6.2.0/jspsych.js @@ -32,6 +32,8 @@ window.jsPsych = (function() { // 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 @@ -52,153 +54,178 @@ window.jsPsych = (function() { // 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 + }; - if(typeof options.timeline === 'undefined'){ - console.error('No timeline declared in jsPsych.init. 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; - 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, - 'experiment_width': null - }; - - // 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.'); + // 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 { - opts.display_element = display; + // 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'); + 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 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'; + // 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"; - } + // 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 - }); + // 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, 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); + // 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(); } - } else { - startExperiment(); - } - }, - function(){ - // fail. incompatible user. + }, + function(){ + // fail. incompatible user. - } - ); - }; + } + ); + }; + + // execute init() when the document is ready + if (document.readyState === "complete") { + init(); + } else { + window.addEventListener("load", init); + } + } core.progress = function() { @@ -309,9 +336,9 @@ window.jsPsych = (function() { core.addNodeToEndOfTimeline = function(new_timeline, preload_callback){ timeline.insert(new_timeline); - if(typeof preload_callback !== 'undefinded'){ + if(typeof preload_callback !== 'undefined'){ if(opts.auto_preload){ - jsPsych.pluginAPI.autoPreload(timeline, preload_callback); + jsPsych.pluginAPI.autoPreload(timeline, preload_callback, file_protocol); } else { preload_callback(); } @@ -577,8 +604,16 @@ window.jsPsych = (function() { // 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); - return timeline_parameters.timeline[loc].timelineVariable(variable_name); + 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); } } @@ -922,7 +957,7 @@ window.jsPsych = (function() { if(jsPsych.plugins[trial.type].info.parameters[param].type == jsPsych.plugins.parameterType.COMPLEX){ if(jsPsych.plugins[trial.type].info.parameters[param].array == true){ // iterate over each entry in the array - for(var i in trial[param]){ + trial[param].forEach(function(ip, i){ // check each parameter in the plugin description for(var p in jsPsych.plugins[trial.type].info.parameters[param].nested){ if(typeof trial[param][i][p] == 'undefined' || trial[param][i][p] === null){ @@ -933,7 +968,7 @@ window.jsPsych = (function() { } } } - } + }); } } // if it's not nested, checking is much easier and do that here: @@ -1129,22 +1164,48 @@ jsPsych.data = (function() { } } + /** + * 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 } - var out = []; - for(var i=0; i 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(){ @@ -1725,7 +1786,7 @@ jsPsych.randomization = (function() { repetitions = reps; } else { // throw warning if too long, and then use the first N - repetitions = repetions.slice(0, array.length); + repetitions = repetitions.slice(0, array.length); } } } @@ -1764,7 +1825,7 @@ jsPsych.randomization = (function() { if(!Array.isArray(arr)){ console.error('First argument to jsPsych.randomization.shuffleNoRepeats() must be an array.') } - if(typeof equalityTest !== 'undefined' || typeof equalityTest !== 'function'){ + if(typeof equalityTest !== 'undefined' && typeof equalityTest !== 'function'){ console.error('Second argument to jsPsych.randomization.shuffleNoRepeats() must be a function.') } // define a default equalityTest @@ -2001,6 +2062,7 @@ jsPsych.pluginAPI = (function() { } module.getKeyboardResponse = function(parameters) { + //parameters are: callback_function, valid_responses, rt_method, persist, audio_context, audio_context_start_time, allow_held_key? parameters.rt_method = (typeof parameters.rt_method === 'undefined') ? 'performance' : parameters.rt_method; @@ -2012,20 +2074,30 @@ jsPsych.pluginAPI = (function() { var start_time; if (parameters.rt_method == 'performance') { start_time = performance.now(); - } else if (parameters.rt_method == 'audio') { + } else if (parameters.rt_method === 'audio') { start_time = parameters.audio_context_start_time; } var listener_id; var listener_function = function(e) { - var key_time; if (parameters.rt_method == 'performance') { key_time = performance.now(); - } else if (parameters.rt_method == 'audio') { + } else if (parameters.rt_method === 'audio') { key_time = parameters.audio_context.currentTime } + var rt = key_time - start_time; + + // overiding via parameters for testing purposes. + var minimum_valid_rt = parameters.minimum_valid_rt; + if(!minimum_valid_rt){ + minimum_valid_rt = jsPsych.initSettings().minimum_valid_rt || 0; + } + + if(rt < minimum_valid_rt){ + return; + } var valid_response = false; if (typeof parameters.valid_responses === 'undefined' || parameters.valid_responses == jsPsych.ALL_KEYS) { @@ -2050,7 +2122,7 @@ jsPsych.pluginAPI = (function() { } // check if key was already held down - if (((typeof parameters.allow_held_key == 'undefined') || !parameters.allow_held_key) && valid_response) { + if (((typeof parameters.allow_held_key === 'undefined') || !parameters.allow_held_key) && valid_response) { if (typeof held_keys[e.keyCode] !== 'undefined' && held_keys[e.keyCode] == true) { valid_response = false; } @@ -2063,7 +2135,7 @@ jsPsych.pluginAPI = (function() { parameters.callback_function({ key: e.keyCode, - rt: key_time - start_time + rt: rt, }); if (keyboard_listeners.includes(listener_id)) { @@ -2328,15 +2400,16 @@ jsPsych.pluginAPI = (function() { function load_audio_file_html5audio(source, count){ count = count || 1; var audio = new Audio(); - audio.addEventListener('canplaythrough', function(){ + audio.addEventListener('canplaythrough', function handleCanPlayThrough(){ audio_buffers[source] = audio; n_loaded++; loadfn(n_loaded); if(n_loaded == files.length){ finishfn(); } + audio.removeEventListener('canplaythrough', handleCanPlayThrough); }); - audio.addEventListener('onerror', function(){ + audio.addEventListener('error', function handleError(){ if(count < jsPsych.initSettings().max_preload_attempts){ setTimeout(function(){ load_audio_file_html5audio(source, count+1) @@ -2344,17 +2417,9 @@ jsPsych.pluginAPI = (function() { } else { jsPsych.loadFail(); } + audio.removeEventListener('error', handleError); }); - audio.addEventListener('onstalled', function(){ - if(count < jsPsych.initSettings().max_preload_attempts){ - setTimeout(function(){ - load_audio_file_html5audio(source, count+1) - }, 200); - } else { - jsPsych.loadFail(); - } - }); - audio.addEventListener('onabort', function(){ + audio.addEventListener('abort', function handleAbort(){ if(count < jsPsych.initSettings().max_preload_attempts){ setTimeout(function(){ load_audio_file_html5audio(source, count+1) @@ -2362,6 +2427,7 @@ jsPsych.pluginAPI = (function() { } else { jsPsych.loadFail(); } + audio.removeEventListener('abort', handleAbort); }); audio.src = source; } @@ -2501,7 +2567,7 @@ jsPsych.pluginAPI = (function() { preloads.push(preload); } - module.autoPreload = function(timeline, callback, images, audio, video, progress_bar) { + module.autoPreload = function(timeline, callback, file_protocol, images, audio, video, progress_bar) { // list of items to preload images = images || []; audio = audio || []; @@ -2516,15 +2582,16 @@ jsPsych.pluginAPI = (function() { var trials = timeline.trialsOfType(type); for (var j = 0; j < trials.length; j++) { - if (trials[j][param] && typeof trials[j][param] !== 'function') { - + if (typeof trials[j][param] == 'undefined') { + console.warn("jsPsych failed to auto preload one or more files:"); + console.warn("no parameter called "+param+" in plugin "+type); + } else if (typeof trials[j][param] !== 'function') { if ( !func || func(trials[j]) ){ if (media === 'image') { images = images.concat(jsPsych.utils.flatten([trials[j][param]])); } else if (media === 'audio') { audio = audio.concat(jsPsych.utils.flatten([trials[j][param]])); - } - else if (media === 'video') { + } else if (media === 'video') { video = video.concat(jsPsych.utils.flatten([trials[j][param]])); } } @@ -2539,14 +2606,19 @@ jsPsych.pluginAPI = (function() { // remove any nulls false values images = images.filter(function(x) { return x != false && x != null}) audio = audio.filter(function(x) { return x != false && x != null}) - video = video.filter(function(x) { return x != false && x != null}) + // prevent all video preloading (auto and manual) when file is opened directly in browser + if (file_protocol === true) { + video = []; + } else { + video = video.filter(function(x) { return x != false && x != null}) + } var total_n = images.length + audio.length + video.length; var loaded = 0; if(progress_bar){ - var pb_html = "
"; + var pb_html = "
"; pb_html += "
"; pb_html += "
"; jsPsych.getDisplayElement().innerHTML = pb_html; @@ -2556,7 +2628,10 @@ jsPsych.pluginAPI = (function() { loaded++; if(progress_bar){ var percent_loaded = (loaded/total_n)*100; - jsPsych.getDisplayElement().querySelector('#jspsych-loading-progress-bar').style.width = percent_loaded+"%"; + var preload_progress_bar = jsPsych.getDisplayElement().querySelector('#jspsych-loading-progress-bar'); + if (preload_progress_bar !== null) { + preload_progress_bar.style.width = percent_loaded+"%"; + } } } diff --git a/app/static/lib/jspsych-6.1.0/license.txt b/app/static/lib/jspsych-6.2.0/license.txt old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/license.txt rename to app/static/lib/jspsych-6.2.0/license.txt diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-animation.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-animation.js old mode 100755 new mode 100644 similarity index 66% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-animation.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-animation.js index e52fa721..501378b4 --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-animation.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-animation.js @@ -52,6 +52,13 @@ jsPsych.plugins.animation = (function() { pretty_name: 'Prompt', default: null, description: 'Any content here will be displayed below stimulus.' + }, + render_on_canvas: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Render on canvas', + default: true, + description: 'If true, the images will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers).'+ + 'If false, the image will be shown via an img element.' } } } @@ -66,9 +73,27 @@ jsPsych.plugins.animation = (function() { var responses = []; var current_stim = ""; + if (trial.render_on_canvas) { + // first clear the display element (because the render_on_canvas method appends to display_element instead of overwriting it with .innerHTML) + if (display_element.hasChildNodes()) { + // can't loop through child list because the list will be modified by .removeChild() + while (display_element.firstChild) { + display_element.removeChild(display_element.firstChild); + } + } + var canvas = document.createElement("canvas"); + canvas.id = "jspsych-animation-image"; + canvas.style.margin = 0; + canvas.style.padding = 0; + display_element.insertBefore(canvas, null); + var ctx = canvas.getContext("2d"); + } + var animate_interval = setInterval(function() { var showImage = true; - display_element.innerHTML = ''; // clear everything + if (!trial.render_on_canvas) { + display_element.innerHTML = ''; // clear everything + } animate_frame++; if (animate_frame == trial.stimuli.length) { animate_frame = 0; @@ -85,9 +110,23 @@ jsPsych.plugins.animation = (function() { }, interval_time); function show_next_frame() { - // show image - display_element.innerHTML = ''; - + if (trial.render_on_canvas) { + display_element.querySelector('#jspsych-animation-image').style.visibility = 'visible'; + var img = new Image(); + img.src = trial.stimuli[animate_frame]; + canvas.height = img.naturalHeight; + canvas.width = img.naturalWidth; + ctx.drawImage(img,0,0); + if (trial.prompt !== null & animate_frame == 0 & reps == 0) { + display_element.insertAdjacentHTML('beforeend', trial.prompt); + } + } else { + // show image + display_element.innerHTML = ''; + if (trial.prompt !== null) { + display_element.innerHTML += trial.prompt; + } + } current_stim = trial.stimuli[animate_frame]; // record when image was shown @@ -96,10 +135,6 @@ jsPsych.plugins.animation = (function() { "time": performance.now() - startTime }); - if (trial.prompt !== null) { - display_element.innerHTML += trial.prompt; - } - if (trial.frame_isi > 0) { jsPsych.pluginAPI.setTimeout(function() { display_element.querySelector('#jspsych-animation-image').style.visibility = 'hidden'; diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-audio-button-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-audio-button-response.js old mode 100755 new mode 100644 similarity index 75% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-audio-button-response.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-audio-button-response.js index 6f999c76..646c6f8c --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-audio-button-response.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-audio-button-response.js @@ -73,6 +73,13 @@ jsPsych.plugins["audio-button-response"] = (function() { default: false, description: 'If true, then the trial will end as soon as the audio file finishes playing.' }, + response_allowed_while_playing: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response allowed while playing', + default: true, + description: 'If true, then responses are allowed while the audio is playing. '+ + 'If false, then the audio must finish playing before a response is accepted.' + } } } @@ -92,14 +99,21 @@ jsPsych.plugins["audio-button-response"] = (function() { // set up end event if trial needs it if(trial.trial_ends_after_audio){ if(context !== null){ - source.onended = function() { - end_trial(); - } + source.addEventListener('ended', end_trial); } else { audio.addEventListener('ended', end_trial); } } + // enable buttons after audio ends if necessary + if ((!trial.response_allowed_while_playing) & (!trial.trial_ends_after_audio)) { + if (context !== null) { + source.addEventListener('ended', enable_buttons); + } else { + audio.addEventListener('ended', enable_buttons); + } + } + //display buttons var buttons = []; if (Array.isArray(trial.button_html)) { @@ -133,6 +147,9 @@ jsPsych.plugins["audio-button-response"] = (function() { var choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility after_response(choice); }); + if (!trial.response_allowed_while_playing) { + display_element.querySelector('#jspsych-audio-button-response-button-' + i).querySelector('button').disabled = true; + } } // store response @@ -145,9 +162,13 @@ jsPsych.plugins["audio-button-response"] = (function() { function after_response(choice) { // measure rt - var end_time = performance.now(); - var rt = end_time - start_time; - response.button = choice; + var endTime = performance.now(); + var rt = endTime - startTime; + if(context !== null){ + endTime = context.currentTime; + rt = Math.round((endTime - startTime) * 1000); + } + response.button = parseInt(choice); response.rt = rt; // disable all the buttons after a response @@ -160,24 +181,26 @@ jsPsych.plugins["audio-button-response"] = (function() { if (trial.response_ends_trial) { end_trial(); } - }; + } // function to end trial when it is time function end_trial() { + // kill any remaining setTimeout handlers + jsPsych.pluginAPI.clearAllTimeouts(); + // stop the audio file if it is playing // remove end event listeners if they exist if(context !== null){ source.stop(); - source.onended = function() { } + source.removeEventListener('ended', end_trial); + source.removeEventListener('ended', enable_buttons); } else { audio.pause(); - audio.removeEventListener('ended', end_trial); + audio.removeEventListener('ended', end_trial); + audio.removeEventListener('ended', enable_buttons); } - // kill any remaining setTimeout handlers - jsPsych.pluginAPI.clearAllTimeouts(); - // gather the data to store for the trial var trial_data = { "rt": response.rt, @@ -190,10 +213,18 @@ jsPsych.plugins["audio-button-response"] = (function() { // move on to the next trial jsPsych.finishTrial(trial_data); - }; + } + + // function to enable buttons after audio ends + function enable_buttons() { + var btns = document.querySelectorAll('.jspsych-audio-button-response-button button'); + for (var i=0; i'; - html += '
' + html += ''; + var label_width_perc = 100/(trial.labels.length-1); + var percent_of_range = j * (100/(trial.labels.length - 1)); + var percent_dist_from_center = ((percent_of_range-50)/50)*100; + var offset = (percent_dist_from_center * half_thumb_width)/100; + html += '
'; html += ''+trial.labels[j]+''; html += '
' } @@ -132,7 +157,11 @@ jsPsych.plugins['audio-slider-response'] = (function() { } // add submit button - html += ''; + var next_disabled_attribute = ""; + if (trial.require_movement | !trial.response_allowed_while_playing) { + next_disabled_attribute = "disabled"; + } + html += ''; display_element.innerHTML = html; @@ -141,10 +170,15 @@ jsPsych.plugins['audio-slider-response'] = (function() { response: null }; + if (!trial.response_allowed_while_playing) { + display_element.querySelector('#jspsych-audio-slider-response-response').disabled = true; + display_element.querySelector('#jspsych-audio-slider-response-next').disabled = true; + } + if(trial.require_movement){ - display_element.querySelector('#jspsych-audio-slider-response-response').addEventListener('change', function(){ + display_element.querySelector('#jspsych-audio-slider-response-response').addEventListener('click', function(){ display_element.querySelector('#jspsych-audio-slider-response-next').disabled = false; - }) + }); } display_element.querySelector('#jspsych-audio-slider-response-next').addEventListener('click', function() { @@ -156,7 +190,7 @@ jsPsych.plugins['audio-slider-response'] = (function() { rt = Math.round((endTime - startTime) * 1000); } response.rt = rt; - response.response = display_element.querySelector('#jspsych-audio-slider-response-response').value; + response.response = display_element.querySelector('#jspsych-audio-slider-response-response').valueAsNumber; if(trial.response_ends_trial){ end_trial(); @@ -168,20 +202,26 @@ jsPsych.plugins['audio-slider-response'] = (function() { function end_trial(){ + // kill any remaining setTimeout handlers jsPsych.pluginAPI.clearAllTimeouts(); + // stop the audio file if it is playing + // remove end event listeners if they exist if(context !== null){ source.stop(); - source.onended = function() { } + source.removeEventListener('ended', end_trial); + source.removeEventListener('ended', enable_slider); } else { audio.pause(); audio.removeEventListener('ended', end_trial); + audio.removeEventListener('ended', enable_slider); } // save data var trialdata = { "rt": response.rt, - "stimulus": trial.stimulus, + "stimulus": trial.stimulus, + "slider_start": trial.slider_start, "response": response.response }; @@ -191,6 +231,14 @@ jsPsych.plugins['audio-slider-response'] = (function() { jsPsych.finishTrial(trialdata); } + // function to enable slider after audio ends + function enable_slider() { + document.querySelector('#jspsych-audio-slider-response-response').disabled = false; + if (!trial.require_movement) { + document.querySelector('#jspsych-audio-slider-response-next').disabled = false; + } + } + var startTime = performance.now(); // start audio if(context !== null){ diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-call-function.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-call-function.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-call-function.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-call-function.js diff --git a/app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-button-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-button-response.js new file mode 100644 index 00000000..c0fd90b0 --- /dev/null +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-button-response.js @@ -0,0 +1,199 @@ +/** + * jspsych-canvas-button-response + * Chris Jungerius (modified from Josh de Leeuw) + * + * a jsPsych plugin for displaying a canvas stimulus and getting a button response + * + * documentation: docs.jspsych.org + * + **/ + +jsPsych.plugins["canvas-button-response"] = (function () { + + var plugin = {}; + + plugin.info = { + name: 'canvas-button-response', + description: '', + parameters: { + stimulus: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Stimulus', + default: undefined, + description: 'The drawing function to apply to the canvas. Should take the canvas object as argument.' + }, + choices: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Choices', + default: undefined, + array: true, + description: 'The labels for the buttons.' + }, + button_html: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button HTML', + default: '', + array: true, + description: 'The html of the button. Can create own style.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed under the button.' + }, + stimulus_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Stimulus duration', + default: null, + description: 'How long to hide the stimulus.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show the trial.' + }, + margin_vertical: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin vertical', + default: '0px', + description: 'The vertical margin of the button.' + }, + margin_horizontal: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin horizontal', + default: '8px', + description: 'The horizontal margin of the button.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, then trial will end when user responds.' + }, + canvas_size: { + type: jsPsych.plugins.parameterType.INT, + array: true, + pretty_name: 'Canvas size', + default: [500, 500], + description: 'Array containing the height (first value) and width (second value) of the canvas element.' + } + + } + } + + plugin.trial = function (display_element, trial) { + + // create canvas + var html = '
' + '' + '
'; + + //display buttons + var buttons = []; + if (Array.isArray(trial.button_html)) { + if (trial.button_html.length == trial.choices.length) { + buttons = trial.button_html; + } else { + console.error('Error in canvas-button-response plugin. The length of the button_html array does not equal the length of the choices array'); + } + } else { + for (var i = 0; i < trial.choices.length; i++) { + buttons.push(trial.button_html); + } + } + html += '
'; + for (var i = 0; i < trial.choices.length; i++) { + var str = buttons[i].replace(/%choice%/g, trial.choices[i]); + html += '
' + str + '
'; + } + html += '
'; + + //show prompt if there is one + if (trial.prompt !== null) { + html += trial.prompt; + } + display_element.innerHTML = html; + + //draw + let c = document.getElementById("jspsych-canvas-stimulus") + trial.stimulus(c) + + // start time + var start_time = performance.now(); + + // add event listeners to buttons + for (var i = 0; i < trial.choices.length; i++) { + display_element.querySelector('#jspsych-canvas-button-response-button-' + i).addEventListener('click', function (e) { + var choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility + after_response(choice); + }); + } + + // store response + var response = { + rt: null, + button: null + }; + + // function to handle responses by the subject + function after_response(choice) { + + // measure rt + var end_time = performance.now(); + var rt = end_time - start_time; + response.button = parseInt(choice); + response.rt = rt; + + // after a valid response, the stimulus will have the CSS class 'responded' + // which can be used to provide visual feedback that a response was recorded + display_element.querySelector('#jspsych-canvas-button-response-stimulus').className += ' responded'; + + // disable all the buttons after a response + var btns = document.querySelectorAll('.jspsych-canvas-button-response-button button'); + for (var i = 0; i < btns.length; i++) { + //btns[i].removeEventListener('click'); + btns[i].setAttribute('disabled', 'disabled'); + } + + if (trial.response_ends_trial) { + end_trial(); + } + }; + + // function to end trial when it is time + function end_trial() { + + // kill any remaining setTimeout handlers + jsPsych.pluginAPI.clearAllTimeouts(); + + // gather the data to store for the trial + var trial_data = { + "rt": response.rt, + "button_pressed": response.button + }; + + // clear the display + display_element.innerHTML = ''; + + // move on to the next trial + jsPsych.finishTrial(trial_data); + }; + + // hide image if timing is set + if (trial.stimulus_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + display_element.querySelector('#jspsych-canvas-button-response-stimulus').style.visibility = 'hidden'; + }, trial.stimulus_duration); + } + + // end trial if time limit is set + if (trial.trial_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + end_trial(); + }, trial.trial_duration); + } + + }; + + return plugin; +})(); diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-image-keyboard-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-keyboard-response.js old mode 100755 new mode 100644 similarity index 57% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-image-keyboard-response.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-keyboard-response.js index 84bab0bb..94da2631 --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-image-keyboard-response.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-keyboard-response.js @@ -1,47 +1,27 @@ /** - * jspsych-image-keyboard-response - * Josh de Leeuw + * jspsych-canvas-keyboard-response + * Chris Jungerius (modified from Josh de Leeuw) * - * plugin for displaying a stimulus and getting a keyboard response + * a jsPsych plugin for displaying a canvas stimulus and getting a keyboard response * * documentation: docs.jspsych.org * **/ -jsPsych.plugins["image-keyboard-response"] = (function() { +jsPsych.plugins["canvas-keyboard-response"] = (function () { var plugin = {}; - jsPsych.pluginAPI.registerPreload('image-keyboard-response', 'stimulus', 'image'); - plugin.info = { - name: 'image-keyboard-response', + name: 'canvas-keyboard-response', description: '', parameters: { stimulus: { - type: jsPsych.plugins.parameterType.IMAGE, + type: jsPsych.plugins.parameterType.FUNCTION, pretty_name: 'Stimulus', default: undefined, - description: 'The image to be displayed' - }, - stimulus_height: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Image height', - default: null, - description: 'Set the image height in pixels' - }, - stimulus_width: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Image width', - default: null, - description: 'Set the image width in pixels' - }, - maintain_aspect_ratio: { - type: jsPsych.plugins.parameterType.BOOL, - pretty_name: 'Maintain aspect ratio', - default: true, - description: 'Maintain the aspect ratio after setting width or height' + description: 'The drawing function to apply to the canvas. Should take the canvas object as argument.' }, choices: { type: jsPsych.plugins.parameterType.KEYCODE, @@ -74,35 +54,29 @@ jsPsych.plugins["image-keyboard-response"] = (function() { default: true, description: 'If true, trial will end when subject makes a response.' }, + canvas_size: { + type: jsPsych.plugins.parameterType.INT, + array: true, + pretty_name: 'Canvas size', + default: [500, 500], + description: 'Array containing the height (first value) and width (second value) of the canvas element.' + } + } } - plugin.trial = function(display_element, trial) { - - // display stimulus - var html = ''; + plugin.trial = function (display_element, trial) { + var new_html = '
' + '' + '
'; // add prompt - if (trial.prompt !== null){ - html += trial.prompt; + if (trial.prompt !== null) { + new_html += trial.prompt; } - // render - display_element.innerHTML = html; - + // draw + display_element.innerHTML = new_html; + let c = document.getElementById("jspsych-canvas-stimulus") + trial.stimulus(c) // store response var response = { rt: null, @@ -110,7 +84,7 @@ jsPsych.plugins["image-keyboard-response"] = (function() { }; // function to end trial when it is time - var end_trial = function() { + var end_trial = function () { // kill any remaining setTimeout handlers jsPsych.pluginAPI.clearAllTimeouts(); @@ -123,7 +97,6 @@ jsPsych.plugins["image-keyboard-response"] = (function() { // gather the data to store for the trial var trial_data = { "rt": response.rt, - "stimulus": trial.stimulus, "key_press": response.key }; @@ -135,11 +108,11 @@ jsPsych.plugins["image-keyboard-response"] = (function() { }; // function to handle responses by the subject - var after_response = function(info) { + var after_response = function (info) { // after a valid response, the stimulus will have the CSS class 'responded' // which can be used to provide visual feedback that a response was recorded - display_element.querySelector('#jspsych-image-keyboard-response-stimulus').className += ' responded'; + display_element.querySelector('#jspsych-canvas-keyboard-response-stimulus').className += ' responded'; // only record the first response if (response.key == null) { @@ -164,14 +137,14 @@ jsPsych.plugins["image-keyboard-response"] = (function() { // hide stimulus if stimulus_duration is set if (trial.stimulus_duration !== null) { - jsPsych.pluginAPI.setTimeout(function() { - display_element.querySelector('#jspsych-image-keyboard-response-stimulus').style.visibility = 'hidden'; + jsPsych.pluginAPI.setTimeout(function () { + display_element.querySelector('#jspsych-canvas-keyboard-response-stimulus').style.visibility = 'hidden'; }, trial.stimulus_duration); } // end trial if trial_duration is set if (trial.trial_duration !== null) { - jsPsych.pluginAPI.setTimeout(function() { + jsPsych.pluginAPI.setTimeout(function () { end_trial(); }, trial.trial_duration); } diff --git a/app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-slider-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-slider-response.js new file mode 100644 index 00000000..21f1feed --- /dev/null +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-canvas-slider-response.js @@ -0,0 +1,207 @@ +/** + * jspsych-canvas-slider-response + * Chris Jungerius (modified from Josh de Leeuw) + * + * a jsPsych plugin for displaying a canvas stimulus and getting a slider response + * + * documentation: docs.jspsych.org + * + */ + + +jsPsych.plugins['canvas-slider-response'] = (function () { + + var plugin = {}; + + plugin.info = { + name: 'canvas-slider-response', + description: '', + parameters: { + stimulus: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Stimulus', + default: undefined, + description: 'The drawing function to apply to the canvas. Should take the canvas object as argument.' + }, + min: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Min slider', + default: 0, + description: 'Sets the minimum value of the slider.' + }, + max: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Max slider', + default: 100, + description: 'Sets the maximum value of the slider', + }, + slider_start: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Slider starting value', + default: 50, + description: 'Sets the starting value of the slider', + }, + step: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Step', + default: 1, + description: 'Sets the step of the slider' + }, + labels: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name: 'Labels', + default: [], + array: true, + description: 'Labels of the slider.', + }, + slider_width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Slider width', + default: null, + description: 'Width of the slider in pixels.' + }, + button_label: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button label', + default: 'Continue', + array: false, + description: 'Label of the button to advance.' + }, + require_movement: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Require movement', + default: false, + description: 'If true, the participant will have to move the slider before continuing.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed below the slider.' + }, + stimulus_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Stimulus duration', + default: null, + description: 'How long to hide the stimulus.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show the trial.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, trial will end when user makes a response.' + }, + canvas_size: { + type: jsPsych.plugins.parameterType.INT, + array: true, + pretty_name: 'Canvas size', + default: [500, 500], + description: 'Array containing the height (first value) and width (second value) of the canvas element.' + } + + } + } + + plugin.trial = function (display_element, trial) { + + var html = '
'; + html += '
' + '' + '
'; + html += '
'; + html += ''; + html += '
' + for (var j = 0; j < trial.labels.length; j++) { + var width = 100 / (trial.labels.length - 1); + var left_offset = (j * (100 / (trial.labels.length - 1))) - (width / 2); + html += '
'; + html += '' + trial.labels[j] + ''; + html += '
' + } + html += '
'; + html += '
'; + html += '
'; + + if (trial.prompt !== null) { + html += trial.prompt; + } + + // add submit button + html += ''; + + display_element.innerHTML = html; + + // draw + let c = document.getElementById("jspsych-canvas-stimulus") + trial.stimulus(c) + + var response = { + rt: null, + response: null + }; + + if (trial.require_movement) { + display_element.querySelector('#jspsych-canvas-slider-response-response').addEventListener('click', function () { + display_element.querySelector('#jspsych-canvas-slider-response-next').disabled = false; + }) + } + + display_element.querySelector('#jspsych-canvas-slider-response-next').addEventListener('click', function () { + // measure response time + var endTime = performance.now(); + response.rt = endTime - startTime; + response.response = display_element.querySelector('#jspsych-canvas-slider-response-response').valueAsNumber; + + if (trial.response_ends_trial) { + end_trial(); + } else { + display_element.querySelector('#jspsych-canvas-slider-response-next').disabled = true; + } + + }); + + function end_trial() { + + jsPsych.pluginAPI.clearAllTimeouts(); + + // save data + var trialdata = { + "rt": response.rt, + "response": response.response, + "slider_start": trial.slider_start + }; + + display_element.innerHTML = ''; + + // next trial + jsPsych.finishTrial(trialdata); + } + + if (trial.stimulus_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + display_element.querySelector('#jspsych-canvas-slider-response-stimulus').style.visibility = 'hidden'; + }, trial.stimulus_duration); + } + + // end trial if trial_duration is set + if (trial.trial_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + end_trial(); + }, trial.trial_duration); + } + + var startTime = performance.now(); + }; + + return plugin; +})(); diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-categorize-animation.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-categorize-animation.js old mode 100755 new mode 100644 similarity index 63% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-categorize-animation.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-categorize-animation.js index 7507c114..d6a7c745 --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-categorize-animation.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-categorize-animation.js @@ -83,6 +83,13 @@ jsPsych.plugins["categorize-animation"] = (function() { default: null, description: 'Any content here will be displayed below the stimulus.' }, + render_on_canvas: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Render on canvas', + default: true, + description: 'If true, the images will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers).'+ + 'If false, the image will be shown via an img element.' + } } } @@ -97,9 +104,36 @@ jsPsych.plugins["categorize-animation"] = (function() { var timeoutSet = false; var correct; + if (trial.render_on_canvas) { + // first clear the display element (because the render_on_canvas method appends to display_element instead of overwriting it with .innerHTML) + if (display_element.hasChildNodes()) { + // can't loop through child list because the list will be modified by .removeChild() + while (display_element.firstChild) { + display_element.removeChild(display_element.firstChild); + } + } + var canvas = document.createElement("canvas"); + canvas.id = "jspsych-categorize-animation-stimulus"; + canvas.style.margin = 0; + canvas.style.padding = 0; + display_element.insertBefore(canvas, null); + var ctx = canvas.getContext("2d"); + if (trial.prompt !== null) { + var prompt_div = document.createElement("div"); + prompt_div.id = "jspsych-categorize-animation-prompt"; + prompt_div.style.visibility = "hidden"; + prompt_div.innerHTML = trial.prompt; + display_element.insertBefore(prompt_div, canvas.nextElementSibling); + } + var feedback_div = document.createElement("div"); + display_element.insertBefore(feedback_div, display_element.nextElementSibling); + } + // show animation var animate_interval = setInterval(function() { - display_element.innerHTML = ''; // clear everything + if (!trial.render_on_canvas) { + display_element.innerHTML = ''; // clear everything + } animate_frame++; if (animate_frame == trial.stimuli.length) { animate_frame = 0; @@ -112,20 +146,45 @@ jsPsych.plugins["categorize-animation"] = (function() { } if (showAnimation) { - display_element.innerHTML += ''; + if (trial.render_on_canvas) { + display_element.querySelector('#jspsych-categorize-animation-stimulus').style.visibility = 'visible'; + var img = new Image(); + img.src = trial.stimuli[animate_frame]; + canvas.height = img.naturalHeight; + canvas.width = img.naturalWidth; + ctx.drawImage(img,0,0); + } else { + display_element.innerHTML += ''; + } } if (!responded && trial.allow_response_before_complete) { // in here if the user can respond before the animation is done if (trial.prompt !== null) { - display_element.innerHTML += trial.prompt; + if (trial.render_on_canvas) { + prompt_div.style.visibility = "visible"; + } else { + display_element.innerHTML += trial.prompt; + } + } + if (trial.render_on_canvas) { + if (!showAnimation) { + canvas.remove(); + } } } else if (!responded) { // in here if the user has to wait to respond until animation is done. // if this is the case, don't show the prompt until the animation is over. if (!showAnimation) { if (trial.prompt !== null) { - display_element.innerHTML += trial.prompt; + if (trial.render_on_canvas) { + prompt_div.style.visibility = "visible"; + } else { + display_element.innerHTML += trial.prompt; + } + } + if (trial.render_on_canvas) { + canvas.remove(); } } } else { @@ -138,7 +197,14 @@ jsPsych.plugins["categorize-animation"] = (function() { } else { feedback_text = trial.incorrect_text.replace("%ANS%", trial.text_answer); } - display_element.innerHTML += feedback_text; + if (trial.render_on_canvas) { + if (trial.prompt !== null) { + prompt_div.remove(); + } + feedback_div.innerHTML = feedback_text; + } else { + display_element.innerHTML += feedback_text; + } // set timeout to clear feedback if (!timeoutSet) { diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-categorize-html.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-categorize-html.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-categorize-html.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-categorize-html.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-categorize-image.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-categorize-image.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-categorize-image.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-categorize-image.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-cloze.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-cloze.js old mode 100755 new mode 100644 similarity index 94% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-cloze.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-cloze.js index eb02359e..b5ecdc2e --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-cloze.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-cloze.js @@ -48,7 +48,7 @@ jsPsych.plugins['cloze'] = (function () { var elements = trial.text.split('%'); var solutions = []; - for (i=0; i'; + + // another div for border + html += '
'+get_counter_text(trial.stimuli.length)+'

'; + + // position prompt above or below + if (trial.prompt_location == "below") { + html += html_text + } else { + html = html_text + html + } + // add button + html += '
'; + + display_element.innerHTML = html; + + // store initial location data + let init_locations = []; + + if (!trial.stim_starts_inside) { + // determine number of rows and colums, must be a even number + let num_rows = Math.ceil(Math.sqrt(trial.stimuli.length)) + if ( num_rows % 2 != 0) { + num_rows = num_rows + 1 + } + + // compute coords for left and right side of arena + var r_coords = []; + var l_coords = []; + for (const x of make_arr(0, trial.sort_area_width - trial.stim_width, num_rows) ) { + for (const y of make_arr(0, trial.sort_area_height - trial.stim_height, num_rows) ) { + if ( x > ( (trial.sort_area_width - trial.stim_width) * .5 ) ) { + //r_coords.push({ x:x, y:y } ) + r_coords.push({ x:x + (trial.sort_area_width) * .5 , y:y }); + } else { + l_coords.push({ x:x - (trial.sort_area_width) * .5 , y:y }); + //l_coords.push({ x:x, y:y } ) + } + } + } + + // repeat coordinates until you have enough coords (may be obsolete) + while ( ( r_coords.length + l_coords.length ) < trial.stimuli.length ) { + r_coords = r_coords.concat(r_coords) + l_coords = l_coords.concat(l_coords) + } + // reverse left coords, so that coords closest to arena is used first + l_coords = l_coords.reverse() + + // shuffle stimuli, so that starting positions are random + trial.stimuli = shuffle(trial.stimuli); + } + + let inside = [] + for (let i = 0; i < trial.stimuli.length; i++) { + var coords; + if (trial.stim_starts_inside) { + coords = random_coordinate(trial.sort_area_width - trial.stim_width, trial.sort_area_height - trial.stim_height); + } else { + if ( (i % 2) == 0 ) { + coords = r_coords[Math.floor(i * .5)]; + } else { + coords = l_coords[Math.floor(i * .5)]; + } + } + + display_element.querySelector("#jspsych-free-sort-arena").innerHTML += ''+ + ''; + + init_locations.push({ + "src": trial.stimuli[i], + "x": coords.x, + "y": coords.y + }); + if (trial.stim_starts_inside) { + inside.push(true); + } else { + inside.push(false); + } + } + + // moves within a trial + let moves = []; + + // are objects currently inside + let cur_in = false + + // draggable items + const draggables = display_element.querySelectorAll('.jspsych-free-sort-draggable'); + + // button (will show when all items are inside) and border (will change color) + const border = display_element.querySelector("#jspsych-free-sort-border") + const button = display_element.querySelector('#jspsych-free-sort-done-btn') + + // when trial starts, modify text and border/background if all items are inside (stim_starts_inside: true) + if (inside.some(Boolean) && trial.change_border_background_color) { + border.style.borderColor = trial.border_color_in; + } + if (inside.every(Boolean)) { + if (trial.change_border_background_color) { + border.style.background = trial.border_color_in; + } + button.style.visibility = "visible"; + display_element.querySelector("#jspsych-free-sort-counter").innerHTML = trial.counter_text_finished; + } + + for(let i=0; i 1) { + text_out += "s"; + } + } + } + return text_out; + } + }; + + // helper functions + + function shuffle(array) { + // define three variables + let cur_idx = array.length, tmp_val, rand_idx; + + // While there remain elements to shuffle... + while (0 !== cur_idx) { + // Pick a remaining element... + rand_idx = Math.floor(Math.random() * cur_idx); + cur_idx -= 1; + + // And swap it with the current element. + tmp_val = array[cur_idx]; + array[cur_idx] = array[rand_idx]; + array[rand_idx] = tmp_val; + } + return array; + } + + function make_arr(startValue, stopValue, cardinality) { + const step = (stopValue - startValue) / (cardinality - 1); + let arr = []; + for (let i = 0; i < cardinality; i++) { + arr.push(startValue + (step * i)); + } + return arr; + } + + function inside_ellipse(x, y, x0, y0, rx, ry, square=false) { + const results = []; + if (square) { + result = ( Math.abs(x - x0) <= rx ) && ( Math.abs(y - y0) <= ry ) + } else { + result = (( x - x0 ) * ( x - x0 )) * (ry * ry) + ((y - y0) * ( y - y0 )) * ( rx * rx ) <= ( (rx * rx) * (ry * ry) ) + } + return result + } + + function random_coordinate(max_width, max_height) { + const rnd_x = Math.floor(Math.random() * (max_width - 1)); + const rnd_y = Math.floor(Math.random() * (max_height - 1)); + return { + x: rnd_x, + y: rnd_y + }; + } + + return plugin; +})(); diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-fullscreen.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-fullscreen.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-fullscreen.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-fullscreen.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-html-button-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-html-button-response.js old mode 100755 new mode 100644 similarity index 96% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-html-button-response.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-html-button-response.js index dfeec701..445ab71b --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-html-button-response.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-html-button-response.js @@ -129,7 +129,7 @@ jsPsych.plugins["html-button-response"] = (function() { // measure rt var end_time = performance.now(); var rt = end_time - start_time; - response.button = choice; + response.button = parseInt(choice); response.rt = rt; // after a valid response, the stimulus will have the CSS class 'responded' diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-html-keyboard-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-html-keyboard-response.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-html-keyboard-response.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-html-keyboard-response.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-html-slider-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-html-slider-response.js old mode 100755 new mode 100644 similarity index 80% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-html-slider-response.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-html-slider-response.js index adcbef18..3e8a555d --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-html-slider-response.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-html-slider-response.js @@ -35,7 +35,7 @@ jsPsych.plugins['html-slider-response'] = (function() { default: 100, description: 'Sets the maximum value of the slider', }, - start: { + slider_start: { type: jsPsych.plugins.parameterType.INT, pretty_name: 'Slider starting value', default: 50, @@ -102,19 +102,27 @@ jsPsych.plugins['html-slider-response'] = (function() { plugin.trial = function(display_element, trial) { + // half of the thumb width value from jspsych.css, used to adjust the label positions + var half_thumb_width = 7.5; + var html = '
'; html += '
' + trial.stimulus + '
'; html += '
'; - html += ''; + html += ''; html += '
' for(var j=0; j < trial.labels.length; j++){ - var width = 100/(trial.labels.length-1); - var left_offset = (j * (100 /(trial.labels.length - 1))) - (width/2); - html += '
'; + var label_width_perc = 100/(trial.labels.length-1); + var percent_of_range = j * (100/(trial.labels.length - 1)); + var percent_dist_from_center = ((percent_of_range-50)/50)*100; + var offset = (percent_dist_from_center * half_thumb_width)/100; + html += '
'; html += ''+trial.labels[j]+''; html += '
' } @@ -135,18 +143,18 @@ jsPsych.plugins['html-slider-response'] = (function() { rt: null, response: null }; - + if(trial.require_movement){ - display_element.querySelector('#jspsych-html-slider-response-response').addEventListener('change', function(){ + display_element.querySelector('#jspsych-html-slider-response-response').addEventListener('click', function(){ display_element.querySelector('#jspsych-html-slider-response-next').disabled = false; - }) + }); } display_element.querySelector('#jspsych-html-slider-response-next').addEventListener('click', function() { // measure response time var endTime = performance.now(); response.rt = endTime - startTime; - response.response = display_element.querySelector('#jspsych-html-slider-response-response').value; + response.response = display_element.querySelector('#jspsych-html-slider-response-response').valueAsNumber; if(trial.response_ends_trial){ end_trial(); @@ -163,8 +171,9 @@ jsPsych.plugins['html-slider-response'] = (function() { // save data var trialdata = { "rt": response.rt, - "response": response.response, - "stimulus": trial.stimulus + "stimulus": trial.stimulus, + "slider_start": trial.slider_start, + "response": response.response }; display_element.innerHTML = ''; diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-iat-html.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-iat-html.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-iat-html.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-iat-html.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-iat-image.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-iat-image.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-iat-image.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-iat-image.js diff --git a/app/static/lib/jspsych-6.2.0/plugins/jspsych-image-button-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-image-button-response.js new file mode 100644 index 00000000..e4954af7 --- /dev/null +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-image-button-response.js @@ -0,0 +1,311 @@ +/** + * jspsych-image-button-response + * Josh de Leeuw + * + * plugin for displaying a stimulus and getting a button response + * + * documentation: docs.jspsych.org + * + **/ + +jsPsych.plugins["image-button-response"] = (function() { + + var plugin = {}; + + jsPsych.pluginAPI.registerPreload('image-button-response', 'stimulus', 'image'); + + plugin.info = { + name: 'image-button-response', + description: '', + parameters: { + stimulus: { + type: jsPsych.plugins.parameterType.IMAGE, + pretty_name: 'Stimulus', + default: undefined, + description: 'The image to be displayed' + }, + stimulus_height: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Image height', + default: null, + description: 'Set the image height in pixels' + }, + stimulus_width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Image width', + default: null, + description: 'Set the image width in pixels' + }, + maintain_aspect_ratio: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Maintain aspect ratio', + default: true, + description: 'Maintain the aspect ratio after setting width or height' + }, + choices: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Choices', + default: undefined, + array: true, + description: 'The labels for the buttons.' + }, + button_html: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button HTML', + default: '', + array: true, + description: 'The html of the button. Can create own style.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed under the button.' + }, + stimulus_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Stimulus duration', + default: null, + description: 'How long to hide the stimulus.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show the trial.' + }, + margin_vertical: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin vertical', + default: '0px', + description: 'The vertical margin of the button.' + }, + margin_horizontal: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin horizontal', + default: '8px', + description: 'The horizontal margin of the button.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, then trial will end when user responds.' + }, + render_on_canvas: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Render on canvas', + default: true, + description: 'If true, the image will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers).'+ + 'If false, the image will be shown via an img element.' + } + } + } + + plugin.trial = function(display_element, trial) { + + var height, width; + var html; + if (trial.render_on_canvas) { + // first clear the display element (because the render_on_canvas method appends to display_element instead of overwriting it with .innerHTML) + if (display_element.hasChildNodes()) { + // can't loop through child list because the list will be modified by .removeChild() + while (display_element.firstChild) { + display_element.removeChild(display_element.firstChild); + } + } + // create canvas element and image + var canvas = document.createElement("canvas"); + canvas.id = "jspsych-image-button-response-stimulus"; + canvas.style.margin = 0; + canvas.style.padding = 0; + var img = new Image(); + img.src = trial.stimulus; + // determine image height and width + if (trial.stimulus_height !== null) { + height = trial.stimulus_height; + if (trial.stimulus_width == null && trial.maintain_aspect_ratio) { + width = img.naturalWidth * (trial.stimulus_height/img.naturalHeight); + } + } else { + height = img.naturalHeight; + } + if (trial.stimulus_width !== null) { + width = trial.stimulus_width; + if (trial.stimulus_height == null && trial.maintain_aspect_ratio) { + height = img.naturalHeight * (trial.stimulus_width/img.naturalWidth); + } + } else if (!(trial.stimulus_height !== null & trial.maintain_aspect_ratio)) { + // if stimulus width is null, only use the image's natural width if the width value wasn't set + // in the if statement above, based on a specified height and maintain_aspect_ratio = true + width = img.naturalWidth; + } + canvas.height = height; + canvas.width = width; + // create buttons + var buttons = []; + if (Array.isArray(trial.button_html)) { + if (trial.button_html.length == trial.choices.length) { + buttons = trial.button_html; + } else { + console.error('Error in image-button-response plugin. The length of the button_html array does not equal the length of the choices array'); + } + } else { + for (var i = 0; i < trial.choices.length; i++) { + buttons.push(trial.button_html); + } + } + var btngroup_div = document.createElement('div'); + btngroup_div.id = "jspsych-image-button-response-btngroup"; + html = ''; + for (var i = 0; i < trial.choices.length; i++) { + var str = buttons[i].replace(/%choice%/g, trial.choices[i]); + html += '
'+str+'
'; + } + btngroup_div.innerHTML = html; + // add canvas to screen and draw image + display_element.insertBefore(canvas, null); + var ctx = canvas.getContext("2d"); + ctx.drawImage(img,0,0,width,height); + // add buttons to screen + display_element.insertBefore(btngroup_div, canvas.nextElementSibling); + // add prompt if there is one + if (trial.prompt !== null) { + display_element.insertAdjacentHTML('beforeend', trial.prompt); + } + + } else { + + // display stimulus as an image element + html = ''; + //display buttons + var buttons = []; + if (Array.isArray(trial.button_html)) { + if (trial.button_html.length == trial.choices.length) { + buttons = trial.button_html; + } else { + console.error('Error in image-button-response plugin. The length of the button_html array does not equal the length of the choices array'); + } + } else { + for (var i = 0; i < trial.choices.length; i++) { + buttons.push(trial.button_html); + } + } + html += '
'; + + for (var i = 0; i < trial.choices.length; i++) { + var str = buttons[i].replace(/%choice%/g, trial.choices[i]); + html += '
'+str+'
'; + } + html += '
'; + // add prompt + if (trial.prompt !== null){ + html += trial.prompt; + } + // update the page content + display_element.innerHTML = html; + + // set image dimensions after image has loaded (so that we have access to naturalHeight/naturalWidth) + var img = display_element.querySelector('#jspsych-image-button-response-stimulus'); + if (trial.stimulus_height !== null) { + height = trial.stimulus_height; + if (trial.stimulus_width == null && trial.maintain_aspect_ratio) { + width = img.naturalWidth * (trial.stimulus_height/img.naturalHeight); + } + } else { + height = img.naturalHeight; + } + if (trial.stimulus_width !== null) { + width = trial.stimulus_width; + if (trial.stimulus_height == null && trial.maintain_aspect_ratio) { + height = img.naturalHeight * (trial.stimulus_width/img.naturalWidth); + } + } else if (!(trial.stimulus_height !== null & trial.maintain_aspect_ratio)) { + // if stimulus width is null, only use the image's natural width if the width value wasn't set + // in the if statement above, based on a specified height and maintain_aspect_ratio = true + width = img.naturalWidth; + } + img.style.height = height.toString() + "px"; + img.style.width = width.toString() + "px"; + } + + // start timing + var start_time = performance.now(); + + for (var i = 0; i < trial.choices.length; i++) { + display_element.querySelector('#jspsych-image-button-response-button-' + i).addEventListener('click', function(e){ + var choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility + after_response(choice); + }); + } + + // store response + var response = { + rt: null, + button: null + }; + + // function to handle responses by the subject + function after_response(choice) { + + // measure rt + var end_time = performance.now(); + var rt = end_time - start_time; + response.button = parseInt(choice); + response.rt = rt; + + // after a valid response, the stimulus will have the CSS class 'responded' + // which can be used to provide visual feedback that a response was recorded + display_element.querySelector('#jspsych-image-button-response-stimulus').className += ' responded'; + + // disable all the buttons after a response + var btns = document.querySelectorAll('.jspsych-image-button-response-button button'); + for(var i=0; i'; + // add prompt + if (trial.prompt !== null){ + html += trial.prompt; + } + // update the page content + display_element.innerHTML = html; + + // set image dimensions after image has loaded (so that we have access to naturalHeight/naturalWidth) + var img = display_element.querySelector('#jspsych-image-keyboard-response-stimulus'); + if (trial.stimulus_height !== null) { + height = trial.stimulus_height; + if (trial.stimulus_width == null && trial.maintain_aspect_ratio) { + width = img.naturalWidth * (trial.stimulus_height/img.naturalHeight); + } + } else { + height = img.naturalHeight; + } + if (trial.stimulus_width !== null) { + width = trial.stimulus_width; + if (trial.stimulus_height == null && trial.maintain_aspect_ratio) { + height = img.naturalHeight * (trial.stimulus_width/img.naturalWidth); + } + } else if (!(trial.stimulus_height !== null & trial.maintain_aspect_ratio)) { + // if stimulus width is null, only use the image's natural width if the width value wasn't set + // in the if statement above, based on a specified height and maintain_aspect_ratio = true + width = img.naturalWidth; + } + img.style.height = height.toString() + "px"; + img.style.width = width.toString() + "px"; + } + + // store response + var response = { + rt: null, + key: null + }; + + // function to end trial when it is time + var end_trial = function() { + + // kill any remaining setTimeout handlers + jsPsych.pluginAPI.clearAllTimeouts(); + + // kill keyboard listeners + if (typeof keyboardListener !== 'undefined') { + jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); + } + + // gather the data to store for the trial + var trial_data = { + "rt": response.rt, + "stimulus": trial.stimulus, + "key_press": response.key + }; + + // clear the display + display_element.innerHTML = ''; + + // move on to the next trial + jsPsych.finishTrial(trial_data); + }; + + // function to handle responses by the subject + var after_response = function(info) { + + // after a valid response, the stimulus will have the CSS class 'responded' + // which can be used to provide visual feedback that a response was recorded + display_element.querySelector('#jspsych-image-keyboard-response-stimulus').className += ' responded'; + + // only record the first response + if (response.key == null) { + response = info; + } + + if (trial.response_ends_trial) { + end_trial(); + } + }; + + // start the response listener + if (trial.choices != jsPsych.NO_KEYS) { + var keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: after_response, + valid_responses: trial.choices, + rt_method: 'performance', + persist: false, + allow_held_key: false + }); + } + + // hide stimulus if stimulus_duration is set + if (trial.stimulus_duration !== null) { + jsPsych.pluginAPI.setTimeout(function() { + display_element.querySelector('#jspsych-image-keyboard-response-stimulus').style.visibility = 'hidden'; + }, trial.stimulus_duration); + } + + // end trial if trial_duration is set + if (trial.trial_duration !== null) { + jsPsych.pluginAPI.setTimeout(function() { + end_trial(); + }, trial.trial_duration); + } else if (trial.response_ends_trial === false) { + console.warn("The experiment may be deadlocked. Try setting a trial duration or set response_ends_trial to true."); + } + }; + + return plugin; +})(); diff --git a/app/static/lib/jspsych-6.2.0/plugins/jspsych-image-slider-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-image-slider-response.js new file mode 100644 index 00000000..e76f7314 --- /dev/null +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-image-slider-response.js @@ -0,0 +1,353 @@ +/** + * jspsych-image-slider-response + * a jspsych plugin for free response survey questions + * + * Josh de Leeuw + * + * documentation: docs.jspsych.org + * + */ + + +jsPsych.plugins['image-slider-response'] = (function() { + + var plugin = {}; + + jsPsych.pluginAPI.registerPreload('image-slider-response', 'stimulus', 'image'); + + plugin.info = { + name: 'image-slider-response', + description: '', + parameters: { + stimulus: { + type: jsPsych.plugins.parameterType.IMAGE, + pretty_name: 'Stimulus', + default: undefined, + description: 'The image to be displayed' + }, + stimulus_height: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Image height', + default: null, + description: 'Set the image height in pixels' + }, + stimulus_width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Image width', + default: null, + description: 'Set the image width in pixels' + }, + maintain_aspect_ratio: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Maintain aspect ratio', + default: true, + description: 'Maintain the aspect ratio after setting width or height' + }, + min: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Min slider', + default: 0, + description: 'Sets the minimum value of the slider.' + }, + max: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Max slider', + default: 100, + description: 'Sets the maximum value of the slider', + }, + slider_start: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Slider starting value', + default: 50, + description: 'Sets the starting value of the slider', + }, + step: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Step', + default: 1, + description: 'Sets the step of the slider' + }, + labels: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name:'Labels', + default: [], + array: true, + description: 'Labels of the slider.', + }, + slider_width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name:'Slider width', + default: null, + description: 'Width of the slider in pixels.' + }, + button_label: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button label', + default: 'Continue', + array: false, + description: 'Label of the button to advance.' + }, + require_movement: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Require movement', + default: false, + description: 'If true, the participant will have to move the slider before continuing.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed below the slider.' + }, + stimulus_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Stimulus duration', + default: null, + description: 'How long to hide the stimulus.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show the trial.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, trial will end when user makes a response.' + }, + render_on_canvas: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Render on canvas', + default: true, + description: 'If true, the image will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers).'+ + 'If false, the image will be shown via an img element.' + } + } + } + + plugin.trial = function(display_element, trial) { + + var height, width; + var html; + // half of the thumb width value from jspsych.css, used to adjust the label positions + var half_thumb_width = 7.5; + + if (trial.render_on_canvas) { + // first clear the display element (because the render_on_canvas method appends to display_element instead of overwriting it with .innerHTML) + if (display_element.hasChildNodes()) { + // can't loop through child list because the list will be modified by .removeChild() + while (display_element.firstChild) { + display_element.removeChild(display_element.firstChild); + } + } + // create wrapper div, canvas element and image + var content_wrapper = document.createElement('div'); + content_wrapper.id = "jspsych-image-slider-response-wrapper"; + content_wrapper.style.margin = "100px 0px"; + var canvas = document.createElement("canvas"); + canvas.id = "jspsych-image-slider-response-stimulus"; + canvas.style.margin = 0; + canvas.style.padding = 0; + var img = new Image(); + img.src = trial.stimulus; + // determine image height and width + if (trial.stimulus_height !== null) { + height = trial.stimulus_height; + if (trial.stimulus_width == null && trial.maintain_aspect_ratio) { + width = img.naturalWidth * (trial.stimulus_height/img.naturalHeight); + } + } else { + height = img.naturalHeight; + } + if (trial.stimulus_width !== null) { + width = trial.stimulus_width; + if (trial.stimulus_height == null && trial.maintain_aspect_ratio) { + height = img.naturalHeight * (trial.stimulus_width/img.naturalWidth); + } + } else if (!(trial.stimulus_height !== null & trial.maintain_aspect_ratio)) { + // if stimulus width is null, only use the image's natural width if the width value wasn't set + // in the if statement above, based on a specified height and maintain_aspect_ratio = true + width = img.naturalWidth; + } + canvas.height = height; + canvas.width = width; + // create container with slider and labels + var slider_container = document.createElement('div'); + slider_container.classList.add("jspsych-image-slider-response-container"); + slider_container.style.position = "relative"; + slider_container.style.margin = "0 auto 3em auto"; + if(trial.slider_width !== null){ + slider_container.style.width = trial.slider_width.toString()+'px'; + } + // create html string with slider and labels, and add to slider container + html =''; + html += '
' + for(var j=0; j < trial.labels.length; j++){ + var label_width_perc = 100/(trial.labels.length-1); + var percent_of_range = j * (100/(trial.labels.length - 1)); + var percent_dist_from_center = ((percent_of_range-50)/50)*100; + var offset = (percent_dist_from_center * half_thumb_width)/100; + html += '
'; + html += ''+trial.labels[j]+''; + html += '
' + } + html += '
'; + slider_container.innerHTML = html; + // add canvas and slider to content wrapper div + content_wrapper.insertBefore(canvas, content_wrapper.firstElementChild); + content_wrapper.insertBefore(slider_container, canvas.nextElementSibling); + // add content wrapper div to screen and draw image on canvas + display_element.insertBefore(content_wrapper, null); + var ctx = canvas.getContext("2d"); + ctx.drawImage(img,0,0,width,height); + // add prompt if there is one + if (trial.prompt !== null) { + display_element.insertAdjacentHTML('beforeend', trial.prompt); + } + // add submit button + var submit_btn = document.createElement('button'); + submit_btn.id = "jspsych-image-slider-response-next"; + submit_btn.classList.add("jspsych-btn"); + submit_btn.disabled = (trial.require_movement) ? true : false; + submit_btn.innerHTML = trial.button_label; + display_element.insertBefore(submit_btn, display_element.nextElementSibling); + + } else { + + html = '
'; + html += '
'; + html += ''; + html += '
'; + html += '
'; + html += ''; + html += '
' + for(var j=0; j < trial.labels.length; j++){ + var label_width_perc = 100/(trial.labels.length-1); + var percent_of_range = j * (100/(trial.labels.length - 1)); + var percent_dist_from_center = ((percent_of_range-50)/50)*100; + var offset = (percent_dist_from_center * half_thumb_width)/100; + html += '
'; + html += ''+trial.labels[j]+''; + html += '
' + } + html += '
'; + html += '
'; + html += '
'; + + if (trial.prompt !== null){ + html += trial.prompt; + } + + // add submit button + html += ''; + + display_element.innerHTML = html; + + // set image dimensions after image has loaded (so that we have access to naturalHeight/naturalWidth) + var img = display_element.querySelector('img'); + if (trial.stimulus_height !== null) { + height = trial.stimulus_height; + if (trial.stimulus_width == null && trial.maintain_aspect_ratio) { + width = img.naturalWidth * (trial.stimulus_height/img.naturalHeight); + } + } else { + height = img.naturalHeight; + } + if (trial.stimulus_width !== null) { + width = trial.stimulus_width; + if (trial.stimulus_height == null && trial.maintain_aspect_ratio) { + height = img.naturalHeight * (trial.stimulus_width/img.naturalWidth); + } + } else if (!(trial.stimulus_height !== null & trial.maintain_aspect_ratio)) { + // if stimulus width is null, only use the image's natural width if the width value wasn't set + // in the if statement above, based on a specified height and maintain_aspect_ratio = true + width = img.naturalWidth; + } + img.style.height = height.toString() + "px"; + img.style.width = width.toString() + "px"; + } + + var response = { + rt: null, + response: null + }; + + if(trial.require_movement){ + display_element.querySelector('#jspsych-image-slider-response-response').addEventListener('click', function(){ + display_element.querySelector('#jspsych-image-slider-response-next').disabled = false; + }); + } + + display_element.querySelector('#jspsych-image-slider-response-next').addEventListener('click', function() { + // measure response time + var endTime = performance.now(); + response.rt = endTime - startTime; + response.response = display_element.querySelector('#jspsych-image-slider-response-response').valueAsNumber; + + if(trial.response_ends_trial){ + end_trial(); + } else { + display_element.querySelector('#jspsych-image-slider-response-next').disabled = true; + } + + }); + + function end_trial(){ + + jsPsych.pluginAPI.clearAllTimeouts(); + + // save data + var trialdata = { + "rt": response.rt, + "stimulus": trial.stimulus, + "slider_start": trial.slider_start, + "response": response.response + }; + + display_element.innerHTML = ''; + + // next trial + jsPsych.finishTrial(trialdata); + } + + if (trial.stimulus_duration !== null) { + jsPsych.pluginAPI.setTimeout(function() { + display_element.querySelector('#jspsych-image-slider-response-stimulus').style.visibility = 'hidden'; + }, trial.stimulus_duration); + } + + // end trial if trial_duration is set + if (trial.trial_duration !== null) { + jsPsych.pluginAPI.setTimeout(function() { + end_trial(); + }, trial.trial_duration); + } + + var startTime = performance.now(); + }; + + return plugin; +})(); diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-instructions.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-instructions.js old mode 100755 new mode 100644 similarity index 91% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-instructions.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-instructions.js index 7d00f3ff..0f71010a --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-instructions.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-instructions.js @@ -63,6 +63,12 @@ jsPsych.plugins.instructions = (function() { default: false, description: 'If true, and clickable navigation is enabled, then Page x/y will be shown between the nav buttons.' }, + page_label: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Page label', + default: 'Page', + description: 'The text that appears before x/y (current/total) pages displayed with show_page_number' + }, button_label_previous: { type: jsPsych.plugins.parameterType.STRING, pretty_name: 'Button label previous', @@ -104,7 +110,7 @@ jsPsych.plugins.instructions = (function() { var pagenum_display = ""; if(trial.show_page_number) { pagenum_display = "Page "+(current_page+1)+"/"+trial.pages.length+""; + "jspsych-instructions-pagenum'>"+ trial.page_label + ' ' +(current_page+1)+"/"+trial.pages.length+""; } if (trial.show_clickable_nav) { diff --git a/app/static/lib/jspsych-6.2.0/plugins/jspsych-maxdiff.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-maxdiff.js new file mode 100644 index 00000000..946ce027 --- /dev/null +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-maxdiff.js @@ -0,0 +1,174 @@ +/** + * jspsych-maxdiff + * Angus Hughes + * + * a jspsych plugin for maxdiff/conjoint analysis designs + * + */ + +jsPsych.plugins['maxdiff'] = (function () { + + var plugin = {}; + + plugin.info = { + name: 'maxdiff', + description: '', + parameters: { + alternatives: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Alternatives', + array: true, + default: undefined, + description: 'Alternatives presented in the maxdiff table.' + }, + labels: { + type: jsPsych.plugins.parameterType.STRING, + array: true, + pretty_name: 'Labels', + default: undefined, + description: 'Labels to display for left and right response columns.' + }, + randomize_alternative_order: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Randomize Alternative Order', + default: false, + description: 'If true, the order of the alternatives will be randomized' + }, + preamble: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Preamble', + default: '', + description: 'String to display at top of the page.' + }, + button_label: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button Label', + default: 'Continue', + description: 'Label of the button.' + }, + required: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Required', + default: false, + description: 'Makes answering the alternative required.' + } + } + } + + plugin.trial = function (display_element, trial) { + + var html = ""; + // inject CSS for trial + html += ''; + + // show preamble text + if (trial.preamble !== null) { + html += '
' + trial.preamble + '
'; + } + html += '
'; + + // add maxdiff options /// + // first generate alternative order, randomized here as opposed to randomizing the order of alternatives + // so that the data are always associated with the same alternative regardless of order. + var alternative_order = []; + for (var i = 0; i < trial.alternatives.length; i++) { + alternative_order.push(i); + } + if (trial.randomize_alternative_order) { + alternative_order = jsPsych.randomization.shuffle(alternative_order); + } + + // Start with column headings + var maxdiff_table = ''; + + // construct each row of the maxdiff table + for (var i = 0; i < trial.alternatives.length; i++) { + var alternative = trial.alternatives[alternative_order[i]]; + // add alternative + maxdiff_table += ''; + maxdiff_table += ''; + maxdiff_table += ''; + } + maxdiff_table += '
' + trial.labels[0] + '' + trial.labels[1] + '

' + alternative + '


'; + html += maxdiff_table; + + // add submit button + var enable_submit = trial.required == true ? 'disabled = "disabled"' : ''; + html += ''; + html += '
'; + + display_element.innerHTML = html; + + // function to control responses + // first checks that the same alternative cannot be endorsed in the left and right columns simultaneously. + // then enables the submit button if the trial is required. + const left_right = ["left", "right"] + left_right.forEach(function(p) { + // Get all elements either 'left' or 'right' + document.getElementsByName(p).forEach(function(alt) { + alt.addEventListener('click', function() { + // Find the opposite (if left, then right & vice versa) identified by the class (jspsych-maxdiff-alt-1, 2, etc) + var op = alt.name == 'left' ? 'right' : 'left'; + var n = document.getElementsByClassName(alt.className).namedItem(op); + // If it's checked, uncheck it. + if (n.checked) { + n.checked = false; + } + + // check response + if (trial.required){ + // Now check if one of both left and right have been enabled to allow submission + var left_checked = [...document.getElementsByName('left')].some(c => c.checked); + var right_checked = [...document.getElementsByName('right')].some(c => c.checked); + if (left_checked && right_checked) { + document.getElementById("jspsych-maxdiff-next").disabled = false; + } else { + document.getElementById("jspsych-maxdiff-next").disabled = true; + } + } + }); + }); + }); + + // Get the data once the submit button is clicked + // Get the data once the submit button is clicked + display_element.querySelector('#jspsych-maxdiff-form').addEventListener('submit', function(e){ + e.preventDefault(); + + // measure response time + var endTime = performance.now(); + var response_time = endTime - startTime; + + // get the alternative by the data-name attribute, allowing a null response if unchecked + get_response = function(side){ + var col = display_element.querySelectorAll('[name=\"' + side + '\"]:checked')[0]; + if (col === undefined){ + return null; + } else { + var i = parseInt(col.getAttribute('data-name')); + return trial.alternatives[i]; + } + } + + // data saving + var trial_data = { + "rt": response_time, + "labels": JSON.stringify({"left": trial.labels[0], "right": trial.labels[1]}), + "left": get_response('left'), + "right": get_response('right') + }; + + // next trial + jsPsych.finishTrial(trial_data); + }); + + var startTime = performance.now(); + }; + + return plugin; +})(); \ No newline at end of file diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-rdk.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-rdk.js old mode 100755 new mode 100644 similarity index 97% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-rdk.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-rdk.js index 24c202b5..829b8415 --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-rdk.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-rdk.js @@ -371,7 +371,11 @@ jsPsych.plugins["rdk"] = (function() { //Remove the margins and padding of the canvas canvas.style.margin = 0; - canvas.style.padding = 0; + canvas.style.padding = 0; + // use absolute positioning in top left corner to get rid of scroll bars + canvas.style.position = 'absolute'; + canvas.style.top = 0; + canvas.style.left = 0; //Get the context of the canvas so that it can be painted on. var ctx = canvas.getContext("2d"); @@ -949,13 +953,12 @@ jsPsych.plugins["rdk"] = (function() { //Draw the fixation cross if we want it if(fixationCross === true){ - //Horizontal line ctx.beginPath(); ctx.lineWidth = fixationCrossThickness; ctx.moveTo(canvasWidth/2 - fixationCrossWidth, canvasHeight/2); ctx.lineTo(canvasWidth/2 + fixationCrossWidth, canvasHeight/2); - ctx.fillStyle = fixationCrossColor; + ctx.strokeStyle = fixationCrossColor; ctx.stroke(); //Vertical line @@ -963,7 +966,7 @@ jsPsych.plugins["rdk"] = (function() { ctx.lineWidth = fixationCrossThickness; ctx.moveTo(canvasWidth/2, canvasHeight/2 - fixationCrossHeight); ctx.lineTo(canvasWidth/2, canvasHeight/2 + fixationCrossHeight); - ctx.fillStyle = fixationCrossColor; + ctx.strokeStyle = fixationCrossColor; ctx.stroke(); } diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-reconstruction.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-reconstruction.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-reconstruction.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-reconstruction.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-resize.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-resize.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-resize.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-resize.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-same-different-html.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-same-different-html.js old mode 100755 new mode 100644 similarity index 91% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-same-different-html.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-same-different-html.js index fa3b1530..104f7a6a --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-same-different-html.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-same-different-html.js @@ -27,7 +27,7 @@ jsPsych.plugins['same-different-html'] = (function() { type: jsPsych.plugins.parameterType.SELECT, pretty_name: 'Answer', options: ['same', 'different'], - default: 75, + default: undefined, description: 'Either "same" or "different".' }, same_key: { @@ -45,8 +45,8 @@ jsPsych.plugins['same-different-html'] = (function() { first_stim_duration: { type: jsPsych.plugins.parameterType.INT, pretty_name: 'First stimulus duration', - default: 1000, - description: 'How long to show the first stimulus for in milliseconds.' + default: null, + description: 'How long to show the first stimulus for in milliseconds. If null, then the stimulus will remain on the screen until any keypress is made.' }, gap_duration: { type: jsPsych.plugins.parameterType.INT, @@ -57,8 +57,8 @@ jsPsych.plugins['same-different-html'] = (function() { second_stim_duration: { type: jsPsych.plugins.parameterType.INT, pretty_name: 'Second stimulus duration', - default: 1000, - description: 'How long to show the second stimulus for in milliseconds.' + default: null, + description: 'How long to show the second stimulus for in milliseconds. If null, then the stimulus will remain on the screen until a valid response is made.' }, prompt: { type: jsPsych.plugins.parameterType.STRING, diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-same-different-image.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-same-different-image.js old mode 100755 new mode 100644 similarity index 91% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-same-different-image.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-same-different-image.js index 5ab8b870..63879e1e --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-same-different-image.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-same-different-image.js @@ -29,7 +29,7 @@ jsPsych.plugins['same-different-image'] = (function() { type: jsPsych.plugins.parameterType.SELECT, pretty_name: 'Answer', options: ['same', 'different'], - default: 75, + default: undefined, description: 'Either "same" or "different".' }, same_key: { @@ -47,8 +47,8 @@ jsPsych.plugins['same-different-image'] = (function() { first_stim_duration: { type: jsPsych.plugins.parameterType.INT, pretty_name: 'First stimulus duration', - default: 1000, - description: 'How long to show the first stimulus for in milliseconds.' + default: null, + description: 'How long to show the first stimulus for in milliseconds. If null, then the stimulus will remain on the screen until any keypress is made.' }, gap_duration: { type: jsPsych.plugins.parameterType.INT, @@ -59,8 +59,8 @@ jsPsych.plugins['same-different-image'] = (function() { second_stim_duration: { type: jsPsych.plugins.parameterType.INT, pretty_name: 'Second stimulus duration', - default: 1000, - description: 'How long to show the second stimulus for in milliseconds.' + default: null, + description: 'How long to show the second stimulus for in milliseconds. If null, then the stimulus will remain on the screen until a valid response is made.' }, prompt: { type: jsPsych.plugins.parameterType.STRING, diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-serial-reaction-time-mouse.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-serial-reaction-time-mouse.js old mode 100755 new mode 100644 similarity index 96% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-serial-reaction-time-mouse.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-serial-reaction-time-mouse.js index 3ddb7cae..23d0e982 --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-serial-reaction-time-mouse.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-serial-reaction-time-mouse.js @@ -105,7 +105,7 @@ jsPsych.plugins["serial-reaction-time-mouse"] = (function() { //show prompt if there is one if (trial.prompt !== null) { - display_element.innerHTML += trial.prompt; + display_element.insertAdjacentHTML('beforeend', trial.prompt); } function showTarget(){ diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-serial-reaction-time.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-serial-reaction-time.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-serial-reaction-time.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-serial-reaction-time.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-html-form.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-html-form.js old mode 100755 new mode 100644 similarity index 77% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-html-form.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-html-form.js index ef56e1bc..7aca82fe --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-html-form.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-html-form.js @@ -34,11 +34,23 @@ jsPsych.plugins['survey-html-form'] = (function() { default: 'Continue', description: 'The text that appears on the button to finish the trial.' }, + autofocus: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Element ID to focus', + default: '', + description: 'The HTML element ID of a form field to autofocus on.' + }, dataAsArray: { type: jsPsych.plugins.parameterType.BOOLEAN, pretty_name: 'Data As Array', default: false, description: 'Retrieve the data as an array e.g. [{name: "INPUT_NAME", value: "INPUT_VALUE"}, ...] instead of an object e.g. {INPUT_NAME: INPUT_VALUE, ...}.' + }, + autocomplete: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Allow autocomplete', + default: false, + description: "Setting this to true will enable browser auto-complete or auto-fill for the form." } } } @@ -51,7 +63,11 @@ jsPsych.plugins['survey-html-form'] = (function() { html += '
'+trial.preamble+'
'; } // start form - html += '
' + if ( trial.autocomplete ) { + html += '' + } else { + html += '' + } // add form HTML / input elements html += trial.html; @@ -59,9 +75,20 @@ jsPsych.plugins['survey-html-form'] = (function() { // add submit button html += ''; - html += '
' + html += ''; display_element.innerHTML = html; + if ( trial.autofocus !== '' ) { + var focus_elements = display_element.querySelectorAll('#'+trial.autofocus); + if ( focus_elements.length === 0 ) { + console.warn('No element found with id: '+trial.autofocus); + } else if ( focus_elements.length > 1 ) { + console.warn('The id "'+trial.autofocus+'" is not unique so autofocus will not work.'); + } else { + focus_elements[0].focus(); + } + } + display_element.querySelector('#jspsych-survey-html-form').addEventListener('submit', function(event) { // don't submit form event.preventDefault(); diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-likert.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-likert.js old mode 100755 new mode 100644 similarity index 87% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-likert.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-likert.js index e95f6256..5c8de4ad --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-likert.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-likert.js @@ -71,6 +71,12 @@ jsPsych.plugins['survey-likert'] = (function() { pretty_name: 'Button label', default: 'Continue', description: 'Label of the button.' + }, + autocomplete: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Allow autocomplete', + default: false, + description: "Setting this to true will enable browser auto-complete or auto-fill for the form." } } } @@ -99,7 +105,12 @@ jsPsych.plugins['survey-likert'] = (function() { if(trial.preamble !== null){ html += '
'+trial.preamble+'
'; } - html += '
'; + + if ( trial.autocomplete ) { + html += ''; + } else { + html += ''; + } // add likert scale questions /// // generate question order. this is randomized here as opposed to randomizing the order of trial.questions @@ -120,11 +131,11 @@ jsPsych.plugins['survey-likert'] = (function() { var width = 100 / question.labels.length; var options_string = '
    '; for (var j = 0; j < question.labels.length; j++) { - options_string += '
  • '; + options_string += '>' + question.labels[j] + ''; } options_string += '
'; html += options_string; diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-multi-choice.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-multi-choice.js old mode 100755 new mode 100644 similarity index 90% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-multi-choice.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-multi-choice.js index bdc82e12..af9a3325 --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-multi-choice.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-multi-choice.js @@ -71,6 +71,12 @@ jsPsych.plugins['survey-multi-choice'] = (function() { pretty_name: 'Button label', default: 'Continue', description: 'Label of the button.' + }, + autocomplete: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Allow autocomplete', + default: false, + description: "Setting this to true will enable browser auto-complete or auto-fill for the form." } } } @@ -95,8 +101,11 @@ jsPsych.plugins['survey-multi-choice'] = (function() { } // form element - html += ''; - + if ( trial.autocomplete ) { + html += ''; + } else { + html += ''; + } // generate question order. this is randomized here as opposed to randomizing the order of trial.questions // so that the data are always associated with the same question regardless of order var question_order = []; @@ -140,8 +149,9 @@ jsPsych.plugins['survey-multi-choice'] = (function() { // add radio button container html += '
'; - html += ''; + html += ''; html += '
'; } diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-multi-select.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-multi-select.js old mode 100755 new mode 100644 similarity index 93% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-multi-select.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-multi-select.js index 1c398ae3..5f6ee2ca --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-multi-select.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-multi-select.js @@ -75,6 +75,12 @@ jsPsych.plugins['survey-multi-select'] = (function() { pretty_name: 'Required message', default: 'You must choose at least one response for this question', description: 'Message that will be displayed if required question is not answered.' + }, + autocomplete: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Allow autocomplete', + default: false, + description: "Setting this to true will enable browser auto-complete or auto-fill for the form." } } } @@ -99,6 +105,9 @@ jsPsych.plugins['survey-multi-select'] = (function() { var trial_form_id = _join(plugin_id_name, "form"); display_element.innerHTML += '
'; var trial_form = display_element.querySelector("#" + trial_form_id); + if ( !trial.autocomplete ) { + trial_form.setAttribute('autocomplete',"off"); + } // show preamble text var preamble_id_name = _join(plugin_id_name, 'preamble'); if(trial.preamble !== null){ @@ -146,14 +155,14 @@ jsPsych.plugins['survey-multi-select'] = (function() { label.innerHTML = question.options[j]; label.setAttribute('for', input_id) - // create checkboxes + // create checkboxes var input = document.createElement('input'); input.setAttribute('type', "checkbox"); input.setAttribute('name', input_name); input.setAttribute('id', input_id); input.setAttribute('value', question.options[j]) form.appendChild(label) - form.insertBefore(input, label) + label.insertBefore(input, label.firstChild) } } // add submit button diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-text.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-text.js old mode 100755 new mode 100644 similarity index 90% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-text.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-text.js index c46e945c..2eff429e --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-survey-text.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-survey-text.js @@ -72,6 +72,12 @@ jsPsych.plugins['survey-text'] = (function() { pretty_name: 'Button label', default: 'Continue', description: 'The text that appears on the button to finish the trial.' + }, + autocomplete: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Allow autocomplete', + default: false, + description: "Setting this to true will enable browser auto-complete or auto-fill for the form." } } } @@ -100,8 +106,11 @@ jsPsych.plugins['survey-text'] = (function() { html += '
'+trial.preamble+'
'; } // start form - html += '
' - + if (trial.autocomplete) { + html += ''; + } else { + html += ''; + } // generate question order var question_order = []; for(var i=0; i -1){ file_name = file_name.substring(0, file_name.indexOf('?')); } var type = file_name.substr(file_name.lastIndexOf('.') + 1); type = type.toLowerCase(); + if (type == "mov") { + console.warn('Warning: video-button-response plugin does not reliably support .mov files.') + } video_html+=''; } } @@ -183,37 +200,56 @@ jsPsych.plugins["video-button-response"] = (function() { var start_time = performance.now(); + var video_element = display_element.querySelector('#jspsych-video-button-response-stimulus'); + if(video_preload_blob){ - display_element.querySelector('#jspsych-video-button-response-stimulus').src = video_preload_blob; + video_element.src = video_preload_blob; } - display_element.querySelector('#jspsych-video-button-response-stimulus').onended = function(){ + video_element.onended = function(){ if(trial.trial_ends_after_video){ end_trial(); + } else if (!trial.response_allowed_while_playing) { + // enable response buttons + for (var i=0; i= trial.stop){ - display_element.querySelector('#jspsych-video-button-response-stimulus').pause(); + video_element.pause(); } }) } - display_element.querySelector('#jspsych-video-button-response-stimulus').playbackRate = trial.rate; - // add event listeners to buttons for (var i = 0; i < trial.choices.length; i++) { display_element.querySelector('#jspsych-video-button-response-button-' + i).addEventListener('click', function(e){ var choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility after_response(choice); }); + if (!trial.response_allowed_while_playing) { + display_element.querySelector('#jspsych-video-button-response-button-' + i).querySelector('button').disabled = true; + } } // store response @@ -228,10 +264,15 @@ jsPsych.plugins["video-button-response"] = (function() { // kill any remaining setTimeout handlers jsPsych.pluginAPI.clearAllTimeouts(); + // stop the video file if it is playing + // remove any remaining end event handlers + display_element.querySelector('#jspsych-video-button-response-stimulus').pause(); + display_element.querySelector('#jspsych-video-button-response-stimulus').onended = function() {}; + // gather the data to store for the trial var trial_data = { "rt": response.rt, - "stimulus": trial.stimulus, + "stimulus": JSON.stringify(trial.stimulus), "button_pressed": response.button }; @@ -240,7 +281,7 @@ jsPsych.plugins["video-button-response"] = (function() { // move on to the next trial jsPsych.finishTrial(trial_data); - }; + } // function to handle responses by the subject function after_response(choice) { @@ -248,12 +289,12 @@ jsPsych.plugins["video-button-response"] = (function() { // measure rt var end_time = performance.now(); var rt = end_time - start_time; - response.button = choice; + response.button = parseInt(choice); response.rt = rt; // after a valid response, the stimulus will have the CSS class 'responded' // which can be used to provide visual feedback that a response was recorded - display_element.querySelector('#jspsych-video-button-response-stimulus').className += ' responded'; + video_element.className += ' responded'; // disable all the buttons after a response var btns = document.querySelectorAll('.jspsych-video-button-response-button button'); @@ -265,7 +306,7 @@ jsPsych.plugins["video-button-response"] = (function() { if (trial.response_ends_trial) { end_trial(); } - }; + } // end trial if time limit is set if (trial.trial_duration !== null) { diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-video-keyboard-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-video-keyboard-response.js old mode 100755 new mode 100644 similarity index 68% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-video-keyboard-response.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-video-keyboard-response.js index 6d960e56..5094392a --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-video-keyboard-response.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-video-keyboard-response.js @@ -18,7 +18,7 @@ jsPsych.plugins["video-keyboard-response"] = (function() { name: 'video-keyboard-response', description: '', parameters: { - sources: { + stimulus: { type: jsPsych.plugins.parameterType.VIDEO, pretty_name: 'Video', default: undefined, @@ -96,6 +96,13 @@ jsPsych.plugins["video-keyboard-response"] = (function() { pretty_name: 'Response ends trial', default: true, description: 'If true, the trial will end when subject makes a response.' + }, + response_allowed_while_playing: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response allowed while playing', + default: true, + description: 'If true, then responses are allowed while the video is playing. '+ + 'If false, then the video must finish playing before a response is accepted.' } } } @@ -112,23 +119,33 @@ jsPsych.plugins["video-keyboard-response"] = (function() { if(trial.height) { video_html += ' height="'+trial.height+'"'; } - if(trial.autoplay){ + if(trial.autoplay & (trial.start == null)){ + // if autoplay is true and the start time is specified, then the video will start automatically + // via the play() method, rather than the autoplay attribute, to prevent showing the first frame video_html += " autoplay "; } if(trial.controls){ video_html +=" controls "; } + if (trial.start !== null) { + // hide video element when page loads if the start time is specified, + // to prevent the video element from showing the first frame + video_html += ' style="visibility: hidden;"'; + } video_html +=">"; - var video_preload_blob = jsPsych.pluginAPI.getVideoBuffer(trial.sources[0]); + var video_preload_blob = jsPsych.pluginAPI.getVideoBuffer(trial.stimulus[0]); if(!video_preload_blob) { - for(var i=0; i -1){ file_name = file_name.substring(0, file_name.indexOf('?')); } var type = file_name.substr(file_name.lastIndexOf('.') + 1); type = type.toLowerCase(); + if (type == "mov") { + console.warn('Warning: video-keyboard-response plugin does not reliably support .mov files.') + } video_html+=''; } } @@ -142,31 +159,52 @@ jsPsych.plugins["video-keyboard-response"] = (function() { display_element.innerHTML = video_html; + var video_element = display_element.querySelector('#jspsych-video-keyboard-response-stimulus'); + if(video_preload_blob){ - display_element.querySelector('#jspsych-video-keyboard-response-stimulus').src = video_preload_blob; + video_element.src = video_preload_blob; } - display_element.querySelector('#jspsych-video-keyboard-response-stimulus').onended = function(){ + video_element.onended = function(){ if(trial.trial_ends_after_video){ end_trial(); } + if ((trial.response_allowed_while_playing == false) & (!trial.trial_ends_after_video)) { + // start keyboard listener + var keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: after_response, + valid_responses: trial.choices, + rt_method: 'performance', + persist: false, + allow_held_key: false, + }); + } } + + video_element.playbackRate = trial.rate; + // if video start time is specified, hide the video and set the starting time + // before showing and playing, so that the video doesn't automatically show the first frame if(trial.start !== null){ - display_element.querySelector('#jspsych-video-keyboard-response-stimulus').currentTime = trial.start; + video_element.pause(); + video_element.currentTime = trial.start; + video_element.onseeked = function() { + video_element.style.visibility = "visible"; + if (trial.autoplay) { + video_element.play(); + } + } } if(trial.stop !== null){ - display_element.querySelector('#jspsych-video-keyboard-response-stimulus').addEventListener('timeupdate', function(e){ - var currenttime = display_element.querySelector('#jspsych-video-keyboard-response-stimulus').currentTime; + video_element.addEventListener('timeupdate', function(e){ + var currenttime = video_element.currentTime; if(currenttime >= trial.stop){ - display_element.querySelector('#jspsych-video-keyboard-response-stimulus').pause(); + video_element.pause(); } }) } - display_element.querySelector('#jspsych-video-keyboard-response-stimulus').playbackRate = trial.rate; - // store response var response = { rt: null, @@ -181,11 +219,16 @@ jsPsych.plugins["video-keyboard-response"] = (function() { // kill keyboard listeners jsPsych.pluginAPI.cancelAllKeyboardResponses(); + + // stop the video file if it is playing + // remove end event listeners if they exist + display_element.querySelector('#jspsych-video-keyboard-response-stimulus').pause(); + display_element.querySelector('#jspsych-video-keyboard-response-stimulus').onended = function(){ }; // gather the data to store for the trial var trial_data = { "rt": response.rt, - "stimulus": trial.stimulus, + "stimulus": JSON.stringify(trial.stimulus), "key_press": response.key }; @@ -194,7 +237,7 @@ jsPsych.plugins["video-keyboard-response"] = (function() { // move on to the next trial jsPsych.finishTrial(trial_data); - }; + } // function to handle responses by the subject var after_response = function(info) { @@ -214,7 +257,7 @@ jsPsych.plugins["video-keyboard-response"] = (function() { }; // start the response listener - if (trial.choices != jsPsych.NO_KEYS) { + if ((trial.choices != jsPsych.NO_KEYS) & (trial.response_allowed_while_playing)) { var keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({ callback_function: after_response, valid_responses: trial.choices, diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-video-slider-response.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-video-slider-response.js old mode 100755 new mode 100644 similarity index 64% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-video-slider-response.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-video-slider-response.js index e32293ce..19a33181 --- a/app/static/lib/jspsych-6.1.0/plugins/jspsych-video-slider-response.js +++ b/app/static/lib/jspsych-6.2.0/plugins/jspsych-video-slider-response.js @@ -18,7 +18,7 @@ jsPsych.plugins["video-slider-response"] = (function() { name: 'video-slider-response', description: '', parameters: { - sources: { + stimulus: { type: jsPsych.plugins.parameterType.VIDEO, pretty_name: 'Video', default: undefined, @@ -139,14 +139,24 @@ jsPsych.plugins["video-slider-response"] = (function() { pretty_name: 'Response ends trial', default: true, description: 'If true, the trial will end when subject makes a response.' + }, + response_allowed_while_playing: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response allowed while playing', + default: true, + description: 'If true, then responses are allowed while the video is playing. '+ + 'If false, then the video must finish playing before a response is accepted.' } } } plugin.trial = function(display_element, trial) { + // half of the thumb width value from jspsych.css, used to adjust the label positions + var half_thumb_width = 7.5; + // setup stimulus - var video_html = '"; var html = '
'; html += '
' + video_html + '
'; - html += '
'; - html += ''; - html += '
' + html += ''; + var label_width_perc = 100/(trial.labels.length-1); + var percent_of_range = j * (100/(trial.labels.length - 1)); + var percent_dist_from_center = ((percent_of_range-50)/50)*100; + var offset = (percent_dist_from_center * half_thumb_width)/100; + html += '
'; html += ''+trial.labels[j]+''; html += '
' } @@ -202,39 +230,56 @@ jsPsych.plugins["video-slider-response"] = (function() { } // add submit button - html += ''; + var next_disabled_attribute = ""; + if (trial.require_movement | !trial.response_allowed_while_playing) { + next_disabled_attribute = "disabled"; + } + html += ''; display_element.innerHTML = html; + var video_element = display_element.querySelector('#jspsych-video-slider-response-stimulus-video'); + if(video_preload_blob){ - display_element.querySelector('#jspsych-video-slider-response-stimulus').src = video_preload_blob; + video_element.src = video_preload_blob; } - display_element.querySelector('#jspsych-video-slider-response-stimulus').onended = function(){ + video_element.onended = function(){ if(trial.trial_ends_after_video){ end_trial(); + } else if (!trial.response_allowed_while_playing) { + enable_slider(); } } + video_element.playbackRate = trial.rate; + + // if video start time is specified, hide the video and set the starting time + // before showing and playing, so that the video doesn't automatically show the first frame if(trial.start !== null){ - display_element.querySelector('#jspsych-video-slider-response-stimulus').currentTime = trial.start; + video_element.pause(); + video_element.currentTime = trial.start; + video_element.onseeked = function() { + video_element.style.visibility = "visible"; + if (trial.autoplay) { + video_element.play(); + } + } } if(trial.stop !== null){ - display_element.querySelector('#jspsych-video-slider-response-stimulus').addEventListener('timeupdate', function(e){ - var currenttime = display_element.querySelector('#jspsych-video-slider-response-stimulus').currentTime; + video_element.addEventListener('timeupdate', function(e){ + var currenttime = video_element.currentTime; if(currenttime >= trial.stop){ - display_element.querySelector('#jspsych-video-slider-response-stimulus').pause(); + video_element.pause(); } }) } - display_element.querySelector('#jspsych-video-slider-response-stimulus').playbackRate = trial.rate; - if(trial.require_movement){ - display_element.querySelector('#jspsych-video-slider-response-response').addEventListener('change', function(){ + display_element.querySelector('#jspsych-video-slider-response-response').addEventListener('click', function(){ display_element.querySelector('#jspsych-video-slider-response-next').disabled = false; - }) + }); } var startTime = performance.now(); @@ -249,7 +294,7 @@ jsPsych.plugins["video-slider-response"] = (function() { // measure response time var endTime = performance.now(); response.rt = endTime - startTime; - response.response = display_element.querySelector('#jspsych-video-slider-response-response').value; + response.response = display_element.querySelector('#jspsych-video-slider-response-response').valueAsNumber; if(trial.response_ends_trial){ end_trial(); @@ -265,10 +310,17 @@ jsPsych.plugins["video-slider-response"] = (function() { // kill any remaining setTimeout handlers jsPsych.pluginAPI.clearAllTimeouts(); + // stop the video file if it is playing + // remove any remaining end event handlers + display_element.querySelector('#jspsych-video-slider-response-stimulus-video').pause(); + display_element.querySelector('#jspsych-video-slider-response-stimulus-video').onended = function() {}; + // gather the data to store for the trial var trial_data = { "rt": response.rt, - "stimulus": trial.stimulus, + "stimulus": JSON.stringify(trial.stimulus), + "start": trial.start, + "slider_start": trial.slider_start, "response": response.response }; @@ -279,6 +331,14 @@ jsPsych.plugins["video-slider-response"] = (function() { jsPsych.finishTrial(trial_data); }; + // function to enable slider after video ends + function enable_slider() { + document.querySelector('#jspsych-video-slider-response-response').disabled = false; + if (!trial.require_movement) { + document.querySelector('#jspsych-video-slider-response-next').disabled = false; + } + } + // end trial if time limit is set if (trial.trial_duration !== null) { jsPsych.pluginAPI.setTimeout(function() { diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-visual-search-circle.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-visual-search-circle.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-visual-search-circle.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-visual-search-circle.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-vsl-animate-occlusion.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-vsl-animate-occlusion.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-vsl-animate-occlusion.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-vsl-animate-occlusion.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/jspsych-vsl-grid-scene.js b/app/static/lib/jspsych-6.2.0/plugins/jspsych-vsl-grid-scene.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/jspsych-vsl-grid-scene.js rename to app/static/lib/jspsych-6.2.0/plugins/jspsych-vsl-grid-scene.js diff --git a/app/static/lib/jspsych-6.1.0/plugins/template/jspsych-plugin-template.js b/app/static/lib/jspsych-6.2.0/plugins/template/jspsych-plugin-template.js old mode 100755 new mode 100644 similarity index 100% rename from app/static/lib/jspsych-6.1.0/plugins/template/jspsych-plugin-template.js rename to app/static/lib/jspsych-6.2.0/plugins/template/jspsych-plugin-template.js diff --git a/app/templates/experiment.html b/app/templates/experiment.html index 6c2f300c..504379fe 100755 --- a/app/templates/experiment.html +++ b/app/templates/experiment.html @@ -5,16 +5,16 @@ - + - + - + diff --git a/monitor b/monitor deleted file mode 100644 index 99041281..00000000 --- a/monitor +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# monitor - basic script to monitor incoming data -# Author: Sam Zorowitz - -# Check for arguments: time to sleep -local wait="${1:-30s}" - -# Main loop -while -do - - # Clear screen / print timestamp - clear; - echo "Current Status"; - echo "-------------------"; - date +"%Y-%m-%d %H:%M:%S" - echo "-------------------"; - - # Print - echo "experiment:" $(grep -il "experiment" metadata/** | wc -l) - echo "success:" $(grep -il "success" metadata/** | wc -l) - echo "reject:" $(grep -il "reject" metadata/** | wc -l) - echo "error:" $(grep -il "error" metadata/** | wc -l) - echo "data:" $(ls data | wc -l) - echo "\nUpdating every $wait, press Ctrl-C to interrupt." - - # Pause update - sleep $wait - -done