diff --git a/templates/play.html b/templates/play.html
index 356428c3..8b65705f 100644
--- a/templates/play.html
+++ b/templates/play.html
@@ -339,30 +339,63 @@
const screenData = screensData.find(s => s.id === window.currentScreenId);
const screenDiv = document.querySelector('.playScreen[data-screen-id="' + window.currentScreenId + '"]');
if (!row || !screenData || !screenDiv) return;
- screenData.elements.filter(function(e){ return !e.parent_id; }).forEach(function(el) {
- const wrapper = screenDiv.querySelector('.playElement[data-el-id="' + el.id + '"] .playElementInner');
- if (!wrapper || !el.rendered_html || el.rendered_html.indexOf(PLACEHOLDER_OPEN) === -1) return;
- // Avant de regénérer ce sous-arbre depuis le HTML D'ORIGINE (nécessaire
- // pour ne pas laisser les valeurs d'un mail précédent affichées, voir
- // plus haut), sauvegarde l'état visuel TRANSITOIRE posé par une action
+ // Cible directement, à N'IMPORTE QUEL niveau d'imbrication, le ou les
+ // éléments qui portent vraiment un {{champ}} non résolu — et non plus
+ // seulement les éléments de PREMIER NIVEAU de l'écran. Avant ce
+ // correctif, un panneau de détail niché à 2-3 niveaux (ex. "mail
+ // content" posé dans un conteneur, lui-même dans un autre) forçait à
+ // regénérer TOUT son ancêtre de premier niveau — potentiellement
+ // l'écran entier (jauges, onglets, Répéteur compris) s'il n'y avait
+ // qu'un seul gros conteneur racine — d'où l'impression trompeuse d'un
+ // rechargement de page (flash à vide) à chaque clic sur une ligne.
+ //
+ // Parmi les éléments qui ont un {{champ}} non résolu, on ne garde que
+ // les plus "hauts" dans l'arbre : si un ancêtre ET un de ses
+ // descendants sont tous deux candidats, seul l'ancêtre est régénéré
+ // (son rendered_html inclut déjà ce descendant) — sans ce filtre, le
+ // descendant serait régénéré deux fois, la 2e fois à partir du DOM déjà
+ // à jour de la 1re passe.
+ const byId = {};
+ screenData.elements.forEach(function(e){ byId[e.id] = e; });
+ function hasUnresolvedPlaceholder(e) {
+ return !!(e.rendered_html && e.rendered_html.indexOf(PLACEHOLDER_OPEN) !== -1);
+ }
+ function hasCandidateAncestor(e) {
+ let p = e.parent_id ? byId[e.parent_id] : null;
+ while (p) {
+ if (hasUnresolvedPlaceholder(p)) return true;
+ p = p.parent_id ? byId[p.parent_id] : null;
+ }
+ return false;
+ }
+ const targets = screenData.elements.filter(function(e) {
+ return hasUnresolvedPlaceholder(e) && !hasCandidateAncestor(e);
+ });
+
+ targets.forEach(function(el) {
+ const target = screenDiv.querySelector('[data-element-id="' + el.id + '"]');
+ if (!target) return;
+
+ // Avant de regénérer ce nœud depuis le HTML D'ORIGINE (nécessaire pour
+ // ne pas laisser les valeurs d'un mail précédent affichées, voir plus
+ // haut), sauvegarde l'état visuel TRANSITOIRE posé par une action
// "Modifier un élément" (jamais enregistré en base — voir
- // applyElementProperty) sur cet élément ou l'un de ses descendants,
- // pour le réappliquer après. Sans ça, un panneau rendu visible par
- // cette action (ex. "mail content") redevenait invisible à CHAQUE
+ // applyElementProperty) sur ce nœud ou l'un de ses descendants, pour
+ // le réappliquer après. Sans ça, un panneau rendu visible par cette
+ // action (ex. "mail content") redevenait invisible à CHAQUE
// rafraîchissement de données, y compris pour un changement de donnée
- // sans rapport avec lui — puisque wrapper.innerHTML = html ci-dessous
- // recrée entièrement ses nœuds à partir de leur état PAR DÉFAUT.
+ // sans rapport avec lui — puisque le remplacement ci-dessous recrée
+ // entièrement ses nœuds à partir de leur état PAR DÉFAUT.
const savedState = {};
- wrapper.querySelectorAll('[data-element-id]').forEach(function(node) {
+ [target].concat(Array.from(target.querySelectorAll('[data-element-id]'))).forEach(function(node) {
// clickBound/hoverBound/hoverTriggerBound (voir bindClicks/
// bindHoverTexts/bindHoverTriggers) sont des indicateurs INTERNES du
// moteur, jamais un état posé par une action "Modifier un élément" —
// les restaurer sur le nœud tout neuf créé ci-dessous lui ferait
// croire à tort qu'un écouteur de clic/survol lui est déjà rattaché
// (alors qu'aucun ne l'est réellement), ce qui bloquait
- // silencieusement TOUS les clics suivants sur cet élément (ex. les
- // lignes d'un Répéteur voisin, une fois régénérées avec lui).
+ // silencieusement TOUS les clics suivants sur cet élément.
const dataset = Object.assign({}, node.dataset);
delete dataset.clickBound;
delete dataset.hoverBound;
@@ -382,9 +415,13 @@
tmp.textContent = value;
html = html.split(PLACEHOLDER_OPEN + fieldName + PLACEHOLDER_CLOSE).join(tmp.innerHTML);
});
- wrapper.innerHTML = html;
+ const container = document.createElement('div');
+ container.innerHTML = html;
+ const newNode = container.firstElementChild;
+ if (!newNode) return;
+ target.replaceWith(newNode);
- wrapper.querySelectorAll('[data-element-id]').forEach(function(node) {
+ [newNode].concat(Array.from(newNode.querySelectorAll('[data-element-id]'))).forEach(function(node) {
const saved = savedState[node.dataset.elementId];
if (!saved) return;
node.setAttribute('style', saved.style);