mise à niveau de tout le site
This commit is contained in:
@@ -0,0 +1,557 @@
|
||||
/* ==================================================================
|
||||
Moteur d'exercices hors ligne — 2e commune
|
||||
------------------------------------------------------------------
|
||||
Aucune dépendance externe. Chaque page de bloc déclare ses exercices
|
||||
dans window.DATA, un tableau d'objets. Types reconnus :
|
||||
qcm, vf, assoc, tri, ordre, hotspot, calcul, texte, intrus, chrono
|
||||
|
||||
Règle de comptage, la même pour tous les types :
|
||||
un exercice compte pour un point, une seule fois, au moment où il
|
||||
est terminé, et il est réussi s'il a été terminé sans erreur.
|
||||
Recommencer une série chronométrée ne réajoute donc pas de point.
|
||||
================================================================== */
|
||||
|
||||
"use strict";
|
||||
|
||||
var TOTAL = 0, REUSSIS = 0, TERMINES = 0;
|
||||
|
||||
/* ---------- petits utilitaires ---------- */
|
||||
|
||||
function norm(s){
|
||||
return (s || "").toString().toLowerCase()
|
||||
.normalize("NFD").replace(/[̀-ͯ]/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;
|
||||
}
|
||||
|
||||
/* ---------- score ---------- */
|
||||
|
||||
function majScore(){
|
||||
var s = document.getElementById("score");
|
||||
if (!s) return;
|
||||
var pct = TOTAL ? Math.round(REUSSIS / TOTAL * 100) : 0;
|
||||
s.querySelector(".score__txt").innerHTML =
|
||||
"Exercices réussis : <strong>" + REUSSIS + " / " + TOTAL + "</strong> (" + pct + " %)" +
|
||||
(TERMINES ? " — " + TERMINES + " terminé" + (TERMINES > 1 ? "s" : "") : "");
|
||||
s.querySelector(".jauge span").style.width = pct + "%";
|
||||
}
|
||||
|
||||
/* Clôture un exercice : affiche le verdict et le compte, une seule fois.
|
||||
Un exercice déjà clos peut encore être rejoué (série chronométrée),
|
||||
mais il ne change plus le score. */
|
||||
function clore(box, reussi, msg){
|
||||
var r = box.querySelector(".retour");
|
||||
r.className = "retour " + (reussi ? "retour--ok" : "retour--ko");
|
||||
r.textContent = (reussi ? "Réussi. " : "Terminé, mais avec des erreurs. ") + (msg || "");
|
||||
if (box.dataset.compte === "1") return;
|
||||
box.dataset.compte = "1";
|
||||
TERMINES++;
|
||||
if (reussi) REUSSIS++;
|
||||
majScore();
|
||||
}
|
||||
|
||||
/* Message intermédiaire : ne clôt pas l'exercice, ne compte pas. */
|
||||
function signaler(box, ok, msg){
|
||||
var r = box.querySelector(".retour");
|
||||
r.className = "retour " + (ok ? "retour--ok" : "retour--ko");
|
||||
r.textContent = msg;
|
||||
}
|
||||
|
||||
/* ---------- cadre commun ---------- */
|
||||
|
||||
function cadre(d){
|
||||
var box = el("article", "ex");
|
||||
box.appendChild(el("p", "consigne", d.consigne || "Exercice"));
|
||||
if (d.enonce) box.appendChild(el("p", "enonce", d.enonce));
|
||||
return box;
|
||||
}
|
||||
|
||||
function finir(box){
|
||||
/* role="status" : le verdict est annoncé aux lecteurs d'écran
|
||||
sans déplacer le focus de l'élève. */
|
||||
var r = el("div", "retour");
|
||||
r.setAttribute("role", "status");
|
||||
r.setAttribute("aria-live", "polite");
|
||||
box.appendChild(r);
|
||||
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.type = "button";
|
||||
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; });
|
||||
clore(box, ok, d.explication || "");
|
||||
};
|
||||
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.type = "button";
|
||||
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; });
|
||||
clore(box, ok, d.explication || "");
|
||||
};
|
||||
box.appendChild(b);
|
||||
});
|
||||
return finir(box);
|
||||
}
|
||||
|
||||
/* ---------- Association : un clic à gauche, un clic à droite ---------- */
|
||||
|
||||
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, erreurs = 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 jeton--bloc", p[0]);
|
||||
b.type = "button";
|
||||
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 jeton--bloc", o.t);
|
||||
b.type = "button";
|
||||
b.onclick = function(){
|
||||
if (b.disabled) return;
|
||||
if (!choisi){
|
||||
signaler(box, false, "Choisis d'abord une étiquette dans la colonne A.");
|
||||
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){
|
||||
clore(box, erreurs === 0, (d.explication || "") +
|
||||
(erreurs ? " Erreurs : " + erreurs + "." : ""));
|
||||
} else {
|
||||
signaler(box, true, "Bonne association. Il en reste " +
|
||||
(d.paires.length - trouves) + ".");
|
||||
}
|
||||
} else {
|
||||
erreurs++;
|
||||
b.classList.add("faux");
|
||||
setTimeout(function(){ b.classList.remove("faux"); }, 600);
|
||||
signaler(box, false, "Ce n'est pas la bonne correspondance, essaie autre chose.");
|
||||
}
|
||||
};
|
||||
dr.appendChild(b);
|
||||
});
|
||||
|
||||
zone.appendChild(g); zone.appendChild(dr);
|
||||
box.appendChild(zone);
|
||||
return finir(box);
|
||||
}
|
||||
|
||||
/* ---------- Tri dans des paniers ----------
|
||||
Les paniers sont des div : un bouton ne peut pas contenir les
|
||||
étiquettes déposées, qui sont elles-mêmes des boutons. On leur
|
||||
donne donc explicitement le rôle, la tabulation et les touches
|
||||
Entrée et Espace, pour qu'ils soient utilisables au clavier. */
|
||||
|
||||
function faireTri(d){
|
||||
var box = cadre(d);
|
||||
var reserve = el("div", "reserve");
|
||||
var choisi = null, places = 0, erreurs = 0;
|
||||
|
||||
melange(d.items).forEach(function(it){
|
||||
var b = el("button", "item", it[0]);
|
||||
b.type = "button";
|
||||
b.dataset.p = it[1];
|
||||
b.onclick = function(){
|
||||
if (b.disabled) return;
|
||||
if (choisi) choisi.classList.remove("choisi");
|
||||
choisi = b; b.classList.add("choisi");
|
||||
signaler(box, true, "« " + it[0] + " » est sélectionné. Choisis maintenant son panier.");
|
||||
};
|
||||
reserve.appendChild(b);
|
||||
});
|
||||
box.appendChild(reserve);
|
||||
|
||||
var paniers = el("div", "paniers");
|
||||
d.paniers.forEach(function(nom, pi){
|
||||
var p = el("div", "panier");
|
||||
p.tabIndex = 0;
|
||||
p.setAttribute("role", "button");
|
||||
p.setAttribute("aria-label", "Panier " + nom);
|
||||
p.appendChild(el("h4", null, nom));
|
||||
|
||||
function deposer(){
|
||||
if (!choisi){
|
||||
signaler(box, false, "Choisis d'abord une étiquette dans la réserve.");
|
||||
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){
|
||||
clore(box, erreurs === 0, (d.explication || "") +
|
||||
(erreurs ? " Erreurs : " + erreurs + "." : ""));
|
||||
} else {
|
||||
signaler(box, true, "Bien classé. Il en reste " + (d.items.length - places) + ".");
|
||||
}
|
||||
} else {
|
||||
erreurs++;
|
||||
p.classList.add("vise");
|
||||
setTimeout(function(){ p.classList.remove("vise"); }, 500);
|
||||
signaler(box, false, "Ce n'est pas le bon panier.");
|
||||
}
|
||||
}
|
||||
|
||||
p.onclick = deposer;
|
||||
p.onkeydown = function(e){
|
||||
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar"){
|
||||
e.preventDefault();
|
||||
deposer();
|
||||
}
|
||||
};
|
||||
paniers.appendChild(p);
|
||||
});
|
||||
box.appendChild(paniers);
|
||||
return finir(box);
|
||||
}
|
||||
|
||||
/* ---------- Remise en ordre ---------- */
|
||||
|
||||
function faireOrdre(d){
|
||||
var box = cadre(d);
|
||||
var liste = el("div", "liste-ordre");
|
||||
var ordre = melange(d.etapes.map(function(t, i){ return { t: t, i: i }; }));
|
||||
var fini = false, essais = 0;
|
||||
|
||||
function dessiner(){
|
||||
liste.innerHTML = "";
|
||||
ordre.forEach(function(o, pos){
|
||||
var ligne = el("div", "item item--bloc");
|
||||
ligne.appendChild(document.createTextNode((pos + 1) + ". " + o.t + " "));
|
||||
var up = el("button", "jeton jeton--mini", "monter");
|
||||
var dn = el("button", "jeton jeton--mini", "descendre");
|
||||
up.type = dn.type = "button";
|
||||
up.setAttribute("aria-label", "Monter : " + o.t);
|
||||
dn.setAttribute("aria-label", "Descendre : " + o.t);
|
||||
up.disabled = (pos === 0);
|
||||
dn.disabled = (pos === ordre.length - 1);
|
||||
up.onclick = function(){
|
||||
var x = ordre[pos - 1]; ordre[pos - 1] = ordre[pos]; ordre[pos] = x; dessiner();
|
||||
};
|
||||
dn.onclick = function(){
|
||||
var x = ordre[pos + 1]; ordre[pos + 1] = ordre[pos]; ordre[pos] = x; dessiner();
|
||||
};
|
||||
ligne.appendChild(up); ligne.appendChild(dn);
|
||||
liste.appendChild(ligne);
|
||||
});
|
||||
}
|
||||
dessiner();
|
||||
box.appendChild(liste);
|
||||
|
||||
var v = el("button", "action", "Vérifier l'ordre");
|
||||
v.type = "button";
|
||||
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"); });
|
||||
clore(box, essais === 0, (d.explication || "") +
|
||||
(essais ? " Vérifications ratées : " + essais + "." : ""));
|
||||
} else {
|
||||
essais++;
|
||||
signaler(box, false, "L'ordre n'est pas encore correct, déplace encore des lignes.");
|
||||
}
|
||||
};
|
||||
box.appendChild(v);
|
||||
return finir(box);
|
||||
}
|
||||
|
||||
/* ---------- Zones cliquables sur schéma ----------
|
||||
Les zones SVG reçoivent tabindex et role pour être atteignables
|
||||
au clavier, comme les boutons du reste de la page. */
|
||||
|
||||
function faireHotspot(d){
|
||||
var box = cadre(d);
|
||||
var wrap = el("div", "schema");
|
||||
wrap.innerHTML = d.svg;
|
||||
box.appendChild(wrap);
|
||||
|
||||
var question = el("p", "enonce");
|
||||
question.setAttribute("role", "status");
|
||||
question.setAttribute("aria-live", "polite");
|
||||
box.appendChild(question);
|
||||
|
||||
var restants = melange(d.zones.slice());
|
||||
var erreurs = 0;
|
||||
|
||||
function demander(){
|
||||
if (!restants.length){
|
||||
question.textContent = "Terminé.";
|
||||
clore(box, erreurs === 0, (d.explication || "") +
|
||||
(erreurs ? " Erreurs : " + erreurs + "." : ""));
|
||||
return;
|
||||
}
|
||||
question.innerHTML = "Clique sur : <strong>" + restants[0] + "</strong>";
|
||||
}
|
||||
|
||||
wrap.querySelectorAll(".zone").forEach(function(z){
|
||||
z.setAttribute("tabindex", "0");
|
||||
z.setAttribute("role", "button");
|
||||
z.setAttribute("aria-label", z.dataset.zone || "zone");
|
||||
function essayer(){
|
||||
if (!restants.length) return;
|
||||
if (z.dataset.zone === restants[0]){
|
||||
z.classList.add("juste");
|
||||
restants.shift();
|
||||
demander();
|
||||
} else {
|
||||
erreurs++;
|
||||
z.classList.add("faux");
|
||||
setTimeout(function(){ z.classList.remove("faux"); }, 500);
|
||||
signaler(box, false, "Ce n'est pas cet élément.");
|
||||
}
|
||||
}
|
||||
z.addEventListener("click", essayer);
|
||||
z.addEventListener("keydown", function(e){
|
||||
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar"){
|
||||
e.preventDefault();
|
||||
essayer();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
demander();
|
||||
return finir(box);
|
||||
}
|
||||
|
||||
/* ---------- Calcul ou réponse courte ---------- */
|
||||
|
||||
function faireSaisie(d){
|
||||
var box = cadre(d);
|
||||
var champ = el("input", "court");
|
||||
champ.type = "text";
|
||||
champ.setAttribute("aria-label", d.enonce || d.consigne || "Réponse");
|
||||
var v = el("button", "action", "Vérifier");
|
||||
v.type = "button";
|
||||
var fini = false, essais = 0;
|
||||
|
||||
function verifier(){
|
||||
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;
|
||||
clore(box, essais === 0, (d.explication || "") +
|
||||
(essais ? " Essais ratés : " + essais + "." : ""));
|
||||
} else {
|
||||
essais++;
|
||||
signaler(box, false, d.indice || "Relis la théorie du chapitre et réessaie.");
|
||||
}
|
||||
}
|
||||
|
||||
v.onclick = verifier;
|
||||
champ.onkeydown = function(e){ if (e.key === "Enter"){ e.preventDefault(); verifier(); } };
|
||||
|
||||
box.appendChild(champ);
|
||||
box.appendChild(document.createTextNode(" "));
|
||||
box.appendChild(v);
|
||||
return finir(box);
|
||||
}
|
||||
|
||||
/* ---------- Chasse aux 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.type = "button";
|
||||
b.onclick = function(){
|
||||
if (b.disabled) return;
|
||||
b.disabled = true;
|
||||
if (it[1]){
|
||||
b.classList.add("juste");
|
||||
restants--;
|
||||
if (restants === 0){
|
||||
clore(box, erreurs === 0, (d.explication || "") +
|
||||
(erreurs ? " Affirmations correctes cliquées par erreur : " + erreurs + "." : ""));
|
||||
} else {
|
||||
signaler(box, true, "Bien vu. Il reste " + restants + " affirmation" +
|
||||
(restants > 1 ? "s" : "") + " fausse" + (restants > 1 ? "s" : "") + ".");
|
||||
}
|
||||
} else {
|
||||
b.classList.add("faux");
|
||||
erreurs++;
|
||||
signaler(box, false, "Cette affirmation est correcte, ce n'est pas un intrus.");
|
||||
}
|
||||
};
|
||||
box.appendChild(b);
|
||||
});
|
||||
return finir(box);
|
||||
}
|
||||
|
||||
/* ---------- Série chronométrée ----------
|
||||
Le premier passage fixe le score de l'exercice. Les suivants
|
||||
servent à s'entraîner : ils affichent un résultat mais ne
|
||||
touchent plus au compteur de la page. */
|
||||
|
||||
function faireChrono(d){
|
||||
var box = cadre(d);
|
||||
var minuteur = el("span", "minuteur", "");
|
||||
minuteur.setAttribute("role", "timer");
|
||||
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", "Démarrer la série");
|
||||
demarrer.type = "button";
|
||||
box.appendChild(demarrer);
|
||||
|
||||
demarrer.onclick = function(){
|
||||
if (encours) return;
|
||||
encours = true;
|
||||
demarrer.hidden = true;
|
||||
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.type = "button";
|
||||
b.onclick = function(){
|
||||
if (!encours) return;
|
||||
if (i === q.reponse) bons++;
|
||||
idx++;
|
||||
poser();
|
||||
};
|
||||
zone.appendChild(b);
|
||||
});
|
||||
}
|
||||
|
||||
function terminer(){
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
encours = false;
|
||||
zone.innerHTML = "";
|
||||
var seuil = Math.ceil(d.questions.length * 0.7);
|
||||
var ok = bons >= seuil;
|
||||
var deja = (box.dataset.compte === "1");
|
||||
var detail = "Score : " + bons + " / " + d.questions.length +
|
||||
". Seuil de réussite : " + seuil + ".";
|
||||
if (deja){
|
||||
/* Déjà compté au premier passage : on informe sans toucher au score. */
|
||||
signaler(box, ok, detail + " Ce nouvel essai ne change plus le compteur de la page.");
|
||||
} else {
|
||||
clore(box, ok, detail);
|
||||
}
|
||||
demarrer.textContent = "Recommencer la série";
|
||||
demarrer.hidden = false;
|
||||
}
|
||||
|
||||
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");
|
||||
if (!cible) return;
|
||||
|
||||
TOTAL = data.length; REUSSIS = 0; TERMINES = 0;
|
||||
|
||||
var s = el("div", "score");
|
||||
s.id = "score";
|
||||
s.appendChild(el("div", "score__txt"));
|
||||
var jauge = el("div", "jauge");
|
||||
jauge.appendChild(el("span"));
|
||||
s.appendChild(jauge);
|
||||
var rz = el("button", "action action--secondaire", "Tout recommencer");
|
||||
rz.type = "button";
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user