Skip to content

Commit

Permalink
Merge pull request #136 from yeatmanlab/add/swipe-response-stimulus-c…
Browse files Browse the repository at this point in the history
…ontainer-dragging

Making stimulus-swipe-container drag with stimulus div
  • Loading branch information
jodeleeuw authored Jan 23, 2025
2 parents 440bb75 + b100705 commit e09e80b
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-jobs-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@jspsych-contrib/plugin-html-swipe-response": patch
---

the patch ensures that both the container (`#jspsych-html-swipe-response-stimulus-container`) and the stimulus (`#jspsych-html-swipe-response-stimulus`) move together when dragged, providing a unified and seamless interaction.
2 changes: 2 additions & 0 deletions packages/plugin-html-swipe-response/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This plugin displays HTML content and records responses generated by swipe gestu

Setting the `stimulus_duration` parameter while using the swipe modality can result in a user experience issue, wherein the user must swipe a stimulus div tag that has been hidden after the stimulus duration has elapsed. To solve this, this plugin wraps the stimulus div tag in another tag with the ID `#jspsych-html-swipe-response-stimulus-container`. This div tag can then be styled so that they user has some visual representation of the stimulus even after the `#jspsych-html-swipe-response-stimulus-container` div has been hidden.

The plugin now ensures that both the container (`#jspsych-html-swipe-response-stimulus-container`) and the stimulus (`#jspsych-html-swipe-response-stimulus`) move together when dragged, providing a unified and seamless interaction.

## Loading

```js
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This plugin displays HTML content and records responses generated by swipe gestu

Setting the `stimulus_duration` parameter while using the swipe modality can result in a user experience issue, wherein the user must swipe a stimulus div tag that has been hidden after the stimulus duration has elapsed. To solve this, this plugin wraps the stimulus div tag in another tag with the ID `#jspsych-html-swipe-response-stimulus-container`. This div tag can then be styled so that they user has some visual representation of the stimulus even after the `#jspsych-html-swipe-response-stimulus-container` div has been hidden.

The plugin now ensures that both the container (`#jspsych-html-swipe-response-stimulus-container`) and the stimulus (`#jspsych-html-swipe-response-stimulus`) move together when dragged, providing a unified and seamless interaction.

## 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.
Expand Down
30 changes: 30 additions & 0 deletions packages/plugin-html-swipe-response/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { clickTarget, pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils";
import interact from "interactjs";

import htmlSwipeResponse from ".";

Expand Down Expand Up @@ -289,6 +290,34 @@ describe("plugin-html-swipe-response", () => {
expect(element.getAttribute("disabled")).toBe("disabled");
});
});

test("should move container and stimulus together during drag", async () => {
const { displayElement } = await startTimeline([
{
type: htmlSwipeResponse,
stimulus: "this is html",
swipe_animation_duration: 0,
},
]);

const container = displayElement.querySelector<HTMLElement>(
"#jspsych-html-swipe-response-stimulus-container"
);
const stimulus = displayElement.querySelector<HTMLElement>(
"#jspsych-html-swipe-response-stimulus"
);

// Simulate drag event manually using interact.js drag events
interact(container).fire({
type: "dragmove",
target: container,
delta: { x: 100, y: 50 },
});

// Now test if the transforms are applied correctly
expect(container.style.transform).toBe("translate3D(100px, 50px, 0)");
expect(stimulus.style.transform).toBe("translate3D(100px, 50px, 0) rotate(20deg)");
});
});

