Dev #2
+74
-52
@@ -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
|
||||
// <span data-bind-field="champ"> 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-<nom> 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user