Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tricky-hairs-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@jspsych-contrib/plugin-image-hotspots": minor
"@jspsych-contrib/plugin-video-hotspots": minor
---

Adds an optional `prompt` parameter to the `image-hotspots` and `video-hotspots` plugins, which is an HTML-formatted string displayed below the stimulus. This can be used to remind participants how to respond. By default, the `video-hotspots` plugin will display the prompt after the video ends, but this can be changed by setting the `show_prompt_on_video_end` parameter to `false`.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ In addition to the [parameters available in all plugins](https://www.jspsych.org
| hotspots | complex | [] | Array of hotspot regions. Each hotspot should have x, y, width, height, and id properties. |
| trial_duration | int | null | How long to show the trial in milliseconds. If null, the trial will wait for a response. |
| hotspot_highlight_css | string | "background-color: rgba(255, 255, 0, 0.3); border: 2px solid yellow;" | CSS string to style the hotspot highlight overlay that appears when clicking/touching a region. |
| 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., click on a specific area). |

### Hotspot Object Properties

Expand Down
23 changes: 23 additions & 0 deletions packages/plugin-image-hotspots/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,27 @@ describe("jsPsychImageHotspots plugin", () => {
const data = getData().values()[0];
expect(data.hotspot_clicked).toBeNull();
});