describe("html-swipe-response simulation", () => {
Expand Down Expand Up @@ -344,6 +373,7 @@ describe("html-swipe-response simulation", () => {
const buttonResponse = getData().values()[0].button_response;
const keyboardResponse = getData().values()[0].keyboard_response;
const responseSource = getData().values()[0].response_source;

expect(getData().values()[0].rt).toBeGreaterThan(0);
if (responseSource == "keyboard") {
expect(typeof keyboardResponse).toBe("string");
Expand Down
91 changes: 55 additions & 36 deletions packages/plugin-html-swipe-response/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ class HtmlSwipeResponsePlugin implements JsPsychPlugin<Info> {
source: null,
};

// References to container and stimulus
const container_div = document.getElementById("jspsych-html-swipe-response-stimulus-container");
const stimulus_div = document.getElementById("jspsych-html-swipe-response-stimulus");

let position = {
Expand All @@ -178,26 +180,35 @@ class HtmlSwipeResponsePlugin implements JsPsychPlugin<Info> {
const setPosition = (coordinates) => {
const { x = 0, y = 0, rotation = 0 } = coordinates;
position = { x, y, rotation };
container_div.style.transform = `translate3D(${x}px, ${y}px, 0)`;
stimulus_div.style.transform = `translate3D(${x}px, ${y}px, 0) rotate(${rotation}deg)`;
};

// Reset the position of the stimulus and container
const resetPosition = async () => {
stimulus_div.style.transition = `${trial.swipe_animation_duration / 1000}s ease-in-out, ${
trial.swipe_animation_duration / 1000
}s ease-in`;
for (const div of [container_div, stimulus_div]) {
div.style.transition = `${trial.swipe_animation_duration / 1000}s ease-in-out, ${
trial.swipe_animation_duration / 1000
}s ease-in`;
}
setPosition({ x: 0, y: 0, rotation: 0 });
stimulus_div.style.transition = null;
for (const div of [container_div, stimulus_div]) {
div.style.transition = null;
}
};

// Handle drag movement of the stimulus and container together
const dragMoveListener = (event) => {
const x = position.x + event.delta.x;
const y = position.y + event.delta.y;
let rotation = 0;
if (position.x > 0) {
rotation = Math.min(trial.swipe_animation_max_rotation, position.x / 4);

if (x > 0) {
rotation = Math.min(trial.swipe_animation_max_rotation, x / 4);
} else {
rotation = Math.max(-trial.swipe_animation_max_rotation, position.x / 4);
rotation = Math.max(-trial.swipe_animation_max_rotation, x / 4);
}

setPosition({ x: x, y: y, rotation });
};

Expand All @@ -216,9 +227,11 @@ class HtmlSwipeResponsePlugin implements JsPsychPlugin<Info> {
}

const sendCardToLeft = async () => {
stimulus_div.style.transition = `${trial.swipe_animation_duration / 1000}s ease-in-out, ${
trial.swipe_animation_duration / 1000
}s ease-in`;
for (const div of [container_div, stimulus_div]) {
div.style.transition = `${trial.swipe_animation_duration / 1000}s ease-in-out, ${
trial.swipe_animation_duration / 1000
}s ease-in`;
}
setPosition({
x: -trial.swipe_offscreen_coordinate,
y: position.y,
Expand All @@ -227,9 +240,11 @@ class HtmlSwipeResponsePlugin implements JsPsychPlugin<Info> {
};

const sendCardToRight = async () => {
stimulus_div.style.transition = `${trial.swipe_animation_duration / 1000}s ease-in-out, ${
trial.swipe_animation_duration / 1000
}s ease-in`;
for (const div of [container_div, stimulus_div]) {
div.style.transition = `${trial.swipe_animation_duration / 1000}s ease-in-out, ${
trial.swipe_animation_duration / 1000
}s ease-in`;
}
setPosition({
x: trial.swipe_offscreen_coordinate,
y: position.y,
Expand Down Expand Up @@ -288,29 +303,31 @@ class HtmlSwipeResponsePlugin implements JsPsychPlugin<Info> {
}
};

interact(stimulus_div).draggable({
inertia: false,
autoScroll: true,
modifiers: [
interact.modifiers.restrictRect({
endOnly: true,
}),
],
listeners: {
move: dragMoveListener,
end: () => {
if (position.x < -trial.swipe_threshold) {
sendCardToLeft();
after_swipe_response("left");
} else if (position.x > trial.swipe_threshold) {
sendCardToRight();
after_swipe_response("right");
} else {
resetPosition();
}
for (const div of [stimulus_div, container_div]) {
interact(div).draggable({
inertia: false,
autoScroll: true,
modifiers: [
interact.modifiers.restrictRect({
endOnly: true,
}),
],
listeners: {
move: dragMoveListener,
end: () => {
if (position.x < -trial.swipe_threshold) {
sendCardToLeft();
after_swipe_response("left");
} else if (position.x > trial.swipe_threshold) {
sendCardToRight();
after_swipe_response("right");
} else {
resetPosition();
}
},
},
},
});
});
}

// function to handle responses by the subject
const after_keyboard_response = (info) => {
Expand Down Expand Up @@ -392,7 +409,9 @@ class HtmlSwipeResponsePlugin implements JsPsychPlugin<Info> {
this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener);
}

interact(stimulus_div).unset();
for (const div of [stimulus_div, container_div]) {
interact(div).unset();
}

// gather the data to store for the trial
const trial_data = {
Expand Down

0 comments on commit e09e80b

Please sign in to comment.