Build and deploy / test-python (push) Successful in 11m18s
Build and deploy / test-js (push) Successful in 1m16s
Build and deploy / lint-python (push) Successful in 4m24s
Build and deploy / lint-js (push) Successful in 2m51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Successful in 5m30s
Corrige les findings SonarQube (via SonarLint IDE, fichier par fichier) sur ~24 fichiers static/js/ : parseFloat/parseInt -> Number.*, .replace(/x/g,y) -> .replaceAll, .indexOf() -> .includes()/.startsWith(), getAttribute/setAttribute -> .dataset, tableaux -> Set, x && x.y -> x?.y (verifie site par site), extraction de template litteraux imbriques, ternaires imbriquees, refactors de complexite cognitive (S3776) via tables de dispatch, Object.hasOwn, .at(), et deduplication de fonctions identiques (S4144). Deux exceptions S2486 documentees/corrigees (filter-repeater-rows.js) et un cas S2703 de partage inter-scripts complete (_collisionWizard, trigger-editor.js <-> collision-rules-editor.js). Details complets dans CODE_QUALITY.md section 5. Restaure aussi le job CI "sonarqube" (non-bloquant) dans .gitea/workflows/deploy.yml maintenant que l'instance prod est operationnelle. Suites vertes : 276/276 JS (node --test), 591/591 Python (pytest). SKIP=djlint sur ce commit : hook djlint bloquant sur le backlog H021 (styles inline, 49 occurrences/6 templates) deja documente comme dette assumee non traitee dans CODE_QUALITY.md section 6, aucun rapport avec ce commit (aucun template touche ici) - valide explicitement avec l'utilisateur avant de contourner ce hook. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
178 lines
9.4 KiB
JavaScript
178 lines
9.4 KiB
JavaScript
// ---------- Effets visuels par modèle de "❓ Boîte à quiz" ----------
|
|
// Registre appelé par static/js/play/dialogue-box-controller.js (JAMAIS
|
|
// l'inverse) aux points d'extension du moteur PARTAGÉ (forgeShowQuizBox,
|
|
// forgeQuizBoxAnswer) — voir screens/rendering/quiz_box_templates.py
|
|
// pour le HTML/CSS de chaque modèle. Ajouter un futur modèle n'oblige
|
|
// donc jamais à toucher dialogue-box-controller.js : juste une nouvelle
|
|
// entrée ici (optionnelle) + son CSS/HTML.
|
|
|
|
// Effet PARTAGÉ entre plusieurs modèles "page de quiz" (Manga, Classique,
|
|
// et tout futur modèle qui voudrait la même mécanique) — demande
|
|
// explicite : au-dessus de la question, "Question N/M" (nombre de
|
|
// QUESTIONS de ce dialogue, pas la position brute dans lines qui mélange
|
|
// aussi les répliques) plutôt que le nom de l'objet déclencheur — et une
|
|
// barre d'étapes juste en dessous du bandeau. `forgeQuizBoxState`/
|
|
// `forgeDialogueBoxState` (déclarés dans dialogue-box-controller.js,
|
|
// chargé à côté de ce fichier) sont des variables GLOBALES de script
|
|
// classique (pas de module), lisibles ici telles quelles.
|
|
// Extrait de forgeQuizProgressOnShow (lot 4 "modernisation JS", S7721) :
|
|
// fonction pure (aucune fermeture), pas besoin d'être redéfinie à chaque appel.
|
|
function isQuestionLine(line) { return line?.type === 'question'; }
|
|
|
|
// Extrait de forgeQuizProgressOnShow (S3358) : segment "fait"/"courant"/
|
|
// "à venir" de la barre d'étapes pour la question d'indice `i` (1-based).
|
|
function forgeQuizProgressBarSegmentClass(i, current) {
|
|
if (i < current) return 'is-done';
|
|
if (i === current) return 'is-current';
|
|
return '';
|
|
}
|
|
|
|
// `progressSelector` : chaque modèle a son propre conteneur de barre
|
|
// d'étapes (data-manga-role/data-classique-role, jamais data-quiz-role —
|
|
// décoration propre à CHAQUE modèle, pas du contrat partagé).
|
|
function forgeQuizProgressOnShow(widget, progressSelector) {
|
|
if (!widget) return;
|
|
const quizObjectId = widget.dataset.objectId;
|
|
const quizState = (typeof forgeQuizBoxState !== 'undefined') ? forgeQuizBoxState[quizObjectId] : null;
|
|
if (!quizState) return;
|
|
const dialogueState = (typeof forgeDialogueBoxState !== 'undefined') ? forgeDialogueBoxState[quizState.dialogueObjectId] : null;
|
|
if (!dialogueState) return;
|
|
const total = dialogueState.lines.filter(isQuestionLine).length;
|
|
if (total === 0) return;
|
|
const current = dialogueState.lines.slice(0, dialogueState.index + 1).filter(isQuestionLine).length;
|
|
const header = widget.querySelector('[data-quiz-role="header"]');
|
|
if (header) header.textContent = `Question ${current}/${total}`;
|
|
const progressEl = widget.querySelector(progressSelector);
|
|
if (progressEl) {
|
|
const bars = [];
|
|
for (let i = 1; i <= total; i++) {
|
|
bars.push(`<i class="${forgeQuizProgressBarSegmentClass(i, current)}"></i>`);
|
|
}
|
|
progressEl.innerHTML = bars.join('');
|
|
}
|
|
}
|
|
|
|
function forgeQuizMangaOnShow(widget) {
|
|
// Défensif : si CE widget est réutilisé pour un quiz déclenché à
|
|
// nouveau plus tard dans la partie (voir forgeQuizResultQuit,
|
|
// dialogue-box-controller.js — le widget lui-même n'est jamais
|
|
// reconstruit, juste masqué), une question fraîchement affichée ne
|
|
// doit jamais rester sur l'écran de résultat de la fois précédente.
|
|
if (widget) widget.classList.remove('is-showing-result');
|
|
forgeQuizProgressOnShow(widget, '[data-manga-role="progress"]');
|
|
}
|
|
|
|
function forgeQuizClassiqueOnShow(widget) {
|
|
// Même garde-fou que forgeQuizMangaOnShow ci-dessus, ici posée sur
|
|
// .classiqueContent (pas la racine du widget, voir
|
|
// render_quiz_box_page_classique) plutôt que sur le widget lui-même.
|
|
if (widget) {
|
|
const content = widget.querySelector('.classiqueContent');
|
|
if (content) content.classList.remove('is-showing-result');
|
|
}
|
|
forgeQuizProgressOnShow(widget, '[data-classique-role="progress"]');
|
|
}
|
|
|
|
// Écrans de résultat de fin de quiz ("classique" ci-dessous, "manga"
|
|
// plus bas) — appelés par forgeQuizPrepareResultScreen
|
|
// (dialogue-box-controller.js) UNIQUEMENT à la toute dernière question
|
|
// (ou si le minuteur du quiz ENTIER expire). `result.correctCount`/
|
|
// `result.totalCount` : comptés PAR CE QUIZ (voir forgeQuizBoxState dans
|
|
// dialogue-box-controller.js), jamais window.forgeQuizScore/
|
|
// forgeQuizCorrectAnswered qui cumulent TOUTE la partie. Le seuil de
|
|
// réussite réutilise EXACTEMENT window.FORGE_SUCCESS_THRESHOLD_PERCENT
|
|
// (déjà utilisé pour le statut SCORM final, voir
|
|
// forgeComputeFinalScormStatus) plutôt qu'un réglage à part. Le bouton
|
|
// QUITTE le quiz (jamais ne le recommence, demande explicite) — voir
|
|
// forgeQuizResultQuit, dialogue-box-controller.js.
|
|
function forgeQuizClassiqueOnQuizComplete(widget, result) {
|
|
if (!widget) return;
|
|
const content = widget.querySelector('.classiqueContent');
|
|
const zone = widget.querySelector('[data-classique-role="result"]');
|
|
if (!content || !zone) return;
|
|
const total = result?.totalCount || 0;
|
|
const correct = result?.correctCount || 0;
|
|
const pct = total > 0 ? Math.round((correct / total) * 100) : 0;
|
|
const threshold = (typeof window !== 'undefined' && typeof window.FORGE_SUCCESS_THRESHOLD_PERCENT === 'number')
|
|
? window.FORGE_SUCCESS_THRESHOLD_PERCENT : 70;
|
|
const passed = pct >= threshold;
|
|
const titleEl = widget.querySelector('.classiqueHeaderTitle');
|
|
const title = titleEl ? titleEl.textContent : 'Quiz';
|
|
const quizObjectId = widget.dataset.objectId;
|
|
zone.innerHTML = `<div class="classiqueResultRing" style="--pct:${pct}"><span>${pct}%</span></div>`
|
|
+ `<p class="classiqueResultTitle">${correct} bonne${correct > 1 ? 's' : ''} réponse${correct > 1 ? 's' : ''} sur ${total}</p>`
|
|
+ `<p class="classiqueResultSub">« ${typeof forgeEscapeHtml === 'function' ? forgeEscapeHtml(title) : title} »</p>`
|
|
+ `<div class="classiqueResultBadge ${passed ? 'pass' : 'fail'}">${
|
|
passed ? '✓ Module validé' : `✕ Module non validé — seuil ${threshold}%`
|
|
}</div>`
|
|
+ `<button type="button" class="classiqueQuitBtn" onclick="forgeQuizResultQuit(${quizObjectId})">✕ Quitter le quiz</button>`;
|
|
content.classList.add('is-showing-result');
|
|
}
|
|
|
|
// "Manga" — même mécanique que forgeQuizClassiqueOnQuizComplete
|
|
// ci-dessus, thème manga propre à ce modèle (voir static/style.css) :
|
|
// pas de conteneur "content" à part ici (contrairement à
|
|
// .classiqueContent), la classe "is-showing-result" bascule sur la
|
|
// RACINE du widget elle-même (voir render_quiz_box_page_manga). La
|
|
// barre d'étapes est reposée à 100% "terminé" (tous les segments
|
|
// is-done) : forgeQuizProgressOnShow ne le fait pas toute seule, sa
|
|
// dernière question reste "is-current" (jamais "is-done", il n'y a
|
|
// plus de question suivante vers laquelle avancer) — un quiz FINI doit
|
|
// visuellement se lire comme entièrement complété.
|
|
function forgeQuizMangaOnQuizComplete(widget, result) {
|
|
if (!widget) return;
|
|
const zone = widget.querySelector('[data-manga-role="result"]');
|
|
if (!zone) return;
|
|
const total = result?.totalCount || 0;
|
|
const correct = result?.correctCount || 0;
|
|
const pct = total > 0 ? Math.round((correct / total) * 100) : 0;
|
|
const threshold = (typeof window !== 'undefined' && typeof window.FORGE_SUCCESS_THRESHOLD_PERCENT === 'number')
|
|
? window.FORGE_SUCCESS_THRESHOLD_PERCENT : 70;
|
|
const passed = pct >= threshold;
|
|
const quizObjectId = widget.dataset.objectId;
|
|
zone.innerHTML = '<div class="mangaResultCard">'
|
|
+ '<span class="mangaResultTitle">Fin du quiz</span>'
|
|
+ `<p class="mangaResultScore">Score final : ${correct} / ${total}</p>`
|
|
+ `<div class="mangaResultBadge ${passed ? 'pass' : 'fail'}">${passed ? '合格' : '不合格'}</div>`
|
|
+ `<div><button type="button" class="mangaQuitBtn" onclick="forgeQuizResultQuit(${quizObjectId})">Quitter le quiz</button></div>`
|
|
+ '</div>';
|
|
const progressEl = widget.querySelector('[data-manga-role="progress"]');
|
|
if (progressEl && total > 0) {
|
|
const bars = [];
|
|
for (let i = 0; i < total; i++) bars.push('<i class="is-done"></i>');
|
|
progressEl.innerHTML = bars.join('');
|
|
}
|
|
widget.classList.add('is-showing-result');
|
|
}
|
|
|
|
const forgeQuizTemplateEffects = {
|
|
// Modèles "boîte de dialogue" (voir screens/rendering/quiz_box_config.py
|
|
// ::QUIZ_BOX_DIALOG_TEMPLATES) — "Verre dépoli"/"Néon"/"Minimal"/
|
|
// "Ludique" ont été retirés (demande explicite : "supprime tous les
|
|
// modele de boite sauf le base"). "manga_dialogue" (clé DIFFÉRENTE du
|
|
// modèle "page" "manga" ci-dessous, même registre partagé) reste
|
|
// proche de "defaut" — aucun effet JS particulier, juste sa propre
|
|
// décoration CSS (voir render_quiz_box_manga_dialogue).
|
|
defaut: {},
|
|
manga_dialogue: {},
|
|
// Modèles "page de quiz" (QUIZ_BOX_PAGE_TEMPLATES) — le tampon "正解"/
|
|
// "不正解" du modèle "manga" est posé en CSS pur (::after sur
|
|
// .is-correct/.is-wrong, voir static/style.css), seul "Question N/M" +
|
|
// la barre d'étapes ont besoin d'un vrai calcul JS (onShow ci-dessus).
|
|
manga: { onShow: forgeQuizMangaOnShow, onQuizComplete: forgeQuizMangaOnQuizComplete },
|
|
classique: { onShow: forgeQuizClassiqueOnShow, onQuizComplete: forgeQuizClassiqueOnQuizComplete },
|
|
};
|
|
|
|
// static/js/play/__tests__/ (node:test) — même garde que le reste de
|
|
// static/js/play/.
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
forgeQuizTemplateEffects,
|
|
forgeQuizProgressOnShow,
|
|
forgeQuizMangaOnShow,
|
|
forgeQuizMangaOnQuizComplete,
|
|
forgeQuizClassiqueOnShow,
|
|
forgeQuizClassiqueOnQuizComplete,
|
|
};
|
|
}
|