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

Add slider keyboard functionality #2827

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/plugins/html-slider-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Current version: 1.1.2. [See version history](https://github.com/jspsych/jsPsych/blob/main/packages/plugin-html-slider-response/CHANGELOG.md).

This plugin displays HTML content and allows the participant to respond by dragging a slider.
This plugin displays HTML content and allows the participant to respond by dragging or using a keyboard to pan through a slider.

## Parameters

Expand All @@ -19,6 +19,9 @@ slider_start | integer | 50 | Sets the starting value of the slider
step | integer | 1 | Sets the step of the slider. This is the smallest amount by which the slider can change.
slider_width | integer | null | Set the width of the slider in pixels. If left null, then the width will be equal to the widest element in the display.
require_movement | boolean | false | If true, the participant must move the slider before clicking the continue button.
enable_keys | boolean | false | If true, the participant can use the keyboard to adjust or pan the slider.
keys_adjust | array of strings | ['ArrowLeft', 'ArrowDown'] | The keys that will adjust the slider by the step value.
keys_panning | array of strings | ['1', '2', '3', '4', '5'] | The keys that will pan through the slider, each value that can be panned to is spaced equidistantly.
prompt | string | null | This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press).
stimulus_duration | numeric | null | How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends.
trial_duration | numeric | null | How long to wait for the participant to make a response before ending the trial in milliseconds. If the participant fails to make a response before this timer is reached, the participant's response will be recorded as null for the trial and the trial will end. If the value of this parameter is null, then the trial will wait for a response indefinitely.
Expand Down
15 changes: 13 additions & 2 deletions examples/jspsych-html-slider-response.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@
};

var trial_2 = {
type: jsPsychHtmlSliderResponse,
stimulus: '<div style="margin: 50px auto; width: 100px; height: 100px; background-color: rgb(46, 26, 122);"></div>',
labels: ['Purple', 'Blue'],
slider_width: 500,
require_movement: true,
enable_keys: true,
keys_adjust: ['f', 'j'],
prompt: '<p>Is this color closer to purple or blue? Use the slider above, and use the f or j key to adjust further.</p>'
};

var trial_3 = {
type: jsPsychHtmlSliderResponse,
stimulus: '<div style="margin: 50px auto; width: 100px; height: 100px; background-color: rgb(29, 23, 138)"></div>',
labels: ['Purple', 'Blue'],
Expand All @@ -39,7 +50,7 @@
trial_duration: 5000
};

var trial_3 = {
var trial_4 = {
type: jsPsychHtmlSliderResponse,
stimulus: '<div style="margin: 50px auto; width: 100px; height: 100px; background-color: rgb(63, 17, 129)"></div>',
labels: ['Purple', 'Blue'],
Expand All @@ -48,7 +59,7 @@
stimulus_duration: 1000
};

jsPsych.run([trial_1, trial_2, trial_3]);
jsPsych.run([trial_1, trial_2, trial_3, trial_4]);

</script>
</html>
2 changes: 1 addition & 1 deletion packages/plugin-html-slider-response/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jsPsych is a JavaScript framework for creating behavioral experiments that run i

## Plugin Description

The html-slider-response plugin displays HTML content and records responses generated by dragging a slider. The stimulus can be displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the participant does not respond within a fixed length of time.
The html-slider-response plugin displays HTML content and records responses generated by dragging or using a keyboard to pan through a slider. The stimulus can be displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the participant does not respond within a fixed length of time.

## Examples

Expand Down
45 changes: 43 additions & 2 deletions packages/plugin-html-slider-response/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clickTarget, simulateTimeline, startTimeline } from "@jspsych/test-utils";
import { pressKey, clickTarget, simulateTimeline, startTimeline } from "@jspsych/test-utils";

import htmlSliderResponse from ".";

Expand Down Expand Up @@ -49,7 +49,7 @@ describe("html-slider-response", () => {
);
});

