/* * pjax.js — navigation sans rechargement de page complet. * * Principe : le serveur ne change pas (mêmes routes Flask, mêmes pages * générées en Jinja) — on intercepte simplement chaque clic sur un lien * interne et chaque soumission de formulaire interne, on va chercher la * page suivante nous-mêmes en arrière-plan (fetch), et on ne remplace que * , la classe du <body>, l'en-tête (fil d'Ariane) et le contenu * principal (<main>) — au lieu de laisser le navigateur tout recharger. * L'URL affichée et l'historique (bouton "précédent") restent corrects * grâce à history.pushState/popstate. * * Les scripts propres à chaque page (dans le bloc "content", donc réinjectés * à chaque changement de page) ne s'exécutent pas automatiquement quand on * les insère via innerHTML — on les rejoue explicitement à chaque * navigation (voir runScriptsIn ci-dessous). C'est pour ça que ces scripts * ne doivent déclarer aucune variable globale avec `let`/`const` au premier * niveau (une seconde exécution lèverait "already declared") — seuls `var` * et les déclarations de fonction sont sûrs à rejouer, ce qui est déjà le * cas de tous les scripts actuels du moteur. * * Pour désactiver ce comportement sur un lien ou un formulaire précis (cas * rare), ajouter l'attribut data-no-pjax. */ (function () { "use strict"; function isInternalUrl(url) { return url.origin === location.origin; } function runScriptsIn(container) { var scripts = container.querySelectorAll("script"); scripts.forEach(function (old) { var fresh = document.createElement("script"); for (var i = 0; i < old.attributes.length; i++) { var attr = old.attributes[i]; fresh.setAttribute(attr.name, attr.value); } fresh.textContent = old.textContent; old.replaceWith(fresh); }); } function mergeHeadAssets(newHead) { // N'ajoute que ce qui manque encore (ex. la police Google Fonts propre // à l'éditeur d'écran) — ne retire jamais rien, un lien en trop est sans // conséquence alors que le retirer pourrait faire clignoter le rendu. var current = document.head; var currentHrefs = Array.prototype.map.call( current.querySelectorAll("link[href]"), function (l) { return l.getAttribute("href"); } ); newHead.querySelectorAll("link[rel='stylesheet']").forEach(function (link) { var href = link.getAttribute("href"); if (href && currentHrefs.indexOf(href) === -1) { current.appendChild(link.cloneNode(true)); } }); } function swapDocument(html, url, push) { var doc = new DOMParser().parseFromString(html, "text/html"); var newMain = doc.querySelector("main.content"); var currentMain = document.querySelector("main.content"); if (!newMain || !currentMain) { // Page inattendue (pas une page du moteur, ex. redirection externe) : // par sécurité, on fait une vraie navigation plutôt que d'afficher un // contenu à moitié cohérent. location.href = url; return; } document.title = doc.title; document.body.className = doc.body.className; currentMain.className = newMain.className; currentMain.innerHTML = newMain.innerHTML; var newHeader = doc.querySelector("header.topbar"); var currentHeader = document.querySelector("header.topbar"); if (newHeader && currentHeader) { currentHeader.innerHTML = newHeader.innerHTML; } mergeHeadAssets(doc.head); runScriptsIn(currentMain); if (push) { history.pushState({ pjax: true }, "", url); } window.scrollTo(0, 0); document.dispatchEvent(new CustomEvent("pjax:navigated", { detail: { url: url } })); } function navigate(url, push, fetchOpts) { fetch(url, Object.assign({ headers: { "X-Requested-With": "pjax" } }, fetchOpts || {})) .then(function (resp) { return resp.text().then(function (html) { return { html: html, finalUrl: resp.url || url }; }); }) .then(function (result) { swapDocument(result.html, result.finalUrl, push); }) .catch(function () { // Échec réseau ou réponse inexploitable : on retombe sur une vraie // navigation plutôt que de laisser l'utilisateur bloqué. location.href = url; }); } document.addEventListener("click", function (e) { if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; var a = e.target.closest("a"); if (!a || !a.href) return; if (a.target === "_blank" || a.hasAttribute("download") || a.hasAttribute("data-no-pjax")) return; if (a.getAttribute("href") && a.getAttribute("href").charAt(0) === "#") return; var url = new URL(a.href, location.href); if (!isInternalUrl(url)) return; if (url.href === location.href.split("#")[0]) return; // lien vers soi-même : rien à refaire e.preventDefault(); navigate(url.href, true); }); document.addEventListener("submit", function (e) { if (e.defaultPrevented) return; // ex: confirm() annulé par l'utilisateur var form = e.target; if (!(form instanceof HTMLFormElement)) return; if (form.target === "_blank" || form.hasAttribute("data-no-pjax")) return; var url = new URL(form.getAttribute("action") || location.href, location.href); if (!isInternalUrl(url)) return; var method = (form.getAttribute("method") || "GET").toUpperCase(); e.preventDefault(); if (method === "GET") { url.search = new URLSearchParams(new FormData(form)).toString(); navigate(url.href, true); } else { navigate(url.href, true, { method: "POST", body: new FormData(form) }); } }); window.addEventListener("popstate", function () { navigate(location.href, false); }); })();