mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 11:10:54 +00:00
(rebased-with-history from commit 6d99a71fb1
)
This commit is contained in:
commit
15d7d73528
5
.changeset/bright-pillows-collect.md
Normal file
5
.changeset/bright-pillows-collect.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jspsych/plugin-survey": major
|
||||
---
|
||||
|
||||
To take advantage of all of the SurveyJS features, we have re-written the survey plugin so that it now takes a SurveyJS-compatible JSON string ('survey_json') and/or a SurveyJS-compatible function ('survey_function') that manipulates a SurveyJS model. This is a breaking change. See the jsPsych Survey Plugin page for documentation and examples: https://www.jspsych.org/latest/plugins/survey/. More details about creating the SurveyJS JSON strings and functions can be found on their website: https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#create-a-survey-model.
|
@ -4,9 +4,9 @@
|
||||
<script src="docs-demo-timeline.js"></script>
|
||||
<script src="https://unpkg.com/jspsych@7.3.4"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.1.3"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-survey@0.2.2"></script>
|
||||
<script src="../../packages/plugin-survey/dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.3.4/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/@jspsych/plugin-survey@0.2.2/css/survey.css">
|
||||
<link rel="stylesheet" href="../../packages/plugin-survey/css/survey.css">
|
||||
<link rel="stylesheet" href="docs-demo.css" type="text/css">
|
||||
</head>
|
||||
<body></body>
|
||||
@ -16,28 +16,29 @@
|
||||
|
||||
const trial = {
|
||||
type: jsPsychSurvey,
|
||||
pages: [
|
||||
[
|
||||
{
|
||||
type: 'html',
|
||||
prompt: 'Please answer the following questions:',
|
||||
},
|
||||
{
|
||||
type: 'multi-choice',
|
||||
prompt: "Which of the following do you like the most?",
|
||||
name: 'VegetablesLike',
|
||||
options: ['Tomato', 'Cucumber', 'Eggplant', 'Corn', 'Peas'],
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'multi-select',
|
||||
prompt: "Which of the following do you like?",
|
||||
name: 'FruitLike',
|
||||
options: ['Apple', 'Banana', 'Orange', 'Grape', 'Strawberry'],
|
||||
required: false,
|
||||
}
|
||||
survey_json: {
|
||||
showQuestionNumbers: false,
|
||||
elements:
|
||||
[
|
||||
{
|
||||
type: 'radiogroup',
|
||||
title: "Which of the following do you like the most?",
|
||||
name: 'vegetablesLike',
|
||||
choices: ['Tomato', 'Cucumber', 'Eggplant', 'Corn', 'Peas', 'Broccoli']
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
title: "Which of the following do you like?",
|
||||
name: 'fruitLike',
|
||||
description: "You can select as many as you want.",
|
||||
choices: ['Apple', 'Banana', 'Orange', 'Grape', 'Strawberry', 'Kiwi', 'Mango'],
|
||||
showOtherItem: true,
|
||||
showSelectAllItem: true,
|
||||
showNoneItem: true,
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const timeline = [trial];
|
||||
|
@ -5,9 +5,9 @@
|
||||
<script src="docs-demo-timeline.js"></script>
|
||||
<script src="https://unpkg.com/jspsych@7.3.4"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.1.3"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-survey@0.2.2"></script>
|
||||
<script src="../../packages/plugin-survey/dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.3.4/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/@jspsych/plugin-survey@0.2.2/css/survey.css">
|
||||
<link rel="stylesheet" href="../../packages/plugin-survey/css/survey.css">
|
||||
<link rel="stylesheet" href="docs-demo.css" type="text/css">
|
||||
</head>
|
||||
<body></body>
|
||||
@ -17,45 +17,60 @@
|
||||
|
||||
const trial = {
|
||||
type: jsPsychSurvey,
|
||||
pages: [
|
||||
[
|
||||
survey_json: {
|
||||
showQuestionNumbers: false,
|
||||
title: 'My questionnaire',
|
||||
completeText: 'Done!',
|
||||
pageNextText: 'Continue',
|
||||
pagePrevText: 'Previous',
|
||||
pages: [
|
||||
{
|
||||
type: 'text',
|
||||
prompt: "Where were you born?",
|
||||
placeholder: 'City, State, Country',
|
||||
name: 'birthplace',
|
||||
required: true,
|
||||
},
|
||||
name: 'page1',
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
title: 'Where were you born?',
|
||||
placeholder: 'City, State/Region, Country',
|
||||
name: 'birthplace',
|
||||
size: 30,
|
||||
isRequired: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
title: 'How old are you?',
|
||||
name: 'age',
|
||||
isRequired: false,
|
||||
inputType: 'number',
|
||||
min: 0,
|
||||
max: 100,
|
||||
defaultValue: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
prompt: "How old are you?",
|
||||
name: 'age',
|
||||
textbox_columns: 5,
|
||||
required: false,
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'multi-choice',
|
||||
prompt: "What's your favorite color?",
|
||||
options: ['blue','yellow','pink','teal','orange','lime green','other','none of these'],
|
||||
name: 'FavColor',
|
||||
},
|
||||
{
|
||||
type: 'multi-select',
|
||||
prompt: "Which of these animals do you like? Select all that apply.",
|
||||
options: ['lion','squirrel','badger','whale'],
|
||||
option_reorder: 'random',
|
||||
columns: 0,
|
||||
name: 'AnimalLike',
|
||||
name: 'page2',
|
||||
elements: [
|
||||
{
|
||||
type: 'radiogroup',
|
||||
title: "What's your favorite color?",
|
||||
choices: ['Blue','Yellow','Pink','Teal','Orange','Lime green'],
|
||||
showNoneItem: true,
|
||||
showOtherItem: true,
|
||||
colCount: 0,
|
||||
name: 'FavColor',
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
title: 'Which of these animals do you like? Select all that apply.',
|
||||
choices: ['Lion','Squirrel','Badger','Whale', 'Turtle'],
|
||||
choicesOrder: 'random',
|
||||
colCount: 0,
|
||||
name: 'FavAnimals',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
title: 'My questionnaire',
|
||||
button_label_next: 'Continue',
|
||||
button_label_back: 'Previous',
|
||||
button_label_finish: 'Submit',
|
||||
show_question_numbers: 'onPage'
|
||||
}
|
||||
};
|
||||
|
||||
const timeline = [trial];
|
||||
|
@ -5,9 +5,9 @@
|
||||
<script src="docs-demo-timeline.js"></script>
|
||||
<script src="https://unpkg.com/jspsych@7.3.4"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.1.3"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-survey@0.2.2"></script>
|
||||
<script src="../../packages/plugin-survey/dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.3.4/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/@jspsych/plugin-survey@0.2.2/css/survey.css">
|
||||
<link rel="stylesheet" href="../../packages/plugin-survey/css/survey.css">
|
||||
<link rel="stylesheet" href="docs-demo.css" type="text/css">
|
||||
</head>
|
||||
<body></body>
|
||||
@ -17,62 +17,79 @@
|
||||
|
||||
const trial = {
|
||||
type: jsPsychSurvey,
|
||||
pages: [
|
||||
[
|
||||
survey_json: {
|
||||
showQuestionNumbers: false,
|
||||
title: 'Likert scale examples',
|
||||
pages: [
|
||||
{
|
||||
type: 'likert',
|
||||
prompt: 'I like to eat vegetables.',
|
||||
likert_scale_min_label: 'Strongly Disagree',
|
||||
likert_scale_max_label: 'Strongly Agree',
|
||||
likert_scale_values: [
|
||||
{value: 1},
|
||||
{value: 2},
|
||||
{value: 3},
|
||||
{value: 4},
|
||||
{value: 5}
|
||||
elements: [
|
||||
{
|
||||
type: 'rating',
|
||||
name: 'like-vegetables',
|
||||
title: 'I like to eat vegetables.',
|
||||
description: 'Button rating scale with min/max descriptions',
|
||||
minRateDescription: 'Strongly Disagree',
|
||||
maxRateDescription: 'Strongly Agree',
|
||||
displayMode: 'buttons',
|
||||
rateValues: [1,2,3,4,5]
|
||||
},
|
||||
{
|
||||
type: 'rating',
|
||||
name: 'like-cake',
|
||||
title: 'I like to eat cake.',
|
||||
description: 'Star rating scale with min/max descriptions',
|
||||
minRateDescription: 'Strongly Disagree',
|
||||
maxRateDescription: 'Strongly Agree',
|
||||
rateType: 'stars',
|
||||
rateCount: 10,
|
||||
rateMax: 10,
|
||||
},
|
||||
{
|
||||
type: 'rating',
|
||||
name: 'like-cooking',
|
||||
title: 'How much do you enjoy cooking?',
|
||||
description: 'Smiley rating scale without min/max descriptions',
|
||||
rateType: 'smileys',
|
||||
rateCount: 10,
|
||||
rateMax: 10,
|
||||
scaleColorMode: 'colored',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'likert',
|
||||
prompt: 'I like to eat fruit.',
|
||||
likert_scale_min_label: 'Strongly Disagree',
|
||||
likert_scale_max_label: 'Strongly Agree',
|
||||
likert_scale_values: [
|
||||
{value: 1},
|
||||
{value: 2},
|
||||
{value: 3},
|
||||
{value: 4},
|
||||
{value: 5}
|
||||
}, {
|
||||
elements: [
|
||||
{
|
||||
type: 'matrix',
|
||||
name: 'like-food-matrix',
|
||||
title: 'Matrix question for rating mutliple statements on the same scale.',
|
||||
alternateRows: true,
|
||||
isAllRowRequired: true,
|
||||
rows: [
|
||||
{text: 'I like to eat vegetables.', value: 'VeggiesTable'},
|
||||
{text: 'I like to eat fruit.', value: 'FruitTable'},
|
||||
{text: 'I like to eat cake.', value: 'CakeTable'},
|
||||
{text: 'I like to cook.', value: 'CookTable'},
|
||||
],
|
||||
columns: [{
|
||||
"value": 5,
|
||||
"text": "Strongly agree"
|
||||
}, {
|
||||
"value": 4,
|
||||
"text": "Agree"
|
||||
}, {
|
||||
"value": 3,
|
||||
"text": "Neutral"
|
||||
}, {
|
||||
"value": 2,
|
||||
"text": "Disagree"
|
||||
}, {
|
||||
"value": 1,
|
||||
"text": "Strongly disagree"
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'likert',
|
||||
prompt: 'I like to eat meat.',
|
||||
likert_scale_min_label: 'Strongly Disagree',
|
||||
likert_scale_max_label: 'Strongly Agree',
|
||||
likert_scale_values: [
|
||||
{value: 1},
|
||||
{value: 2},
|
||||
{value: 3},
|
||||
{value: 4},
|
||||
{value: 5}
|
||||
]
|
||||
},
|
||||
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'likert-table',
|
||||
prompt: ' ',
|
||||
statements: [
|
||||
{prompt: 'I like to eat vegetables', name: 'VeggiesTable'},
|
||||
{prompt: 'I like to eat fruit', name: 'FruitTable'},
|
||||
{prompt: 'I like to eat meat', name: 'MeatTable'},
|
||||
],
|
||||
options: ['Strongly Disagree', 'Disagree', 'Neutral', 'Agree', 'Strongly Agree'],
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
const timeline = [trial];
|
||||
|
@ -5,9 +5,9 @@
|
||||
<script src="docs-demo-timeline.js"></script>
|
||||
<script src="https://unpkg.com/jspsych@7.3.4"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.1.3"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-survey@0.2.2"></script>
|
||||
<script src="../../packages/plugin-survey/dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.3.4/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/@jspsych/plugin-survey@0.2.2/css/survey.css">
|
||||
<link rel="stylesheet" href="../../packages/plugin-survey/css/survey.css">
|
||||
<link rel="stylesheet" href="docs-demo.css" type="text/css">
|
||||
</head>
|
||||
<body></body>
|
||||
@ -15,19 +15,57 @@
|
||||
|
||||
const jsPsych = initJsPsych();
|
||||
|
||||
const survey_function = (survey) => {
|
||||
// If it's the question page, then hide the buttons and move on automatically.
|
||||
// If it's the feedback page, then show the navigation buttons.
|
||||
function updateNavButtons(sender, options) {
|
||||
if (options.newCurrentPage.getPropertyValue("name") === "feedback") {
|
||||
survey.showNavigationButtons = "bottom";
|
||||
} else {
|
||||
survey.showNavigationButtons = "none";
|
||||
}
|
||||
}
|
||||
survey.onCurrentPageChanging.add(updateNavButtons);
|
||||
}
|
||||
|
||||
const trial = {
|
||||
type: jsPsychSurvey,
|
||||
pages: [
|
||||
[
|
||||
{
|
||||
type: 'multi-choice',
|
||||
prompt: 'During the experiment, are allowed to write things down on paper to help you?',
|
||||
options: ["Yes", "No"],
|
||||
correct_response: "No",
|
||||
required: true
|
||||
},
|
||||
]
|
||||
],
|
||||
survey_json: {
|
||||
showQuestionNumbers: false,
|
||||
title: 'Conditional question visibility.',
|
||||
showNavigationButtons: "none",
|
||||
goNextPageAutomatic: true,
|
||||
allowCompleteSurveyAutomatic: true,
|
||||
pages: [{
|
||||
name: 'question',
|
||||
elements: [
|
||||
{
|
||||
type: 'radiogroup',
|
||||
title: 'During the experiment, are you allowed to write things down on paper to help you?',
|
||||
choices: ["Yes", "No"],
|
||||
name: "WriteOK",
|
||||
isRequired: true
|
||||
}
|
||||
],
|
||||
}, {
|
||||
name: 'feedback',
|
||||
elements: [
|
||||
{
|
||||
type: 'html',
|
||||
name: 'incorrect',
|
||||
visibleIf: '{WriteOK} = "Yes"',
|
||||
html: '<h4>That response was incorrect.</h4><p>Please return to the previous page and try again.</p>'
|
||||
},
|
||||
{
|
||||
type: 'html',
|
||||
name: 'correct',
|
||||
visibleIf: '{WriteOK} == "No"',
|
||||
html: '<h4>Congratulations!</h4>'
|
||||
}
|
||||
]
|
||||
}]
|
||||
},
|
||||
survey_function: survey_function
|
||||
};
|
||||
|
||||
const timeline = [trial];
|
||||
|
@ -5,9 +5,9 @@
|
||||
<script src="docs-demo-timeline.js"></script>
|
||||
<script src="https://unpkg.com/jspsych@7.3.4"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.1.3"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-survey@0.2.2"></script>
|
||||
<script src="../../packages/plugin-survey/dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.3.4/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/@jspsych/plugin-survey@0.2.2/css/survey.css">
|
||||
<link rel="stylesheet" href="../../packages/plugin-survey/css/survey.css">
|
||||
<link rel="stylesheet" href="docs-demo.css" type="text/css">
|
||||
</head>
|
||||
<body></body>
|
||||
@ -15,53 +15,71 @@
|
||||
|
||||
const jsPsych = initJsPsych();
|
||||
|
||||
const question_info = [
|
||||
{
|
||||
'fruit': 'apples',
|
||||
'Q1_prompt': 'Do you like apples?',
|
||||
'Q1_type': 'regular'
|
||||
},
|
||||
{
|
||||
'fruit': 'bananas',
|
||||
'Q1_prompt': 'Do you NOT like bananas?',
|
||||
'Q1_type': 'reverse'
|
||||
},
|
||||
];
|
||||
|
||||
const survey = {
|
||||
type: jsPsychSurvey,
|
||||
pages:[
|
||||
[
|
||||
// values that change across survey trials - each object represents a single trial
|
||||
const question_variables = [
|
||||
{
|
||||
type: 'multi-choice',
|
||||
prompt: jsPsych.timelineVariable('Q1_prompt'),
|
||||
options: ['Yes', 'No'],
|
||||
name: 'Q1'
|
||||
'fruit': 'apples',
|
||||
'Q1_prompt': 'Do you like apples?',
|
||||
'Q1_type': 'regular',
|
||||
'Q2_word': 'like'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
prompt: function() {
|
||||
return `What's your favorite thing about ${jsPsych.evaluateTimelineVariable('fruit')}?`;
|
||||
},
|
||||
name: 'Q2'
|
||||
}
|
||||
]
|
||||
],
|
||||
data: {
|
||||
'Q1_prompt': jsPsych.timelineVariable('Q1_prompt'),
|
||||
'Q1_type': jsPsych.timelineVariable('Q1_type'),
|
||||
'fruit': jsPsych.timelineVariable('fruit')
|
||||
},
|
||||
button_label_finish: 'Continue'
|
||||
};
|
||||
'fruit': 'pears',
|
||||
'Q1_prompt': 'Do you like pears?',
|
||||
'Q1_type': 'regular',
|
||||
'Q2_word': 'like'
|
||||
},
|
||||
{
|
||||
'fruit': 'bananas',
|
||||
'Q1_prompt': 'Do you NOT like bananas?',
|
||||
'Q1_type': 'reverse',
|
||||
'Q2_word': 'hate'
|
||||
},
|
||||
];
|
||||
|
||||
const survey_procedure = {
|
||||
timeline: [survey],
|
||||
timeline_variables: question_info,
|
||||
randomize_order: true
|
||||
};
|
||||
// create an array to store all of our survey trials so that we can easily randomize their order
|
||||
survey_trials = [];
|
||||
|
||||
const timeline = [survey_procedure];
|
||||
// construct the survey trials dynamically using an array of question-specific information
|
||||
for (let i=0; i<question_variables.length; i++) {
|
||||
|
||||
// set up the survey JSON for this trial
|
||||
// any question-specific variables come from the appropriate object in the question_variables array
|
||||
let survey_json = {
|
||||
showQuestionNumbers: false,
|
||||
title: 'Dynamically constructing survey trials.',
|
||||
completeText: 'Next >>',
|
||||
elements: [
|
||||
{
|
||||
type: 'radiogroup',
|
||||
title: question_variables[i].Q1_prompt,
|
||||
choices: ['Yes', 'No'],
|
||||
name: 'Q1'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
title: 'What do you '+question_variables[i].Q2_word+' most about '+question_variables[i].fruit+'?',
|
||||
name: 'Q2'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// set up a survey trial object using the JSON we've just created for this question,
|
||||
// and add the trial object to the survey trials array
|
||||
survey_trials.push({
|
||||
type: jsPsychSurvey,
|
||||
survey_json: survey_json,
|
||||
data: {
|
||||
'Q1_prompt': question_variables[i].Q1_prompt,
|
||||
'Q1_type': question_variables[i].Q1_type,
|
||||
'Q2_word': question_variables[i].Q2_word,
|
||||
'fruit': question_variables[i].fruit
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const timeline = jsPsych.randomization.shuffle(survey_trials);
|
||||
|
||||
if (typeof jsPsych !== "undefined") {
|
||||
jsPsych.run(generateDocsDemoTimeline(timeline));
|
||||
|
144
docs/demos/jspsych-survey-demo6.html
Normal file
144
docs/demos/jspsych-survey-demo6.html
Normal file
@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="docs-demo-timeline.js"></script>
|
||||
<script src="https://unpkg.com/jspsych@7.3.4"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.1.3"></script>
|
||||
<script src="../../packages/plugin-survey/dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.3.4/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="../../packages/plugin-survey/css/survey.css" />
|
||||
<link rel="stylesheet" href="docs-demo.css" type="text/css" />
|
||||
<style>
|
||||
/* center the audio player and all image question types in the survey */
|
||||
div.sd-question--image,
|
||||
div.sd-question[data-name="audio-player"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* use 'data-name' to select any specific question by name */
|
||||
div[data-name="audio-response"] {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
<script>
|
||||
|
||||
const jsPsych = initJsPsych();
|
||||
|
||||
// Embed HTML, images, videos and audio into the survey
|
||||
const image_video_html_trial_info = {
|
||||
pages: [{
|
||||
elements: [{
|
||||
type: "panel",
|
||||
name: "html-img-panel",
|
||||
description: "This panel contains an HTML element and an image element.",
|
||||
elements: [{
|
||||
type: "html",
|
||||
name: "html",
|
||||
html: "<div style='text-align: center; align-items: center; align-content: center; justify-content: center;'><p style='text-align: center; color: darkgreen; font-size: 2em;'>This demo shows how you can add <em>HTML</em>, <strong>images</strong>, and <sub>video</sub> to your jsPsych survey trial.</p></div>"
|
||||
}, {
|
||||
type: "image",
|
||||
name: "monkey",
|
||||
imageLink: "img/monkey.png",
|
||||
altText: "Monkey",
|
||||
imageWidth: 300
|
||||
}]
|
||||
}, {
|
||||
type: "panel",
|
||||
name: "video-panel",
|
||||
description: "This panel contains a fun fish video.",
|
||||
elements: [{
|
||||
type: "image",
|
||||
name: "jspsych-tutorial",
|
||||
imageLink: "video/fish.mp4",
|
||||
imageWidth: 700,
|
||||
imageHeight: 350
|
||||
}],
|
||||
}]
|
||||
}],
|
||||
widthMode: "static",
|
||||
width: 900,
|
||||
completeText: 'Next'
|
||||
};
|
||||
|
||||
const image_video_html_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: image_video_html_trial_info
|
||||
};
|
||||
|
||||
// Using images as response options
|
||||
const image_choice_trial_info = {
|
||||
elements: [{
|
||||
type: "imagepicker",
|
||||
name: "animals",
|
||||
title: "Which animals would you like to see in real life?",
|
||||
description: "Please select all that apply.",
|
||||
choices: [{
|
||||
value: "lion",
|
||||
imageLink: "img/lion.png",
|
||||
text: "Lion"
|
||||
}, {
|
||||
value: "monkey",
|
||||
imageLink: "img/monkey.png",
|
||||
text: "Monkey"
|
||||
}, {
|
||||
value: "elephant",
|
||||
imageLink: "img/elephant.png",
|
||||
text: "Elephant"
|
||||
}],
|
||||
showLabel: true,
|
||||
multiSelect: true
|
||||
}],
|
||||
showQuestionNumbers: "off",
|
||||
completeText: 'Next',
|
||||
};
|
||||
|
||||
const image_choice_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: image_choice_trial_info
|
||||
};
|
||||
|
||||
// Add sound to an HTML element
|
||||
// This also demonstrates response validation
|
||||
const sound_trial_info = {
|
||||
elements: [{
|
||||
type: "html",
|
||||
name: "audio-player",
|
||||
html: "<audio controls><source src='sound/speech_red.mp3' type='audio/mp3'></audio>"
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "audio-response",
|
||||
title: "Please play the sound above and then type the word that you heard in the box below.",
|
||||
description: "Try getting it wrong to see the response validation.",
|
||||
required: true,
|
||||
validators: [{
|
||||
type: "regex",
|
||||
text: "Oops, that's not correct. Try again!",
|
||||
regex: "[rR]{1}[eE]{1}[dD]{1}"
|
||||
}],
|
||||
}],
|
||||
completeText: "Check my response",
|
||||
showQuestionNumbers: "off"
|
||||
};
|
||||
|
||||
const sound_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: sound_trial_info
|
||||
}
|
||||
|
||||
const timeline = [image_video_html_trial, image_choice_trial, sound_trial];
|
||||
|
||||
if (typeof jsPsych !== "undefined") {
|
||||
jsPsych.run(generateDocsDemoTimeline(timeline));
|
||||
} else {
|
||||
document.body.innerHTML = '<div style="text-align:center; margin-top:50%; transform:translate(0,-50%);">You must be online to view the plugin demo.</div>';
|
||||
}
|
||||
</script>
|
||||
|
||||
</html>
|
96
docs/demos/jspsych-survey-demo7.html
Normal file
96
docs/demos/jspsych-survey-demo7.html
Normal file
@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="docs-demo-timeline.js"></script>
|
||||
<script src="https://unpkg.com/jspsych@7.3.4"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.1.3"></script>
|
||||
<script src="../../packages/plugin-survey/dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.3.4/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="../../packages/plugin-survey/css/survey.css" />
|
||||
<link rel="stylesheet" href="docs-demo.css" type="text/css" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
<script>
|
||||
|
||||
const jsPsych = initJsPsych();
|
||||
|
||||
const timeline = [];
|
||||
|
||||
const text_masking_json = {
|
||||
elements: [
|
||||
{
|
||||
type: "html",
|
||||
name: "intro",
|
||||
html: "<h3>Input masking examples</h3><p>You can use input masking with text questions to add automatic formatting to the participant's answer. The mask types are: currency, decimal, pattern, and datetime. These masks will also restrict the types of characters that can be entered, e.g. only numbers or letters."
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "currency",
|
||||
title: "Currency:",
|
||||
description: "This currency mask adds a prefix/suffix to the number to indicate the currency. Enter some numbers to see the result.",
|
||||
maskType: "currency",
|
||||
maskSettings: {
|
||||
prefix: "$",
|
||||
suffix: " USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "decimal",
|
||||
title: "Decimal:",
|
||||
description: "This numeric mask will specify the number of decimals allowed. You can enter numbers with up to three decimals (precision: 3).",
|
||||
maskType: "numeric",
|
||||
maskSettings: {
|
||||
precision: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "phone",
|
||||
title: "Phone:",
|
||||
description: "This pattern mask will format the numbers as a phone number.",
|
||||
maskType: "pattern",
|
||||
maskSettings: {
|
||||
pattern: "+9 (999)-999-9999"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "creditcard",
|
||||
title: "Credit card number:",
|
||||
description: "This pattern mask will format the numbers as a credit card number.",
|
||||
maskType: "pattern",
|
||||
maskSettings: {
|
||||
pattern: "9999 9999 9999 9999"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "licenseplate",
|
||||
title: "License plate number:",
|
||||
description: "A pattern mask can also be used with letters. Enter a license plate number in the format ABC-1234.",
|
||||
maskType: "pattern",
|
||||
maskSettings: {
|
||||
pattern: "aaa-9999"
|
||||
}
|
||||
}
|
||||
],
|
||||
showQuestionNumbers: false
|
||||
};
|
||||
|
||||
timeline.push({
|
||||
type: jsPsychSurvey,
|
||||
survey_json: text_masking_json
|
||||
});
|
||||
|
||||
if (typeof jsPsych !== "undefined") {
|
||||
jsPsych.run(generateDocsDemoTimeline(timeline));
|
||||
} else {
|
||||
document.body.innerHTML = '<div style="text-align:center; margin-top:50%; transform:translate(0,-50%);">You must be online to view the plugin demo.</div>';
|
||||
}
|
||||
</script>
|
||||
|
||||
</html>
|
BIN
docs/img/surveyjs_docs_example_json.png
Normal file
BIN
docs/img/surveyjs_docs_example_json.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
455
docs/overview/building-surveys.md
Normal file
455
docs/overview/building-surveys.md
Normal file
@ -0,0 +1,455 @@
|
||||
# Building Surveys in jsPsych
|
||||
|
||||
## Choosing which jsPsych survey plugin to use
|
||||
|
||||
jsPsych has several plugins that allow you to present survey questions during your experiment. The one you choose will depend on what exactly you'd like to do, and your preferences for convenience/parameterization versus flexibility.
|
||||
|
||||
* Survey-* plugins: [`survey-likert`](../plugins/survey-likert.md), [`survey-multi-choice`](../plugins/survey-multi-choice.md), [`survey-multi-select`](../plugins/survey-multi-select.md), [`survey-text`](../plugins/survey-text.md)
|
||||
* Only one question type and one page per trial
|
||||
* Parameterization makes it easy to define questions
|
||||
* Work well with [timeline variables](timeline.md#timeline-variables)
|
||||
* Ideal for adding survey-style questions into repetitive trial procedures
|
||||
* Limited functionality and customization options
|
||||
* [`survey-html-form`](../plugins/survey-html-form.md) plugin
|
||||
* Can mix different question types on the same page
|
||||
* No parameters for defining questions - you write the form HTML
|
||||
* Maximally flexible, so ideal if you need a lot of control over the survey content and style
|
||||
* [`survey`](../plugins/survey.md) plugin
|
||||
* Not well-suited for use with [timeline variables](timeline.md#timeline-variables)
|
||||
* Large set of built-in question types and parameterized customization options
|
||||
* Can mix different question types on the same page
|
||||
* Can present mutliple pages that participants can navigate back and forth through, without losing responses
|
||||
* Parameters for defining questions, but more config/code than the `survey-*` plugins
|
||||
* Built-in convenience parameters for many survey question types and features (e.g. response validation, conditional question display, 'other'/'none'/'select all' options)
|
||||
|
||||
The [`survey` plugin](../plugins/survey.md) differs from most other jsPsych plugins in that is a simple wrapper for an external JavaScript library called SurveyJS. This allows jsPsych users to take advantage of all of the SurveyJS features, documentation, and example code, but it also means that the `survey` plugin does not follow the same familiar conventions as most other jsPsych plugins. Users will need to familiarize themselves somewhat with the SurveyJS library in order to use the plugin. The remaining documentation on this page provides some guidance for getting started with SurveyJS and the jsPsych `survey` plugin.
|
||||
|
||||
## Getting started with SurveyJS
|
||||
|
||||
The [SurveyJS form library](https://surveyjs.io/form-library/documentation/overview) is a large and powerful survey-building framework with its own helpful documentation, examples, and demos. Here we have tried to orient jsPsych users to the basic steps for constructing surveys and highlight the features that jsPsych users may find most useful. However, it is not possible for us to reproduce the SurveyJS documentation here, so we encourage you to take advantage of their comprehensive documentation and demo/code examples.
|
||||
|
||||
SurveyJS allows you to build surveys using a JavaScript/JSON object, a JavaScript function, or a combination of both. You can read more about these options in the SurveyJS documentation:
|
||||
|
||||
- [Define a survey in JSON](https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#define-a-static-survey-model-in-json)
|
||||
- [Define a survey with JavaScript](https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#create-or-change-a-survey-model-dynamically)
|
||||
|
||||
The jsPsych `survey` plugin provides the `survey_json` and `survey_function` parameters to allow you to construct a SurveyJS survey using these JSON and JavaScript methods. The next two sections on this page explain more about each method: [Creating a survey with JSON](#creating-a-survey-with-json) and [Using JavaScript to create or modify the survey](#using-javascript-to-create-or-modify-the-survey).
|
||||
|
||||
Here are some other places to start learning about SurveyJS:
|
||||
|
||||
- [Add multiple pages to a survey](https://surveyjs.io/form-library/documentation/design-survey/create-a-multi-page-survey#add-multiple-pages-to-a-survey)
|
||||
- [Configure conditional page visibility](https://surveyjs.io/form-library/documentation/design-survey/create-a-multi-page-survey#configure-page-visibility)
|
||||
- [Page navigation UI](https://surveyjs.io/form-library/documentation/design-survey/create-a-multi-page-survey#page-navigation-ui) (previous, next, and submit buttons)
|
||||
- [Add conditional logic and dynamic texts](https://surveyjs.io/form-library/documentation/design-survey/conditional-logic)
|
||||
- [Set default values](https://surveyjs.io/form-library/documentation/design-survey/pre-populate-form-fields#default-question-values)
|
||||
|
||||
SurveyJS has some more specific features that some researchers might find useful, including:
|
||||
|
||||
- [Automatic question numbering](https://surveyjs.io/form-library/examples/how-to-number-pages-and-questions/jquery) (across the survey, within each page, and using custom values/characters)
|
||||
- [Response validation](https://surveyjs.io/form-library/examples/javascript-form-validation/jquery)
|
||||
- [Table of contents and navigation across sections](https://surveyjs.io/form-library/examples/table-of-contents/jquery)
|
||||
- [Progress bar](https://surveyjs.io/form-library/examples/configure-form-navigation-with-progress-indicators/jquery)
|
||||
- [Carry responses forward from a selection question](https://surveyjs.io/form-library/examples/carry-forward-responses/jquery)
|
||||
- [Carry responses forward from a dynamic matrix/panel](https://surveyjs.io/form-library/examples/pipe-answers-from-dynamic-matrix-or-panel/jquery)
|
||||
- [Conditional visibility for elements/questions](https://surveyjs.io/form-library/documentation/design-survey/conditional-logic#conditional-visibility) (see also [this example](https://surveyjs.io/form-library/examples/implement-conditional-logic-to-change-question-visibility/jquery))
|
||||
- Special choices for multi-choice-type questions: [None](https://surveyjs.io/form-library/documentation/api-reference/radio-button-question-model#showNoneItem), [Other](https://surveyjs.io/form-library/documentation/api-reference/radio-button-question-model#showOtherItem), [Select All](https://surveyjs.io/form-library/documentation/api-reference/checkbox-question-model#showSelectAllItem), [Refuse to answer](https://surveyjs.io/form-library/documentation/api-reference/radio-button-question-model#showRefuseItem), and [Don't know](https://surveyjs.io/form-library/documentation/api-reference/radio-button-question-model#showDontKnowItem)
|
||||
- [Localization](https://surveyjs.io/form-library/examples/survey-localization/jquery) (adapting the survey's language based on a country/region value)
|
||||
- [Text piping](https://surveyjs.io/form-library/examples/text-piping-in-surveys/jquery) (dynamically insert text into questions/answers based on previous responses)
|
||||
|
||||
You can find realistic examples on the [SurveyJS examples/demos page](https://surveyjs.io/form-library/examples/overview). And to view all of the survey-level options, see the [Survey API documentation](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model).
|
||||
|
||||
### Creating a survey with JSON
|
||||
|
||||
SurveyJS allows you to define the survey contents using an object with parameters names and values. At a minimum, the survey JSON object should contain a property called 'elements'. The value of 'elements' is an array that contains at least one element/question to be shown on the page.
|
||||
|
||||
```javascript
|
||||
// Survey with a single text entry question.
|
||||
const survey_json = {
|
||||
elements: [{
|
||||
name: "example",
|
||||
title: "Enter some text in the box below!",
|
||||
type: "text"
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
Each element is an object with a 'type', which is the element/question type (see the `survey` plugin's [Questions/Elements section](../plugins/survey.md#questionelement-types) for a list of type options). The element objects should also contain any other parameters and configuration options for that question, such as the question name (used to identify the question in the data), title (prompt shown to the participant), whether or not a response is required, and other parameters that might be relevant to that particular question type. The [Questions/Elements section](../plugins/survey.md#questionelement-types) in the `survey` plugin documentation contains links to the SurveyJS documentation for each question type, where you can find more information about the required and optional parameters.
|
||||
|
||||
Once you've created the survey JSON object, as we've done above, it can be used as the `survey_json` parameter in a jsPsych `survey` trial:
|
||||
|
||||
```javascript
|
||||
const survey_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: survey_json
|
||||
};
|
||||
|
||||
timeline.push(survey_trial);
|
||||
```
|
||||
|
||||
That's it! The code above will create a valid survey.
|
||||
|
||||
!!! note "JSON vs JavaScript objects"
|
||||
|
||||
[JSON (JavaScript Object Notation)](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON) is a text format for organizing data. It is very similar to a JavaScript object, but not exactly the same. The `survey_json` parameter takes a JSON-compatible JavaScript object, rather than a JSON string. We use the 'JSON' term for this parameter to make it clear that this parameter should not contain functions, and for consistency with SurveyJS documentation. To read more about JSON vs JavaScript objects, see e.g. [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON#javascript_and_json_differences) and [here](https://www.w3schools.com/js/js_json_intro.asp).
|
||||
|
||||
|
||||
#### Multiple pages
|
||||
|
||||
You can specify 'elements' as a top-level property in the survey JSON, and those elements will be shown on a single page. If you'd like the survey to have more than one page, then you can add a 'pages' property to the survey JSON object. The value of 'pages' should be an array of objects, where each object defines a single page. Each page object should contain its own 'elements' array.
|
||||
|
||||
The example below defines a survey with two pages. Each page has a set of elements/questions, as well as some optional parameters (page name and title).
|
||||
|
||||
```javascript
|
||||
const survey_json = {
|
||||
pages: [
|
||||
{
|
||||
name: "page_1",
|
||||
title: "Your Name",
|
||||
elements: [{
|
||||
type: "text",
|
||||
name: "first_name",
|
||||
title: "Enter your first name:"
|
||||
}, {
|
||||
type: "text",
|
||||
name: "last_name",
|
||||
title: "Enter your last name:"
|
||||
}
|
||||
}, {
|
||||
name: "page_2",
|
||||
title: "Personal Information",
|
||||
elements: [{
|
||||
type: "text",
|
||||
name: "location",
|
||||
title: "Where do you live?"
|
||||
}, {
|
||||
type: "text",
|
||||
name: "occupation",
|
||||
title: "What is your occupation?"
|
||||
}, {
|
||||
type: "text",
|
||||
name: "age",
|
||||
title: "How old are you?",
|
||||
inputType: "number",
|
||||
min: 0,
|
||||
max: 120
|
||||
}
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
#### Survey-level options
|
||||
|
||||
Along with either the 'elements' or 'pages' property, you can add optional survey-level properties to the top-level of your survey JSON object. The survey-level properties might include things like: a title (shown at the top of each page), whether to use automatic question numbering, labels for the page navigation buttons, and text to use for marking required questions. These and other survey-level parameters are not required - you only need to set these values if you want to change them from the defaults.
|
||||
|
||||
```javascript
|
||||
const survey_json = {
|
||||
title: "Survey title",
|
||||
showQuestionNumbers: "off",
|
||||
completeText: "Done",
|
||||
pageNextText: "Next",
|
||||
pagePrevText: "Back",
|
||||
requiredText: "[REQUIRED]",
|
||||
pages: [{
|
||||
elements: {
|
||||
// ... page 1 questions
|
||||
}
|
||||
}, {
|
||||
elements: {
|
||||
// ... page 2 questions
|
||||
}
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
Some of the survey-level options can also be set a the page level. See the [Page API documentation](https://surveyjs.io/form-library/documentation/api-reference/page-model) for more information.
|
||||
|
||||
For more survey JSON examples, see the [SurveyJS JSON documentation](https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#define-a-static-survey-model-in-json), the [Examples](../plugins/survey.md#examples) section on the `survey` plugin documentation page, and the [examples folder](https://github.com/jspsych/jsPsych/tree/main/packages/plugin-survey) in the `survey` plugin package.
|
||||
|
||||
|
||||
### Using JavaScript to create or modify the survey
|
||||
|
||||
SurveyJS allows you to create or modify your survey using JavaScript. The JavaScript approach can do any of the configuration that can be done in JSON, plus it allows you to make your survey more dynamic. For instance, you could use the survey function parameter to change the contents of the survey based on the participant's earlier responses. The survey function parameter also allows you to define any other functions that should run during the survey. For instance, you might want to run custom code in response to a page change or response input event.
|
||||
|
||||
In the jsPsych `survey` plugin, the `survey_function` parameter receives a 'survey' argument, which is a SurveyJS survey model that you can manipulate. If you do not include a value for the `survey_json` parameter, then the `survey_function` will receive an empty survey. In this case, your `survey_function` must add at least one page with at least one element/question to produce a valid survey.
|
||||
|
||||
Here's the JavaScript function that would create the same survey that's defined in the first JSON example above:
|
||||
|
||||
```javascript
|
||||
const survey_function = (survey) => {
|
||||
// add page
|
||||
const page = survey.addNewPage("page1");
|
||||
// add question
|
||||
const text_question = page.addNewQuestion("text", "example");
|
||||
text_question.title = "Enter some text in the box below!";
|
||||
};
|
||||
|
||||
const survey_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_function: survey_function
|
||||
};
|
||||
|
||||
timeline.push(survey_trial);
|
||||
```
|
||||
|
||||
### Combining JSON and function parameters
|
||||
|
||||
If you specify survey JSON using the `survey_json` parameter, then the `survey_function` will receive a survey object that was created using your JSON. This means that, in your survey function, you can access all of the survey elements that you have defined in the JSON.
|
||||
|
||||
Here's a slightly more realistic case for when you might want to use the `survey_function` parameter. In this example, we want to ask the participant to make a color choice at the start of the experiment, and then reference their choice in a later `survey` trial question. We can't do this with the JSON configuration because we cannot know the participant's color choice in advance - it only becomes available during the experiment.
|
||||
|
||||
However, we can use the `survey_function` to dynamically access the participant's color response from the jsPsych data and use that value in the survey question title. We'll use the `survey_function` just for this one dynamic part of the survey, and define everything else in JSON.
|
||||
|
||||
```javascript
|
||||
// Create an array of color choices
|
||||
const color_choices = ['red', 'green', 'blue', 'yellow', 'pink', 'orange', 'purple'];
|
||||
|
||||
// Create an html-button-response trial where the participant can choose a color
|
||||
const select_color_trial = {
|
||||
type: jsPsychHtmlButtonResponse,
|
||||
stimulus: '<p>Which of these is your favorite color?</p>',
|
||||
choices: color_choices,
|
||||
button_html: '<button class="jspsych-btn" style="color:%choice%";">%choice%</button>',
|
||||
data: {trial_id: 'color_trial'}
|
||||
};
|
||||
|
||||
// Create the survey JSON
|
||||
const color_survey_json = {
|
||||
elements: [{
|
||||
type: "boolean",
|
||||
renderAs: "radio",
|
||||
name: "color_confirmation",
|
||||
title: "" // This value will be set in the survey function
|
||||
}]
|
||||
};
|
||||
|
||||
// Create a survey function to access the participant's response
|
||||
// from an earlier trial and modify the survey accordingly
|
||||
const color_survey_function = (survey) => {
|
||||
// Get the earlier color selection response (button index) from the jsPsych data
|
||||
const color_choice_index = jsPsych.data.get().filter({trial_id: 'color_trial'}).values()[0].response;
|
||||
const color_choice = color_choices[color_choice_index];
|
||||
// Get the question that we want to modify
|
||||
const color_confirmation_question = survey.getQuestionByName('color_confirmation');
|
||||
// Change the question title to include the name of the color that was selected
|
||||
color_confirmation_question.title = `
|
||||
Earlier you chose ${color_choice.toUpperCase()}. Do you still like that color?
|
||||
`;
|
||||
}
|
||||
|
||||
// Create the jsPsych survey trial using both the survey JSON and survey function
|
||||
const color_survey_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: color_survey_json,
|
||||
survey_function: color_survey_function
|
||||
};
|
||||
|
||||
jsPsych.run([select_color_trial, color_survey_trial]);
|
||||
```
|
||||
|
||||
For more information about creating/modifying surveys with JavaScript, see the [SurveyJS documentation](https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#create-or-change-a-survey-model-dynamically). The SurveyJS API reference contains all of the properties (parameters), methods, and events you can use when working with the survey, page, and question objects.
|
||||
|
||||
### Deciding between JSON and function parameters
|
||||
|
||||
You can create `survey` trials entirely with JSON, entirely with a JavaScript function, or using a combination of both. Sometimes this is just a matter of preference. But you must use the JavaScript `survey_function` method when you want to:
|
||||
|
||||
- Dynamically modify the survey based on a participant's response from an earlier trial, or any other information you don't have access to before the survey trial begins.
|
||||
- Use custom functions as part of your survey's configuration. For instance, you might want to write a function that is triggered by a particular survey event, such as when response values change or when the survey is completed. You cannot put JavaScript functions into the `survey_json` object, so you will need to add them using the `survey_function` parameter.
|
||||
|
||||
!!! note "Custom response validation"
|
||||
There is a special case where you don't need to use the `survey_function` parameter for running a custom function, which is for adding custom response validation. The `survey` plugin includes a convenience parameter called `validation_function`, which allows you to add some custom JavaScript code to validate responses. Of course, you can also use the `survey_function` parameter for this, in which case you would set your custom function to run in response to the survey's [`onValidateQuestion`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#onValidateQuestion) event.
|
||||
|
||||
### Creating dynamic surveys with JSON
|
||||
|
||||
Although you cannot include JavaScript functions as values in your `survey_json`, SurveyJS has implemented some convenience options for setting up certain kinds of dynamic survey behavior from within the JSON configuration. For instance, you can define a condition expression from within the JSON that will dynamically show/hide a question or choice/column/row (see [Conditional Visibility](https://surveyjs.io/form-library/documentation/design-survey/conditional-logic#conditional-visibility) and [Expressions](https://surveyjs.io/form-library/documentation/design-survey/conditional-logic#expressions)). As another example, you can use a placeholder value inside a text string to insert the response from a particular question into that string (see [Dynamic Texts: Question Values](https://surveyjs.io/form-library/documentation/design-survey/conditional-logic#question-values)).
|
||||
|
||||
In general, you can access information in the survey JSON that exists *within that same `survey` trial* and use it to produce dynamic behavior (e.g. putting placeholder values in text strings, automatically populating choice values, etc.). But if you need to access information that becomes available during the experiment but *outside of that particular `survey` trial*, you will need to use the `survey_function` parameter.
|
||||
|
||||
### Defining survey trials/questions programmatically
|
||||
|
||||
Sometimes it's useful to be able to create your survey content programmatically. For instance, let's say you want to present a page with questions that all use the exact same format but with different prompts. You _could_ define them one-by-one in a `survey_json` object, but doing it this way might produce a very large JSON object with lots of repeated configuration across all questions.
|
||||
|
||||
Instead, it's often preferrable to separate the information that changes across questions from the things that stay the same. This can make it easier to make changes and prevent errors, since the things that are common across questions only need to be defined once. Similarly, if you are repeating a trial procedure lots of times, then you might want to define a single `survey` trial that repeats with slightly different parameters.
|
||||
|
||||
The following section presents some different options for programmatically defining multiple questions in a survey trial, or multiple survey trials, based on an array of values that should change across questions or trials.
|
||||
|
||||
The example below shows how to use the `survey_function` to loop over a set of question-level variables (titles/prompts and names), and dynamically add each question to a single survey page. You could use this same approach to add questions across multiple pages within the same survey trial.
|
||||
|
||||
```javascript
|
||||
const survey_function = (survey) => {
|
||||
|
||||
// this array stores any information that changes across questions
|
||||
const questions = [
|
||||
{title: "Question 1", name: "q1"},
|
||||
{title: "Question 2", name: "q2"},
|
||||
// ... more question-level variables ...
|
||||
{title: "Question N", name: "qN"}
|
||||
];
|
||||
|
||||
// create a single page
|
||||
const page = survey.addNewPage("questions");
|
||||
|
||||
for (let i=0; i<questions.length; i++) {
|
||||
// for each object in the questions array,
|
||||
// create a new question and add it to the same page
|
||||
let q = page.addNewQuestion("text", questions[i].name, i);
|
||||
q.title = questions[i].title;
|
||||
q.inputType = "range";
|
||||
q.min = 0;
|
||||
q.max = 100;
|
||||
q.step = 10;
|
||||
q.defaultValue = 50;
|
||||
q.isRequired = true;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
!!! tip "Use the 'matrix' question type for repeated response options"
|
||||
The example above was used to illustrate how you can loop over information to programmatically construct a series of questions that are shown on the same page. But in cases where you have different question prompts that all use the same multiple choice options, you might prefer to use the the SurveyJS ["matrix" question type](https://surveyjs.io/form-library/examples/single-selection-matrix-table-question/jquery). This question type creates a table where each row is a question and each column is a response option. The table format is often used for Likert scales, satisfaction surveys, etc. You can even create a table that repeats a set of questions with different response types using the SurveyJS [multi-select matrix](https://surveyjs.io/form-library/examples/multi-select-matrix-question/jquery#) ("matrixdropdown") question type.
|
||||
|
||||
|
||||
Rather than repeating a question format within the same trial, perhaps you want to use trial-level variables to generate separate `survey` trials, for instance in order to incorporate them into a larger repeating trial procedure. jsPsych's [timeline variables](timeline.md#timeline-variables) feature was designed to address this use case, but the use of timeline variables looks a little different with the `survey` plugin. This is because the various individual parameters that you might want to change across `survey` trials don't have their own plugin parameters - instead everything is nested within the `survey_json` parameter. Below are some examples showing how you can programmatically generate survey trials using a set of trial-level variables.
|
||||
|
||||
1. **Use the conventional timeline variables approach with the `survey_json` parameter.** This approach is probably not ideal, because the timeline variables array has to contain the entire `survey_json` object for each trial. This kind of defeats the purpose of timeline variables, because you are still defining all of the survey JSON separately for each question/trial. In any case, here's what it looks like:
|
||||
|
||||
```javascript
|
||||
const word_trials = {
|
||||
timeline: [
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: '+',
|
||||
choices: "NO_KEYS",
|
||||
trial_duration: 500
|
||||
},
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: jsPsych.timelineVariable('word'),
|
||||
choices: "NO_KEYS",
|
||||
trial_duration: 1000
|
||||
},
|
||||
{
|
||||
type: jsPsychSurvey,
|
||||
survey_json: jsPsych.timelineVariable('survey_json'),
|
||||
data: { word: jsPsych.timelineVariable('word') }
|
||||
}
|
||||
],
|
||||
timeline_variables: [
|
||||
{
|
||||
word: 'cheese',
|
||||
survey_json: { elements: [ {type: "text", title: "Enter a word related to CHEESE:", autocomplete: "off" } ], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true }
|
||||
},
|
||||
{
|
||||
word: 'ring',
|
||||
survey_json: { elements: [ {type: "text", title: "Enter a word related to RING:", autocomplete: "off" } ], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true }
|
||||
},
|
||||
{
|
||||
word: 'bat',
|
||||
survey_json: { elements: [ {type: "text", title: "Enter a word related to BAT:", autocomplete: "off" } ], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true }
|
||||
},
|
||||
{
|
||||
word: 'cow',
|
||||
survey_json: { elements: [ {type: "text", title: "Enter a word related to COW:", autocomplete: "off" } ], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true }
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
!!! tip "Consider using a survey-* plugin for presenting a single question type"
|
||||
The example above was created just to demonstrate how to combine the `survey` plugin and timeline variables. But if this were a real experiment, since each survey trial contains just one question, we'd be better off using one of the other survey-* plugins because the parameterization of those plugins works well with timeline variables. Of course, you may have other reasons for wanting to use the `survey` plugin in this type of trial procedure, for instance to take advantage of some its convenience features (e.g. different question types on the same page, response validation).
|
||||
|
||||
|
||||
2. **Use jsPsych's [functions-as-parameters](https://www.jspsych.org/7.3/overview/dynamic-parameters/#dynamic-parameters) approach for the `survey_json` parameter.** With this approach, instead of defining a static JSON object for the value of `survey_json`, you would write a _function_ that returns the survey JSON object for that specific trial.
|
||||
|
||||
This approach allows you to define the survey content using a combination of variable and static values. Also, jsPsych will run this function right before the trial begins, which means that you can change the survey content dynamically based on information that only becomes available during the experiment. Here's an example using timeline variables, though you can use the same approach to dynamically create the survey content without using timeline variables.
|
||||
|
||||
```javascript
|
||||
const word_trials = {
|
||||
timeline: [
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: '+',
|
||||
choices: "NO_KEYS",
|
||||
trial_duration: 500
|
||||
},
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: jsPsych.timelineVariable('word'),
|
||||
choices: "NO_KEYS",
|
||||
trial_duration: 1000
|
||||
},
|
||||
{
|
||||
type: jsPsychSurvey,
|
||||
survey_json: function() {
|
||||
// This is a function that dynamically creates the JSON configuration for each trial.
|
||||
// Inside the function, you can access timeline variables, jsPsych data, and other global variables
|
||||
// that you define. This function will be called right before the trial starts.
|
||||
const this_trial_json = {
|
||||
elements: [
|
||||
{
|
||||
type: "text",
|
||||
title: `Enter a word related to ${jsPsych.timelineVariable('word').toUpperCase()}:`,
|
||||
autocomplete: "off"
|
||||
}
|
||||
],
|
||||
showQuestionNumbers: false,
|
||||
completeText: "Next",
|
||||
focusFirstQuestionAutomatic: true
|
||||
}
|
||||
return this_trial_json;
|
||||
},
|
||||
data: { word: jsPsych.timelineVariable('word') }
|
||||
}
|
||||
],
|
||||
timeline_variables: [
|
||||
{ word: 'cheese' },
|
||||
{ word: 'ring' },
|
||||
{ word: 'bat' },
|
||||
{ word: 'cow' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
3. **Use the `survey_function` parameter.** You can reference trial-level variables from inside this function, so the general idea here is the same as the example above. The difference is that here you are using SurveyJS's JavaScript API syntax to create the survey content, instead of the JSON configuration. Again, this could be done without timeline variables, but this example uses timeline variables for convenience:
|
||||
|
||||
```javascript
|
||||
const create_word_survey = (survey) => {
|
||||
// Create question using timeline variables
|
||||
const page = survey.addNewPage('page1');
|
||||
const question = page.addNewQuestion('text');
|
||||
question.title = `Enter a word related to ${jsPsych.timelineVariable('word').toUpperCase()}`;
|
||||
question.autocomplete = "off";
|
||||
// Set survey-level parameters
|
||||
survey.showQuestionNumbers = false;
|
||||
survey.completeText = "Next";
|
||||
survey.focusFirstQuestionAutomatic = true;
|
||||
}
|
||||
|
||||
const word_trials = {
|
||||
timeline: [
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: '+',
|
||||
choices: "NO_KEYS",
|
||||
trial_duration: 500
|
||||
},
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: jsPsych.timelineVariable('word'),
|
||||
choices: "NO_KEYS",
|
||||
trial_duration: 1000
|
||||
},
|
||||
{
|
||||
type: jsPsychSurvey,
|
||||
survey_function: create_word_survey,
|
||||
data: { word: jsPsych.timelineVariable('word') }
|
||||
}
|
||||
],
|
||||
timeline_variables: [
|
||||
{ word: 'cheese' },
|
||||
{ word: 'ring' },
|
||||
{ word: 'bat' },
|
||||
{ word: 'cow' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -59,6 +59,7 @@ nav:
|
||||
- 'Media Preloading': 'overview/media-preloading.md'
|
||||
- 'Fullscreen Experiments': 'overview/fullscreen.md'
|
||||
- 'Eye Tracking': 'overview/eye-tracking.md'
|
||||
- 'Building Surveys': 'overview/building-surveys.md'
|
||||
- 'Exclude Participants Based on Browser Features': 'overview/exclude-browser.md'
|
||||
- 'Automatic Progress Bar': 'overview/progress-bar.md'
|
||||
- 'Integrating with Prolific': 'overview/prolific.md'
|
||||
|
20
package-lock.json
generated
20
package-lock.json
generated
@ -15936,12 +15936,18 @@
|
||||
"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.139",
|
||||
"resolved": "https://registry.npmjs.org/survey-core/-/survey-core-1.9.139.tgz",
|
||||
"integrity": "sha512-4ETo41TQmhdJt9qtANstNiYjnSxyyvEOxbDXGG/xEtmRYrigYctnmhorsJV8JkRskeb50bOO7jrHjb9QwzvAiQ=="
|
||||
},
|
||||
"node_modules/survey-knockout-ui": {
|
||||
"version": "1.9.139",
|
||||
"resolved": "https://registry.npmjs.org/survey-knockout-ui/-/survey-knockout-ui-1.9.139.tgz",
|
||||
"integrity": "sha512-Wo7UtbcxBHecJN8VTraU0t8li7P4TnHAA6UVoXbszq0slhjFxGCbKUlxp2CjbqTWt3d12ZBM6kOxHteJ7xiSPg==",
|
||||
"dependencies": {
|
||||
"knockout": "^3.5.1"
|
||||
"knockout": "^3.5.0",
|
||||
"survey-core": "1.9.139"
|
||||
}
|
||||
},
|
||||
"node_modules/sver-compat": {
|
||||
@ -18621,8 +18627,8 @@
|
||||
"version": "0.2.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"knockout": "3.5.1",
|
||||
"survey-knockout": "1.9.30"
|
||||
"survey-core": "^1.9.138",
|
||||
"survey-knockout-ui": "^1.9.139"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jspsych/config": "^2.0.0",
|
||||
|
8163
packages/plugin-survey/css/survey.css
Normal file
8163
packages/plugin-survey/css/survey.css
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/plugin-survey/css/survey.css.map
Normal file
1
packages/plugin-survey/css/survey.css.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,25 +1,67 @@
|
||||
@use "survey-knockout/survey.css";
|
||||
@use "survey-core/defaultV2.min.css";
|
||||
|
||||
.sv_main {
|
||||
font-family: "Open Sans", "Arial", sans-serif;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
|
||||
.sv_body {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
// move buttons to right (couldn't figure out a way to do this with the SurveyJS class name map
|
||||
div#sv-nav-complete.sv-action, div#sv-nav-next.sv-action {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
// TO DO: get this to work with the SurveyJS class name map
|
||||
input[type="text"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// center the question content
|
||||
.jspsych-question-content {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// prevent question content from overflowing question border/panel
|
||||
.jspsych-question-root {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
// left-align all text in the main questions section (title is still centered)
|
||||
.jspsych-body-container {
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// For some reason, when there is no survey title, the content's max-width is not restricted.
|
||||
// This means that the left text align for sv-components-row pushes the content to the far left in this case.
|
||||
// The CSS below fixes this by restricting the max-width and centering the content (and setting padding).
|
||||
.jspsych-body {
|
||||
width: auto;
|
||||
max-width: 80%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-top: calc(6 * (var(--sjs-base-unit, var(--base-unit, 8px))));
|
||||
padding-bottom: calc(10 * (var(--sjs-base-unit, var(--base-unit, 8px))));
|
||||
}
|
||||
|
||||
// removing the padding around the question content helps align the content with the nav buttons
|
||||
.jspsych-page {
|
||||
padding: 0;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
// removing the padding around the navigation button container helps align the content with the nav buttons
|
||||
.jspsych-footer {
|
||||
padding: calc(2 * (var(--sjs-base-unit, var(--base-unit, 8px)))) 0;
|
||||
}
|
||||
|
||||
// change the 'complete' button colors to match the previous/next buttons
|
||||
.jspsych-nav-complete {
|
||||
background: var(--sjs-questionpanel-backcolor, var(--sjs-question-background, var(--sjs-general-backcolor, var(--background, #fff))));
|
||||
color: var(--sjs-primary-backcolor, var(--primary, #19b394));
|
||||
display: block;
|
||||
}
|
||||
|
||||
// remove the complete button from page flow when it is hidden
|
||||
div#sv-nav-complete.sv-action.sv-action--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// remove min-width from questions that appear in the same row
|
||||
.jspsych-row-multiple > div {
|
||||
min-width: unset !important;
|
||||
}
|
103011
packages/plugin-survey/dist/index.browser.js
vendored
Normal file
103011
packages/plugin-survey/dist/index.browser.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
packages/plugin-survey/dist/index.browser.js.map
vendored
Normal file
1
packages/plugin-survey/dist/index.browser.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
74287
packages/plugin-survey/dist/index.browser.min.js
vendored
Normal file
74287
packages/plugin-survey/dist/index.browser.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
packages/plugin-survey/dist/index.browser.min.js.map
vendored
Normal file
1
packages/plugin-survey/dist/index.browser.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
101984
packages/plugin-survey/dist/index.cjs
vendored
Normal file
101984
packages/plugin-survey/dist/index.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
1
packages/plugin-survey/dist/index.cjs.map
vendored
Normal file
1
packages/plugin-survey/dist/index.cjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
83
packages/plugin-survey/dist/index.d.ts
vendored
Normal file
83
packages/plugin-survey/dist/index.d.ts
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
|
||||
declare const info: {
|
||||
readonly name: "survey";
|
||||
readonly parameters: {
|
||||
/**
|
||||
* A SurveyJS survey model defined as a JavaScript object.
|
||||
* See: https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#define-a-static-survey-model-in-json
|
||||
*/
|
||||
readonly survey_json: {
|
||||
readonly type: ParameterType.OBJECT;
|
||||
readonly default: {};
|
||||
readonly pretty_name: "Survey JSON object";
|
||||
};
|
||||
/**
|
||||
* A SurveyJS survey model defined as a function. The function receives an empty SurveyJS survey object as an argument.
|
||||
* See: https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#create-or-change-a-survey-model-dynamically
|
||||
*/
|
||||
readonly survey_function: {
|
||||
readonly type: ParameterType.FUNCTION;
|
||||
readonly default: any;
|
||||
readonly pretty_name: "Survey function";
|
||||
};
|
||||
/**
|
||||
* A function that can be used to validate responses. This function is called whenever the SurveyJS onValidateQuestion event occurs.
|
||||
* See: https://surveyjs.io/form-library/documentation/data-validation#implement-custom-client-side-validation
|
||||
*/
|
||||
readonly validation_function: {
|
||||
readonly type: ParameterType.FUNCTION;
|
||||
readonly default: any;
|
||||
readonly pretty_name: "Validation function";
|
||||
};
|
||||
};
|
||||
};
|
||||
type Info = typeof info;
|
||||
/**
|
||||
* **survey**
|
||||
*
|
||||
* 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}
|
||||
*/
|
||||
declare class SurveyPlugin implements JsPsychPlugin<Info> {
|
||||
private jsPsych;
|
||||
static info: {
|
||||
readonly name: "survey";
|
||||
readonly parameters: {
|
||||
/**
|
||||
* A SurveyJS survey model defined as a JavaScript object.
|
||||
* See: https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#define-a-static-survey-model-in-json
|
||||
*/
|
||||
readonly survey_json: {
|
||||
readonly type: ParameterType.OBJECT;
|
||||
readonly default: {};
|
||||
readonly pretty_name: "Survey JSON object";
|
||||
};
|
||||
/**
|
||||
* A SurveyJS survey model defined as a function. The function receives an empty SurveyJS survey object as an argument.
|
||||
* See: https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#create-or-change-a-survey-model-dynamically
|
||||
*/
|
||||
readonly survey_function: {
|
||||
readonly type: ParameterType.FUNCTION;
|
||||
readonly default: any;
|
||||
readonly pretty_name: "Survey function";
|
||||
};
|
||||
/**
|
||||
* A function that can be used to validate responses. This function is called whenever the SurveyJS onValidateQuestion event occurs.
|
||||
* See: https://surveyjs.io/form-library/documentation/data-validation#implement-custom-client-side-validation
|
||||
*/
|
||||
readonly validation_function: {
|
||||
readonly type: ParameterType.FUNCTION;
|
||||
readonly default: any;
|
||||
readonly pretty_name: "Validation function";
|
||||
};
|
||||
};
|
||||
};
|
||||
private survey;
|
||||
private start_time;
|
||||
constructor(jsPsych: JsPsych);
|
||||
applyStyles(survey: any): void;
|
||||
trial(display_element: HTMLElement, trial: TrialType<Info>): void;
|
||||
}
|
||||
export default SurveyPlugin;
|
101982
packages/plugin-survey/dist/index.js
vendored
Normal file
101982
packages/plugin-survey/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
packages/plugin-survey/dist/index.js.map
vendored
Normal file
1
packages/plugin-survey/dist/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,97 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="../jspsych/dist/index.browser.js"></script>
|
||||
<script src="./dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="../jspsych/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="css/survey.css" />
|
||||
</head>
|
||||
<body></body>
|
||||
<script type="text/javascript">
|
||||
|
||||
var jsPsych = initJsPsych({
|
||||
on_finish: function() {
|
||||
jsPsych.data.displayData();
|
||||
}
|
||||
});
|
||||
|
||||
var options = ['Option 1', 'Option 2', 'Option 3', 'Option 4', 'Option 5', 'Option 6'];
|
||||
|
||||
var survey_trial = {
|
||||
type: jsPsychSurvey,
|
||||
pages: [
|
||||
[
|
||||
{type: 'html', prompt: '<p>Here is some arbitrary text via an "html" question type.<br>Similar to preamble but can be inserted anywhere in the question set.</p><p>This trial uses automatic question numbering (continued across pages).</p>'},
|
||||
{type: 'text', prompt: 'This is a single-line text question. The correct answer is "hello".', required: true, correct_response: "hello"},
|
||||
{type: 'text', prompt: 'This is a multi-line text question.', placeholder: 'This is a placeholder.', textbox_rows: 10, textbox_columns: 40},
|
||||
{type: 'text', prompt: 'This is a single-line text question of input_type "number"', input_type: 'number'},
|
||||
{type: 'text', prompt: 'This is a single-line text question of input_type "date"', input_type: 'date'},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'likert',
|
||||
prompt: 'This is a likert question prompt.',
|
||||
likert_scale_values: [
|
||||
{value: 1},
|
||||
{value: 2},
|
||||
{value: 3}
|
||||
],
|
||||
likert_scale_min_label: 'Agree',
|
||||
likert_scale_max_label: 'Disagree',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'likert-table',
|
||||
prompt: 'Please indicate how much you agree with the following statements:',
|
||||
statements: [{prompt: 'I like cats.', name: 'cat'},{prompt: 'I like giraffes.', name: 'giraffe'},{prompt: 'I like antelopes.', name: 'antelope'},{prompt: 'I like lizards.', name: 'lizard'}],
|
||||
options: ['A lot', 'Somewhat', 'Not very much'],
|
||||
name: 'animals',
|
||||
required: true,
|
||||
randomize_statement_order: true,
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'ranking', prompt: 'Please order the shapes from your most to least favorite.', options: ['Triangle','Circle','Square'], option_reorder: 'random'
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'drop-down', prompt: 'Which shape do you like the best?', options: ['Triangle','Circle','Square'], add_other_option: true, option_reorder: 'asc', dropdown_select_prompt: 'Select one:', required: true
|
||||
}
|
||||
],
|
||||
[
|
||||
{type: 'multi-choice', prompt: 'This is a multi-choice question with options in one column (the default).', options: options},
|
||||
{type: 'multi-choice', prompt: 'This is a multi-choice question with options in one row. The correct response is option 5.', options: options, columns: 0, correct_response: "Option 5"},
|
||||
{type: 'multi-choice', prompt: 'This is a multi-choice question with options in two columns, with an "other" option.', options: options, columns: 2, add_other_option: true},
|
||||
{type: 'multi-select', prompt: 'This is a multi-select question.', options: options},
|
||||
{type: 'multi-select', prompt: 'This is a multi-select question with three columns and random option ordering.', options: options, columns: 3, option_reorder: 'random'},
|
||||
]
|
||||
],
|
||||
button_label_next: "Next >",
|
||||
button_label_back: "< Back",
|
||||
button_label_finish: "Finish!",
|
||||
show_question_numbers: 'on',
|
||||
required_question_label: "",
|
||||
required_error_text: "You forgot to answer this question!"
|
||||
};
|
||||
|
||||
var survey_trial_random = {
|
||||
type: jsPsychSurvey,
|
||||
pages: [[
|
||||
{type: 'text', prompt: 'Question 1.', textbox_rows: 2, textbox_columns: 20},
|
||||
{type: 'text', prompt: 'Question 2.'},
|
||||
{type: 'text', prompt: 'Question 3.', textbox_columns: 50},
|
||||
{type: 'text', prompt: 'Question 4.', textbox_rows: 2},
|
||||
{type: 'text', prompt: 'Question 5.'},
|
||||
{type: 'text', prompt: 'Question 6.', textbox_rows: 10, textbox_columns: 20},
|
||||
]],
|
||||
title: 'This is a separate survey trial. The order of questions should be randomized.',
|
||||
randomize_question_order: true
|
||||
};
|
||||
|
||||
jsPsych.run([survey_trial, survey_trial_random]);
|
||||
</script>
|
||||
</html>
|
126
packages/plugin-survey/examples/basic_question_types.html
Normal file
126
packages/plugin-survey/examples/basic_question_types.html
Normal file
@ -0,0 +1,126 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="../../jspsych/dist/index.browser.js"></script>
|
||||
<script src="../dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="../../jspsych/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="../css/survey.css" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
<script type="text/javascript">
|
||||
|
||||
const jsPsych = initJsPsych({
|
||||
on_finish: function() {
|
||||
jsPsych.data.displayData();
|
||||
}
|
||||
});
|
||||
|
||||
// This example shows how to combine several basic question types:
|
||||
// text (with autocomplete options), dropdown, multi-select dropdown ("tagbox").
|
||||
// This also shows how to create a collapsible panel (group) of questions.
|
||||
const contact_info = {
|
||||
title: "jsPsych Survey Plugin example",
|
||||
pages: [{
|
||||
title: "Page 1 title: Personal Details",
|
||||
name: "PersonalDetails",
|
||||
elements: [{
|
||||
type: "text",
|
||||
name: "FirstName",
|
||||
title: "First name:",
|
||||
isRequired: true,
|
||||
autocomplete: 'given-name'
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "LastName",
|
||||
title: "Last name:",
|
||||
isRequired: true,
|
||||
startWithNewLine: false,
|
||||
autocomplete: 'family-name'
|
||||
},
|
||||
{
|
||||
type: "panel",
|
||||
name: "Contacts",
|
||||
state: "collapsed",
|
||||
title: "Contact (optional)",
|
||||
elements: [{
|
||||
type: "text",
|
||||
inputType: 'tel',
|
||||
name: "Phone",
|
||||
title: "Phone number:",
|
||||
defaultValue: "(123) 456-7890",
|
||||
autocomplete: 'tel'
|
||||
}, {
|
||||
type: "text",
|
||||
name: "GitHub",
|
||||
title: "GitHub username:"
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
inputType: 'email',
|
||||
name: "email",
|
||||
title: "Email:",
|
||||
autocomplete: 'email'
|
||||
}]
|
||||
}]
|
||||
},
|
||||
{
|
||||
title: "Page 2 title: Location",
|
||||
name: "Location",
|
||||
description: "Here are some questions with the 'description' shown below the question. The titles are hidden by setting the 'title' string to a space character. (You can also set the survey's 'questionTitleLocation' to' 'hidden', but that applies to the whole survey.)",
|
||||
elements: [
|
||||
{
|
||||
type: "text",
|
||||
name: "State",
|
||||
title: " ",
|
||||
width: "20%",
|
||||
minWidth: "128px",
|
||||
startWithNewLine: false,
|
||||
description: "Enter a state or region",
|
||||
autocomplete: "off"
|
||||
},
|
||||
{
|
||||
type: "dropdown",
|
||||
name: "Country",
|
||||
title: " ",
|
||||
startWithNewLine: false,
|
||||
width: "60%",
|
||||
minWidth: "256px",
|
||||
description: "Select a country (start typing to search, press Enter to select)",
|
||||
choicesByUrl: {
|
||||
url: "https://surveyjs.io/api/CountriesExample"
|
||||
},
|
||||
placeholder: "",
|
||||
allowClear: false
|
||||
},
|
||||
{
|
||||
type: "tagbox",
|
||||
choicesByUrl: {
|
||||
url: "https://surveyjs.io/api/CountriesExample"
|
||||
},
|
||||
name: "all-countries",
|
||||
title: "Which countries have you been to?",
|
||||
description: "Multi-select dropdown - please select all that apply. Try selecting lots of countries to see how the input area grows."
|
||||
}]
|
||||
}],
|
||||
showQuestionNumbers: "off",
|
||||
questionDescriptionLocation: "underInput",
|
||||
completeText: "Continue",
|
||||
widthMode: "static",
|
||||
width: "900",
|
||||
fitToContainer: true
|
||||
};
|
||||
|
||||
const contact_info_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: contact_info
|
||||
};
|
||||
|
||||
jsPsych.run([contact_info_trial]);
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
90
packages/plugin-survey/examples/combine_json_function.html
Normal file
90
packages/plugin-survey/examples/combine_json_function.html
Normal file
@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="../../jspsych/dist/index.browser.js"></script>
|
||||
<script src="../dist/index.browser.js"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.1.3"></script>
|
||||
<link rel="stylesheet" href="../../jspsych/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="../css/survey.css" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
<script type="text/javascript">
|
||||
|
||||
const jsPsych = initJsPsych({
|
||||
on_finish: function() {
|
||||
jsPsych.data.displayData();
|
||||
}
|
||||
});
|
||||
|
||||
// This non-survey trial is used to demonstrate how to access prior jsPsych data when
|
||||
// creating the survey trial
|
||||
const color_choices = ['red', 'green', 'blue', 'yellow', 'pink', 'orange', 'purple'];
|
||||
const color_trial = {
|
||||
type: jsPsychHtmlButtonResponse,
|
||||
stimulus: '<h3>jsPsych button response trial</h3><p>Which of these is your favorite color?</p>',
|
||||
choices: color_choices,
|
||||
button_html: '<button class="jspsych-btn" style="color:%choice%";">%choice%</button>',
|
||||
data: {trial_id: 'color_trial'}
|
||||
};
|
||||
|
||||
// This example shows how to combine the survey_json and survey_function parameters.
|
||||
const jspsych_rating_json = {
|
||||
title: "jsPsych survey trial",
|
||||
pages: [{
|
||||
name: "example_page",
|
||||
elements: [{
|
||||
type: "radiogroup",
|
||||
name: "jspsych_rating",
|
||||
title: "How much do you like jsPsych?",
|
||||
description: "(Select one of the first three options to see the conditional question)",
|
||||
choices: [
|
||||
{ value: 1, text: "Not at all" },
|
||||
{ value: 2, text: "Not very much" },
|
||||
{ value: 3, text: "It's ok" },
|
||||
{ value: 4, text: "Somewhat" },
|
||||
{ value: 5, text: "A lot" },
|
||||
],
|
||||
colCount: 0
|
||||
}]
|
||||
}]
|
||||
};
|
||||
|
||||
// The survey_function is a function that can be used to make the contents of the survey conditional
|
||||
// on things that happened earlier in the experiment, outside of the survey trial.
|
||||
const jspsych_rating_function = (survey) => {
|
||||
survey.showQuestionNumbers = false;
|
||||
|
||||
// Add follow up question to the existing page "example_page" based on response to jspsych_rating question
|
||||
// presented in this survey trial
|
||||
const page = survey.getPageByName('example_page');
|
||||
const jspsych_improve = page.addNewQuestion("comment", "jspsych_improve");
|
||||
jspsych_improve.title = "What would make jsPsych better?";
|
||||
jspsych_improve.visibleIf = "{jspsych_rating} < 4";
|
||||
|
||||
// Add a new page with a follow up question based on response to the color question presented in a
|
||||
// separate jsPsych trial.
|
||||
// First get the response to the color question from the jsPsych data
|
||||
const color_choice_index = jsPsych.data.get().filter({trial_id: 'color_trial'}).values()[0].response;
|
||||
// Get the color choice value from the response index
|
||||
const color_choice = color_choices[color_choice_index];
|
||||
// Add a new page, and add a new question to that page
|
||||
const color_page = survey.addNewPage('color_page');
|
||||
const color_confirmation = color_page.addNewQuestion("boolean", "color_confirmation");
|
||||
color_confirmation.title = `Earlier you said you liked the color ${color_choice.toUpperCase()}. Do you still like that color?`;
|
||||
color_confirmation.renderAs = "radio";
|
||||
}
|
||||
|
||||
const jspsych_rating_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: jspsych_rating_json,
|
||||
survey_function: jspsych_rating_function
|
||||
};
|
||||
|
||||
jsPsych.run([color_trial, jspsych_rating_trial]);
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="../../jspsych/dist/index.browser.js"></script>
|
||||
<script src="../dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="../../jspsych/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="../css/survey.css" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
<script type="text/javascript">
|
||||
|
||||
const jsPsych = initJsPsych({
|
||||
on_finish: function() {
|
||||
jsPsych.data.displayData();
|
||||
}
|
||||
});
|
||||
|
||||
// This example shows how to make some questions conditional on previous answers
|
||||
// from the same survey trial.
|
||||
const vegetables = {
|
||||
pages: [{
|
||||
title: "Example of conditional questions",
|
||||
elements: [{
|
||||
name: "vegetables-score",
|
||||
title: "I like vegetables.",
|
||||
description: "Choose 'neutral' to skip the conditional question, and any other option to see a conditional question.",
|
||||
type: "radiogroup",
|
||||
choices: [
|
||||
{ value: 1, text: "Strongly Disagree" },
|
||||
{ value: 2, text: "Disagree" },
|
||||
{ value: 3, text: "Neutral" },
|
||||
{ value: 4, text: "Agree" },
|
||||
{ value: 5, text: "Strongly Agree" }
|
||||
],
|
||||
isRequired: true
|
||||
}]
|
||||
}, {
|
||||
elements: [{
|
||||
name: "vegetables-like",
|
||||
title: "You like vegetables! Which one is your favorite?",
|
||||
description: "(You can go back and change your earlier answer to see the other conditional questions)",
|
||||
type: "comment",
|
||||
visibleIf: "{vegetables-score} >= 4"
|
||||
}, {
|
||||
name: "vegetables-eat",
|
||||
title: "On a scale of zero to ten, how likely are you to eat broccoli today?",
|
||||
type: "rating",
|
||||
rateMin: 0,
|
||||
rateMax: 10
|
||||
}],
|
||||
visibleIf: "{vegetables-score} >= 4"
|
||||
}, {
|
||||
elements: [{
|
||||
name: "vegetables-dislike",
|
||||
description: "(You can go back and change your earlier answer to see the other conditional questions)",
|
||||
title: "You don't like vegetables! Please explain why.",
|
||||
type: "comment"
|
||||
}],
|
||||
visibleIf: "{vegetables-score} =< 2"
|
||||
}],
|
||||
showQuestionNumbers: "off",
|
||||
completeText: "Next",
|
||||
questionTitleLocation: "top"
|
||||
};
|
||||
|
||||
const vegetables_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: vegetables
|
||||
};
|
||||
|
||||
jsPsych.run([vegetables_trial]);
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="../../jspsych/dist/index.browser.js"></script>
|
||||
<script src="../dist/index.browser.js"></script>
|
||||
<script src="https://unpkg.com/@jspsych/plugin-html-keyboard-response@1.1.3"></script>
|
||||
<link rel="stylesheet" href="../../jspsych/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="../css/survey.css" />
|
||||
<style>
|
||||
/* These CSS changes make the survey plugin content and position more similar to other jsPsych trials. */
|
||||
/* Remove the border around the survey question and align the text to center instead of left. */
|
||||
.jspsych-question-root {
|
||||
box-shadow: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Use flex display for presenting the survey content in the middle of the page instead of at the top. */
|
||||
.jspsych-content-wrapper {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* This prevents a horizontal scroll bar from appearing on survey trials. */
|
||||
.jspsych-content {
|
||||
width: 95%;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
/* Align the survey navigation buttons to the center */
|
||||
.sv-components-column {
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
<script type="text/javascript">
|
||||
|
||||
const jsPsych = initJsPsych({
|
||||
on_finish: function () {
|
||||
jsPsych.data.displayData();
|
||||
}
|
||||
});
|
||||
|
||||
const word_instructions = {
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: '<p>You will see a series of words, each shown one at a time.</p><p>Each time you see a word, please type in the first related word that comes to mind.</p><p>For instance, if you see the word "banana", you might enter "apple" or "fruit".</p><p>Press any key to start!</p>'
|
||||
};
|
||||
|
||||
// This example shows how to use timeline variables to run a timeline that includes a survey trial.
|
||||
// The survey_json value is stored in the objects in the timeline_variables array.
|
||||
// Note: This example is just meant to show how you can pass the survey_json in using timeline variables. If you wanted to
|
||||
// create a similar task to this one (just one survey question and no other Survey plugin features),
|
||||
// you would be better off using a survey-* plugin!
|
||||
const word_trials = {
|
||||
timeline: [
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: '+',
|
||||
choices: "NO_KEYS",
|
||||
trial_duration: 500
|
||||
},
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: jsPsych.timelineVariable('word'),
|
||||
choices: "NO_KEYS",
|
||||
trial_duration: 1000
|
||||
},
|
||||
{
|
||||
type: jsPsychSurvey,
|
||||
survey_json: jsPsych.timelineVariable('survey_json'),
|
||||
data: { word: jsPsych.timelineVariable('word') }
|
||||
}
|
||||
],
|
||||
timeline_variables: [
|
||||
{ word: 'cheese', survey_json: { elements: [{ type: "text", title: "Enter a word related to CHEESE:", autocomplete: "off" }], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true } },
|
||||
{ word: 'ring', survey_json: { elements: [{ type: "text", title: "Enter a word related to RING:", autocomplete: "off" }], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true } },
|
||||
{ word: 'bat', survey_json: { elements: [{ type: "text", title: "Enter a word related to BAT:", autocomplete: "off" }], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true } },
|
||||
{ word: 'cow', survey_json: { elements: [{ type: "text", title: "Enter a word related to COW:", autocomplete: "off" }], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true } }
|
||||
]
|
||||
};
|
||||
|
||||
const semantic_relatedness_instructions = {
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: '<p>You will see pairs of words, shown one at a time.</p><p>Each time you see a word pair:</p><p>Press <strong>"j"</strong> if the meanings of the two words are <strong>related</strong>, or</p><p>press <strong>"f"</strong> if the meanings of the two words are <strong>not related</strong>.</p><p>Press any key to start!</p>'
|
||||
};
|
||||
|
||||
// This example shows how to use a custom function to dynamically generate the survey_json on each trial.
|
||||
// The custom function takes information from timeline_variables and the participant's response on the last trial
|
||||
// and uses that to create the title for a survey question.
|
||||
const semantic_relatedness = {
|
||||
timeline: [
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: '+',
|
||||
choices: "NO_KEYS",
|
||||
trial_duration: 500
|
||||
},
|
||||
{
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
stimulus: function () {
|
||||
var html = `
|
||||
<div style="display:flex;width:70%;margin:auto;justify-content:space-around;"><span>${jsPsych.timelineVariable('word1')}</span><span>${jsPsych.timelineVariable('word2')}</span></div>`;
|
||||
return html;
|
||||
},
|
||||
choices: ['f', 'j'],
|
||||
trial_duration: 2500
|
||||
},
|
||||
{
|
||||
type: jsPsychSurvey,
|
||||
data: { word1: jsPsych.timelineVariable('word1'), word2: jsPsych.timelineVariable('word2') },
|
||||
survey_json: function () {
|
||||
const last_response = jsPsych.data.getLastTrialData().values()[0].response;
|
||||
const response_type = (last_response === 'j') ? "RELATED" : "NOT RELATED";
|
||||
const question_text = `You said that the words "${jsPsych.timelineVariable('word1')}" and "${jsPsych.timelineVariable('word2')}" are ${response_type}. Please explain your answer.`;
|
||||
const survey_json = {
|
||||
showQuestionNumbers: false,
|
||||
completeText: "Next",
|
||||
focusFirstQuestionAutomatic: true,
|
||||
elements: [
|
||||
{
|
||||
type: "comment",
|
||||
title: question_text,
|
||||
name: "response_explanation"
|
||||
},
|
||||
{
|
||||
type: "rating",
|
||||
title: "How confident do you feel about your response?",
|
||||
rateCount: 10,
|
||||
rateMax: 10,
|
||||
displayMode: "buttons",
|
||||
minRateDescription: "Not confident",
|
||||
maxRateDescription: "Very confident",
|
||||
name: "response_confidence"
|
||||
}
|
||||
]
|
||||
}
|
||||
return survey_json
|
||||
}
|
||||
}
|
||||
],
|
||||
timeline_variables: [
|
||||
{ word1: 'cheese', word2: 'hotel' },
|
||||
{ word1: 'ring', word2: 'pear' },
|
||||
{ word1: 'bat', word2: 'fly' },
|
||||
{ word1: 'cow', word2: 'pig' }
|
||||
]
|
||||
};
|
||||
|
||||
jsPsych.run([word_instructions, word_trials, semantic_relatedness_instructions, semantic_relatedness]);
|
||||
</script>
|
||||
</html>
|
123
packages/plugin-survey/examples/reference_previous_answers.html
Normal file
123
packages/plugin-survey/examples/reference_previous_answers.html
Normal file
@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="../../jspsych/dist/index.browser.js"></script>
|
||||
<script src="../dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="../../jspsych/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="../css/survey.css" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
<script type="text/javascript">
|
||||
|
||||
const jsPsych = initJsPsych({
|
||||
on_finish: function() {
|
||||
jsPsych.data.displayData();
|
||||
}
|
||||
});
|
||||
|
||||
// This example shows how to reference previous answers during the same survey trial,
|
||||
// for instance to create a variable ('calculated value') that can be referenced elsewhere in the survey, or
|
||||
// to use selected answers as choices for a subsequent question.
|
||||
const reference_previous_answers = {
|
||||
title: "jsPsych Survey Plugin: referencing answers from previous questions",
|
||||
// These "calculated values" are defined at the top level of your survey JSON
|
||||
// and allow you to create variables based on responses that you can reference in the survey.
|
||||
// These expressions reference answers to questions using the syntax {questionName},
|
||||
// are are updated dynamically as the participant enters/changes their responses.
|
||||
// These values will be added to the data with "includeIntoResult: true".
|
||||
calculatedValues: [{
|
||||
name: "fullname",
|
||||
expression: "{firstName} + ' ' + {lastName}",
|
||||
includeIntoResult: true
|
||||
}, {
|
||||
name: "age",
|
||||
expression: "age({birthdate})",
|
||||
includeIntoResult: true
|
||||
}],
|
||||
// Define the survey pages and elements (questions).
|
||||
pages: [{
|
||||
elements: [
|
||||
{ name: "firstName", type: "text", title: "First Name", isRequired: true },
|
||||
{ name: "lastName", type: "text", title: "Last Name", isRequired: true },
|
||||
{
|
||||
name: "greetings-name",
|
||||
type: "html",
|
||||
html: "<p>Hello, <strong>{fullname}</strong>!</p>",
|
||||
visibleIf: "{firstName} notempty and {lastName} notempty"
|
||||
},
|
||||
{
|
||||
name: "greetings-empty",
|
||||
type: "html",
|
||||
html: "<p>Hello! Please enter your name above.</p>",
|
||||
visibleIf: "{firstName} empty or {lastName} empty"
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "birthdate",
|
||||
title: "Please enter your birth date:",
|
||||
isRequired: true,
|
||||
inputType: "date",
|
||||
maxValueExpression: "today()",
|
||||
validators: [
|
||||
{
|
||||
type: "expression",
|
||||
text: "You should be younger than 200 years old.",
|
||||
expression: "{age} <= 200"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "show-age",
|
||||
type: "html",
|
||||
html: "<p>You are <strong>{age}</strong> years old!</p>",
|
||||
visibleIf: "{birthdate} notempty"
|
||||
}]
|
||||
}, {
|
||||
elements: [{
|
||||
type: "checkbox",
|
||||
name: "favorite-animals",
|
||||
title: "What are your favorite animals?",
|
||||
description: "Please select at least TWO features to see the Carry Forward functionality.",
|
||||
isRequired: true,
|
||||
colCount: 2,
|
||||
choices: [
|
||||
"Hippopotamus",
|
||||
"Raccoon",
|
||||
"Kangaroo",
|
||||
"Shark",
|
||||
"Cat",
|
||||
"Hedgehog",
|
||||
"Bunny",
|
||||
"Monkey"
|
||||
]
|
||||
},
|
||||
// This question gets the selected choices from the previous question and uses those as the choices
|
||||
// for this question (choicesFromQuestion: "favorite-animals").
|
||||
// Note that this question only appears if 2 or more answers have been selected in the previous question
|
||||
// (visibleIf: "{favorite-animals.length} > 1") because this ranking question doesn't make sense unless
|
||||
// there are at least 2 choices.
|
||||
{
|
||||
type: "ranking",
|
||||
name: "animals-ranked",
|
||||
title: "Which of these animals would make the best pet? Please rank your favorite animals from BEST (1) to WORST pet option.",
|
||||
visibleIf: "{favorite-animals.length} > 1",
|
||||
isRequired: true,
|
||||
choicesFromQuestion: "favorite-animals",
|
||||
choicesFromQuestionMode: "selected"
|
||||
}],
|
||||
}],
|
||||
showQuestionNumbers: false
|
||||
};
|
||||
const reference_previous_answers_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: reference_previous_answers
|
||||
};
|
||||
|
||||
jsPsych.run([reference_previous_answers_trial]);
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
88
packages/plugin-survey/examples/response_validation.html
Normal file
88
packages/plugin-survey/examples/response_validation.html
Normal file
@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="../../jspsych/dist/index.browser.js"></script>
|
||||
<script src="../dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="../../jspsych/css/jspsych.css" />
|
||||
<link rel="stylesheet" href="../css/survey.css" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
<script type="text/javascript">
|
||||
|
||||
const jsPsych = initJsPsych({
|
||||
on_finish: function() {
|
||||
jsPsych.data.displayData();
|
||||
}
|
||||
});
|
||||
|
||||
// This example shows how to validate responses using a function passed to jsPsych's "validation_function"
|
||||
// parameter, and using a survey question's 'validators' parameter.
|
||||
|
||||
// Define a function to be passed to jsPsych's "validation_function" parameter.
|
||||
// This function runs whenever the survey's 'onValidateQuestion' event occurs, and is more flexible than the
|
||||
// built-in question validators that SurveyJS allows you to add through the survey JSON.
|
||||
// https://surveyjs.io/form-library/documentation/data-validation#implement-custom-client-side-validation
|
||||
function validate_yes(_, options) {
|
||||
// Select the question you want to validate using the question name
|
||||
if (options.name === "yesno") {
|
||||
// Do your validation here...
|
||||
if (options.value !== "Yes") {
|
||||
options.error = "Oh no! You have to select 'Yes' to continue.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validation = {
|
||||
name: "Validation",
|
||||
title: "jsPsych Survey Plugin: response validation",
|
||||
elements: [
|
||||
{
|
||||
type: "text",
|
||||
name: "number",
|
||||
title: "Please enter a 9-digit number.",
|
||||
requiredErrorText: "You must enter a 9-digit number.",
|
||||
// There are some question validators that can be added directly into the survey JSON
|
||||
// https://surveyjs.io/form-library/documentation/data-validation#built-in-client-side-validators
|
||||
validators: [{
|
||||
type: "regex",
|
||||
text: "You must enter a 9-digit number",
|
||||
regex: "(?=\\d{9})"
|
||||
}],
|
||||
maxLength: 9,
|
||||
autocomplete: "off"
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "birthdate",
|
||||
title: "Date of birth",
|
||||
inputType: "date",
|
||||
maxValueExpression: "today()",
|
||||
description: "To view the validation, type in a future date and try to continue."
|
||||
},
|
||||
{
|
||||
type: "boolean",
|
||||
name: "yesno",
|
||||
title: "Select 'Yes'",
|
||||
valueTrue: "Yes",
|
||||
valueFalse: "No"
|
||||
},
|
||||
],
|
||||
checkErrorsMode: "onValueChanged",
|
||||
showQuestionNumbers: false,
|
||||
completeText: "Continue",
|
||||
};
|
||||
|
||||
const validation_trial = {
|
||||
type: jsPsychSurvey,
|
||||
survey_json: validation,
|
||||
validation_function: validate_yes
|
||||
};
|
||||
|
||||
jsPsych.run([validation_trial]);
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
@ -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,7 @@
|
||||
"sass": "^1.43.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"knockout": "3.5.1",
|
||||
"survey-knockout": "1.9.30"
|
||||
"survey-core": "^1.9.138",
|
||||
"survey-knockout-ui": "^1.9.139"
|
||||
}
|
||||
}
|
||||
|
@ -1,287 +1,181 @@
|
||||
import { clickTarget, startTimeline } from "@jspsych/test-utils";
|
||||
import { initJsPsych } from "jspsych";
|
||||
|
||||
import survey from ".";
|
||||
|
||||
describe("survey plugin", () => {
|
||||
test("loads", async () => {
|
||||
const { displayElement, expectRunning, getData } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
pages: [
|
||||
[
|
||||
const survey_json = {
|
||||
pages: [
|
||||
{
|
||||
name: "page1",
|
||||
elements: [
|
||||
{
|
||||
type: "drop-down",
|
||||
prompt: "foo",
|
||||
options: ["1", "2"],
|
||||
type: "text",
|
||||
name: "question1",
|
||||
title: "Question 1",
|
||||
},
|
||||
{
|
||||
type: "ranking",
|
||||
name: "question2",
|
||||
title: "Question 2",
|
||||
choices: ["Item 1", "Item 2", "Item 3"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { expectRunning } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
survey_json: survey_json,
|
||||
},
|
||||
]);
|
||||
|
||||
await expectRunning();
|
||||
});
|
||||
|
||||
// drop-down
|
||||
test("loads drop-down question with defaults", async () => {
|
||||
const { displayElement, getHTML, expectFinished } = await startTimeline([
|
||||
test("works with empty JSON and a survey function", async () => {
|
||||
const survey_function = (survey) => {
|
||||
const page = survey.addNewPage("DynamicExample");
|
||||
const radio_question = page.addNewQuestion("radiogroup", "radio_question");
|
||||
radio_question.title = "Example question.";
|
||||
radio_question.choices = [
|
||||
{ value: 1, text: "Option 1" },
|
||||
{ value: 2, text: "Option 2" },
|
||||
];
|
||||
};
|
||||
|
||||
const { displayElement, expectRunning, expectFinished } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
pages: [
|
||||
[
|
||||
{
|
||||
type: "drop-down",
|
||||
prompt: "foo",
|
||||
options: ["1", "2"],
|
||||
},
|
||||
],
|
||||
],
|
||||
survey_function: survey_function,
|
||||
},
|
||||
]);
|
||||
|
||||
// check that label displayed
|
||||
const question = displayElement.querySelector('div[data-name="P0_Q0"]');
|
||||
expect(question).not.toBeNull();
|
||||
expect(question.querySelector("span").innerHTML).toBe("foo");
|
||||
|
||||
// check that dropdown displayed
|
||||
const dropdown_menu = displayElement.getElementsByTagName("select");
|
||||
expect(dropdown_menu[0]).not.toBeNull();
|
||||
|
||||
// check that finish button displayed
|
||||
const finish_button = displayElement.querySelector("input.sv_complete_btn");
|
||||
expect(finish_button).not.toBeNull();
|
||||
|
||||
await clickTarget(finish_button);
|
||||
await expectRunning();
|
||||
|
||||
const complete_button = displayElement.querySelector(
|
||||
'input[type="button"].jspsych-nav-complete'
|
||||
);
|
||||
expect(complete_button).not.toBeNull();
|
||||
await clickTarget(complete_button);
|
||||
await expectFinished();
|
||||
});
|
||||
|
||||
// html
|
||||
test("loads html question with defaults", async () => {
|
||||
const { displayElement, expectFinished, getData } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
pages: [
|
||||
[
|
||||
{
|
||||
type: "html",
|
||||
prompt: "<span id='prompt'>foo</span>",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const question = displayElement.querySelector('div[data-name="P0_Q0"]');
|
||||
expect(question).not.toBeNull();
|
||||
expect(question.querySelector("#prompt").innerHTML).toBe("foo");
|
||||
|
||||
const finish_button = displayElement.querySelector("input.sv_complete_btn");
|
||||
expect(finish_button).not.toBeNull();
|
||||
await clickTarget(finish_button);
|
||||
|
||||
await expectFinished();
|
||||
});
|
||||
|
||||
// likert
|
||||
// test("loads likert question with defaults", async () => {
|
||||
// const { displayElement, expectFinished, getData } = await startTimeline([
|
||||
// {
|
||||
// type: survey,
|
||||
// pages: [[{
|
||||
// type: 'likert', prompt: 'foo', statements: [{prompt: 's1'},{prompt: 's2'}], options: ['fizz','buzz']
|
||||
// }]]
|
||||
// },
|
||||
// ]);
|
||||
// });
|
||||
|
||||
// multi-choice
|
||||
test("loads multi-choice question with defaults", async () => {
|
||||
const { displayElement, expectFinished, getData } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
pages: [
|
||||
[
|
||||
{
|
||||
type: "multi-choice",
|
||||
prompt: "foo",
|
||||
options: ["fizz", "buzz"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const question = displayElement.querySelector('div[data-name="P0_Q0"]');
|
||||
expect(question).not.toBeNull();
|
||||
expect(question.querySelector("span").innerHTML).toBe("foo");
|
||||
|
||||
const radio_btns = displayElement.querySelectorAll("input[type='radio']");
|
||||
expect(radio_btns).not.toBeNull();
|
||||
expect(radio_btns.length).toBe(2);
|
||||
|
||||
const finish_button = displayElement.querySelector("input.sv_complete_btn");
|
||||
expect(finish_button).not.toBeNull();
|
||||
|
||||
await clickTarget(finish_button);
|
||||
|
||||
await expectFinished();
|
||||
});
|
||||
|
||||
// multi-select
|
||||
test("loads multi-select question with defaults", async () => {
|
||||
const { displayElement, expectFinished, getData } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
pages: [
|
||||
[
|
||||
{
|
||||
type: "multi-select",
|
||||
prompt: "foo",
|
||||
options: ["fizz", "buzz"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const question = displayElement.querySelector('div[data-name="P0_Q0"]');
|
||||
expect(question).not.toBeNull();
|
||||
expect(question.querySelector("span").innerHTML).toBe("foo");
|
||||
|
||||
const checkboxes = displayElement.querySelectorAll("input[type='checkbox']");
|
||||
expect(checkboxes).not.toBeNull();
|
||||
expect(checkboxes.length).toBe(2);
|
||||
|
||||
const finish_button = displayElement.querySelector("input.sv_complete_btn");
|
||||
expect(finish_button).not.toBeNull();
|
||||
await clickTarget(finish_button);
|
||||
|
||||
await expectFinished();
|
||||
});
|
||||
|
||||
// text
|
||||
test("loads single-line text question with defaults", async () => {
|
||||
const { displayElement, expectFinished, getData } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
pages: [
|
||||
[
|
||||
{
|
||||
type: "text",
|
||||
prompt: "foo",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const question = displayElement.querySelector('div[data-name="P0_Q0"]');
|
||||
expect(question).not.toBeNull();
|
||||
expect(question.querySelector("span").innerHTML).toBe("foo");
|
||||
|
||||
const textinput = displayElement.querySelectorAll("input");
|
||||
expect(textinput[0]).not.toBeNull();
|
||||
expect(textinput[0].type).toBe("text");
|
||||
expect(textinput[0].size).toBe(40);
|
||||
|
||||
const finish_button = displayElement.querySelector("input.sv_complete_btn");
|
||||
expect(finish_button).not.toBeNull();
|
||||
await clickTarget(finish_button);
|
||||
|
||||
await expectFinished();
|
||||
});
|
||||
|
||||
test("loads multi-line text question with defaults", async () => {
|
||||
const { displayElement, expectFinished, getData } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
pages: [
|
||||
[
|
||||
{
|
||||
type: "text",
|
||||
prompt: "foo",
|
||||
textbox_rows: 2,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const question = displayElement.querySelector('div[data-name="P0_Q0"]');
|
||||
expect(question).not.toBeNull();
|
||||
expect(question.querySelector("span").innerHTML).toBe("foo");
|
||||
|
||||
const textarea = displayElement.querySelectorAll("textarea");
|
||||
expect(textarea[0]).not.toBeNull();
|
||||
expect(textarea[0].cols).toBe(40);
|
||||
expect(textarea[0].rows).toBe(2);
|
||||
|
||||
const finish_button = displayElement.querySelector("input.sv_complete_btn");
|
||||
expect(finish_button).not.toBeNull();
|
||||
await clickTarget(finish_button);
|
||||
|
||||
await expectFinished();
|
||||
});
|
||||
|
||||
test("loads single-line text questions of various input types", async () => {
|
||||
jest.setTimeout(40000); // default timeout of 5s is too short for this test
|
||||
|
||||
const inputTypes = [
|
||||
"color",
|
||||
"date",
|
||||
"datetime-local",
|
||||
"email",
|
||||
"month",
|
||||
"number",
|
||||
"password",
|
||||
"range",
|
||||
"tel",
|
||||
"text",
|
||||
"time",
|
||||
"url",
|
||||
"week",
|
||||
];
|
||||
|
||||
for (const inputType of inputTypes) {
|
||||
const { displayElement, expectFinished, getData } = await startTimeline([
|
||||
test("survey_json can be combined with survey_function", async () => {
|
||||
const survey_json = {
|
||||
pages: [
|
||||
{
|
||||
type: survey,
|
||||
pages: [
|
||||
[
|
||||
{
|
||||
type: "text",
|
||||
prompt: "foo",
|
||||
input_type: inputType,
|
||||
textbox_columns: 10,
|
||||
},
|
||||
],
|
||||
name: "test_page",
|
||||
elements: [
|
||||
{
|
||||
type: "radiogroup",
|
||||
name: "question_1",
|
||||
choices: [
|
||||
{ value: 1, text: "Option 1" },
|
||||
{ value: 2, text: "Option 2" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
],
|
||||
};
|
||||
|
||||
const question = displayElement.querySelector('div[data-name="P0_Q0"]');
|
||||
expect(question).not.toBeNull();
|
||||
expect(question.querySelector("span").innerHTML).toBe("foo");
|
||||
const survey_function = (survey) => {
|
||||
const page = survey.getPageByName("test_page");
|
||||
page.addNewQuestion("comment", "question_2");
|
||||
};
|
||||
|
||||
const input = displayElement.querySelectorAll("input")[0];
|
||||
expect(input).not.toBeNull();
|
||||
expect(input.type).toEqual(inputType);
|
||||
if (["email", "password", "tel", "url", "text"].includes(inputType)) {
|
||||
// size can be specified only for text input types
|
||||
expect(input.size).toEqual(10);
|
||||
} else {
|
||||
expect(input.size).not.toEqual(10);
|
||||
}
|
||||
const { displayElement, expectRunning, expectFinished } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
survey_json: survey_json,
|
||||
survey_function: survey_function,
|
||||
},
|
||||
]);
|
||||
|
||||
const finish_button = displayElement.querySelector("input.sv_complete_btn");
|
||||
expect(finish_button).not.toBeNull();
|
||||
await clickTarget(finish_button);
|
||||
await expectRunning();
|
||||
|
||||
await expectFinished();
|
||||
}
|
||||
expect(displayElement.querySelector('div[data-name="question_1"]')).not.toBeNull();
|
||||
expect(displayElement.querySelector('div[data-name="question_2"]')).not.toBeNull();
|
||||
|
||||
const complete_button = displayElement.querySelector(
|
||||
'input[type="button"].jspsych-nav-complete'
|
||||
);
|
||||
expect(complete_button).not.toBeNull();
|
||||
await clickTarget(complete_button);
|
||||
await expectFinished();
|
||||
});
|
||||
|
||||
// survey options
|
||||
test("survey_json can be a function that returns a valid survey_json object", async () => {
|
||||
const survey_json = {
|
||||
elements: [
|
||||
{
|
||||
type: "radiogroup",
|
||||
name: "question_1",
|
||||
choices: [
|
||||
{ value: 1, text: "Option 1" },
|
||||
{ value: 2, text: "Option 2" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const getSurveyJson = () => survey_json;
|
||||
|
||||
const { displayElement, expectRunning, expectFinished } = await startTimeline([
|
||||
{
|
||||
type: survey,
|
||||
survey_json: getSurveyJson,
|
||||
},
|
||||
]);
|
||||
|
||||
await expectRunning();
|
||||
|
||||
expect(displayElement.querySelector('div[data-name="question_1"]')).not.toBeNull();
|
||||
|
||||
const complete_button = displayElement.querySelector(
|
||||
'input[type="button"].jspsych-nav-complete'
|
||||
);
|
||||
expect(complete_button).not.toBeNull();
|
||||
await clickTarget(complete_button);
|
||||
await expectFinished();
|
||||
});
|
||||
|
||||
test("survey_json can come from timeline variables", async () => {
|
||||
let jsPsych = initJsPsych();
|
||||
|
||||
const {} = await startTimeline(
|
||||
[
|
||||
{
|
||||
timeline: [
|
||||
{
|
||||
type: survey,
|
||||
survey_json: jsPsych.timelineVariable("surveyJson"),
|
||||
on_load: function () {
|
||||
// setTimeout is needed to allow the survey content to load
|
||||
// TO DO: fix survey plugin so that on_loads fires at the correct time
|
||||
setTimeout(function () {
|
||||
expect(document.querySelector('div[data-name="question1"]')).not.toBeNull();
|
||||
const complete_button = document.querySelector(
|
||||
'input[type="button"].jspsych-nav-complete'
|
||||
);
|
||||
expect(complete_button).not.toBeNull();
|
||||
clickTarget(complete_button);
|
||||
}, 100);
|
||||
},
|
||||
},
|
||||
],
|
||||
timeline_variables: [
|
||||
{ surveyJson: { elements: { type: "text", title: "q1" } } },
|
||||
{ surveyJson: { elements: { type: "text", title: "q2" } } },
|
||||
{ surveyJson: { elements: { type: "text", title: "q3" } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
jsPsych
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,525 +1,177 @@
|
||||
// import SurveyJS dependencies: survey-core and survey-knockout-ui (UI theme): https://surveyjs.io/documentation/surveyjs-architecture#surveyjs-packages
|
||||
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
|
||||
import {
|
||||
QuestionCheckbox,
|
||||
QuestionComment,
|
||||
QuestionDropdown,
|
||||
QuestionHtml,
|
||||
QuestionMatrix,
|
||||
QuestionRadiogroup,
|
||||
QuestionRanking,
|
||||
QuestionRating,
|
||||
QuestionText,
|
||||
StylesManager,
|
||||
Survey,
|
||||
} from "survey-knockout";
|
||||
import * as SurveyJS from "survey-knockout-ui";
|
||||
|
||||
const info = <const>{
|
||||
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: [],
|
||||
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: [],
|
||||
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: [],
|
||||
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 <input> 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: {
|
||||
type: ParameterType.STRING,
|
||||
pretty_name: "Finish button label",
|
||||
default: "Finish",
|
||||
},
|
||||
/** 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,
|
||||
/**
|
||||
* A SurveyJS survey model defined as a JavaScript object.
|
||||
* See: https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#define-a-static-survey-model-in-json
|
||||
*/
|
||||
survey_json: {
|
||||
type: ParameterType.OBJECT,
|
||||
default: {},
|
||||
pretty_name: "Survey JSON object",
|
||||
},
|
||||
/**
|
||||
* 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.
|
||||
* A SurveyJS survey model defined as a function. The function receives an empty SurveyJS survey object as an argument.
|
||||
* See: https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#create-or-change-a-survey-model-dynamically
|
||||
*/
|
||||
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",
|
||||
survey_function: {
|
||||
type: ParameterType.FUNCTION,
|
||||
default: null,
|
||||
pretty_name: "Survey function",
|
||||
},
|
||||
/** 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: "*",
|
||||
/**
|
||||
* A function that can be used to validate responses. This function is called whenever the SurveyJS onValidateQuestion event occurs.
|
||||
* See: https://surveyjs.io/form-library/documentation/data-validation#implement-custom-client-side-validation
|
||||
*/
|
||||
validation_function: {
|
||||
type: ParameterType.FUNCTION,
|
||||
default: null,
|
||||
pretty_name: "Validation function",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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",
|
||||
];
|
||||
// Define the mapping between custom jsPsych class names (jspsych-*) and class names provided by SurveyJS.
|
||||
// See here for full list: https://github.com/surveyjs/survey-library/blob/master/src/defaultCss/defaultV2Css.ts.
|
||||
// To modify the survey plugin CSS:
|
||||
// (1) search for the CSS selector that you want to modify,
|
||||
// (2) look it up and get the associated ID (note that some of these are nested)
|
||||
// (3) if the ID isn't already listed as a key here, add it and use a new jspsych class name as the value
|
||||
// (4) in survey.scss, use the jspsych class name as the selector and add/modify the rule
|
||||
|
||||
const question_types = [
|
||||
"drop-down",
|
||||
"html",
|
||||
"likert",
|
||||
"likert-table",
|
||||
"multi-choice",
|
||||
"multi-select",
|
||||
"ranking",
|
||||
"text",
|
||||
"comment",
|
||||
];
|
||||
const jsPsychSurveyCssClassMap = {
|
||||
body: "jspsych-body",
|
||||
bodyContainer: "jspsych-body-container",
|
||||
question: {
|
||||
content: "jspsych-question-content",
|
||||
mainRoot: "jspsych-question-root",
|
||||
},
|
||||
page: {
|
||||
root: "jspsych-page",
|
||||
},
|
||||
footer: "jspsych-footer",
|
||||
navigation: {
|
||||
complete: "jspsych-nav-complete",
|
||||
},
|
||||
rowMultiple: "jspsych-row-multiple",
|
||||
};
|
||||
|
||||
/**
|
||||
* **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<Info> {
|
||||
static info = info;
|
||||
private survey: Survey;
|
||||
private trial_data: any = {};
|
||||
private survey: SurveyJS.Survey;
|
||||
private start_time: number;
|
||||
|
||||
constructor(private jsPsych: JsPsych) {}
|
||||
constructor(private jsPsych: JsPsych) {
|
||||
this.jsPsych = jsPsych;
|
||||
}
|
||||
|
||||
applyStyles() {
|
||||
// https://surveyjs.io/Examples/Library/?id=custom-theme
|
||||
const colors = StylesManager.ThemeColors["default"];
|
||||
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
|
||||
|
||||
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";
|
||||
// import { StylesManager } from "survey-core";
|
||||
|
||||
StylesManager.applyTheme();
|
||||
// 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
|
||||
|
||||
//colors["$border-color"] = "#e7e7e7";
|
||||
|
||||
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)",
|
||||
// all shadow and border variables below affect the question/panel borders
|
||||
"--sjs-shadow-small": "0px 0px 0px 1px rgba(0, 0, 0, 0.15)",
|
||||
"--sjs-shadow-small-reset": "0px 0px 0px 0px rgba(0, 0, 0, 0.15)",
|
||||
"--sjs-shadow-medium": "0px 0px 0px 1px rgba(0, 0, 0, 0.1)",
|
||||
"--sjs-shadow-large": "0px 8px 16px 0px rgba(0, 0, 0, 0.05)",
|
||||
"--sjs-shadow-inner": "0px 0px 0px 1px rgba(0, 0, 0, 0.15)",
|
||||
"--sjs-shadow-inner-reset": "0px 0px 0px 0px rgba(0, 0, 0, 0.15)",
|
||||
"--sjs-border-light": "rgba(0, 0, 0, 0.15)",
|
||||
"--sjs-border-default": "rgba(0, 0, 0, 0.15)",
|
||||
"--sjs-border-inside": " rgba(0, 0, 0, 0.16)",
|
||||
},
|
||||
themeName: "plain",
|
||||
colorPalette: "light",
|
||||
isPanelless: false,
|
||||
});
|
||||
}
|
||||
|
||||
trial(display_element: HTMLElement, trial: TrialType<Info>) {
|
||||
this.survey = new Survey(); // set up survey in code: https://surveyjs.io/Documentation/Library#survey-objects
|
||||
this.applyStyles(); // applies bootstrap theme
|
||||
// check for empty JSON and no survey function
|
||||
if (JSON.stringify(trial.survey_json) === "{}" && trial.survey_function === null) {
|
||||
console.error(
|
||||
"Survey plugin warning: you must define the survey using a non-empty JSON object and/or a survey function."
|
||||
);
|
||||
}
|
||||
this.survey = new SurveyJS.Survey(trial.survey_json);
|
||||
|
||||
// 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;
|
||||
if (trial.survey_function !== null) {
|
||||
trial.survey_function(this.survey);
|
||||
}
|
||||
|
||||
// required question label
|
||||
this.survey.requiredText = trial.required_question_label;
|
||||
this.applyStyles(this.survey); // customize colors
|
||||
|
||||
// TO DO: add response validation
|
||||
this.survey.checkErrorsMode = "onNextPage"; // onValueChanged
|
||||
// apply our custom CSS class names
|
||||
this.survey.css = jsPsychSurveyCssClassMap;
|
||||
|
||||
// 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
|
||||
|
||||
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];
|
||||
|
||||
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();
|
||||
const data_names = Object.keys(sender.data);
|
||||
@ -529,271 +181,24 @@ class SurveyPlugin implements JsPsychPlugin<Info> {
|
||||
}
|
||||
}
|
||||
|
||||
// TO DO: restructure survey data (sender.data) here?
|
||||
// clear display and reset flex on jspsych-content-wrapper
|
||||
display_element.innerHTML = "";
|
||||
document.querySelector<HTMLElement>(".jspsych-content-wrapper").style.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,
|
||||
});
|
||||
});
|
||||
|
||||
// remove flex display from jspsych-content-wrapper to get formatting to work
|
||||
document.querySelector<HTMLElement>(".jspsych-content-wrapper").style.display = "block";
|
||||
|
||||
this.survey.render(display_element);
|
||||
|
||||
this.start_time = performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate parameters for any question type
|
||||
*
|
||||
* @param supplied
|
||||
* @param required
|
||||
* @param optional
|
||||
* @returns
|
||||
*/
|
||||
private static validate_question_params(
|
||||
supplied: Record<string, unknown>,
|
||||
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}".`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const invalid_params = Object.keys(supplied).filter(
|
||||
(param) => !(optional.includes(param) || required.includes(param))
|
||||
);
|
||||
|
||||
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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set defaults for undefined question-specific parameters
|
||||
**/
|
||||
private static set_question_defaults = (
|
||||
supplied_params: Record<string, unknown>,
|
||||
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.length > 0) {
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user