test("should set min, max and step", async () => {
test("should set min, max, step, and initial value", async () => {
const { displayElement } = await startTimeline([
{
type: htmlSliderResponse,
Expand All @@ -58,6 +58,7 @@ describe("html-slider-response", () => {
min: 2,
max: 10,
step: 2,
slider_start: 6,
button_label: "button",
},
]);
Expand All @@ -68,6 +69,7 @@ describe("html-slider-response", () => {
expect(responseElement.min).toBe("2");
expect(responseElement.max).toBe("10");
expect(responseElement.step).toBe("2");
expect(responseElement.value).toBe("6");
});

test("should append to bottom on stimulus", async () => {
Expand Down Expand Up @@ -141,6 +143,45 @@ describe("html-slider-response", () => {

await expectFinished();
});

test("should modify value on adjusting keys", async () => {
const { displayElement } = await startTimeline([
{
type: htmlSliderResponse,
stimulus: "this is html",
labels: ["left", "right"],
button_label: "button",
enable_keys: true,
}
]);

const responseElement = displayElement.querySelector<HTMLInputElement>(
"#jspsych-html-slider-response-response"
);

pressKey("ArrowLeft");
expect(responseElement.value).toBe("49");
});

test("should modify value on panning keys", async () => {
const { displayElement } = await startTimeline([
{
type: htmlSliderResponse,
stimulus: "this is html",
labels: ["left", "right"],
button_label: "button",
enable_keys: true,
keys_panning: ['q', 'w', 'e', 'r', 't', 'y'],
}
]);

const responseElement = displayElement.querySelector<HTMLInputElement>(
"#jspsych-html-slider-response-response"
);

pressKey("w");
expect(responseElement.value).toBe("20");
});
});

describe("html-slider-response simulation", () => {
Expand Down
127 changes: 91 additions & 36 deletions packages/plugin-html-slider-response/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ const info = <const>{
pretty_name: "Require movement",
default: false,
},
/** If true, allows the participant to use the keyboard to adjust or pan the slider. */
enable_keys: {
type: ParameterType.BOOL,
pretty_name: "Enable keys",
default: false,
},
/** The keys that the participant can use to adjust the slider by the step value. */
keys_adjust: {
type: ParameterType.KEYS,
pretty_name: "Keys adjust",
default: ['ArrowLeft', 'ArrowRight'],
},
/** The keys that the participant can use to pan the slider. Each key's corresponding value is evenly spaced from one another, with the first value and last value being at `min` and `max` respectively. */
keys_panning: {
type: ParameterType.KEYS,
pretty_name: "Keys panning",
default: ['1', '2', '3', '4', '5'],
},
/** Any content here will be displayed below the slider. */
prompt: {
type: ParameterType.HTML_STRING,
Expand Down Expand Up @@ -99,12 +117,17 @@ type Info = typeof info;
class HtmlSliderResponsePlugin implements JsPsychPlugin<Info> {
static info = info;

constructor(private jsPsych: JsPsych) {}
constructor(private jsPsych: JsPsych) { }

trial(display_element: HTMLElement, trial: TrialType<Info>) {
// half of the thumb width value from jspsych.css, used to adjust the label positions
/** Half of the thumb width value from jspsych.css, used to adjust the label positions */
var half_thumb_width = 7.5;

var response = {
rt: null,
response: null,
};

var html = '<div id="jspsych-html-slider-response-wrapper" style="margin: 100px 0px;">';
html += '<div id="jspsych-html-slider-response-stimulus">' + trial.stimulus + "</div>";
html +=
Expand Down Expand Up @@ -163,29 +186,57 @@ class HtmlSliderResponsePlugin implements JsPsychPlugin<Info> {

display_element.innerHTML = html;

var response = {
rt: null,
response: null,
const changeValue = (info) => {
let input = display_element.querySelector<HTMLInputElement>("#jspsych-html-slider-response-response");
if (this.jsPsych.pluginAPI.compareKeys(info.key, trial.keys_adjust[0])) {
input.stepDown();
}
if (this.jsPsych.pluginAPI.compareKeys(info.key, trial.keys_adjust[1])) {
input.stepUp();
}
if (trial.keys_panning.includes(info.key)) {
const panConst = (trial.max - trial.min) / (trial.keys_panning.length - 1);
input.value = String(trial.min + panConst * trial.keys_panning.indexOf(info.key));
return;
}
}

if (trial.enable_keys) {
var listener = this.jsPsych.pluginAPI.getKeyboardResponse({
callback_function: changeValue,
valid_responses: "ALL_KEYS",
rt_method: "performance",
persist: true,
});
}

const enable_button = () => {
display_element.querySelector<HTMLInputElement>(
"#jspsych-html-slider-response-next"
).disabled = false;
};

if (trial.require_movement) {
const enable_button = () => {
display_element.querySelector<HTMLInputElement>(
"#jspsych-html-slider-response-next"
).disabled = false;
};

display_element
.querySelector("#jspsych-html-slider-response-response")
.addEventListener("mousedown", enable_button);
['mousedown', 'touchstart', 'change'].forEach((type) => {
display_element.addEventListener(type, enable_button);
});

display_element
.querySelector("#jspsych-html-slider-response-response")
.addEventListener("touchstart", enable_button);
// prevent unnecessary events from being formed
if (trial.enable_keys) {
const key_enable_button = (info) => {
if (trial.keys_adjust.includes(info.key) || trial.keys_panning.includes(info.key)) {
enable_button();
this.jsPsych.pluginAPI.cancelKeyboardResponse(listener);
}
}

display_element
.querySelector("#jspsych-html-slider-response-response")
.addEventListener("change", enable_button);
var listener = this.jsPsych.pluginAPI.getKeyboardResponse({
callback_function: key_enable_button,
valid_responses: "ALL_KEYS",
rt_method: "performance",
persist: true,
});
}
}

const end_trial = () => {
Expand All @@ -199,30 +250,34 @@ class HtmlSliderResponsePlugin implements JsPsychPlugin<Info> {
response: response.response,
};

this.jsPsych.pluginAPI.cancelAllKeyboardResponses();

display_element.innerHTML = "";

// next trial
this.jsPsych.finishTrial(trialdata);
};

const gatherData = () => {
// measure response time
var endTime = performance.now();
response.rt = Math.round(endTime - startTime);
response.response = display_element.querySelector<HTMLInputElement>(
"#jspsych-html-slider-response-response"
).valueAsNumber;

if (trial.response_ends_trial) {
end_trial();
} else {
display_element.querySelector<HTMLButtonElement>(
"#jspsych-html-slider-response-next"
).disabled = true;
}
}

display_element
.querySelector("#jspsych-html-slider-response-next")
.addEventListener("click", () => {
// measure response time
var endTime = performance.now();
response.rt = Math.round(endTime - startTime);
response.response = display_element.querySelector<HTMLInputElement>(
"#jspsych-html-slider-response-response"
).valueAsNumber;

if (trial.response_ends_trial) {
end_trial();
} else {
display_element.querySelector<HTMLButtonElement>(
"#jspsych-html-slider-response-next"
).disabled = true;
}
});
.addEventListener("click", gatherData);

if (trial.stimulus_duration !== null) {
this.jsPsych.pluginAPI.setTimeout(() => {
Expand Down