From 036c40a285acc1d2ccadb585701bba8325cfb3b3 Mon Sep 17 00:00:00 2001 From: Becky Gilbert Date: Wed, 3 Jan 2024 12:18:05 -0800 Subject: [PATCH] initial survey plugin rewrite with survey-jquery and updated versions --- package-lock.json | 30 +- packages/plugin-survey/css/survey.scss | 47 +- packages/plugin-survey/package.json | 7 +- packages/plugin-survey/src/index.ts | 834 +++---------------------- 4 files changed, 138 insertions(+), 780 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7e175b797..7ba11b1201 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11199,6 +11199,11 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11597,11 +11602,6 @@ "node": ">= 8" } }, - "node_modules/knockout": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/knockout/-/knockout-3.5.1.tgz", - "integrity": "sha512-wRJ9I4az0QcsH7A4v4l0enUpkS++MBx0BnL/68KaLzJg7x1qmbjSlwEoCNol7KTYZ+pmtI7Eh2J0Nu6/2Z5J/Q==" - }, "node_modules/last-run": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", @@ -15587,12 +15587,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/survey-knockout": { - "version": "1.9.30", - "resolved": "https://registry.npmjs.org/survey-knockout/-/survey-knockout-1.9.30.tgz", - "integrity": "sha512-Bdu+cMEdS6VwePyPfD1f5wZtZIGCbGPLmw2IgOC2xZOrfevWGkwne5hmRGDKKxpAVRO7oKygZYy6W6t49W3a4A==", + "node_modules/survey-core": { + "version": "1.9.121", + "resolved": "https://registry.npmjs.org/survey-core/-/survey-core-1.9.121.tgz", + "integrity": "sha512-CCZMA7j+WGf2WBOOl7WUNTTrNmyP8CbhPdCZWKxvgJnKXSdalFmOD67/VuCeE1paa/XI0USlZtAjxxtMupya4w==" + }, + "node_modules/survey-jquery": { + "version": "1.9.122", + "resolved": "https://registry.npmjs.org/survey-jquery/-/survey-jquery-1.9.122.tgz", + "integrity": "sha512-1t39OR6HrdP8rK0G+QNgDp2Obq+JNj+GZ5cOWwVSClDRrmss+kfQ7aAKbQqnWf9Yp37JrxlAII1fNgBG3mkhuw==", "dependencies": { - "knockout": "^3.5.1" + "jquery": ">=1.12.4" } }, "node_modules/sver-compat": { @@ -18008,8 +18013,9 @@ "version": "0.2.2", "license": "MIT", "dependencies": { - "knockout": "3.5.1", - "survey-knockout": "1.9.30" + "jquery": "^3.7.1", + "survey-core": "^1.9.121", + "survey-jquery": "^1.9.122" }, "devDependencies": { "@jspsych/config": "^2.0.0", diff --git a/packages/plugin-survey/css/survey.scss b/packages/plugin-survey/css/survey.scss index 779de84c59..3739652599 100644 --- a/packages/plugin-survey/css/survey.scss +++ b/packages/plugin-survey/css/survey.scss @@ -1,25 +1,32 @@ -@use "survey-knockout/survey.css"; +@use "survey-jquery/defaultV2.min.css"; -.sv_main { - font-family: "Open Sans", "Arial", sans-serif; - font-size: 18px; - text-align: left; +// move buttons to right +div#sv-nav-complete.sv-action, div#sv-nav-next.sv-action { + margin-left: auto !important; +} - .sv_body { - border-top-width: 0; - } +// TO DO: keeping this here for reference until we decide on default style - .sv_p_root { - .sv_row { - border-bottom: none; - } - } +// .sv_main { +// font-family: "Open Sans", "Arial", sans-serif; +// font-size: 18px; +// text-align: left; - .sv_container .sv_body .sv_p_root .sv_q_title { - font-weight: normal; - } +// .sv_body { +// border-top-width: 0; +// } - .sv_q_erbox { - font-size: 0.85em; - } -} +// .sv_p_root { +// .sv_row { +// border-bottom: none; +// } +// } + +// .sv_container .sv_body .sv_p_root .sv_q_title { +// font-weight: normal; +// } + +// .sv_q_erbox { +// font-size: 0.85em; +// } +// } diff --git a/packages/plugin-survey/package.json b/packages/plugin-survey/package.json index 06a6a59664..5a606dd4ef 100644 --- a/packages/plugin-survey/package.json +++ b/packages/plugin-survey/package.json @@ -34,7 +34,7 @@ "url": "git+https://github.com/jspsych/jsPsych.git", "directory": "packages/plugin-survey" }, - "author": "", + "author": "Becky Gilbert", "license": "MIT", "bugs": { "url": "https://github.com/jspsych/jsPsych/issues" @@ -50,7 +50,8 @@ "sass": "^1.43.5" }, "dependencies": { - "knockout": "3.5.1", - "survey-knockout": "1.9.30" + "jquery": "^3.7.1", + "survey-core": "^1.9.121", + "survey-jquery": "^1.9.122" } } diff --git a/packages/plugin-survey/src/index.ts b/packages/plugin-survey/src/index.ts index 39f27ecbb7..cb810eca79 100644 --- a/packages/plugin-survey/src/index.ts +++ b/packages/plugin-survey/src/index.ts @@ -1,799 +1,143 @@ +// import SurveyJS dependencies: survey-core and survey-jquery (UI theme): https://surveyjs.io/documentation/surveyjs-architecture#surveyjs-packages +import $ from "jquery"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; -import { - QuestionCheckbox, - QuestionComment, - QuestionDropdown, - QuestionHtml, - QuestionMatrix, - QuestionRadiogroup, - QuestionRanking, - QuestionRating, - QuestionText, - StylesManager, - Survey, -} from "survey-knockout"; +import { StylesManager } from "survey-core"; +// TO DO: decide whether to apply this theme or remove it +import { PlainLightPanelless } from "survey-core/themes/plain-light-panelless"; +import * as SurveyJS from "survey-jquery"; const info = { name: "survey", parameters: { - pages: { - type: ParameterType.COMPLEX, // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX - default: undefined, - pretty_name: "Pages", - array: true, - nested: { - /** Question type: one of "drop-down", "html", "likert", "likert-table", "multi-choice", "multi-select", "ranking", "text" */ - type: { - type: ParameterType.SELECT, - pretty_name: "Type", - default: null, - options: [ - "drop-down", - "html", - "likert", - "likert-table", - "multi-choice", - "multi-select", - "ranking", - "text", - ], // TO DO: fix likert-table, fix ranking - }, - /** Question prompt. */ - prompt: { - type: ParameterType.HTML_STRING, - pretty_name: "Prompt", - default: null, - }, - /** Whether or not a response to this question must be given in order to continue. For likert-table questions, this applies to all statements in the table. */ - required: { - type: ParameterType.BOOL, - pretty_name: "Required", - default: false, - }, - /** Name of the question in the trial data. If no name is given, the questions are named P0_Q0, P0_Q1, etc. Names must be unique across pages. */ - name: { - type: ParameterType.STRING, - pretty_name: "Question Name", - default: "", - }, - /** - * Likert only: Array of objects that defines the rating scale values. - * Each object defines a single rating option and must have a "value" property (integer or string). - * Each object can optionally have a "text" property (string) that contains a different text label that should be displayed for the rating option. - * If this array is not provided, then the likert_scale_min/max/stepsize values will be used to generate the scale. - */ - likert_scale_values: { - type: ParameterType.COMPLEX, - pretty_name: "Likert scale values", - default: null, - array: true, - }, - /** Likert only: Minimum rating scale value. */ - likert_scale_min: { - type: ParameterType.INT, - pretty_name: "Likert scale min", - default: 1, - }, - /** Likert only: Maximum rating scale value. */ - likert_scale_max: { - type: ParameterType.INT, - pretty_name: "Likert scale max", - default: 5, - }, - /** Likert only: Step size for generating rating scale values between the minimum and maximum. */ - likert_scale_stepsize: { - type: ParameterType.INT, - pretty_name: "Likert scale step size", - default: 1, - }, - /** Likert only: Text description to be shown for the minimum (first) rating option. */ - likert_scale_min_label: { - type: ParameterType.STRING, - pretty_name: "Likert scale min label", - default: null, - }, - /** Likert only: Text description to be shown for the maximum (last) rating option. */ - likert_scale_max_label: { - type: ParameterType.STRING, - pretty_name: "Likert scale max label", - default: null, - }, - /** Likert-table only: array of objects, where each object represents a single statement/question to be displayed in a table row. */ - statements: { - type: ParameterType.COMPLEX, - pretty_name: "Statements", - array: true, - default: null, - nested: { - /** Statement text */ - prompt: { - type: ParameterType.STRING, - pretty_name: "Prompt", - default: null, - }, - /** Identifier for the statement in the trial data. If none is given, the statements will be named "S0", "S1", etc. */ - name: { - type: ParameterType.STRING, - pretty_name: "Name", - default: null, - }, - }, - }, - /** Likert-table only: Whether or not to randomize the order of statements (rows) in the likert table. */ - randomize_statement_order: { - type: ParameterType.BOOL, - pretty_name: "Randomize statement order", - default: false, - }, - /** - * Drop-down only: Text to be displayed in the drop-down menu as a prompt for making a selection. - * This text is not a valid answer, so submitting this selection will produce an error if a response is required. - * For a blank prompt, use a space character (" "). - */ - dropdown_select_prompt: { - type: ParameterType.STRING, - pretty_name: "Drop-down select prompt", - default: "Choose...", - }, - /** Drop-down/multi-choice/multi-select/likert-table/ranking only: Array of strings that contains the set of multiple choice options to display for the question. */ - options: { - type: ParameterType.STRING, - pretty_name: "Options", - default: null, - array: true, - }, - /** Drop-down/multi-choice/multi-select/ranking only: re-ordering of options array */ - option_reorder: { - type: ParameterType.SELECT, - pretty_name: "Option reorder", - options: ["none", "asc", "desc", "random"], - default: "none", - }, - /** - * Multi-choice/multi-select only: The number of columns that should be used for displaying the options. - * If 1 (default), the choices will be displayed in a single column (vertically). - * If 0, choices will be displayed in a single row (horizontally). - * Any value greater than 1 can be used to display options in multiple columns. - */ - columns: { - type: ParameterType.INT, - pretty_name: "Columns", - default: 1, - }, - /** - * Drop-down/multi-choice/multi-select/ranking only: Whether or not to include an additional "other" option. - * If true, an "other" radio/checkbox option will be added on to the list multi-choice/multi-select options. - * Selecting this option will automatically produce a textbox to allow the participant to write in a response. - */ - add_other_option: { - type: ParameterType.BOOL, - pretty_name: "Add other option", - default: false, - }, - /** Drop-down/multi-choice/multi-select/ranking only: If add_other_option is true, then this is the text label for the "other" option. */ - other_option_text: { - type: ParameterType.BOOL, - pretty_name: "Other option text", - default: "Other", - }, - /** Text only: Placeholder text in the response text box. */ - placeholder: { - type: ParameterType.STRING, - pretty_name: "Placeholder", - default: "", - }, - /** Text only: The number of rows (height) for the response text box. */ - textbox_rows: { - type: ParameterType.INT, - pretty_name: "Textbox rows", - default: 1, - }, - /** Text only: The number of columns (width) for the response text box. */ - textbox_columns: { - type: ParameterType.INT, - pretty_name: "Textbox columns", - default: 40, - }, - /** - * Text only: Type for the HTML element. - * The `input_type` parameter must be one of "color", "date", "datetime-local", "email", "month", "number", "password", "range", "tel", "text", "time", "url", "week". - * If the `textbox_rows` parameter is larger than 1, the `input_type` parameter will be ignored. - * The `textbox_columns` parameter only affects questions with `input_type` "email", "password", "tel", "url", or "text". - */ - input_type: { - type: ParameterType.SELECT, - pretty_name: "Input type", - default: "text", - options: [ - "color", - "date", - "datetime-local", - "email", - "month", - "number", - "password", - "range", - "tel", - "text", - "time", - "url", - "week", - ], - }, - /** - * All question types except HTML: value of the correct response. If specified, the response will be compared to this value, - * and an additional data property "correct" will store response accuracy (true or false). - */ - correct_response: { - // TO DO: add correct response and accuracy scoring to data - type: ParameterType.STRING, - pretty_name: "Correct response", - default: null, - }, - }, - }, - /** Whether or not to randomize the question order on each page */ - randomize_question_order: { - type: ParameterType.BOOL, - pretty_name: "Randomize question order", - default: false, - }, - /** Label of the button to move forward thorugh survey pages. */ - button_label_next: { - type: ParameterType.STRING, - pretty_name: "Next button label", - default: "Next", - }, - /** Label of the button to move backward through survey pages. */ - button_label_back: { - type: ParameterType.STRING, - pretty_name: "Back button label", - default: "Back", - }, - /** Label of the button to submit responses. */ - button_label_finish: { + survey_json: { type: ParameterType.STRING, - pretty_name: "Finish button label", - default: "Finish", + default: {}, }, - /** Setting this to true will enable browser auto-complete or auto-fill for the form. */ - autocomplete: { - // TO DO: add auto-complete settings - type: ParameterType.BOOL, - pretty_name: "Allow autocomplete", - default: false, - }, - /** - * Whether or not to show numbers next to each question prompt. Options are: - * "on": questions will be labelled starting with "1." on the first page, and numbering will continue across pages. - * "onPage": questions will be labelled starting with "1.", with separate numbering on each page. - * "off": no question numbering. - */ - show_question_numbers: { - type: ParameterType.SELECT, - pretty_name: "Show question numbers", - default: "off", - options: ["on", "onPage", "off"], - }, - /** - * HTML-formatted text to be shown at the top of the survey pages. This also provides a method for fixing any arbitrary text to the top of the page when - * randomizing the question order, since HTML question types are also randomized. - */ - title: { - type: ParameterType.STRING, - pretty_name: "Title", + validation_function: { + type: ParameterType.FUNCTION, default: null, }, - /** Text to display if a required answer is not responded to. */ - required_error_text: { - type: ParameterType.STRING, - pretty_name: "Required error text", - default: "Please answer the question.", - }, - /** String to display at the end of required questions. */ - required_question_label: { - type: ParameterType.STRING, - pretty_name: "Required question label", - default: "*", - }, }, }; type Info = typeof info; -// available parameters for each question type -const all_question_params_req = ["type", "prompt"]; -const all_question_params_opt = ["name", "required"]; -const all_question_params = [...all_question_params_req, ...all_question_params_opt]; -const dropdown_params = [ - ...all_question_params, - "options", - "option_reorder", - "add_other_option", - "other_option_text", - "dropdown_select_prompt", - "correct_response", -]; -const html_params = [...all_question_params]; -const likert_params = [ - ...all_question_params, - "likert_scale_values", - "likert_scale_min", - "likert_scale_max", - "likert_scale_stepsize", - "likert_scale_min_label", - "likert_scale_max_label", - "correct_response", -]; -const likert_table_params = [ - ...all_question_params, - "statements", - "options", - "randomize_statement_order", - "correct_response", -]; -const multichoice_params = [ - ...all_question_params, - "options", - "option_reorder", - "columns", - "add_other_option", - "other_option_text", - "correct_response", -]; -const text_params = [ - ...all_question_params, - "placeholder", - "textbox_rows", - "textbox_columns", - "input_type", - "correct_response", -]; - -const question_types = [ - "drop-down", - "html", - "likert", - "likert-table", - "multi-choice", - "multi-select", - "ranking", - "text", - "comment", -]; - /** * **survey** * - * jsPsych plugin for presenting survey questions (questionnaires) - SurveyJS version + * jsPsych plugin for presenting complex questionnaires using the SurveyJS library * * @author Becky Gilbert * @see {@link https://www.jspsych.org/plugins/survey/ survey plugin documentation on jspsych.org} */ class SurveyPlugin implements JsPsychPlugin { static info = info; - private survey: Survey; - private trial_data: any = {}; + private survey: $.Survey; + private start_time: number; - constructor(private jsPsych: JsPsych) {} - - applyStyles() { - // https://surveyjs.io/Examples/Library/?id=custom-theme - const colors = StylesManager.ThemeColors["default"]; - - colors["$background-dim"] = "#f3f3f3"; - colors["$body-background-color"] = "white"; - colors["$body-container-background-color"] = "white"; - colors["$border-color"] = "#e7e7e7"; - colors["$disable-color"] = "#dbdbdb"; - colors["$disabled-label-color"] = "rgba(64, 64, 64, 0.5)"; - colors["$disabled-slider-color"] = "#cfcfcf"; - colors["$disabled-switch-color"] = "#9f9f9f"; - colors["$error-background-color"] = "#fd6575"; - colors["$error-color"] = "#ed5565"; - colors["$foreground-disabled"] = "#161616"; - //colors['$foreground-light'] = "orange" - colors["$header-background-color"] = "white"; - colors["$header-color"] = "#6d7072"; - colors["$inputs-background-color"] = "white"; - colors["$main-color"] = "#919191"; - colors["$main-hover-color"] = "#6b6b6b"; - colors["$progress-buttons-color"] = "#8dd9ca"; - colors["$progress-buttons-line-color"] = "#d4d4d4"; - colors["$progress-text-color"] = "#9d9d9d"; - colors["$slider-color"] = "white"; - colors["$text-color"] = "#6d7072"; - colors["$text-input-color"] = "#6d7072"; + constructor(private jsPsych: JsPsych) { + this.jsPsych = jsPsych; + } - StylesManager.applyTheme(); + applyStyles(survey) { + // TO DO: this method of applying custom styles is deprecated, but I'm + // saving this here for reference while we make decisions about default style + + // const colors = StylesManager.ThemeColors["default"]; + + // colors["$background-dim"] = "#f3f3f3"; + // colors["$body-background-color"] = "white"; + // colors["$body-container-background-color"] = "white"; + // colors["$border-color"] = "#e7e7e7"; + // colors["$disable-color"] = "#dbdbdb"; + // colors["$disabled-label-color"] = "rgba(64, 64, 64, 0.5)"; + // colors["$disabled-slider-color"] = "#cfcfcf"; + // colors["$disabled-switch-color"] = "#9f9f9f"; + // colors["$error-background-color"] = "#fd6575"; + // colors["$error-color"] = "#ed5565"; + // colors["$foreground-disabled"] = "#161616"; + // //colors['$foreground-light'] = "orange" + // colors["$header-background-color"] = "white"; + // colors["$header-color"] = "#6d7072"; + // colors["$inputs-background-color"] = "white"; + // colors["$main-color"] = "#919191"; + // colors["$main-hover-color"] = "#6b6b6b"; + // colors["$progress-buttons-color"] = "#8dd9ca"; + // colors["$progress-buttons-line-color"] = "#d4d4d4"; + // colors["$progress-text-color"] = "#9d9d9d"; + // colors["$slider-color"] = "white"; + // colors["$text-color"] = "#6d7072"; + // colors["$text-input-color"] = "#6d7072"; + + // StylesManager.applyTheme(); + + // Updated method for creating custom themes + // https://surveyjs.io/form-library/documentation/manage-default-themes-and-styles#create-a-custom-theme + + survey.applyTheme({ + cssVariables: { + "--sjs-general-backcolor": "rgba(255, 255, 255, 1)", + "--sjs-general-backcolor-dim": "rgba(255, 255, 255, 1)", // panel background color + "--sjs-general-backcolor-dim-light": "rgba(249, 249, 249, 1)", // input element background, including single next or previous buttons + "--sjs-general-forecolor": "rgba(0, 0, 0, 0.91)", + "--sjs-general-forecolor-light": "rgba(0, 0, 0, 0.45)", + "--sjs-general-dim-forecolor": "rgba(0, 0, 0, 0.91)", + "--sjs-general-dim-forecolor-light": "rgba(0, 0, 0, 0.45)", + "--sjs-primary-backcolor": "#474747", // title, selected input border, next/submit button background, previous button text color + "--sjs-primary-backcolor-light": "rgba(0, 0, 0, 0.1)", + "--sjs-primary-backcolor-dark": "#000000", // next/submit button hover backgound + "--sjs-primary-forecolor": "rgba(255, 255, 255, 1)", // next/submit button text color + "--sjs-primary-forecolor-light": "rgba(255, 255, 255, 0.25)", + }, + themeName: "plain", + colorPalette: "light", + isPanelless: true, + }); } trial(display_element: HTMLElement, trial: TrialType) { - this.survey = new Survey(); // set up survey in code: https://surveyjs.io/Documentation/Library#survey-objects - this.applyStyles(); // applies bootstrap theme - - // add custom CSS classes to survey elements - // https://surveyjs.io/Examples/Library/?id=survey-customcss&platform=Knockoutjs&theme=bootstrap#content-docs - this.survey.css = { - // root: "sv_main sv_bootstrap_css jspsych-survey-question", - // question: { - // mainRoot: "sv_qstn jspsych-survey-question", - // flowRoot: "sv_q_flow sv_qstn jspsych-survey-question", - // title: "jspsych-survey-question-prompt", - // requiredText: "sv_q_required_text jspsych-survey-required", - // }, - // html: { - // root: "jspsych-survey-html", - // }, - // navigationButton: "jspsych-btn jspsych-survey-btn", - // dropdown: { - // control: "jspsych-survey-dropdown", - // }, - // error: { - // root: "alert alert-danger jspsych-survey-required", - // }, - }; - - // navigation buttons - this.survey.pagePrevText = trial.button_label_back; - this.survey.pageNextText = trial.button_label_next; - this.survey.completeText = trial.button_label_finish; - - // page numbers - this.survey.showQuestionNumbers = trial.show_question_numbers; - - // survey title - if (trial.title !== null) { - this.survey.title = trial.title; - } - - // required question label - this.survey.requiredText = trial.required_question_label; - - // TO DO: add response validation - this.survey.checkErrorsMode = "onNextPage"; // onValueChanged - - // initialize trial data - this.trial_data.accuracy = []; - this.trial_data.question_order = []; - - // response scoring function - const score_response = (sender, options) => { - if (options.question?.correctAnswer) { - this.trial_data.accuracy.push({ - [options.name]: options.question.correctAnswer == options.value, - }); - } - }; - - // pages and questions - for (const [pageIndex, questions] of trial.pages.entries()) { - const page = this.survey.addNewPage(`page${pageIndex}`); - - if (trial.randomize_question_order) { - page.questionsOrder = "random"; // TO DO: save question presentation order to data - } - for (const [questionIndex, question_params] of (questions as any[]).entries()) { - let question_type = question_params.type; - - if (typeof question_type === "undefined") { - throw new Error( - 'Error in survey plugin: question is missing the required "type" parameter.' - ); - } - if (!question_types.includes(question_type)) { - throw new Error(`Error in survey plugin: invalid question type "${question_type}".`); - } - - // set up question + this.survey = new SurveyJS.Model(trial.survey_json); - const setup_function = { - "drop-down": this.setup_dropdown_question, - html: this.setup_html_question, - "likert-table": this.setup_likert_table_question, - "multi-choice": this.setup_multichoice_question, - "multi-select": this.setup_multichoice_question, - ranking: this.setup_multichoice_question, - likert: this.setup_likert_question, - text: this.setup_text_question, - }[question_type]; + //this.survey.applyTheme(PlainLightPanelless); // TO DO: can we apply this theme and still customize some values? + this.applyStyles(this.survey); // customize colors - const question = setup_function( - question_params.name ?? `P${pageIndex}_Q${questionIndex}`, - question_params - ); - question.requiredErrorText = trial.required_error_text; - page.addQuestion(question); - } + if (trial.validation_function) { + this.survey.onValidateQuestion.add(trial.validation_function); } - // add the accuracy scoring for questions with a "correct_response" parameter value - // TO DO: onValueChanged is not the right method to use for this because it doesn't score responses when - // a value is not changed (i.e. no response or default/placeholder response) - this.survey.onValueChanged.add(score_response); - - // render the survey and record start time - this.survey.render(display_element); - - const start_time = performance.now(); - this.survey.onComplete.add((sender, options) => { - // clear display - display_element.innerHTML = ""; // add default values to any questions without responses const all_questions = sender.getAllQuestions(); + console.log("all questions: ", all_questions); const data_names = Object.keys(sender.data); + console.log("data names: ", data_names); for (const question of all_questions) { + console.log("question name: ", question.name); if (!data_names.includes(question.name)) { + console.log("data names does not include ", question.name); + console.log("question default value: ", question.defaultValue); sender.mergeData({ [question.name]: question.defaultValue ?? null }); } } - // TO DO: restructure survey data (sender.data) here? + // clear display and reset flex on jspsych-content-wrapper + display_element.innerHTML = ""; + $(".jspsych-content-wrapper").css("display", "flex"); + // finish trial and save data this.jsPsych.finishTrial({ - rt: Math.round(performance.now() - start_time), + rt: Math.round(performance.now() - this.start_time), response: sender.data, - accuracy: this.trial_data.accuracy, }); }); - } - - /** - * Validate parameters for any question type - * - * @param supplied - * @param required - * @param optional - * @returns - */ - private static validate_question_params( - supplied: Record, - required: string[], - optional: string[] - ) { - required = [...all_question_params_req, ...required]; - optional = [...all_question_params_opt, ...optional]; - for (const param of required) { - if (!supplied.hasOwnProperty(param)) { - throw new Error( - param === "type" - ? 'Error in survey plugin: question is missing the required "type" parameter.' - : `Error in survey plugin: question is missing required parameter "${param}" for question type "${supplied.type}".` - ); - } - } + // remove flex display from jspsych-content-wrapper to get formatting to work + $(".jspsych-content-wrapper").css("display", "block"); - const invalid_params = Object.keys(supplied).filter( - (param) => !(optional.includes(param) || required.includes(param)) - ); + $(display_element).Survey({ model: this.survey }); - if (invalid_params.length > 0) { - console.warn( - `Warning in survey plugin: the following question parameters have been specified but are not allowed for the question type "${supplied.type}" and will be ignored: ${invalid_params}` - ); - } + this.start_time = performance.now(); } - - /** - * Set defaults for undefined question-specific parameters - **/ - private static set_question_defaults = ( - supplied_params: Record, - available_params: string[] - ) => { - for (const param of available_params) { - if (typeof supplied_params[param] === "undefined") { - supplied_params[param] = info.parameters.pages.nested[param].default; - } - } - }; - - // methods for setting up different question types - - private setup_dropdown_question = (name: string, params) => { - SurveyPlugin.validate_question_params( - params, - ["options"], - [ - "option_reorder", - "add_other_option", - "other_option_text", - "dropdown_select_prompt", - "correct_response", - ] - ); - - SurveyPlugin.set_question_defaults(params, dropdown_params); - - const question = new QuestionDropdown(name); - - question.title = params.prompt; - question.isRequired = params.required; - question.hasOther = params.add_other_option; - question.optionsCaption = params.dropdown_select_prompt; - if (question.hasOther) { - question.otherText = params.other_option_text; - } - question.choices = params.options; - if (typeof params.option_reorder === "undefined") { - question.choicesOrder = info.parameters.pages.nested.option_reorder.default; - } else { - question.choicesOrder = params.option_reorder; - } - if (params.correct_response !== null) { - question.correctAnswer = params.correct_response; - } - question.defaultValue = ""; - - return question; - }; - - private setup_html_question = (name: string, params) => { - SurveyPlugin.validate_question_params(params, [], []); - SurveyPlugin.set_question_defaults(params, html_params); - - const question = new QuestionHtml(name); - question.html = params.prompt; - - return question; - }; - - private setup_likert_question = (name: string, params) => { - SurveyPlugin.validate_question_params( - params, - [], - [ - "likert_scale_values", - "likert_scale_min", - "likert_scale_max", - "likert_scale_stepsize", - "likert_scale_min_label", - "likert_scale_max_label", - "correct_response", - ] - ); - - SurveyPlugin.set_question_defaults(params, likert_params); - - const question = new QuestionRating(name); - - question.title = params.prompt; - question.isRequired = params.required; - if (params.likert_scale_values !== null) { - question.rateValues = params.likert_scale_values; - } else { - question.rateMin = params.likert_scale_min; - question.rateMax = params.likert_scale_max; - question.rateStep = params.likert_scale_stepsize; - } - if (params.likert_scale_min_label !== null) { - question.minRateDescription = params.likert_scale_min_label; - } - if (params.likert_scale_min_label !== null) { - question.maxRateDescription = params.likert_scale_max_label; - } - if (params.correct_response !== null) { - question.correctAnswer = params.correct_response; - } - // TO DO: add likert default value (empty string?: question.defaultValue = "";) - - return question; - }; - - private setup_likert_table_question = (name: string, params) => { - SurveyPlugin.validate_question_params( - params, - ["options", "statements"], - ["randomize_statement_order", "correct_response"] - ); - - SurveyPlugin.set_question_defaults(params, likert_table_params); - - const question = new QuestionMatrix(name); - - question.title = params.prompt; - question.isAllRowRequired = params.required; - question.columns = params.options.map((opt: string, ind: number) => ({ - value: ind, - text: opt, - })); - question.rows = params.statements.map((stmt: { name: string; prompt: string }) => ({ - value: stmt.name, - text: stmt.prompt, - })); - question.rowsOrder = params.randomize_statement_order ? "random" : "initial"; - if (params.correct_response !== null) { - question.correctAnswer = params.correct_response; - } - // TO DO: add likert-table default value (empty array?: question.defaultValue = [];) - - return question; - }; - - // multi-choice, multi-select, ranking - private setup_multichoice_question = (name: string, params) => { - SurveyPlugin.validate_question_params( - params, - ["options"], - ["columns", "option_reorder", "add_other_option", "other_option_text", "correct_response"] - ); - - SurveyPlugin.set_question_defaults(params, multichoice_params); - - let question: QuestionRadiogroup | QuestionCheckbox | QuestionRanking; - switch (params.type) { - case "multi-choice": - question = new QuestionRadiogroup(name); - question.defaultValue = ""; - break; - - case "multi-select": - question = new QuestionCheckbox(name); - question.defaultValue = []; - break; - - case "ranking": - question = new QuestionRanking(name); - break; - } - - question.title = params.prompt; - question.isRequired = params.required; - question.hasOther = params.add_other_option; - if (question.hasOther) { - question.otherText = params.other_option_text; - } - question.choices = params.options; - if (typeof params.option_reorder === "undefined") { - question.choicesOrder = info.parameters.pages.nested.option_reorder.default; - } else { - question.choicesOrder = params.option_reorder; - } - question.colCount = params.columns; - if (params.correct_response !== null) { - question.correctAnswer = params.correct_response; - } - - if (question instanceof QuestionRanking) { - // Hack to initialize `question.dragDropRankingChoices` which is only done by the - // `endLoadingFromJson()` method - question.endLoadingFromJson(); - } - - return question; - }; - - // text or comment - private setup_text_question = (name: string, params) => { - SurveyPlugin.validate_question_params( - params, - [], - ["placeholder", "textbox_rows", "textbox_columns", "input_type", "correct_response"] - ); - - SurveyPlugin.set_question_defaults(params, text_params); - - const question = params.textbox_rows > 1 ? new QuestionComment(name) : new QuestionText(name); - - question.title = params.prompt; - question.isRequired = params.required; - question.placeHolder = params.placeholder; - if (params.correct_response !== null) { - question.correctAnswer = params.correct_response; - } - if (question instanceof QuestionComment) { - question.rows = params.textbox_rows; - question.cols = params.textbox_columns; - } else { - question.size = params.textbox_columns; - question.inputType = params.input_type; - } - question.defaultValue = ""; - - return question; - }; } export default SurveyPlugin;