Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Html Free Recall plugin #137

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
35 changes: 35 additions & 0 deletions packages/plugin-html-written-recall/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# html-written-recall

## Overview

This plugin displays a text box for participants to write a response to a prompt. The response is saved as a string. The trial advances when a certain keyboard input is pressed (default is `Spacebar`). The idea is that a general survey-text box allows for editing as well as observing prior responses whereas this input mimics verbal free recall.

## Loading

### In browser

```js
<script src="https://unpkg.com/@jspsych-contrib/[email protected]">
```

### Via NPM

```
npm install @jspsych-contrib/html-written-recall
```

```js
import jsPsychPluginName from '@jspsych-contrib/html-written-recall';
```

## Compatibility

jsPsych v7.0 (developed using v7.3.0)

## Documentation

See [documentation](docs/jspsych-html-written-recall.md)

## Author / Citation

[Ata Karagoz](https://www.github.com/atakaragoz)
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# jspsych plugin for html-written-recall

This plugin displays a text box for participants to write a response to a prompt. The response is saved as a string. The trial advances when a certain keyboard input is pressed (default is `Spacebar`). The idea is that a general survey-text box allows for editing as well as observing prior responses whereas this input mimics verbal free recall. After a given period of time, a button appears that allows the participant to advance beyond the recall period.

Another use case for this plugin is to allow participants to see a set of images and write single word names in response to them or free-associate words. For instance, on a set of trials participants can see a set of images and write the first word that comes to mind.

This code was developed to be used in a `loop_function()` such that the participant sets the amount of recalls or until a timer allows them to move onto the next trial (See example 1).

## Parameters
In addition to the [parameters available in all plugins](https://www.jspsych.org/overview/plugins#parameters-available-in-all-plugins), this plugin accepts the following parameters. Parameters with a default value of *undefined* must be specified. Parameters can be left unspecified if the default value is acceptable.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `stimulus` | string | `''` | A stimulus for the participant to respond to. |
| `prompt` | string | `''` | The prompt to be displayed above the text box. |
| `stimulus_duration` | numeric | `null` | How long to show the stimulus. If null, then the stimulus will remain on the screen until the trial ends. |
| `trial_duration` | numeric | `null` | How long to show the trial. If null, then the trial will remain on the screen until the participant advances. |
| `next_word_key` | string | `'Spacebar'` | The key to advance to the next word. |
| `button_string` | string | `null` | The string to display on the button that advances the trial. If null, then no button is displayed. |
| `button_delay` | numeric | `0` | How long to wait before displaying the button. If 0, the button is displayed immediately |
| `block_time_start` | numeric | `null` | The time at which the block started. If null then this value is not used for the button appearance calculation|
| `total_block_duration` | numeric | `null` | The total duration of the block. If null then this value is not used for the button appearance calculation|

## Data Generated
In addition to the [default data collected by all plugins](https://www.jspsych.org/overview/plugins#data-collected-by-all-plugins), this plugin collects all parameter data described above and the following data for each trial.
| Name | Type | Value |
| --- | --- | --- |
| `rt` | numeric | The response time in milliseconds for the participant to advance to the next word. |
| `stimulus` | string | The prompt given to the participant. |
| `response` | string | The response given by the participant. |
| `button_pressed` | string | A binary indicator of whether or not the button to move onto next portion of experiment is pressed |

## Example

### Single word association
```javascript
var free_association_trial = {
type: 'html-written-recall',
stimulus: 'Dog',
prompt: 'Write the first word that comes to mind.',
stimulus_duration: 500, // the word disappears after 500 ms
button_string: 'Next task', // Button displayed to finish the block
button_delay: 5000 // button appears after 5 seconds, though this timer resets after each word submitted (so 5 seconds from final submission they can move on)
}

var free_association_block = {
timeline: [free_association_trial],
loop_function() {
// Loop until 10 trials are completed
if (jsPsych.data.get().last(1).values()[0].button_pressed) {
return false;
} else {
return true;
}
}
}
```
See `examples/example1.html` for a demo.

### Free recall with button appearing after 10 seconds
```javascript
var total_block_duration = 10000 // 10 seconds
var free_recall_trial = {
type: 'html-written-recall',
prompt: 'Write down as many words as you can remember from the previous list.',
stimulus_duration: 500, // the word disappears after 500 ms
button_string: 'Next task', // Button displayed to finish the block
block_time_start: begin_block_time,
total_block_duration: total_block_duration // button appears based on global clock
}
var free_recall_block = {
timeline: [free_recall_trial],
on_start: function() {
begin_block_time = performance.now();
}, // set the initial block time so that participants have 30 seconds before the button appears
loop_function() {
// Loop until 30 seconds have passed
if (jsPsych.data.get().last(1).values()[0].button_pressed) {
return false;
} else {
return true;
}
}
}
```
See `examples/example2.html` for a demo.
40 changes: 40 additions & 0 deletions packages/plugin-html-written-recall/examples/example1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]"></script>
<script src="../dist/index.browser.js"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/css/jspsych.css">
</head>
<body></body>
<script>
// Example where a participant gives as many words as possible in time of 10 seconds
// The participant is given a cue word and they have to give as many words as possible
var jsPsych = initJsPsych({
on_finish: function() {
jsPsych.data.displayData();
}
});
var free_association_trial = {
type: 'html-written-recall',
stimulus: 'Dog',
prompt: 'Write the first word that comes to mind.',
stimulus_duration: 500, // the word disappears after 500 ms
button_string: 'Next task', // Button displayed to finish the block
button_delay: 5000 // button appears after 5 seconds, though this timer resets after each word submitted (so 5 seconds from final submission they can move on)
}

var free_association_block = {
timeline: [free_association_trial],
loop_function() {
// Loop until 10 trials are completed
if (jsPsych.data.get().last(1).values()[0].button_pressed) {
return false;
} else {
return true;
}
}
}
jsPsych.run([free_association_block]);

</script>
</html>
43 changes: 43 additions & 0 deletions packages/plugin-html-written-recall/examples/example2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]"></script>
<script src="../dist/index.browser.js"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/css/jspsych.css">
</head>
<body></body>
<script>
// Example where a participant gives as many words as possible in time of 10 seconds
//
var jsPsych = initJsPsych({
on_finish: function() {
jsPsych.data.displayData();
}
});
const total_block_duration = 10000 // 10 seconds
var free_recall_trial = {
type: 'html-written-recall',
prompt: 'Write down as many words as you can remember from the previous list.',
stimulus_duration: 500, // the word disappears after 500 ms (this is repeated after each subsequent press)
button_string: 'Next task', // Button displayed to finish the block
block_time_start: begin_block_time,
total_block_duration: total_block_duration // button appears based on global clock
}
var free_recall_block = {
timeline: [free_recall_trial],
on_start: function() {
begin_block_time = performance.now();
}, // set the initial block time so that participants have 30 seconds before the button appears
loop_function() {
// Loop until button pressed (not shown until 30 seconds)
if (jsPsych.data.get().last(1).values()[0].button_pressed) {
return false;
} else {
return true;
}
}
}
jsPsych.run([free_recall_block]);

</script>
</html>
183 changes: 183 additions & 0 deletions packages/plugin-html-written-recall/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
var htmlWrittenRecall = (function (jspsych) {
"use strict";

const info = {
name: "html-written-recall",
parameters: {
stimulus: {
type: jspsych.ParameterType.HTML_STRING,
pretty_name: "Stimulus",
default: undefined,
},
stimulus_duration: {
type: jspsych.ParameterType.INT,
pretty_name: "Stimulus duration",
default: null, // might add a stimulus or prompt repeat where it only shows for first word in a block and then disappears
},
stimulus_only_first: {
type: jspsych.ParameterType.BOOL,
pretty_name: "Stimulus only first",
default: false,
},
prompt: {
type: jspsych.ParameterType.HTML_STRING,
pretty_name: "Prompt",
default: null,
},
prompt_only_first: {
type: jspsych.ParameterType.BOOL,
pretty_name: "Prompt only first",
default: false,
},
trial_duration: {
type: jspsych.ParameterType.INT,
pretty_name: "Trial duration",
default: null,
},
next_word_key: {
type: jspsych.ParameterType.STRING,
pretty_name: "key for next word",
default: " ",
},
button_string: {
type: jspsych.ParameterType.HTML_STRING,
pretty_name: "Button HTML",
default: null,
},
button_delay: {
type: jspsych.ParameterType.INT,
pretty_name: "Button delay",
default: 0,
},
block_time_start: {
type: jspsych.ParameterType.INT,
pretty_name: "The time the recall block began",
default: null,
},
total_block_duration: {
type: jspsych.ParameterType.INT,
pretty_name: "The total duration of the recall block",
default: null,
},
},
};

class HtmlFreeRecallPlugin {
constructor(jsPsych) {
this.jsPsych = jsPsych;
}

trial(display_element, trial) {

// if the stimulus should only be displayed for the first word, check the block_time_start and if the block has been going for more than 100ms, don't show the stimulus
if (trial.stimulus_only_first && trial.block_time_start !== null) {
var currentTime = performance.now();
var elapsedTime = currentTime - trial.block_time_start;
if (elapsedTime > 100) {
trial.stimulus = "";
}
}

var new_html =
'<div id="jspsych-html-keyboard-response-stimulus">' + trial.stimulus + "</div>";

// Add a textbox for input
new_html +=
'<div><input type="text" id="jspsych-html-keyboard-response-textbox" autocomplete="off" /></div>'; // need to make sure autocomplete is off

if (trial.prompt !== null && !trial.prompt_only_first) {
new_html += trial.prompt;
} else if (trial.prompt !== null && trial.prompt_only_first && trial.block_time_start !== null) {
var currentTime = performance.now();
var elapsedTime = currentTime - trial.block_time_start;
if (elapsedTime < 100) {
new_html += trial.prompt;
}
}

if (trial.button_string !== null) {
new_html +=
'<div><button id="jspsych-html-keyboard-response-button" style="display: none; justify-content:center">' +
trial.button_string +
"</button></div>";
}

display_element.innerHTML = new_html;

// Focus the textbox to enable typing immediately
var textbox = display_element.querySelector("#jspsych-html-keyboard-response-textbox");
textbox.focus();
var response = {
rt: null,
button: null,
};
// Add an event listener to the button, if it exists, and show it after a delay
if (trial.button_string !== null) {
var button = display_element.querySelector("#jspsych-html-keyboard-response-button");
button.addEventListener("click", () => {
response.button = "pressed"; // Set button to "pressed" when clicked
end_trial();
});
var buttonDisplayTime;
if (trial.block_time_start !== null) {
// Dynamic button delay calculation
var currentTime = performance.now();
var elapsedTime = currentTime - trial.block_time_start;
buttonDisplayTime = Math.max(trial.total_block_duration - elapsedTime, 0);
} else {
// Static button delay
buttonDisplayTime = trial.button_delay;
}
this.jsPsych.pluginAPI.setTimeout(() => {
button.style.display = "initial"; // Show the button after the delay
}, buttonDisplayTime);
}
const end_trial = () => {
this.jsPsych.pluginAPI.clearAllTimeouts();

textbox.removeEventListener("keydown", checkForNextWordKey); // Remove the event listener

var trial_data = {
rt: response.rt,
stimulus: trial.stimulus,
response: textbox.value, // Get the value of the textbox
button: response.button,
};

display_element.innerHTML = "";

this.jsPsych.finishTrial(trial_data);
};
// if the next_word_key is "Spacebar" convert it to " " for comparison
if (trial.next_word_key.toLowerCase() === "spacebar") {
trial.next_word_key = " ";
}
// Function to check for space key press in the textbox
const checkForNextWordKey = (event) => {
if (textbox.value.trim() !== "" && event.key === trial.next_word_key) {
response.rt = performance.now() - start_time;
end_trial();
}
};

// Add event listener to the textbox for "keydown" event
textbox.addEventListener("keydown", checkForNextWordKey);

if (trial.stimulus_duration !== null) {
this.jsPsych.pluginAPI.setTimeout(() => {
display_element.querySelector(
"#jspsych-html-keyboard-response-stimulus",
).style.visibility = "hidden";
}, trial.stimulus_duration);
}

if (trial.trial_duration !== null) {
this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);
}
var start_time = performance.now();
}
}
HtmlFreeRecallPlugin.info = info;

return HtmlFreeRecallPlugin;
})(jsPsychModule);
Loading