Merge branch 'master' into feature-key-code-update

This commit is contained in:
Josh de Leeuw 2021-02-08 22:00:09 -05:00 committed by GitHub
commit b10c69eb21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1036 additions and 130 deletions

View File

@ -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 "<img style='width:100px; height:100px;' src='"+jsPsych.timelineVariable('image')+"'></img>";
}
}
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);
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

306
docs/overview/style.md Normal file
View File

@ -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 " \<parameter-name> : \<parameter-value> ;".
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: '<p style="font-size:30px;color:red;">hello world!</p>'
}
```
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 = '<p style="font-size:30px;font-weight:bold;">'+jsPsych.timelineVariable('text')+'</p>';
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 { \<parameter-name> : \<parameter-value> ; }".
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 \<p> 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 \<p> tag. You could add the ID "correct" to the \<p> element for correct response feedback, and the ID "incorrect" to the \<p> 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 \<style> tags. These rules will be applied to your _whole experiment_. This method can be useful for making general changes to the way that your experiment looks.
In the example below, the default font size is set to 25px throughout the experiment. This will overrule the default font size of 18px that is set in the jspsych.css file.
```html
<head>
<script src="jsPsych/jspsych.js"></script>
<script src="jsPsych/plugins/jspsych-html-keyboard-response.js"></script>
<link rel="stylesheet" href="jsPsych/css/jspsych.css">
<style>
.jspsych-display-element {
font-size: 25px;
}
</style>
</head>
```
### 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
<head>
<script src="jsPsych/jspsych.js"></script>
<script src="jsPsych/plugins/jspsych-image-keyboard-response.js"></script>
<link rel="stylesheet" href="jsPsych/css/jspsych.css">
<link rel="stylesheet" href="my_experiment_style.css">
</head>
```
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 \<style> tags are not used inside of an external CSS file.
### Using the css_classes trial parameter
CSS rules can also be applied in a trial-specific way using the `css_classes` parameter. This parameter will apply one or more class to the \<div> element that holds all of the jsPsych page content during that specific trial. This way you can treat CSS styles just like any other trial parameter.
You can use a static `css_classes` parameter value if you always want to apply the same CSS rules to the trial. In the 'fixation' example below, separating the style rules from the `stimulus` string makes the code a little bit 'cleaner', and this makes it easier to re-use the same style rules in other parts of the experiment.
```html
<head>
<script src="jsPsych/jspsych.js"></script>
<script src="jsPsych/plugins/jspsych-html-keyboard-response.js"></script>
<link rel="stylesheet" href="jsPsych/css/jspsych.css">
<style>
.fixation {font-size: 90px; font-weight: bold; color: gray;}
</style>
</head>
<script>
var fixation = {
type: 'html-keyboard-response',
stimulus: '+',
choices: jsPsych.NO_KEYS,
trial_duration: 500,
css_classes: ['fixation']
}
// ...
</script>
```
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 \<div> 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 \<div>. For instance, in the `fixation` example above, the CSS rules that change the font size, weight and color are applied to the parent \<div> 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
<head>
<script src="jsPsych/jspsych.js"></script>
<script src="jsPsych/plugins/jspsych-html-keyboard-response.js"></script>
<link rel="stylesheet" href="jsPsych/css/jspsych.css">
<style>
.left-align #stimulus {text-align: left; width: 600px;}
.right-align #stimulus {text-align: right; width: 600px;}
</style>
</head>
<script>
var trial_procedure = {
timeline: [{
type: 'html-keyboard-response',
stimulus: '<p id="stimulus">This is the stimulus.</p>',
prompt: '<p>This text will not be affected by the CSS classes '+
'because it does not have the "stimulus" ID.</p>',
css_classes: jsPsych.timelineVariable('css_classes')
}],
timeline_variables: [
{css_classes: ['left-align']},
{css_classes: ['right-align']}
]
}
// ...
</script>
```
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
<head>
<script src="jsPsych/jspsych.js"></script>
<script src="jsPsych/plugins/jspsych-html-keyboard-response.js"></script>
<link rel="stylesheet" href="jsPsych/css/jspsych.css">
<style>
.left-align #stimulus {text-align: left; width: 600px;}
.right-align #stimulus {text-align: right; width: 600px;}
.teal #stimulus {color: teal;}
.purple #stimulus {color: purple;}
</style>
</head>
<script>
var trial_procedure = {
timeline: [{
type: 'html-keyboard-response',
stimulus: '<p id="stimulus">This is the stimulus.</p>',
prompt: '<p>This text will not be affected by the CSS classes '+
'because it does not have the "stimulus" ID.</p>',
css_classes: jsPsych.timelineVariable('css_classes'),
data: {condition: jsPsych.timelineVariable('condition')}
}],
timeline_variables: [
{css_classes: ['left-align','teal'], condition: 'left-teal'},
{css_classes: ['right-align','teal'], condition: 'right-teal'},
{css_classes: ['left-align','purple'], condition: 'left-purple'},
{css_classes: ['right-align','purple'], condition: 'right-purple'}
]
}
// ...
</script>
```
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 \<div> 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)

