Ajout mode présentation
This commit is contained in:
1 parent
4862d61ccd
commit
08875d3b5a
18 files changed
+1781
-270
No files matched your search
@@ -0,0 +1,483 @@
|
||||
<?php
|
||||
|
||||
// Page "Mode présentation" : contenu markdown protégé par un code PIN à 9 chiffres.
|
||||
// Le compteur de tentatives est géré côté serveur par IP (voir functions/presentationAuth.php)
|
||||
// pour ne pas pouvoir être remis à zéro en vidant les cookies. Au bout de 4 tentatives
|
||||
// ratées, on ne redemande plus le code : on propose une connexion admin à la place.
|
||||
//
|
||||
// Une fois débloquée (code correct, ou compte admin connecté), la page se rend sans le
|
||||
// header ni le footer du site : contenu markdown en plein écran, lecture immersive.
|
||||
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once('../functions/databaseConnection.php');
|
||||
require_once('../functions/listSettings.php');
|
||||
require_once('../functions/presentationAuth.php');
|
||||
|
||||
$adminConnecte = isset($_SESSION['role']) && $_SESSION['role'] === 'admin';
|
||||
$ipHash = hashIpPresentation();
|
||||
$maxTentatives = 4;
|
||||
|
||||
// Un administrateur connecté a toujours accès, et ça lève un éventuel blocage sur son IP
|
||||
// (pratique s'il teste lui-même depuis chez lui après s'être fait bloquer en visiteur)
|
||||
if ($adminConnecte) {
|
||||
reinitialiserTentativesPresentation($ipHash);
|
||||
$_SESSION['presentation_unlocked'] = true;
|
||||
}
|
||||
|
||||
$deverrouille = $adminConnecte || (($_SESSION['presentation_unlocked'] ?? false) === true);
|
||||
$verrouille = false;
|
||||
$erreur = '';
|
||||
|
||||
if (!$deverrouille) {
|
||||
$verrouille = presentationEstVerrouillee($ipHash, $maxTentatives);
|
||||
|
||||
if (!$verrouille && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$pinSaisi = preg_replace('/\D/', '', $_POST['pin'] ?? '');
|
||||
|
||||
if (verifierPinPresentation($pinSaisi)) {
|
||||
reinitialiserTentativesPresentation($ipHash);
|
||||
// Empêche la fixation de session, comme à la connexion admin
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['presentation_unlocked'] = true;
|
||||
$deverrouille = true;
|
||||
} else {
|
||||
$etat = enregistrerEchecPresentation($ipHash, $maxTentatives);
|
||||
$verrouille = (int) $etat['attempts'] >= $maxTentatives;
|
||||
$erreur = $verrouille ? '' : 'Code incorrect. Réessaie.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tentatives restantes à afficher (pastilles), calculées après un éventuel échec ci-dessus
|
||||
$tentativesRestantes = $maxTentatives;
|
||||
$etatActuel = null;
|
||||
if (!$deverrouille) {
|
||||
$etatActuel = recupererTentativePresentation($ipHash);
|
||||
$tentativesRestantes = $maxTentatives - ($etatActuel ? (int) $etatActuel['attempts'] : 0);
|
||||
if ($tentativesRestantes < 0) {
|
||||
$tentativesRestantes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if ($deverrouille) {
|
||||
|
||||
// ---- Présentation en cours : page autonome plein écran, sans le header du site ----
|
||||
|
||||
require_once('../scripts/requireCaptcha.php');
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once(__DIR__ . '/../functions/assets.php');
|
||||
sendPageLog();
|
||||
|
||||
$markdown = recupererSetting('presentation_markdown') ?? '';
|
||||
|
||||
// Diapositives séparées par une ligne ne contenant que --- (même convention que
|
||||
// Marp/reveal.js : facile à retenir, et ça reste du markdown lisible tel quel).
|
||||
// Sans --- dans le texte, tout le contenu forme une seule diapositive : dans ce
|
||||
// cas on garde une simple page de lecture, sans flèches ni compteur inutiles.
|
||||
$morceaux = preg_split('/^[ \t]*-{3,}[ \t]*$/m', $markdown);
|
||||
$diapositives = [];
|
||||
foreach ($morceaux as $morceau) {
|
||||
$texte = trim($morceau);
|
||||
if ($texte !== '') {
|
||||
$diapositives[] = $texte;
|
||||
}
|
||||
}
|
||||
$nombreDiapositives = count($diapositives);
|
||||
$modeDiaporama = $nombreDiapositives > 1;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mode présentation - Esteban MOUSSU</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="icon" type="image/png" href="../memoji.png">
|
||||
|
||||
<!-- Applique le thème sauvegardé AVANT le chargement du CSS pour éviter un flash sombre/clair -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
if (localStorage.getItem('theme') === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- On réutilise les variables et styles de base du site (thème clair/sombre, police...) -->
|
||||
<link rel="stylesheet" href="<?php echo htmlspecialchars(urlAsset('styles/primary_css.css')); ?>">
|
||||
<link rel="stylesheet" href="<?php echo htmlspecialchars(urlAsset('styles/presentation.css')); ?>">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||
</head>
|
||||
<body class="presentation-body">
|
||||
|
||||
<!-- Barre minimale : pas de nav du site, juste un rappel du mode + sortie -->
|
||||
<div class="presentation-bar">
|
||||
<span class="presentation-bar-tag"><i class="fa-solid fa-lock-open"></i> <span>Mode présentation</span></span>
|
||||
<div class="presentation-bar-actions">
|
||||
<button class="theme-toggle" type="button" aria-label="Changer de thème"
|
||||
onclick="var h=document.documentElement;var l=h.getAttribute('data-theme')==='light';if(l){h.removeAttribute('data-theme');try{localStorage.setItem('theme','dark');}catch(e){}}else{h.setAttribute('data-theme','light');try{localStorage.setItem('theme','light');}catch(e){}}">
|
||||
<i class="fa-solid fa-sun"></i>
|
||||
<i class="fa-solid fa-moon"></i>
|
||||
</button>
|
||||
<a class="presentation-exit" href="../pages/home.php"><i class="fa-solid fa-xmark"></i> Quitter</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$modeDiaporama) { ?>
|
||||
<!-- Un seul bloc de contenu : barre de progression de lecture (s'anime au scroll dans
|
||||
les navigateurs qui supportent animation-timeline: scroll() ; reste simplement
|
||||
invisible sinon, aucune régression) -->
|
||||
<div class="presentation-progress" aria-hidden="true"></div>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($nombreDiapositives === 0) { ?>
|
||||
<main class="presentation-reader">
|
||||
<p class="presentation-empty">Contenu à venir. Ajoute du texte depuis le panel admin, section « Mode présentation ».</p>
|
||||
</main>
|
||||
<?php } elseif (!$modeDiaporama) { ?>
|
||||
<!-- Mode lecture simple : tout défile dans une seule colonne -->
|
||||
<main class="presentation-reader">
|
||||
<?php
|
||||
// Même mécanisme que sur les pages de détail (projets/xp/écoles) : le markdown
|
||||
// brut est déposé ici (échappé), puis rendu en HTML par marked.js plus bas.
|
||||
// Les images s'insèrent avec la syntaxe markdown standard : 
|
||||
echo '<div id="markdown-source" hidden>' . htmlspecialchars($diapositives[0]) . '</div>';
|
||||
echo '<div class="markdown-content presentation-markdown" id="markdown-target"><p>Chargement du contenu...</p></div>';
|
||||
?>
|
||||
</main>
|
||||
<?php } else { ?>
|
||||
<!-- Mode diaporama : une diapositive à la fois, séparées dans le champ admin par
|
||||
une ligne "---". Navigation en bas (flèches, barre de progression, compteur) -->
|
||||
<main class="presentation-deck" data-total="<?php echo $nombreDiapositives; ?>">
|
||||
<?php
|
||||
foreach ($diapositives as $index => $texteDiapositive) {
|
||||
$classeActive = $index === 0 ? ' is-active' : '';
|
||||
echo '<section class="slide' . $classeActive . '" data-slide="' . $index . '">';
|
||||
echo '<div class="slide-source" hidden>' . htmlspecialchars($texteDiapositive) . '</div>';
|
||||
echo '<div class="markdown-content presentation-markdown slide-target"><p>Chargement du contenu...</p></div>';
|
||||
echo '</section>';
|
||||
}
|
||||
?>
|
||||
</main>
|
||||
|
||||
<nav class="slide-nav" aria-label="Navigation entre les diapositives">
|
||||
<button type="button" class="slide-nav-btn slide-prev" aria-label="Diapositive précédente" disabled>
|
||||
<i class="fa-solid fa-chevron-left"></i>
|
||||
</button>
|
||||
<div class="slide-progress" aria-hidden="true">
|
||||
<div class="slide-progress-bar"></div>
|
||||
</div>
|
||||
<span class="slide-counter"><span class="slide-counter-current">1</span> / <?php echo $nombreDiapositives; ?></span>
|
||||
<button type="button" class="slide-nav-btn slide-next" aria-label="Diapositive suivante">
|
||||
<i class="fa-solid fa-chevron-right"></i>
|
||||
</button>
|
||||
</nav>
|
||||
<?php } ?>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
|
||||
<?php
|
||||
if (!$modeDiaporama) {
|
||||
// ---- Rendu simple : un seul bloc, apparition douce au fil du scroll ----
|
||||
echo <<<'JS'
|
||||
<script>
|
||||
(function () {
|
||||
var source = document.getElementById('markdown-source');
|
||||
var cible = document.getElementById('markdown-target');
|
||||
|
||||
if (source && cible) {
|
||||
var markdown = source.textContent;
|
||||
cible.innerHTML = DOMPurify.sanitize(marked.parse(markdown, { breaks: true }));
|
||||
}
|
||||
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
if (!('IntersectionObserver' in window)) return;
|
||||
|
||||
// Chaque bloc du contenu (titre, paragraphe, image...) apparaît en douceur à la lecture
|
||||
var cibles = document.querySelectorAll('.presentation-markdown > *');
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1, rootMargin: '0px 0px -60px 0px' });
|
||||
|
||||
cibles.forEach(function (el) {
|
||||
el.classList.add('reveal');
|
||||
observer.observe(el);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
JS;
|
||||
} else {
|
||||
// ---- Rendu diaporama : une diapositive affichée à la fois, navigation clavier/
|
||||
// tactile/boutons, position mémorisée dans l'URL (#slide-3) pour pouvoir
|
||||
// partager un lien direct vers une diapositive précise ----
|
||||
echo <<<'JS'
|
||||
<script>
|
||||
(function () {
|
||||
var slides = Array.prototype.slice.call(document.querySelectorAll('.slide'));
|
||||
|
||||
// Rendu du markdown de chaque diapositive (source échappée côté serveur,
|
||||
// transformée en HTML ici, comme sur les fiches projets)
|
||||
slides.forEach(function (slide) {
|
||||
var source = slide.querySelector('.slide-source');
|
||||
var cible = slide.querySelector('.slide-target');
|
||||
if (source && cible) {
|
||||
cible.innerHTML = DOMPurify.sanitize(marked.parse(source.textContent, { breaks: true }));
|
||||
}
|
||||
});
|
||||
|
||||
var index = 0;
|
||||
var deck = document.querySelector('.presentation-deck');
|
||||
var boutonPrec = document.querySelector('.slide-prev');
|
||||
var boutonSuiv = document.querySelector('.slide-next');
|
||||
var barre = document.querySelector('.slide-progress-bar');
|
||||
var compteur = document.querySelector('.slide-counter-current');
|
||||
|
||||
function afficher(nouvelIndex) {
|
||||
nouvelIndex = Math.max(0, Math.min(slides.length - 1, nouvelIndex));
|
||||
|
||||
slides[index].classList.remove('is-active');
|
||||
index = nouvelIndex;
|
||||
slides[index].classList.add('is-active');
|
||||
|
||||
boutonPrec.disabled = index === 0;
|
||||
boutonSuiv.disabled = index === slides.length - 1;
|
||||
if (barre) barre.style.width = ((index + 1) / slides.length * 100) + '%';
|
||||
if (compteur) compteur.textContent = String(index + 1);
|
||||
|
||||
history.replaceState(null, '', '#slide-' + (index + 1));
|
||||
}
|
||||
|
||||
boutonPrec.addEventListener('click', function () { afficher(index - 1); });
|
||||
boutonSuiv.addEventListener('click', function () { afficher(index + 1); });
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
// On ignore les flèches si le focus est dans un champ de texte (aucun ici,
|
||||
// mais garde-fou si jamais du contenu interactif est ajouté un jour)
|
||||
var balise = (document.activeElement || {}).tagName;
|
||||
if (balise === 'INPUT' || balise === 'TEXTAREA') return;
|
||||
|
||||
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); afficher(index + 1); }
|
||||
if (e.key === 'ArrowLeft') { afficher(index - 1); }
|
||||
});
|
||||
|
||||
// Clic sur la diapositive (comme Google Slides / Keynote) : moitié gauche de l'écran
|
||||
// = diapositive précédente, moitié droite = suivante. On laisse passer le clic sans
|
||||
// rien faire s'il vise un lien, un bouton ou la barre de navigation elle-même, pour
|
||||
// ne pas voler le clic d'un lien présent dans le contenu markdown
|
||||
deck.addEventListener('click', function (e) {
|
||||
if (e.target.closest('a, button')) return;
|
||||
afficher(e.clientX < window.innerWidth / 2 ? index - 1 : index + 1);
|
||||
});
|
||||
|
||||
// Glissement tactile (mobile) : swipe vers la gauche = diapositive suivante
|
||||
var startX = null;
|
||||
|
||||
deck.addEventListener('touchstart', function (e) {
|
||||
startX = e.touches[0].clientX;
|
||||
}, { passive: true });
|
||||
|
||||
deck.addEventListener('touchend', function (e) {
|
||||
if (startX === null) return;
|
||||
var delta = e.changedTouches[0].clientX - startX;
|
||||
if (Math.abs(delta) > 50) {
|
||||
afficher(delta < 0 ? index + 1 : index - 1);
|
||||
}
|
||||
startX = null;
|
||||
}, { passive: true });
|
||||
|
||||
// Reprend la diapositive indiquée dans l'URL au chargement (#slide-3)
|
||||
var depart = parseInt((location.hash.match(/slide-(\d+)/) || [])[1], 10);
|
||||
afficher(isNaN(depart) ? 0 : depart - 1);
|
||||
})();
|
||||
</script>
|
||||
JS;
|
||||
}
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
|
||||
} else {
|
||||
|
||||
// ---- Écran de saisie du code, ou écran "trop de tentatives" : chrome normal du site ----
|
||||
|
||||
$pageTitle = 'Mode présentation';
|
||||
require_once('../elements/header.php');
|
||||
|
||||
if ($verrouille) {
|
||||
$jusqua = ($etatActuel && !empty($etatActuel['locked_until']))
|
||||
? date('d/m/Y à H:i', strtotime($etatActuel['locked_until']))
|
||||
: '';
|
||||
?>
|
||||
<div class="presentation-lock-container">
|
||||
<div class="presentation-lock-card presentation-lock-card--blocked">
|
||||
<span class="presentation-lock-badge presentation-lock-badge--danger">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i> Accès bloqué
|
||||
</span>
|
||||
<h1>Trop de tentatives</h1>
|
||||
<p>Le code a été saisi plusieurs fois de suite sans succès. L'accès par code PIN est bloqué pour cet appareil<?php echo $jusqua !== '' ? ' jusqu\'au ' . htmlspecialchars($jusqua) : ''; ?>.</p>
|
||||
<p>Connecte-toi en administrateur pour débloquer l'accès immédiatement.</p>
|
||||
<a class="btn btn-primary presentation-lock-btn" href="../pages/login.php?redirect=presentation">
|
||||
<i class="fa-solid fa-right-to-bracket"></i> Se connecter en administrateur
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<div class="presentation-lock-container">
|
||||
<div class="presentation-lock-card">
|
||||
<span class="presentation-lock-badge"><i class="fa-solid fa-lock"></i> Accès protégé</span>
|
||||
<h1>Mode présentation</h1>
|
||||
<p>Entre le code à 9 chiffres pour accéder au contenu.</p>
|
||||
|
||||
<?php
|
||||
if ($erreur !== '') {
|
||||
echo '<div class="presentation-lock-error">' . htmlspecialchars($erreur) . '</div>';
|
||||
}
|
||||
?>
|
||||
|
||||
<form method="POST" action="" class="presentation-pin-form" novalidate>
|
||||
<div class="pin-boxes" role="group" aria-label="Code à 9 chiffres">
|
||||
<?php
|
||||
// type="password" : les chiffres s'affichent en points par défaut, comme un
|
||||
// mot de passe. Le bouton œil juste en dessous les révèle, mais seulement
|
||||
// tant qu'il reste appuyé (voir le JS plus bas)
|
||||
for ($i = 0; $i < 9; $i++) {
|
||||
echo '<input type="password" class="pin-box" inputmode="numeric" pattern="[0-9]*" maxlength="1" autocomplete="off" aria-label="Chiffre ' . ($i + 1) . ' sur 9">';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<!-- Maintenir appuyé pour voir le code, relâcher pour le remasquer -->
|
||||
<button type="button" class="pin-reveal" aria-pressed="false">
|
||||
<i class="fa-solid fa-eye"></i> <span>Maintenir pour afficher</span>
|
||||
</button>
|
||||
|
||||
<input type="hidden" name="pin" id="pin-value">
|
||||
<input type="submit" value="Valider" class="btn btn-primary presentation-pin-submit">
|
||||
</form>
|
||||
|
||||
<div class="pin-attempts">
|
||||
<span>Tentatives restantes</span>
|
||||
<div class="pin-attempts-dots">
|
||||
<?php
|
||||
for ($i = 0; $i < $maxTentatives; $i++) {
|
||||
echo '<span class="' . ($i < $tentativesRestantes ? 'dot-filled' : 'dot-empty') . '"></span>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var boxes = Array.prototype.slice.call(document.querySelectorAll('.pin-box'));
|
||||
var hidden = document.getElementById('pin-value');
|
||||
var submit = document.querySelector('.presentation-pin-submit');
|
||||
|
||||
function syncHidden() {
|
||||
var valeur = boxes.map(function (b) { return b.value; }).join('');
|
||||
hidden.value = valeur;
|
||||
submit.disabled = valeur.length !== 9;
|
||||
}
|
||||
|
||||
boxes.forEach(function (box, index) {
|
||||
box.addEventListener('input', function () {
|
||||
box.value = box.value.replace(/\D/g, '').slice(0, 1);
|
||||
if (box.value && boxes[index + 1]) {
|
||||
boxes[index + 1].focus();
|
||||
}
|
||||
syncHidden();
|
||||
});
|
||||
|
||||
box.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Backspace' && !box.value && boxes[index - 1]) {
|
||||
boxes[index - 1].focus();
|
||||
}
|
||||
});
|
||||
|
||||
box.addEventListener('paste', function (e) {
|
||||
var texte = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
|
||||
if (texte.length === 0) return;
|
||||
e.preventDefault();
|
||||
texte.slice(0, 9).split('').forEach(function (chiffre, i) {
|
||||
if (boxes[i]) boxes[i].value = chiffre;
|
||||
});
|
||||
syncHidden();
|
||||
var prochain = boxes[Math.min(texte.length, 8)];
|
||||
if (prochain) prochain.focus();
|
||||
});
|
||||
});
|
||||
|
||||
// Bouton œil : le code n'est visible QUE tant que le bouton reste appuyé. Dès qu'on
|
||||
// relâche (ou que le doigt/la souris quitte le bouton, ou que l'onglet perd le focus),
|
||||
// on repasse en points. Évite de laisser le code affiché par inadvertance à l'écran.
|
||||
var oeil = document.querySelector('.pin-reveal');
|
||||
var icone = oeil.querySelector('i');
|
||||
|
||||
function afficherCode(visible) {
|
||||
boxes.forEach(function (box) {
|
||||
box.type = visible ? 'text' : 'password';
|
||||
});
|
||||
oeil.setAttribute('aria-pressed', visible ? 'true' : 'false');
|
||||
oeil.classList.toggle('is-revealing', visible);
|
||||
icone.className = visible ? 'fa-solid fa-eye-slash' : 'fa-solid fa-eye';
|
||||
}
|
||||
|
||||
['mousedown', 'touchstart'].forEach(function (evt) {
|
||||
oeil.addEventListener(evt, function (e) {
|
||||
// Empêche le navigateur mobile de déclencher aussi un mousedown en double,
|
||||
// et évite de voler le focus au champ en cours de saisie
|
||||
e.preventDefault();
|
||||
afficherCode(true);
|
||||
});
|
||||
});
|
||||
|
||||
['mouseup', 'mouseleave', 'touchend', 'touchcancel', 'blur'].forEach(function (evt) {
|
||||
oeil.addEventListener(evt, function () {
|
||||
afficherCode(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Accessibilité clavier : Espace ou Entrée maintenus révèlent le code de la même façon
|
||||
oeil.addEventListener('keydown', function (e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
afficherCode(true);
|
||||
}
|
||||
});
|
||||
|
||||
oeil.addEventListener('keyup', function (e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
afficherCode(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Garde-fou : si l'utilisateur change d'onglet en gardant le bouton appuyé, le
|
||||
// relâchement n'est jamais reçu par la page, donc on remasque de force
|
||||
window.addEventListener('blur', function () {
|
||||
afficherCode(false);
|
||||
});
|
||||
|
||||
syncHidden();
|
||||
if (boxes[0]) boxes[0].focus();
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
require_once('../elements/footer.php');
|
||||
}
|
||||
Reference in new issue
Block a user