first commit
This commit is contained in:
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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
|
||||
* <title>, 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);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user