mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-12 16:48:12 +00:00
Merge pull request #1376 from jspsych/feature-timeline-variable-simplify
Merge feature-timeline-variable-simplify - fixes #883
This commit is contained in:
commit
d1a876d280
@ -581,11 +581,11 @@ jsPsych.timelineVariable(variable, call_immediate)
|
|||||||
Parameter | Type | Description
|
Parameter | Type | Description
|
||||||
----------|------|------------
|
----------|------|------------
|
||||||
variable | string | Name of the timeline variable
|
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
|
### 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
|
### Description
|
||||||
|
|
||||||
@ -613,6 +613,25 @@ var procedure = {
|
|||||||
|
|
||||||
#### Invoking immediately in a function
|
#### Invoking immediately in a function
|
||||||
```javascript
|
```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 = {
|
var trial = {
|
||||||
type: 'html-keyboard-response',
|
type: 'html-keyboard-response',
|
||||||
stimulus: function(){
|
stimulus: function(){
|
||||||
@ -631,7 +650,6 @@ var procedure = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
## jsPsych.totalTime
|
## jsPsych.totalTime
|
||||||
|
|
||||||
|
@ -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.)
|
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
|
```javascript
|
||||||
@ -151,8 +152,8 @@ var face_name_procedure = {
|
|||||||
{
|
{
|
||||||
type: 'html-keyboard-response',
|
type: 'html-keyboard-response',
|
||||||
stimulus: function(){
|
stimulus: function(){
|
||||||
var html="<img src='"+jsPsych.timelineVariable('face', true)+"'>";
|
var html="<img src='"+jsPsych.timelineVariable('face')+"'>";
|
||||||
html += "<p>"+jsPsych.timelineVariable('name', true)+"</p>";
|
html += "<p>"+jsPsych.timelineVariable('name')+"</p>";
|
||||||
return html;
|
return html;
|
||||||
},
|
},
|
||||||
choices: jsPsych.NO_KEYS,
|
choices: jsPsych.NO_KEYS,
|
||||||
|
@ -67,12 +67,12 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// to use the canvas stimulus function with timeline variables,
|
// 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 = {
|
var circle_procedure = {
|
||||||
timeline: [{
|
timeline: [{
|
||||||
type: 'canvas-button-response',
|
type: 'canvas-button-response',
|
||||||
stimulus: function(c) {
|
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'],
|
choices: ['Red', 'Green', 'Blue'],
|
||||||
prompt: '<p>What color is the circle?</p>',
|
prompt: '<p>What color is the circle?</p>',
|
||||||
|
@ -41,19 +41,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// to use the canvas stimulus function with timeline variables,
|
// 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 = {
|
var trial_procedure = {
|
||||||
timeline: [{
|
timeline: [{
|
||||||
type: 'canvas-keyboard-response',
|
type: 'canvas-keyboard-response',
|
||||||
stimulus: function(c) {
|
stimulus: function(c) {
|
||||||
var ctx = c.getContext('2d');
|
var ctx = c.getContext('2d');
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.fillStyle = jsPsych.timelineVariable('color', true);
|
ctx.fillStyle = jsPsych.timelineVariable('color');
|
||||||
ctx.fillRect(
|
ctx.fillRect(
|
||||||
jsPsych.timelineVariable('upper_left_x', true),
|
jsPsych.timelineVariable('upper_left_x'),
|
||||||
jsPsych.timelineVariable('upper_left_y', true),
|
jsPsych.timelineVariable('upper_left_y'),
|
||||||
jsPsych.timelineVariable('width', true),
|
jsPsych.timelineVariable('width'),
|
||||||
jsPsych.timelineVariable('height', true)
|
jsPsych.timelineVariable('height')
|
||||||
);
|
);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
},
|
},
|
||||||
|
@ -79,7 +79,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'html-keyboard-response',
|
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'],
|
choices: ['y','n'],
|
||||||
post_trial_gap: 0,
|
post_trial_gap: 0,
|
||||||
data: {
|
data: {
|
||||||
|
39
jspsych.js
39
jspsych.js
@ -273,6 +273,9 @@ window.jsPsych = (function() {
|
|||||||
// of the DataCollection, for easy access and editing.
|
// of the DataCollection, for easy access and editing.
|
||||||
var trial_data_values = trial_data.values()[0];
|
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
|
// handle callback at plugin level
|
||||||
if (typeof current_trial.on_finish === 'function') {
|
if (typeof current_trial.on_finish === 'function') {
|
||||||
current_trial.on_finish(trial_data_values);
|
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.
|
// data object that just went through the trial's finish handlers.
|
||||||
opts.on_data_update(trial_data_values);
|
opts.on_data_update(trial_data_values);
|
||||||
|
|
||||||
|
// done with callbacks
|
||||||
|
jsPsych.internal.call_immediate = false;
|
||||||
|
|
||||||
// wait for iti
|
// wait for iti
|
||||||
if (typeof current_trial.post_trial_gap === null || typeof current_trial.post_trial_gap === 'undefined') {
|
if (typeof current_trial.post_trial_gap === null || typeof current_trial.post_trial_gap === 'undefined') {
|
||||||
if (opts.default_iti > 0) {
|
if (opts.default_iti > 0) {
|
||||||
@ -326,8 +332,9 @@ window.jsPsych = (function() {
|
|||||||
return timeline.activeID();
|
return timeline.activeID();
|
||||||
};
|
};
|
||||||
|
|
||||||
core.timelineVariable = function(varname, execute){
|
core.timelineVariable = function(varname, immediate){
|
||||||
if(execute){
|
if(typeof immediate == 'undefined'){ immediate = false; }
|
||||||
|
if(jsPsych.internal.call_immediate || immediate === true){
|
||||||
return timeline.timelineVariable(varname);
|
return timeline.timelineVariable(varname);
|
||||||
} else {
|
} else {
|
||||||
return function() { return timeline.timelineVariable(varname); }
|
return function() { return timeline.timelineVariable(varname); }
|
||||||
@ -489,7 +496,9 @@ window.jsPsych = (function() {
|
|||||||
// check for conditonal function on nodes with timelines
|
// check for conditonal function on nodes with timelines
|
||||||
if (typeof timeline_parameters != 'undefined') {
|
if (typeof timeline_parameters != 'undefined') {
|
||||||
if (typeof timeline_parameters.conditional_function !== 'undefined') {
|
if (typeof timeline_parameters.conditional_function !== 'undefined') {
|
||||||
|
jsPsych.internal.call_immediate = true;
|
||||||
var conditional_result = timeline_parameters.conditional_function();
|
var conditional_result = timeline_parameters.conditional_function();
|
||||||
|
jsPsych.internal.call_immediate = false;
|
||||||
// if the conditional_function() returns false, then the timeline
|
// if the conditional_function() returns false, then the timeline
|
||||||
// doesn't run and is marked as complete.
|
// doesn't run and is marked as complete.
|
||||||
if (conditional_result == false) {
|
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.
|
// if we're all done with the repetitions, check if there is a loop function.
|
||||||
else if (typeof timeline_parameters.loop_function !== 'undefined') {
|
else if (typeof timeline_parameters.loop_function !== 'undefined') {
|
||||||
|
jsPsych.internal.call_immediate = true;
|
||||||
if (timeline_parameters.loop_function(this.generatedData())) {
|
if (timeline_parameters.loop_function(this.generatedData())) {
|
||||||
this.reset();
|
this.reset();
|
||||||
|
jsPsych.internal.call_immediate = false;
|
||||||
return parent_node.advance();
|
return parent_node.advance();
|
||||||
} else {
|
} else {
|
||||||
progress.done = true;
|
progress.done = true;
|
||||||
|
jsPsych.internal.call_immediate = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -871,6 +883,9 @@ window.jsPsych = (function() {
|
|||||||
// get default values for parameters
|
// get default values for parameters
|
||||||
setDefaultValues(trial);
|
setDefaultValues(trial);
|
||||||
|
|
||||||
|
// about to execute callbacks
|
||||||
|
jsPsych.internal.call_immediate = true;
|
||||||
|
|
||||||
// call experiment wide callback
|
// call experiment wide callback
|
||||||
opts.on_trial_start(trial);
|
opts.on_trial_start(trial);
|
||||||
|
|
||||||
@ -892,6 +907,9 @@ window.jsPsych = (function() {
|
|||||||
if(typeof trial.on_load == 'function'){
|
if(typeof trial.on_load == 'function'){
|
||||||
trial.on_load();
|
trial.on_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// done with callbacks
|
||||||
|
jsPsych.internal.call_immediate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluateTimelineVariables(trial){
|
function evaluateTimelineVariables(trial){
|
||||||
@ -911,6 +929,9 @@ window.jsPsych = (function() {
|
|||||||
|
|
||||||
function evaluateFunctionParameters(trial){
|
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
|
// first, eval the trial type if it is a function
|
||||||
// this lets users set the plugin type with a function
|
// this lets users set the plugin type with a function
|
||||||
if(typeof trial.type === '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){
|
function setDefaultValues(trial){
|
||||||
@ -1066,6 +1090,17 @@ window.jsPsych = (function() {
|
|||||||
return core;
|
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() {
|
jsPsych.plugins = (function() {
|
||||||
|
|
||||||
var module = {};
|
var module = {};
|
||||||
|
@ -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');
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -125,7 +125,7 @@ describe('loop function', function(){
|
|||||||
stimulus: 'foo'
|
stimulus: 'foo'
|
||||||
}],
|
}],
|
||||||
loop_function: function(){
|
loop_function: function(){
|
||||||
if(jsPsych.timelineVariable('word', true) == 'b' && counter < 2){
|
if(jsPsych.timelineVariable('word') == 'b' && counter < 2){
|
||||||
counter++;
|
counter++;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@ -283,7 +283,7 @@ describe('conditional function', function(){
|
|||||||
var innertimeline = {
|
var innertimeline = {
|
||||||
timeline: [trial],
|
timeline: [trial],
|
||||||
conditional_function: function(){
|
conditional_function: function(){
|
||||||
if(jsPsych.timelineVariable('word', true) == 'b'){
|
if(jsPsych.timelineVariable('word') == 'b'){
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
|
Loading…
Reference in New Issue
Block a user