@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.
@@ -1,7 +1,7 @@
1
1
  var jsPsychSurveyMultiChoice = (function (jspsych) {
2
2
  'use strict';
3
3
 
4
- var version = "2.1.0";
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(" ")}" data-name="${question.name}">`;
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
- if (match.querySelector("input[type=radio]:checked") !== null) {
179
- val = match.querySelector("input[type=radio]:checked").value;
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 (const q of trial.questions) {
213
- const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`;
214
- question_data[name] = this.jsPsych.randomization.sampleWithoutReplacement(q.options, 1)[0];
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.1.0/dist/index.browser.js.map
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(n){"use strict";var b="2.1.0";const g={name:"survey-multi-choice",version:b,parameters:{questions:{type:n.ParameterType.COMPLEX,array:!0,nested:{prompt:{type:n.ParameterType.HTML_STRING,default:void 0},options:{type:n.ParameterType.STRING,array:!0,default:void 0},required:{type:n.ParameterType.BOOL,default:!1},horizontal:{type:n.ParameterType.BOOL,default:!1},name:{type:n.ParameterType.STRING,default:""}}},randomize_question_order:{type:n.ParameterType.BOOL,default:!1},preamble:{type:n.ParameterType.HTML_STRING,default:null},button_label:{type:n.ParameterType.STRING,default:"Continue"},autocomplete:{type:n.ParameterType.BOOL,default:!1}},data:{response:{type:n.ParameterType.OBJECT},rt:{type:n.ParameterType.INT},question_order:{type:n.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 h{constructor(a){this.jsPsych=a}trial(a,o){const s=`${e}_form`;var t="";t+=`
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>`,o.preamble!==null&&(t+=`<div id="${e}-preamble" class="${e}-preamble">${o.preamble}</div>`),o.autocomplete?t+=`<form id="${s}">`:t+=`<form id="${s}" autocomplete="off">`;for(var l=[],i=0;i<o.questions.length;i++)l.push(i);o.randomize_question_order&&(l=this.jsPsych.randomization.shuffle(l));for(var i=0;i<o.questions.length;i++){var r=o.questions[l[i]],u=l[i],y=[`${e}-question`];r.horizontal&&y.push(`${e}-horizontal`),t+=`<div id="${e}-${u}" class="${y.join(" ")}" data-name="${r.name}">`,t+=`<p class="${e}-text survey-multi-choice">${r.prompt}`,r.required&&(t+="<span class='required'>*</span>"),t+="</p>";for(var p=0;p<r.options.length;p++){var q=`${e}-option-${u}-${p}`,T=`${e}-response-${u}`,$=`${e}-response-${u}-${p}`,_=r.required?"required":"";t+=`
10
- <div id="${q}" class="${e}-option">
11
- <label class="${e}-text" for="${$}">
12
- <input type="radio" name="${T}" id="${$}" value="${r.options[p]}" ${_} />
13
- ${r.options[p]}
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"${o.button_label?' value="'+o.button_label+'"':""} />`,t+="</form>",a.innerHTML=t,a.querySelector(`#${s}`).addEventListener("submit",S=>{S.preventDefault();for(var j=performance.now(),x=Math.round(j-O),f={},m=0;m<o.questions.length;m++){var c=a.querySelector(`#${e}-${m}`),L="Q"+m,d;c.querySelector("input[type=radio]:checked")!==null?d=c.querySelector("input[type=radio]:checked").value:d="";var v={},P=L;c.attributes["data-name"].value!==""&&(P=c.attributes["data-name"].value),v[P]=d,Object.assign(f,v)}var z={rt:x,response:f,question_order:l};this.jsPsych.finishTrial(z)});var O=performance.now()}simulate(a,o,s,t){o=="data-only"&&(t(),this.simulate_data_only(a,s)),o=="visual"&&this.simulate_visual(a,s,t)}create_simulation_data(a,o){const s={};let t=1e3;for(const r of a.questions){const u=r.name?r.name:`Q${a.questions.indexOf(r)}`;s[u]=this.jsPsych.randomization.sampleWithoutReplacement(r.options,1)[0],t+=this.jsPsych.randomization.sampleExGaussian(1500,400,.005,!0)}const l={response:s,rt:t,question_order:a.randomize_question_order?this.jsPsych.randomization.shuffle([...Array(a.questions.length).keys()]):[...Array(a.questions.length).keys()]},i=this.jsPsych.pluginAPI.mergeSimulationData(l,o);return this.jsPsych.pluginAPI.ensureSimulationDataConsistency(a,i),i}simulate_data_only(a,o){const s=this.create_simulation_data(a,o);this.jsPsych.finishTrial(s)}simulate_visual(a,o,s){const t=this.create_simulation_data(a,o),l=this.jsPsych.getDisplayElement();this.trial(l,a),s();const i=Object.entries(t.response);for(let r=0;r<i.length;r++)this.jsPsych.pluginAPI.clickTarget(l.querySelector(`#${e}-response-${r}-${a.questions[r].options.indexOf(i[r][1])}`),(t.rt-1e3)/i.length*(r+1));this.jsPsych.pluginAPI.clickTarget(l.querySelector(`#${e}-next`),t.rt)}}return h.info=g,h}(jsPsychModule);
16
- //# sourceMappingURL=https://unpkg.com/@jspsych/plugin-survey-multi-choice@2.1.0/dist/index.browser.min.js.map
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.1.0";
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(" ")}" data-name="${question.name}">`;
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
- if (match.querySelector("input[type=radio]:checked") !== null) {
180
- val = match.querySelector("input[type=radio]:checked").value;
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 (const q of trial.questions) {
214
- const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`;
215
- question_data[name] = this.jsPsych.randomization.sampleWithoutReplacement(q.options, 1)[0];
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
  }
@@ -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.1.0";
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(" ")}" data-name="${question.name}">`;
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
- if (match.querySelector("input[type=radio]:checked") !== null) {
178
- val = match.querySelector("input[type=radio]:checked").value;
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 (const q of trial.questions) {
212
- const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`;
213
- question_data[name] = this.jsPsych.randomization.sampleWithoutReplacement(q.options, 1)[0];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jspsych/plugin-survey-multi-choice",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "a jspsych plugin for multiple choice survey questions",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
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
- document.querySelector(
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 container = document.createElement('div')
17
- const outerForm = document.createElement('form')
18
- outerForm.id = 'outer_form'
19
- container.appendChild(outerForm)
20
- const innerDiv = document.createElement('div')
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 { getData, expectFinished } = await startTimeline([
28
- {
29
- type: surveyMultiChoice,
30
- questions: [
31
- { prompt: "Q0", options },
32
- { prompt: "Q1", options },
33
- ]
34
- },
35
- ], jsPsychInst);
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(document.querySelector("#jspsych-survey-multi-choice-next"));
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(document.querySelector("#jspsych-survey-multi-choice-next"));
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].response;
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].response;
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(" ")}" data-name="${question.name}">`;
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"${trial.button_label ? ' value="' + trial.button_label + '"' : ""} />`;
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
- if (match.querySelector("input[type=radio]:checked") !== null) {
220
- val = match.querySelector<HTMLInputElement>("input[type=radio]:checked").value;
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 (const q of trial.questions) {
266
- const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`;
267
- question_data[name] = this.jsPsych.randomization.sampleWithoutReplacement(q.options, 1)[0];
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
  }