Dev #4
@@ -41,13 +41,24 @@ def _element_style(el):
|
||||
SEUL le z-index est omis pour ce widget précis. position:absolute SANS
|
||||
z-index explicite (donc z-index:auto) ne crée PAS de nouveau contexte
|
||||
d'empilement — le z-index:9999 posé plus profond se compare alors
|
||||
directement aux autres éléments de l'écran, et gagne toujours."""
|
||||
directement aux autres éléments de l'écran, et gagne toujours.
|
||||
|
||||
Même omission pour un exemplaire (element_type_id) d'un élément de jeu
|
||||
dont le modèle N'EST QU'une superposition (marqué "_overlay_only_instance"
|
||||
par list_elements.py) : son propre widget est "conteneur" par défaut
|
||||
(add_element.py), pas "superposition", mais son cadre entoure quand
|
||||
même une superposition rendue à l'intérieur (voir
|
||||
render_element_html.py/_render_element_type_children) — exactement le
|
||||
même risque de contexte d'empilement piégeant."""
|
||||
exempt_from_z_index = (
|
||||
el.get("widget") == "superposition" or el.get("_overlay_only_instance")
|
||||
)
|
||||
parts = [
|
||||
"position:absolute",
|
||||
f"left:{el['x']}%", f"top:{el['y']}%",
|
||||
f"width:{el['width']}%", f"height:{el['height']}%",
|
||||
]
|
||||
if el.get("widget") != "superposition":
|
||||
if not exempt_from_z_index:
|
||||
parts.append(f"z-index:{el['z_index']}")
|
||||
return "; ".join(parts) + ";"
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from .load_template_tree import _load_template_tree
|
||||
|
||||
|
||||
def _is_overlay_only_element_type(slug, element_type_id):
|
||||
"""True si le modèle d'un élément de jeu N'EST QU'une "Superposition /
|
||||
boîte de dialogue" (un seul élément de premier niveau, de ce widget) —
|
||||
utilisé à deux endroits qui doivent s'accorder : render_element_html.py
|
||||
(court-circuite l'enveloppe générique "conteneur" d'un exemplaire, qui
|
||||
resterait sinon visible en permanence) et element_style_filter.py (le
|
||||
cadre .canvasElement/.playElement de cet exemplaire ne doit pas non
|
||||
plus poser de z-index, pour la même raison qu'une superposition posée
|
||||
directement — voir son propre commentaire)."""
|
||||
template_tree = _load_template_tree(slug, element_type_id)
|
||||
if not template_tree:
|
||||
return False
|
||||
return {c.get("widget") for c in template_tree["top"]} == {"superposition"}
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
import db
|
||||
|
||||
from ..rendering.render_element_html import render_element_html
|
||||
from ..element_types.is_overlay_only import _is_overlay_only_element_type
|
||||
|
||||
|
||||
def list_elements(slug, screen_id, enforce_visibility=False):
|
||||
@@ -75,5 +76,14 @@ def list_elements(slug, screen_id, enforce_visibility=False):
|
||||
children_map.setdefault(d["parent_id"], []).append(d)
|
||||
play_ctx = {"_forge_play_mode": True} if enforce_visibility else None
|
||||
for d in result:
|
||||
# Un exemplaire de premier niveau d'un élément de jeu dont le
|
||||
# modèle N'EST QU'une superposition : son cadre de positionnement
|
||||
# (.canvasElement/.playElement, voir filters/element_style_filter.py)
|
||||
# ne doit pas non plus poser de z-index, pour la même raison qu'une
|
||||
# superposition posée directement (sinon elle crée un contexte
|
||||
# d'empilement CSS qui piège le z-index:9999 de la superposition
|
||||
# rendue à l'intérieur — voir element_style_filter.py).
|
||||
if d.get("element_type_id") and not d.get("parent_id"):
|
||||
d["_overlay_only_instance"] = _is_overlay_only_element_type(slug, d["element_type_id"])
|
||||
d["rendered_html"] = render_element_html(d, slug, children_map, play_ctx)
|
||||
return result
|
||||
|
||||
@@ -18,6 +18,7 @@ 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):
|
||||
@@ -107,9 +108,28 @@ def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_
|
||||
return _mark(f"<{tag} {_attr_string(attrs, style)}>")
|
||||
|
||||
content = _apply_ctx(el.get("content") or "", ctx)
|
||||
child_html = _render_element_type_children(el, slug, ctx) if el.get("element_type_id") else None
|
||||
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()]
|
||||
@@ -128,15 +148,18 @@ def _render_element_type_children(el, slug, ctx):
|
||||
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 (pas "") si l'élément de jeu n'a
|
||||
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."""
|
||||
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
|
||||
return "".join(
|
||||
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"])
|
||||
|
||||
@@ -232,6 +232,41 @@ def test_overlay_stays_visible_in_the_editor_despite_starting_masked(client, gam
|
||||
assert wrapper_style2.rstrip().endswith("display:none;")
|
||||
|
||||
|
||||
def test_overlay_element_type_instance_has_no_visible_wrapper_box(client, game):
|
||||
"""Régression : poser un élément de jeu réutilisable ("Mes éléments de
|
||||
jeu") dont le MODÈLE n'est qu'une superposition affichait, sur la
|
||||
vraie scène, une boîte "conteneur" vide et TOUJOURS VISIBLE à
|
||||
l'endroit où l'exemplaire a été déposé — "un conteneur vide apparaît,
|
||||
pas la boîte de dialogue" (elle existe bien, masquée comme prévu ;
|
||||
c'est l'enveloppe générique "conteneur" AUTOUR, posée par défaut pour
|
||||
tout exemplaire (add_element.py), qui n'aurait jamais dû être visible
|
||||
pour un modèle qui n'est QUE ça). Corrigé en court-circuitant cette
|
||||
enveloppe dès que le modèle entier est une superposition."""
|
||||
import screens
|
||||
resp = client.post(f"/game/{game}/element-types", data={"name": "Dialogue"}, follow_redirects=False)
|
||||
assert resp.status_code == 302
|
||||
et = next(t for t in screens.list_element_types(game) if t["name"] == "Dialogue")
|
||||
template_screen_id = et["template_screen_id"]
|
||||
overlay_id = _add_element(client, game, template_screen_id, "superposition")
|
||||
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"})
|
||||
|
||||
screen_id = _create_screen(client, game, "Scène")
|
||||
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add",
|
||||
data={"widget": "__catalogue__", "element_type_id": et["id"]}, follow_redirects=False)
|
||||
assert resp.status_code == 302
|
||||
# Un frère ajouté APRÈS -> z_index plus grand, comme dans le scénario
|
||||
# rapporté (une scène avec d'autres éléments déjà en place).
|
||||
_add_element(client, game, screen_id, "bouton")
|
||||
|
||||
html = client.get(f"/game/{game}/play").data.decode()
|
||||
assert "forgeOverlayBox" in html
|
||||
assert 'class="box"' not in html # l'enveloppe "conteneur" par défaut ne doit plus apparaître
|
||||
idx = html.find('class="modal is-active"')
|
||||
wrapper_start = html.rfind('<div class="playElement"', 0, idx)
|
||||
wrapper_style = re.search(r'style="([^"]*)"', html[wrapper_start:idx]).group(1)
|
||||
assert "z-index" not in wrapper_style
|
||||
|
||||
|
||||
def test_overlay_wrapper_does_not_trap_its_own_z_index(client, game):
|
||||
"""Régression : le cadre .playElement/.canvasElement partagé par TOUS
|
||||
les widgets (voir filters/element_style_filter.py) posait quand même
|
||||
|
||||
Reference in New Issue
Block a user