Files
50ohm/assets/exam-simulator.js
SuperminarenandClaude Sonnet 5.5 874838f525 Swedish adaptation: Instegscertifikat and HAREC
- Editions replaced by INSTEG, HAREC and UPPGRADERING (Instegscertifikat -> HAREC)
- Question pool optional; simulator, homework tool and solutions switch off without one
- All UI strings, templates, renderers and project JS localised to sv-SE
  (decimal comma, Swedish non-breaking-space rules, Å/Ä/Ö in Morse table)
- DARC branding, membership CTA and German links removed; attribution footer added
- Page names centralised in src/pages.py
- README and docs rewritten in Swedish

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-10 15:03:02 +02:00

874 lines
35 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
"use strict";
var STORAGE_KEY = "50ohm-exam-simulator-v1";
var STORAGE_VERSION = 3;
var QUESTION_COUNT = 25;
var ANSWER_LABELS = ["A", "B", "C", "D"];
// TODO: Adapt exam parts and passing thresholds once a Swedish question pool exists.
var PART_DEFINITIONS = {
B: { label: "Operativa kunskaper", minutes: 45 },
V: { label: "Kunskaper om föreskrifter", minutes: 45 },
N: { label: "Tekniska kunskaper, nivå N", minutes: 45 },
E: { label: "Tekniska kunskaper, nivå E", minutes: 45 },
A: { label: "Tekniska kunskaper, nivå A", minutes: 60 },
H: { label: "Övning", minutes: null }
};
var EXAM_CHOICES = [
{ id: "N", group: "primary", label: "Nivå N", detail: "B + V + N", parts: ["B", "V", "N"], color: "#47ABE8" },
{ id: "E", group: "primary", label: "Nivå E", detail: "B + V + N + E", parts: ["B", "V", "N", "E"], color: "#FE756C" },
{ id: "A", group: "primary", label: "Nivå A", detail: "B + V + N + E + A", parts: ["B", "V", "N", "E", "A"], color: "#3BB583" },
{ id: "N-E", group: "upgrade", label: "N → E", detail: "Teknik E", parts: ["E"], color: "#FE756C" },
{ id: "N-A", group: "upgrade", label: "N → A", detail: "Teknik E + A", parts: ["E", "A"], color: "#3BB583" },
{ id: "E-A", group: "upgrade", label: "E → A", detail: "Teknik A", parts: ["A"], color: "#3BB583" },
{ id: "part-B", group: "single", label: "B", detail: "Operativa kunskaper", parts: ["B"], color: "#47ABE8" },
{ id: "part-V", group: "single", label: "V", detail: "Kunskaper om föreskrifter", parts: ["V"], color: "#47ABE8" },
{ id: "part-N", group: "single", label: "N", detail: "Teknik nivå N", parts: ["N"], color: "#47ABE8" },
{ id: "part-E", group: "single", label: "E", detail: "Teknik nivå E", parts: ["E"], color: "#FE756C" },
{ id: "part-A", group: "single", label: "A", detail: "Teknik nivå A", parts: ["A"], color: "#3BB583" }
];
function queryValue(name) {
return new URLSearchParams(window.location.search).get(name);
}
function parseHomeworkQuestionIds() {
var value = queryValue("homework");
if (!value) return [];
var seen = new Set();
return value.toUpperCase().split(/[\s+,]+/).filter(function (questionId) {
if (!/^[A-Z]{2,3}\d{3}$/.test(questionId) || seen.has(questionId)) return false;
seen.add(questionId);
return true;
});
}
function parseSharedResult() {
var fields = (queryValue("result") || "").split(".");
if (fields[0] !== "1") return null;
if (fields[1] === "H") {
var homeworkScore = Number(fields[2]);
var homeworkTotal = Number(fields[3]);
if (fields.length !== 4 || !Number.isInteger(homeworkScore) || !Number.isInteger(homeworkTotal) ||
homeworkTotal < 1 || homeworkScore < 0 || homeworkScore > homeworkTotal) return null;
return { kind: "homework", score: homeworkScore, total: homeworkTotal };
}
var choice = EXAM_CHOICES.find(function (item) { return item.id === fields[1]; });
if (!choice || fields.length !== choice.parts.length + 2) return null;
var scores = fields.slice(2).map(Number);
if (scores.some(function (score) { return !Number.isInteger(score) || score < 0 || score > QUESTION_COUNT; })) return null;
return { kind: "exam", choice: choice, scores: scores, passed: scores.every(function (score) { return score >= 19; }) };
}
function copyText(value) {
if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(value);
var input = document.createElement("textarea");
input.value = value;
input.setAttribute("readonly", "");
input.style.position = "fixed";
input.style.opacity = "0";
document.body.appendChild(input);
input.select();
var copied = document.execCommand("copy");
input.remove();
return copied ? Promise.resolve() : Promise.reject(new Error("Det gick inte att kopiera"));
}
function shuffled(values) {
var result = values.slice();
for (var i = result.length - 1; i > 0; i -= 1) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = result[i];
result[i] = result[j];
result[j] = tmp;
}
return result;
}
function buildCategoryTree(pool) {
var root = { children: new Map(), items: [], size: 0 };
pool.forEach(function (item) {
var node = root;
node.size += 1;
(item.category || []).forEach(function (categoryPart) {
if (!node.children.has(categoryPart)) {
node.children.set(categoryPart, { children: new Map(), items: [], size: 0 });
}
node = node.children.get(categoryPart);
node.size += 1;
});
node.items.push(item);
});
return root;
}
function allocateQuotas(children, quota, totalSize) {
var allocations = children.map(function (child) {
var target = quota * child.size / totalSize;
return { child: child, quota: Math.floor(target), fraction: target - Math.floor(target) };
});
var assigned = allocations.reduce(function (sum, allocation) { return sum + allocation.quota; }, 0);
while (assigned < quota) {
var candidates = allocations.filter(function (allocation) {
return allocation.quota < allocation.child.size;
});
var weight = candidates.reduce(function (sum, allocation) { return sum + allocation.fraction; }, 0);
var selected;
if (weight <= 0) {
selected = candidates[Math.floor(Math.random() * candidates.length)];
} else {
var draw = Math.random() * weight;
selected = candidates[candidates.length - 1];
for (var i = 0; i < candidates.length; i += 1) {
draw -= candidates[i].fraction;
if (draw <= 0) {
selected = candidates[i];
break;
}
}
}
selected.quota += 1;
selected.fraction = 0;
assigned += 1;
}
return allocations;
}
function pickFromTree(node, quota) {
if (quota <= 0) return [];
if (node.children.size === 0) return shuffled(node.items).slice(0, quota);
var allocations = allocateQuotas(Array.from(node.children.values()), quota, node.size);
var result = [];
allocations.forEach(function (allocation) {
result = result.concat(pickFromTree(allocation.child, allocation.quota));
});
return shuffled(result);
}
function balancedSample(pool, count) {
if (!Array.isArray(pool) || pool.length < count) {
throw new Error("Det finns inte tillräckligt många frågor för den här provdelen.");
}
return pickFromTree(buildCategoryTree(pool), count);
}
function renderMathSoon(VueApi) {
VueApi.nextTick(function () {
if (typeof window.renderFiftyOhmMath !== "function") return;
window.renderFiftyOhmMath(document.getElementById("exam-simulator"));
});
}
var app = Vue.createApp({
setup: function () {
var ref = Vue.ref;
var computed = Vue.computed;
var watch = Vue.watch;
var onMounted = Vue.onMounted;
var onBeforeUnmount = Vue.onBeforeUnmount;
var loading = ref(true);
var loadError = ref("");
var catalog = ref({});
var altTexts = Vue.reactive({});
var requestedAltTexts = new Set();
var session = ref(null);
var view = ref("exam");
var reviewPartIndex = ref(null);
var sharedResult = ref(null);
var shareMessage = ref("");
var now = ref(Date.now());
var timerId = null;
var catalogLoadHandler = null;
var headerObserver = null;
var primaryChoices = computed(function () {
return EXAM_CHOICES.filter(function (choice) { return choice.group === "primary"; });
});
var upgradeChoices = computed(function () {
return EXAM_CHOICES.filter(function (choice) { return choice.group === "upgrade"; });
});
var singlePartChoices = computed(function () {
return EXAM_CHOICES.filter(function (choice) { return choice.group === "single"; });
});
var currentChoice = computed(function () {
if (!session.value) return null;
if (session.value.mode === "homework") {
return { id: "H", label: "Övning", parts: ["H"] };
}
return EXAM_CHOICES.find(function (choice) { return choice.id === session.value.choiceId; });
});
var displayedPartIndex = computed(function () {
return reviewPartIndex.value === null ? session.value.currentPart : reviewPartIndex.value;
});
var currentPart = computed(function () {
if (!session.value) return null;
return session.value.parts[displayedPartIndex.value];
});
var currentPartDefinition = computed(function () {
if (!currentPart.value) return null;
return PART_DEFINITIONS[currentPart.value.code];
});
var isHomework = computed(function () {
return Boolean(session.value && session.value.mode === "homework");
});
var currentQuestionCount = computed(function () {
return currentPart.value ? currentPart.value.questions.length : 0;
});
var remainingSeconds = computed(function () {
if (isHomework.value || !currentPart.value || currentPart.value.status !== "running") return 0;
if (currentPart.value.paused) return currentPart.value.pausedRemainingSeconds;
return Math.max(0, Math.ceil((currentPart.value.deadline - now.value) / 1000));
});
var formattedTime = computed(function () {
var minutes = Math.floor(remainingSeconds.value / 60);
var seconds = remainingSeconds.value % 60;
return String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0");
});
var answeredCount = computed(function () {
if (!currentPart.value) return 0;
return currentPart.value.questions.filter(function (question) { return question.selected !== null; }).length;
});
var hasNextPart = computed(function () {
if (!session.value) return false;
return session.value.currentPart < session.value.parts.length - 1;
});
var isReviewing = computed(function () { return reviewPartIndex.value !== null; });
function evaluatedParts() {
return session.value.parts.filter(function (part) { return part.status === "evaluated"; });
}
var overallState = computed(function () {
if (isHomework.value) return "homework";
if (session.value.parts.some(function (part) { return part.status !== "evaluated"; })) return "incomplete";
var failed = session.value.parts.filter(function (part) { return part.score < 19; });
if (failed.length === 0) return "passed";
if (failed.length === 1 && failed[0].score >= 17) return "oral";
return "failed";
});
var overallAlertClass = computed(function () {
return { homework: "alert-primary", passed: "alert-success", oral: "alert-warning", failed: "alert-danger", incomplete: "alert-danger" }[overallState.value];
});
var overallIcon = computed(function () {
return { homework: "task_alt", passed: "verified", oral: "record_voice_over", failed: "cancel", incomplete: "stop_circle" }[overallState.value];
});
var overallTitle = computed(function () {
return { homework: "Övningen är rättad", passed: "Provet godkänt", oral: "Muntlig komplettering möjlig", failed: "Provet inte godkänt", incomplete: "Provet avslutades i förtid" }[overallState.value];
});
var overallText = computed(function () {
if (overallState.value === "homework") {
var homeworkPart = session.value.parts[0];
return homeworkPart.score + " av " + homeworkPart.questions.length + " frågor besvarades rätt.";
}
if (overallState.value === "passed") return "Grattis! Du har klarat varje provdel med minst 19 poäng.";
if (overallState.value === "oral") return "I exakt en provdel nåddes 17 eller 18 poäng; enligt provreglerna kan en muntlig komplettering vara möjlig.";
if (overallState.value === "failed") return "Minst en provdel är inte godkänd.";
return evaluatedParts().length + " av " + session.value.parts.length + " provdelar har slutförts.";
});
function questionData(questionState) {
return catalog.value[questionState.id];
}
function padNumber(number) {
return String(number).padStart(2, "0");
}
function displayQuestionNumber(questionState, questionIndex) {
if (isHomework.value || currentPart.value.status === "evaluated") return questionState.id;
return padNumber(questionIndex + 1);
}
function pictureAlt(picture, fallback) {
return altTexts[picture] || fallback;
}
function loadAltText(picture) {
if (!picture || requestedAltTexts.has(picture)) return;
requestedAltTexts.add(picture);
fetch("pictures/" + encodeURIComponent(picture) + ".txt")
.then(function (response) {
if (!response.ok) return "";
return response.text();
})
.then(function (description) {
if (description && description.trim()) altTexts[picture] = description.trim();
})
.catch(function () { /* the generic alternative text remains available */ });
}
function loadPartAltTexts(part) {
if (!part) return;
part.questions.forEach(function (questionState) {
var question = questionData(questionState);
if (!question) return;
loadAltText(question.picture);
question.answers.forEach(function (answer) { loadAltText(answer.picture); });
});
}
function createPart(code) {
var picked = balancedSample(Object.values(catalog.value).filter(function (question) {
return question.part === code;
}), QUESTION_COUNT);
return {
code: code,
status: "pending",
deadline: null,
paused: false,
pausedRemainingSeconds: null,
score: null,
autoSubmitted: false,
questions: picked.map(function (question) {
return { id: question.id, order: shuffled([0, 1, 2, 3]), selected: null, history: [] };
})
};
}
function createHomeworkPart(questionIds) {
var questions = questionIds.map(function (questionId) { return catalog.value[questionId]; }).filter(Boolean);
if (questions.length !== questionIds.length) {
throw new Error("Minst en fråga i övningslänken finns inte i den aktuella frågebanken.");
}
if (questions.length === 0) throw new Error("Övningslänken innehåller inga giltiga frågor.");
return {
code: "H",
status: "pending",
deadline: null,
paused: false,
pausedRemainingSeconds: null,
score: null,
autoSubmitted: false,
questions: questions.map(function (question) {
return { id: question.id, order: shuffled([0, 1, 2, 3]), selected: null, history: [] };
})
};
}
function activatePart(part) {
part.status = "running";
part.deadline = part.code === "H" ? null : Date.now() + PART_DEFINITIONS[part.code].minutes * 60 * 1000;
part.paused = false;
part.pausedRemainingSeconds = null;
now.value = Date.now();
}
function toggleTimer() {
var part = currentPart.value;
if (isHomework.value || !part || part.status !== "running") return;
if (part.paused) {
part.deadline = Date.now() + part.pausedRemainingSeconds * 1000;
part.pausedRemainingSeconds = null;
part.paused = false;
now.value = Date.now();
return;
}
var seconds = remainingSeconds.value;
if (seconds <= 0) {
submitPart(true);
return;
}
part.pausedRemainingSeconds = seconds;
part.paused = true;
part.deadline = null;
}
function startExam(choice) {
try {
var parts = choice.parts.map(createPart);
activatePart(parts[0]);
session.value = {
version: STORAGE_VERSION,
mode: "exam",
choiceId: choice.id,
currentPart: 0,
startedAt: Date.now(),
parts: parts
};
view.value = "exam";
reviewPartIndex.value = null;
renderMathSoon(Vue);
} catch (error) {
loadError.value = error.message;
}
}
function startHomework(questionIds) {
try {
var part = createHomeworkPart(questionIds);
activatePart(part);
session.value = {
version: STORAGE_VERSION,
mode: "homework",
choiceId: "H",
homeworkIds: questionIds.slice(),
currentPart: 0,
startedAt: Date.now(),
parts: [part]
};
view.value = "exam";
reviewPartIndex.value = null;
renderMathSoon(Vue);
} catch (error) {
loadError.value = error.message;
}
}
function selectAnswer(questionIndex, answerIndex) {
if (currentPart.value.status !== "running" || currentPart.value.paused) return;
var question = currentPart.value.questions[questionIndex];
if (question.selected === answerIndex) return;
var firstAnswer = question.selected === null;
question.selected = answerIndex;
question.history.push(answerIndex);
if (firstAnswer && questionIndex < currentPart.value.questions.length - 1) {
window.setTimeout(function () { scrollToQuestion(questionIndex + 1); }, 150);
}
}
function answerMark(questionIndex, answerIndex) {
var question = currentPart.value.questions[questionIndex];
if (question.selected === answerIndex) {
var restored = question.history.slice(0, -1).indexOf(answerIndex) !== -1;
return '<span class="answer-mark-symbol' + (restored ? " answer-mark-restored" : "") + '">×</span>';
}
if (question.history.indexOf(answerIndex) !== -1) {
return '<span class="answer-mark-symbol">=</span>';
}
return "";
}
function answerCellClass(questionIndex, answerIndex) {
if (currentPart.value.status === "running") return "";
var question = currentPart.value.questions[questionIndex];
var sourceIndex = question.order[answerIndex];
if (question.selected === answerIndex && sourceIndex === 0) return "answer-cell-correct";
if (question.selected === answerIndex) return "answer-cell-wrong";
if (sourceIndex === 0) return "answer-cell-solution";
return "";
}
function questionAnswerClass(questionIndex, answerIndex) {
var question = currentPart.value.questions[questionIndex];
if (currentPart.value.status === "running") {
return question.selected === answerIndex ? "exam-answer-selected" : "";
}
var sourceIndex = question.order[answerIndex];
if (sourceIndex === 0) return "correct";
if (question.selected === answerIndex) return "wrong";
return "";
}
function submitPart(autoSubmitted) {
if (!session.value || currentPart.value.status !== "running") return;
var score = currentPart.value.questions.reduce(function (sum, question) {
return sum + (question.selected !== null && question.order[question.selected] === 0 ? 1 : 0);
}, 0);
currentPart.value.score = score;
currentPart.value.status = "evaluated";
currentPart.value.deadline = null;
currentPart.value.paused = false;
currentPart.value.pausedRemainingSeconds = null;
currentPart.value.autoSubmitted = Boolean(autoSubmitted);
renderMathSoon(Vue);
window.scrollTo({ top: 0, behavior: "smooth" });
}
function requestSubmit() {
var unanswered = currentQuestionCount.value - answeredCount.value;
var text = unanswered > 0
? "Du har inte besvarat " + unanswered + " frågor. Vill du lämna in " + (isHomework.value ? "övningen" : "provdelen") + " ändå?"
: "Vill du lämna in " + (isHomework.value ? "övningen" : "den här provdelen") + " nu? Det går inte att ångra.";
if (window.confirm(text)) submitPart(false);
}
function startNextPart() {
var nextIndex = session.value.currentPart + 1;
var nextPart = session.value.parts[nextIndex];
if (!nextPart) {
showSummary();
return;
}
session.value.currentPart = nextIndex;
reviewPartIndex.value = null;
view.value = "exam";
activatePart(nextPart);
renderMathSoon(Vue);
window.scrollTo({ top: 0, behavior: "smooth" });
}
function finishExamEarly() {
if (!window.confirm("Vill du verkligen avsluta provet här? Delar som inte har gjorts räknas som ej slutförda.")) return;
showSummary();
}
function showSummary() {
reviewPartIndex.value = null;
view.value = "summary";
if (completedResultUrl.value) {
var target = new URL(completedResultUrl.value);
if (window.location.pathname !== target.pathname || window.location.search !== target.search) {
window.location.assign(target.toString());
}
}
window.scrollTo({ top: 0, behavior: "smooth" });
}
function reviewPart(index) {
reviewPartIndex.value = index;
view.value = "exam";
renderMathSoon(Vue);
window.scrollTo({ top: 0, behavior: "smooth" });
}
function clearStoredSession() {
try { window.localStorage.removeItem(STORAGE_KEY); } catch (error) { /* storage can be unavailable */ }
}
function resetExam() {
clearStoredSession();
session.value = null;
view.value = "exam";
reviewPartIndex.value = null;
sharedResult.value = null;
shareMessage.value = "";
window.scrollTo({ top: 0, behavior: "smooth" });
}
function abortExam() {
var noun = isHomework.value ? "övningen" : "simuleringen";
if (!window.confirm("Vill du avbryta " + noun + "? Det sparade läget raderas.")) return;
resetExam();
window.history.replaceState(null, "", "simulering.html");
}
function newExam() {
resetExam();
window.history.replaceState(null, "", "simulering.html");
}
function retryHomework() {
var questionIds = session.value && session.value.homeworkIds;
if (!questionIds) return;
clearStoredSession();
var homeworkUrl = new URL("simulering.html", window.location.href);
homeworkUrl.search = "?homework=" + questionIds.join("+");
window.history.replaceState(null, "", homeworkUrl.toString());
startHomework(questionIds);
window.scrollTo({ top: 0, behavior: "smooth" });
}
function scrollToQuestion(index) {
var element = document.getElementById("exam-question-" + index);
if (element) element.scrollIntoView({ behavior: "smooth", block: "start" });
}
function resultLabel(part) {
if (part.code === "H") return "Rättad";
if (part.score >= 19) return "Godkänd";
if (part.score >= 17) return "Komplettering möjlig";
return "Inte godkänd";
}
function resultAlertClass(part) {
if (part.code === "H") return "alert-primary";
if (part.score >= 19) return "alert-success";
if (part.score >= 17) return "alert-warning";
return "alert-danger";
}
function resultBadgeClass(part) {
if (part.code === "H") return "text-bg-primary";
if (part.score >= 19) return "text-bg-success";
if (part.score >= 17) return "text-bg-warning";
return "text-bg-danger";
}
function resultIcon(part) {
if (part.code === "H") return "task_alt";
if (part.score >= 19) return "check_circle";
if (part.score >= 17) return "record_voice_over";
return "cancel";
}
function resultDescription(part) {
if (part.code === "H") return part.score + " av " + part.questions.length + " frågor besvarades rätt.";
var prefix = part.autoSubmitted ? "Tiden gick ut och provdelen lämnades in automatiskt. " : "";
if (part.score >= 19) return prefix + "De nödvändiga 19 poängen uppnåddes.";
if (part.score >= 17) return prefix + "Med 17 eller 18 poäng kan en muntlig komplettering vara möjlig enligt provreglerna.";
return prefix + "De nödvändiga 19 poängen uppnåddes inte.";
}
function encodedResult(result) {
if (result.kind === "homework") return "1.H." + result.score + "." + result.total;
return "1." + result.choice.id + "." + result.scores.join(".");
}
var resultForSharing = computed(function () {
if (sharedResult.value) return sharedResult.value;
if (view.value !== "summary") return null;
return shareableSessionResult();
});
function buildResultUrl(result) {
if (!result) return "";
var url = new URL("resultat.html", window.location.href);
url.search = "?result=" + encodedResult(result);
return url.toString();
}
var completedResultUrl = computed(function () {
return buildResultUrl(shareableSessionResult());
});
var resultShareUrl = computed(function () {
return buildResultUrl(resultForSharing.value);
});
// There are no result images for sharing.
var resultImageUrl = computed(function () {
return "";
});
var shareText = computed(function () {
var result = resultForSharing.value;
if (!result) return "";
if (result.kind === "homework") {
return "Min övning på 50Ω: " + result.score + " av " + result.total + " frågor rätt.";
}
var scores = result.choice.parts.map(function (part, index) { return part + ": " + result.scores[index] + "/25"; }).join(", ");
return "Min provsimulering på 50Ω " + result.choice.label + ": " + scores + " – " + (result.passed ? "godkänd!" : "ännu inte godkänd.");
});
var socialShareLinks = computed(function () {
var textWithUrl = shareText.value + " " + resultShareUrl.value;
return {
mastodon: "https://share.joinmastodon.org/#text=" + encodeURIComponent(textWithUrl),
bluesky: "https://bsky.app/intent/compose?text=" + encodeURIComponent(textWithUrl),
facebook: "https://www.facebook.com/sharer/sharer.php?u=" + encodeURIComponent(resultShareUrl.value),
x: "https://x.com/intent/tweet?text=" + encodeURIComponent(textWithUrl)
};
});
function copyResultLink() {
copyText(resultShareUrl.value)
.then(function () { shareMessage.value = "Resultatlänken kopierades."; })
.catch(function () { shareMessage.value = "Länken kunde inte kopieras automatiskt."; });
}
function shareNative() {
if (!navigator.share) {
copyResultLink();
return;
}
navigator.share({ title: "Resultat från 50Ω", text: shareText.value, url: resultShareUrl.value }).catch(function () {});
}
function restoreSession(homeworkQuestionIds, allowHomeworkResult) {
try {
var raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return;
var stored = JSON.parse(raw);
if (!stored || stored.version !== STORAGE_VERSION || !stored.session) {
clearStoredSession();
return;
}
var homeworkMode = stored.session.mode === "homework";
var choice = EXAM_CHOICES.find(function (item) { return item.id === stored.session.choiceId; });
var homeworkRequested = homeworkQuestionIds.length > 0;
var homeworkMatches = homeworkMode && (allowHomeworkResult || (homeworkQuestionIds.length > 0 &&
JSON.stringify(stored.session.homeworkIds) === JSON.stringify(homeworkQuestionIds)));
var requestedSessionMatches = allowHomeworkResult
? (homeworkMatches || choice)
: (homeworkRequested ? homeworkMatches : (!homeworkMode && choice));
var valid = requestedSessionMatches && stored.session.parts.every(function (part) {
return Array.isArray(part.questions) && (homeworkMode || part.questions.length === QUESTION_COUNT) && part.questions.every(function (question) {
return Boolean(catalog.value[question.id]);
});
});
if (!valid) {
clearStoredSession();
return;
}
stored.session.parts.forEach(function (part) {
part.paused = Boolean(part.paused);
if (!part.paused) part.pausedRemainingSeconds = null;
});
session.value = stored.session;
if (!session.value.mode) session.value.mode = "exam";
view.value = stored.view === "summary" ? "summary" : "exam";
if (session.value.parts[session.value.currentPart].status === "running" &&
!session.value.parts[session.value.currentPart].paused &&
session.value.parts[session.value.currentPart].deadline <= Date.now()) {
submitPart(true);
}
} catch (error) {
clearStoredSession();
}
}
watch([session, view], function () {
if (!session.value) return;
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify({
version: STORAGE_VERSION,
session: session.value,
view: view.value
}));
} catch (error) { /* simulator remains usable without persistence */ }
}, { deep: true });
watch(currentPart, function (part) {
loadPartAltTexts(part);
});
watch(remainingSeconds, function (seconds) {
if (!isHomework.value && session.value && currentPart.value && currentPart.value.status === "running" &&
!currentPart.value.paused && seconds <= 0) submitPart(true);
});
function loadCatalog() {
Promise.all([
fetch("assets/question_pool.json"),
fetch("assets/metadata.json"),
fetch("assets/question_index.json")
])
.then(function (responses) {
var names = ["Frågebanken", "Metadata", "Frågeindex"];
responses.forEach(function (response, index) {
if (!response.ok) throw new Error(names[index] + " hittades inte (HTTP " + response.status + ").");
});
return Promise.all(responses.map(function (response) { return response.json(); }));
})
.then(function (data) {
catalog.value = window.FiftyOhmQuestions.normalizeCatalog(data[0], data[1] || {}, data[2] || {});
if (Object.keys(catalog.value).length === 0) throw new Error("Frågebanken är tom.");
var parsedResult = parseSharedResult();
var homeworkQuestionIds = parseHomeworkQuestionIds();
if (queryValue("result")) {
if (!parsedResult) throw new Error("Resultatlänken är ogiltig.");
restoreSession([], true);
var storedResult = shareableSessionResult();
if (storedResult && encodedResult(storedResult) === queryValue("result")) {
sharedResult.value = null;
view.value = "summary";
} else {
session.value = null;
sharedResult.value = parsedResult;
view.value = "shared";
}
} else {
restoreSession(homeworkQuestionIds);
if (homeworkQuestionIds.length > 0 && !session.value) startHomework(homeworkQuestionIds);
}
loading.value = false;
renderMathSoon(Vue);
})
.catch(function (error) {
loadError.value = error.message || "Okänt fel vid laddning";
loading.value = false;
});
}
function updateHeaderHeight() {
var header = document.querySelector(".fiftyohm-layout-outer-head");
if (!header) return;
document.documentElement.style.setProperty("--exam-header-height", header.offsetHeight + "px");
}
onMounted(function () {
document.body.classList.add("exam-simulator-page");
updateHeaderHeight();
if (typeof window.ResizeObserver === "function") {
headerObserver = new window.ResizeObserver(updateHeaderHeight);
headerObserver.observe(document.querySelector(".fiftyohm-layout-outer-head"));
}
if (document.readyState === "loading") {
catalogLoadHandler = function () { window.setTimeout(loadCatalog, 0); };
document.addEventListener("DOMContentLoaded", catalogLoadHandler, { once: true });
} else {
loadCatalog();
}
timerId = window.setInterval(function () { now.value = Date.now(); }, 1000);
});
onBeforeUnmount(function () {
if (timerId !== null) window.clearInterval(timerId);
if (catalogLoadHandler !== null) document.removeEventListener("DOMContentLoaded", catalogLoadHandler);
if (headerObserver !== null) headerObserver.disconnect();
document.documentElement.style.removeProperty("--exam-header-height");
document.body.classList.remove("exam-simulator-page");
});
return {
loading: loading,
loadError: loadError,
session: session,
view: view,
sharedResult: sharedResult,
shareMessage: shareMessage,
primaryChoices: primaryChoices,
upgradeChoices: upgradeChoices,
singlePartChoices: singlePartChoices,
currentChoice: currentChoice,
currentPart: currentPart,
currentPartDefinition: currentPartDefinition,
isHomework: isHomework,
currentQuestionCount: currentQuestionCount,
displayedPartIndex: displayedPartIndex,
partDefinitions: PART_DEFINITIONS,
answerLabels: ANSWER_LABELS,
remainingSeconds: remainingSeconds,
formattedTime: formattedTime,
toggleTimer: toggleTimer,
answeredCount: answeredCount,
hasNextPart: hasNextPart,
isReviewing: isReviewing,
overallAlertClass: overallAlertClass,
overallState: overallState,
overallIcon: overallIcon,
overallTitle: overallTitle,
overallText: overallText,
padNumber: padNumber,
displayQuestionNumber: displayQuestionNumber,
pictureAlt: pictureAlt,
questionData: questionData,
startExam: startExam,
selectAnswer: selectAnswer,
answerMark: answerMark,
answerCellClass: answerCellClass,
questionAnswerClass: questionAnswerClass,
requestSubmit: requestSubmit,
startNextPart: startNextPart,
finishExamEarly: finishExamEarly,
showSummary: showSummary,
reviewPart: reviewPart,
abortExam: abortExam,
newExam: newExam,
retryHomework: retryHomework,
scrollToQuestion: scrollToQuestion,
resultLabel: resultLabel,
resultAlertClass: resultAlertClass,
resultBadgeClass: resultBadgeClass,
resultIcon: resultIcon,
resultDescription: resultDescription,
resultForSharing: resultForSharing,
completedResultUrl: completedResultUrl,
resultShareUrl: resultShareUrl,
resultImageUrl: resultImageUrl,
shareText: shareText,
socialShareLinks: socialShareLinks,
copyResultLink: copyResultLink,
shareNative: shareNative
};
}
});
app.mount("#exam-simulator");
})();