Files
Forge-Engine/static/pjax.js
T
williamandClaude Sonnet 5 b8dfdcb205 Corrige la nav pjax, panneau déplaçable/redimensionnable pour éditer un objet
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>
2026-08-27 11:56:49 +02:00

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);
});
})();