From 1a9e0aef5121aa1c8dd68d255b1394a24bdf3202 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Fri, 2 Feb 2024 16:48:09 -0500 Subject: [PATCH 1/3] add survey-number plugin --- .changeset/twenty-tomatoes-sparkle.md | 5 + README.md | 1 + packages/plugin-survey-number/README.md | 35 +++ .../docs/survey-number.md | 53 +++++ .../examples/example1.html | 23 ++ packages/plugin-survey-number/jest.config.cjs | 1 + packages/plugin-survey-number/package.json | 44 ++++ .../plugin-survey-number/rollup.config.mjs | 3 + .../plugin-survey-number/src/index.spec.ts | 19 ++ packages/plugin-survey-number/src/index.ts | 223 ++++++++++++++++++ packages/plugin-survey-number/tsconfig.json | 7 + 11 files changed, 414 insertions(+) create mode 100644 .changeset/twenty-tomatoes-sparkle.md create mode 100644 packages/plugin-survey-number/README.md create mode 100644 packages/plugin-survey-number/docs/survey-number.md create mode 100644 packages/plugin-survey-number/examples/example1.html create mode 100644 packages/plugin-survey-number/jest.config.cjs create mode 100644 packages/plugin-survey-number/package.json create mode 100644 packages/plugin-survey-number/rollup.config.mjs create mode 100644 packages/plugin-survey-number/src/index.spec.ts create mode 100644 packages/plugin-survey-number/src/index.ts create mode 100644 packages/plugin-survey-number/tsconfig.json diff --git a/.changeset/twenty-tomatoes-sparkle.md b/.changeset/twenty-tomatoes-sparkle.md new file mode 100644 index 00000000..c6c9de19 --- /dev/null +++ b/.changeset/twenty-tomatoes-sparkle.md @@ -0,0 +1,5 @@ +--- +"@jspsych-contrib/plugin-survey-number": major +--- + +New plugin for displaying a survey question and getting a numeric response diff --git a/README.md b/README.md index e9151413..27811182 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Plugin/Extension | Contributor | Description [rdk](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-rdk/docs/jspsych-rdk.md#jspsych-rdk-plugin) | [Sivananda Rajananda](https://github.com/vrsivananda) | This plugin displays a Random Dot Kinematogram (RDK) and allows the subject to report the primary direction of motion by pressing a key on the keyboard. [rok](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-rok/docs/jspsych-rok.md#jspsych-rok-plugin) | [Younes Strittmatter](https://github.com/younesStrittmatter) | This plugin displays a Random Object Kinematogram (ROK) and allows the subject to report the primary direction of motion or the primary orientation by pressing a key on the keyboard. [self-paced-reading](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-self-paced-reading/docs/jspsych-self-paced-reading.md) | [@igmmgi](https://github.com/igmmgi) | Self-paced reading tasks with different display options. +[survey-number](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-survey-number/README.md) | [Josh de Leeuw](https://github.com/jodeleeuw) | This plugin displays a survey question and collects a numeric response. [vsl-animate-occlusion](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-vsl-animate-occlusion/docs/jspsych-vsl-animate-occlusion.md#jspsych-vsl-animate-occlusion-plugin) | [Josh de Leeuw](https://github.com/jodeleeuw) | The VSL (visual statistical learning) animate occlusion plugin displays an animated sequence of shapes that disappear behind an occluding rectangle while they change from one shape to another. [vsl-grid-scene](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-vsl-grid-scene/docs/jspsych-vsl-grid-scene.md#jspsych-vsl-grid-scene-plugin) | [Josh de Leeuw](https://github.com/jodeleeuw) | The VSL (visual statistical learning) grid scene plugin displays images arranged in a grid. diff --git a/packages/plugin-survey-number/README.md b/packages/plugin-survey-number/README.md new file mode 100644 index 00000000..829f9d40 --- /dev/null +++ b/packages/plugin-survey-number/README.md @@ -0,0 +1,35 @@ +# survey-number + +## Overview + +Collects a number response in a text box + +## Loading + +### In browser + +```js + +``` + +Using the JavaScript file downloaded from a GitHub release dist archive: + +```js + +``` + +Using NPM: + +``` +npm install @jspsych-contrib/plugin-survey-number +``` + +```js +import SurveyNumber from '@jspsych-contrib/plugin-survey-number'; +``` + +## Examples + +### Title of Example + +```javascript +var trial = { + type: jsPsychSurveyNumber +} +``` \ No newline at end of file diff --git a/packages/plugin-survey-number/examples/example1.html b/packages/plugin-survey-number/examples/example1.html new file mode 100644 index 00000000..09a1bf45 --- /dev/null +++ b/packages/plugin-survey-number/examples/example1.html @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/plugin-survey-number/jest.config.cjs b/packages/plugin-survey-number/jest.config.cjs new file mode 100644 index 00000000..6ac19d5c --- /dev/null +++ b/packages/plugin-survey-number/jest.config.cjs @@ -0,0 +1 @@ +module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname); diff --git a/packages/plugin-survey-number/package.json b/packages/plugin-survey-number/package.json new file mode 100644 index 00000000..b4838680 --- /dev/null +++ b/packages/plugin-survey-number/package.json @@ -0,0 +1,44 @@ +{ + "name": "@jspsych-contrib/plugin-survey-number", + "version": "0.0.1", + "description": "Collects a number response in a text box", + "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/plugin-survey-number" + }, + "author": "Josh de Leeuw", + "license": "MIT", + "bugs": { + "url": "https://github.com/jspsych/jspsych-contrib/issues" + }, + "homepage": "https://github.com/jspsych/jspsych-contrib/tree/main/packages/plugin-survey-number", + "peerDependencies": { + "jspsych": ">=7.0.0" + }, + "devDependencies": { + "@jspsych/config": "^2.0.0", + "@jspsych/test-utils": "^1.0.0", + "jspsych": "^7.0.0" + } +} diff --git a/packages/plugin-survey-number/rollup.config.mjs b/packages/plugin-survey-number/rollup.config.mjs new file mode 100644 index 00000000..f66af188 --- /dev/null +++ b/packages/plugin-survey-number/rollup.config.mjs @@ -0,0 +1,3 @@ +import { makeRollupConfig } from "@jspsych/config/rollup"; + +export default makeRollupConfig("jsPsychSurveyNumber"); diff --git a/packages/plugin-survey-number/src/index.spec.ts b/packages/plugin-survey-number/src/index.spec.ts new file mode 100644 index 00000000..1e29cff2 --- /dev/null +++ b/packages/plugin-survey-number/src/index.spec.ts @@ -0,0 +1,19 @@ +import { startTimeline } from "@jspsych/test-utils"; + +import jsPsychSurveyNumber from "."; + +jest.useFakeTimers(); + +describe("my plugin", () => { + it("should load", async () => { + const { expectFinished, getHTML, getData, displayElement, jsPsych } = await startTimeline([ + { + type: jsPsychSurveyNumber, + parameter_name: 1, + parameter_name2: "img.png", + }, + ]); + + await expectFinished(); + }); +}); diff --git a/packages/plugin-survey-number/src/index.ts b/packages/plugin-survey-number/src/index.ts new file mode 100644 index 00000000..8e1b505d --- /dev/null +++ b/packages/plugin-survey-number/src/index.ts @@ -0,0 +1,223 @@ +import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; + +const info = { + name: "survey-number", + parameters: { + questions: { + type: ParameterType.COMPLEX, + array: true, + pretty_name: "Questions", + default: undefined, + nested: { + /** Question prompt. */ + prompt: { + type: ParameterType.HTML_STRING, + pretty_name: "Prompt", + default: undefined, + }, + /** Placeholder text in the response text box. */ + placeholder: { + type: ParameterType.STRING, + pretty_name: "Placeholder", + default: "", + }, + /** The number of rows for the response text box. */ + rows: { + type: ParameterType.INT, + pretty_name: "Rows", + default: 1, + }, + /** The number of columns for the response text box. */ + columns: { + type: ParameterType.INT, + pretty_name: "Columns", + default: 40, + }, + /** Whether or not a response to this question must be given in order to continue. */ + required: { + type: ParameterType.BOOL, + pretty_name: "Required", + default: false, + }, + /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */ + name: { + type: ParameterType.STRING, + pretty_name: "Question Name", + default: "", + }, + }, + }, + /** If true, the order of the questions in the 'questions' array will be randomized. */ + randomize_question_order: { + type: ParameterType.BOOL, + pretty_name: "Randomize Question Order", + default: false, + }, + /** HTML-formatted string to display at top of the page above all of the questions. */ + preamble: { + type: ParameterType.HTML_STRING, + pretty_name: "Preamble", + default: null, + }, + /** Label of the button to submit responses. */ + button_label: { + type: ParameterType.STRING, + pretty_name: "Button label", + default: "Continue", + }, + /** Setting this to true will enable browser auto-complete or auto-fill for the form. */ + autocomplete: { + type: ParameterType.BOOL, + pretty_name: "Allow autocomplete", + default: false, + }, + }, +}; + +type Info = typeof info; + +/** + * **survey-number** + * + * Collects a number response in a text box + * + * @author Josh de Leeuw + * @see {@link https://github.com/jspsych/jspsych-contrib/packages/plugin-survey-number/README.md}} + */ +class SurveyNumberPlugin implements JsPsychPlugin { + static info = info; + + constructor(private jsPsych: JsPsych) {} + + trial(display_element: HTMLElement, trial: TrialType) { + for (var i = 0; i < trial.questions.length; i++) { + if (typeof trial.questions[i].rows == "undefined") { + trial.questions[i].rows = 1; + } + } + for (var i = 0; i < trial.questions.length; i++) { + if (typeof trial.questions[i].columns == "undefined") { + trial.questions[i].columns = 40; + } + } + for (var i = 0; i < trial.questions.length; i++) { + if (typeof trial.questions[i].value == "undefined") { + trial.questions[i].value = ""; + } + } + var html = ""; + // show preamble text + if (trial.preamble !== null) { + html += + '
' + + trial.preamble + + "
"; + } + // start form + if (trial.autocomplete) { + html += '
'; + } else { + html += ''; + } + // generate question order + var question_order = []; + for (var i = 0; i < trial.questions.length; i++) { + question_order.push(i); + } + if (trial.randomize_question_order) { + question_order = this.jsPsych.randomization.shuffle(question_order); + } + // add questions + for (var i = 0; i < trial.questions.length; i++) { + var question = trial.questions[question_order[i]]; + var question_index = question_order[i]; + html += + '
'; + html += '

' + question.prompt + "

"; + var autofocus = i == 0 ? "autofocus" : ""; + var req = question.required ? "required" : ""; + if (question.rows == 1) { + html += + ''; + } else { + html += + ''; + } + html += "
"; + } + // add submit button + html += + ''; + html += "
"; + display_element.innerHTML = html; + // backup in case autofocus doesn't work + (display_element.querySelector("#input-" + question_order[0]) as HTMLInputElement).focus(); + display_element.querySelector("#jspsych-survey-text-form").addEventListener("submit", (e) => { + e.preventDefault(); + // measure response time + var endTime = performance.now(); + var response_time = Math.round(endTime - startTime); + // create object to hold responses + var question_data = {}; + for (var index = 0; index < trial.questions.length; index++) { + var id = "Q" + index; + var q_element: HTMLInputElement | HTMLTextAreaElement = document + .querySelector("#jspsych-survey-text-" + index) + .querySelector("textarea, input"); + var val = q_element.value; + var name = q_element.attributes["data-name"].value; + if (name == "") { + name = id; + } + var obje = {}; + obje[name] = val; + Object.assign(question_data, obje); + } + // save data + var trialdata = { + rt: response_time, + response: question_data, + }; + display_element.innerHTML = ""; + // next trial + this.jsPsych.finishTrial(trialdata); + }); + var startTime = performance.now(); + } +} + +export default SurveyNumberPlugin; diff --git a/packages/plugin-survey-number/tsconfig.json b/packages/plugin-survey-number/tsconfig.json new file mode 100644 index 00000000..3eabd0c2 --- /dev/null +++ b/packages/plugin-survey-number/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@jspsych/config/tsconfig.contrib.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src"] +} From 48fa3e6788164c4c329c14891883ef972bf83e74 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Fri, 2 Feb 2024 16:49:59 -0500 Subject: [PATCH 2/3] update package-lock.json --- package-lock.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index 1b589fb4..5046257c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3155,6 +3155,10 @@ "resolved": "packages/plugin-self-paced-reading", "link": true }, + "node_modules/@jspsych-contrib/plugin-survey-number": { + "resolved": "packages/plugin-survey-number", + "link": true + }, "node_modules/@jspsych-contrib/plugin-video-several-keyboard-responses": { "resolved": "packages/plugin-video-several-keyboard-responses", "link": true @@ -17822,6 +17826,18 @@ "jspsych": ">=7.0.0" } }, + "packages/plugin-survey-number": { + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@jspsych/config": "^2.0.0", + "@jspsych/test-utils": "^1.0.0", + "jspsych": "^7.0.0" + }, + "peerDependencies": { + "jspsych": ">=7.0.0" + } + }, "packages/plugin-testing-stuff": { "version": "0.0.1", "extraneous": true, From 4594f5ccecb29ce9324ff9355d0b1528bcb5c18d Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Fri, 2 Feb 2024 16:57:27 -0500 Subject: [PATCH 3/3] fix failing test because I forgot to implement it --- packages/plugin-survey-number/src/index.spec.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/plugin-survey-number/src/index.spec.ts b/packages/plugin-survey-number/src/index.spec.ts index 1e29cff2..f484040f 100644 --- a/packages/plugin-survey-number/src/index.spec.ts +++ b/packages/plugin-survey-number/src/index.spec.ts @@ -9,11 +9,16 @@ describe("my plugin", () => { const { expectFinished, getHTML, getData, displayElement, jsPsych } = await startTimeline([ { type: jsPsychSurveyNumber, - parameter_name: 1, - parameter_name2: "img.png", + questions: [{ prompt: "How old are you?" }], }, ]); + expect(getHTML()).toMatch("How old are you?"); + + displayElement.querySelector("input").value = "25"; + + (displayElement.querySelector("#jspsych-survey-text-next") as HTMLInputElement).click(); + await expectFinished(); }); });