Bug rapporté : poser un élément de jeu ("Mes éléments de jeu") dont le
modèle n'est qu'une "Superposition / boîte de dialogue" sur une vraie
scène affichait un conteneur vide à l'endroit du dépôt — jamais le
dialogue. La boîte de dialogue existait bien (masquée comme prévu, en
attente d'une action qui l'affiche) : c'est l'enveloppe qui l'entoure qui
n'aurait jamais dû être visible.
Cause : tout exemplaire d'élément de jeu est posé avec le widget générique
"conteneur" par défaut (add_element.py, colonne default_widget) — son
contenu réel (le modèle) est rechargé EN DIRECT à l'intérieur
(_render_element_type_children), mais l'enveloppe "conteneur" elle-même
reste une boîte NORMALE, toujours visible, avec sa propre couleur de fond/
bordure et sa position fixe sur le canevas (contrairement à une
superposition posée directement, qui, elle, démarre masquée). Résultat :
une boîte vide et permanente à l'endroit du dépôt, pendant que le vrai
dialogue (démarré masqué, correctement) reste invisible en dessous/
au-dessus tant qu'aucune action ne le déclenche.
Correctif : quand le modèle ENTIER d'un élément de jeu n'est qu'une seule
superposition (screens/element_types/is_overlay_only.py, nouveau), on
court-circuite entièrement l'enveloppe "conteneur" (render_element_html.py)
et on retire aussi le z-index de son cadre de positionnement
(element_style_filter.py, list_elements.py) — même raison que pour une
superposition posée directement (81c31a9) : sans ça, ce cadre reste un
candidat à piéger le z-index:9999 du dialogue rendu à l'intérieur dès
qu'un autre élément de la scène a un z-index plus grand.
Nouveau test, confirmé en échec sur l'ancien code (même "class=\"box\""
fantôme reproduit) puis au vert avec le correctif. 140 tests au vert au
total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
166 lines
9.3 KiB
Python
166 lines
9.3 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
|
|
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'<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, 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", cette enveloppe
|
|
# (fond, bordure, position normale sur le canevas — voir
|
|
# widgets/registry.py "conteneur") resterait visible EN PERMANENCE
|
|
# à 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û être visible). On court-circuite
|
|
# donc entièrement l'enveloppe et on renvoie directement le
|
|
# contenu du modèle.
|
|
return _mark(child_html)
|
|
|
|
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}>")
|
|
|
|
|
|
def _render_element_type_children(el, slug, ctx):
|
|
"""Contenu d'un exemplaire d'élément de jeu posé sur un écran (voir
|
|
add_element.py, colonne element_type_id) : rechargé EN DIRECT depuis
|
|
l'écran-modèle à chaque affichage, jamais depuis d'éventuels enfants
|
|
copiés en base — exactement le même principe qu'un modèle de ligne de
|
|
Répéteur (voir _render_repeater/_load_template_tree). Modifier
|
|
l'élément de jeu dans son propre éditeur met donc à jour tous ses
|
|
exemplaires déjà posés, sur n'importe quel écran, sans avoir à les
|
|
retoucher un par un. Renvoie (None, False) si l'élément de jeu n'a
|
|
plus de modèle valide, pour que l'appelant retombe sur le rendu
|
|
générique (d'éventuels enfants en base issus d'une version antérieure
|
|
de ce mécanisme) plutôt que d'afficher un exemplaire silencieusement
|
|
vide. Le second élément renvoyé indique si le modèle N'EST QUE une
|
|
superposition (voir l'appelant : dans ce cas précis, l'enveloppe
|
|
générique "conteneur" de l'exemplaire doit être court-circuitée)."""
|
|
template_tree = _load_template_tree(slug, el["element_type_id"])
|
|
if not template_tree:
|
|
return None, False
|
|
html = "".join(
|
|
render_element_html(c, slug, template_tree["children_map"], ctx)
|
|
for c in template_tree["top"]
|
|
)
|
|
return html, _is_overlay_only_element_type(slug, el["element_type_id"])
|