Skip to content

Commit

Permalink
v1.2.6
Browse files Browse the repository at this point in the history
  • Loading branch information
andreas-schwenk committed Sep 24, 2024
1 parent 31a88d8 commit 6df627b
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 73 deletions.
16 changes: 10 additions & 6 deletions web/src/eval.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,23 @@ export function evalQuestion(question) {
let text = choices[Math.floor(Math.random() * choices.length)];
question.feedbackPopupDiv.innerHTML = text;
question.feedbackPopupDiv.style.color =
question.state === QuestionState.passed ? "green" : "maroon";
question.feedbackPopupDiv.style.display = "block";
question.state === QuestionState.passed ? "var(--green)" : "var(--red)";
question.feedbackPopupDiv.style.display = "flex";
setTimeout(() => {
question.feedbackPopupDiv.style.display = "none";
}, 500);
}, 1000);
// change the question button
question.editingEnabled = true;
if (question.state === QuestionState.passed) {
if (question.src.instances.length > 0) {
question.editingEnabled = false;
if (question.src.instances.length > 1) {
// if the student passed and there are other question instances,
// provide the ability to repeat the question
question.checkAndRepeatBtn.innerHTML = iconRepeat;
} else question.checkAndRepeatBtn.style.display = "none";
} else {
} else {
question.checkAndRepeatBtn.style.visibility = "hidden";
}
} else if (question.checkAndRepeatBtn != null) {
// in case of non-passing, the check button must be provided (kept)
question.checkAndRepeatBtn.innerHTML = iconCheck;
}
Expand Down
4 changes: 2 additions & 2 deletions web/src/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const iconCircleChecked =

// icon at the bottom of a question to check the question
export const iconCheck =
'<svg xmlns="http://www.w3.org/2000/svg" height="25" viewBox="0 0 384 512" fill="white"><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>';
'<svg xmlns="http://www.w3.org/2000/svg" width="50" height="25" viewBox="0 0 384 512" fill="white"><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>';

// icon at the bottom of a question to repeat the question
export const iconRepeat = `<svg xmlns="http://www.w3.org/2000/svg" height="25" viewBox="0 0 512 512" fill="white"><path d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z"/></svg>`;
export const iconRepeat = `<svg xmlns="http://www.w3.org/2000/svg" width="50" height="25" viewBox="0 0 512 512" fill="white"><path d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z"/></svg>`;
212 changes: 174 additions & 38 deletions web/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@
******************************************************************************/

import { genDiv } from "./dom.js";
import { courseInfo1, courseInfo2, courseInfo3 } from "./lang.js";
import { Question } from "./question.js";
import { evalQuestion } from "./eval.js";
import {
courseInfo1,
courseInfo2,
courseInfo3,
dataPolicy,
evalNowText,
pointsText,
timerInfoText,
} from "./lang.js";
import { Question, QuestionState } from "./question.js";

