@jspsych/plugin-survey-multi-choice 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.browser.js +37 -15
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +8 -8
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +36 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +36 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.spec.ts +92 -37
- package/src/index.ts +39 -16
package/dist/index.browser.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
var jsPsychSurveyMultiChoice = (function (jspsych) {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var version = "2.
|
|
4
|
+
var version = "2.2.0";
|
|
5
5
|
|
|
6
6
|
const info = {
|
|
7
7
|
name: "survey-multi-choice",
|
|
@@ -83,6 +83,11 @@ var jsPsychSurveyMultiChoice = (function (jspsych) {
|
|
|
83
83
|
response: {
|
|
84
84
|
type: jspsych.ParameterType.OBJECT
|
|
85
85
|
},
|
|
86
|
+
/** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */
|
|
87
|
+
response_index: {
|
|
88
|
+
type: jspsych.ParameterType.INT,
|
|
89
|
+
array: true
|
|
90
|
+
},
|
|
86
91
|
/** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */
|
|
87
92
|
rt: {
|
|
88
93
|
type: jspsych.ParameterType.INT
|
|
@@ -141,7 +146,9 @@ var jsPsychSurveyMultiChoice = (function (jspsych) {
|
|
|
141
146
|
if (question.horizontal) {
|
|
142
147
|
question_classes.push(`${plugin_id_name}-horizontal`);
|
|
143
148
|
}
|
|
144
|
-
html += `<div id="${plugin_id_name}-${question_id}" class="${question_classes.join(
|
|
149
|
+
html += `<div id="${plugin_id_name}-${question_id}" class="${question_classes.join(
|
|
150
|
+
" "
|
|
151
|
+
)}" data-name="${question.name}">`;
|
|
145
152
|
html += `<p class="${plugin_id_name}-text survey-multi-choice">${question.prompt}`;
|
|
146
153
|
if (question.required) {
|
|
147
154
|
html += "<span class='required'>*</span>";
|
|
@@ -155,7 +162,7 @@ var jsPsychSurveyMultiChoice = (function (jspsych) {
|
|
|
155
162
|
html += `
|
|
156
163
|
<div id="${option_id_name}" class="${plugin_id_name}-option">
|
|
157
164
|
<label class="${plugin_id_name}-text" for="${input_id}">
|
|
158
|
-
<input type="radio" name="${input_name}" id="${input_id}" value="${question.options[j]}" ${required_attr} />
|
|
165
|
+
<input type="radio" name="${input_name}" id="${input_id}" value="${question.options[j]}" data-option-index="${j}" ${required_attr} />
|
|
159
166
|
${question.options[j]}
|
|
160
167
|
</label>
|
|
161
168
|
</div>`;
|
|
@@ -171,12 +178,16 @@ var jsPsychSurveyMultiChoice = (function (jspsych) {
|
|
|
171
178
|
var endTime = performance.now();
|
|
172
179
|
var response_time = Math.round(endTime - startTime);
|
|
173
180
|
var question_data = {};
|
|
181
|
+
var response_index = [];
|
|
174
182
|
for (var i2 = 0; i2 < trial.questions.length; i2++) {
|
|
175
183
|
var match = display_element.querySelector(`#${plugin_id_name}-${i2}`);
|
|
176
184
|
var id = "Q" + i2;
|
|
177
|
-
var val;
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
var val = "";
|
|
186
|
+
var selected_index = -1;
|
|
187
|
+
var checked = match.querySelector("input[type=radio]:checked");
|
|
188
|
+
if (checked !== null) {
|
|
189
|
+
val = checked.value;
|
|
190
|
+
selected_index = Number(checked.dataset.optionIndex);
|
|
180
191
|
} else {
|
|
181
192
|
val = "";
|
|
182
193
|
}
|
|
@@ -187,10 +198,12 @@ var jsPsychSurveyMultiChoice = (function (jspsych) {
|
|
|
187
198
|
}
|
|
188
199
|
obje[name] = val;
|
|
189
200
|
Object.assign(question_data, obje);
|
|
201
|
+
response_index.push(selected_index);
|
|
190
202
|
}
|
|
191
203
|
var trial_data = {
|
|
192
204
|
rt: response_time,
|
|
193
205
|
response: question_data,
|
|
206
|
+
response_index,
|
|
194
207
|
question_order
|
|
195
208
|
};
|
|
196
209
|
this.jsPsych.finishTrial(trial_data);
|
|
@@ -208,14 +221,19 @@ var jsPsychSurveyMultiChoice = (function (jspsych) {
|
|
|
208
221
|
}
|
|
209
222
|
create_simulation_data(trial, simulation_options) {
|
|
210
223
|
const question_data = {};
|
|
224
|
+
const response_index = [];
|
|
211
225
|
let rt = 1e3;
|
|
212
|
-
for (
|
|
213
|
-
const
|
|
214
|
-
|
|
226
|
+
for (let i = 0; i < trial.questions.length; i++) {
|
|
227
|
+
const q = trial.questions[i];
|
|
228
|
+
const name = q.name ? q.name : `Q${i}`;
|
|
229
|
+
const option_index = this.jsPsych.randomization.randomInt(0, q.options.length - 1);
|
|
230
|
+
question_data[name] = q.options[option_index];
|
|
231
|
+
response_index.push(option_index);
|
|
215
232
|
rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);
|
|
216
233
|
}
|
|
217
234
|
const default_data = {
|
|
218
235
|
response: question_data,
|
|
236
|
+
response_index,
|
|
219
237
|
rt,
|
|
220
238
|
question_order: trial.randomize_question_order ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()]) : [...Array(trial.questions.length).keys()]
|
|
221
239
|
};
|
|
@@ -233,13 +251,17 @@ var jsPsychSurveyMultiChoice = (function (jspsych) {
|
|
|
233
251
|
this.trial(display_element, trial);
|
|
234
252
|
load_callback();
|
|
235
253
|
const answers = Object.entries(data.response);
|
|
254
|
+
const response_index = Array.isArray(data.response_index) ? data.response_index : [];
|
|
236
255
|
for (let i = 0; i < answers.length; i++) {
|
|
256
|
+
let option_index = response_index[i];
|
|
257
|
+
if (typeof option_index !== "number" || option_index < 0) {
|
|
258
|
+
option_index = trial.questions[i].options.indexOf(answers[i][1]);
|
|
259
|
+
}
|
|
260
|
+
if (option_index < 0) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
237
263
|
this.jsPsych.pluginAPI.clickTarget(
|
|
238
|
-
display_element.querySelector(
|
|
239
|
-
`#${plugin_id_name}-response-${i}-${trial.questions[i].options.indexOf(
|
|
240
|
-
answers[i][1]
|
|
241
|
-
)}`
|
|
242
|
-
),
|
|
264
|
+
display_element.querySelector(`#${plugin_id_name}-response-${i}-${option_index}`),
|
|
243
265
|
(data.rt - 1e3) / answers.length * (i + 1)
|
|
244
266
|
);
|
|
245
267
|
}
|
|
@@ -253,4 +275,4 @@ var jsPsychSurveyMultiChoice = (function (jspsych) {
|
|
|
253
275
|
return SurveyMultiChoicePlugin;
|
|
254
276
|
|
|
255
277
|
})(jsPsychModule);
|
|
256
|
-
//# sourceMappingURL=https://unpkg.com/@jspsych/plugin-survey-multi-choice@2.
|
|
278
|
+
//# sourceMappingURL=https://unpkg.com/@jspsych/plugin-survey-multi-choice@2.2.0/dist/index.browser.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.browser.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-survey-multi-choice\",\n \"version\": \"2.1.0\",\n \"description\": \"a jspsych plugin for multiple choice survey questions\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-survey-multi-choice\"\n },\n \"author\": \"Shane Martin\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/survey-multi-choice\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"survey-multi-choice\",\n version: version,\n parameters: {\n /**\n * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt,\n * options, required, and horizontal parameter that will be applied to the question. See examples below for further\n * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be\n * associated with a group of options (radio buttons). All questions will get presented on the same page (trial).\n * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to\n * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates\n * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is\n * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the\n * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing\n * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.\n */\n questions: {\n type: ParameterType.COMPLEX,\n array: true,\n nested: {\n /** Question prompt. */\n prompt: {\n type: ParameterType.HTML_STRING,\n default: undefined,\n },\n /** Array of multiple choice options for this question. */\n options: {\n type: ParameterType.STRING,\n array: true,\n default: undefined,\n },\n /** Whether or not a response to this question must be given in order to continue. */\n required: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** If true, then the question will be centered and options will be displayed horizontally. */\n horizontal: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */\n name: {\n type: ParameterType.STRING,\n default: \"\",\n },\n },\n },\n /**\n * If true, the display order of `questions` is randomly determined at the start of the trial. In the data object,\n * `Q0` will still refer to the first question in the array, regardless of where it was presented visually.\n */\n randomize_question_order: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** HTML formatted string to display at the top of the page above all the questions. */\n preamble: {\n type: ParameterType.HTML_STRING,\n default: null,\n },\n /** Label of the button. */\n button_label: {\n type: ParameterType.STRING,\n default: \"Continue\",\n },\n /**\n * This determines whether or not all of the input elements on the page should allow autocomplete. Setting\n * this to true will enable autocomplete or auto-fill for the form.\n */\n autocomplete: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n response: {\n type: ParameterType.OBJECT,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */\n rt: {\n type: ParameterType.INT,\n },\n /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n question_order: {\n type: ParameterType.INT,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\nconst plugin_id_name = \"jspsych-survey-multi-choice\";\n\n/**\n * **survey-multi-choice**\n *\n * The survey-multi-choice plugin displays a set of questions with multiple choice response fields. The participant selects a single answer.\n *\n * @author Shane Martin\n * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org}\n */\nclass SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) { }\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n\n const trial_form_id = `${plugin_id_name}_form`;\n\n var html = \"\";\n\n // inject CSS for trial\n html += `\n <style id=\"${plugin_id_name}-css\">\n .${plugin_id_name}-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }\n .${plugin_id_name}-text span.required {color: darkred;}\n .${plugin_id_name}-horizontal .${plugin_id_name}-text { text-align: center;}\n .${plugin_id_name}-option { line-height: 2; }\n .${plugin_id_name}-horizontal .${plugin_id_name}-option { display: inline-block; margin-left: 1em; margin-right: 1em; vertical-align: top;}\n label.${plugin_id_name}-text input[type='radio'] {margin-right: 1em;}\n </style>`;\n\n // show preamble text\n if (trial.preamble !== null) {\n html += `<div id=\"${plugin_id_name}-preamble\" class=\"${plugin_id_name}-preamble\">${trial.preamble}</div>`;\n }\n\n // form element\n if (trial.autocomplete) {\n html += `<form id=\"${trial_form_id}\">`;\n } else {\n html += `<form id=\"${trial_form_id}\" autocomplete=\"off\">`;\n }\n\n // generate question order. this is randomized here as opposed to randomizing the order of trial.questions\n // so that the data are always associated with the same question regardless of order\n var question_order = [];\n for (var i = 0; i < trial.questions.length; i++) {\n question_order.push(i);\n }\n if (trial.randomize_question_order) {\n question_order = this.jsPsych.randomization.shuffle(question_order);\n }\n\n // add multiple-choice questions\n for (var i = 0; i < trial.questions.length; i++) {\n // get question based on question_order\n var question = trial.questions[question_order[i]];\n var question_id = question_order[i];\n\n // create question container\n var question_classes = [`${plugin_id_name}-question`];\n if (question.horizontal) {\n question_classes.push(`${plugin_id_name}-horizontal`);\n }\n\n html += `<div id=\"${plugin_id_name}-${question_id}\" class=\"${question_classes.join(\" \")}\" data-name=\"${question.name}\">`;\n\n // add question text\n html += `<p class=\"${plugin_id_name}-text survey-multi-choice\">${question.prompt}`;\n if (question.required) {\n html += \"<span class='required'>*</span>\";\n }\n html += \"</p>\";\n\n // create option radio buttons\n for (var j = 0; j < question.options.length; j++) {\n // add label and question text\n var option_id_name = `${plugin_id_name}-option-${question_id}-${j}`;\n var input_name = `${plugin_id_name}-response-${question_id}`;\n var input_id = `${plugin_id_name}-response-${question_id}-${j}`;\n\n var required_attr = question.required ? \"required\" : \"\";\n\n // add radio button container\n html += `\n <div id=\"${option_id_name}\" class=\"${plugin_id_name}-option\">\n <label class=\"${plugin_id_name}-text\" for=\"${input_id}\">\n <input type=\"radio\" name=\"${input_name}\" id=\"${input_id}\" value=\"${question.options[j]}\" ${required_attr} />\n ${question.options[j]}\n </label>\n </div>`;\n }\n\n html += \"</div>\";\n }\n\n // add submit button\n html += `<input type=\"submit\" id=\"${plugin_id_name}-next\" class=\"${plugin_id_name} jspsych-btn\"${trial.button_label ? ' value=\"' + trial.button_label + '\"' : \"\"} />`;\n html += \"</form>\";\n\n // render\n display_element.innerHTML = html;\n\n const trial_form = display_element.querySelector<HTMLFormElement>(`#${trial_form_id}`);\n\n trial_form.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n // measure response time\n var endTime = performance.now();\n var response_time = Math.round(endTime - startTime);\n\n // create object to hold responses\n var question_data = {};\n for (var i = 0; i < trial.questions.length; i++) {\n var match = display_element.querySelector(`#${plugin_id_name}-${i}`);\n var id = \"Q\" + i;\n var val: String;\n if (match.querySelector(\"input[type=radio]:checked\") !== null) {\n val = match.querySelector<HTMLInputElement>(\"input[type=radio]:checked\").value;\n } else {\n val = \"\";\n }\n var obje = {};\n var name = id;\n if (match.attributes[\"data-name\"].value !== \"\") {\n name = match.attributes[\"data-name\"].value;\n }\n obje[name] = val;\n Object.assign(question_data, obje);\n }\n // save data\n var trial_data = {\n rt: response_time,\n response: question_data,\n question_order: question_order,\n };\n\n // next trial\n this.jsPsych.finishTrial(trial_data);\n });\n\n var startTime = performance.now();\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const question_data = {};\n let rt = 1000;\n\n for (const q of trial.questions) {\n const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`;\n question_data[name] = this.jsPsych.randomization.sampleWithoutReplacement(q.options, 1)[0];\n rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);\n }\n\n const default_data = {\n response: question_data,\n rt: rt,\n question_order: trial.randomize_question_order\n ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()])\n : [...Array(trial.questions.length).keys()],\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const answers = Object.entries(data.response);\n for (let i = 0; i < answers.length; i++) {\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(\n `#${plugin_id_name}-response-${i}-${trial.questions[i].options.indexOf(\n answers[i][1]\n )}`\n ),\n ((data.rt - 1000) / answers.length) * (i + 1)\n );\n }\n\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-next`),\n data.rt\n );\n }\n}\n\nexport default SurveyMultiChoicePlugin;\n"],"names":[],"mappings":";;;EAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IC6FA,SAAA,EAAA;EAAA;;KAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"index.browser.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-survey-multi-choice\",\n \"version\": \"2.2.0\",\n \"description\": \"a jspsych plugin for multiple choice survey questions\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-survey-multi-choice\"\n },\n \"author\": \"Shane Martin\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/survey-multi-choice\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"survey-multi-choice\",\n version: version,\n parameters: {\n /**\n * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt,\n * options, required, and horizontal parameter that will be applied to the question. See examples below for further\n * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be\n * associated with a group of options (radio buttons). All questions will get presented on the same page (trial).\n * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to\n * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates\n * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is\n * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the\n * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing\n * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.\n */\n questions: {\n type: ParameterType.COMPLEX,\n array: true,\n nested: {\n /** Question prompt. */\n prompt: {\n type: ParameterType.HTML_STRING,\n default: undefined,\n },\n /** Array of multiple choice options for this question. */\n options: {\n type: ParameterType.STRING,\n array: true,\n default: undefined,\n },\n /** Whether or not a response to this question must be given in order to continue. */\n required: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** If true, then the question will be centered and options will be displayed horizontally. */\n horizontal: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */\n name: {\n type: ParameterType.STRING,\n default: \"\",\n },\n },\n },\n /**\n * If true, the display order of `questions` is randomly determined at the start of the trial. In the data object,\n * `Q0` will still refer to the first question in the array, regardless of where it was presented visually.\n */\n randomize_question_order: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** HTML formatted string to display at the top of the page above all the questions. */\n preamble: {\n type: ParameterType.HTML_STRING,\n default: null,\n },\n /** Label of the button. */\n button_label: {\n type: ParameterType.STRING,\n default: \"Continue\",\n },\n /**\n * This determines whether or not all of the input elements on the page should allow autocomplete. Setting\n * this to true will enable autocomplete or auto-fill for the form.\n */\n autocomplete: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n response: {\n type: ParameterType.OBJECT,\n },\n /** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */\n response_index: {\n type: ParameterType.INT,\n array: true,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */\n rt: {\n type: ParameterType.INT,\n },\n /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n question_order: {\n type: ParameterType.INT,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\nconst plugin_id_name = \"jspsych-survey-multi-choice\";\n\n/**\n * **survey-multi-choice**\n *\n * The survey-multi-choice plugin displays a set of questions with multiple choice response fields. The participant selects a single answer.\n *\n * @author Shane Martin\n * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org}\n */\nclass SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n const trial_form_id = `${plugin_id_name}_form`;\n\n var html = \"\";\n\n // inject CSS for trial\n html += `\n <style id=\"${plugin_id_name}-css\">\n .${plugin_id_name}-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }\n .${plugin_id_name}-text span.required {color: darkred;}\n .${plugin_id_name}-horizontal .${plugin_id_name}-text { text-align: center;}\n .${plugin_id_name}-option { line-height: 2; }\n .${plugin_id_name}-horizontal .${plugin_id_name}-option { display: inline-block; margin-left: 1em; margin-right: 1em; vertical-align: top;}\n label.${plugin_id_name}-text input[type='radio'] {margin-right: 1em;}\n </style>`;\n\n // show preamble text\n if (trial.preamble !== null) {\n html += `<div id=\"${plugin_id_name}-preamble\" class=\"${plugin_id_name}-preamble\">${trial.preamble}</div>`;\n }\n\n // form element\n if (trial.autocomplete) {\n html += `<form id=\"${trial_form_id}\">`;\n } else {\n html += `<form id=\"${trial_form_id}\" autocomplete=\"off\">`;\n }\n\n // generate question order. this is randomized here as opposed to randomizing the order of trial.questions\n // so that the data are always associated with the same question regardless of order\n var question_order = [];\n for (var i = 0; i < trial.questions.length; i++) {\n question_order.push(i);\n }\n if (trial.randomize_question_order) {\n question_order = this.jsPsych.randomization.shuffle(question_order);\n }\n\n // add multiple-choice questions\n for (var i = 0; i < trial.questions.length; i++) {\n // get question based on question_order\n var question = trial.questions[question_order[i]];\n var question_id = question_order[i];\n\n // create question container\n var question_classes = [`${plugin_id_name}-question`];\n if (question.horizontal) {\n question_classes.push(`${plugin_id_name}-horizontal`);\n }\n\n html += `<div id=\"${plugin_id_name}-${question_id}\" class=\"${question_classes.join(\n \" \"\n )}\" data-name=\"${question.name}\">`;\n\n // add question text\n html += `<p class=\"${plugin_id_name}-text survey-multi-choice\">${question.prompt}`;\n if (question.required) {\n html += \"<span class='required'>*</span>\";\n }\n html += \"</p>\";\n\n // create option radio buttons\n for (var j = 0; j < question.options.length; j++) {\n // add label and question text\n var option_id_name = `${plugin_id_name}-option-${question_id}-${j}`;\n var input_name = `${plugin_id_name}-response-${question_id}`;\n var input_id = `${plugin_id_name}-response-${question_id}-${j}`;\n\n var required_attr = question.required ? \"required\" : \"\";\n\n // add radio button container\n html += `\n <div id=\"${option_id_name}\" class=\"${plugin_id_name}-option\">\n <label class=\"${plugin_id_name}-text\" for=\"${input_id}\">\n <input type=\"radio\" name=\"${input_name}\" id=\"${input_id}\" value=\"${question.options[j]}\" data-option-index=\"${j}\" ${required_attr} />\n ${question.options[j]}\n </label>\n </div>`;\n }\n\n html += \"</div>\";\n }\n\n // add submit button\n html += `<input type=\"submit\" id=\"${plugin_id_name}-next\" class=\"${plugin_id_name} jspsych-btn\"${\n trial.button_label ? ' value=\"' + trial.button_label + '\"' : \"\"\n } />`;\n html += \"</form>\";\n\n // render\n display_element.innerHTML = html;\n\n const trial_form = display_element.querySelector<HTMLFormElement>(`#${trial_form_id}`);\n\n trial_form.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n // measure response time\n var endTime = performance.now();\n var response_time = Math.round(endTime - startTime);\n\n // create object to hold responses\n var question_data = {};\n var response_index = [];\n for (var i = 0; i < trial.questions.length; i++) {\n var match = display_element.querySelector(`#${plugin_id_name}-${i}`);\n var id = \"Q\" + i;\n var val: String = \"\";\n var selected_index = -1;\n var checked = match.querySelector<HTMLInputElement>(\"input[type=radio]:checked\");\n if (checked !== null) {\n val = checked.value;\n selected_index = Number(checked.dataset.optionIndex);\n } else {\n val = \"\";\n }\n var obje = {};\n var name = id;\n if (match.attributes[\"data-name\"].value !== \"\") {\n name = match.attributes[\"data-name\"].value;\n }\n obje[name] = val;\n Object.assign(question_data, obje);\n response_index.push(selected_index);\n }\n // save data\n var trial_data = {\n rt: response_time,\n response: question_data,\n response_index: response_index,\n question_order: question_order,\n };\n\n // next trial\n this.jsPsych.finishTrial(trial_data);\n });\n\n var startTime = performance.now();\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const question_data = {};\n const response_index = [];\n let rt = 1000;\n\n for (let i = 0; i < trial.questions.length; i++) {\n const q = trial.questions[i];\n const name = q.name ? q.name : `Q${i}`;\n const option_index = this.jsPsych.randomization.randomInt(0, q.options.length - 1);\n question_data[name] = q.options[option_index];\n response_index.push(option_index);\n rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);\n }\n\n const default_data = {\n response: question_data,\n response_index: response_index,\n rt: rt,\n question_order: trial.randomize_question_order\n ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()])\n : [...Array(trial.questions.length).keys()],\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const answers = Object.entries(data.response);\n const response_index = Array.isArray(data.response_index) ? data.response_index : [];\n for (let i = 0; i < answers.length; i++) {\n let option_index = response_index[i];\n if (typeof option_index !== \"number\" || option_index < 0) {\n option_index = trial.questions[i].options.indexOf(answers[i][1]);\n }\n if (option_index < 0) {\n continue;\n }\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-response-${i}-${option_index}`),\n ((data.rt - 1000) / answers.length) * (i + 1)\n );\n }\n\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-next`),\n data.rt\n );\n }\n}\n\nexport default SurveyMultiChoicePlugin;\n"],"names":[],"mappings":";;;EAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ICkGA,SAAA,EAAA;EAAA;;KAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var jsPsychSurveyMultiChoice=function(
|
|
1
|
+
var jsPsychSurveyMultiChoice=function(i){"use strict";var T="2.2.0";const x={name:"survey-multi-choice",version:T,parameters:{questions:{type:i.ParameterType.COMPLEX,array:!0,nested:{prompt:{type:i.ParameterType.HTML_STRING,default:void 0},options:{type:i.ParameterType.STRING,array:!0,default:void 0},required:{type:i.ParameterType.BOOL,default:!1},horizontal:{type:i.ParameterType.BOOL,default:!1},name:{type:i.ParameterType.STRING,default:""}}},randomize_question_order:{type:i.ParameterType.BOOL,default:!1},preamble:{type:i.ParameterType.HTML_STRING,default:null},button_label:{type:i.ParameterType.STRING,default:"Continue"},autocomplete:{type:i.ParameterType.BOOL,default:!1}},data:{response:{type:i.ParameterType.OBJECT},response_index:{type:i.ParameterType.INT,array:!0},rt:{type:i.ParameterType.INT},question_order:{type:i.ParameterType.INT,array:!0}},citations:{apa:"de Leeuw, J. R., Gilbert, R. A., & Luchterhandt, B. (2023). jsPsych: Enabling an Open-Source Collaborative Ecosystem of Behavioral Experiments. Journal of Open Source Software, 8(85), 5351. https://doi.org/10.21105/joss.05351 ",bibtex:'@article{Leeuw2023jsPsych, author = {de Leeuw, Joshua R. and Gilbert, Rebecca A. and Luchterhandt, Bj{\\" o}rn}, journal = {Journal of Open Source Software}, doi = {10.21105/joss.05351}, issn = {2475-9066}, number = {85}, year = {2023}, month = {may 11}, pages = {5351}, publisher = {Open Journals}, title = {jsPsych: Enabling an {Open}-{Source} {Collaborative} {Ecosystem} of {Behavioral} {Experiments}}, url = {https://joss.theoj.org/papers/10.21105/joss.05351}, volume = {8}, } '}},e="jspsych-survey-multi-choice";class f{constructor(r){this.jsPsych=r}trial(r,a){const s=`${e}_form`;var t="";t+=`
|
|
2
2
|
<style id="${e}-css">
|
|
3
3
|
.${e}-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }
|
|
4
4
|
.${e}-text span.required {color: darkred;}
|
|
@@ -6,11 +6,11 @@ var jsPsychSurveyMultiChoice=function(n){"use strict";var b="2.1.0";const g={nam
|
|
|
6
6
|
.${e}-option { line-height: 2; }
|
|
7
7
|
.${e}-horizontal .${e}-option { display: inline-block; margin-left: 1em; margin-right: 1em; vertical-align: top;}
|
|
8
8
|
label.${e}-text input[type='radio'] {margin-right: 1em;}
|
|
9
|
-
</style>`,
|
|
10
|
-
<div id="${
|
|
11
|
-
<label class="${e}-text" for="${
|
|
12
|
-
<input type="radio" name="${
|
|
13
|
-
${
|
|
9
|
+
</style>`,a.preamble!==null&&(t+=`<div id="${e}-preamble" class="${e}-preamble">${a.preamble}</div>`),a.autocomplete?t+=`<form id="${s}">`:t+=`<form id="${s}" autocomplete="off">`;for(var l=[],n=0;n<a.questions.length;n++)l.push(n);a.randomize_question_order&&(l=this.jsPsych.randomization.shuffle(l));for(var n=0;n<a.questions.length;n++){var u=a.questions[l[n]],o=l[n],p=[`${e}-question`];u.horizontal&&p.push(`${e}-horizontal`),t+=`<div id="${e}-${o}" class="${p.join(" ")}" data-name="${u.name}">`,t+=`<p class="${e}-text survey-multi-choice">${u.prompt}`,u.required&&(t+="<span class='required'>*</span>"),t+="</p>";for(var m=0;m<u.options.length;m++){var d=`${e}-option-${o}-${m}`,S=`${e}-response-${o}`,v=`${e}-response-${o}-${m}`,O=u.required?"required":"";t+=`
|
|
10
|
+
<div id="${d}" class="${e}-option">
|
|
11
|
+
<label class="${e}-text" for="${v}">
|
|
12
|
+
<input type="radio" name="${S}" id="${v}" value="${u.options[m]}" data-option-index="${m}" ${O} />
|
|
13
|
+
${u.options[m]}
|
|
14
14
|
</label>
|
|
15
|
-
</div>`}t+="</div>"}t+=`<input type="submit" id="${e}-next" class="${e} jspsych-btn"${
|
|
16
|
-
//# sourceMappingURL=https://unpkg.com/@jspsych/plugin-survey-multi-choice@2.
|
|
15
|
+
</div>`}t+="</div>"}t+=`<input type="submit" id="${e}-next" class="${e} jspsych-btn"${a.button_label?' value="'+a.button_label+'"':""} />`,t+="</form>",r.innerHTML=t,r.querySelector(`#${s}`).addEventListener("submit",I=>{I.preventDefault();for(var L=performance.now(),z=Math.round(L-j),P={},b=[],c=0;c<a.questions.length;c++){var y=r.querySelector(`#${e}-${c}`),E="Q"+c,h="",g=-1,$=y.querySelector("input[type=radio]:checked");$!==null?(h=$.value,g=Number($.dataset.optionIndex)):h="";var _={},q=E;y.attributes["data-name"].value!==""&&(q=y.attributes["data-name"].value),_[q]=h,Object.assign(P,_),b.push(g)}var A={rt:z,response:P,response_index:b,question_order:l};this.jsPsych.finishTrial(A)});var j=performance.now()}simulate(r,a,s,t){a=="data-only"&&(t(),this.simulate_data_only(r,s)),a=="visual"&&this.simulate_visual(r,s,t)}create_simulation_data(r,a){const s={},t=[];let l=1e3;for(let o=0;o<r.questions.length;o++){const p=r.questions[o],m=p.name?p.name:`Q${o}`,d=this.jsPsych.randomization.randomInt(0,p.options.length-1);s[m]=p.options[d],t.push(d),l+=this.jsPsych.randomization.sampleExGaussian(1500,400,.005,!0)}const n={response:s,response_index:t,rt:l,question_order:r.randomize_question_order?this.jsPsych.randomization.shuffle([...Array(r.questions.length).keys()]):[...Array(r.questions.length).keys()]},u=this.jsPsych.pluginAPI.mergeSimulationData(n,a);return this.jsPsych.pluginAPI.ensureSimulationDataConsistency(r,u),u}simulate_data_only(r,a){const s=this.create_simulation_data(r,a);this.jsPsych.finishTrial(s)}simulate_visual(r,a,s){const t=this.create_simulation_data(r,a),l=this.jsPsych.getDisplayElement();this.trial(l,r),s();const n=Object.entries(t.response),u=Array.isArray(t.response_index)?t.response_index:[];for(let o=0;o<n.length;o++){let p=u[o];(typeof p!="number"||p<0)&&(p=r.questions[o].options.indexOf(n[o][1])),!(p<0)&&this.jsPsych.pluginAPI.clickTarget(l.querySelector(`#${e}-response-${o}-${p}`),(t.rt-1e3)/n.length*(o+1))}this.jsPsych.pluginAPI.clickTarget(l.querySelector(`#${e}-next`),t.rt)}}return f.info=x,f}(jsPsychModule);
|
|
16
|
+
//# sourceMappingURL=https://unpkg.com/@jspsych/plugin-survey-multi-choice@2.2.0/dist/index.browser.min.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.browser.min.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-survey-multi-choice\",\n \"version\": \"2.1.0\",\n \"description\": \"a jspsych plugin for multiple choice survey questions\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-survey-multi-choice\"\n },\n \"author\": \"Shane Martin\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/survey-multi-choice\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"survey-multi-choice\",\n version: version,\n parameters: {\n /**\n * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt,\n * options, required, and horizontal parameter that will be applied to the question. See examples below for further\n * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be\n * associated with a group of options (radio buttons). All questions will get presented on the same page (trial).\n * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to\n * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates\n * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is\n * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the\n * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing\n * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.\n */\n questions: {\n type: ParameterType.COMPLEX,\n array: true,\n nested: {\n /** Question prompt. */\n prompt: {\n type: ParameterType.HTML_STRING,\n default: undefined,\n },\n /** Array of multiple choice options for this question. */\n options: {\n type: ParameterType.STRING,\n array: true,\n default: undefined,\n },\n /** Whether or not a response to this question must be given in order to continue. */\n required: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** If true, then the question will be centered and options will be displayed horizontally. */\n horizontal: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */\n name: {\n type: ParameterType.STRING,\n default: \"\",\n },\n },\n },\n /**\n * If true, the display order of `questions` is randomly determined at the start of the trial. In the data object,\n * `Q0` will still refer to the first question in the array, regardless of where it was presented visually.\n */\n randomize_question_order: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** HTML formatted string to display at the top of the page above all the questions. */\n preamble: {\n type: ParameterType.HTML_STRING,\n default: null,\n },\n /** Label of the button. */\n button_label: {\n type: ParameterType.STRING,\n default: \"Continue\",\n },\n /**\n * This determines whether or not all of the input elements on the page should allow autocomplete. Setting\n * this to true will enable autocomplete or auto-fill for the form.\n */\n autocomplete: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n response: {\n type: ParameterType.OBJECT,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */\n rt: {\n type: ParameterType.INT,\n },\n /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n question_order: {\n type: ParameterType.INT,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\nconst plugin_id_name = \"jspsych-survey-multi-choice\";\n\n/**\n * **survey-multi-choice**\n *\n * The survey-multi-choice plugin displays a set of questions with multiple choice response fields. The participant selects a single answer.\n *\n * @author Shane Martin\n * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org}\n */\nclass SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) { }\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n\n const trial_form_id = `${plugin_id_name}_form`;\n\n var html = \"\";\n\n // inject CSS for trial\n html += `\n <style id=\"${plugin_id_name}-css\">\n .${plugin_id_name}-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }\n .${plugin_id_name}-text span.required {color: darkred;}\n .${plugin_id_name}-horizontal .${plugin_id_name}-text { text-align: center;}\n .${plugin_id_name}-option { line-height: 2; }\n .${plugin_id_name}-horizontal .${plugin_id_name}-option { display: inline-block; margin-left: 1em; margin-right: 1em; vertical-align: top;}\n label.${plugin_id_name}-text input[type='radio'] {margin-right: 1em;}\n </style>`;\n\n // show preamble text\n if (trial.preamble !== null) {\n html += `<div id=\"${plugin_id_name}-preamble\" class=\"${plugin_id_name}-preamble\">${trial.preamble}</div>`;\n }\n\n // form element\n if (trial.autocomplete) {\n html += `<form id=\"${trial_form_id}\">`;\n } else {\n html += `<form id=\"${trial_form_id}\" autocomplete=\"off\">`;\n }\n\n // generate question order. this is randomized here as opposed to randomizing the order of trial.questions\n // so that the data are always associated with the same question regardless of order\n var question_order = [];\n for (var i = 0; i < trial.questions.length; i++) {\n question_order.push(i);\n }\n if (trial.randomize_question_order) {\n question_order = this.jsPsych.randomization.shuffle(question_order);\n }\n\n // add multiple-choice questions\n for (var i = 0; i < trial.questions.length; i++) {\n // get question based on question_order\n var question = trial.questions[question_order[i]];\n var question_id = question_order[i];\n\n // create question container\n var question_classes = [`${plugin_id_name}-question`];\n if (question.horizontal) {\n question_classes.push(`${plugin_id_name}-horizontal`);\n }\n\n html += `<div id=\"${plugin_id_name}-${question_id}\" class=\"${question_classes.join(\" \")}\" data-name=\"${question.name}\">`;\n\n // add question text\n html += `<p class=\"${plugin_id_name}-text survey-multi-choice\">${question.prompt}`;\n if (question.required) {\n html += \"<span class='required'>*</span>\";\n }\n html += \"</p>\";\n\n // create option radio buttons\n for (var j = 0; j < question.options.length; j++) {\n // add label and question text\n var option_id_name = `${plugin_id_name}-option-${question_id}-${j}`;\n var input_name = `${plugin_id_name}-response-${question_id}`;\n var input_id = `${plugin_id_name}-response-${question_id}-${j}`;\n\n var required_attr = question.required ? \"required\" : \"\";\n\n // add radio button container\n html += `\n <div id=\"${option_id_name}\" class=\"${plugin_id_name}-option\">\n <label class=\"${plugin_id_name}-text\" for=\"${input_id}\">\n <input type=\"radio\" name=\"${input_name}\" id=\"${input_id}\" value=\"${question.options[j]}\" ${required_attr} />\n ${question.options[j]}\n </label>\n </div>`;\n }\n\n html += \"</div>\";\n }\n\n // add submit button\n html += `<input type=\"submit\" id=\"${plugin_id_name}-next\" class=\"${plugin_id_name} jspsych-btn\"${trial.button_label ? ' value=\"' + trial.button_label + '\"' : \"\"} />`;\n html += \"</form>\";\n\n // render\n display_element.innerHTML = html;\n\n const trial_form = display_element.querySelector<HTMLFormElement>(`#${trial_form_id}`);\n\n trial_form.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n // measure response time\n var endTime = performance.now();\n var response_time = Math.round(endTime - startTime);\n\n // create object to hold responses\n var question_data = {};\n for (var i = 0; i < trial.questions.length; i++) {\n var match = display_element.querySelector(`#${plugin_id_name}-${i}`);\n var id = \"Q\" + i;\n var val: String;\n if (match.querySelector(\"input[type=radio]:checked\") !== null) {\n val = match.querySelector<HTMLInputElement>(\"input[type=radio]:checked\").value;\n } else {\n val = \"\";\n }\n var obje = {};\n var name = id;\n if (match.attributes[\"data-name\"].value !== \"\") {\n name = match.attributes[\"data-name\"].value;\n }\n obje[name] = val;\n Object.assign(question_data, obje);\n }\n // save data\n var trial_data = {\n rt: response_time,\n response: question_data,\n question_order: question_order,\n };\n\n // next trial\n this.jsPsych.finishTrial(trial_data);\n });\n\n var startTime = performance.now();\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const question_data = {};\n let rt = 1000;\n\n for (const q of trial.questions) {\n const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`;\n question_data[name] = this.jsPsych.randomization.sampleWithoutReplacement(q.options, 1)[0];\n rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);\n }\n\n const default_data = {\n response: question_data,\n rt: rt,\n question_order: trial.randomize_question_order\n ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()])\n : [...Array(trial.questions.length).keys()],\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const answers = Object.entries(data.response);\n for (let i = 0; i < answers.length; i++) {\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(\n `#${plugin_id_name}-response-${i}-${trial.questions[i].options.indexOf(\n answers[i][1]\n )}`\n ),\n ((data.rt - 1000) / answers.length) * (i + 1)\n );\n }\n\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-next`),\n data.rt\n );\n }\n}\n\nexport default SurveyMultiChoicePlugin;\n"],"names":["version"],"mappings":"sDAEEA,IAAAA,EAAW,+uBC6FA,UAAA,iuBAAe;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"index.browser.min.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-survey-multi-choice\",\n \"version\": \"2.2.0\",\n \"description\": \"a jspsych plugin for multiple choice survey questions\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-survey-multi-choice\"\n },\n \"author\": \"Shane Martin\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/survey-multi-choice\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"survey-multi-choice\",\n version: version,\n parameters: {\n /**\n * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt,\n * options, required, and horizontal parameter that will be applied to the question. See examples below for further\n * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be\n * associated with a group of options (radio buttons). All questions will get presented on the same page (trial).\n * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to\n * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates\n * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is\n * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the\n * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing\n * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.\n */\n questions: {\n type: ParameterType.COMPLEX,\n array: true,\n nested: {\n /** Question prompt. */\n prompt: {\n type: ParameterType.HTML_STRING,\n default: undefined,\n },\n /** Array of multiple choice options for this question. */\n options: {\n type: ParameterType.STRING,\n array: true,\n default: undefined,\n },\n /** Whether or not a response to this question must be given in order to continue. */\n required: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** If true, then the question will be centered and options will be displayed horizontally. */\n horizontal: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */\n name: {\n type: ParameterType.STRING,\n default: \"\",\n },\n },\n },\n /**\n * If true, the display order of `questions` is randomly determined at the start of the trial. In the data object,\n * `Q0` will still refer to the first question in the array, regardless of where it was presented visually.\n */\n randomize_question_order: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** HTML formatted string to display at the top of the page above all the questions. */\n preamble: {\n type: ParameterType.HTML_STRING,\n default: null,\n },\n /** Label of the button. */\n button_label: {\n type: ParameterType.STRING,\n default: \"Continue\",\n },\n /**\n * This determines whether or not all of the input elements on the page should allow autocomplete. Setting\n * this to true will enable autocomplete or auto-fill for the form.\n */\n autocomplete: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n response: {\n type: ParameterType.OBJECT,\n },\n /** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */\n response_index: {\n type: ParameterType.INT,\n array: true,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */\n rt: {\n type: ParameterType.INT,\n },\n /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n question_order: {\n type: ParameterType.INT,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\nconst plugin_id_name = \"jspsych-survey-multi-choice\";\n\n/**\n * **survey-multi-choice**\n *\n * The survey-multi-choice plugin displays a set of questions with multiple choice response fields. The participant selects a single answer.\n *\n * @author Shane Martin\n * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org}\n */\nclass SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n const trial_form_id = `${plugin_id_name}_form`;\n\n var html = \"\";\n\n // inject CSS for trial\n html += `\n <style id=\"${plugin_id_name}-css\">\n .${plugin_id_name}-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }\n .${plugin_id_name}-text span.required {color: darkred;}\n .${plugin_id_name}-horizontal .${plugin_id_name}-text { text-align: center;}\n .${plugin_id_name}-option { line-height: 2; }\n .${plugin_id_name}-horizontal .${plugin_id_name}-option { display: inline-block; margin-left: 1em; margin-right: 1em; vertical-align: top;}\n label.${plugin_id_name}-text input[type='radio'] {margin-right: 1em;}\n </style>`;\n\n // show preamble text\n if (trial.preamble !== null) {\n html += `<div id=\"${plugin_id_name}-preamble\" class=\"${plugin_id_name}-preamble\">${trial.preamble}</div>`;\n }\n\n // form element\n if (trial.autocomplete) {\n html += `<form id=\"${trial_form_id}\">`;\n } else {\n html += `<form id=\"${trial_form_id}\" autocomplete=\"off\">`;\n }\n\n // generate question order. this is randomized here as opposed to randomizing the order of trial.questions\n // so that the data are always associated with the same question regardless of order\n var question_order = [];\n for (var i = 0; i < trial.questions.length; i++) {\n question_order.push(i);\n }\n if (trial.randomize_question_order) {\n question_order = this.jsPsych.randomization.shuffle(question_order);\n }\n\n // add multiple-choice questions\n for (var i = 0; i < trial.questions.length; i++) {\n // get question based on question_order\n var question = trial.questions[question_order[i]];\n var question_id = question_order[i];\n\n // create question container\n var question_classes = [`${plugin_id_name}-question`];\n if (question.horizontal) {\n question_classes.push(`${plugin_id_name}-horizontal`);\n }\n\n html += `<div id=\"${plugin_id_name}-${question_id}\" class=\"${question_classes.join(\n \" \"\n )}\" data-name=\"${question.name}\">`;\n\n // add question text\n html += `<p class=\"${plugin_id_name}-text survey-multi-choice\">${question.prompt}`;\n if (question.required) {\n html += \"<span class='required'>*</span>\";\n }\n html += \"</p>\";\n\n // create option radio buttons\n for (var j = 0; j < question.options.length; j++) {\n // add label and question text\n var option_id_name = `${plugin_id_name}-option-${question_id}-${j}`;\n var input_name = `${plugin_id_name}-response-${question_id}`;\n var input_id = `${plugin_id_name}-response-${question_id}-${j}`;\n\n var required_attr = question.required ? \"required\" : \"\";\n\n // add radio button container\n html += `\n <div id=\"${option_id_name}\" class=\"${plugin_id_name}-option\">\n <label class=\"${plugin_id_name}-text\" for=\"${input_id}\">\n <input type=\"radio\" name=\"${input_name}\" id=\"${input_id}\" value=\"${question.options[j]}\" data-option-index=\"${j}\" ${required_attr} />\n ${question.options[j]}\n </label>\n </div>`;\n }\n\n html += \"</div>\";\n }\n\n // add submit button\n html += `<input type=\"submit\" id=\"${plugin_id_name}-next\" class=\"${plugin_id_name} jspsych-btn\"${\n trial.button_label ? ' value=\"' + trial.button_label + '\"' : \"\"\n } />`;\n html += \"</form>\";\n\n // render\n display_element.innerHTML = html;\n\n const trial_form = display_element.querySelector<HTMLFormElement>(`#${trial_form_id}`);\n\n trial_form.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n // measure response time\n var endTime = performance.now();\n var response_time = Math.round(endTime - startTime);\n\n // create object to hold responses\n var question_data = {};\n var response_index = [];\n for (var i = 0; i < trial.questions.length; i++) {\n var match = display_element.querySelector(`#${plugin_id_name}-${i}`);\n var id = \"Q\" + i;\n var val: String = \"\";\n var selected_index = -1;\n var checked = match.querySelector<HTMLInputElement>(\"input[type=radio]:checked\");\n if (checked !== null) {\n val = checked.value;\n selected_index = Number(checked.dataset.optionIndex);\n } else {\n val = \"\";\n }\n var obje = {};\n var name = id;\n if (match.attributes[\"data-name\"].value !== \"\") {\n name = match.attributes[\"data-name\"].value;\n }\n obje[name] = val;\n Object.assign(question_data, obje);\n response_index.push(selected_index);\n }\n // save data\n var trial_data = {\n rt: response_time,\n response: question_data,\n response_index: response_index,\n question_order: question_order,\n };\n\n // next trial\n this.jsPsych.finishTrial(trial_data);\n });\n\n var startTime = performance.now();\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const question_data = {};\n const response_index = [];\n let rt = 1000;\n\n for (let i = 0; i < trial.questions.length; i++) {\n const q = trial.questions[i];\n const name = q.name ? q.name : `Q${i}`;\n const option_index = this.jsPsych.randomization.randomInt(0, q.options.length - 1);\n question_data[name] = q.options[option_index];\n response_index.push(option_index);\n rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);\n }\n\n const default_data = {\n response: question_data,\n response_index: response_index,\n rt: rt,\n question_order: trial.randomize_question_order\n ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()])\n : [...Array(trial.questions.length).keys()],\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const answers = Object.entries(data.response);\n const response_index = Array.isArray(data.response_index) ? data.response_index : [];\n for (let i = 0; i < answers.length; i++) {\n let option_index = response_index[i];\n if (typeof option_index !== \"number\" || option_index < 0) {\n option_index = trial.questions[i].options.indexOf(answers[i][1]);\n }\n if (option_index < 0) {\n continue;\n }\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-response-${i}-${option_index}`),\n ((data.rt - 1000) / answers.length) * (i + 1)\n );\n }\n\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-next`),\n data.rt\n );\n }\n}\n\nexport default SurveyMultiChoicePlugin;\n"],"names":["version"],"mappings":"sDAEEA,IAAAA,EAAW,kyBCkGA,UAAA,iuBAAe;;;;;;;;;;;;;;"}
|
package/dist/index.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var jspsych = require('jspsych');
|
|
4
4
|
|
|
5
|
-
var version = "2.
|
|
5
|
+
var version = "2.2.0";
|
|
6
6
|
|
|
7
7
|
const info = {
|
|
8
8
|
name: "survey-multi-choice",
|
|
@@ -84,6 +84,11 @@ const info = {
|
|
|
84
84
|
response: {
|
|
85
85
|
type: jspsych.ParameterType.OBJECT
|
|
86
86
|
},
|
|
87
|
+
/** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */
|
|
88
|
+
response_index: {
|
|
89
|
+
type: jspsych.ParameterType.INT,
|
|
90
|
+
array: true
|
|
91
|
+
},
|
|
87
92
|
/** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */
|
|
88
93
|
rt: {
|
|
89
94
|
type: jspsych.ParameterType.INT
|
|
@@ -142,7 +147,9 @@ class SurveyMultiChoicePlugin {
|
|
|
142
147
|
if (question.horizontal) {
|
|
143
148
|
question_classes.push(`${plugin_id_name}-horizontal`);
|
|
144
149
|
}
|
|
145
|
-
html += `<div id="${plugin_id_name}-${question_id}" class="${question_classes.join(
|
|
150
|
+
html += `<div id="${plugin_id_name}-${question_id}" class="${question_classes.join(
|
|
151
|
+
" "
|
|
152
|
+
)}" data-name="${question.name}">`;
|
|
146
153
|
html += `<p class="${plugin_id_name}-text survey-multi-choice">${question.prompt}`;
|
|
147
154
|
if (question.required) {
|
|
148
155
|
html += "<span class='required'>*</span>";
|
|
@@ -156,7 +163,7 @@ class SurveyMultiChoicePlugin {
|
|
|
156
163
|
html += `
|
|
157
164
|
<div id="${option_id_name}" class="${plugin_id_name}-option">
|
|
158
165
|
<label class="${plugin_id_name}-text" for="${input_id}">
|
|
159
|
-
<input type="radio" name="${input_name}" id="${input_id}" value="${question.options[j]}" ${required_attr} />
|
|
166
|
+
<input type="radio" name="${input_name}" id="${input_id}" value="${question.options[j]}" data-option-index="${j}" ${required_attr} />
|
|
160
167
|
${question.options[j]}
|
|
161
168
|
</label>
|
|
162
169
|
</div>`;
|
|
@@ -172,12 +179,16 @@ class SurveyMultiChoicePlugin {
|
|
|
172
179
|
var endTime = performance.now();
|
|
173
180
|
var response_time = Math.round(endTime - startTime);
|
|
174
181
|
var question_data = {};
|
|
182
|
+
var response_index = [];
|
|
175
183
|
for (var i2 = 0; i2 < trial.questions.length; i2++) {
|
|
176
184
|
var match = display_element.querySelector(`#${plugin_id_name}-${i2}`);
|
|
177
185
|
var id = "Q" + i2;
|
|
178
|
-
var val;
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
var val = "";
|
|
187
|
+
var selected_index = -1;
|
|
188
|
+
var checked = match.querySelector("input[type=radio]:checked");
|
|
189
|
+
if (checked !== null) {
|
|
190
|
+
val = checked.value;
|
|
191
|
+
selected_index = Number(checked.dataset.optionIndex);
|
|
181
192
|
} else {
|
|
182
193
|
val = "";
|
|
183
194
|
}
|
|
@@ -188,10 +199,12 @@ class SurveyMultiChoicePlugin {
|
|
|
188
199
|
}
|
|
189
200
|
obje[name] = val;
|
|
190
201
|
Object.assign(question_data, obje);
|
|
202
|
+
response_index.push(selected_index);
|
|
191
203
|
}
|
|
192
204
|
var trial_data = {
|
|
193
205
|
rt: response_time,
|
|
194
206
|
response: question_data,
|
|
207
|
+
response_index,
|
|
195
208
|
question_order
|
|
196
209
|
};
|
|
197
210
|
this.jsPsych.finishTrial(trial_data);
|
|
@@ -209,14 +222,19 @@ class SurveyMultiChoicePlugin {
|
|
|
209
222
|
}
|
|
210
223
|
create_simulation_data(trial, simulation_options) {
|
|
211
224
|
const question_data = {};
|
|
225
|
+
const response_index = [];
|
|
212
226
|
let rt = 1e3;
|
|
213
|
-
for (
|
|
214
|
-
const
|
|
215
|
-
|
|
227
|
+
for (let i = 0; i < trial.questions.length; i++) {
|
|
228
|
+
const q = trial.questions[i];
|
|
229
|
+
const name = q.name ? q.name : `Q${i}`;
|
|
230
|
+
const option_index = this.jsPsych.randomization.randomInt(0, q.options.length - 1);
|
|
231
|
+
question_data[name] = q.options[option_index];
|
|
232
|
+
response_index.push(option_index);
|
|
216
233
|
rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);
|
|
217
234
|
}
|
|
218
235
|
const default_data = {
|
|
219
236
|
response: question_data,
|
|
237
|
+
response_index,
|
|
220
238
|
rt,
|
|
221
239
|
question_order: trial.randomize_question_order ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()]) : [...Array(trial.questions.length).keys()]
|
|
222
240
|
};
|
|
@@ -234,13 +252,17 @@ class SurveyMultiChoicePlugin {
|
|
|
234
252
|
this.trial(display_element, trial);
|
|
235
253
|
load_callback();
|
|
236
254
|
const answers = Object.entries(data.response);
|
|
255
|
+
const response_index = Array.isArray(data.response_index) ? data.response_index : [];
|
|
237
256
|
for (let i = 0; i < answers.length; i++) {
|
|
257
|
+
let option_index = response_index[i];
|
|
258
|
+
if (typeof option_index !== "number" || option_index < 0) {
|
|
259
|
+
option_index = trial.questions[i].options.indexOf(answers[i][1]);
|
|
260
|
+
}
|
|
261
|
+
if (option_index < 0) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
238
264
|
this.jsPsych.pluginAPI.clickTarget(
|
|
239
|
-
display_element.querySelector(
|
|
240
|
-
`#${plugin_id_name}-response-${i}-${trial.questions[i].options.indexOf(
|
|
241
|
-
answers[i][1]
|
|
242
|
-
)}`
|
|
243
|
-
),
|
|
265
|
+
display_element.querySelector(`#${plugin_id_name}-response-${i}-${option_index}`),
|
|
244
266
|
(data.rt - 1e3) / answers.length * (i + 1)
|
|
245
267
|
);
|
|
246
268
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-survey-multi-choice\",\n \"version\": \"2.1.0\",\n \"description\": \"a jspsych plugin for multiple choice survey questions\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-survey-multi-choice\"\n },\n \"author\": \"Shane Martin\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/survey-multi-choice\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"survey-multi-choice\",\n version: version,\n parameters: {\n /**\n * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt,\n * options, required, and horizontal parameter that will be applied to the question. See examples below for further\n * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be\n * associated with a group of options (radio buttons). All questions will get presented on the same page (trial).\n * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to\n * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates\n * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is\n * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the\n * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing\n * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.\n */\n questions: {\n type: ParameterType.COMPLEX,\n array: true,\n nested: {\n /** Question prompt. */\n prompt: {\n type: ParameterType.HTML_STRING,\n default: undefined,\n },\n /** Array of multiple choice options for this question. */\n options: {\n type: ParameterType.STRING,\n array: true,\n default: undefined,\n },\n /** Whether or not a response to this question must be given in order to continue. */\n required: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** If true, then the question will be centered and options will be displayed horizontally. */\n horizontal: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */\n name: {\n type: ParameterType.STRING,\n default: \"\",\n },\n },\n },\n /**\n * If true, the display order of `questions` is randomly determined at the start of the trial. In the data object,\n * `Q0` will still refer to the first question in the array, regardless of where it was presented visually.\n */\n randomize_question_order: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** HTML formatted string to display at the top of the page above all the questions. */\n preamble: {\n type: ParameterType.HTML_STRING,\n default: null,\n },\n /** Label of the button. */\n button_label: {\n type: ParameterType.STRING,\n default: \"Continue\",\n },\n /**\n * This determines whether or not all of the input elements on the page should allow autocomplete. Setting\n * this to true will enable autocomplete or auto-fill for the form.\n */\n autocomplete: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n response: {\n type: ParameterType.OBJECT,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */\n rt: {\n type: ParameterType.INT,\n },\n /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n question_order: {\n type: ParameterType.INT,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\nconst plugin_id_name = \"jspsych-survey-multi-choice\";\n\n/**\n * **survey-multi-choice**\n *\n * The survey-multi-choice plugin displays a set of questions with multiple choice response fields. The participant selects a single answer.\n *\n * @author Shane Martin\n * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org}\n */\nclass SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) { }\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n\n const trial_form_id = `${plugin_id_name}_form`;\n\n var html = \"\";\n\n // inject CSS for trial\n html += `\n <style id=\"${plugin_id_name}-css\">\n .${plugin_id_name}-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }\n .${plugin_id_name}-text span.required {color: darkred;}\n .${plugin_id_name}-horizontal .${plugin_id_name}-text { text-align: center;}\n .${plugin_id_name}-option { line-height: 2; }\n .${plugin_id_name}-horizontal .${plugin_id_name}-option { display: inline-block; margin-left: 1em; margin-right: 1em; vertical-align: top;}\n label.${plugin_id_name}-text input[type='radio'] {margin-right: 1em;}\n </style>`;\n\n // show preamble text\n if (trial.preamble !== null) {\n html += `<div id=\"${plugin_id_name}-preamble\" class=\"${plugin_id_name}-preamble\">${trial.preamble}</div>`;\n }\n\n // form element\n if (trial.autocomplete) {\n html += `<form id=\"${trial_form_id}\">`;\n } else {\n html += `<form id=\"${trial_form_id}\" autocomplete=\"off\">`;\n }\n\n // generate question order. this is randomized here as opposed to randomizing the order of trial.questions\n // so that the data are always associated with the same question regardless of order\n var question_order = [];\n for (var i = 0; i < trial.questions.length; i++) {\n question_order.push(i);\n }\n if (trial.randomize_question_order) {\n question_order = this.jsPsych.randomization.shuffle(question_order);\n }\n\n // add multiple-choice questions\n for (var i = 0; i < trial.questions.length; i++) {\n // get question based on question_order\n var question = trial.questions[question_order[i]];\n var question_id = question_order[i];\n\n // create question container\n var question_classes = [`${plugin_id_name}-question`];\n if (question.horizontal) {\n question_classes.push(`${plugin_id_name}-horizontal`);\n }\n\n html += `<div id=\"${plugin_id_name}-${question_id}\" class=\"${question_classes.join(\" \")}\" data-name=\"${question.name}\">`;\n\n // add question text\n html += `<p class=\"${plugin_id_name}-text survey-multi-choice\">${question.prompt}`;\n if (question.required) {\n html += \"<span class='required'>*</span>\";\n }\n html += \"</p>\";\n\n // create option radio buttons\n for (var j = 0; j < question.options.length; j++) {\n // add label and question text\n var option_id_name = `${plugin_id_name}-option-${question_id}-${j}`;\n var input_name = `${plugin_id_name}-response-${question_id}`;\n var input_id = `${plugin_id_name}-response-${question_id}-${j}`;\n\n var required_attr = question.required ? \"required\" : \"\";\n\n // add radio button container\n html += `\n <div id=\"${option_id_name}\" class=\"${plugin_id_name}-option\">\n <label class=\"${plugin_id_name}-text\" for=\"${input_id}\">\n <input type=\"radio\" name=\"${input_name}\" id=\"${input_id}\" value=\"${question.options[j]}\" ${required_attr} />\n ${question.options[j]}\n </label>\n </div>`;\n }\n\n html += \"</div>\";\n }\n\n // add submit button\n html += `<input type=\"submit\" id=\"${plugin_id_name}-next\" class=\"${plugin_id_name} jspsych-btn\"${trial.button_label ? ' value=\"' + trial.button_label + '\"' : \"\"} />`;\n html += \"</form>\";\n\n // render\n display_element.innerHTML = html;\n\n const trial_form = display_element.querySelector<HTMLFormElement>(`#${trial_form_id}`);\n\n trial_form.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n // measure response time\n var endTime = performance.now();\n var response_time = Math.round(endTime - startTime);\n\n // create object to hold responses\n var question_data = {};\n for (var i = 0; i < trial.questions.length; i++) {\n var match = display_element.querySelector(`#${plugin_id_name}-${i}`);\n var id = \"Q\" + i;\n var val: String;\n if (match.querySelector(\"input[type=radio]:checked\") !== null) {\n val = match.querySelector<HTMLInputElement>(\"input[type=radio]:checked\").value;\n } else {\n val = \"\";\n }\n var obje = {};\n var name = id;\n if (match.attributes[\"data-name\"].value !== \"\") {\n name = match.attributes[\"data-name\"].value;\n }\n obje[name] = val;\n Object.assign(question_data, obje);\n }\n // save data\n var trial_data = {\n rt: response_time,\n response: question_data,\n question_order: question_order,\n };\n\n // next trial\n this.jsPsych.finishTrial(trial_data);\n });\n\n var startTime = performance.now();\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const question_data = {};\n let rt = 1000;\n\n for (const q of trial.questions) {\n const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`;\n question_data[name] = this.jsPsych.randomization.sampleWithoutReplacement(q.options, 1)[0];\n rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);\n }\n\n const default_data = {\n response: question_data,\n rt: rt,\n question_order: trial.randomize_question_order\n ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()])\n : [...Array(trial.questions.length).keys()],\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const answers = Object.entries(data.response);\n for (let i = 0; i < answers.length; i++) {\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(\n `#${plugin_id_name}-response-${i}-${trial.questions[i].options.indexOf(\n answers[i][1]\n )}`\n ),\n ((data.rt - 1000) / answers.length) * (i + 1)\n );\n }\n\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-next`),\n data.rt\n );\n }\n}\n\nexport default SurveyMultiChoicePlugin;\n"],"names":[],"mappings":";;;;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC6FA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-survey-multi-choice\",\n \"version\": \"2.2.0\",\n \"description\": \"a jspsych plugin for multiple choice survey questions\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-survey-multi-choice\"\n },\n \"author\": \"Shane Martin\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/survey-multi-choice\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"survey-multi-choice\",\n version: version,\n parameters: {\n /**\n * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt,\n * options, required, and horizontal parameter that will be applied to the question. See examples below for further\n * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be\n * associated with a group of options (radio buttons). All questions will get presented on the same page (trial).\n * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to\n * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates\n * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is\n * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the\n * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing\n * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.\n */\n questions: {\n type: ParameterType.COMPLEX,\n array: true,\n nested: {\n /** Question prompt. */\n prompt: {\n type: ParameterType.HTML_STRING,\n default: undefined,\n },\n /** Array of multiple choice options for this question. */\n options: {\n type: ParameterType.STRING,\n array: true,\n default: undefined,\n },\n /** Whether or not a response to this question must be given in order to continue. */\n required: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** If true, then the question will be centered and options will be displayed horizontally. */\n horizontal: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */\n name: {\n type: ParameterType.STRING,\n default: \"\",\n },\n },\n },\n /**\n * If true, the display order of `questions` is randomly determined at the start of the trial. In the data object,\n * `Q0` will still refer to the first question in the array, regardless of where it was presented visually.\n */\n randomize_question_order: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** HTML formatted string to display at the top of the page above all the questions. */\n preamble: {\n type: ParameterType.HTML_STRING,\n default: null,\n },\n /** Label of the button. */\n button_label: {\n type: ParameterType.STRING,\n default: \"Continue\",\n },\n /**\n * This determines whether or not all of the input elements on the page should allow autocomplete. Setting\n * this to true will enable autocomplete or auto-fill for the form.\n */\n autocomplete: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n response: {\n type: ParameterType.OBJECT,\n },\n /** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */\n response_index: {\n type: ParameterType.INT,\n array: true,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */\n rt: {\n type: ParameterType.INT,\n },\n /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n question_order: {\n type: ParameterType.INT,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\nconst plugin_id_name = \"jspsych-survey-multi-choice\";\n\n/**\n * **survey-multi-choice**\n *\n * The survey-multi-choice plugin displays a set of questions with multiple choice response fields. The participant selects a single answer.\n *\n * @author Shane Martin\n * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org}\n */\nclass SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n const trial_form_id = `${plugin_id_name}_form`;\n\n var html = \"\";\n\n // inject CSS for trial\n html += `\n <style id=\"${plugin_id_name}-css\">\n .${plugin_id_name}-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }\n .${plugin_id_name}-text span.required {color: darkred;}\n .${plugin_id_name}-horizontal .${plugin_id_name}-text { text-align: center;}\n .${plugin_id_name}-option { line-height: 2; }\n .${plugin_id_name}-horizontal .${plugin_id_name}-option { display: inline-block; margin-left: 1em; margin-right: 1em; vertical-align: top;}\n label.${plugin_id_name}-text input[type='radio'] {margin-right: 1em;}\n </style>`;\n\n // show preamble text\n if (trial.preamble !== null) {\n html += `<div id=\"${plugin_id_name}-preamble\" class=\"${plugin_id_name}-preamble\">${trial.preamble}</div>`;\n }\n\n // form element\n if (trial.autocomplete) {\n html += `<form id=\"${trial_form_id}\">`;\n } else {\n html += `<form id=\"${trial_form_id}\" autocomplete=\"off\">`;\n }\n\n // generate question order. this is randomized here as opposed to randomizing the order of trial.questions\n // so that the data are always associated with the same question regardless of order\n var question_order = [];\n for (var i = 0; i < trial.questions.length; i++) {\n question_order.push(i);\n }\n if (trial.randomize_question_order) {\n question_order = this.jsPsych.randomization.shuffle(question_order);\n }\n\n // add multiple-choice questions\n for (var i = 0; i < trial.questions.length; i++) {\n // get question based on question_order\n var question = trial.questions[question_order[i]];\n var question_id = question_order[i];\n\n // create question container\n var question_classes = [`${plugin_id_name}-question`];\n if (question.horizontal) {\n question_classes.push(`${plugin_id_name}-horizontal`);\n }\n\n html += `<div id=\"${plugin_id_name}-${question_id}\" class=\"${question_classes.join(\n \" \"\n )}\" data-name=\"${question.name}\">`;\n\n // add question text\n html += `<p class=\"${plugin_id_name}-text survey-multi-choice\">${question.prompt}`;\n if (question.required) {\n html += \"<span class='required'>*</span>\";\n }\n html += \"</p>\";\n\n // create option radio buttons\n for (var j = 0; j < question.options.length; j++) {\n // add label and question text\n var option_id_name = `${plugin_id_name}-option-${question_id}-${j}`;\n var input_name = `${plugin_id_name}-response-${question_id}`;\n var input_id = `${plugin_id_name}-response-${question_id}-${j}`;\n\n var required_attr = question.required ? \"required\" : \"\";\n\n // add radio button container\n html += `\n <div id=\"${option_id_name}\" class=\"${plugin_id_name}-option\">\n <label class=\"${plugin_id_name}-text\" for=\"${input_id}\">\n <input type=\"radio\" name=\"${input_name}\" id=\"${input_id}\" value=\"${question.options[j]}\" data-option-index=\"${j}\" ${required_attr} />\n ${question.options[j]}\n </label>\n </div>`;\n }\n\n html += \"</div>\";\n }\n\n // add submit button\n html += `<input type=\"submit\" id=\"${plugin_id_name}-next\" class=\"${plugin_id_name} jspsych-btn\"${\n trial.button_label ? ' value=\"' + trial.button_label + '\"' : \"\"\n } />`;\n html += \"</form>\";\n\n // render\n display_element.innerHTML = html;\n\n const trial_form = display_element.querySelector<HTMLFormElement>(`#${trial_form_id}`);\n\n trial_form.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n // measure response time\n var endTime = performance.now();\n var response_time = Math.round(endTime - startTime);\n\n // create object to hold responses\n var question_data = {};\n var response_index = [];\n for (var i = 0; i < trial.questions.length; i++) {\n var match = display_element.querySelector(`#${plugin_id_name}-${i}`);\n var id = \"Q\" + i;\n var val: String = \"\";\n var selected_index = -1;\n var checked = match.querySelector<HTMLInputElement>(\"input[type=radio]:checked\");\n if (checked !== null) {\n val = checked.value;\n selected_index = Number(checked.dataset.optionIndex);\n } else {\n val = \"\";\n }\n var obje = {};\n var name = id;\n if (match.attributes[\"data-name\"].value !== \"\") {\n name = match.attributes[\"data-name\"].value;\n }\n obje[name] = val;\n Object.assign(question_data, obje);\n response_index.push(selected_index);\n }\n // save data\n var trial_data = {\n rt: response_time,\n response: question_data,\n response_index: response_index,\n question_order: question_order,\n };\n\n // next trial\n this.jsPsych.finishTrial(trial_data);\n });\n\n var startTime = performance.now();\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const question_data = {};\n const response_index = [];\n let rt = 1000;\n\n for (let i = 0; i < trial.questions.length; i++) {\n const q = trial.questions[i];\n const name = q.name ? q.name : `Q${i}`;\n const option_index = this.jsPsych.randomization.randomInt(0, q.options.length - 1);\n question_data[name] = q.options[option_index];\n response_index.push(option_index);\n rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);\n }\n\n const default_data = {\n response: question_data,\n response_index: response_index,\n rt: rt,\n question_order: trial.randomize_question_order\n ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()])\n : [...Array(trial.questions.length).keys()],\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const answers = Object.entries(data.response);\n const response_index = Array.isArray(data.response_index) ? data.response_index : [];\n for (let i = 0; i < answers.length; i++) {\n let option_index = response_index[i];\n if (typeof option_index !== \"number\" || option_index < 0) {\n option_index = trial.questions[i].options.indexOf(answers[i][1]);\n }\n if (option_index < 0) {\n continue;\n }\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-response-${i}-${option_index}`),\n ((data.rt - 1000) / answers.length) * (i + 1)\n );\n }\n\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-next`),\n data.rt\n );\n }\n}\n\nexport default SurveyMultiChoicePlugin;\n"],"names":[],"mappings":";;;;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECkGA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,11 @@ declare const info: {
|
|
|
80
80
|
readonly response: {
|
|
81
81
|
readonly type: ParameterType.OBJECT;
|
|
82
82
|
};
|
|
83
|
+
/** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */
|
|
84
|
+
readonly response_index: {
|
|
85
|
+
readonly type: ParameterType.INT;
|
|
86
|
+
readonly array: true;
|
|
87
|
+
};
|
|
83
88
|
/** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */
|
|
84
89
|
readonly rt: {
|
|
85
90
|
readonly type: ParameterType.INT;
|
|
@@ -183,6 +188,11 @@ declare class SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {
|
|
|
183
188
|
readonly response: {
|
|
184
189
|
readonly type: ParameterType.OBJECT;
|
|
185
190
|
};
|
|
191
|
+
/** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */
|
|
192
|
+
readonly response_index: {
|
|
193
|
+
readonly type: ParameterType.INT;
|
|
194
|
+
readonly array: true;
|
|
195
|
+
};
|
|
186
196
|
/** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */
|
|
187
197
|
readonly rt: {
|
|
188
198
|
readonly type: ParameterType.INT;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ParameterType } from 'jspsych';
|
|
2
2
|
|
|
3
|
-
var version = "2.
|
|
3
|
+
var version = "2.2.0";
|
|
4
4
|
|
|
5
5
|
const info = {
|
|
6
6
|
name: "survey-multi-choice",
|
|
@@ -82,6 +82,11 @@ const info = {
|
|
|
82
82
|
response: {
|
|
83
83
|
type: ParameterType.OBJECT
|
|
84
84
|
},
|
|
85
|
+
/** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */
|
|
86
|
+
response_index: {
|
|
87
|
+
type: ParameterType.INT,
|
|
88
|
+
array: true
|
|
89
|
+
},
|
|
85
90
|
/** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */
|
|
86
91
|
rt: {
|
|
87
92
|
type: ParameterType.INT
|
|
@@ -140,7 +145,9 @@ class SurveyMultiChoicePlugin {
|
|
|
140
145
|
if (question.horizontal) {
|
|
141
146
|
question_classes.push(`${plugin_id_name}-horizontal`);
|
|
142
147
|
}
|
|
143
|
-
html += `<div id="${plugin_id_name}-${question_id}" class="${question_classes.join(
|
|
148
|
+
html += `<div id="${plugin_id_name}-${question_id}" class="${question_classes.join(
|
|
149
|
+
" "
|
|
150
|
+
)}" data-name="${question.name}">`;
|
|
144
151
|
html += `<p class="${plugin_id_name}-text survey-multi-choice">${question.prompt}`;
|
|
145
152
|
if (question.required) {
|
|
146
153
|
html += "<span class='required'>*</span>";
|
|
@@ -154,7 +161,7 @@ class SurveyMultiChoicePlugin {
|
|
|
154
161
|
html += `
|
|
155
162
|
<div id="${option_id_name}" class="${plugin_id_name}-option">
|
|
156
163
|
<label class="${plugin_id_name}-text" for="${input_id}">
|
|
157
|
-
<input type="radio" name="${input_name}" id="${input_id}" value="${question.options[j]}" ${required_attr} />
|
|
164
|
+
<input type="radio" name="${input_name}" id="${input_id}" value="${question.options[j]}" data-option-index="${j}" ${required_attr} />
|
|
158
165
|
${question.options[j]}
|
|
159
166
|
</label>
|
|
160
167
|
</div>`;
|
|
@@ -170,12 +177,16 @@ class SurveyMultiChoicePlugin {
|
|
|
170
177
|
var endTime = performance.now();
|
|
171
178
|
var response_time = Math.round(endTime - startTime);
|
|
172
179
|
var question_data = {};
|
|
180
|
+
var response_index = [];
|
|
173
181
|
for (var i2 = 0; i2 < trial.questions.length; i2++) {
|
|
174
182
|
var match = display_element.querySelector(`#${plugin_id_name}-${i2}`);
|
|
175
183
|
var id = "Q" + i2;
|
|
176
|
-
var val;
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
var val = "";
|
|
185
|
+
var selected_index = -1;
|
|
186
|
+
var checked = match.querySelector("input[type=radio]:checked");
|
|
187
|
+
if (checked !== null) {
|
|
188
|
+
val = checked.value;
|
|
189
|
+
selected_index = Number(checked.dataset.optionIndex);
|
|
179
190
|
} else {
|
|
180
191
|
val = "";
|
|
181
192
|
}
|
|
@@ -186,10 +197,12 @@ class SurveyMultiChoicePlugin {
|
|
|
186
197
|
}
|
|
187
198
|
obje[name] = val;
|
|
188
199
|
Object.assign(question_data, obje);
|
|
200
|
+
response_index.push(selected_index);
|
|
189
201
|
}
|
|
190
202
|
var trial_data = {
|
|
191
203
|
rt: response_time,
|
|
192
204
|
response: question_data,
|
|
205
|
+
response_index,
|
|
193
206
|
question_order
|
|
194
207
|
};
|
|
195
208
|
this.jsPsych.finishTrial(trial_data);
|
|
@@ -207,14 +220,19 @@ class SurveyMultiChoicePlugin {
|
|
|
207
220
|
}
|
|
208
221
|
create_simulation_data(trial, simulation_options) {
|
|
209
222
|
const question_data = {};
|
|
223
|
+
const response_index = [];
|
|
210
224
|
let rt = 1e3;
|
|
211
|
-
for (
|
|
212
|
-
const
|
|
213
|
-
|
|
225
|
+
for (let i = 0; i < trial.questions.length; i++) {
|
|
226
|
+
const q = trial.questions[i];
|
|
227
|
+
const name = q.name ? q.name : `Q${i}`;
|
|
228
|
+
const option_index = this.jsPsych.randomization.randomInt(0, q.options.length - 1);
|
|
229
|
+
question_data[name] = q.options[option_index];
|
|
230
|
+
response_index.push(option_index);
|
|
214
231
|
rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);
|
|
215
232
|
}
|
|
216
233
|
const default_data = {
|
|
217
234
|
response: question_data,
|
|
235
|
+
response_index,
|
|
218
236
|
rt,
|
|
219
237
|
question_order: trial.randomize_question_order ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()]) : [...Array(trial.questions.length).keys()]
|
|
220
238
|
};
|
|
@@ -232,13 +250,17 @@ class SurveyMultiChoicePlugin {
|
|
|
232
250
|
this.trial(display_element, trial);
|
|
233
251
|
load_callback();
|
|
234
252
|
const answers = Object.entries(data.response);
|
|
253
|
+
const response_index = Array.isArray(data.response_index) ? data.response_index : [];
|
|
235
254
|
for (let i = 0; i < answers.length; i++) {
|
|
255
|
+
let option_index = response_index[i];
|
|
256
|
+
if (typeof option_index !== "number" || option_index < 0) {
|
|
257
|
+
option_index = trial.questions[i].options.indexOf(answers[i][1]);
|
|
258
|
+
}
|
|
259
|
+
if (option_index < 0) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
236
262
|
this.jsPsych.pluginAPI.clickTarget(
|
|
237
|
-
display_element.querySelector(
|
|
238
|
-
`#${plugin_id_name}-response-${i}-${trial.questions[i].options.indexOf(
|
|
239
|
-
answers[i][1]
|
|
240
|
-
)}`
|
|
241
|
-
),
|
|
263
|
+
display_element.querySelector(`#${plugin_id_name}-response-${i}-${option_index}`),
|
|
242
264
|
(data.rt - 1e3) / answers.length * (i + 1)
|
|
243
265
|
);
|
|
244
266
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-survey-multi-choice\",\n \"version\": \"2.1.0\",\n \"description\": \"a jspsych plugin for multiple choice survey questions\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-survey-multi-choice\"\n },\n \"author\": \"Shane Martin\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/survey-multi-choice\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"survey-multi-choice\",\n version: version,\n parameters: {\n /**\n * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt,\n * options, required, and horizontal parameter that will be applied to the question. See examples below for further\n * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be\n * associated with a group of options (radio buttons). All questions will get presented on the same page (trial).\n * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to\n * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates\n * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is\n * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the\n * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing\n * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.\n */\n questions: {\n type: ParameterType.COMPLEX,\n array: true,\n nested: {\n /** Question prompt. */\n prompt: {\n type: ParameterType.HTML_STRING,\n default: undefined,\n },\n /** Array of multiple choice options for this question. */\n options: {\n type: ParameterType.STRING,\n array: true,\n default: undefined,\n },\n /** Whether or not a response to this question must be given in order to continue. */\n required: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** If true, then the question will be centered and options will be displayed horizontally. */\n horizontal: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */\n name: {\n type: ParameterType.STRING,\n default: \"\",\n },\n },\n },\n /**\n * If true, the display order of `questions` is randomly determined at the start of the trial. In the data object,\n * `Q0` will still refer to the first question in the array, regardless of where it was presented visually.\n */\n randomize_question_order: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** HTML formatted string to display at the top of the page above all the questions. */\n preamble: {\n type: ParameterType.HTML_STRING,\n default: null,\n },\n /** Label of the button. */\n button_label: {\n type: ParameterType.STRING,\n default: \"Continue\",\n },\n /**\n * This determines whether or not all of the input elements on the page should allow autocomplete. Setting\n * this to true will enable autocomplete or auto-fill for the form.\n */\n autocomplete: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n response: {\n type: ParameterType.OBJECT,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */\n rt: {\n type: ParameterType.INT,\n },\n /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n question_order: {\n type: ParameterType.INT,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\nconst plugin_id_name = \"jspsych-survey-multi-choice\";\n\n/**\n * **survey-multi-choice**\n *\n * The survey-multi-choice plugin displays a set of questions with multiple choice response fields. The participant selects a single answer.\n *\n * @author Shane Martin\n * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org}\n */\nclass SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) { }\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n\n const trial_form_id = `${plugin_id_name}_form`;\n\n var html = \"\";\n\n // inject CSS for trial\n html += `\n <style id=\"${plugin_id_name}-css\">\n .${plugin_id_name}-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }\n .${plugin_id_name}-text span.required {color: darkred;}\n .${plugin_id_name}-horizontal .${plugin_id_name}-text { text-align: center;}\n .${plugin_id_name}-option { line-height: 2; }\n .${plugin_id_name}-horizontal .${plugin_id_name}-option { display: inline-block; margin-left: 1em; margin-right: 1em; vertical-align: top;}\n label.${plugin_id_name}-text input[type='radio'] {margin-right: 1em;}\n </style>`;\n\n // show preamble text\n if (trial.preamble !== null) {\n html += `<div id=\"${plugin_id_name}-preamble\" class=\"${plugin_id_name}-preamble\">${trial.preamble}</div>`;\n }\n\n // form element\n if (trial.autocomplete) {\n html += `<form id=\"${trial_form_id}\">`;\n } else {\n html += `<form id=\"${trial_form_id}\" autocomplete=\"off\">`;\n }\n\n // generate question order. this is randomized here as opposed to randomizing the order of trial.questions\n // so that the data are always associated with the same question regardless of order\n var question_order = [];\n for (var i = 0; i < trial.questions.length; i++) {\n question_order.push(i);\n }\n if (trial.randomize_question_order) {\n question_order = this.jsPsych.randomization.shuffle(question_order);\n }\n\n // add multiple-choice questions\n for (var i = 0; i < trial.questions.length; i++) {\n // get question based on question_order\n var question = trial.questions[question_order[i]];\n var question_id = question_order[i];\n\n // create question container\n var question_classes = [`${plugin_id_name}-question`];\n if (question.horizontal) {\n question_classes.push(`${plugin_id_name}-horizontal`);\n }\n\n html += `<div id=\"${plugin_id_name}-${question_id}\" class=\"${question_classes.join(\" \")}\" data-name=\"${question.name}\">`;\n\n // add question text\n html += `<p class=\"${plugin_id_name}-text survey-multi-choice\">${question.prompt}`;\n if (question.required) {\n html += \"<span class='required'>*</span>\";\n }\n html += \"</p>\";\n\n // create option radio buttons\n for (var j = 0; j < question.options.length; j++) {\n // add label and question text\n var option_id_name = `${plugin_id_name}-option-${question_id}-${j}`;\n var input_name = `${plugin_id_name}-response-${question_id}`;\n var input_id = `${plugin_id_name}-response-${question_id}-${j}`;\n\n var required_attr = question.required ? \"required\" : \"\";\n\n // add radio button container\n html += `\n <div id=\"${option_id_name}\" class=\"${plugin_id_name}-option\">\n <label class=\"${plugin_id_name}-text\" for=\"${input_id}\">\n <input type=\"radio\" name=\"${input_name}\" id=\"${input_id}\" value=\"${question.options[j]}\" ${required_attr} />\n ${question.options[j]}\n </label>\n </div>`;\n }\n\n html += \"</div>\";\n }\n\n // add submit button\n html += `<input type=\"submit\" id=\"${plugin_id_name}-next\" class=\"${plugin_id_name} jspsych-btn\"${trial.button_label ? ' value=\"' + trial.button_label + '\"' : \"\"} />`;\n html += \"</form>\";\n\n // render\n display_element.innerHTML = html;\n\n const trial_form = display_element.querySelector<HTMLFormElement>(`#${trial_form_id}`);\n\n trial_form.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n // measure response time\n var endTime = performance.now();\n var response_time = Math.round(endTime - startTime);\n\n // create object to hold responses\n var question_data = {};\n for (var i = 0; i < trial.questions.length; i++) {\n var match = display_element.querySelector(`#${plugin_id_name}-${i}`);\n var id = \"Q\" + i;\n var val: String;\n if (match.querySelector(\"input[type=radio]:checked\") !== null) {\n val = match.querySelector<HTMLInputElement>(\"input[type=radio]:checked\").value;\n } else {\n val = \"\";\n }\n var obje = {};\n var name = id;\n if (match.attributes[\"data-name\"].value !== \"\") {\n name = match.attributes[\"data-name\"].value;\n }\n obje[name] = val;\n Object.assign(question_data, obje);\n }\n // save data\n var trial_data = {\n rt: response_time,\n response: question_data,\n question_order: question_order,\n };\n\n // next trial\n this.jsPsych.finishTrial(trial_data);\n });\n\n var startTime = performance.now();\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const question_data = {};\n let rt = 1000;\n\n for (const q of trial.questions) {\n const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`;\n question_data[name] = this.jsPsych.randomization.sampleWithoutReplacement(q.options, 1)[0];\n rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);\n }\n\n const default_data = {\n response: question_data,\n rt: rt,\n question_order: trial.randomize_question_order\n ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()])\n : [...Array(trial.questions.length).keys()],\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const answers = Object.entries(data.response);\n for (let i = 0; i < answers.length; i++) {\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(\n `#${plugin_id_name}-response-${i}-${trial.questions[i].options.indexOf(\n answers[i][1]\n )}`\n ),\n ((data.rt - 1000) / answers.length) * (i + 1)\n );\n }\n\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-next`),\n data.rt\n );\n }\n}\n\nexport default SurveyMultiChoicePlugin;\n"],"names":[],"mappings":";;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC6FA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-survey-multi-choice\",\n \"version\": \"2.2.0\",\n \"description\": \"a jspsych plugin for multiple choice survey questions\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-survey-multi-choice\"\n },\n \"author\": \"Shane Martin\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/survey-multi-choice\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"survey-multi-choice\",\n version: version,\n parameters: {\n /**\n * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt,\n * options, required, and horizontal parameter that will be applied to the question. See examples below for further\n * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be\n * associated with a group of options (radio buttons). All questions will get presented on the same page (trial).\n * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to\n * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates\n * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is\n * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the\n * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing\n * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.\n */\n questions: {\n type: ParameterType.COMPLEX,\n array: true,\n nested: {\n /** Question prompt. */\n prompt: {\n type: ParameterType.HTML_STRING,\n default: undefined,\n },\n /** Array of multiple choice options for this question. */\n options: {\n type: ParameterType.STRING,\n array: true,\n default: undefined,\n },\n /** Whether or not a response to this question must be given in order to continue. */\n required: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** If true, then the question will be centered and options will be displayed horizontally. */\n horizontal: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */\n name: {\n type: ParameterType.STRING,\n default: \"\",\n },\n },\n },\n /**\n * If true, the display order of `questions` is randomly determined at the start of the trial. In the data object,\n * `Q0` will still refer to the first question in the array, regardless of where it was presented visually.\n */\n randomize_question_order: {\n type: ParameterType.BOOL,\n default: false,\n },\n /** HTML formatted string to display at the top of the page above all the questions. */\n preamble: {\n type: ParameterType.HTML_STRING,\n default: null,\n },\n /** Label of the button. */\n button_label: {\n type: ParameterType.STRING,\n default: \"Continue\",\n },\n /**\n * This determines whether or not all of the input elements on the page should allow autocomplete. Setting\n * this to true will enable autocomplete or auto-fill for the form.\n */\n autocomplete: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n response: {\n type: ParameterType.OBJECT,\n },\n /** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */\n response_index: {\n type: ParameterType.INT,\n array: true,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */\n rt: {\n type: ParameterType.INT,\n },\n /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n question_order: {\n type: ParameterType.INT,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\nconst plugin_id_name = \"jspsych-survey-multi-choice\";\n\n/**\n * **survey-multi-choice**\n *\n * The survey-multi-choice plugin displays a set of questions with multiple choice response fields. The participant selects a single answer.\n *\n * @author Shane Martin\n * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org}\n */\nclass SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n const trial_form_id = `${plugin_id_name}_form`;\n\n var html = \"\";\n\n // inject CSS for trial\n html += `\n <style id=\"${plugin_id_name}-css\">\n .${plugin_id_name}-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }\n .${plugin_id_name}-text span.required {color: darkred;}\n .${plugin_id_name}-horizontal .${plugin_id_name}-text { text-align: center;}\n .${plugin_id_name}-option { line-height: 2; }\n .${plugin_id_name}-horizontal .${plugin_id_name}-option { display: inline-block; margin-left: 1em; margin-right: 1em; vertical-align: top;}\n label.${plugin_id_name}-text input[type='radio'] {margin-right: 1em;}\n </style>`;\n\n // show preamble text\n if (trial.preamble !== null) {\n html += `<div id=\"${plugin_id_name}-preamble\" class=\"${plugin_id_name}-preamble\">${trial.preamble}</div>`;\n }\n\n // form element\n if (trial.autocomplete) {\n html += `<form id=\"${trial_form_id}\">`;\n } else {\n html += `<form id=\"${trial_form_id}\" autocomplete=\"off\">`;\n }\n\n // generate question order. this is randomized here as opposed to randomizing the order of trial.questions\n // so that the data are always associated with the same question regardless of order\n var question_order = [];\n for (var i = 0; i < trial.questions.length; i++) {\n question_order.push(i);\n }\n if (trial.randomize_question_order) {\n question_order = this.jsPsych.randomization.shuffle(question_order);\n }\n\n // add multiple-choice questions\n for (var i = 0; i < trial.questions.length; i++) {\n // get question based on question_order\n var question = trial.questions[question_order[i]];\n var question_id = question_order[i];\n\n // create question container\n var question_classes = [`${plugin_id_name}-question`];\n if (question.horizontal) {\n question_classes.push(`${plugin_id_name}-horizontal`);\n }\n\n html += `<div id=\"${plugin_id_name}-${question_id}\" class=\"${question_classes.join(\n \" \"\n )}\" data-name=\"${question.name}\">`;\n\n // add question text\n html += `<p class=\"${plugin_id_name}-text survey-multi-choice\">${question.prompt}`;\n if (question.required) {\n html += \"<span class='required'>*</span>\";\n }\n html += \"</p>\";\n\n // create option radio buttons\n for (var j = 0; j < question.options.length; j++) {\n // add label and question text\n var option_id_name = `${plugin_id_name}-option-${question_id}-${j}`;\n var input_name = `${plugin_id_name}-response-${question_id}`;\n var input_id = `${plugin_id_name}-response-${question_id}-${j}`;\n\n var required_attr = question.required ? \"required\" : \"\";\n\n // add radio button container\n html += `\n <div id=\"${option_id_name}\" class=\"${plugin_id_name}-option\">\n <label class=\"${plugin_id_name}-text\" for=\"${input_id}\">\n <input type=\"radio\" name=\"${input_name}\" id=\"${input_id}\" value=\"${question.options[j]}\" data-option-index=\"${j}\" ${required_attr} />\n ${question.options[j]}\n </label>\n </div>`;\n }\n\n html += \"</div>\";\n }\n\n // add submit button\n html += `<input type=\"submit\" id=\"${plugin_id_name}-next\" class=\"${plugin_id_name} jspsych-btn\"${\n trial.button_label ? ' value=\"' + trial.button_label + '\"' : \"\"\n } />`;\n html += \"</form>\";\n\n // render\n display_element.innerHTML = html;\n\n const trial_form = display_element.querySelector<HTMLFormElement>(`#${trial_form_id}`);\n\n trial_form.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n // measure response time\n var endTime = performance.now();\n var response_time = Math.round(endTime - startTime);\n\n // create object to hold responses\n var question_data = {};\n var response_index = [];\n for (var i = 0; i < trial.questions.length; i++) {\n var match = display_element.querySelector(`#${plugin_id_name}-${i}`);\n var id = \"Q\" + i;\n var val: String = \"\";\n var selected_index = -1;\n var checked = match.querySelector<HTMLInputElement>(\"input[type=radio]:checked\");\n if (checked !== null) {\n val = checked.value;\n selected_index = Number(checked.dataset.optionIndex);\n } else {\n val = \"\";\n }\n var obje = {};\n var name = id;\n if (match.attributes[\"data-name\"].value !== \"\") {\n name = match.attributes[\"data-name\"].value;\n }\n obje[name] = val;\n Object.assign(question_data, obje);\n response_index.push(selected_index);\n }\n // save data\n var trial_data = {\n rt: response_time,\n response: question_data,\n response_index: response_index,\n question_order: question_order,\n };\n\n // next trial\n this.jsPsych.finishTrial(trial_data);\n });\n\n var startTime = performance.now();\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const question_data = {};\n const response_index = [];\n let rt = 1000;\n\n for (let i = 0; i < trial.questions.length; i++) {\n const q = trial.questions[i];\n const name = q.name ? q.name : `Q${i}`;\n const option_index = this.jsPsych.randomization.randomInt(0, q.options.length - 1);\n question_data[name] = q.options[option_index];\n response_index.push(option_index);\n rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);\n }\n\n const default_data = {\n response: question_data,\n response_index: response_index,\n rt: rt,\n question_order: trial.randomize_question_order\n ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()])\n : [...Array(trial.questions.length).keys()],\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const answers = Object.entries(data.response);\n const response_index = Array.isArray(data.response_index) ? data.response_index : [];\n for (let i = 0; i < answers.length; i++) {\n let option_index = response_index[i];\n if (typeof option_index !== \"number\" || option_index < 0) {\n option_index = trial.questions[i].options.indexOf(answers[i][1]);\n }\n if (option_index < 0) {\n continue;\n }\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-response-${i}-${option_index}`),\n ((data.rt - 1000) / answers.length) * (i + 1)\n );\n }\n\n this.jsPsych.pluginAPI.clickTarget(\n display_element.querySelector(`#${plugin_id_name}-next`),\n data.rt\n );\n }\n}\n\nexport default SurveyMultiChoicePlugin;\n"],"names":[],"mappings":";;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECkGA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
CHANGED
package/src/index.spec.ts
CHANGED
|
@@ -1,49 +1,58 @@
|
|
|
1
1
|
import { clickTarget, simulateTimeline, startTimeline } from "@jspsych/test-utils";
|
|
2
|
-
|
|
3
2
|
import { initJsPsych } from "jspsych";
|
|
3
|
+
|
|
4
4
|
import surveyMultiChoice from ".";
|
|
5
5
|
|
|
6
6
|
jest.useFakeTimers();
|
|
7
7
|
|
|
8
|
-
const getInputElement = (choiceId: number, value: string) =>
|
|
9
|
-
|
|
8
|
+
const getInputElement = (choiceId: number, value: string, displayElement: HTMLElement) =>
|
|
9
|
+
displayElement.querySelector(
|
|
10
10
|
`#jspsych-survey-multi-choice-${choiceId} input[value="${value}"]`
|
|
11
11
|
) as HTMLInputElement;
|
|
12
12
|
|
|
13
|
+
const getInputElementByIndex = (
|
|
14
|
+
choiceId: number,
|
|
15
|
+
optionIndex: number,
|
|
16
|
+
displayElement: HTMLElement
|
|
17
|
+
) =>
|
|
18
|
+
displayElement.querySelector(
|
|
19
|
+
`#jspsych-survey-multi-choice-response-${choiceId}-${optionIndex}`
|
|
20
|
+
) as HTMLInputElement;
|
|
21
|
+
|
|
13
22
|
describe("survey-multi-choice plugin", () => {
|
|
14
23
|
test("properly ends when has sibling form", async () => {
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
outerForm
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
innerDiv.id = 'target_id';
|
|
24
|
+
const container = document.createElement("div");
|
|
25
|
+
const outerForm = document.createElement("form");
|
|
26
|
+
outerForm.id = "outer_form";
|
|
27
|
+
container.appendChild(outerForm);
|
|
28
|
+
const innerDiv = document.createElement("div");
|
|
29
|
+
innerDiv.id = "target_id";
|
|
22
30
|
container.appendChild(innerDiv);
|
|
23
|
-
document.body.appendChild(container)
|
|
24
|
-
const jsPsychInst = initJsPsych({ display_element: innerDiv })
|
|
31
|
+
document.body.appendChild(container);
|
|
32
|
+
const jsPsychInst = initJsPsych({ display_element: innerDiv });
|
|
25
33
|
const options = ["a", "b", "c"];
|
|
26
34
|
|
|
27
|
-
const {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const { displayElement, expectFinished } = await startTimeline(
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
type: surveyMultiChoice,
|
|
39
|
+
questions: [
|
|
40
|
+
{ prompt: "Q0", options },
|
|
41
|
+
{ prompt: "Q1", options },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
jsPsychInst
|
|
46
|
+
);
|
|
36
47
|
|
|
37
|
-
getInputElement(0, "a").checked = true;
|
|
38
|
-
await clickTarget(
|
|
48
|
+
getInputElement(0, "a", displayElement).checked = true;
|
|
49
|
+
await clickTarget(displayElement.querySelector("#jspsych-survey-multi-choice-next"));
|
|
39
50
|
await expectFinished();
|
|
40
|
-
|
|
41
|
-
})
|
|
42
|
-
|
|
51
|
+
});
|
|
43
52
|
|
|
44
53
|
test("data are logged with the right question when randomize order is true", async () => {
|
|
45
54
|
var scale = ["a", "b", "c", "d", "e"];
|
|
46
|
-
const { getData, expectFinished } = await startTimeline([
|
|
55
|
+
const { getData, expectFinished, displayElement } = await startTimeline([
|
|
47
56
|
{
|
|
48
57
|
type: surveyMultiChoice,
|
|
49
58
|
questions: [
|
|
@@ -57,13 +66,13 @@ describe("survey-multi-choice plugin", () => {
|
|
|
57
66
|
},
|
|
58
67
|
]);
|
|
59
68
|
|
|
60
|
-
getInputElement(0, "a").checked = true;
|
|
61
|
-
getInputElement(1, "b").checked = true;
|
|
62
|
-
getInputElement(2, "c").checked = true;
|
|
63
|
-
getInputElement(3, "d").checked = true;
|
|
64
|
-
getInputElement(4, "e").checked = true;
|
|
69
|
+
getInputElement(0, "a", displayElement).checked = true;
|
|
70
|
+
getInputElement(1, "b", displayElement).checked = true;
|
|
71
|
+
getInputElement(2, "c", displayElement).checked = true;
|
|
72
|
+
getInputElement(3, "d", displayElement).checked = true;
|
|
73
|
+
getInputElement(4, "e", displayElement).checked = true;
|
|
65
74
|
|
|
66
|
-
await clickTarget(
|
|
75
|
+
await clickTarget(displayElement.querySelector("#jspsych-survey-multi-choice-next"));
|
|
67
76
|
|
|
68
77
|
await expectFinished();
|
|
69
78
|
|
|
@@ -74,6 +83,42 @@ describe("survey-multi-choice plugin", () => {
|
|
|
74
83
|
expect(surveyData.Q3).toBe("d");
|
|
75
84
|
expect(surveyData.Q4).toBe("e");
|
|
76
85
|
});
|
|
86
|
+
|
|
87
|
+
test("records response_index for duplicate options", async () => {
|
|
88
|
+
const options = ["Little", "", "", "Much"];
|
|
89
|
+
const { getData, expectFinished, displayElement } = await startTimeline([
|
|
90
|
+
{
|
|
91
|
+
type: surveyMultiChoice,
|
|
92
|
+
questions: [{ prompt: "How much", options, required: false }],
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
getInputElementByIndex(0, 2, displayElement).checked = true;
|
|
97
|
+
|
|
98
|
+
await clickTarget(displayElement.querySelector("#jspsych-survey-multi-choice-next"));
|
|
99
|
+
await expectFinished();
|
|
100
|
+
|
|
101
|
+
const surveyData = getData().values()[0];
|
|
102
|
+
expect(surveyData.response.Q0).toBe("");
|
|
103
|
+
expect(surveyData.response_index[0]).toBe(2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("records -1 in response_index for unanswered questions", async () => {
|
|
107
|
+
const options = ["Little", "", "", "Much"];
|
|
108
|
+
const { getData, expectFinished, displayElement } = await startTimeline([
|
|
109
|
+
{
|
|
110
|
+
type: surveyMultiChoice,
|
|
111
|
+
questions: [{ prompt: "How much", options, required: false }],
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
await clickTarget(displayElement.querySelector("#jspsych-survey-multi-choice-next"));
|
|
116
|
+
await expectFinished();
|
|
117
|
+
|
|
118
|
+
const surveyData = getData().values()[0];
|
|
119
|
+
expect(surveyData.response.Q0).toBe("");
|
|
120
|
+
expect(surveyData.response_index[0]).toBe(-1);
|
|
121
|
+
});
|
|
77
122
|
});
|
|
78
123
|
|
|
79
124
|
describe("survey-multi-choice plugin simulation", () => {
|
|
@@ -95,11 +140,16 @@ describe("survey-multi-choice plugin simulation", () => {
|
|
|
95
140
|
|
|
96
141
|
await expectFinished();
|
|
97
142
|
|
|
98
|
-
const surveyData = getData().values()[0]
|
|
99
|
-
const all_valid = Object.entries(surveyData).every((x) => {
|
|
143
|
+
const surveyData = getData().values()[0];
|
|
144
|
+
const all_valid = Object.entries(surveyData.response).every((x) => {
|
|
100
145
|
return scale.includes(x[1] as string);
|
|
101
146
|
});
|
|
102
147
|
expect(all_valid).toBe(true);
|
|
148
|
+
expect(surveyData.response_index).toHaveLength(scale.length);
|
|
149
|
+
const indices_valid = surveyData.response_index.every(
|
|
150
|
+
(index) => Number.isInteger(index) && index >= 0 && index < scale.length
|
|
151
|
+
);
|
|
152
|
+
expect(indices_valid).toBe(true);
|
|
103
153
|
});
|
|
104
154
|
|
|
105
155
|
test("visual mode works", async () => {
|
|
@@ -127,10 +177,15 @@ describe("survey-multi-choice plugin simulation", () => {
|
|
|
127
177
|
|
|
128
178
|
await expectFinished();
|
|
129
179
|
|
|
130
|
-
const surveyData = getData().values()[0]
|
|
131
|
-
const all_valid = Object.entries(surveyData).every((x) => {
|
|
180
|
+
const surveyData = getData().values()[0];
|
|
181
|
+
const all_valid = Object.entries(surveyData.response).every((x) => {
|
|
132
182
|
return scale.includes(x[1] as string);
|
|
133
183
|
});
|
|
134
184
|
expect(all_valid).toBe(true);
|
|
185
|
+
expect(surveyData.response_index).toHaveLength(scale.length);
|
|
186
|
+
const indices_valid = surveyData.response_index.every(
|
|
187
|
+
(index) => Number.isInteger(index) && index >= 0 && index < scale.length
|
|
188
|
+
);
|
|
189
|
+
expect(indices_valid).toBe(true);
|
|
135
190
|
});
|
|
136
191
|
});
|
package/src/index.ts
CHANGED
|
@@ -82,6 +82,11 @@ const info = <const>{
|
|
|
82
82
|
response: {
|
|
83
83
|
type: ParameterType.OBJECT,
|
|
84
84
|
},
|
|
85
|
+
/** An array containing the index of the selected option for each question. Unanswered questions are recorded as -1. */
|
|
86
|
+
response_index: {
|
|
87
|
+
type: ParameterType.INT,
|
|
88
|
+
array: true,
|
|
89
|
+
},
|
|
85
90
|
/** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */
|
|
86
91
|
rt: {
|
|
87
92
|
type: ParameterType.INT,
|
|
@@ -111,10 +116,9 @@ const plugin_id_name = "jspsych-survey-multi-choice";
|
|
|
111
116
|
class SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {
|
|
112
117
|
static info = info;
|
|
113
118
|
|
|
114
|
-
constructor(private jsPsych: JsPsych) {
|
|
119
|
+
constructor(private jsPsych: JsPsych) {}
|
|
115
120
|
|
|
116
121
|
trial(display_element: HTMLElement, trial: TrialType<Info>) {
|
|
117
|
-
|
|
118
122
|
const trial_form_id = `${plugin_id_name}_form`;
|
|
119
123
|
|
|
120
124
|
var html = "";
|
|
@@ -164,7 +168,9 @@ class SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {
|
|
|
164
168
|
question_classes.push(`${plugin_id_name}-horizontal`);
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
html += `<div id="${plugin_id_name}-${question_id}" class="${question_classes.join(
|
|
171
|
+
html += `<div id="${plugin_id_name}-${question_id}" class="${question_classes.join(
|
|
172
|
+
" "
|
|
173
|
+
)}" data-name="${question.name}">`;
|
|
168
174
|
|
|
169
175
|
// add question text
|
|
170
176
|
html += `<p class="${plugin_id_name}-text survey-multi-choice">${question.prompt}`;
|
|
@@ -186,7 +192,7 @@ class SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {
|
|
|
186
192
|
html += `
|
|
187
193
|
<div id="${option_id_name}" class="${plugin_id_name}-option">
|
|
188
194
|
<label class="${plugin_id_name}-text" for="${input_id}">
|
|
189
|
-
<input type="radio" name="${input_name}" id="${input_id}" value="${question.options[j]}" ${required_attr} />
|
|
195
|
+
<input type="radio" name="${input_name}" id="${input_id}" value="${question.options[j]}" data-option-index="${j}" ${required_attr} />
|
|
190
196
|
${question.options[j]}
|
|
191
197
|
</label>
|
|
192
198
|
</div>`;
|
|
@@ -196,7 +202,9 @@ class SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {
|
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
// add submit button
|
|
199
|
-
html += `<input type="submit" id="${plugin_id_name}-next" class="${plugin_id_name} jspsych-btn"${
|
|
205
|
+
html += `<input type="submit" id="${plugin_id_name}-next" class="${plugin_id_name} jspsych-btn"${
|
|
206
|
+
trial.button_label ? ' value="' + trial.button_label + '"' : ""
|
|
207
|
+
} />`;
|
|
200
208
|
html += "</form>";
|
|
201
209
|
|
|
202
210
|
// render
|
|
@@ -212,12 +220,16 @@ class SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {
|
|
|
212
220
|
|
|
213
221
|
// create object to hold responses
|
|
214
222
|
var question_data = {};
|
|
223
|
+
var response_index = [];
|
|
215
224
|
for (var i = 0; i < trial.questions.length; i++) {
|
|
216
225
|
var match = display_element.querySelector(`#${plugin_id_name}-${i}`);
|
|
217
226
|
var id = "Q" + i;
|
|
218
|
-
var val: String;
|
|
219
|
-
|
|
220
|
-
|
|
227
|
+
var val: String = "";
|
|
228
|
+
var selected_index = -1;
|
|
229
|
+
var checked = match.querySelector<HTMLInputElement>("input[type=radio]:checked");
|
|
230
|
+
if (checked !== null) {
|
|
231
|
+
val = checked.value;
|
|
232
|
+
selected_index = Number(checked.dataset.optionIndex);
|
|
221
233
|
} else {
|
|
222
234
|
val = "";
|
|
223
235
|
}
|
|
@@ -228,11 +240,13 @@ class SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {
|
|
|
228
240
|
}
|
|
229
241
|
obje[name] = val;
|
|
230
242
|
Object.assign(question_data, obje);
|
|
243
|
+
response_index.push(selected_index);
|
|
231
244
|
}
|
|
232
245
|
// save data
|
|
233
246
|
var trial_data = {
|
|
234
247
|
rt: response_time,
|
|
235
248
|
response: question_data,
|
|
249
|
+
response_index: response_index,
|
|
236
250
|
question_order: question_order,
|
|
237
251
|
};
|
|
238
252
|
|
|
@@ -260,16 +274,21 @@ class SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {
|
|
|
260
274
|
|
|
261
275
|
private create_simulation_data(trial: TrialType<Info>, simulation_options) {
|
|
262
276
|
const question_data = {};
|
|
277
|
+
const response_index = [];
|
|
263
278
|
let rt = 1000;
|
|
264
279
|
|
|
265
|
-
for (
|
|
266
|
-
const
|
|
267
|
-
|
|
280
|
+
for (let i = 0; i < trial.questions.length; i++) {
|
|
281
|
+
const q = trial.questions[i];
|
|
282
|
+
const name = q.name ? q.name : `Q${i}`;
|
|
283
|
+
const option_index = this.jsPsych.randomization.randomInt(0, q.options.length - 1);
|
|
284
|
+
question_data[name] = q.options[option_index];
|
|
285
|
+
response_index.push(option_index);
|
|
268
286
|
rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true);
|
|
269
287
|
}
|
|
270
288
|
|
|
271
289
|
const default_data = {
|
|
272
290
|
response: question_data,
|
|
291
|
+
response_index: response_index,
|
|
273
292
|
rt: rt,
|
|
274
293
|
question_order: trial.randomize_question_order
|
|
275
294
|
? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()])
|
|
@@ -298,13 +317,17 @@ class SurveyMultiChoicePlugin implements JsPsychPlugin<Info> {
|
|
|
298
317
|
load_callback();
|
|
299
318
|
|
|
300
319
|
const answers = Object.entries(data.response);
|
|
320
|
+
const response_index = Array.isArray(data.response_index) ? data.response_index : [];
|
|
301
321
|
for (let i = 0; i < answers.length; i++) {
|
|
322
|
+
let option_index = response_index[i];
|
|
323
|
+
if (typeof option_index !== "number" || option_index < 0) {
|
|
324
|
+
option_index = trial.questions[i].options.indexOf(answers[i][1]);
|
|
325
|
+
}
|
|
326
|
+
if (option_index < 0) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
302
329
|
this.jsPsych.pluginAPI.clickTarget(
|
|
303
|
-
display_element.querySelector(
|
|
304
|
-
`#${plugin_id_name}-response-${i}-${trial.questions[i].options.indexOf(
|
|
305
|
-
answers[i][1]
|
|
306
|
-
)}`
|
|
307
|
-
),
|
|
330
|
+
display_element.querySelector(`#${plugin_id_name}-response-${i}-${option_index}`),
|
|
308
331
|
((data.rt - 1000) / answers.length) * (i + 1)
|
|
309
332
|
);
|
|
310
333
|
}
|