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

Create countdown extension #90

Merged
merged 11 commits into from
Jan 11, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/strange-baboons-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@jspsych-contrib/extension-countdown": major
---

Added countdown extension.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Plugin/Extension | Contributor | Description
[audio-multi-response](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-audio-multi-response/README.md) | [Adam Richie-Halford](https://github.com/richford) | This plugin collects responses to an audio file using both button clicks and key presses.
[audio-swipe-response](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-audio-swipe-response/README.md) | [Adam Richie-Halford](https://github.com/richford) | This plugin collects responses to an audio file using swipe gestures and keyboard responses.
[corsi-blocks](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-corsi-blocks/README.md) | [Josh de Leeuw](https://github.com/jodeleeuw) | This plugin displays a configurable Corsi blocks task and records a series of click responses.
[countdown](https://github.com/jspsych/jspsych-contrib/blob/main/packages/extension-countdown/README.md) | [Shaobin Jiang](https://github.com/Shaobin-Jiang) | This extension adds a countdown during a trial.
[gamepad](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-gamepad/README.md) | [Shaobin Jiang](https://github.com/Shaobin-Jiang) | This plugin allows one to use gamepads in a jsPsych experiment.
[html-choice](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-html-choice/README.md) | [Younes Strittmatter](https://github.com/younesStrittmatter) | This plugin displays clickable html elements that can be used to present a choice.
[html-multi-response](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-html-multi-response/README.md) | [Adam Richie-Halford](https://github.com/richford) | This plugin collects responses to an arbitrary HTML string using both button clicks and key presses.
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 95 additions & 0 deletions packages/extension-countdown/docs/jspsych-countdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# countdown

This extension adds a countdown during a trial.

## Parameters

### Initialization Parameters

None

### Trial Parameters

Trial parameters can be set when adding the extension to a trial object.

```javascript
let trial = {
type: jsPsych...,
extensions: [
{type: jsPsychExtensionWebgazer, params: {...}}
]
}
```

| Parameter | Type | Default Value | Description |
| --------- | ---- | ------------- | ----------- |
| time | number | undefined | Time in milliseconds of the countdown |
| update_time | number | 50 | How often to update the countdown display; in milliseconds |
| format | function | (time) => String(Math.floor(time / 1000)) | The displayed content of the countdown. Receives the current time left in milliseconds and returns a string for display. |

## Data Generated

None

## Functions

These functions below are provided to enable a better interaction with the countdown. Note that all of the functions below must be prefixed with `jsPsych.extensions.countdown` (e.g. `jsPsych.extensions.countdown.pause()`).

### `pause()`

Pauses the countdown.

### `resume()`

Resumes the countdown.

## Example

```javascript
let jsPsych = initJsPsych({
extensions: [{ type: jsPsychExtensionCountdown }],
});

let trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "Hello world",
extensions: [
{
type: jsPsychExtensionCountdown,
params: {
time: 5000,
update_time: 20,
format: (time) => {
if (time < 3000) {
document.querySelector(".jspsych-extension-countdown").style.color = "red";
}

let time_in_seconds = time / 1000;

let minutes = Math.floor(time_in_seconds / 60);
time_in_seconds -= minutes * 60;

let seconds = Math.floor(time_in_seconds);

let format_number = (number) => {
let temp_str = `0${number}`;
return temp_str.substring(temp_str.length - 2);
};

return `${format_number(minutes)}:${format_number(seconds)}`;
},
},
},
],
on_load: function () {
setTimeout(() => {
jsPsych.extensions.countdown.pause();
setTimeout(() => {
jsPsych.extensions.countdown.resume();
}, 2000);
}, 1000);
},
};

jsPsych.run([trial]);
```
64 changes: 64 additions & 0 deletions packages/extension-countdown/examples/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Countdown Extension Example</title>
<script src="https://unpkg.com/[email protected]"></script>
<script src="https://unpkg.com/@jspsych/[email protected]"></script>
<script src="../dist/index.browser.min.js"></script>
<link href="https://unpkg.com/[email protected]/css/jspsych.css" rel="stylesheet" />
</head>
<body>
<script>
let jsPsych = initJsPsych({
extensions: [{ type: jsPsychExtensionCountdown }],
});

let trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "Hello world",
extensions: [
{
type: jsPsychExtensionCountdown,
params: {
time: 5000,
update_time: 20,
// Change the format of the countdown string
format: (time) => {
if (time < 3000) {
document.querySelector(".jspsych-extension-countdown").style.color = "red";
}

let time_in_seconds = time / 1000;

let minutes = Math.floor(time_in_seconds / 60);
time_in_seconds -= minutes * 60;

let seconds = Math.floor(time_in_seconds);

let format_number = (number) => {
let temp_str = `0${number}`;
return temp_str.substring(temp_str.length - 2);
};

return `${format_number(minutes)}:${format_number(seconds)}`;
},
},
},
],
// Pause / Resume midway
on_load: function () {
setTimeout(() => {
jsPsych.extensions.countdown.pause();
setTimeout(() => {
jsPsych.extensions.countdown.resume();
}, 2000);
}, 1000);
},
};

jsPsych.run([trial]);
</script>
</body>
</html>
1 change: 1 addition & 0 deletions packages/extension-countdown/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname);
44 changes: 44 additions & 0 deletions packages/extension-countdown/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@jspsych-contrib/extension-countdown",
"version": "0.0.1",
"description": "jsPsych extension for adding a countdown during a trial",
"type": "module",
"main": "dist/index.cjs",
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"typings": "dist/index.d.ts",
"unpkg": "dist/index.browser.min.js",
"files": [
"src",
"dist"
],
"source": "src/index.ts",
"scripts": {
"test": "jest",
"test:watch": "npm test -- --watch",
"tsc": "tsc",
"build": "rollup --config",
"build:watch": "npm run build -- --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/jspsych/jspsych-contrib.git",
"directory": "packages/extension-countdown"
},
"author": "Shaobin Jiang",
"license": "MIT",
"bugs": {
"url": "https://github.com/jspsych/jspsych-contrib/issues"
},
"homepage": "https://github.com/jspsych/jspsych-contrib/tree/main/packages/extension-countdown",
"peerDependencies": {
"jspsych": ">=7.0.0"
},
"devDependencies": {
"@jspsych/config": "^2.0.0",
"@jspsych/test-utils": "^1.0.0",
"jspsych": "^7.0.0"
}
}
3 changes: 3 additions & 0 deletions packages/extension-countdown/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { makeRollupConfig } from "@jspsych/config/rollup";

export default makeRollupConfig("jsPsychExtensionCountdown");
21 changes: 21 additions & 0 deletions packages/extension-countdown/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import htmlButtonResponse from "@jspsych/plugin-html-button-response";
import { initJsPsych } from "jspsych";

import CountdownExtension from ".";

describe("Countdown Extension", () => {
it("should pass", () => {
const jsPsych = initJsPsych({
extensions: [{ type: CountdownExtension }],
});

let trial = {
type: htmlButtonResponse,
stimulus: "Hello world",
choices: ["Foo", "Bar"],
extensions: [{ type: CountdownExtension, params: { time: 5000 } }],
};

jsPsych.run([trial]);
});
});
87 changes: 87 additions & 0 deletions packages/extension-countdown/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { JsPsych, JsPsychExtension, JsPsychExtensionInfo } from "jspsych";

interface OnStartParameters {
format: (time: number) => string;
time: number;
update_time: number;
}

/**
* **Extension-Countdown**
*
* jsPsych extension for adding a countdown for a trial
*
* @author Shaobin Jiang
*/
class CountdownExtension implements JsPsychExtension {
static info: JsPsychExtensionInfo = {
name: "countdown",
};

constructor(private jsPsych: JsPsych) {}

private format: (time: number) => string;
private time: number;
private update_time: number;

private countdown_element: HTMLElement;
private timer: number;
private last_recorded_time: number;
private time_elapsed: number = 0;
private is_running: boolean = true;

initialize = (): Promise<void> => {
return new Promise((resolve, _) => {
resolve();
});
};

on_start = ({
format = (time: number) => String(Math.floor(time / 1000)),
time,
update_time = 50,
}: OnStartParameters): void => {
this.format = format;
this.time = time;
this.update_time = update_time;

this.countdown_element = document.createElement("div");
this.countdown_element.innerHTML = this.format(time);
this.countdown_element.className = "jspsych-extension-countdown";
this.countdown_element.style.cssText = "font-size: 18px; position: fixed; top: 5%; right: 5%;";
};

on_load = (): void => {
this.jsPsych.getDisplayContainerElement().appendChild(this.countdown_element);
this.last_recorded_time = performance.now();
this.timer = window.setInterval(() => {
let now: number = performance.now();
if (this.is_running) {
this.time_elapsed += now - this.last_recorded_time;
}
this.last_recorded_time = now;
let time_left = this.time - this.time_elapsed;
if (time_left <= 0) {
window.clearInterval(this.timer);
} else {
this.countdown_element.innerHTML = this.format(time_left);
}
}, this.update_time);
};

on_finish = () => {
window.clearInterval(this.timer);
this.countdown_element.remove();
return {};
};

pause = (): void => {
this.is_running = false;
};

resume = (): void => {
this.is_running = true;
};
}

export default CountdownExtension;
7 changes: 7 additions & 0 deletions packages/extension-countdown/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "@jspsych/config/tsconfig.contrib.json",
"compilerOptions": {
"baseUrl": "."
},
"include": ["src"]
}