View File

@ -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="<img src='"+jsPsych.timelineVariable('face', true)+"'>";
html += "<p>"+jsPsych.timelineVariable('name', true)+"</p>";
var html="<img src='"+jsPsych.timelineVariable('face')+"'>";
html += "<p>"+jsPsych.timelineVariable('name')+"</p>";
return html;
},
choices: jsPsych.NO_KEYS,

View File

@ -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

View File

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html>
<head>
<script src="../jspsych.js"></script>
<script src="../plugins/jspsych-image-keyboard-response.js"></script>
<script src="../plugins/jspsych-html-keyboard-response.js"></script>
<link rel="stylesheet" href="../css/jspsych.css">
<style>
/*
Any CSS classes listed in the 'css_classes' parameter will only be added to the jspsych-content div.
Certain CSS rules will automatically be applied to all other elements _inside_ this div (stimulus, prompt, etc.),
through CSS inheritance. However, not all CSS properties are inherited from parent elements.
To learn more about CSS inheritance, see:
https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance
https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Cascade_and_inheritance#inheritance
https://stackoverflow.com/questions/5612302/which-css-properties-are-inherited
In the CSS rules below, the rules are applied to the jspsych-content div, and then applied to
other elements inside of the jspsych-content div through inheritance. This is why both the stimulus and
prompt are affected by these rules in the trial_procedure_1 trials.
*/
.condition1 {
color: red;
font: small-caps bold 25px/25px Arial, sans-serif;
line-height: 150%;
}
.condition2 {
font-size: 20px;
font-weight: bold;
text-shadow: 2px 2px white;
line-height: 200%;
background-color: lightgray
}
/*
It's also possible to use more specific CSS selectors so that the CSS rule only affects certain elements on the page.
To create these CSS rules, we start with the class name used in "css_classes". This will select the 'parent' element (jspsych-content div):
.left-align
After that, add the more specific CSS selector(s) to select any particular element(s) that should be modified inside the parent element.
For example, to select only the "p" elements that are inside of the 'parent' element, you can use this:
.left-align p {...}
This method allows you to use different styles for different 'child' elements inside the jspsych-content div.
This method also allows you to modify properties that can't be modified through inheritance.
Options for CSS selectors include the element's tag name, ID, class, and combinations of these.
For example, to select a the element with the ID "jspsych-html-keyboard-response-stimulus":
.left-align #jspsych-html-keyboard-response-stimulus {...}
Or to select all "p" elements that also have the class "stimulus":
.left-align p.stimulus {...}
See here for more info about CSS selectors: https://www.w3schools.com/css/css_selectors.asp
In the trial_procedure_2 trials, the stimulus text is inside of <p> tags and the prompt text is not inside of <p> tags.
Therefore the CSS rules below will only affect the stimulus element in these trials.
*/
.left-align p {text-align: left; width: 600px;}
.right-align p {text-align: right; width: 600px;}
.teal p {color: teal;}
.purple p {color: purple;}
.large-text p {font-size: 40px; line-height: 1.5em;}
.black-border p {border: 4px solid black;}
/*
The CSS rules below are used in trial_procedure_3 to format the fixation cross
and move the img element to the left or right.
*/
.fixation {font-size: 90px; font-weight: bold;}
.img-left img {transform: translate(-300px);}
.img-right img {transform: translate(300px);}
</style>
</head>
<body></body>
<script>
var trial_procedure_1 = {
timeline: [{
type: 'html-keyboard-response',
stimulus: jsPsych.timelineVariable('stim'),
css_classes: jsPsych.timelineVariable('css_class'),
prompt: 'This is the prompt.<br>In this set of trials, the CSS styles are applied to the parent element<br>'+
'and passed on to all other jsPsych content through inheritance,<br>'+
'which is why they also affect both the stimulus and prompt text.<br>Press any key to continue.'
}],
timeline_variables: [
{css_class: ['condition1'], stim: '<p>This is a Condition 1 stimulus.</p>'},
{css_class: ['condition2'], stim: '<p>This is a Condition 2 stimulus.</p>'}
]
}
var trial_procedure_2 = {
timeline: [{
type: 'html-keyboard-response',
stimulus: '<p>This is the stimulus.</p>',
css_classes: jsPsych.timelineVariable('css_classes'),
prompt: 'In this set of trials, the CSS rules are applied more selectively<br>so that they only affect the stimulus text, not the prompt text<br>'+
'(see the comments in the HTML file for more information).<br>Press any key to continue.'
}],
timeline_variables: [
{css_classes: ['teal','left-align','black-border','large-text']},
{css_classes: ['teal','right-align','black-border','large-text']},
{css_classes: ['purple','left-align','large-text']},
{css_classes: ['purple','right-align','large-text']}
]
}
var trial_procedure_3 = {
timeline: [
{
type: 'html-keyboard-response',
stimulus: '+',
choices: jsPsych.NO_KEYS,
trial_duration: 500,
css_classes: ['fixation']
},
{
type: 'image-keyboard-response',
stimulus: 'img/blue.png',
css_classes: jsPsych.timelineVariable('image_side'),
prompt: '<p>Press <strong>f</strong> if the image is on the <strong>left</strong>.</p>'+
'<p>Press <strong>j</strong> if the image is on the <strong>right</strong>.</p>',
choices: ['f','j'],
render_on_canvas: false
}
],
timeline_variables: [
{image_side: ['img-left'], correct_response: 'f'},
{image_side: ['img-right'], correct_response: 'j'}
]
}
jsPsych.init({
timeline: [trial_procedure_1, trial_procedure_2, trial_procedure_3],
on_finish: function() {
jsPsych.data.displayData();
}
});
</script>
</html>

