quizdown-extended
Version:
A fork from [bonartm/quizdown-js](https://github.com/bonartm/quizdown-js) with more features. > The Katex-extension is currently not supported
343 lines (301 loc) • 9.94 kB
text/typescript
import { writable, get, type Writable } from 'svelte/store';
import autoBind from 'auto-bind';
import type { Config } from './lib/config.js';
import quizdown from './quizdown.js';
import { isEqualArray } from './lib/utils/arrayComparison'
import { shuffle } from './lib/utils/shuffle';
function isEqual(a1: Array<number>, a2: Array<number>): boolean {
return isEqualArray(a1, a2);
}
// we need to reference the classes in the svelte app despite minifaction of class names
export type QuestionType = 'MultipleChoice' | 'SingleChoice' | 'Sequence';
export abstract class BaseQuestion {
readonly text: string;
answers: Array<Answer>;
readonly originalAnswers: Array<Answer>; // To preserve initial order
readonly explanation: string;
selected: Array<number>;
solved: boolean;
correct: boolean;
readonly hint: string;
readonly questionType: QuestionType;
readonly options: Config;
showHint: Writable<boolean>;
visited: boolean;
constructor(
text: string,
explanation: string,
hint: string,
answers: Array<Answer>,
questionType: QuestionType,
options: Config
) {
if (answers.length === 0) {
throw new Error('no answers for question provided');
}
this.text = text;
this.explanation = explanation;
this.hint = hint;
this.solved = false;
this.correct = false;
this.showHint = writable(false);
this.options = options;
this.answers = [...answers]; // Create a mutable copy
this.originalAnswers = [...answers]; // Create a copy to preserve original order
this.questionType = questionType;
this.visited = false;
autoBind(this);
this.reset();
}
enableHint() {
this.showHint.update((val) => true);
}
reset() {
this.selected = [];
this.solved = false;
this.visited = false;
this.showHint.set(false);
if (this.options.shuffleAnswers) {
this.answers = shuffle([...this.originalAnswers], this.originalAnswers.length);
} else {
this.answers = [...this.originalAnswers];
}
}
getTextWithoutHTML() {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = this.text;
return tempDiv.textContent || tempDiv.innerText || '';
}
abstract isCorrect(): boolean;
}
class Blanks extends BaseQuestion {
isCorrect() {
let trueAnswerIds = this.answers
.filter((answer) => answer.correct)
.map((answer) => answer.id);
let selectedAnswerIds = this.selected.map((i) => this.answers[i].id);
this.solved = isEqual(trueAnswerIds.sort(), selectedAnswerIds.sort());
return this.solved;
}
}
class Pairs extends BaseQuestion {
isCorrect() {
let trueAnswerIds = this.answers
.filter((answer) => answer.correct)
.map((answer) => answer.id);
let selectedAnswerIds = this.selected.map((i) => this.answers[i].id);
this.solved = isEqual(trueAnswerIds.sort(), selectedAnswerIds.sort());
return this.solved;
}
}
export class Sequence extends BaseQuestion {
constructor(
text: string,
explanation: string,
hint: string,
answers: Array<Answer>,
options: Config
) {
// always enable shuffling for sequence questions
options.shuffleAnswers = true;
super(text, explanation, hint, answers, 'Sequence', options);
}
isCorrect() {
if (this.answers.length !== this.originalAnswers.length) return false;
const ids1 = this.answers.map(obj => obj.id);
const ids2 = this.originalAnswers.map(obj => obj.id);
return ids1.every((id, index) => id === ids2[index]);
}
}
class Choice extends BaseQuestion {
isCorrect() {
let trueAnswerIds = this.answers
.filter((answer) => answer.correct)
.map((answer) => answer.id);
let selectedAnswerIds = this.selected.map((i) => this.answers[i].id);
this.solved = isEqual(trueAnswerIds.sort(), selectedAnswerIds.sort());
return this.solved;
}
}
export class MultipleChoice extends Choice {
constructor(
text: string,
explanation: string,
hint: string,
answers: Array<Answer>,
options: Config
) {
super(text, explanation, hint, answers, 'MultipleChoice', options);
}
}
export class SingleChoice extends Choice {
constructor(
text: string,
explanation: string,
hint: string,
answers: Array<Answer>,
options: Config
) {
super(text, explanation, hint, answers, 'SingleChoice', options);
let nCorrect = this.answers.filter((answer) => answer.correct).length;
if (nCorrect > 1) {
throw new Error('Single Choice questions can not have more than one correct answer.');
}
}
isCorrect(): boolean {
// 1. Find the ID of the answer that is marked as correct.
const correctAnswer = this.answers.find(answer => answer.correct);
if (!correctAnswer) {
this.solved = false;
return false;
}
const correctAnswerId = correctAnswer.id;
// 2. Find the ID of the answer the user selected.
if (this.selected.length === 0) {
this.solved = false;
return false;
}
const selectedIndex = this.selected[0];
const selectedAnswer = this.answers[selectedIndex];
if (!selectedAnswer) {
this.solved = false;
return false;
}
const selectedAnswerId = selectedAnswer.id;
// 3. Compare the two stable IDs.
this.solved = (correctAnswerId === selectedAnswerId);
return this.solved;
}
}
export class Answer {
html: string;
correct: boolean;
id: number;
comment: string;
constructor(id: number, html: string, correct: boolean, comment: string) {
this.html = html;
this.correct = correct;
this.id = id;
this.comment = comment;
autoBind(this);
}
}
export class Quiz {
questions: Array<BaseQuestion>;
active: Writable<BaseQuestion>;
index: Writable<number>;
config: Config;
onLast: Writable<boolean>;
onResults: Writable<boolean>;
onFirst: Writable<boolean>;
isEvaluated: Writable<boolean>;
allVisited: Writable<boolean>;
constructor(questions: Array<BaseQuestion>, config: Config) {
this.index = writable(0);
this.questions = questions;
this.config = config;
if (this.config.shuffleQuestions) {
this.questions = shuffle(this.questions, this.config.nQuestions);
}
if (this.questions.length == 0) {
throw new Error('No questions for quiz provided');
}
// setup first question
this.active = writable(this.questions[0]);
this.questions[0].visited = true;
this.onLast = writable(this.questions.length == 1);
this.onResults = writable(false);
this.onFirst = writable(true);
this.allVisited = writable(this.questions.length == 1);
this.isEvaluated = writable(false);
autoBind(this);
}
private setActive() {
let idx = get(this.index);
this.active.update((act) => this.questions[idx]);
this.questions[idx].visited = true;
}
private checkAllVisited(): boolean {
for (let question of this.questions) {
if (!question.visited) {
return false;
}
}
return true;
}
jump(index: number): boolean {
if (index <= this.questions.length - 1 && index >= 0) {
// on a question
this.index.set(index);
this.setActive();
this.allVisited.set(this.checkAllVisited());
this.onResults.set(false);
this.onLast.set(index == this.questions.length - 1);
this.onFirst.set(index == 0);
return true;
} else if (index == this.questions.length) {
// on results page
this.onResults.set(true);
this.onLast.set(false);
this.index.set(index);
return true;
} else {
return false;
}
}
next(): boolean {
return this.jump(get(this.index) + 1);
}
previous(): boolean {
return this.jump(get(this.index) - 1);
}
reset(): boolean {
this.onLast.set(false);
this.onResults.set(false);
this.allVisited.set(false);
this.isEvaluated.set(false);
this.questions.forEach((q) => q.reset());
if (this.config.shuffleQuestions) {
this.questions = shuffle(this.questions, this.config.nQuestions);
}
return this.jump(0);
}
evaluate(): number {
var points = 0;
for (var q of this.questions) {
if (q.isCorrect()) {
points += 1;
q.correct = q.isCorrect();
}
}
this.isEvaluated.set(true);
return points;
}
getStats(): object {
const numberOfQuestions = this.questions.length;
let visited = 0;
let solved = 0;
let wrong = 0;
let right = 0;
this.questions.forEach((question) => {
if (question.visited === true) {
visited++;
}
if (question.solved === true) {
solved++;
}
if (question.isCorrect() === true) {
right++;
} else {
wrong++;
}
});
return {
numberOfQuestions,
visited,
solved,
wrong,
right,
}
}
}