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 from ..element_types.load_template_tree import _load_template_tree from ..element_types.is_overlay_only import _is_overlay_only_element_type 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'
' # "Donnée liée" (DATA_BINDING_CONTROLS, Texte/Titre) : son contenu # ({{champ}}) dépend de la ligne trouvée par ses conditions au moment du # rendu — potentiellement via une "Variable globale" en valeur de # comparaison (voir _resolve_filter_value). Comme "visibilityGated" # juste au-dessus, un marqueur ("dataBound") reste posé sur le HTML 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" — sans lui, changer la variable choisie changeait bien QUELLE # ligne aurait dû s'afficher (recalculé correctement côté serveur à # chaque rendu), mais le client ne redemandait jamais le nouveau HTML de # CET élément précis : le texte affiché restait figé sur son ancienne # ligne jusqu'au prochain rechargement complet de la page. has_data_binding = bool(attrs_raw.get("_data_definition_id")) def _mark(html_str): markers = "" if has_visibility_condition: markers += "" if has_data_binding: markers += "" return html_str + markers 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, is_overlay_template = ( _render_element_type_children(el, slug, ctx) if el.get("element_type_id") else (None, False) ) if child_html is None: child_html = _render_children(el, slug, children_map, ctx) elif is_overlay_template: # Un exemplaire d'élément de jeu est posé par défaut avec le widget # générique "conteneur" (add_element.py, "default_widget") — utile # pour la plupart des modèles, mais QUAND le modèle entier n'est # qu'une "Superposition / boîte de dialogue", la classe Bulma "box" # de ce widget (widgets/registry.py "conteneur") lui donnait un # fond opaque TOUJOURS VISIBLE à l'endroit où l'exemplaire a été # déposé, alors que la superposition à l'intérieur gère déjà # entièrement sa propre apparence et son propre masquage # (position:fixed plein écran, démarre masquée) — vécu comme "un # conteneur vide apparaît sur la scène, pas la boîte de dialogue" # (en réalité la boîte de dialogue existe bien, juste masquée comme # prévu ; c'est le conteneur AUTOUR qui n'aurait jamais dû avoir sa # propre apparence visuelle). # # On ne court-circuite PLUS entièrement cette enveloppe (un essai # précédent le faisait, en renvoyant `child_html` directement) : # cette balise porte le SEUL id/data-element-id de CET EXEMPLAIRE # précis (id=el["id"], distinct de l'id interne du modèle, # ex. l'overlay du modèle "dialog" garde son propre id 72 alors que # CET exemplaire est, par ex., 77) — une action "Modifier un # élément" posée sur LA SCÈNE qui l'accueille (pas sur le modèle # lui-même) cible cet id d'exemplaire, jamais celui, interne et # partagé par tous les exemplaires, du modèle. La court-circuiter # entièrement faisait disparaître cet id du DOM, rendant # IMPOSSIBLE tout contrôle de la boîte de dialogue depuis la # logique de la scène qui l'accueille ("j'ai aucun contrôle sur la # visibilité de la modale depuis la scène"). On garde donc la # balise (avec son id), mais SANS la classe "box" du widget # générique — juste une balise transparente, invisible tant # qu'elle ne porte ni fond ni bordure.""" attrs.pop("class", None) return _mark(f"