/**
* This file is the entry point of the quiz website and populates the DOM.
Expand All @@ -17,42 +26,169 @@ import { Question } from "./question.js";
* @param {boolean} debug -- true for enabling extensive debugging features
*/
export function init(quizSrc, debug) {
// default to English, if the provided language abbreviation is unknown
if (["en", "de", "es", "it", "fr"].includes(quizSrc.lang) == false)
quizSrc.lang = "en";
// if debugging is enabled, show a DEBUG info at the start of the page
if (debug) document.getElementById("debug").style.display = "block";
// show the quiz' meta data
document.getElementById("date").innerHTML = quizSrc.date;
document.getElementById("title").innerHTML = quizSrc.title;
document.getElementById("author").innerHTML = quizSrc.author;
document.getElementById("courseInfo1").innerHTML = courseInfo1[quizSrc.lang];
let reload =
'<span onclick="location.reload()" style="text-decoration: underline; font-weight: bold; cursor: pointer">' +
courseInfo3[quizSrc.lang] +
"</span>";
document.getElementById("courseInfo2").innerHTML = courseInfo2[
quizSrc.lang
].replace("*", reload);

// generate questions
/** @type {Question[]} */
let questions = [];
/** @type {HTMLElement} */
let questionsDiv = document.getElementById("questions");
let idx = 1; // question index 1, 2, ...
for (let questionSrc of quizSrc.questions) {
questionSrc.title = "" + idx + ". " + questionSrc.title;
let div = genDiv();
questionsDiv.appendChild(div);
let question = new Question(div, questionSrc, quizSrc.lang, debug);
question.showSolution = debug;
questions.push(question);
question.populateDom();
if (debug && questionSrc.error.length == 0) {
// if the debug version is active, evaluate the question immediately
if (question.hasCheckButton) question.checkAndRepeatBtn.click();
new Quiz(quizSrc, debug);
}

/**
* Question management.
*/
export class Quiz {
/**
*
* @param {Object.<Object,Object>} quizSrc -- JSON object as output from sell.py
* @param {boolean} debug -- true for enabling extensive debugging features
*/
constructor(quizSrc, debug) {
/** @type {Object.<Object,Object>} */
this.quizSrc = quizSrc;

// default to English, if the provided language abbreviation is unknown
if (["en", "de", "es", "it", "fr"].includes(this.quizSrc.lang) == false)
this.quizSrc.lang = "en";

/** @type {boolean} */
this.debug = debug;

// if debugging is enabled, show a DEBUG info at the start of the page
if (this.debug) document.getElementById("debug").style.display = "block";

/** @type {Question[]} -- the questions */
this.questions = [];

/** @type {number} -- positive value := limited time */
this.timeLeft = quizSrc.timer;
/** @type {boolean} -- whether the quiz is time limited */
this.timeLimited = quizSrc.timer > 0;

this.fillPageMetadata();

if (this.timeLimited) {
document.getElementById("timer-info").style.display = "block";
document.getElementById("timer-info-text").innerHTML =
timerInfoText[this.quizSrc.lang];
document.getElementById("start-btn").addEventListener("click", () => {
document.getElementById("timer-info").style.display = "none";
this.generateQuestions();
this.runTimer();
});
} else {
this.generateQuestions();
}
}

/**
* Shows the quiz' meta data.
*/
fillPageMetadata() {
document.getElementById("date").innerHTML = this.quizSrc.date;
document.getElementById("title").innerHTML = this.quizSrc.title;
document.getElementById("author").innerHTML = this.quizSrc.author;
document.getElementById("courseInfo1").innerHTML =
courseInfo1[this.quizSrc.lang];
let reload =
'<span onclick="location.reload()" style="text-decoration: none; font-weight: bold; cursor: pointer">' +
courseInfo3[this.quizSrc.lang] +
"</span>";
document.getElementById("courseInfo2").innerHTML = courseInfo2[
this.quizSrc.lang
].replace("*", reload);

document.getElementById("data-policy").innerHTML =
dataPolicy[this.quizSrc.lang];
}

/**
* Generates the questions.
*/
generateQuestions() {
/** @type {HTMLElement} */
let questionsDiv = document.getElementById("questions");
let idx = 1; // question index 1, 2, ...
for (let questionSrc of this.quizSrc.questions) {
questionSrc.title = "" + idx + ". " + questionSrc.title;
let div = genDiv();
questionsDiv.appendChild(div);
let question = new Question(
div,
questionSrc,
this.quizSrc.lang,
this.debug
);
question.showSolution = this.debug;
this.questions.push(question);
question.populateDom(this.timeLimited);
if (this.debug && questionSrc.error.length == 0) {
// if the debug version is active, evaluate the question immediately
if (question.hasCheckButton) question.checkAndRepeatBtn.click();
}
idx++;
}
idx++;
}

/**
* Runs the timer countdown.
*/
runTimer() {
// button to evaluate quiz immediately
document.getElementById("stop-now").style.display = "block";
document.getElementById("stop-now-btn").innerHTML =
evalNowText[this.quizSrc.lang];
document.getElementById("stop-now-btn").addEventListener("click", () => {
this.timeLeft = 1;
});
// create and show timer
let timerDiv = document.getElementById("timer");
timerDiv.style.display = "block";
timerDiv.innerHTML = formatTime(this.timeLeft);
// tick every second
let interval = setInterval(() => {
this.timeLeft--;
timerDiv.innerHTML = formatTime(this.timeLeft);
// stop, if no time is left
if (this.timeLeft <= 0) {
this.stopTimer(interval);
}
}, 1000);
}

stopTimer(interval) {
document.getElementById("stop-now").style.display = "none";
clearInterval(interval);
let score = 0;
let maxScore = 0;
for (let question of this.questions) {
let pts = question.src["points"];
maxScore += pts;
evalQuestion(question);
if (question.state === QuestionState.passed) score += pts;
question.editingEnabled = false;
}
document.getElementById("questions-eval").style.display = "block";
//document.getElementById("questions-eval-text").innerHTML =
// evalText[quizSrc.lang] + ":";
let p = document.getElementById("questions-eval-percentage");
p.innerHTML =
maxScore == 0
? ""
: "" +
score +
" / " +
maxScore +
" " +
pointsText[this.quizSrc.lang] +
" " +
"<br/><br/>" +
Math.round((score / maxScore) * 100) +
" %";
}
}

/**
* @param {number} seconds
* @returns {string}
*/
function formatTime(seconds) {
let mins = Math.floor(seconds / 60);
let secs = seconds % 60;
return mins + ":" + ("" + secs).padStart(2, "0");
}
12 changes: 12 additions & 0 deletions web/src/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class GapInput {
let input = genInputField(width);
question.gapInputs[this.inputId] = input;
input.addEventListener("keyup", () => {
if (question.editingEnabled == false) return;
this.question.editedQuestion();
input.value = input.value.toUpperCase();
this.question.student[this.inputId] = input.value.trim();
Expand Down Expand Up @@ -120,21 +121,32 @@ export class TermInput {
this.outerSpan.appendChild(this.equationPreviewDiv);
// events
this.inputElement.addEventListener("click", () => {
if (question.editingEnabled == false) return;
// mark the question as altered
this.question.editedQuestion();
this.edited();
});
this.inputElement.addEventListener("keyup", () => {
if (question.editingEnabled == false) return;
// mark the question as altered
this.question.editedQuestion();
this.edited();
});

this.inputElement.addEventListener("focus", () => {
if (question.editingEnabled == false) return;
});

this.inputElement.addEventListener("focusout", () => {
// hide the TeX preview in case that the focus to the input was lost
this.equationPreviewDiv.innerHTML = "";
this.equationPreviewDiv.style.display = "none";
});
this.inputElement.addEventListener("keydown", (e) => {
if (question.editingEnabled == false) {
e.preventDefault();
return;
}
// forbid special characters
let allowed = "abcdefghijklmnopqrstuvwxyz";
allowed += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Expand Down
40 changes: 36 additions & 4 deletions web/src/lang.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@
*/

export let courseInfo1 = {
en: "This page runs in your browser and does not store any data on servers.",
en: "This page operates entirely in your browser and does not store any data on external servers.",
de: "Diese Seite wird in Ihrem Browser ausgeführt und speichert keine Daten auf Servern.",
es: "Esta página se ejecuta en su navegador y no almacena ningún dato en los servidores.",
it: "Questa pagina viene eseguita nel browser e non memorizza alcun dato sui server.",
fr: "Cette page fonctionne dans votre navigateur et ne stocke aucune donnée sur des serveurs.",
};

export let courseInfo2 = {
en: "You can * this page in order to get new randomized tasks.",
en: "* this page to receive a new set of randomized tasks.",
de: "Sie können diese Seite *, um neue randomisierte Aufgaben zu erhalten.",
es: "Puedes * esta página para obtener nuevas tareas aleatorias.",
it: "È possibile * questa pagina per ottenere nuovi compiti randomizzati",
fr: "Vous pouvez * cette page pour obtenir de nouvelles tâches aléatoires",
};

export let courseInfo3 = {
en: "reload",
en: "Refresh",
de: "aktualisieren",
es: "recargar",
it: "ricaricare",
Expand All @@ -41,7 +41,7 @@ export let feedbackOK = {
};

export let feedbackIncomplete = {
en: ["fill all fields"],
en: ["please complete all fields"],
de: ["bitte alles ausfüllen"],
es: ["por favor, rellene todo"],
it: ["compilare tutto"],
Expand All @@ -55,3 +55,35 @@ export let feedbackErr = {
it: ["riprova", "ancora qualche errore", "risposta sbagliata"],
fr: ["réessayer", "encore des erreurs", "mauvaise réponse"],
};

export let pointsText = {
en: "point(s)",
de: "Punkt(e)",
es: "punto(s)",
it: "punto/i",
fr: "point(s)",
};

export let evalNowText = {
en: "Evaluate now",
de: "Jetzt auswerten",
es: "Evaluar ahora",
it: "Valuta ora",
fr: "Évaluer maintenant",
};

export let dataPolicy = {
en: "Data Policy: This website does not collect, store, or process any personal data on external servers. All functionality is executed locally in your browser, ensuring complete privacy. No cookies are used, and no data is transmitted to or from the server. Your activity on this site remains entirely private and local to your device.",
de: "Datenschutzrichtlinie: Diese Website sammelt, speichert oder verarbeitet keine personenbezogenen Daten auf externen Servern. Alle Funktionen werden lokal in Ihrem Browser ausgeführt, um vollständige Privatsphäre zu gewährleisten. Es werden keine Cookies verwendet, und es werden keine Daten an den Server gesendet oder von diesem empfangen. Ihre Aktivität auf dieser Seite bleibt vollständig privat und lokal auf Ihrem Gerät.",
es: "Política de datos: Este sitio web no recopila, almacena ni procesa ningún dato personal en servidores externos. Toda la funcionalidad se ejecuta localmente en su navegador, garantizando una privacidad completa. No se utilizan cookies y no se transmiten datos hacia o desde el servidor. Su actividad en este sitio permanece completamente privada y local en su dispositivo.",
it: "Politica sui dati: Questo sito web non raccoglie, memorizza o elabora alcun dato personale su server esterni. Tutte le funzionalità vengono eseguite localmente nel tuo browser, garantendo una privacy completa. Non vengono utilizzati cookie e nessun dato viene trasmesso da o verso il server. La tua attività su questo sito rimane completamente privata e locale sul tuo dispositivo.",
fr: "Politique de confidentialité: Ce site web ne collecte, ne stocke ni ne traite aucune donnée personnelle sur des serveurs externes. Toutes les fonctionnalités sont exécutées localement dans votre navigateur, garantissant une confidentialité totale. Aucun cookie n’est utilisé et aucune donnée n’est transmise vers ou depuis le serveur. Votre activité sur ce site reste entièrement privée et locale sur votre appareil.",
};

export let timerInfoText = {
en: "You have a limited time to complete this quiz. The countdown, displayed in minutes, is visible at the top-left of the screen. When you're ready to begin, simply press the Start button.",
de: "Die Zeit für dieses Quiz ist begrenzt. Der Countdown, in Minuten angezeigt, läuft oben links auf dem Bildschirm. Mit dem Start-Button beginnt das Quiz.",
es: "Tienes un tiempo limitado para completar este cuestionario. La cuenta regresiva, mostrada en minutos, se encuentra en la parte superior izquierda de la pantalla. Cuando estés listo, simplemente presiona el botón de inicio.",
it: "Hai un tempo limitato per completare questo quiz. Il conto alla rovescia, visualizzato in minuti, è visibile in alto a sinistra dello schermo. Quando sei pronto, premi semplicemente il pulsante Start.",
fr: "Vous disposez d’un temps limité pour compléter ce quiz. Le compte à rebours, affiché en minutes, est visible en haut à gauche de l’écran. Lorsque vous êtes prêt, appuyez simplement sur le bouton Démarrer.",
};
3 changes: 1 addition & 2 deletions web/src/math.js
Original file line number Diff line number Diff line change
Expand Up @@ -1125,8 +1125,7 @@ export class TermNode {
case "sinc":
case "sinh":
case "tan":
case "tanh":
case "ln": {
case "tanh": {
let u = this.c[0].toTexString(true); // operand w/o parentheses!
s += "\\" + this.op + "\\left(" + u + "\\right)";
break;
Expand Down
Loading

0 comments on commit 6df627b

Please sign in to comment.