/* 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);
});