From 20adfa416911c691b5b07df3447942ff9f220a93 Mon Sep 17 00:00:00 2001 From: william Date: Wed, 26 Aug 2026 09:02:49 +0200 Subject: [PATCH] =?UTF-8?q?Ne=20r=C3=A9g=C3=A9n=C3=A8re=20plus=20que=20l'?= =?UTF-8?q?=C3=A9l=C3=A9ment=20concern=C3=A9=20par=20"Ouvrir=20la=20ligne?= =?UTF-8?q?=20cliqu=C3=A9e"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cause du "rechargement" perçu par l'utilisateur (et du flash à vide sur la ligne de Répéteur juste cliquée, visible sur une vidéo de repro) : applyOpenRowBindings() ne pouvait cibler que les éléments de PREMIER NIVEAU de l'écran (seuls eux ont un wrapper .playElement dans le DOM). Sur cet écran, "mail content" est niché à 2 conteneurs de profondeur, et le SEUL élément de premier niveau est le conteneur racine de tout l'écran — donc chaque clic sur une ligne de Répéteur régénérait littéralement tout l'écran (jauges, onglets, Répéteur compris) pour ne mettre à jour qu'un seul panneau de détail, avec un flash à vide pendant la reconstruction. Fix : applyOpenRowBindings() cible maintenant directement, à n'importe quel niveau d'imbrication, le(s) élément(s) qui portent réellement un {{champ}} non résolu (repéré via document.querySelector ('[data-element-id=...]'), disponible sur CHAQUE élément rendu, pas seulement les élément de premier niveau) — et seulement les plus "hauts" parmi eux, pour ne jamais régénérer un même nœud deux fois. Seul "mail content" est donc désormais remplacé (via replaceWith), sans toucher au Répéteur ni au reste de l'écran. La sauvegarde/restauration de l'état visuel transitoire (style, classes, dataset hors clickBound/hoverBound/ hoverTriggerBound) suit le même principe, appliquée au nœud remplacé et à ses descendants. Aucun aller-retour réseau n'a jamais eu lieu ici (refreshRuntimeData() utilise déjà fetch/JSON, pas de navigation de page) — la sensation de rechargement venait uniquement de la granularité du remplacement DOM, pas d'un manque d'AJAX. Co-Authored-By: Claude Sonnet 5 --- templates/play.html | 69 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 16 deletions(-) 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);