{"version":3,"file":"index.cjs","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n  \"name\": \"@jspsych/plugin-survey-multi-select\",\n  \"version\": \"2.1.1\",\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-select\"\n  },\n  \"author\": \"Josh de Leeuw\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n  },\n  \"homepage\": \"https://www.jspsych.org/latest/plugins/survey-multi-select\",\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-select\",\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 select options for this question. */\n        options: {\n          type: ParameterType.STRING,\n          array: true,\n          default: undefined,\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        /** 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        /** 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\n     * object, `Q0` will still refer to the first question in the array, regardless of where it was presented\n     * 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 to submit responses. */\n    button_label: {\n      type: ParameterType.STRING,\n      default: \"Continue\",\n    },\n    /** 'You must choose at least one response for this question' | Message to display if required response is not given. */\n    required_message: {\n      type: ParameterType.STRING,\n      default: \"You must choose at least one response for this question\",\n    },\n    /** This determines whether or not all of the input elements on the page should allow autocomplete.\n     * Setting this to true will enable autocomplete or auto-fill for the form. */\n    autocomplete: {\n      type: ParameterType.BOOL,\n      default: false,\n    },\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. For each question, the response is an array containing the option(s) that the participant selected. 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\n/**\n * The survey-multi-select plugin displays a set of questions with multiple select response fields. The participant can\n * select multiple answers.\n *\n * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-select/ survey-multi-select plugin documentation on jspsych.org}\n */\nclass SurveyMultiSelectPlugin implements JsPsychPlugin<Info> {\n  static info = info;\n\n  constructor(private jsPsych: JsPsych) {}\n\n  trial(display_element: HTMLElement, trial: TrialType<Info>) {\n    var plugin_id_name = \"jspsych-survey-multi-select\";\n    var plugin_id_selector = \"#\" + plugin_id_name;\n    const _join = (...args: Array<string | number>) => args.join(\"-\");\n\n    // inject CSS for trial\n    var cssstr =\n      \".jspsych-survey-multi-select-question { margin-top: 2em; margin-bottom: 2em; text-align: left; }\" +\n      \".jspsych-survey-multi-select-text span.required {color: darkred;}\" +\n      \".jspsych-survey-multi-select-horizontal .jspsych-survey-multi-select-text {  text-align: center;}\" +\n      \".jspsych-survey-multi-select-option { line-height: 2; }\" +\n      \".jspsych-survey-multi-select-horizontal .jspsych-survey-multi-select-option {  display: inline-block;  margin-left: 1em;  margin-right: 1em;  vertical-align: top;}\" +\n      \"label.jspsych-survey-multi-select-text input[type='checkbox'] {margin-right: 1em;}\";\n    display_element.innerHTML =\n      '<style id=\"jspsych-survey-multi-select-css\">' + cssstr + \"</style>\";\n\n    // form element\n    var trial_form_id = _join(plugin_id_name, \"form\");\n    display_element.innerHTML += '<form id=\"' + trial_form_id + '\"></form>';\n    var trial_form = display_element.querySelector<HTMLFormElement>(\"#\" + trial_form_id);\n    if (!trial.autocomplete) {\n      trial_form.setAttribute(\"autocomplete\", \"off\");\n    }\n    // show preamble text\n    var preamble_id_name = _join(plugin_id_name, \"preamble\");\n    if (trial.preamble !== null) {\n      trial_form.innerHTML +=\n        '<div id=\"' +\n        preamble_id_name +\n        '\" class=\"' +\n        preamble_id_name +\n        '\">' +\n        trial.preamble +\n        \"</div>\";\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    // add multiple-select questions\n    for (var i = 0; i < trial.questions.length; i++) {\n      var question = trial.questions[question_order[i]];\n      var question_id = question_order[i];\n      // create question container\n      var question_classes = [_join(plugin_id_name, \"question\")];\n      if (question.horizontal) {\n        question_classes.push(_join(plugin_id_name, \"horizontal\"));\n      }\n\n      trial_form.innerHTML +=\n        '<div id=\"' +\n        _join(plugin_id_name, question_id) +\n        '\" data-name=\"' +\n        question.name +\n        '\" class=\"' +\n        question_classes.join(\" \") +\n        '\"></div>';\n\n      var question_selector = _join(plugin_id_selector, question_id);\n\n      // add question text\n      display_element.querySelector(question_selector).innerHTML +=\n        '<p id=\"survey-question\" class=\"' +\n        plugin_id_name +\n        '-text survey-multi-select\">' +\n        question.prompt +\n        \"</p>\";\n\n      // create option check boxes\n      for (var j = 0; j < question.options.length; j++) {\n        var option_id_name = _join(plugin_id_name, \"option\", question_id, j);\n\n        // add check box container\n        display_element.querySelector(question_selector).innerHTML +=\n          '<div id=\"' + option_id_name + '\" class=\"' + _join(plugin_id_name, \"option\") + '\"></div>';\n\n        // add label and question text\n        var form = document.getElementById(option_id_name);\n        var input_name = _join(plugin_id_name, \"response\", question_id);\n        var input_id = _join(plugin_id_name, \"response\", question_id, j);\n        var label = document.createElement(\"label\");\n        label.setAttribute(\"class\", plugin_id_name + \"-text\");\n        label.innerHTML = question.options[j];\n        label.setAttribute(\"for\", input_id);\n\n        // create checkboxes\n        var input = document.createElement(\"input\");\n        input.setAttribute(\"type\", \"checkbox\");\n        input.setAttribute(\"name\", input_name);\n        input.setAttribute(\"id\", input_id);\n        input.setAttribute(\"value\", question.options[j]);\n        form.appendChild(label);\n        label.insertBefore(input, label.firstChild);\n      }\n    }\n    // add submit button\n    trial_form.innerHTML += '<div class=\"fail-message\"></div>';\n    trial_form.innerHTML +=\n      '<button id=\"' +\n      plugin_id_name +\n      '-next\" class=\"' +\n      plugin_id_name +\n      ' jspsych-btn\">' +\n      trial.button_label +\n      \"</button>\";\n\n    // validation check on the data first for custom validation handling\n    // then submit the form\n    display_element\n      .querySelector(\"#jspsych-survey-multi-select-next\")\n      .addEventListener(\"click\", () => {\n        for (var i = 0; i < trial.questions.length; i++) {\n          if (trial.questions[i].required) {\n            if (\n              display_element.querySelector(\n                \"#jspsych-survey-multi-select-\" + i + \" input:checked\"\n              ) == null\n            ) {\n              display_element\n                .querySelector<HTMLInputElement>(\"#jspsych-survey-multi-select-\" + i + \" input\")\n                .setCustomValidity(trial.required_message);\n            } else {\n              display_element\n                .querySelector<HTMLInputElement>(\"#jspsych-survey-multi-select-\" + i + \" input\")\n                .setCustomValidity(\"\");\n            }\n          }\n        }\n        trial_form.reportValidity();\n      });\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 has_response = [];\n      for (var index = 0; index < trial.questions.length; index++) {\n        var match = display_element.querySelector(\"#jspsych-survey-multi-select-\" + index);\n        var val = [];\n        var inputboxes = match.querySelectorAll<HTMLInputElement>(\"input[type=checkbox]:checked\");\n        for (var j = 0; j < inputboxes.length; j++) {\n          var currentChecked = inputboxes[j];\n          val.push(currentChecked.value);\n        }\n        var id = \"Q\" + index;\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        if (val.length == 0) {\n          has_response.push(false);\n        } else {\n          has_response.push(true);\n        }\n      }\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      let n_answers;\n      if (q.required) {\n        n_answers = this.jsPsych.randomization.randomInt(1, q.options.length);\n      } else {\n        n_answers = this.jsPsych.randomization.randomInt(0, q.options.length);\n      }\n      const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`;\n      const selections = this.jsPsych.randomization.sampleWithoutReplacement(q.options, n_answers);\n      question_data[name] = selections;\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: [string, []][] = Object.entries(data.response);\n    for (let i = 0; i < answers.length; i++) {\n      for (const a of answers[i][1]) {\n        this.jsPsych.pluginAPI.clickTarget(\n          display_element.querySelector(\n            `#jspsych-survey-multi-select-response-${i}-${trial.questions[i].options.indexOf(a)}`\n          ),\n          ((data.rt - 1000) / answers.length) * (i + 1)\n        );\n      }\n    }\n\n    this.jsPsych.pluginAPI.clickTarget(\n      display_element.querySelector(\"#jspsych-survey-multi-select-next\"),\n      data.rt\n    );\n  }\n}\n\nexport default SurveyMultiSelectPlugin;\n"],"names":[],"mappings":";;;;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECkGA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}