Bug remonté avec deux icônes (enveloppe fermée / ouverte) posées
directement sur l'écran, chacune conditionnée sur is_opened : après avoir
ouvert le mail (une action "Modifier une donnée"), les DEUX icônes
restaient affichées en même temps au lieu que l'ouverte remplace la fermée.
Cause : refreshRuntimeData() (play.html) ne réévalue, après une action,
que les éléments dont le HTML porte un marqueur ("visibilityGated"/
"repeaterItem"/"jaugeBar"). render_element_html() posait bien ce marqueur
quand un élément sous condition est actuellement visible - mais seulement
sur le chemin de rendu GÉNÉRIQUE (texte, titre, conteneur...), jamais sur
les 9 widgets "special_render" (Icône, Tableau, Superposition, Onglets,
Case à cocher, Liste déroulante, Groupe de champs, Répéteur, Jauge) - un
élément CACHÉ portait toujours son marqueur (via son placeholder), mais un
élément VISIBLE de ce type non. Résultat : l'icône "fermée", visible au
premier chargement, ne portait aucun marqueur et restait donc figée dans
son état d'origine après toute action suivante, pendant que l'icône
"ouverte" (cachée au départ, donc marquée) se mettait, elle, correctement
à jour - d'où les deux affichées ensemble.
Fix : les 9 branches special_render passent maintenant, elles aussi, par
_mark() comme le chemin générique.
Ajoute un test de régression dédié (icône visible sous condition = doit
porter le marqueur).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
118 lines
6.5 KiB
Python
118 lines
6.5 KiB
Python
import html as html_lib
|
|
|
|
from ..widgets.widget_meta import widget_meta
|
|
from .visible_attrs import _visible_attrs
|
|
from .style_string import _style_string
|
|
from .attr_string import _attr_string
|
|
from .apply_ctx import _apply_ctx
|
|
from .render_children import _render_children
|
|
from .render_table import _render_table
|
|
from .render_select import _render_select
|
|
from .render_fieldset import _render_fieldset
|
|
from .render_checkbox_or_radio import _render_checkbox_or_radio
|
|
from .render_repeater import _render_repeater
|
|
from .render_jauge import _render_jauge
|
|
from .render_overlay import _render_overlay
|
|
from .render_onglets import _render_onglets
|
|
from .render_icone import _render_icone
|
|
from .resolve_bound_row import _resolve_bound_row_ctx
|
|
from .visibility_condition import is_element_visible
|
|
|
|
|
|
def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_direction=None):
|
|
"""Construit le HTML réel (balise + attributs + style + contenu, y
|
|
compris les éléments physiquement posés à l'intérieur pour un
|
|
conteneur/répéteur/groupe de champs) — utilisé par l'éditeur (canevas)
|
|
et le mode jouable. N'écrit jamais rien sur disque : uniquement du HTML
|
|
généré à la volée à partir de ce qui est stocké en base. `slug` est
|
|
nécessaire pour le Répéteur de données (il doit lire les données du jeu
|
|
au moment de l'affichage). `ctx` porte les valeurs {{champ}} de la ligne
|
|
en cours quand cet élément est descendant d'un répéteur. `parent_flex_direction`
|
|
est la disposition interne (voir _parent_flex_direction) du conteneur qui
|
|
pose physiquement cet élément, s'il y en a un — elle influence la largeur
|
|
par défaut de cet élément (voir _style_string)."""
|
|
meta = widget_meta(el.get("widget"))
|
|
attrs_raw = el.get("attributes") or {}
|
|
# La condition de visibilité (voir VISIBILITY_CONDITION_CONTROLS) n'est
|
|
# appliquée qu'en mode JOUABLE (ctx["_forge_play_mode"], posé par
|
|
# full_game_payload.py via list_elements(..., enforce_visibility=True))
|
|
# — jamais dans l'éditeur, où l'élément doit rester visible/sélectionnable
|
|
# même si la condition serait actuellement fausse, sans quoi il
|
|
# deviendrait impossible de le retrouver pour le régler. Un commentaire
|
|
# marqueur ("visibilityGated") reste posé sur le HTML même quand la
|
|
# condition est vraie, pour que refreshRuntimeData() (play.html) sache
|
|
# qu'il faut réévaluer cet élément après toute action "Modifier une
|
|
# donnée"/"Modifier une variable" — pas seulement Jauge/Répéteur, qui ont
|
|
# déjà chacun leur propre marqueur ("jaugeBar"/"repeaterItem") couvrant
|
|
# ce cas d'office.
|
|
has_visibility_condition = attrs_raw.get("_visibilite_cond_mode") in ("variable", "objet")
|
|
play_mode = bool((ctx or {}).get("_forge_play_mode"))
|
|
if play_mode and has_visibility_condition and not is_element_visible(slug, attrs_raw, ctx):
|
|
return f'<div class="visibilityGated" data-element-id="{el["id"]}" style="display:none;"></div>'
|
|
|
|
def _mark(html_str):
|
|
return html_str + "<!--visibilityGated-->" if has_visibility_condition else html_str
|
|
|
|
bound_ctx = _resolve_bound_row_ctx(slug, attrs_raw)
|
|
if bound_ctx:
|
|
ctx = {**(ctx or {}), **bound_ctx}
|
|
# Chaque branche "special_render" passe elle aussi par _mark() : sans
|
|
# ça, un widget de ce genre (Icône, Tableau, Superposition...) actuellement
|
|
# VISIBLE sous condition ne portait aucun marqueur, et refreshRuntimeData()
|
|
# (play.html) ne savait donc jamais qu'il fallait le réévaluer après une
|
|
# action "Modifier une donnée/variable" — il restait figé dans son état
|
|
# de l'affichage précédent (ex: une icône "enveloppe fermée" restée
|
|
# visible après l'ouverture du mail, en même temps que "enveloppe
|
|
# ouverte" qui apparaît, elle, correctement puisqu'un élément CACHÉ
|
|
# porte toujours son marqueur via le placeholder ci-dessus).
|
|
special = meta.get("special_render")
|
|
if special == "table":
|
|
return _mark(_render_table(el, meta, slug, children_map, ctx, parent_flex_direction))
|
|
if special == "select_options":
|
|
return _mark(_render_select(el, meta, slug, children_map, ctx, parent_flex_direction))
|
|
if special == "fieldset_legend":
|
|
return _mark(_render_fieldset(el, meta, slug, children_map, ctx, parent_flex_direction))
|
|
if special == "checkbox_label":
|
|
return _mark(_render_checkbox_or_radio(el, meta, slug, children_map, ctx, parent_flex_direction))
|
|
if special == "repeater":
|
|
return _mark(_render_repeater(el, meta, slug, children_map, ctx, parent_flex_direction))
|
|
if special == "jauge":
|
|
return _mark(_render_jauge(el, meta, slug, children_map, ctx, parent_flex_direction))
|
|
if special == "overlay":
|
|
return _mark(_render_overlay(el, meta, slug, children_map, ctx, parent_flex_direction))
|
|
if special == "onglets":
|
|
return _mark(_render_onglets(el, meta, slug, children_map, ctx, parent_flex_direction))
|
|
if special == "icone":
|
|
return _mark(_render_icone(el, meta, slug, children_map, ctx, parent_flex_direction))
|
|
|
|
tag = meta.get("tag")
|
|
if tag is None: # widget à balise dynamique (ex: "titre" -> h1/h2/h3)
|
|
niveau = (el.get("attributes") or {}).get("_niveau", "2")
|
|
tag = f"h{niveau}"
|
|
|
|
attrs = _visible_attrs(el, meta, ctx)
|
|
style = _style_string(el, parent_flex_direction=parent_flex_direction, widget_key=el.get("widget"))
|
|
|
|
# 3.1 (Confort) — interactions au survol : un texte de remplacement
|
|
# optionnel, affiché à la place du contenu normal quand le joueur
|
|
# survole cet élément à la souris (voir bindHoverTexts() dans
|
|
# templates/play.html). Absent par défaut => aucun changement, donc
|
|
# aucune régression sur les éléments déjà créés.
|
|
hover_text = (el.get("attributes") or {}).get("_survol_texte")
|
|
if hover_text:
|
|
attrs["data-hover-text"] = _apply_ctx(hover_text, ctx)
|
|
|
|
if meta.get("self_closing"):
|
|
return _mark(f"<{tag} {_attr_string(attrs, style)}>")
|
|
|
|
content = _apply_ctx(el.get("content") or "", ctx)
|
|
child_html = _render_children(el, slug, children_map, ctx)
|
|
|
|
if tag in ("ul", "ol"):
|
|
items = [line.strip() for line in content.split("\n") if line.strip()]
|
|
inner = "".join(f"<li>{html_lib.escape(item)}</li>" for item in items) + child_html
|
|
return _mark(f"<{tag} {_attr_string(attrs, style)}>{inner}</{tag}>")
|
|
|
|
safe_content = html_lib.escape(content).replace("\n", "<br>") if content else ""
|
|
return _mark(f"<{tag} {_attr_string(attrs, style)}>{safe_content}{child_html}</{tag}>")
|