diff --git a/docs/core_library/jspsych-core.md b/docs/core_library/jspsych-core.md index 59d7d7f0..8813663c 100644 --- a/docs/core_library/jspsych-core.md +++ b/docs/core_library/jspsych-core.md @@ -578,14 +578,16 @@ jsPsych.timelineVariable(variable, call_immediate) ### Parameters -| Parameter | Type | Description | -| -------------- | ------ | ---------------------------------------- | -| variable | string | Name of the timeline variable | -| call_immediate | bool | Typically this parameter is `false`, or simply ommitted. When `false`, the return value is a function that returns the timeline variable. This makes `jsPsych.timelineVariable` suitable for dynamic parameters by default. If `true` the function returns the value of the timeline variable immediately. | + +Parameter | Type | Description +----------|------|------------ +variable | string | Name of the timeline variable +call_immediate | bool | This parameter is optional and can usually be omitted. It determines the return value of `jsPsych.timelineVariable`. If `true`, the function returns the _value_ of the current timeline variable. If `false`, the function returns _a function that returns the value_ of the current timeline variable. When `call_immediate` is omitted, the appropriate option is determined automatically based on the context in which this function is called. When `jsPsych.timelineVariable` is used as a parameter value, `call_immediate` will be `false`. This allows it to be used as a [dynamic trial parameter](/overview/trial/#dynamic-parameters). When `jsPsych.timelineVariable` is used inside of a function, `call_immediate` will be `true`. It is possible to explicitly set this option to `true` to force the function to immediately return the current value of the timeline variable. + ### Return value -Depends on the value of `call_immediate` parameter. See description above. +Either a function that returns the value of the timeline variable, or the value of the timeline variable, depending on the context in which it is used. See `call_immediate` description above. ### Description @@ -613,6 +615,25 @@ var procedure = { #### Invoking immediately in a function ```javascript +var trial = { + type: 'html-keyboard-response', + stimulus: function(){ + return ""; + } +} + +var procedure = { + timeline: [trial], + timeline_variables: [ + {image: 'face1.png'}, + {image: 'face2.png'}, + {image: 'face3.png'}, + {image: 'face4.png'} + ] +} +``` +Prior to jsPsych v6.3.0, the `call_immediate` parameter must be set to `true` when `jsPsych.timelineVariable` is called from within a function, such as a [dynamic parameter](/overview/trial/#dynamic-parameters): +```javascript var trial = { type: 'html-keyboard-response', stimulus: function(){ @@ -658,3 +679,29 @@ var time = jsPsych.totalTime(); console.log(time); ``` + +--- +## jsPsych.version + +``` +jsPsych.version +``` + +### Parameters + +None. + +### Return value + +Returns the version number as a string. + +### Description + +Gets the version of jsPsych. + +### Example + +```javascript +var version = jsPsych.version(); +console.log(version); +``` diff --git a/docs/img/devtools-change-css.png b/docs/img/devtools-change-css.png new file mode 100644 index 00000000..e6dca266 Binary files /dev/null and b/docs/img/devtools-change-css.png differ diff --git a/docs/img/devtools-css-errors.png b/docs/img/devtools-css-errors.png new file mode 100644 index 00000000..209ed652 Binary files /dev/null and b/docs/img/devtools-css-errors.png differ diff --git a/docs/img/devtools-inspect-element.png b/docs/img/devtools-inspect-element.png new file mode 100644 index 00000000..bdfa21da Binary files /dev/null and b/docs/img/devtools-inspect-element.png differ diff --git a/docs/overview/style.md b/docs/overview/style.md new file mode 100644 index 00000000..f5f57a12 --- /dev/null +++ b/docs/overview/style.md @@ -0,0 +1,306 @@ +# Style and Formatting + +Your experiment's style and formatting comes the CSS (cascading style sheet) rules that are stored in the jspsych.css file, and the browser's defaults. There are a few ways to change the style and formatting in your experiment. The method that you choose is partly a matter of personal preference. It might also depend on whether you want the style/formatting change(s) to apply to _specific trials_, to _the whole experiment_ (HTML page), or across _different experiments_. This section discusses the different ways of incorporating CSS into your jsPsych experiment. You can also see [this page about adding CSS to web pages](https://www.w3schools.com/html/html_css.asp) to learn more. + + + +## Inline CSS + +Whenever you're using a parameter that accepts an HTML-formatted string, you have the option to include inline CSS. Inline CSS is a way of adding style and formatting directly into a specific HTML element using its ["style" attribute](https://www.w3schools.com/tags/att_style.asp). This is a good option for when you want to make few and/or simple style changes to an HTML trial parameter. + +To change an element's style using inline CSS, you can set the element's "style" attribute to a string that contains the CSS parameters that you want to change, along with the values that you want to use. The syntax is " \ : \ ;". + +In the example below, the stimulus font size is set to 30px and the text color is set to red. These changes will _only_ be applied to this stimulus text in this specific trial. + +```javascript +var trial = { + type: 'html-keyboard-response', + stimulus: '

