228 lines
8.5 KiB
Vue
228 lines
8.5 KiB
Vue
<template>
|
|
<div v-for="(q, i) in questions" :key="i">
|
|
<transition appear name="fade" mode="out-in">
|
|
<buttonQuestion
|
|
v-if="isQuestionVisible(q)"
|
|
class="radioQuestion"
|
|
:class="isCurrentQuestion(q) && 'current-question'"
|
|
:questionText="q.questionText"
|
|
:answers="q.answers"
|
|
:groupName="`question-${index}-${q.questionSequence}`"
|
|
v-model="q.answerSelected"
|
|
@update:modelValue="handleAnswer(q, $event)"
|
|
isRequired
|
|
:validationRules="validationRules" />
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import buttonQuestion from "@/digital-components/button-question/button-question";
|
|
import { useValidateForm } from "vee-validate";
|
|
import {
|
|
mapQuestionAnswersForChain,
|
|
normalizeAnswerSelectedValue,
|
|
} from "@/helpers/question-chain-helper";
|
|
|
|
export default {
|
|
name: "questionChain",
|
|
data() {
|
|
return {
|
|
currentQuestionNum: 0,
|
|
questions: [],
|
|
};
|
|
},
|
|
props: {
|
|
questionData: Array,
|
|
validationRules: String,
|
|
modelValue: Object,
|
|
index: Number,
|
|
answerKey: String,
|
|
hasSavedAnswer: Boolean,
|
|
hasDownstreamSavedAnswers: Boolean,
|
|
},
|
|
async created() {
|
|
// do a test validation check upon create to prevent out of sync / incorrect valid states
|
|
await useValidateForm(); // NOTE: needs to have async/await here; tested and won't work without it
|
|
|
|
this.questionData.map((q, i) => {
|
|
const answers = mapQuestionAnswersForChain(q);
|
|
const question = {
|
|
questionText: q.questionText,
|
|
questionSequence: q.questionSequence,
|
|
answers,
|
|
answerSelected: normalizeAnswerSelectedValue(q.answerSelected, answers),
|
|
};
|
|
if (!q.suppressThisQuestion) {
|
|
this.questions.push(question);
|
|
}
|
|
});
|
|
|
|
if (this.questions.length > 0) {
|
|
this.initializeCurrentQuestionNum();
|
|
this.scrollCurrentQuestionIntoView();
|
|
}
|
|
},
|
|
methods: {
|
|
getQuestionSequence(question) {
|
|
return Number(question?.questionSequence);
|
|
},
|
|
isCurrentQuestion(question) {
|
|
return this.getQuestionSequence(question) === Number(this.currentQuestionNum);
|
|
},
|
|
isQuestionVisible(question) {
|
|
return !!question.answerSelected || this.isCurrentQuestion(question);
|
|
},
|
|
initializeCurrentQuestionNum() {
|
|
const pendingNextQuestion = [...this.questions]
|
|
.reverse()
|
|
.find((question) => question.answerSelected?.includes("|nextQuestion|"));
|
|
|
|
if (pendingNextQuestion) {
|
|
const nextQuestionSequence = Number(
|
|
pendingNextQuestion.answerSelected.split("|")[2]
|
|
);
|
|
|
|
if (
|
|
this.questions.some(
|
|
(question) => this.getQuestionSequence(question) === nextQuestionSequence
|
|
)
|
|
) {
|
|
this.currentQuestionNum = nextQuestionSequence;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const firstUnanswered = this.questions.find((question) => !question.answerSelected);
|
|
|
|
if (firstUnanswered) {
|
|
this.currentQuestionNum = this.getQuestionSequence(firstUnanswered);
|
|
}
|
|
},
|
|
scrollCurrentQuestionIntoView() {
|
|
this.$nextTick(() => {
|
|
document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" });
|
|
});
|
|
},
|
|
syncAnswerSelectedToQuestionData(questionSequence, answerSelected) {
|
|
const sourceQuestion = this.questionData?.find(
|
|
(q) => this.getQuestionSequence(q) === Number(questionSequence)
|
|
);
|
|
|
|
if (sourceQuestion) {
|
|
sourceQuestion.answerSelected = answerSelected;
|
|
}
|
|
},
|
|
getProblemQuestionIdFromSelectedAnswer(question, selectedAnswer) {
|
|
if (!question?.answers || !selectedAnswer) {
|
|
return null;
|
|
}
|
|
|
|
const matchedAnswer = question.answers.find(
|
|
(answer) => answer.value === selectedAnswer
|
|
);
|
|
|
|
return matchedAnswer?.problemQuestionId ?? null;
|
|
},
|
|
handleAnswer(question, returnedAnswer) {
|
|
/*
|
|
returnedAnswer example format:
|
|
"1|answer|DD11132|Yes"
|
|
*/
|
|
|
|
question.answerSelected = returnedAnswer;
|
|
this.syncAnswerSelectedToQuestionData(question.questionSequence, returnedAnswer);
|
|
const questionChainAnswer = this.getQuestionChainAnswerIfComplete(returnedAnswer);
|
|
|
|
if (!questionChainAnswer) {
|
|
return;
|
|
}
|
|
|
|
const shouldNotifyParent =
|
|
!questionChainAnswer.incomplete ||
|
|
this.hasSavedAnswer ||
|
|
this.hasDownstreamSavedAnswers;
|
|
|
|
if (shouldNotifyParent) {
|
|
this.$emit("update:modelValue", questionChainAnswer);
|
|
}
|
|
},
|
|
getQuestionChainAnswerIfComplete(returnedAnswer) {
|
|
// this method will return either a final answer or Boolean false
|
|
if (!returnedAnswer) {
|
|
return false;
|
|
}
|
|
|
|
// returnedAnswer examples:
|
|
// "1|nextQuestion|3|No"
|
|
// "5|answer|DW02104|Yes"
|
|
|
|
const returnedAnswerArray = returnedAnswer.split("|");
|
|
const questionNum = Number(returnedAnswerArray[0]);
|
|
const questionType = returnedAnswerArray[1];
|
|
const questionAnswer = returnedAnswerArray[2];
|
|
const answeredQuestions = [];
|
|
|
|
this.questions.forEach((q) => {
|
|
const questionSequence = this.getQuestionSequence(q);
|
|
|
|
// find this question and mark it as "answered" by populating answerSelected
|
|
if (questionSequence === questionNum) {
|
|
q.answerSelected = returnedAnswer;
|
|
this.syncAnswerSelectedToQuestionData(questionSequence, returnedAnswer);
|
|
}
|
|
// remove all answers AFTER this question...
|
|
// (needed in case user is changing previously answered questions)
|
|
if (questionSequence > questionNum) {
|
|
delete q.answerSelected;
|
|
this.syncAnswerSelectedToQuestionData(questionSequence, "");
|
|
}
|
|
if (q.answerSelected) {
|
|
answeredQuestions.push({
|
|
questionText: q.questionText,
|
|
selectedAnswer: q.answerSelected,
|
|
selectedAnswerText: q.answerSelected.split("|")[3],
|
|
questionNum: q.questionSequence,
|
|
problemQuestionId: this.getProblemQuestionIdFromSelectedAnswer(
|
|
q,
|
|
q.answerSelected
|
|
),
|
|
});
|
|
}
|
|
});
|
|
|
|
// return incomplete answer if there's a nextQuestion... or return final answers (truthy)
|
|
if (questionType === "nextQuestion") {
|
|
// update to next question index
|
|
this.currentQuestionNum = Number(questionAnswer);
|
|
this.scrollCurrentQuestionIntoView();
|
|
return {
|
|
incomplete: true,
|
|
answeredQuestions,
|
|
index: this.index,
|
|
};
|
|
} else {
|
|
// reset current question index (removes .current-question class)
|
|
this.currentQuestionNum = 0; // reset count
|
|
const answeredQuestion = this.questions.find(
|
|
(q) => this.getQuestionSequence(q) === questionNum
|
|
);
|
|
|
|
// return an object with the part answer, all the answered questions, and the part index
|
|
return {
|
|
answerResult: questionAnswer,
|
|
problemQuestionId: this.getProblemQuestionIdFromSelectedAnswer(
|
|
answeredQuestion,
|
|
returnedAnswer
|
|
),
|
|
answeredQuestions: answeredQuestions,
|
|
index: this.index,
|
|
};
|
|
}
|
|
},
|
|
},
|
|
components: {
|
|
buttonQuestion,
|
|
},
|
|
};
|
|
</script>
|