Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
+168
-167
@@ -17,15 +17,15 @@
|
||||
// statique jamais passé par Jinja, ce n'est plus nécessaire, mais la
|
||||
// construction est conservée pour rester visuellement cohérente avec le
|
||||
// reste du moteur et éviter toute confusion en relisant le code.
|
||||
var PLACEHOLDER_OPEN = '{' + '{';
|
||||
var PLACEHOLDER_CLOSE = '}' + '}';
|
||||
const PLACEHOLDER_OPEN = '{' + '{';
|
||||
const PLACEHOLDER_CLOSE = '}' + '}';
|
||||
|
||||
function applyOpenRowBindings() {
|
||||
if (!window.currentOpenRow) return;
|
||||
const rows = gameData.data[String(window.currentOpenRow.definitionId)] || [];
|
||||
const row = rows.find(r => r.id === window.currentOpenRow.rowId);
|
||||
const screenData = screensData.find(s => s.id === window.currentScreenId);
|
||||
const screenDiv = document.querySelector('.playScreen[data-screen-id="' + window.currentScreenId + '"]');
|
||||
const row = rows.find((r) => r.id === window.currentOpenRow.rowId);
|
||||
const screenData = screensData.find((s) => s.id === window.currentScreenId);
|
||||
const screenDiv = document.querySelector(`.playScreen[data-screen-id="${window.currentScreenId}"]`);
|
||||
if (!row || !screenData || !screenDiv) return;
|
||||
|
||||
// Cible directement, à N'IMPORTE QUEL niveau d'imbrication, le ou les
|
||||
@@ -51,7 +51,7 @@ function applyOpenRowBindings() {
|
||||
// le contient), ce filtre ne gardait en pratique QUE l'ancêtre racine
|
||||
// de l'écran, annulant complètement l'effet recherché par ce correctif.
|
||||
const byId = {};
|
||||
screenData.elements.forEach(function(e){ byId[e.id] = e; });
|
||||
screenData.elements.forEach((e) => { byId[e.id] = e; });
|
||||
function hasUnresolvedPlaceholder(e) {
|
||||
return !!(e.rendered_html && e.rendered_html.indexOf(PLACEHOLDER_OPEN) !== -1);
|
||||
}
|
||||
@@ -64,9 +64,9 @@ function applyOpenRowBindings() {
|
||||
return false;
|
||||
}
|
||||
const candidates = screenData.elements.filter(hasUnresolvedPlaceholder);
|
||||
const targets = candidates.filter(function(e) {
|
||||
return !candidates.some(function(other) { return other.id !== e.id && isDescendantOf(other, e.id); });
|
||||
});
|
||||
const targets = candidates.filter(
|
||||
(e) => !candidates.some((other) => other.id !== e.id && isDescendantOf(other, e.id)),
|
||||
);
|
||||
|
||||
// Résout {{champ}} EN PLACE dans le DOM déjà affiché, sans jamais
|
||||
// détruire/recréer le moindre nœud — un peu comme un diff React ciblé,
|
||||
@@ -85,7 +85,7 @@ function applyOpenRowBindings() {
|
||||
// élément", donnant l'impression trompeuse d'un rechargement de page).
|
||||
// Avec la résolution en place, plus aucun nœud du sous-arbre ne bouge :
|
||||
// rien à sauvegarder ni à restaurer.
|
||||
const placeholderRe = new RegExp(PLACEHOLDER_OPEN + '([^{}]+)' + PLACEHOLDER_CLOSE, 'g');
|
||||
const placeholderRe = new RegExp(`${PLACEHOLDER_OPEN}([^{}]+)${PLACEHOLDER_CLOSE}`, 'g');
|
||||
|
||||
function resolveTextNodes(root) {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
@@ -94,11 +94,12 @@ function applyOpenRowBindings() {
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.nodeValue.indexOf(PLACEHOLDER_OPEN) !== -1) toSplit.push(node);
|
||||
}
|
||||
toSplit.forEach(function(textNode) {
|
||||
toSplit.forEach((textNode) => {
|
||||
const raw = textNode.nodeValue;
|
||||
placeholderRe.lastIndex = 0;
|
||||
const frag = document.createDocumentFragment();
|
||||
let lastIndex = 0, match;
|
||||
let lastIndex = 0; let
|
||||
match;
|
||||
while ((match = placeholderRe.exec(raw))) {
|
||||
if (match.index > lastIndex) frag.appendChild(document.createTextNode(raw.slice(lastIndex, match.index)));
|
||||
const span = document.createElement('span');
|
||||
@@ -113,31 +114,31 @@ function applyOpenRowBindings() {
|
||||
}
|
||||
|
||||
function updateTextSlots(root) {
|
||||
root.querySelectorAll('[data-bind-field]').forEach(function(slot) {
|
||||
root.querySelectorAll('[data-bind-field]').forEach((slot) => {
|
||||
const value = row[slot.dataset.bindField] == null ? '' : String(row[slot.dataset.bindField]);
|
||||
if (slot.textContent !== value) slot.textContent = value;
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAttributes(root) {
|
||||
[root].concat(Array.from(root.querySelectorAll('*'))).forEach(function(node) {
|
||||
Array.from(node.attributes).forEach(function(attr) {
|
||||
[root].concat(Array.from(root.querySelectorAll('*'))).forEach((node) => {
|
||||
Array.from(node.attributes).forEach((attr) => {
|
||||
if (attr.name.indexOf('data-bind-attr-') === 0) return; // gabarit déjà mémorisé, pas un attribut à lire
|
||||
const bindKey = 'data-bind-attr-' + attr.name;
|
||||
const template = node.hasAttribute(bindKey) ? node.getAttribute(bindKey) : (attr.value.indexOf(PLACEHOLDER_OPEN) !== -1 ? attr.value : null);
|
||||
const bindKey = `data-bind-attr-${attr.name}`;
|
||||
const template = node.hasAttribute(bindKey)
|
||||
? node.getAttribute(bindKey)
|
||||
: (attr.value.indexOf(PLACEHOLDER_OPEN) !== -1 ? attr.value : null);
|
||||
if (template === null) return;
|
||||
if (!node.hasAttribute(bindKey)) node.setAttribute(bindKey, template);
|
||||
placeholderRe.lastIndex = 0;
|
||||
const resolved = template.replace(placeholderRe, function(_, fieldName) {
|
||||
return row[fieldName] == null ? '' : String(row[fieldName]);
|
||||
});
|
||||
const resolved = template.replace(placeholderRe, (_, fieldName) => (row[fieldName] == null ? '' : String(row[fieldName])));
|
||||
if (attr.value !== resolved) node.setAttribute(attr.name, resolved);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
targets.forEach(function(el) {
|
||||
const target = screenDiv.querySelector('[data-element-id="' + el.id + '"]');
|
||||
targets.forEach((el) => {
|
||||
const target = screenDiv.querySelector(`[data-element-id="${el.id}"]`);
|
||||
if (!target) return;
|
||||
if (target.querySelector('[data-bind-field]')) {
|
||||
updateTextSlots(target);
|
||||
@@ -167,156 +168,156 @@ function refreshRuntimeData() {
|
||||
// en ligne comme hors ligne.
|
||||
if (window.FORGE_OFFLINE) {
|
||||
forgeRecomputeFullPayloadOffline(gameData);
|
||||
return Promise.resolve().then(function () { return _forgeApplyFreshRuntimeData(gameData); });
|
||||
return Promise.resolve().then(() => _forgeApplyFreshRuntimeData(gameData));
|
||||
}
|
||||
return fetch(FORGE_PLAY_URLS.runtimePayload)
|
||||
.then(r => r.json())
|
||||
.then((r) => r.json())
|
||||
.then(_forgeApplyFreshRuntimeData);
|
||||
}
|
||||
|
||||
function _forgeApplyFreshRuntimeData(newData) {
|
||||
gameData = newData;
|
||||
screensData = gameData.screens;
|
||||
const screenData = screensData.find(s => s.id === window.currentScreenId);
|
||||
const screenDiv = document.querySelector('.playScreen[data-screen-id="' + window.currentScreenId + '"]');
|
||||
// Le bloc ci-dessous (régénération du contenu de l'écran courant)
|
||||
// est sauté si l'écran n'est pas trouvé, mais bindClicks() et les
|
||||
// autres réattachements juste après ne doivent JAMAIS dépendre de
|
||||
// cette condition : un retour anticipé ici a déjà laissé, dans la
|
||||
// pratique, un Répéteur fraîchement régénéré sans AUCUN gestionnaire
|
||||
// de clic pour le reste de la partie (plus aucune ligne ne
|
||||
// répondait, silencieusement, sans erreur) — ces fonctions sont sans
|
||||
// risque à rappeler même quand rien n'a changé.
|
||||
if (screenData && screenDiv) {
|
||||
// try/catch : une erreur inattendue pendant la régénération (ex.
|
||||
// structure de données imprévue) ne doit jamais empêcher les
|
||||
// réattachements ci-dessous de s'exécuter — sinon le Répéteur (ou
|
||||
// tout autre élément cliquable) reste silencieusement inerte pour
|
||||
// le reste de la partie, sans le moindre message d'erreur visible.
|
||||
try {
|
||||
// "repeaterItem" (liste de données), "jaugeBar" (2.1 — jauge liée
|
||||
// à une donnée), "visibilityGated" (condition de visibilité) et
|
||||
// "dataBound" ("Donnée liée" d'un Texte/Titre — voir
|
||||
// render_element_html.py) sont les types d'éléments dont le
|
||||
// contenu/l'affichage dépend de la donnée elle-même plutôt que
|
||||
// d'une action "Modifier un élément" posée à la main dans le
|
||||
// graphe — eux seuls doivent être régénérés après un changement
|
||||
// de donnée ou de variable.
|
||||
//
|
||||
// On cible ces éléments à N'IMPORTE QUEL niveau d'imbrication
|
||||
// (pas seulement le premier niveau de l'écran), via leur
|
||||
// data-element-id — disponible sur chaque élément rendu, pas
|
||||
// seulement les éléments de premier niveau. Avant ce correctif,
|
||||
// un Répéteur niché dans un conteneur (comme ici) n'était JAMAIS
|
||||
// du premier niveau, donc c'est son ANCÊTRE de premier niveau
|
||||
// qui portait le marqueur (le Répéteur imbriqué à l'intérieur)
|
||||
// et se retrouvait régénéré en entier à sa place — potentiellement
|
||||
// l'écran entier (jauges, onglets... compris) s'il n'y avait
|
||||
// qu'un seul gros conteneur racine.
|
||||
//
|
||||
// Parmi les éléments marqués, on ne garde que les plus
|
||||
// SPÉCIFIQUES (les plus profonds) : un ancêtre est presque
|
||||
// toujours "marqué" lui aussi dès qu'un descendant l'est, puisque
|
||||
// son rendered_html contient celui de ses descendants — le
|
||||
// garder EN PLUS régénérerait tout son sous-arbre pour rien.
|
||||
// (Une version antérieure de ce filtre vérifiait l'inverse —
|
||||
// exclure un élément si un ANCÊTRE est marqué — ce qui ne
|
||||
// gardait en pratique que l'ancêtre racine de l'écran et
|
||||
// annulait complètement l'effet recherché.)
|
||||
const byId = {};
|
||||
screenData.elements.forEach(function(e){ byId[e.id] = e; });
|
||||
function hasMarker(e) {
|
||||
return !!(e.rendered_html && (
|
||||
e.rendered_html.indexOf('repeaterItem') !== -1 ||
|
||||
e.rendered_html.indexOf('jaugeBar') !== -1 ||
|
||||
e.rendered_html.indexOf('visibilityGated') !== -1 ||
|
||||
e.rendered_html.indexOf('dataBound') !== -1
|
||||
));
|
||||
}
|
||||
function isDescendantOf(e, ancestorId) {
|
||||
let p = e.parent_id ? byId[e.parent_id] : null;
|
||||
while (p) {
|
||||
if (p.id === ancestorId) return true;
|
||||
p = p.parent_id ? byId[p.parent_id] : null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// "visibilityGated"/"dataBound" sont posés comme un COMMENTAIRE
|
||||
// HTML juste après la balise concernée (voir _mark() dans
|
||||
// render_element_html.py) — jamais comme une classe CSS, à la
|
||||
// différence de "repeaterItem"/"jaugeBar" (qui, eux, désignent
|
||||
// un widget à régénérer EN BLOC, lignes/valeur comprises, un
|
||||
// comportement déjà correct pour ces deux-là). Repérer ce
|
||||
// commentaire permet de retrouver, à l'intérieur d'un fragment
|
||||
// fraîchement rendu, l'élément PRÉCIS qui a changé — même s'il
|
||||
// est niché dans un exemplaire d'élément de jeu réutilisable
|
||||
// (ex. un Texte "Donnée liée" à l'intérieur d'une boîte de
|
||||
// dialogue), où lui seul porte l'attribut data-element-id qui
|
||||
// l'identifie, son marqueur ne remontant jusqu'au SEUL élément
|
||||
// de premier niveau existant (l'exemplaire) que parce que son
|
||||
// HTML est imbriqué dans le sien.
|
||||
const COMMENT_MARKERS = ['visibilityGated', 'dataBound'];
|
||||
function findCommentMarkedDescendants(root) {
|
||||
const found = [];
|
||||
root.querySelectorAll('[data-element-id]').forEach(function(node) {
|
||||
const sib = node.nextSibling;
|
||||
const hasCommentMarker = !!(sib && sib.nodeType === Node.COMMENT_NODE && COMMENT_MARKERS.indexOf(sib.data) !== -1);
|
||||
// Un élément actuellement CACHÉ par une condition de
|
||||
// visibilité (voir render_element_html.py) n'a pas de
|
||||
// commentaire marqueur à sa suite — sa balise porte
|
||||
// directement la classe "visibilityGated" à la place. Sans
|
||||
// ce second cas, un élément qui passe de VISIBLE à CACHÉ
|
||||
// n'était jamais retrouvé ici (seul le sens caché->visible
|
||||
// l'était, via le commentaire) : son ancien nœud visible
|
||||
// restait donc affiché indéfiniment à côté du nouveau,
|
||||
// maintenant visible, qui le remplace normalement (ex. une
|
||||
// icône "enveloppe fermée" jamais masquée en même temps que
|
||||
// "enveloppe ouverte" qui apparaît après avoir cliqué).
|
||||
const isHiddenPlaceholder = node.classList.contains('visibilityGated');
|
||||
if (hasCommentMarker || isHiddenPlaceholder) {
|
||||
found.push(node);
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
const markedCandidates = screenData.elements.filter(hasMarker);
|
||||
markedCandidates.filter(function(e) {
|
||||
return !markedCandidates.some(function(other) { return other.id !== e.id && isDescendantOf(other, e.id); });
|
||||
}).forEach(function(el) {
|
||||
const target = screenDiv.querySelector('[data-element-id="' + el.id + '"]');
|
||||
if (!target) return;
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = el.rendered_html;
|
||||
const newNode = container.firstElementChild;
|
||||
if (!newNode) return;
|
||||
const innerMarked = findCommentMarkedDescendants(newNode);
|
||||
if (innerMarked.length) {
|
||||
// Des éléments plus précis existent À L'INTÉRIEUR (ex. un
|
||||
// Texte "Donnée liée" posé dans un élément de jeu
|
||||
// réutilisable) : on ne patche QUE ceux-là, en préservant
|
||||
// tout le reste — une boîte de dialogue ne doit ni
|
||||
// disparaître ni réapparaître juste parce qu'un texte à
|
||||
// l'intérieur a changé.
|
||||
innerMarked.forEach(function(innerNode) {
|
||||
const innerId = innerNode.getAttribute('data-element-id');
|
||||
const liveInner = target.querySelector('[data-element-id="' + innerId + '"]');
|
||||
if (liveInner) liveInner.replaceWith(innerNode);
|
||||
});
|
||||
} else {
|
||||
target.replaceWith(newNode);
|
||||
}
|
||||
});
|
||||
applyOpenRowBindings();
|
||||
} catch (err) {
|
||||
console.error('refreshRuntimeData: erreur pendant la régénération de l\'écran', err);
|
||||
gameData = newData;
|
||||
screensData = gameData.screens;
|
||||
const screenData = screensData.find((s) => s.id === window.currentScreenId);
|
||||
const screenDiv = document.querySelector(`.playScreen[data-screen-id="${window.currentScreenId}"]`);
|
||||
// Le bloc ci-dessous (régénération du contenu de l'écran courant)
|
||||
// est sauté si l'écran n'est pas trouvé, mais bindClicks() et les
|
||||
// autres réattachements juste après ne doivent JAMAIS dépendre de
|
||||
// cette condition : un retour anticipé ici a déjà laissé, dans la
|
||||
// pratique, un Répéteur fraîchement régénéré sans AUCUN gestionnaire
|
||||
// de clic pour le reste de la partie (plus aucune ligne ne
|
||||
// répondait, silencieusement, sans erreur) — ces fonctions sont sans
|
||||
// risque à rappeler même quand rien n'a changé.
|
||||
if (screenData && screenDiv) {
|
||||
// try/catch : une erreur inattendue pendant la régénération (ex.
|
||||
// structure de données imprévue) ne doit jamais empêcher les
|
||||
// réattachements ci-dessous de s'exécuter — sinon le Répéteur (ou
|
||||
// tout autre élément cliquable) reste silencieusement inerte pour
|
||||
// le reste de la partie, sans le moindre message d'erreur visible.
|
||||
try {
|
||||
// "repeaterItem" (liste de données), "jaugeBar" (2.1 — jauge liée
|
||||
// à une donnée), "visibilityGated" (condition de visibilité) et
|
||||
// "dataBound" ("Donnée liée" d'un Texte/Titre — voir
|
||||
// render_element_html.py) sont les types d'éléments dont le
|
||||
// contenu/l'affichage dépend de la donnée elle-même plutôt que
|
||||
// d'une action "Modifier un élément" posée à la main dans le
|
||||
// graphe — eux seuls doivent être régénérés après un changement
|
||||
// de donnée ou de variable.
|
||||
//
|
||||
// On cible ces éléments à N'IMPORTE QUEL niveau d'imbrication
|
||||
// (pas seulement le premier niveau de l'écran), via leur
|
||||
// data-element-id — disponible sur chaque élément rendu, pas
|
||||
// seulement les éléments de premier niveau. Avant ce correctif,
|
||||
// un Répéteur niché dans un conteneur (comme ici) n'était JAMAIS
|
||||
// du premier niveau, donc c'est son ANCÊTRE de premier niveau
|
||||
// qui portait le marqueur (le Répéteur imbriqué à l'intérieur)
|
||||
// et se retrouvait régénéré en entier à sa place — potentiellement
|
||||
// l'écran entier (jauges, onglets... compris) s'il n'y avait
|
||||
// qu'un seul gros conteneur racine.
|
||||
//
|
||||
// Parmi les éléments marqués, on ne garde que les plus
|
||||
// SPÉCIFIQUES (les plus profonds) : un ancêtre est presque
|
||||
// toujours "marqué" lui aussi dès qu'un descendant l'est, puisque
|
||||
// son rendered_html contient celui de ses descendants — le
|
||||
// garder EN PLUS régénérerait tout son sous-arbre pour rien.
|
||||
// (Une version antérieure de ce filtre vérifiait l'inverse —
|
||||
// exclure un élément si un ANCÊTRE est marqué — ce qui ne
|
||||
// gardait en pratique que l'ancêtre racine de l'écran et
|
||||
// annulait complètement l'effet recherché.)
|
||||
const byId = {};
|
||||
screenData.elements.forEach((e) => { byId[e.id] = e; });
|
||||
const hasMarker = (e) => !!(e.rendered_html && (
|
||||
e.rendered_html.indexOf('repeaterItem') !== -1
|
||||
|| e.rendered_html.indexOf('jaugeBar') !== -1
|
||||
|| e.rendered_html.indexOf('visibilityGated') !== -1
|
||||
|| e.rendered_html.indexOf('dataBound') !== -1
|
||||
));
|
||||
const isDescendantOf = (e, ancestorId) => {
|
||||
let p = e.parent_id ? byId[e.parent_id] : null;
|
||||
while (p) {
|
||||
if (p.id === ancestorId) return true;
|
||||
p = p.parent_id ? byId[p.parent_id] : null;
|
||||
}
|
||||
}
|
||||
bindClicks();
|
||||
bindHoverTexts();
|
||||
bindHoverTriggers();
|
||||
// Réévalue aussi les nœuds "À l'affichage de l'écran" de l'écran où
|
||||
// l'on se trouve déjà : une donnée vient de changer (ex: un outil
|
||||
// qu'on vient de débloquer), donc l'affichage doit se mettre à jour
|
||||
// tout de suite, sans attendre un aller-retour d'écran.
|
||||
runScreenShowTriggers(window.currentScreenId);
|
||||
return false;
|
||||
};
|
||||
// "visibilityGated"/"dataBound" sont posés comme un COMMENTAIRE
|
||||
// HTML juste après la balise concernée (voir _mark() dans
|
||||
// render_element_html.py) — jamais comme une classe CSS, à la
|
||||
// différence de "repeaterItem"/"jaugeBar" (qui, eux, désignent
|
||||
// un widget à régénérer EN BLOC, lignes/valeur comprises, un
|
||||
// comportement déjà correct pour ces deux-là). Repérer ce
|
||||
// commentaire permet de retrouver, à l'intérieur d'un fragment
|
||||
// fraîchement rendu, l'élément PRÉCIS qui a changé — même s'il
|
||||
// est niché dans un exemplaire d'élément de jeu réutilisable
|
||||
// (ex. un Texte "Donnée liée" à l'intérieur d'une boîte de
|
||||
// dialogue), où lui seul porte l'attribut data-element-id qui
|
||||
// l'identifie, son marqueur ne remontant jusqu'au SEUL élément
|
||||
// de premier niveau existant (l'exemplaire) que parce que son
|
||||
// HTML est imbriqué dans le sien.
|
||||
const COMMENT_MARKERS = ['visibilityGated', 'dataBound'];
|
||||
const findCommentMarkedDescendants = (root) => {
|
||||
const found = [];
|
||||
root.querySelectorAll('[data-element-id]').forEach((node) => {
|
||||
const sib = node.nextSibling;
|
||||
const hasCommentMarker = !!(
|
||||
sib && sib.nodeType === Node.COMMENT_NODE && COMMENT_MARKERS.indexOf(sib.data) !== -1
|
||||
);
|
||||
// Un élément actuellement CACHÉ par une condition de
|
||||
// visibilité (voir render_element_html.py) n'a pas de
|
||||
// commentaire marqueur à sa suite — sa balise porte
|
||||
// directement la classe "visibilityGated" à la place. Sans
|
||||
// ce second cas, un élément qui passe de VISIBLE à CACHÉ
|
||||
// n'était jamais retrouvé ici (seul le sens caché->visible
|
||||
// l'était, via le commentaire) : son ancien nœud visible
|
||||
// restait donc affiché indéfiniment à côté du nouveau,
|
||||
// maintenant visible, qui le remplace normalement (ex. une
|
||||
// icône "enveloppe fermée" jamais masquée en même temps que
|
||||
// "enveloppe ouverte" qui apparaît après avoir cliqué).
|
||||
const isHiddenPlaceholder = node.classList.contains('visibilityGated');
|
||||
if (hasCommentMarker || isHiddenPlaceholder) {
|
||||
found.push(node);
|
||||
}
|
||||
});
|
||||
return found;
|
||||
};
|
||||
const markedCandidates = screenData.elements.filter(hasMarker);
|
||||
markedCandidates
|
||||
.filter((e) => !markedCandidates.some((other) => other.id !== e.id && isDescendantOf(other, e.id)))
|
||||
.forEach((el) => {
|
||||
const target = screenDiv.querySelector(`[data-element-id="${el.id}"]`);
|
||||
if (!target) return;
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = el.rendered_html;
|
||||
const newNode = container.firstElementChild;
|
||||
if (!newNode) return;
|
||||
const innerMarked = findCommentMarkedDescendants(newNode);
|
||||
if (innerMarked.length) {
|
||||
// Des éléments plus précis existent À L'INTÉRIEUR (ex. un
|
||||
// Texte "Donnée liée" posé dans un élément de jeu
|
||||
// réutilisable) : on ne patche QUE ceux-là, en préservant
|
||||
// tout le reste — une boîte de dialogue ne doit ni
|
||||
// disparaître ni réapparaître juste parce qu'un texte à
|
||||
// l'intérieur a changé.
|
||||
innerMarked.forEach((innerNode) => {
|
||||
const innerId = innerNode.getAttribute('data-element-id');
|
||||
const liveInner = target.querySelector(`[data-element-id="${innerId}"]`);
|
||||
if (liveInner) liveInner.replaceWith(innerNode);
|
||||
});
|
||||
} else {
|
||||
target.replaceWith(newNode);
|
||||
}
|
||||
});
|
||||
applyOpenRowBindings();
|
||||
} catch (err) {
|
||||
console.error('refreshRuntimeData: erreur pendant la régénération de l\'écran', err);
|
||||
}
|
||||
}
|
||||
bindClicks();
|
||||
bindHoverTexts();
|
||||
bindHoverTriggers();
|
||||
// Réévalue aussi les nœuds "À l'affichage de l'écran" de l'écran où
|
||||
// l'on se trouve déjà : une donnée vient de changer (ex: un outil
|
||||
// qu'on vient de débloquer), donc l'affichage doit se mettre à jour
|
||||
// tout de suite, sans attendre un aller-retour d'écran.
|
||||
runScreenShowTriggers(window.currentScreenId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user