diff --git a/exercices/Exo_BioCID.html b/exercices/Exo_BioCID.html index b6da2a2..0c930f5 100644 --- a/exercices/Exo_BioCID.html +++ b/exercices/Exo_BioCID.html @@ -859,21 +859,116 @@ function exporterPDF() { setTimeout(() => w.print(), 300); } -/* ======================== PROTECTION CONTRE LA PERTE DES RÉPONSES ======================== */ -/* Les réponses ne sont pas enregistrées : on avertit avant tout rechargement ou départ. - Le texte de la boîte est imposé par le navigateur, il n'est pas personnalisable. */ +/* ======================== SAUVEGARDE AUTOMATIQUE ======================== */ +/* Les réponses sont enregistrées dans le navigateur au fil de la saisie, puis + proposées à la restauration au rechargement. En file://, localStorage peut être + indisponible (Safari) : dans ce cas la page fonctionne normalement, sans filet. */ +const CLE_SAUVEGARDE = 'certibiocide:biocid:v1'; + let reponsesModifiees = false; -function marquerModifie() { reponsesModifiees = true; } +function marquerModifie() { reponsesModifiees = true; sauvegarder(); } document.addEventListener('input', marquerModifie, true); document.addEventListener('change', marquerModifie, true); +// Avertit avant tout rechargement ou départ. Le texte de la boîte est imposé +// par le navigateur, il n'est pas personnalisable. window.addEventListener('beforeunload', e => { if (!reponsesModifiees) return; e.preventDefault(); e.returnValue = ''; }); +function etatCourant() { + const champs = {}; + document.querySelectorAll('input[id], textarea[id]').forEach(el => { + if (el.value) champs[el.id] = el.value; + }); + return { savedAt: new Date().toISOString(), slide: currentSlide, champs }; +} + +let minuteurSauvegarde = null; +function sauvegarder() { + clearTimeout(minuteurSauvegarde); + minuteurSauvegarde = setTimeout(() => { + try { + localStorage.setItem(CLE_SAUVEGARDE, JSON.stringify(etatCourant())); + } catch (e) { /* navigation privée ou quota : on continue sans sauvegarde */ } + }, 400); +} + +function lireSauvegarde() { + try { return JSON.parse(localStorage.getItem(CLE_SAUVEGARDE)); } + catch (e) { return null; } +} +function effacerSauvegarde() { + try { localStorage.removeItem(CLE_SAUVEGARDE); } catch (e) {} +} + +function appliquerSauvegarde(data) { + Object.entries(data.champs || {}).forEach(([id, v]) => { + const el = document.getElementById(id); + if (el) el.value = v; + }); + reponsesModifiees = true; + if (typeof data.slide === 'number' && data.slide > 0) goTo(data.slide); + updateUI(); +} + +/* Bandeau de restauration : on ne restaure jamais sans demander, pour qu'un + nouveau groupe n'hérite pas des réponses du précédent sur un poste partagé. */ +function injecterStylesBandeau() { + const style = document.createElement('style'); + style.textContent = ` + .restore-bar { + position: fixed; top: 84px; left: 50%; transform: translateX(-50%); + z-index: 900; max-width: min(680px, calc(100vw - 32px)); + display: flex; align-items: center; gap: 16px; flex-wrap: wrap; + background: var(--white); border: 1.5px solid var(--amber); + border-radius: 14px; padding: 14px 18px; + box-shadow: 0 8px 28px rgba(0,0,0,0.12); font-size: 13.5px; + } + .restore-bar .restore-text { flex: 1; min-width: 220px; line-height: 1.5; } + .restore-bar .restore-actions { display: flex; gap: 8px; flex-shrink: 0; } + .restore-bar button { + font-family: 'Syne', sans-serif; font-weight: 700; font-size: 12.5px; + padding: 8px 16px; border-radius: 20px; cursor: pointer; + border: 1px solid var(--border); background: transparent; color: var(--text-muted); + } + .restore-bar button:hover { border-color: var(--amber); color: var(--amber); } + .restore-bar button.primary { + background: var(--amber); border-color: var(--amber); color: #fff; + } + .restore-bar button.primary:hover { background: var(--amber-dark); border-color: var(--amber-dark); color: #fff; } + `; + document.head.appendChild(style); +} + +function proposerRestauration() { + const data = lireSauvegarde(); + if (!data || !data.champs || Object.keys(data.champs).length === 0) return; + + const quand = new Date(data.savedAt); + const libelle = isNaN(quand.getTime()) ? '' : + ` du ${quand.toLocaleDateString('fr-FR')} à ${quand.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`; + + injecterStylesBandeau(); + const bar = document.createElement('div'); + bar.className = 'restore-bar'; + bar.innerHTML = ` + 💾 Des réponses enregistrées${libelle} ont été retrouvées sur ce poste. + + + + `; + document.body.appendChild(bar); + + document.getElementById('btnRestaurer').onclick = () => { appliquerSauvegarde(data); bar.remove(); }; + document.getElementById('btnRepartir').onclick = () => { effacerSauvegarde(); bar.remove(); }; +} + +proposerRestauration(); + updateUI(); diff --git a/exercices/TP_Nuisibles.html b/exercices/TP_Nuisibles.html index dbf1964..9d82c77 100644 --- a/exercices/TP_Nuisibles.html +++ b/exercices/TP_Nuisibles.html @@ -1621,21 +1621,156 @@ function escapeHtml(str) { return str.replace(/&/g, '&').replace(//g, '>'); } -/* ======================== PROTECTION CONTRE LA PERTE DES RÉPONSES ======================== */ -/* Les réponses ne sont pas enregistrées : on avertit avant tout rechargement ou départ. - Le texte de la boîte est imposé par le navigateur, il n'est pas personnalisable. */ +/* ======================== SAUVEGARDE AUTOMATIQUE ======================== */ +/* Les réponses sont enregistrées dans le navigateur au fil de la saisie, puis + proposées à la restauration au rechargement. En file://, localStorage peut être + indisponible (Safari) : dans ce cas la page fonctionne normalement, sans filet. */ +const CLE_SAUVEGARDE = 'certibiocide:tp-nuisibles:v1'; + let reponsesModifiees = false; -function marquerModifie() { reponsesModifiees = true; } +function marquerModifie() { reponsesModifiees = true; sauvegarder(); } document.addEventListener('input', marquerModifie, true); document.addEventListener('change', marquerModifie, true); +// Avertit avant tout rechargement ou départ. Le texte de la boîte est imposé +// par le navigateur, il n'est pas personnalisable. window.addEventListener('beforeunload', e => { if (!reponsesModifiees) return; e.preventDefault(); e.returnValue = ''; }); +function etatCourant() { + const champs = {}; + document.querySelectorAll('input[id], textarea[id]').forEach(el => { + if (el.value) champs[el.id] = el.value; + }); + // Les cellules du tableau TP3 n'ont pas d'id : on les repère par leur position. + const risques = Array.from(document.querySelectorAll('#riskTableBody .risk-row')) + .map(row => Array.from(row.querySelectorAll('.risk-cell')).map(c => c.value)); + + return { + savedAt: new Date().toISOString(), + slide: currentSlide, + champs, + risques, + cas: selectedCase.key || '', + formateur: stagiaire.formateur || '', + nom: stagiaire.nom || '', + date: stagiaire.date || '', + }; +} + +let minuteurSauvegarde = null; +function sauvegarder() { + clearTimeout(minuteurSauvegarde); + minuteurSauvegarde = setTimeout(() => { + try { + localStorage.setItem(CLE_SAUVEGARDE, JSON.stringify(etatCourant())); + } catch (e) { /* navigation privée ou quota : on continue sans sauvegarde */ } + }, 400); +} + +function lireSauvegarde() { + try { return JSON.parse(localStorage.getItem(CLE_SAUVEGARDE)); } + catch (e) { return null; } +} +function effacerSauvegarde() { + try { localStorage.removeItem(CLE_SAUVEGARDE); } catch (e) {} +} + +function appliquerSauvegarde(data) { + Object.entries(data.champs || {}).forEach(([id, v]) => { + const el = document.getElementById(id); + if (el) el.value = v; + }); + + // Recrée autant de lignes de risques que nécessaire avant de les remplir + const risques = data.risques || []; + const tbody = document.getElementById('riskTableBody'); + while (tbody.querySelectorAll('.risk-row').length < risques.length) addRiskRow(); + tbody.querySelectorAll('.risk-row').forEach((row, i) => { + const valeurs = risques[i] || []; + row.querySelectorAll('.risk-cell').forEach((cell, j) => { cell.value = valeurs[j] || ''; }); + }); + + // Le cas pratique et le formateur passent par leurs boutons, qui remettent + // aussi l'interface (carte sélectionnée, bouton de confirmation) dans le bon état. + if (data.cas) { + const carte = Array.from(document.querySelectorAll('.case-card')) + .find(c => (c.getAttribute('onclick') || '').includes("'" + data.cas + "'")); + if (carte) carte.click(); + } + if (data.formateur) { + const btn = document.getElementById('btn' + data.formateur); + if (btn) btn.click(); + } + stagiaire.nom = data.nom || stagiaire.nom; + stagiaire.date = data.date || stagiaire.date; + + reponsesModifiees = true; + if (typeof data.slide === 'number' && data.slide > 0) goTo(data.slide); + updateUI(); +} + +/* Bandeau de restauration : on ne restaure jamais sans demander, pour qu'un + nouveau groupe n'hérite pas des réponses du précédent sur un poste partagé. */ +function injecterStylesBandeau() { + const style = document.createElement('style'); + style.textContent = ` + .restore-bar { + position: fixed; top: 84px; left: 50%; transform: translateX(-50%); + z-index: 900; max-width: min(680px, calc(100vw - 32px)); + display: flex; align-items: center; gap: 16px; flex-wrap: wrap; + background: var(--white); border: 1.5px solid var(--amber); + border-radius: 14px; padding: 14px 18px; + box-shadow: 0 8px 28px rgba(0,0,0,0.12); font-size: 13.5px; + } + .restore-bar .restore-text { flex: 1; min-width: 220px; line-height: 1.5; } + .restore-bar .restore-actions { display: flex; gap: 8px; flex-shrink: 0; } + .restore-bar button { + font-family: 'Syne', sans-serif; font-weight: 700; font-size: 12.5px; + padding: 8px 16px; border-radius: 20px; cursor: pointer; + border: 1px solid var(--border); background: transparent; color: var(--text-muted); + } + .restore-bar button:hover { border-color: var(--amber); color: var(--amber); } + .restore-bar button.primary { + background: var(--amber); border-color: var(--amber); color: #fff; + } + .restore-bar button.primary:hover { background: var(--amber-dark); border-color: var(--amber-dark); color: #fff; } + `; + document.head.appendChild(style); +} + +function proposerRestauration() { + const data = lireSauvegarde(); + if (!data) return; + const aDesReponses = (data.champs && Object.keys(data.champs).length > 0) + || (data.risques || []).some(r => r.some(v => v)); + if (!aDesReponses) return; + + const quand = new Date(data.savedAt); + const libelle = isNaN(quand.getTime()) ? '' : + ` du ${quand.toLocaleDateString('fr-FR')} à ${quand.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`; + + injecterStylesBandeau(); + const bar = document.createElement('div'); + bar.className = 'restore-bar'; + bar.innerHTML = ` + 💾 Des réponses enregistrées${libelle} ont été retrouvées sur ce poste. + + + + `; + document.body.appendChild(bar); + + document.getElementById('btnRestaurer').onclick = () => { appliquerSauvegarde(data); bar.remove(); }; + document.getElementById('btnRepartir').onclick = () => { effacerSauvegarde(); bar.remove(); }; +} + +proposerRestauration(); + updateUI();