/*
* 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 , #pageChrome (topbar + barre de navigation
* du jeu, voir base.html) et le contenu principal () — 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 newChrome = doc.querySelector("#pageChrome");
var currentChrome = document.querySelector("#pageChrome");
if (newChrome && currentChrome) {
currentChrome.innerHTML = newChrome.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);
});
})();