(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"]; var PART_DEFINITIONS = { B: { label: "Betriebliche Kenntnisse", minutes: 45 }, V: { label: "Kenntnisse von Vorschriften", minutes: 45 }, N: { label: "Technische Kenntnisse Klasse N", minutes: 45 }, E: { label: "Technische Kenntnisse Klasse E", minutes: 45 }, A: { label: "Technische Kenntnisse Klasse A", minutes: 60 }, H: { label: "Hausaufgabe", minutes: null } }; var EXAM_CHOICES = [ { id: "N", group: "primary", label: "Klasse N", detail: "B + V + N", parts: ["B", "V", "N"], color: "#47ABE8" }, { id: "E", group: "primary", label: "Klasse E", detail: "B + V + N + E", parts: ["B", "V", "N", "E"], color: "#FE756C" }, { id: "A", group: "primary", label: "Klasse A", detail: "B + V + N + E + A", parts: ["B", "V", "N", "E", "A"], color: "#3BB583" }, { id: "N-E", group: "upgrade", label: "N → E", detail: "Technik E", parts: ["E"], color: "#FE756C" }, { id: "N-A", group: "upgrade", label: "N → A", detail: "Technik E + A", parts: ["E", "A"], color: "#3BB583" }, { id: "E-A", group: "upgrade", label: "E → A", detail: "Technik A", parts: ["A"], color: "#3BB583" }, { id: "part-B", group: "single", label: "B", detail: "Betriebliche Kenntnisse", parts: ["B"], color: "#47ABE8" }, { id: "part-V", group: "single", label: "V", detail: "Kenntnisse von Vorschriften", parts: ["V"], color: "#47ABE8" }, { id: "part-N", group: "single", label: "N", detail: "Technik Klasse N", parts: ["N"], color: "#47ABE8" }, { id: "part-E", group: "single", label: "E", detail: "Technik Klasse E", parts: ["E"], color: "#FE756C" }, { id: "part-A", group: "single", label: "A", detail: "Technik Klasse 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("Kopieren nicht möglich")); } 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("Für diesen Prüfungsteil stehen nicht genügend Fragen zur Verfügung."); } 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: "Hausaufgabe", 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: "Hausaufgabe ausgewertet", passed: "Prüfung bestanden", oral: "Mündliche Nachprüfung möglich", failed: "Prüfung nicht bestanden", incomplete: "Prüfung vorzeitig beendet" }[overallState.value]; }); var overallText = computed(function () { if (overallState.value === "homework") { var homeworkPart = session.value.parts[0]; return homeworkPart.score + " von " + homeworkPart.questions.length + " Fragen wurden richtig beantwortet."; } if (overallState.value === "passed") return "Gratulation! Du hast jeden Prüfungsteil mit mindestens 19 Punkten bestanden."; if (overallState.value === "oral") return "In genau einem Prüfungsteil wurden 17 oder 18 Punkte erreicht; nach den Prüfungsregeln kann eine mündliche Nachprüfung möglich sein."; if (overallState.value === "failed") return "Mindestens ein Prüfungsteil wurde nicht bestanden."; return evaluatedParts().length + " von " + session.value.parts.length + " Prüfungsteilen wurden abgeschlossen."; }); function questionData(questionState) { return catalog.value[questionState.id]; } function padNumber(number) { return String(number).padStart(2, "0"); } function displayQuestionNumber(questionState, questionIndex) { return isHomework.value ? questionState.id : 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("Mindestens eine Frage aus dem Hausaufgabenlink ist nicht im aktuellen Fragenkatalog enthalten."); } if (questions.length === 0) throw new Error("Der Hausaufgabenlink enthält keine gültigen Fragen."); 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 '×'; } if (question.history.indexOf(answerIndex) !== -1) { return '='; } 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 hast " + unanswered + " Fragen nicht beantwortet. Möchtest du " + (isHomework.value ? "die Hausaufgabe" : "den Prüfungsteil") + " trotzdem abgeben?" : "Möchtest du " + (isHomework.value ? "die Hausaufgabe" : "diesen Prüfungsteil") + " jetzt verbindlich abgeben?"; 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("Möchtest du die Prüfung wirklich hier beenden? Nicht bearbeitete Teile gelten dann als nicht abgeschlossen.")) 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 ? "Hausaufgabe" : "Simulation"; if (!window.confirm("Möchtest du diese " + noun + " abbrechen? Der gespeicherte Stand wird gelöscht.")) return; resetExam(); window.history.replaceState(null, "", "simulation.html"); } function newExam() { resetExam(); window.history.replaceState(null, "", "simulation.html"); } function retryHomework() { var questionIds = session.value && session.value.homeworkIds; if (!questionIds) return; clearStoredSession(); var homeworkUrl = new URL("simulation.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 "Ausgewertet"; if (part.score >= 19) return "Bestanden"; if (part.score >= 17) return "Nachprüfung möglich"; return "Nicht bestanden"; } 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 + " von " + part.questions.length + " Fragen wurden richtig beantwortet."; var prefix = part.autoSubmitted ? "Die Zeit ist abgelaufen und der Prüfungsteil wurde automatisch abgegeben. " : ""; if (part.score >= 19) return prefix + "Die erforderlichen 19 Punkte wurden erreicht."; if (part.score >= 17) return prefix + "Mit 17 oder 18 Punkten kann nach den Prüfungsregeln eine mündliche Nachprüfung möglich sein."; return prefix + "Die erforderlichen 19 Punkte wurden nicht erreicht."; } function encodedResult(result) { if (result.kind === "homework") return "1.H." + result.score + "." + result.total; return "1." + result.choice.id + "." + result.scores.join("."); } function resultImageCode(result) { if (!result) return ""; if (result.kind === "homework") return result.score === result.total ? "aenbv" : ""; if (!result.passed) return ""; var order = ["A", "E", "N", "B", "V"]; return order.filter(function (part) { return result.choice.parts.indexOf(part) !== -1; }).join("").toLowerCase(); } function shareableSessionResult() { if (!session.value) return null; if (isHomework.value) { return { kind: "homework", score: session.value.parts[0].score, total: session.value.parts[0].questions.length }; } if (session.value.parts.some(function (part) { return part.status !== "evaluated"; })) return null; return { kind: "exam", choice: currentChoice.value, scores: session.value.parts.map(function (part) { return part.score; }), passed: overallState.value === "passed" }; } 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 imageCode = resultImageCode(result); var url = new URL(imageCode ? "result-" + imageCode + ".html" : "result.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); }); var resultImageUrl = computed(function () { var imageCode = resultImageCode(resultForSharing.value); return imageCode ? "assets/images/exam-" + imageCode + ".jpeg" : ""; }); var shareText = computed(function () { var result = resultForSharing.value; if (!result) return ""; if (result.kind === "homework") { return "Meine 50ohm.de-Hausaufgabe: " + result.score + " von " + result.total + " Fragen richtig."; } var scores = result.choice.parts.map(function (part, index) { return part + ": " + result.scores[index] + "/25"; }).join(", "); return "Meine 50ohm.de-Prüfungssimulation " + result.choice.label + ": " + scores + " – " + (result.passed ? "bestanden!" : "noch nicht bestanden."); }); 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 = "Ergebnislink kopiert."; }) .catch(function () { shareMessage.value = "Der Link konnte nicht automatisch kopiert werden."; }); } function shareNative() { if (!navigator.share) { copyResultLink(); return; } navigator.share({ title: "50ohm.de-Ergebnis", 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/fragenkatalog.json"), fetch("assets/metadata.json"), fetch("assets/question_index.json") ]) .then(function (responses) { var names = ["Fragenkatalog", "Metadaten", "Fragenindex"]; responses.forEach(function (response, index) { if (!response.ok) throw new Error(names[index] + " nicht gefunden (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("Der Fragenkatalog ist leer."); var parsedResult = parseSharedResult(); var homeworkQuestionIds = parseHomeworkQuestionIds(); if (queryValue("result")) { if (!parsedResult) throw new Error("Der Ergebnislink ist ungültig."); 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 || "Unbekannter Ladefehler"; 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"); })();