View File

@ -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: "<p>What word was said? (trial ends after 2s)</p>"
choices: ['#00ff00', '#0000ff', '#ff0000'],
response_allowed_while_playing: false,
button_html: '<div style="background-color: %choice%; width:100px; height:100px;"></div>',
prompt: "<p>Which color was said?</p>"
});
timeline.push({
type: 'audio-button-response',
stimulus: 'sound/speech_joke.mp3',
choices: ['Not funny', 'Funny'],
prompt: '<p>How funny was the joke?</p><p>When the audio stops, click a button to end the trial.</p><p>Response buttons are disabled while the audio is playing.</p>',
prompt: '<p>Is the joke funny?</p><p>When the audio stops, click a button to end the trial.</p><p>Response buttons are disabled while the audio is playing.</p>',
response_allowed_while_playing: false
})

View File

@ -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: '<p>What color is the circle?</p>',

View File

@ -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();
},

View File

@ -18,8 +18,7 @@
var trial_1 = {
type: 'video-button-response',
stimulus: ['video/sample_video.mp4'],
choices: ['y','n'],
button_html: '<button class="jspsych-btn">%choice%!</button>',
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: '<div style="font-size:40px;">%choice%</div>',
margin_vertical: '10px',
margin_horizontal: '8px',
prompt: '<p>How great was the video?</p><p>When the video stops, click a button to end the trial.</p><p>Response buttons are disabled while the video is playing.</p>',
prompt: '<p>Click the emoji that best represents your reaction to the video</p><p>When the video stops, click a button to end the trial.</p><p>Response buttons are disabled while the video is playing.</p>',
width: 600,
autoplay: true,
response_ends_trial: true,

View File

@ -72,16 +72,18 @@
timeline: [
{
type: 'html-keyboard-response',
stimulus: '<p class="stimulus">+</p>',
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 "<p class='stimulus'>"+jsPsych.timelineVariable('word', true)+"</p>"; },
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')

View File

@ -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'
}
}

View File

