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