it("should show prompt if there is one", async () => {
const { expectFinished, getHTML, getData, displayElement } = await startTimeline([
{
type: jsPsychImageHotspots,
stimulus: "test.jpg",
prompt: "<p>This is a prompt</p>",
hotspots: [
{
id: "test_hotspot",
x: 50,
y: 50,
width: 100,
height: 100,
},
],
},
]);

expect(getHTML()).toContain(`
</div>
<p>This is a prompt</p>`);
});
});
11 changes: 10 additions & 1 deletion packages/plugin-image-hotspots/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const info = <const>{
type: ParameterType.STRING,
default: "background-color: rgba(255, 255, 0, 0.3); border: 2px solid yellow;",
},
/** 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., click on a specific area). */
prompt: {
type: ParameterType.HTML_STRING,
default: null,
},
},
data: {
/** The ID of the clicked hotspot region. */
Expand Down Expand Up @@ -68,12 +73,16 @@ class ImageHotspotsPlugin implements JsPsychPlugin<Info> {
const start_time = performance.now();

// Create the HTML structure
const html = `
let html = `
<div id="jspsych-image-hotspots-container" style="position: relative; display: inline-block;">
<img id="jspsych-image-hotspots-stimulus" src="${trial.stimulus}">
<div id="jspsych-image-hotspots-overlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></div>
</div>
`;
// Add prompt if there is one
if (trial.prompt !== null) {
html += trial.prompt;
}

display_element.innerHTML = html;

Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-video-hotspots/docs/plugin-video-hotspots.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ In addition to the [parameters available in all plugins](https://www.jspsych.org
| trial_duration | int | null | How long to show the trial in milliseconds after video ends. If null, trial waits for response. |
| hotspot_highlight_css | string | "background-color: rgba(255, 255, 0, 0.3); border: 2px solid yellow;" | CSS string to style the hotspot highlight overlay that appears when clicking/touching a region. |
| video_preload | boolean | true | Whether to preload the video for smoother playback. |
| 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., click on a specific area). |
| show_prompt_on_video_end | boolean | true | Whether to wait until the video ends to display the prompt string (if there is one). If true (the default), the prompt will be shown when the video has ended. If false, the prompt is shown immediately. |

### Hotspot Object Properties

Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-video-hotspots/examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ <h2>Video Hotspots Plugin Demo</h2>
height: 171
}
],
hotspot_highlight_css: "background-color: rgba(0, 255, 0, 0.5); border: 3px solid green; border-radius: 8px; box-shadow: 0 0 15px rgba(0, 255, 0, 0.7);"
hotspot_highlight_css: "background-color: rgba(0, 255, 0, 0.5); border: 3px solid green; border-radius: 8px; box-shadow: 0 0 15px rgba(0, 255, 0, 0.7);",
prompt: "<div>Click on a square.<div>"
};

// Now we can run the actual video hotspots trial with the demo video
Expand Down
58 changes: 58 additions & 0 deletions packages/plugin-video-hotspots/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,62 @@ describe("jsPsychVideoHotspots plugin", () => {
expect(Number.isInteger(data.click_x)).toBe(true);
expect(Number.isInteger(data.click_y)).toBe(true);
});

it("should show prompt after video ends", async () => {
const { expectFinished, getHTML, getData, displayElement } = await startTimeline([
{
type: jsPsychVideoHotspots,
stimulus: "test.mp4",
prompt: "<p>This is a prompt</p>",
hotspots: [
{
id: "test_hotspot",
x: 50,
y: 50,
width: 100,
height: 100,
},
],
},
]);

expect(getHTML()).toContain(
`<span id="jspsych-video-hotspots-prompt" style="visibility: hidden;"><p>This is a prompt</p>`
);

// Trigger video ended event
const video = displayElement.querySelector(
"#jspsych-video-hotspots-stimulus"
) as HTMLVideoElement;
Object.defineProperty(video, "duration", { writable: true, value: 3.0 });
video.dispatchEvent(new Event("ended"));

expect(getHTML()).toContain(
'<span id="jspsych-video-hotspots-prompt" style="visibility: visible;"><p>This is a prompt</p>'
);
});

it("should show prompt immediately if show_prompt_on_video_end is false", async () => {
const { expectFinished, getHTML, getData, displayElement } = await startTimeline([
{
type: jsPsychVideoHotspots,
stimulus: "test.mp4",
prompt: "<p>This is a prompt</p>",
show_prompt_on_video_end: false,
hotspots: [
{
id: "test_hotspot",
x: 50,
y: 50,
width: 100,
height: 100,
},
],
},
]);

expect(getHTML()).toContain(`
</div>
<p>This is a prompt</p>`);
});
});
29 changes: 28 additions & 1 deletion packages/plugin-video-hotspots/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ const info = <const>{
type: ParameterType.BOOL,
default: true,
},
/** 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., click on a specific area). */
prompt: {
type: ParameterType.HTML_STRING,
default: null,
},
/** Whether to wait until the video ends to display the prompt string (if there is one). If true (the default), the prompt will be shown when the video has ended. If false, the prompt is shown immediately. */
show_prompt_on_video_end: {
type: ParameterType.BOOL,
default: true,
},
},
data: {
/** The ID of the clicked hotspot region. */
Expand Down Expand Up @@ -77,7 +87,7 @@ class VideoHotspotsPlugin implements JsPsychPlugin<Info> {
let video_end_time: number | null = null;

// Create the HTML structure
const html = `
let html = `
<div id="jspsych-video-hotspots-container" style="position: relative; display: inline-block;">
<video id="jspsych-video-hotspots-stimulus"
src="${trial.stimulus}"
Expand All @@ -90,6 +100,15 @@ class VideoHotspotsPlugin implements JsPsychPlugin<Info> {
<div id="jspsych-video-hotspots-overlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></div>
</div>
`;
// Add prompt if there is one
if (trial.prompt !== null) {
if (trial.show_prompt_on_video_end) {
// Add to the DOM and toggle visibility after video ends, rather than adding a new element later (which causes content position jump)
html += `<span id="jspsych-video-hotspots-prompt" style="visibility: hidden;">${trial.prompt}</span>`;
} else {
html += trial.prompt;
}
}

display_element.innerHTML = html;

Expand Down Expand Up @@ -258,6 +277,14 @@ class VideoHotspotsPlugin implements JsPsychPlugin<Info> {
// Create hotspots now that video has ended
createHotspots();

// Add prompt if there is one and it should be shown when the video ends
if (trial.prompt !== null && trial.show_prompt_on_video_end) {
const prompt = display_element.querySelector(
"#jspsych-video-hotspots-prompt"
) as HTMLSpanElement;
prompt.style.visibility = "visible";
}

// Handle trial duration after video ends
if (trial.trial_duration !== null) {
this.jsPsych.pluginAPI.setTimeout(() => {
Expand Down