@ -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<btns.length; i++){
//btns[i].removeEventListener('click');
btns[i].setAttribute('disabled', 'disabled');
}
disable_buttons();
if (trial.response_ends_trial) {
end_trial();
@ -215,11 +207,30 @@ jsPsych.plugins["audio-button-response"] = (function() {
jsPsych.finishTrial(trial_data);
}
// function to enable buttons after audio ends
function enable_buttons() {
var btns = document.querySelectorAll('.jspsych-audio-button-response-button button');
function button_response(e){
var choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility
after_response(choice);
}
function disable_buttons() {
var btns = document.querySelectorAll('.jspsych-audio-button-response-button');
for (var i=0; i<btns.length; i++) {
btns[i].disabled = false;
var btn_el = btns[i].querySelector('button');
if(btn_el){
btn_el.disabled = true;
}
btns[i].removeEventListener('click', button_response);
}
}
function enable_buttons() {
var btns = document.querySelectorAll('.jspsych-audio-button-response-button');
for (var i=0; i<btns.length; i++) {
var btn_el = btns[i].querySelector('button');
if(btn_el){
btn_el.disabled = false;
}
btns[i].addEventListener('click', button_response);
}
}

View File

@ -107,6 +107,7 @@ jsPsych.plugins["image-button-response"] = (function() {
var height, width;
var html;
if (trial.render_on_canvas) {
var image_drawn = false;
// 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()
@ -119,29 +120,40 @@ jsPsych.plugins["image-button-response"] = (function() {
canvas.id = "jspsych-image-button-response-stimulus";
canvas.style.margin = 0;
canvas.style.padding = 0;
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function() {
// if image wasn't preloaded, then it will need to be drawn whenever it finishes loading
if (!image_drawn) {
getHeightWidth(); // only possible to get width/height after image loads
ctx.drawImage(img,0,0,width,height);
}
};
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);
// get/set image height and width - this can only be done after image loads because uses image's naturalWidth/naturalHeight properties
function getHeightWidth() {
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;
}
} 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);
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;
}
} 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;
}
canvas.height = height;
canvas.width = width;
getHeightWidth(); // call now, in case image loads immediately (is cached)
// create buttons
var buttons = [];
if (Array.isArray(trial.button_html)) {
@ -165,8 +177,12 @@ jsPsych.plugins["image-button-response"] = (function() {
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);
if (img.complete && Number.isFinite(width) && Number.isFinite(height)) {
// if image has loaded and width/height have been set, then draw it now
// (don't rely on img onload function to draw image when image is in the cache, because that causes a delay in the image presentation)
ctx.drawImage(img,0,0,width,height);
image_drawn = true;
}
// add buttons to screen
display_element.insertBefore(btngroup_div, canvas.nextElementSibling);
// add prompt if there is one

View File

@ -88,6 +88,7 @@ jsPsych.plugins["image-keyboard-response"] = (function() {
var height, width;
if (trial.render_on_canvas) {
var image_drawn = false;
// 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()
@ -100,33 +101,48 @@ jsPsych.plugins["image-keyboard-response"] = (function() {
canvas.id = "jspsych-image-keyboard-response-stimulus";
canvas.style.margin = 0;
canvas.style.padding = 0;
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function() {
// if image wasn't preloaded, then it will need to be drawn whenever it finishes loading
if (!image_drawn) {
getHeightWidth(); // only possible to get width/height after image loads
ctx.drawImage(img,0,0,width,height);
}
};
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);
// get/set image height and width - this can only be done after image loads because uses image's naturalWidth/naturalHeight properties
function getHeightWidth() {
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;
}
} 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);
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;
}
} 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;
}
canvas.height = height;
canvas.width = width;
getHeightWidth(); // call now, in case image loads immediately (is cached)
// add canvas and draw image
display_element.insertBefore(canvas, null);
var ctx = canvas.getContext("2d");
ctx.drawImage(img,0,0,width,height);
if (img.complete && Number.isFinite(width) && Number.isFinite(height)) {
// if image has loaded and width/height have been set, then draw it now
// (don't rely on img onload function to draw image when image is in the cache, because that causes a delay in the image presentation)
ctx.drawImage(img,0,0,width,height);
image_drawn = true;
}
// add prompt if there is one
if (trial.prompt !== null) {
display_element.insertAdjacentHTML('beforeend', trial.prompt);

View File

@ -135,6 +135,7 @@ jsPsych.plugins['image-slider-response'] = (function() {
var half_thumb_width = 7.5;
if (trial.render_on_canvas) {
var image_drawn = false;
// 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()
@ -150,29 +151,40 @@ jsPsych.plugins['image-slider-response'] = (function() {
canvas.id = "jspsych-image-slider-response-stimulus";
canvas.style.margin = 0;
canvas.style.padding = 0;
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function() {
// if image wasn't preloaded, then it will need to be drawn whenever it finishes loading
if (!image_drawn) {
getHeightWidth(); // only possible to get width/height after image loads
ctx.drawImage(img,0,0,width,height);
}
};
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);
// get/set image height and width - this can only be done after image loads because uses image's naturalWidth/naturalHeight properties
function getHeightWidth() {
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;
}
} 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);
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;
}
} 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;
}
canvas.height = height;
canvas.width = width;
getHeightWidth(); // call now, in case image loads immediately (is cached)
// create container with slider and labels
var slider_container = document.createElement('div');
slider_container.classList.add("jspsych-image-slider-response-container");
@ -201,8 +213,12 @@ jsPsych.plugins['image-slider-response'] = (function() {
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);
if (img.complete && Number.isFinite(width) && Number.isFinite(height)) {
// if image has loaded and width/height have been set, then draw it now
// (don't rely on img onload function to draw image when image is in the cache, because that causes a delay in the image presentation)
ctx.drawImage(img,0,0,width,height);
image_drawn = true;
}
// add prompt if there is one
if (trial.prompt !== null) {
display_element.insertAdjacentHTML('beforeend', trial.prompt);

View File

@ -187,7 +187,7 @@ jsPsych.plugins["video-button-response"] = (function() {
video_html += '<div id="jspsych-video-button-response-btngroup">';
for (var i = 0; i < trial.choices.length; i++) {
var str = buttons[i].replace(/%choice%/g, trial.choices[i]);
video_html += '<div class="jspsych-video-button-response-button" style="display: inline-block; margin:'+trial.margin_vertical+' '+trial.margin_horizontal+'" id="jspsych-video-button-response-button-' + i +'" data-choice="'+i+'">'+str+'</div>';
video_html += '<div class="jspsych-video-button-response-button" style="cursor: pointer; display: inline-block; margin:'+trial.margin_vertical+' '+trial.margin_horizontal+'" id="jspsych-video-button-response-button-' + i +'" data-choice="'+i+'">'+str+'</div>';
}
video_html += '</div>';
@ -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; i<trial.choices.length; i++) {
display_element.querySelector('#jspsych-video-button-response-button-' + i).querySelector('button').disabled = false;
}
enable_buttons();
}
}
@ -241,15 +238,10 @@ jsPsych.plugins["video-button-response"] = (function() {
})
}
// 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;
}
if(trial.response_allowed_while_playing){
enable_buttons();
} else {
disable_buttons();
}
// store response
@ -297,17 +289,40 @@ jsPsych.plugins["video-button-response"] = (function() {
video_element.className += ' responded';
// disable all the buttons after a response
var btns = document.querySelectorAll('.jspsych-video-button-response-button button');
for(var i=0; i<btns.length; i++){
//btns[i].removeEventListener('click');
btns[i].setAttribute('disabled', 'disabled');
}
disable_buttons();
if (trial.response_ends_trial) {
end_trial();
}
}
function button_response(e){
var choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility
after_response(choice);
}
function disable_buttons() {
var btns = document.querySelectorAll('.jspsych-video-button-response-button');
for (var i=0; i<btns.length; i++) {
var btn_el = btns[i].querySelector('button');
if(btn_el){
btn_el.disabled = true;
}
btns[i].removeEventListener('click', button_response);
}
}
function enable_buttons() {
var btns = document.querySelectorAll('.jspsych-video-button-response-button');
for (var i=0; i<btns.length; i++) {
var btn_el = btns[i].querySelector('button');
if(btn_el){
btn_el.disabled = false;
}
btns[i].addEventListener('click', button_response);
}
}
// end trial if time limit is set
if (trial.trial_duration !== null) {
jsPsych.pluginAPI.setTimeout(function() {

View File

@ -0,0 +1,107 @@
const root = '../../';
const utils = require('../testing-utils.js');
beforeEach(function(){
require(root + 'jspsych.js');
require(root + 'plugins/jspsych-html-keyboard-response.js');
});
describe('The css_classes parameter for trials', function(){
test('Adds a single CSS class to the root jsPsych element', function(){
var trial = {
type: 'html-keyboard-response',
stimulus: '<p>foo</p>',
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: '<p>foo</p>',
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: '<p>foo</p>',
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: '<p>foo</p>',
}],
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: '<p>foo</p>',
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: '<p>foo</p>',
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);
})
})

View File

@ -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');
})
})

View File

@ -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;