Files
Forge-Engine/static/pjax.js
T
williamandClaude Sonnet 5 b07b231a61
Build and deploy / test-python (push) Successful in 12m24s
Build and deploy / test-js (push) Successful in 1m9s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Ajoute l'assistant IA "Ruby" (Claude + Scenario) et corrige plusieurs bugs de la scène
Intègre un chat IA capable de manipuler la scène via les mêmes fonctions
que l'éditeur manuel (objets, variables, déclencheurs, images générées),
avec conversations multiples par écran façon Claude. Corrige au passage
le rafraîchissement pjax hors-ordre, l'onglet IA/déclencheurs vide après
sélection d'un objet, la comparaison de booléens dans les conditions, et
le blocage du glisser-déposer hors du cadre caméra après un redimensionnement
de fond par l'IA.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 19:40:31 +02:00

163 lines
6.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) {
// Bug corrigé : un <script src="..."> créé via document.createElement
// est ASYNC PAR DÉFAUT (contrairement à un <script> du HTML source,
// exécuté dans l'ordre du document) — plusieurs scripts de page
// (variables globales GAME_SLUG/SCREEN_ID posées en inline, puis
// scene-editor.js/trigger-editor.js/ia-chat.js qui en dépendent
// immédiatement) pouvaient donc s'exécuter dans le désordre après une
// navigation pjax, selon l'ordre d'arrivée réseau — jamais après un
// vrai rechargement de page (où l'ordre du document est respecté).
// Symptôme observé : l'onglet "Générer avec l'IA" se retrouvait vide
// après le moindre changement ailleurs (variable, déclencheur), un
// rechargement complet le "réparant" — en réalité ia-chat.js avait dû
// s'exécuter avant que GAME_SLUG/SCREEN_ID existent. `async = false`
// restaure l'exécution dans l'ordre du document pour CHAQUE script
// externe, exactement comme à l'analyse initiale du HTML.
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);
}
if (fresh.src) fresh.async = false;
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);
});
})();