|
// retoor <retoor@molodetz.nl>
|
|
|
|
import { Component } from "./Component.js";
|
|
import { Http } from "../Http.js";
|
|
import { Format } from "../Format.js";
|
|
|
|
const TICK_MS = 1000;
|
|
const CHECKING_LABEL = "Checking…";
|
|
|
|
export class AppQuizPlayer extends Component {
|
|
connectedCallback() {
|
|
this._attemptUrl = this.attr("data-attempt-url");
|
|
this._remaining = this.intAttr("data-remaining", 0);
|
|
this._hasLimit = this.attr("data-has-limit") === "1";
|
|
this._status = this.attr("data-status", "in_progress");
|
|
this._progress = this.querySelector("[data-quiz-progress]");
|
|
this._score = this.querySelector("[data-quiz-score]");
|
|
this._timer = this.querySelector("[data-quiz-timer]");
|
|
this._bindForms();
|
|
if (this._hasLimit && this._status === "in_progress") this._startTimer();
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this._interval) clearInterval(this._interval);
|
|
}
|
|
|
|
_bindForms() {
|
|
this.querySelectorAll("[data-quiz-answer-form]").forEach((form) => {
|
|
form.addEventListener("submit", (event) => {
|
|
event.preventDefault();
|
|
this._submit(form);
|
|
});
|
|
});
|
|
}
|
|
|
|
_startTimer() {
|
|
this._interval = setInterval(() => {
|
|
this._remaining = Math.max(0, this._remaining - 1);
|
|
if (this._timer) this._timer.textContent = Format.duration(this._remaining);
|
|
if (this._remaining === 0) {
|
|
clearInterval(this._interval);
|
|
window.location.reload();
|
|
}
|
|
}, TICK_MS);
|
|
}
|
|
|
|
async _submit(form) {
|
|
const button = form.querySelector("button[type=submit]");
|
|
const original = button ? button.textContent : "";
|
|
if (button) {
|
|
button.disabled = true;
|
|
button.textContent = CHECKING_LABEL;
|
|
}
|
|
let payload;
|
|
try {
|
|
payload = await Http.sendForm(this._attemptUrl + "/answer", this._params(form));
|
|
} catch (error) {
|
|
if (button) {
|
|
button.disabled = false;
|
|
button.textContent = original;
|
|
}
|
|
return;
|
|
}
|
|
this._renderAnswer(form, payload.answer);
|
|
this._renderHud(payload.attempt);
|
|
this._advance(form);
|
|
}
|
|
|
|
_params(form) {
|
|
const params = [];
|
|
new FormData(form).forEach((value, key) => params.push([key, value]));
|
|
return params;
|
|
}
|
|
|
|
_renderAnswer(form, answer) {
|
|
const question = form.closest("[data-quiz-question]");
|
|
if (!question || !answer) return;
|
|
question.classList.add("quiz-question-answered");
|
|
form.querySelectorAll("input, textarea, select, button").forEach((field) => {
|
|
field.disabled = true;
|
|
});
|
|
const actions = form.querySelector(".quiz-answer-actions");
|
|
if (actions) actions.remove();
|
|
const host = question.querySelector("[data-quiz-result]");
|
|
if (!host) return;
|
|
host.hidden = false;
|
|
host.innerHTML = "";
|
|
host.appendChild(this._gradeNode(question, answer));
|
|
if (answer.feedback) host.appendChild(this._textNode("quiz-grade-feedback", answer.feedback));
|
|
if (answer.graded_by === "fallback") {
|
|
host.appendChild(
|
|
this._textNode(
|
|
"quiz-grade-fallback",
|
|
"Automatic review was unavailable, so this answer was scored on keyword overlap.",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
_gradeNode(question, answer) {
|
|
const points = question.querySelector(".quiz-question-points");
|
|
const max = points ? points.textContent.trim().split(" ")[0] : "";
|
|
const grade = document.createElement("div");
|
|
grade.className = "quiz-grade " + (answer.is_correct ? "quiz-grade-correct" : "quiz-grade-wrong");
|
|
const verdict = document.createElement("span");
|
|
verdict.className = "quiz-grade-verdict";
|
|
verdict.textContent = answer.is_correct ? "Correct" : "Not correct";
|
|
const scored = document.createElement("span");
|
|
scored.className = "quiz-grade-points";
|
|
scored.textContent = `${Format.number(answer.awarded_points)} / ${max} points`;
|
|
grade.appendChild(verdict);
|
|
grade.appendChild(scored);
|
|
return grade;
|
|
}
|
|
|
|
_textNode(className, text) {
|
|
const node = document.createElement("p");
|
|
node.className = className;
|
|
node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
_renderHud(attempt) {
|
|
if (!attempt) return;
|
|
if (this._progress) {
|
|
this._progress.textContent = `${attempt.answered_count} / ${attempt.question_count} answered`;
|
|
}
|
|
if (this._score) {
|
|
this._score.textContent = `${Format.number(attempt.score_points)} / ${attempt.max_points} points`;
|
|
}
|
|
this._remaining = attempt.remaining_seconds;
|
|
}
|
|
|
|
_advance(form) {
|
|
const question = form.closest("[data-quiz-question]");
|
|
if (!question) return;
|
|
const next = question.nextElementSibling;
|
|
if (next && next.scrollIntoView) next.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
}
|
|
}
|
|
|
|
customElements.define("dp-quiz-player", AppQuizPlayer);
|