Annuaire › Code ouvert
Code publié par son auteur, en lecture seule. « Copier dans mon espace » crée un nouveau site à vous (non publié) que vous pouvez modifier librement.
js/ui.js · 14.1 Ko
/* ============================================================
Fichier : js/ui.js
Rôle : l'AFFICHAGE. Ce fichier lit l'état du jeu (state) et
dessine la page ; il remonte les actions de l'utilisateur
(clics, touches) au chef d'orchestre (main.js).
Il ne calcule aucun score et ne décide d'aucune règle.
Dépend de : data/questions.js (NIVEAUX) et des id de index.html.
Expose : l'objet global « UI » avec init() et afficher().
Utilisé par : js/main.js.
------------------------------------------------------------
Séparation des rôles :
- Quiz (quiz.js) : décide. - UI (ce fichier) : montre.
Pour changer le design, on ne touche qu'à ce fichier et au CSS.
============================================================ */
const UI = (function () {
/* ----------------------------------------------------------
1. ÉLÉMENTS DE LA PAGE
On les retrouve une seule fois, ici, puis on les réutilise.
---------------------------------------------------------- */
const el = {
ecranAccueil: document.getElementById("ecran-accueil"),
ecranQuestion: document.getElementById("ecran-question"),
ecranFin: document.getElementById("ecran-fin"),
titreAccueil: document.getElementById("titre-accueil"),
titreFin: document.getElementById("titre-fin"),
formAccueil: document.getElementById("formulaire-accueil"),
listeNiveaux: document.getElementById("liste-niveaux"),
listeMeilleurs: document.getElementById("liste-meilleurs"),
progression: document.getElementById("info-progression"),
score: document.getElementById("info-score"),
minuteur: document.querySelector(".minuteur"),
barreTemps: document.getElementById("barre-temps"),
texteTemps: document.getElementById("texte-temps"),
question: document.getElementById("texte-question"),
listeChoix: document.getElementById("liste-choix"),
zoneRetour: document.getElementById("zone-retour"),
texteRetour: document.getElementById("texte-retour"),
boutonSuivant: document.getElementById("bouton-suivant"),
boutonAbandon: document.getElementById("bouton-abandonner"),
resumeFin: document.getElementById("resume-fin"),
messageRecord: document.getElementById("message-record"),
boutonRejouer: document.getElementById("bouton-rejouer"),
annonce: document.getElementById("annonce")
};
/** Formateur de nombres français (1 230 au lieu de 1230). */
const formatNombre = new Intl.NumberFormat("fr-FR");
/** Vrai après le tout premier affichage : évite de voler le focus au chargement de la page. */
let dejaAffiche = false;
/** Nombre de propositions affichées (pour les touches 1 à 4). */
let nbChoix = 0;
/** Phase du jeu connue (copie pour le gestionnaire de touches). */
let phaseCourante = "accueil";
/* ----------------------------------------------------------
2. PETITS OUTILS
---------------------------------------------------------- */
/**
* Affiche un seul des trois écrans et cache les autres.
* @param {HTMLElement} ecran - l'écran à montrer
* @returns {void}
*/
function montrerEcran(ecran) {
[el.ecranAccueil, el.ecranQuestion, el.ecranFin].forEach(function (e) {
e.hidden = (e !== ecran); // hidden = true cache l'élément
});
}
/**
* Écrit un message dans la zone « aria-live » pour les lecteurs d'écran.
* On vide d'abord puis on remplit : cela garantit que le même message
* répété sera quand même relu.
* @param {string} message - le texte à annoncer
* @returns {void}
*/
function annoncer(message) {
el.annonce.textContent = "";
// setTimeout très court : laisse le navigateur remarquer que la zone a été vidée
setTimeout(function () { el.annonce.textContent = message; }, 50);
}
/**
* Donne le focus à un élément, sans faire défiler brutalement la page.
* @param {HTMLElement} element - l'élément à focaliser
* @returns {void}
*/
function focaliser(element) {
element.focus({ preventScroll: false });
}
/* ----------------------------------------------------------
3. ÉCRAN D'ACCUEIL
---------------------------------------------------------- */
/**
* Construit les boutons radio des niveaux à partir de NIVEAUX.
* Pourquoi par JavaScript ? Pour ajouter un niveau, il suffit de
* l'ajouter dans data/questions.js : la page se met à jour seule.
* @param {string} niveauChoisi - niveau à cocher
* @returns {void}
*/
function afficherNiveaux(niveauChoisi) {
el.listeNiveaux.textContent = "";
Object.keys(NIVEAUX).forEach(function (cle) {
const label = document.createElement("label");
label.className = "niveau";
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "niveau"; // même nom = un seul radio coché à la fois
radio.value = cle;
radio.checked = (cle === niveauChoisi);
const texte = document.createElement("span");
const titre = document.createElement("strong");
titre.textContent = NIVEAUX[cle].libelle;
const detail = document.createElement("small");
detail.textContent = NIVEAUX[cle].detail;
texte.append(titre, detail);
label.append(radio, texte);
el.listeNiveaux.appendChild(label);
});
}
/**
* Affiche la liste des meilleurs scores.
* @param {object} meilleurs - par exemple { facile: 1200, moyen: 0, difficile: 0 }
* @returns {void}
*/
function afficherMeilleurs(meilleurs) {
el.listeMeilleurs.textContent = "";
Object.keys(NIVEAUX).forEach(function (cle) {
const li = document.createElement("li");
const nom = document.createElement("span");
nom.textContent = NIVEAUX[cle].libelle;
const valeur = document.createElement("strong");
valeur.textContent = meilleurs[cle] > 0 ? formatNombre.format(meilleurs[cle]) + " points" : "–";
li.append(nom, valeur);
el.listeMeilleurs.appendChild(li);
});
}
/**
* Dessine l'écran d'accueil.
* @param {object} state - l'état du jeu
* @returns {void}
*/
function dessinerAccueil(state) {
afficherNiveaux(state.niveau);
afficherMeilleurs(state.meilleurs);
montrerEcran(el.ecranAccueil);
if (dejaAffiche) { focaliser(el.titreAccueil); }
}
/* ----------------------------------------------------------
4. ÉCRAN DE QUESTION
---------------------------------------------------------- */
/**
* Met à jour le minuteur (texte et barre).
* @param {object} state - l'état du jeu
* @returns {void}
*/
function dessinerTemps(state) {
const total = NIVEAUX[state.niveau].secondes;
el.texteTemps.textContent = state.tempsRestant + " s";
el.barreTemps.max = total;
el.barreTemps.value = state.tempsRestant;
// Les 5 dernières secondes : couleur d'alerte (la classe est définie dans le CSS)
el.minuteur.classList.toggle("urgent", state.tempsRestant <= 5);
}
/**
* Dessine une nouvelle question et ses propositions.
* @param {object} state - l'état du jeu
* @returns {void}
*/
function dessinerQuestion(state) {
const question = state.questions[state.index];
el.progression.textContent = "Question " + (state.index + 1) + " sur " + state.questions.length;
el.score.textContent = "Score : " + formatNombre.format(state.score);
el.question.textContent = question.question;
// On reconstruit les boutons de réponse
el.listeChoix.textContent = "";
question.choix.forEach(function (texte, position) {
const bouton = document.createElement("button");
bouton.type = "button";
bouton.className = "choix__bouton";
bouton.dataset.position = String(position); // lu plus tard par la délégation d'événements
bouton.textContent = (position + 1) + ". " + texte;
el.listeChoix.appendChild(bouton);
});
nbChoix = question.choix.length;
el.zoneRetour.hidden = true;
el.texteRetour.textContent = "";
el.zoneRetour.className = "retour";
el.boutonAbandon.hidden = false;
dessinerTemps(state);
montrerEcran(el.ecranQuestion);
// Le focus sur la question : un lecteur d'écran la lit, et un utilisateur du clavier
// n'a plus qu'à appuyer sur Tab (ou 1 à 4) pour répondre.
focaliser(el.question);
}
/**
* Montre le résultat de la réponse : couleurs, symboles et explication.
* @param {object} state - l'état du jeu
* @returns {void}
*/
function dessinerReponse(state) {
const question = state.questions[state.index];
// On parcourt les boutons pour les marquer
Array.from(el.listeChoix.children).forEach(function (bouton, position) {
bouton.disabled = true; // on ne peut plus répondre
if (position === question.bonne) {
bouton.classList.add("bonne");
bouton.textContent = "✔ " + bouton.textContent; // symbole : l'information ne repose pas que sur la couleur
} else if (position === state.choisi) {
bouton.classList.add("mauvaise");
bouton.textContent = "✖ " + bouton.textContent;
}
});
// Message de retour
let message;
if (state.correct) {
message = "Bonne réponse ! +" + formatNombre.format(state.gain) + " points. ";
} else if (state.choisi === -1) {
message = "Temps écoulé ! La bonne réponse était : " + question.choix[question.bonne] + ". ";
} else {
message = "Raté. La bonne réponse était : " + question.choix[question.bonne] + ". ";
}
message += question.explication;
el.texteRetour.textContent = message;
el.zoneRetour.className = "retour " + (state.correct ? "ok" : "ko");
el.zoneRetour.hidden = false;
const derniere = (state.index + 1 === state.questions.length);
el.boutonSuivant.textContent = derniere ? "Voir le résultat" : "Question suivante";
el.score.textContent = "Score : " + formatNombre.format(state.score);
el.boutonAbandon.hidden = true;
annoncer(message);
focaliser(el.boutonSuivant); // Entrée suffit pour continuer
}
/* ----------------------------------------------------------
5. ÉCRAN DE FIN
---------------------------------------------------------- */
/**
* Dessine l'écran de fin de partie.
* @param {object} state - l'état du jeu
* @returns {void}
*/
function dessinerFin(state) {
el.resumeFin.textContent = state.bonnes + " bonne(s) réponse(s) sur " + state.questions.length +
" – " + formatNombre.format(state.score) + " points (niveau " + NIVEAUX[state.niveau].libelle.toLowerCase() + ").";
el.messageRecord.textContent = state.nouveauRecord
? "Nouveau meilleur score à ce niveau !"
: "Meilleur score à ce niveau : " + formatNombre.format(state.meilleurs[state.niveau]) + " points.";
montrerEcran(el.ecranFin);
annoncer(el.resumeFin.textContent + " " + el.messageRecord.textContent);
focaliser(el.titreFin);
}
/* ----------------------------------------------------------
6. POINT D'ENTRÉE : réagir aux événements du Quiz
---------------------------------------------------------- */
/**
* Redessine l'écran en fonction de l'événement reçu du Quiz.
* @param {string} evenement - "accueil", "question", "tick", "reponse" ou "fin"
* @param {object} state - l'état du jeu
* @returns {void}
*/
function afficher(evenement, state) {
phaseCourante = state.phase;
switch (evenement) {
case "accueil": dessinerAccueil(state); break;
case "question": dessinerQuestion(state); break;
case "tick":
dessinerTemps(state);
// On n'annonce pas chaque seconde (ce serait insupportable) : seulement 10 s et 5 s.
if (state.tempsRestant === 10 || state.tempsRestant === 5) {
annoncer("Il reste " + state.tempsRestant + " secondes.");
}
break;
case "reponse": dessinerReponse(state); break;
case "fin": dessinerFin(state); break;
}
dejaAffiche = true;
}
/* ----------------------------------------------------------
7. ENTRÉES DE L'UTILISATEUR
---------------------------------------------------------- */
/**
* Branche les écouteurs d'événements. Les actions réelles sont
* fournies par main.js sous forme de fonctions (« rappels »), ce qui
* garde UI indépendant de Quiz.
* @param {{surDemarrer:function(string):void, surChoix:function(number):void,
* surSuivante:function():void, surAbandon:function():void,
* surSon:function(boolean):void}} actions - les fonctions à appeler
* @returns {void}
*/
function init(actions) {
// Choix du niveau + bouton Commencer (un formulaire : Entrée fonctionne aussi)
el.formAccueil.addEventListener("submit", function (evenement) {
evenement.preventDefault();
const coche = el.formAccueil.querySelector("input[name='niveau']:checked");
actions.surDemarrer(coche ? coche.value : "facile");
});
// DÉLÉGATION : un seul écouteur sur le parent des boutons de réponse
el.listeChoix.addEventListener("click", function (evenement) {
const bouton = evenement.target.closest(".choix__bouton");
if (!bouton || bouton.disabled) { return; }
actions.surChoix(Number(bouton.dataset.position));
});
el.boutonSuivant.addEventListener("click", actions.surSuivante);
el.boutonAbandon.addEventListener("click", actions.surAbandon);
el.boutonRejouer.addEventListener("click", actions.surAbandon); // revient à l'accueil pour rechoisir
// Case « effets sonores »
document.getElementById("option-son").addEventListener("change", function (evenement) {
actions.surSon(evenement.target.checked);
});
// Raccourcis clavier : touches 1 à 4 pendant une question
document.addEventListener("keydown", function (evenement) {
if (phaseCourante !== "question") { return; }
if (evenement.ctrlKey || evenement.altKey || evenement.metaKey) { return; } // laisse passer Ctrl+1 etc.
const numero = Number(evenement.key); // "2" -> 2 ; une lettre -> NaN
if (numero >= 1 && numero <= nbChoix) {
actions.surChoix(numero - 1);
}
});
}
return { init: init, afficher: afficher };
})();