diff --git a/templates/play.html b/templates/play.html
index 7e611467..e1db84c6 100644
--- a/templates/play.html
+++ b/templates/play.html
@@ -380,61 +380,83 @@
return !candidates.some(function(other) { return 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é,
+ // mais posé à la main puisqu'il n'y a pas de vrai DOM virtuel ici :
+ // - dans un nœud TEXTE : {{champ}} est coupé en 3 (texte avant, un
+ // qui portera la valeur, texte
+ // après) LA PREMIÈRE FOIS SEULEMENT ; les ouvertures suivantes se
+ // contentent de changer le textContent de ce span.
+ // - dans un ATTRIBUT (ex. href d'un lien "{{link_real_url}}") : le
+ // gabarit d'origine est mémorisé sur un data-bind-attr- lors du
+ // premier passage, pour pouvoir recalculer et réécrire l'attribut à
+ // chaque fois sans jamais avoir besoin de reconstruire le nœud.
+ // Avant ce correctif, tout le sous-arbre ciblé était détruit et recréé
+ // depuis zéro à chaque clic sur une ligne — ce qui causait un flash
+ // visuel (perte de tout état transitoire posé par "Modifier un
+ // é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');
+
+ function resolveTextNodes(root) {
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
+ const toSplit = [];
+ let node;
+ while ((node = walker.nextNode())) {
+ if (node.nodeValue.indexOf(PLACEHOLDER_OPEN) !== -1) toSplit.push(node);
+ }
+ toSplit.forEach(function(textNode) {
+ const raw = textNode.nodeValue;
+ placeholderRe.lastIndex = 0;
+ const frag = document.createDocumentFragment();
+ let lastIndex = 0, match;
+ while ((match = placeholderRe.exec(raw))) {
+ if (match.index > lastIndex) frag.appendChild(document.createTextNode(raw.slice(lastIndex, match.index)));
+ const span = document.createElement('span');
+ span.dataset.bindField = match[1];
+ span.textContent = row[match[1]] == null ? '' : String(row[match[1]]);
+ frag.appendChild(span);
+ lastIndex = match.index + match[0].length;
+ }
+ if (lastIndex < raw.length) frag.appendChild(document.createTextNode(raw.slice(lastIndex)));
+ textNode.replaceWith(frag);
+ });
+ }
+
+ function updateTextSlots(root) {
+ root.querySelectorAll('[data-bind-field]').forEach(function(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) {
+ 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);
+ 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]);
+ });
+ if (attr.value !== resolved) node.setAttribute(attr.name, resolved);
+ });
+ });
+ }
+
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 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 le remplacement ci-dessous recrée
- // entièrement ses nœuds à partir de leur état PAR DÉFAUT.
- const savedState = {};
- [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.
- const dataset = Object.assign({}, node.dataset);
- delete dataset.clickBound;
- delete dataset.hoverBound;
- delete dataset.hoverTriggerBound;
- savedState[node.dataset.elementId] = {
- style: node.getAttribute('style') || '',
- className: node.className,
- dataset: dataset,
- };
- });
-
- let html = el.rendered_html;
- Object.keys(row).forEach(function(fieldName) {
- if (fieldName === 'id') return;
- const value = row[fieldName] == null ? '' : String(row[fieldName]);
- const tmp = document.createElement('div');
- tmp.textContent = value;
- html = html.split(PLACEHOLDER_OPEN + fieldName + PLACEHOLDER_CLOSE).join(tmp.innerHTML);
- });
- const container = document.createElement('div');
- container.innerHTML = html;
- const newNode = container.firstElementChild;
- if (!newNode) return;
- target.replaceWith(newNode);
-
- [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);
- node.className = saved.className;
- Object.assign(node.dataset, saved.dataset);
- });
+ if (target.querySelector('[data-bind-field]')) {
+ updateTextSlots(target);
+ } else {
+ resolveTextNodes(target);
+ }
+ resolveAttributes(target);
});
}