hello world!

' +} +``` + +You can also use a [dynamic parameter]() to combine inline CSS and trial-specific variables. This allows you to easily apply the same inline CSS to multiple trials. Here's an example using a dynamic stimulus parameter and [timeline variables](): + +```javascript +var trial = { + type: 'html-keyboard-response', + stimulus: function() { + var stim = '

'+jsPsych.timelineVariable('text')+'

'; + return stim; + } +} +var trial_procedure = { + timeline: [trial], + timeline_variables: [ + {text: 'Welcome'}, + {text: 'to'}, + {text: 'the'}, + {text: 'experiment!'} + ] +} +``` + + + +## Adding CSS rules + +You may want to add a lot of different CSS changes to your experiment, re-use the same change(s) across lots of different trials, and/or separate the style/formatting from the HTML string. In these cases, you might find it useful to create CSS rules rather than using inline CSS. + +Creating CSS rules is a lot like using inline CSS, except that you also need to use a [CSS selector](https://www.w3schools.com/css/css_selectors.asp). This is because your CSS rules aren't attached to any specific HTML element (unlike inline CSS), so you need to tell the browser which element(s) the style rules should apply to. The syntax is "css-selector { \ : \ ; }". + +In the example below, the CSS selector "p" tells the browser to apply the font size change to any text that is inside of a \

