/* Moteur d'exercices hors ligne. Aucune dependance externe. Types supportes : qcm, vf, assoc, tri, ordre, hotspot, calcul, texte, intrus. */ var TOTAL = 0, REUSSIS = 0, TENTES = 0; function norm(s){ return (s || "").toString().toLowerCase() .normalize("NFD").replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]/g, ""); } function el(tag, cls, txt){ var e = document.createElement(tag); if (cls) e.className = cls; if (txt !== undefined) e.textContent = txt; return e; } function melange(t){ var a = t.slice(); for (var i = a.length - 1; i > 0; i--){ var j = Math.floor(Math.random() * (i + 1)); var x = a[i]; a[i] = a[j]; a[j] = x; } return a; } function compter(ok){ TENTES++; if (ok) REUSSIS++; majScore(); } function majScore(){ var s = document.getElementById("score"); if (!s) return; var pct = TOTAL ? Math.round(REUSSIS / TOTAL * 100) : 0; s.querySelector(".txt").innerHTML = "Exercices reussis : " + REUSSIS + " / " + TOTAL + " (" + pct + " %)"; s.querySelector(".barre span").style.width = pct + "%"; } function retour(box, ok, msg){ var r = box.querySelector(".retour"); r.className = "retour " + (ok ? "ok" : "ko"); r.textContent = (ok ? "Correct. " : "Ce n'est pas cela. ") + (msg || ""); } function cadre(d){ var box = el("article", "ex"); var c = el("p", "consigne", d.consigne || "Exercice"); box.appendChild(c); if (d.enonce) box.appendChild(el("p", "enonce", d.enonce)); return box; } function finir(box){ box.appendChild(el("div", "retour")); return box; } /* ---------- QCM ---------- */ function faireQcm(d){ var box = cadre(d), fini = false; d.options.forEach(function(txt, i){ var b = el("button", "opt", txt); b.onclick = function(){ if (fini) return; fini = true; var ok = (i === d.reponse); b.classList.add(ok ? "juste" : "faux"); if (!ok) box.querySelectorAll(".opt")[d.reponse].classList.add("juste"); box.querySelectorAll(".opt").forEach(function(o){ o.disabled = true; }); retour(box, ok, d.explication || ""); compter(ok); }; box.appendChild(b); }); return finir(box); } /* ---------- Vrai / Faux ---------- */ function faireVf(d){ var box = cadre(d), fini = false; ["Vrai", "Faux"].forEach(function(txt, i){ var b = el("button", "jeton", txt); b.onclick = function(){ if (fini) return; fini = true; var ok = ((i === 0) === d.reponse); b.classList.add(ok ? "juste" : "faux"); box.querySelectorAll(".jeton").forEach(function(o){ o.disabled = true; }); retour(box, ok, d.explication || ""); compter(ok); }; box.appendChild(b); }); return finir(box); } /* ---------- Association (clic gauche puis clic droit) ---------- */ function faireAssoc(d){ var box = cadre(d); var zone = el("div", "deux-col"); var g = el("div"), dr = el("div"); g.appendChild(el("h4", null, "Colonne A")); dr.appendChild(el("h4", null, "Colonne B")); var choisi = null, trouves = 0; var droite = melange(d.paires.map(function(p, i){ return { t: p[1], i: i }; })); d.paires.forEach(function(p, i){ var b = el("button", "jeton", p[0]); b.style.display = "block"; b.dataset.i = i; b.onclick = function(){ if (b.disabled) return; if (choisi) choisi.classList.remove("choisi"); choisi = b; b.classList.add("choisi"); }; g.appendChild(b); }); droite.forEach(function(o){ var b = el("button", "jeton", o.t); b.style.display = "block"; b.onclick = function(){ if (!choisi || b.disabled) return; var ok = (parseInt(choisi.dataset.i, 10) === o.i); if (ok){ choisi.classList.remove("choisi"); choisi.classList.add("juste"); b.classList.add("juste"); choisi.disabled = true; b.disabled = true; choisi = null; trouves++; if (trouves === d.paires.length){ retour(box, true, d.explication || "Toutes les paires sont correctes."); compter(true); } } else { b.classList.add("faux"); setTimeout(function(){ b.classList.remove("faux"); }, 600); retour(box, false, "Essaie une autre correspondance."); } }; dr.appendChild(b); }); zone.appendChild(g); zone.appendChild(dr); box.appendChild(zone); return finir(box); } /* ---------- Tri dans des paniers ---------- */ function faireTri(d){ var box = cadre(d); var reserve = el("div", "reserve"); var choisi = null, places = 0; melange(d.items).forEach(function(it){ var b = el("button", "item", it[0]); b.dataset.p = it[1]; b.onclick = function(){ if (b.disabled) return; if (choisi) choisi.classList.remove("choisi"); choisi = b; b.classList.add("choisi"); }; reserve.appendChild(b); }); box.appendChild(reserve); var paniers = el("div", "paniers"); d.paniers.forEach(function(nom, pi){ var p = el("div", "panier"); p.appendChild(el("h4", null, nom)); p.onclick = function(){ if (!choisi) return; var ok = (parseInt(choisi.dataset.p, 10) === pi); if (ok){ choisi.classList.remove("choisi"); choisi.classList.add("juste"); choisi.disabled = true; p.appendChild(choisi); choisi = null; places++; if (places === d.items.length){ retour(box, true, d.explication || "Tout est bien classe."); compter(true); } } else { p.classList.add("vise"); setTimeout(function(){ p.classList.remove("vise"); }, 500); retour(box, false, "Ce n'est pas le bon panier."); } }; paniers.appendChild(p); }); box.appendChild(paniers); return finir(box); } /* ---------- Remise en ordre ---------- */ function faireOrdre(d){ var box = cadre(d); var liste = el("div"); var ordre = melange(d.etapes.map(function(t, i){ return { t: t, i: i }; })); function dessiner(){ liste.innerHTML = ""; ordre.forEach(function(o, pos){ var ligne = el("div", "item"); ligne.style.display = "block"; ligne.textContent = (pos + 1) + ". " + o.t; var up = el("button", "jeton", "monter"); var dn = el("button", "jeton", "descendre"); up.onclick = function(){ if (pos === 0) return; var x = ordre[pos - 1]; ordre[pos - 1] = ordre[pos]; ordre[pos] = x; dessiner(); }; dn.onclick = function(){ if (pos === ordre.length - 1) return; var x = ordre[pos + 1]; ordre[pos + 1] = ordre[pos]; ordre[pos] = x; dessiner(); }; ligne.appendChild(document.createTextNode(" ")); ligne.appendChild(up); ligne.appendChild(dn); liste.appendChild(ligne); }); } dessiner(); box.appendChild(liste); var v = el("button", "action", "Verifier l'ordre"); var fini = false; v.onclick = function(){ if (fini) return; var ok = ordre.every(function(o, pos){ return o.i === pos; }); if (ok){ fini = true; v.disabled = true; liste.querySelectorAll(".item").forEach(function(i){ i.classList.add("juste"); }); retour(box, true, d.explication || ""); compter(true); } else { retour(box, false, "L'ordre n'est pas encore correct, recommence."); } }; box.appendChild(v); return finir(box); } /* ---------- Zones cliquables sur schema ---------- */ function faireHotspot(d){ var box = cadre(d); var wrap = el("div"); wrap.innerHTML = d.svg; box.appendChild(wrap); var question = el("p", "enonce"); box.appendChild(question); var restants = melange(d.zones.slice()); var trouves = 0; function demander(){ if (!restants.length){ retour(box, true, d.explication || "Toutes les zones ont ete trouvees."); compter(true); question.textContent = "Termine."; return; } question.innerHTML = "Clique sur : " + restants[0] + ""; } wrap.querySelectorAll(".zone").forEach(function(z){ z.addEventListener("click", function(){ if (!restants.length) return; if (z.dataset.zone === restants[0]){ z.classList.add("juste"); restants.shift(); trouves++; demander(); } else { z.classList.add("faux"); setTimeout(function(){ z.classList.remove("faux"); }, 500); retour(box, false, "Ce n'est pas cet element."); } }); }); demander(); return finir(box); } /* ---------- Calcul ou reponse courte ---------- */ function faireSaisie(d){ var box = cadre(d); var champ = el("input", "court"); champ.type = "text"; var v = el("button", "action", "Verifier"); var fini = false; v.onclick = function(){ if (fini) return; var ok; if (d.type === "calcul"){ var n = parseFloat(champ.value.replace(",", ".").replace(/[^0-9.\-]/g, "")); ok = !isNaN(n) && Math.abs(n - d.reponse) <= (d.tolerance || 0); } else { ok = d.acceptees.some(function(a){ return norm(a) === norm(champ.value); }); } if (ok){ fini = true; v.disabled = true; champ.disabled = true; retour(box, true, d.explication || ""); compter(true); } else { retour(box, false, d.indice || "Relis la theorie du chapitre et reessaie."); } }; box.appendChild(champ); box.appendChild(document.createTextNode(" ")); box.appendChild(v); return finir(box); } /* ---------- Chasse a l'intrus (plusieurs affirmations fausses) ---------- */ function faireIntrus(d){ var box = cadre(d); var restants = d.items.filter(function(i){ return i[1]; }).length; var erreurs = 0; d.items.forEach(function(it){ var b = el("button", "opt", it[0]); b.onclick = function(){ if (b.disabled) return; b.disabled = true; if (it[1]){ b.classList.add("juste"); restants--; if (restants === 0){ retour(box, erreurs === 0, d.explication || ""); compter(erreurs === 0); } } else { b.classList.add("faux"); erreurs++; retour(box, false, "Cette affirmation est correcte, ce n'est pas un intrus."); } }; box.appendChild(b); }); return finir(box); } /* ---------- Serie chronometree ---------- */ function faireChrono(d){ var box = cadre(d); var minuteur = el("span", "chrono", ""); box.querySelector(".consigne").appendChild(minuteur); var zone = el("div"); box.appendChild(zone); var idx = 0, bons = 0, temps = d.secondes || 60, timer = null, encours = false; var demarrer = el("button", "action", "Demarrer la serie"); box.appendChild(demarrer); demarrer.onclick = function(){ if (encours) return; encours = true; demarrer.style.display = "none"; idx = 0; bons = 0; temps = d.secondes || 60; minuteur.textContent = temps + " s"; timer = setInterval(function(){ temps--; minuteur.textContent = temps + " s"; if (temps <= 0) terminer(); }, 1000); poser(); }; function poser(){ zone.innerHTML = ""; if (idx >= d.questions.length){ terminer(); return; } var q = d.questions[idx]; zone.appendChild(el("p", "enonce", (idx + 1) + ". " + q.enonce)); q.options.forEach(function(t, i){ var b = el("button", "jeton", t); b.onclick = function(){ if (i === q.reponse) bons++; idx++; poser(); }; zone.appendChild(b); }); } function terminer(){ clearInterval(timer); encours = false; zone.innerHTML = ""; var seuil = Math.ceil(d.questions.length * 0.7); var ok = bons >= seuil; retour(box, ok, "Score : " + bons + " / " + d.questions.length + ". Seuil de reussite : " + seuil + "."); compter(ok); demarrer.textContent = "Recommencer la serie"; demarrer.style.display = "inline-block"; } return finir(box); } /* ---------- Construction de la page ---------- */ var FABRIQUES = { qcm: faireQcm, vf: faireVf, assoc: faireAssoc, tri: faireTri, ordre: faireOrdre, hotspot: faireHotspot, calcul: faireSaisie, texte: faireSaisie, intrus: faireIntrus, chrono: faireChrono }; function construire(data){ var cible = document.getElementById("exercices"); TOTAL = data.length; REUSSIS = 0; TENTES = 0; var s = el("div", "score"); s.id = "score"; var t = el("div", "txt"); s.appendChild(t); var b = el("div", "barre"); b.appendChild(el("span")); s.appendChild(b); var rz = el("button", "action secondaire", "Tout recommencer"); rz.onclick = function(){ location.reload(); }; s.appendChild(rz); cible.parentNode.insertBefore(s, cible); data.forEach(function(d){ var f = FABRIQUES[d.type]; if (f) cible.appendChild(f(d)); }); majScore(); } window.addEventListener("DOMContentLoaded", function(){ if (window.DATA) construire(window.DATA); });