Merge pull request #1376 from jspsych/feature-timeline-variable-simplify

Merge feature-timeline-variable-simplify - fixes #883
This commit is contained in:
Becky Gilbert 2021-01-18 09:52:35 -08:00 committed by GitHub
commit d1a876d280
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 244 additions and 19 deletions

View File

@ -581,11 +581,11 @@ jsPsych.timelineVariable(variable, call_immediate)
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.
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 +613,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(){
@ -631,7 +650,6 @@ var procedure = {
}
```
---
## jsPsych.totalTime

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

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

@ -79,7 +79,7 @@
},
{
type: 'html-keyboard-response',
stimulus: function(){ return "<p class='stimulus'>"+jsPsych.timelineVariable('word', true)+"</p>"; },
stimulus: function(){ return "<p class='stimulus'>"+jsPsych.timelineVariable('word')+"</p>"; },
choices: ['y','n'],
post_trial_gap: 0,
data: {

View File

@ -273,6 +273,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);
@ -286,6 +289,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) {
@ -326,8 +332,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); }
@ -489,7 +496,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) {
@ -550,11 +559,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;
}
}
@ -871,6 +883,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);
@ -892,6 +907,9 @@ window.jsPsych = (function() {
if(typeof trial.on_load == 'function'){
trial.on_load();
}
// done with callbacks
jsPsych.internal.call_immediate = false;
}
function evaluateTimelineVariables(trial){
@ -911,6 +929,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'){
@ -949,6 +970,9 @@ window.jsPsych = (function() {
}
}
}
// reset so jsPsych.timelineVariable() is no longer immediately executed
jsPsych.internal.call_immediate = false;
}
function setDefaultValues(trial){
@ -1066,6 +1090,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 = {};

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;