1) Bug pjax trouvé : swapDocument() cherchait "header.topbar", qui n'a jamais existé (c'est un <nav>, pas un <header>) — la barre de navigation du jeu (ajoutée après pjax.js) n'était donc jamais mise à jour pendant une navigation pjax : absente en arrivant sur un jeu sans Ctrl+F5, et inversement laissée en place en revenant sur l'accueil où elle n'a rien à faire. Fix : tout ce qui est hors <main> mais doit changer d'une page à l'autre (topbar + barre du jeu) est regroupé dans un nouveau conteneur stable #pageChrome, que pjax.js remplace en bloc — plus fiable qu'un sélecteur qui ne correspondait à rien. CSS (flex:0 0 auto des layouts plein-écran) mis à jour en conséquence. 2) Renommages demandés : onglet/panneau "Écrans du jeu" -> "Écrants", "Éléments de jeu" -> "Templates" (tab, titre de panneau, bouton "+ Créer un template", état vide, infobulle, confirmation de suppression). 3) Le crayon ✏️ sur une ligne d'objet ouvre désormais un panneau déplaçable ET redimensionnable (nouveau coin de redimensionnement générique, .floatPanelResizeHandle) au lieu de naviguer vers object_edit.html (retirée) — un panneau par objet, pré-rendu et caché par défaut. Reprend telles quelles les fonctionnalités de l'ancienne page : renommer l'objet, ajouter un champ (ligne compacte), modifier/supprimer un champ existant (tableau dense déjà repris pour "Nouvel objet"), supprimer l'objet. Les routes de champs (object_field_ add/edit/delete) et object_edit lui-même redirigent maintenant vers le tableau de bord avec ?edit=<id>, pour rouvrir automatiquement le bon panneau après l'action plutôt que de le fermer silencieusement. makeFloatPanelDraggable()/makeFloatPanelResizable() généralisées pour être partagées entre "Nouvel objet" et les panneaux d'édition, plutôt que du code dupliqué par panneau. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
148 lines
5.8 KiB
JavaScript
148 lines
5.8 KiB
JavaScript
/*
|
|
* 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>, #pageChrome (topbar + barre de navigation
|
|
* du jeu, voir base.html) 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 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);
|
|
});
|
|
})();
|