import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; import { version } from "../package.json"; const info = { name: "survey-likert", version: version, parameters: { /** Array containing one or more objects with parameters for the question(s) that should be shown on the page. */ questions: { type: ParameterType.COMPLEX, array: true, nested: { /** Question prompt. */ prompt: { type: ParameterType.HTML_STRING, default: undefined, }, /** Array of likert labels to display for this question. */ labels: { type: ParameterType.STRING, array: true, default: undefined, }, /** Whether or not a response to this question must be given in order to continue. */ required: { type: ParameterType.BOOL, default: false, }, /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */ name: { type: ParameterType.STRING, default: "", }, /** Default response value for this question (0-based index of labels array). */ default_response: { type: ParameterType.INT, default: null, }, }, }, /** If true, the order of the questions in the 'questions' array will be randomized. */ randomize_question_order: { type: ParameterType.BOOL, default: false, }, /** HTML-formatted string to display at top of the page above all of the questions. */ preamble: { type: ParameterType.HTML_STRING, default: null, }, /** Width of the likert scales in pixels. */ scale_width: { type: ParameterType.INT, default: null, }, /** Label of the button to submit responses. */ button_label: { type: ParameterType.STRING, default: "Continue", }, /** Setting this to true will enable browser auto-complete or auto-fill for the form. */ autocomplete: { type: ParameterType.BOOL, default: false, }, }, data: { /** 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. */ response: { type: ParameterType.OBJECT, }, /** 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. */ rt: { type: ParameterType.INT, }, /** 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. */ question_order: { type: ParameterType.INT, array: true, }, }, // prettier-ignore citations: '__CITATIONS__', }; type Info = typeof info; /** * The survey-likert plugin displays a set of questions with Likert scale responses. The participant responds * by selecting a radio button. * * @author Josh de Leeuw * @see {@link https://www.jspsych.org/latest/plugins/survey-likert/ survey-likert plugin documentation on jspsych.org} */ class SurveyLikertPlugin implements JsPsychPlugin { static info = info; constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { if (trial.scale_width !== null) { var w = trial.scale_width + "px"; } else { var w = "100%"; } var html = ""; // inject CSS for trial html += '"; // show preamble text if (trial.preamble !== null) { html += '
' + trial.preamble + "
"; } if (trial.autocomplete) { html += '
'; } else { html += ''; } // add likert scale questions /// // generate question order. this is randomized here as opposed to randomizing the order of trial.questions // so that the data are always associated with the same question regardless of order var question_order = []; for (var i = 0; i < trial.questions.length; i++) { question_order.push(i); } if (trial.randomize_question_order) { question_order = this.jsPsych.randomization.shuffle(question_order); } for (var i = 0; i < trial.questions.length; i++) { var question = trial.questions[question_order[i]]; // add question html += '"; // add options var width = 100 / question.labels.length; var options_string = '
    '; for (var j = 0; j < question.labels.length; j++) { options_string += '
  • "; } options_string += "
"; html += options_string; } // add submit button html += ''; html += "
"; display_element.innerHTML = html; display_element.querySelector("#jspsych-survey-likert-form").addEventListener("submit", (e) => { e.preventDefault(); // measure response time var endTime = performance.now(); var response_time = Math.round(endTime - startTime); // create object to hold responses var question_data = {}; var matches = display_element.querySelectorAll( "#jspsych-survey-likert-form .jspsych-survey-likert-opts" ); for (var index = 0; index < matches.length; index++) { var id = matches[index].dataset["radioGroup"]; var el = display_element.querySelector( 'input[name="' + id + '"]:checked' ); if (el === null) { var response: string | number = ""; } else { var response: string | number = parseInt(el.value); } var obje = {}; if (matches[index].attributes["data-name"].value !== "") { var name = matches[index].attributes["data-name"].value; } else { var name = id; } obje[name] = response; Object.assign(question_data, obje); } // save data var trial_data = { rt: response_time, response: question_data, question_order: question_order, }; // next trial this.jsPsych.finishTrial(trial_data); }); var startTime = performance.now(); } simulate( trial: TrialType, simulation_mode, simulation_options: any, load_callback: () => void ) { if (simulation_mode == "data-only") { load_callback(); this.simulate_data_only(trial, simulation_options); } if (simulation_mode == "visual") { this.simulate_visual(trial, simulation_options, load_callback); } } private create_simulation_data(trial: TrialType, simulation_options) { const question_data = {}; let rt = 1000; for (const q of trial.questions) { const name = q.name ? q.name : `Q${trial.questions.indexOf(q)}`; question_data[name] = this.jsPsych.randomization.randomInt(0, q.labels.length - 1); rt += this.jsPsych.randomization.sampleExGaussian(1500, 400, 1 / 200, true); } const default_data = { response: question_data, rt: rt, question_order: trial.randomize_question_order ? this.jsPsych.randomization.shuffle([...Array(trial.questions.length).keys()]) : [...Array(trial.questions.length).keys()], }; const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options); this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data); return data; } private simulate_data_only(trial: TrialType, simulation_options) { const data = this.create_simulation_data(trial, simulation_options); this.jsPsych.finishTrial(data); } private simulate_visual(trial: TrialType, simulation_options, load_callback: () => void) { const data = this.create_simulation_data(trial, simulation_options); const display_element = this.jsPsych.getDisplayElement(); this.trial(display_element, trial); load_callback(); const answers = Object.entries(data.response); for (let i = 0; i < answers.length; i++) { this.jsPsych.pluginAPI.clickTarget( display_element.querySelector( `input[type="radio"][name="${answers[i][0]}"][value="${answers[i][1]}"]` ), ((data.rt - 1000) / answers.length) * (i + 1) ); } this.jsPsych.pluginAPI.clickTarget( display_element.querySelector("#jspsych-survey-likert-next"), data.rt ); } } export default SurveyLikertPlugin;