element. + +```css +p { + font-size: 30px; +} +``` + +You can make more specific changes using CSS rules. The specificity will depend on the CSS selectors that are used. In addition to using the [tag name](https://www.w3schools.com/cssref/sel_element.asp) (e.g. "p"), other common CSS selectors include the element's [ID](https://www.w3schools.com/html/html_id.asp) or [class](https://www.w3schools.com/html/html_classes.asp). If you are selecting an element using it's ID, then the CSS selector needs to have a \# in front of the ID, e.g. "\#stimulus". If you are selecting elements based on their class, then you need to include a . in front of the class, e.g. ".large-text". + +In the example below, the "#stimulus" CSS selector means that the width change will only affect elements with the "stimulus" ID, and the ".large-text" CSS selector means that the font size change will only affect elements that have the "large-text" class. + +```css +#stimulus + width: 300px; +} +.large-text { + font-size: 200%; +} +``` + +It is possible to create even more specific CSS selectors, for instance by combining tags, IDs, and/or classes. For example, let's say that you are showing feedback text to participants, and that this text is inside of a \

tag. You could add the ID "correct" to the \

element for correct response feedback, and the ID "incorrect" to the \

element for incorrect response feedback. Then you can define separate styles for correct and incorrect feedback text like this: + +```css +p#incorrect { + color: red; +} +p#correct { + color: green; +} +``` + +See [this page about CSS selectors](https://www.w3schools.com/cssref/css_selectors.asp) for a complete reference of CSS selector patterns and their meanings. + + + +### With style tags + +You can add CSS rules to your HTML page by putting them inside of \ + +``` + + + +### With a stylesheet + +CSS rules can also be applied to your experiment with a link to an external CSS file. This is the same method that is usually used to apply the style from jspsych.css to an experiment. These rules will be applied to your _whole experiment_. You may find it useful to use a custom stylesheet when you want to re-use the same CSS rules across _multiple experiments_ (HTML files). + +This example shows how to add a custom CSS file in addition to the styles provided in jspsych.css: + +```html + + + + + + +``` + +Below are the some example contents of an external CSS file, like the "my_experiment_style.css" from the example above. This CSS will (1) change the page background color to black, (2) change the default font to 25px and white, and (3) limit the width of the page content so that it can only take up to 80% of its normal width. + +```css +body { + background-color: black; +} +.jspsych-display-element { + font-size: 25px; + color: white; +} +.jspsych-content { + max-width: 80%; +} +``` + +Note that \ + + +``` + +You may want the `css_classes` parameter to vary across trials. If so, you can turn it into a [dynamic parameter]() or use [timeline variables]() (see examples below). + +One thing to note about the `css_classes` parameter is that it only adds the class(es) to the jspsych-content \

element, which is the "parent" element that contains all of the experiment content. Often you'll want your CSS rules to be applied to other elements _inside_ of this jspsych-content div. Sometimes your CSS rules will be "inherited" by all of the other jsPsych content inside of this parent \
. For instance, in the `fixation` example above, the CSS rules that change the font size, weight and color are applied to the parent \
and automatically passed on to the stimulus text through inheritance. + +There are two reasons why a CSS rule like the one above for `fixation` may not work the way you expect: + +1. Not all CSS properties are inherited from the parent element**. ** + + +2. When a CSS property is inherited from the parent element, it will affect _all_ elements in a given trial. + +In these cases, you can change your CSS selector to make it more specific: add a space after class name, then add _more CSS selectors_ to select the specific element(s) that you want to change. + +In the example below, the CSS selector `.left-align #stimulus` selects the element with the ID "stimulus" that is _inside_ of an element with the class "left-align". + +```html + + + + + + + +``` + +It's also possible to pass multiple class names to the `css_classes` parameter. This can be useful for creating conditions that involve crossing different style-related factors. This example shows you to combine two text alignment and two text color factors to produce four different stimulus conditions: + +```html + + + + + + + +``` + +See the "css-classes-parameter.html" file in jsPsych's examples subfolder for more explanation and examples. + + + +## Tips for working with CSS + +Your browser's developer tools contain very useful features for exploring and debugging your experiment's style and formatting. Open your browser's developer tools and click on the Element Inspector button or go to the Elements tab. Once you have selected an element on the page, you can see all of the information that can be used to select it, including: + +1. tag name, e.g., "div", "p", "img", "button" +2. ID, if it has one +3. class(es), if it has any + +You can then use this information to create a CSS selector to modify that element's style. + + + +![devtools-element-inspector](../img/devtools-inspect-element.png) + + + +As you can see, jsPsych adds its own IDs and classes to many elements. You can use the developer tools to determine what IDs and classes already exist for the elements that you want to modify, as you may can often just use these instead of adding your own. For instance, in the "html-keyboard-response" plugin, the stimulus will always be shown in a \
with the ID "jspsych-html-keyboard-response-stimulus". So you can create a CSS rule that is applied to all "html-keyboard-response" stimuli like this: + +```css +#jspsych-html-keyboard-response-stimulus { + color: white; + background-color: blue; + width: 100px; + border: 4px solid black; +} +``` + +As another example, most jsPsych buttons have the class "jspsych-btn", so you can use this class to change the default button styling: + +```css +.jspsych-btn { + padding: 20px 20px; + font-size: 25px; + border-color: black; +} +``` + +You can also use the developer tools to change an element's CSS and immediately see the effect that the changes will have on the page. These changes are just temporary, so you will still need to use one of methods described above to add the CSS changes to your experiment. However, making changes in the developer tools is very useful for figuring out which CSS properties to change and which values to use. This area of the developer tools also shows what styles are currently applied to the element and where those style rules are coming from. + + + +![devtools-change-css](../img/devtools-change-css.png) + + + +There are a few things to be aware of while debugging problems with CSS. + +1. When there are conflicting CSS rules, *some CSS rules will take precedence over others*. For instance, inline CSS usually takes precedence over other CSS rules, and more specific CSS selectors usually take precedence over less specific ones. +2. When there are conflicting CSS rules that have the same level of precedence, *the last rule will override any earlier rules*. For that reason it's important to add your own custom stylesheet _after_ the default jspsych.css stylesheet. See [this page about CSS precedence]() for more information. + +If one CSS style rule is overridden by another one, the rule that is overridden will appear in ~~strikethrough text~~ in the element's "Styles" section. Also, if you are using an incorrect CSS property name or an invalid value, then that will show up here as an error, indicated by both ~~strikethrough text~~ and a little yellow warning symbol. + + + +![devtools-css-error](../img/devtools-css-errors.png) + diff --git a/docs/overview/timeline.md b/docs/overview/timeline.md index 25ad060c..8a0e6594 100644 --- a/docs/overview/timeline.md +++ b/docs/overview/timeline.md @@ -130,7 +130,8 @@ In the above version, there are four separate trials defined in the `timeline_va What if we wanted the stimuli to be a little more complex, with a name displayed below each face? And let's add an additional step where the name is displayed prior to the face appearing. (Maybe this is one condition of an experiment investigating whether the order of name-face or face-name affects retention.) -To do this, we will need to use the `jsPsych.timelineVariable()` method in a slightly different way. Instead of using it as the parameter, we are going to create a dynamic parameter using a function and place the call to `jsPsych.timelineVariable()` inside this function. This will allow us to create an HTML string that has both the image and the name. Note that there is a subtle syntax difference: there is an extra parameter when `jsPsych.timelineVariable()` is called within a function. This `true` value causes the `jsPsych.timelineVariable()` to immediately return the value of the timeline variable. In a normal context, the function `jsPsych.timelineVariable()` returns a function. This is why `jsPsych.timelineVariable()` can be used directly as a parameter even though the parameter is dynamic. +This time, instead of using `jsPsych.timelineVariable()` as the stimulus parameter value, we are going to create a dynamic parameter (function), and place the call to `jsPsych.timelineVariable()` inside this function. This will allow us to create a parameter value that combines multiple bits of information, such as one or more of the values that change across trials (which come from the `timeline_variables` array), and/or anything that doesn't change across trials. In this example, we'll need to switch to using the "html-keyboard-response" plugin so that we can define the stimulus as a custom HTML string that contains an image and text (instead of just an image file). The value of the stimulus parameter will be a function that returns an HTML string that contains both the image and the name. +(Note: in previous versions of jsPsych, there's an extra `true` parameter that you must add when calling `jsPsych.timelineVariable()` from inside a function. As of jsPsych v6.3, `jsPsych.timelineVariable()` automatically detects the context in which it's called, so this additional `true` parameter is not required.) ```javascript @@ -151,8 +152,8 @@ var face_name_procedure = { { type: 'html-keyboard-response', stimulus: function(){ - var html=""; - html += "

"+jsPsych.timelineVariable('name', true)+"

"; + var html=""; + html += "

"+jsPsych.timelineVariable('name')+"

"; return html; }, choices: jsPsych.NO_KEYS, diff --git a/docs/plugins/overview.md b/docs/plugins/overview.md index 68af3d6d..1779e2c7 100644 --- a/docs/plugins/overview.md +++ b/docs/plugins/overview.md @@ -46,6 +46,7 @@ on_finish | function | `function(){ return; }` | A callback function to execute on_start | function | `function(){ return; }` | A callback function to execute when the trial begins, before any loading has occurred. See [this page](../overview/callbacks.md) for more details. on_load | function | `function(){ return; }` | A callback function to execute when the trial has loaded, which typically happens after the initial display of the plugin has loaded. See [this page](../overview/callbacks.md) for more details. data | object | *undefined* | An object containing additional data to store for the trial. See [this page](../overview/data.md) for more details. +css_classes | string | null | A list of CSS classes to add to the jsPsych display element for the duration of this trial. This allows you to create custom formatting rules (CSS classes) that are only applied to specific trials. See jsPsych/examples/css-classes-parameter.html for examples. ## Data collected by plugins diff --git a/examples/css-classes-parameter.html b/examples/css-classes-parameter.html new file mode 100644 index 00000000..9eed7c26 --- /dev/null +++ b/examples/css-classes-parameter.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + diff --git a/examples/jspsych-audio-button-response.html b/examples/jspsych-audio-button-response.html index 0f5a4128..1c51aa5b 100644 --- a/examples/jspsych-audio-button-response.html +++ b/examples/jspsych-audio-button-response.html @@ -29,17 +29,17 @@ timeline.push({ type: 'audio-button-response', stimulus: 'sound/speech_red.mp3', - choices: ['Green', 'Blue', 'Red'], - trial_duration: 2000, - response_ends_trial: false, - prompt: "

What word was said? (trial ends after 2s)

" + choices: ['#00ff00', '#0000ff', '#ff0000'], + response_allowed_while_playing: false, + button_html: '
', + prompt: "

Which color was said?

" }); timeline.push({ type: 'audio-button-response', stimulus: 'sound/speech_joke.mp3', choices: ['Not funny', 'Funny'], - prompt: '

How funny was the joke?

When the audio stops, click a button to end the trial.

Response buttons are disabled while the audio is playing.

', + prompt: '

Is the joke funny?

When the audio stops, click a button to end the trial.

Response buttons are disabled while the audio is playing.

', response_allowed_while_playing: false }) diff --git a/examples/jspsych-canvas-button-response.html b/examples/jspsych-canvas-button-response.html index 29940bae..4034cd78 100644 --- a/examples/jspsych-canvas-button-response.html +++ b/examples/jspsych-canvas-button-response.html @@ -67,12 +67,12 @@ }; // to use the canvas stimulus function with timeline variables, - // use the jsPsych.timelineVariable() function inside your stimulus function with the second 'true' argument + // the jsPsych.timelineVariable() function can be used inside your stimulus function var circle_procedure = { timeline: [{ type: 'canvas-button-response', stimulus: function(c) { - filledCirc(c, jsPsych.timelineVariable('radius', true), jsPsych.timelineVariable('color', true)); + filledCirc(c, jsPsych.timelineVariable('radius'), jsPsych.timelineVariable('color')); }, choices: ['Red', 'Green', 'Blue'], prompt: '

What color is the circle?

', diff --git a/examples/jspsych-canvas-keyboard-response.html b/examples/jspsych-canvas-keyboard-response.html index d12b4828..560546a8 100644 --- a/examples/jspsych-canvas-keyboard-response.html +++ b/examples/jspsych-canvas-keyboard-response.html @@ -41,19 +41,19 @@ } // to use the canvas stimulus function with timeline variables, - // use the jsPsych.timelineVariable() function inside your stimulus function with the second 'true' argument + // the jsPsych.timelineVariable() function can be used inside your stimulus function var trial_procedure = { timeline: [{ type: 'canvas-keyboard-response', stimulus: function(c) { var ctx = c.getContext('2d'); ctx.beginPath(); - ctx.fillStyle = jsPsych.timelineVariable('color', true); + ctx.fillStyle = jsPsych.timelineVariable('color'); ctx.fillRect( - jsPsych.timelineVariable('upper_left_x', true), - jsPsych.timelineVariable('upper_left_y', true), - jsPsych.timelineVariable('width', true), - jsPsych.timelineVariable('height', true) + jsPsych.timelineVariable('upper_left_x'), + jsPsych.timelineVariable('upper_left_y'), + jsPsych.timelineVariable('width'), + jsPsych.timelineVariable('height') ); ctx.stroke(); }, diff --git a/examples/jspsych-video-button-response.html b/examples/jspsych-video-button-response.html index cecb0ff9..2e8d02b3 100644 --- a/examples/jspsych-video-button-response.html +++ b/examples/jspsych-video-button-response.html @@ -18,8 +18,7 @@ var trial_1 = { type: 'video-button-response', stimulus: ['video/sample_video.mp4'], - choices: ['y','n'], - button_html: '', + choices: ['Y','N'], margin_vertical: '10px', margin_horizontal: '8px', prompt: 'Press Y or N', @@ -37,10 +36,11 @@ var trial_2 = { type: 'video-button-response', stimulus: ['video/sample_video.mp4'], - choices: ['Great','Not great'], + choices: ['😄','😁','🥱','😣','🤯'], + button_html: '
%choice%
', margin_vertical: '10px', margin_horizontal: '8px', - prompt: '

How great was the video?

When the video stops, click a button to end the trial.

Response buttons are disabled while the video is playing.

', + prompt: '

Click the emoji that best represents your reaction to the video

When the video stops, click a button to end the trial.

Response buttons are disabled while the video is playing.

', width: 600, autoplay: true, response_ends_trial: true, diff --git a/examples/lexical-decision.html b/examples/lexical-decision.html index c26b9f3f..527d1232 100644 --- a/examples/lexical-decision.html +++ b/examples/lexical-decision.html @@ -72,16 +72,18 @@ timeline: [ { type: 'html-keyboard-response', - stimulus: '

+

', + stimulus: '+', choices: jsPsych.NO_KEYS, trial_duration: 500, - post_trial_gap: 0 + post_trial_gap: 0, + css_classes: ['stimulus'] }, { type: 'html-keyboard-response', - stimulus: function(){ return "

"+jsPsych.timelineVariable('word', true)+"

"; }, + stimulus: jsPsych.timelineVariable('word'), choices: ['y','n'], post_trial_gap: 0, + css_classes: ['stimulus'], data: { word_validity: jsPsych.timelineVariable('word_validity'), word_frequency: jsPsych.timelineVariable('word_frequency') diff --git a/jspsych.js b/jspsych.js index 952eeea9..3924e466 100755 --- a/jspsych.js +++ b/jspsych.js @@ -9,6 +9,8 @@ window.jsPsych = (function() { var core = {}; + core.version = function() { return "6.3.0" }; + // // private variables // @@ -263,6 +265,11 @@ window.jsPsych = (function() { if(current_trial_finished){ return; } current_trial_finished = true; + // remove any CSS classes that were added to the DOM via css_classes parameter + if(typeof current_trial.css_classes !== 'undefined' && Array.isArray(current_trial.css_classes)){ + DOM_target.classList.remove(...current_trial.css_classes); + } + // write the data from the trial data = typeof data == 'undefined' ? {} : data; jsPsych.data.write(data); @@ -274,6 +281,9 @@ window.jsPsych = (function() { // of the DataCollection, for easy access and editing. var trial_data_values = trial_data.values()[0]; + // about to execute lots of callbacks, so switch context. + jsPsych.internal.call_immediate = true; + // handle callback at plugin level if (typeof current_trial.on_finish === 'function') { current_trial.on_finish(trial_data_values); @@ -287,6 +297,9 @@ window.jsPsych = (function() { // data object that just went through the trial's finish handlers. opts.on_data_update(trial_data_values); + // done with callbacks + jsPsych.internal.call_immediate = false; + // wait for iti if (typeof current_trial.post_trial_gap === null || typeof current_trial.post_trial_gap === 'undefined') { if (opts.default_iti > 0) { @@ -327,8 +340,9 @@ window.jsPsych = (function() { return timeline.activeID(); }; - core.timelineVariable = function(varname, execute){ - if(execute){ + core.timelineVariable = function(varname, immediate){ + if(typeof immediate == 'undefined'){ immediate = false; } + if(jsPsych.internal.call_immediate || immediate === true){ return timeline.timelineVariable(varname); } else { return function() { return timeline.timelineVariable(varname); } @@ -490,7 +504,9 @@ window.jsPsych = (function() { // check for conditonal function on nodes with timelines if (typeof timeline_parameters != 'undefined') { if (typeof timeline_parameters.conditional_function !== 'undefined') { + jsPsych.internal.call_immediate = true; var conditional_result = timeline_parameters.conditional_function(); + jsPsych.internal.call_immediate = false; // if the conditional_function() returns false, then the timeline // doesn't run and is marked as complete. if (conditional_result == false) { @@ -551,11 +567,14 @@ window.jsPsych = (function() { // if we're all done with the repetitions, check if there is a loop function. else if (typeof timeline_parameters.loop_function !== 'undefined') { + jsPsych.internal.call_immediate = true; if (timeline_parameters.loop_function(this.generatedData())) { this.reset(); + jsPsych.internal.call_immediate = false; return parent_node.advance(); } else { progress.done = true; + jsPsych.internal.call_immediate = false; return true; } } @@ -872,6 +891,9 @@ window.jsPsych = (function() { // get default values for parameters setDefaultValues(trial); + // about to execute callbacks + jsPsych.internal.call_immediate = true; + // call experiment wide callback opts.on_trial_start(trial); @@ -886,6 +908,16 @@ window.jsPsych = (function() { // reset the scroll on the DOM target DOM_target.scrollTop = 0; + // add CSS classes to the DOM_target if they exist in trial.css_classes + if(typeof trial.css_classes !== 'undefined'){ + if(!Array.isArray(trial.css_classes) && typeof trial.css_classes == 'string'){ + trial.css_classes = [trial.css_classes]; + } + if(Array.isArray(trial.css_classes)){ + DOM_target.classList.add(...trial.css_classes) + } + } + // execute trial method jsPsych.plugins[trial.type].trial(DOM_target, trial); @@ -893,6 +925,9 @@ window.jsPsych = (function() { if(typeof trial.on_load == 'function'){ trial.on_load(); } + + // done with callbacks + jsPsych.internal.call_immediate = false; } function evaluateTimelineVariables(trial){ @@ -912,6 +947,9 @@ window.jsPsych = (function() { function evaluateFunctionParameters(trial){ + // set a flag so that jsPsych.timelineVariable() is immediately executed in this context + jsPsych.internal.call_immediate = true; + // first, eval the trial type if it is a function // this lets users set the plugin type with a function if(typeof trial.type === 'function'){ @@ -950,6 +988,9 @@ window.jsPsych = (function() { } } } + + // reset so jsPsych.timelineVariable() is no longer immediately executed + jsPsych.internal.call_immediate = false; } function setDefaultValues(trial){ @@ -1067,6 +1108,17 @@ window.jsPsych = (function() { return core; })(); +jsPsych.internal = (function() { + var module = {}; + + // this flag is used to determine whether we are in a scope where + // jsPsych.timelineVariable() should be executed immediately or + // whether it should return a function to access the variable later. + module.call_immediate = false; + + return module; +})(); + jsPsych.plugins = (function() { var module = {}; @@ -1118,6 +1170,12 @@ jsPsych.plugins = (function() { pretty_name: 'Post trial gap', default: null, description: 'Length of gap between the end of this trial and the start of the next trial' + }, + css_classes: { + type: module.parameterType.STRING, + pretty_name: 'Custom CSS classes', + default: null, + description: 'A list of CSS classes to add to the jsPsych display element for the duration of this trial' } } diff --git a/plugins/jspsych-audio-button-response.js b/plugins/jspsych-audio-button-response.js index 8ce7a82d..bd4078fe 100644 --- a/plugins/jspsych-audio-button-response.js +++ b/plugins/jspsych-audio-button-response.js @@ -140,16 +140,12 @@ jsPsych.plugins["audio-button-response"] = (function() { html += trial.prompt; } - display_element.innerHTML = html; - - for (var i = 0; i < trial.choices.length; i++) { - display_element.querySelector('#jspsych-audio-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-audio-button-response-button-' + i).querySelector('button').disabled = true; - } + display_element.innerHTML = html; + + if(trial.response_allowed_while_playing){ + enable_buttons(); + } else { + disable_buttons(); } // store response @@ -172,11 +168,7 @@ jsPsych.plugins["audio-button-response"] = (function() { response.rt = rt; // disable all the buttons after a response - var btns = document.querySelectorAll('.jspsych-audio-button-response-button button'); - for(var i=0; i'+str+'
'; + video_html += '
'+str+'
'; } video_html += '
'; @@ -210,10 +210,7 @@ jsPsych.plugins["video-button-response"] = (function() { if(trial.trial_ends_after_video){ end_trial(); } else if (!trial.response_allowed_while_playing) { - // enable response buttons - for (var i=0; ifoo

', + css_classes: ['foo'] + } + + jsPsych.init({timeline:[trial]}); + + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(true); + utils.pressKey(32); + }) + + test('Gracefully handles single class when not in array', function(){ + var trial = { + type: 'html-keyboard-response', + stimulus: '

foo

', + css_classes: 'foo' + } + + jsPsych.init({timeline:[trial]}); + + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(true); + utils.pressKey(32); + }) + + test('Removes the added classes at the end of the trial', function(){ + var trial = { + type: 'html-keyboard-response', + stimulus: '

foo

', + css_classes: ['foo'] + } + + jsPsych.init({timeline:[trial]}); + + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(true); + utils.pressKey(32); + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(false); + + }) + + test('Class inherits in nested timelines', function(){ + var tm = { + timeline: [{ + type: 'html-keyboard-response', + stimulus: '

foo

', + }], + css_classes: ['foo'] + } + + jsPsych.init({timeline:[tm]}); + + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(true); + utils.pressKey(32); + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(false); + + }) + + test('Parameter works when defined as a function', function(){ + var trial = { + type: 'html-keyboard-response', + stimulus: '

foo

', + css_classes: function(){ + return ['foo'] + } + } + + jsPsych.init({timeline:[trial]}); + + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(true); + utils.pressKey(32); + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(false); + + }) + + test('Parameter works when defined as a timeline variable', function(){ + var trial = { + type: 'html-keyboard-response', + stimulus: '

foo

', + css_classes: jsPsych.timelineVariable('css') + } + + var t = { + timeline: [trial], + timeline_variables: [ + {css: ['foo']} + ] + } + + jsPsych.init({timeline:[t]}); + + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(true); + utils.pressKey(32); + expect(jsPsych.getDisplayElement().classList.contains('foo')).toBe(false); + + }) +}) \ No newline at end of file diff --git a/tests/jsPsych/timeline-variables.test.js b/tests/jsPsych/timeline-variables.test.js index 3b18a09a..5f5f4852 100644 --- a/tests/jsPsych/timeline-variables.test.js +++ b/tests/jsPsych/timeline-variables.test.js @@ -250,5 +250,176 @@ describe('timeline variables are correctly evaluated', function(){ }); + test('when used inside a function', function(){ + var tvs = [ + {x: 'foo'}, + {x: 'bar'} + ] + + var trial = { + type: 'html-keyboard-response', + stimulus: function(){ + return jsPsych.timelineVariable('x'); + } + } + + var p = { + timeline: [trial], + timeline_variables: tvs + } + + jsPsych.init({ + timeline: [p] + }) + + expect(jsPsych.getDisplayElement().innerHTML).toMatch('foo'); + utils.pressKey(32); + expect(jsPsych.getDisplayElement().innerHTML).toMatch('bar'); + }); + + test('when used in a conditional_function', function(){ + var tvs = [ + {x: 'foo'} + ] + + var trial = { + type: 'html-keyboard-response', + stimulus: 'hello world' + } + + var x = null; + + var p = { + timeline: [trial], + timeline_variables: tvs, + conditional_function: function(){ + x = jsPsych.timelineVariable('x'); + return true; + } + } + + jsPsych.init({ + timeline: [p] + }) + + + utils.pressKey(32); + expect(x).toBe('foo'); + }) + + test('when used in a loop_function', function(){ + var tvs = [ + {x: 'foo'} + ] + + var trial = { + type: 'html-keyboard-response', + stimulus: 'hello world' + } + + var x = null; + + var p = { + timeline: [trial], + timeline_variables: tvs, + loop_function: function(){ + x = jsPsych.timelineVariable('x'); + return false; + } + } + + jsPsych.init({ + timeline: [p] + }) + + + utils.pressKey(32); + expect(x).toBe('foo'); + }) + + test('when used in on_finish', function(){ + var tvs = [ + {x: 'foo'} + ] + + var trial = { + type: 'html-keyboard-response', + stimulus: 'hello world', + on_finish: function(data){ + data.x = jsPsych.timelineVariable('x'); + } + } + + var t = { + timeline: [trial], + timeline_variables: tvs + } + + jsPsych.init({ + timeline: [t] + }) + + + utils.pressKey(32); + expect(jsPsych.data.get().values()[0].x).toBe('foo'); + }) + + test('when used in on_start', function(){ + var tvs = [ + {x: 'foo'} + ] + + var x = null; + + var trial = { + type: 'html-keyboard-response', + stimulus: 'hello world', + on_start: function(){ + x = jsPsych.timelineVariable('x'); + } + } + + var t = { + timeline: [trial], + timeline_variables: tvs + } + + jsPsych.init({ + timeline: [t] + }) + + + utils.pressKey(32); + expect(x).toBe('foo'); + }) + + test('when used in on_load', function(){ + var tvs = [ + {x: 'foo'} + ] + + var x = null; + + var trial = { + type: 'html-keyboard-response', + stimulus: 'hello world', + on_load: function(){ + x = jsPsych.timelineVariable('x'); + } + } + + var t = { + timeline: [trial], + timeline_variables: tvs + } + + jsPsych.init({ + timeline: [t] + }) + + + utils.pressKey(32); + expect(x).toBe('foo'); + }) }) diff --git a/tests/jsPsych/timelines.test.js b/tests/jsPsych/timelines.test.js index 68aac8d5..36082186 100644 --- a/tests/jsPsych/timelines.test.js +++ b/tests/jsPsych/timelines.test.js @@ -125,7 +125,7 @@ describe('loop function', function(){ stimulus: 'foo' }], loop_function: function(){ - if(jsPsych.timelineVariable('word', true) == 'b' && counter < 2){ + if(jsPsych.timelineVariable('word') == 'b' && counter < 2){ counter++; return true; } else { @@ -283,7 +283,7 @@ describe('conditional function', function(){ var innertimeline = { timeline: [trial], conditional_function: function(){ - if(jsPsych.timelineVariable('word', true) == 'b'){ + if(jsPsych.timelineVariable('word') == 'b'){ return false; } else { return true;