From 7c0f1ed427753755822db0f11da2a86bc8d61e81 Mon Sep 17 00:00:00 2001 From: william Date: Fri, 28 Aug 2026 06:07:08 +0200 Subject: [PATCH 01/16] =?UTF-8?q?Corrige=20la=20r=C3=A9gression=20du=20cor?= =?UTF-8?q?rectif=20overlay=20:=20/elements//geometry=20plantait=20(50?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le correctif précédent (81c31a9) retirait TOUTE position/taille du cadre .canvasElement/.playElement d'une "superposition" pour ne plus piéger son propre z-index:9999 dans un contexte d'empilement local — mais ce même cadre sert aussi, dans l'ÉDITEUR, de prise pour le glisser-déposer/ redimensionnement de ce widget (screen_edit.html). Sans position:absolute + left/top/width/height, ce cadre s'effondre à 0×0 (son contenu réel est en position:fixed, hors flux) : le calcul de geometrie divise alors par une dimension nulle, produit NaN, et JSON.stringify(NaN) envoie littéralement "null" au serveur — qui plantait sur float(None) dans routes/elements/element_geometry.py (500, reproduit par l'utilisateur en posant un template lié à un objet et en essayant de repositionner l'overlay dans l'éditeur). Correctif plus ciblé : le cadre garde position/left/top/width/height comme n'importe quel widget (l'éditeur redevient fonctionnel) — 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 CSS : le z-index:9999 posé plus profond (render_overlay.py) continue donc de se comparer directement aux autres éléments de l'écran, et gagne toujours — le bug de superposition visuelle (81c31a9) reste corrigé, sans regression sur l'éditeur cette fois. test_confort.py mis à jour (position:absolute attendu, plus d'assertion "position:static") + nouveau test qui appelle directement la route /geometry sur une superposition pour confirmer qu'elle répond 200. 137 tests au vert au total. Co-Authored-By: Claude Sonnet 5 --- filters/element_style_filter.py | 43 +++++++++++++++++++------------ tests/test_confort.py | 45 +++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/filters/element_style_filter.py b/filters/element_style_filter.py index daf9cbe9..4618102b 100644 --- a/filters/element_style_filter.py +++ b/filters/element_style_filter.py @@ -20,25 +20,36 @@ def _element_style(el): Bug corrigé : le widget "superposition" (render_overlay.py) ignore déjà x/y/width/height et pose lui-même position:fixed; inset:0; z-index:9999 sur SA PROPRE balise — mais tant que CE cadre-ci gardait quand même - "position:absolute; z-index:{z_index}" (le z-index de sa place dans le - canevas, souvent petit), il devenait un élément positionné avec z-index - explicite, donc un NOUVEAU contexte d'empilement CSS — le z-index:9999 - posé plus profond ne se comparait alors plus qu'AU SEIN de ce contexte, - et perdait face au z-index (plus grand) d'un frère ajouté après lui sur - le canevas, qui se retrouvait affiché PAR-DESSUS le dialogue censé tout - couvrir. En ne posant ICI aucune position/z-index pour ce widget (sa - place dans le flux normal, invisible puisque son contenu est en - position:fixed de toute façon), plus aucun contexte d'empilement n'est - créé à ce niveau : le z-index:9999 se compare alors directement aux - autres éléments de l'écran, et gagne toujours.""" - if el.get("widget") == "superposition": - return "position:static;" - return "; ".join([ + "z-index:{z_index}" (le z-index de sa place dans le canevas, souvent + petit), un élément positionné avec un z-index EXPLICITE (même sur ce + cadre, pas sur son contenu) devient un NOUVEAU contexte d'empilement + CSS — le z-index:9999 posé plus profond ne se comparait alors plus + qu'AU SEIN de ce contexte, et perdait face au z-index (plus grand) d'un + frère ajouté après lui sur le canevas, qui se retrouvait affiché + PAR-DESSUS le dialogue censé tout couvrir. + + Première correction tentée (retirer aussi position/left/top/width/ + height ici) : régression dans l'ÉDITEUR — ce même cadre (.canvasElement) + sert aussi de prise pour glisser-déposer/redimensionner CE widget dans + le canevas (voir screen_edit.html), dont le calcul se base sur ses + dimensions réelles ; sans position:absolute + left/top/width/height, ce + cadre s'effondre à 0×0 (son contenu réel est en position:fixed, donc + hors flux), le calcul division par zéro produit NaN, et + JSON.stringify(NaN) donne "null" — element_geometry.py plantait alors + sur float(None). left/top/width/height restent donc posés comme pour + n'importe quel widget (l'éditeur continue de fonctionner normalement) : + 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.""" + parts = [ "position:absolute", f"left:{el['x']}%", f"top:{el['y']}%", f"width:{el['width']}%", f"height:{el['height']}%", - f"z-index:{el['z_index']}", - ]) + ";" + ] + if el.get("widget") != "superposition": + parts.append(f"z-index:{el['z_index']}") + return "; ".join(parts) + ";" def _element_transform_style(el): diff --git a/tests/test_confort.py b/tests/test_confort.py index 4d622083..d62894bc 100644 --- a/tests/test_confort.py +++ b/tests/test_confort.py @@ -184,15 +184,19 @@ def test_overlay_widget_renders_fullscreen_fixed_box(client, game): 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 - "position:absolute; z-index:" sur la - superposition, MÊME SI son propre contenu (render_overlay.py) ignore - x/y/width/height et pose déjà position:fixed + z-index:9999 lui-même. - Un élément positionné avec un z-index explicite crée un NOUVEAU contexte - d'empilement CSS : le 9999 posé plus profond ne se comparait alors plus - qu'AU SEIN de ce contexte, et perdait face au z-index (plus grand) d'un - élément normal ajouté APRÈS l'overlay sur le canevas — qui s'affichait - donc PAR-DESSUS le dialogue censé tout couvrir. Le cadre ne doit donc - plus poser aucune position/z-index pour ce widget.""" + "z-index:" sur la superposition, MÊME SI son + propre contenu (render_overlay.py) ignore x/y/width/height et pose déjà + position:fixed + z-index:9999 lui-même. Un élément positionné avec un + z-index EXPLICITE crée un NOUVEAU contexte d'empilement CSS : le 9999 + posé plus profond ne se comparait alors plus qu'AU SEIN de ce contexte, + et perdait face au z-index (plus grand) d'un élément normal ajouté + APRÈS l'overlay sur le canevas — qui s'affichait donc PAR-DESSUS le + dialogue censé tout couvrir. Le cadre garde position/left/top/width/ + height comme tout widget (l'éditeur en a besoin pour glisser-déposer/ + redimensionner ce cadre — les retirer a fait planter element_geometry + en régression), mais n'écrit plus DU TOUT de z-index pour ce widget : + position:absolute avec z-index:auto (omis) ne crée pas de contexte + d'empilement, donc le 9999 se compare directement aux autres éléments.""" screen_id = _create_screen(client, game) overlay_id = _add_element(client, game, screen_id, "superposition") # Ajouté APRÈS l'overlay -> z_index plus grand que le sien. @@ -204,8 +208,27 @@ def test_overlay_wrapper_does_not_trap_its_own_z_index(client, game): assert idx != -1 tag_start = html.rfind(" Date: Fri, 28 Aug 2026 06:10:17 +0200 Subject: [PATCH 02/16] =?UTF-8?q?Bo=C3=AEte=20de=20dialogue=20(superpositi?= =?UTF-8?q?on)=20stylis=C3=A9e=20avec=20les=20classes=20Bulma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le widget "Superposition / boîte de dialogue" gagne les classes Bulma "modal is-active" (sur le voile plein écran) et "box" (sur la boîte centrée), en PLUS du style inline existant — jamais à sa place : tout le positionnement/masquage critique (position:fixed, z-index, display) reste en inline, qui gagne toujours sur une règle de classe. Si Bulma (chargé depuis un CDN, voir play.html) ne se charge pas (hors-ligne), la boîte de dialogue continue de fonctionner exactement pareil — ces classes n'ajoutent qu'un habillage visuel (ombre, base de police/espacement Bulma) qui se dégrade sans rien casser. Pas de ".modal-background" séparé (convention Bulma habituelle) : le voile semi-transparent est déjà posé en inline sur la même balise (background:rgba(...)), un second calque tout aussi transparent par-dessus n'aurait ajouté qu'un assombrissement redondant. 137 tests toujours au vert (changement purement visuel, aucune assertion cassée) ; jeu de démo régénéré et vérifié (classes bien présentes dans le rendu). Co-Authored-By: Claude Sonnet 5 --- screens/rendering/render_overlay.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/screens/rendering/render_overlay.py b/screens/rendering/render_overlay.py index cff5569d..f2ff251e 100644 --- a/screens/rendering/render_overlay.py +++ b/screens/rendering/render_overlay.py @@ -22,7 +22,18 @@ def _render_overlay(el, meta, slug, children_map=None, ctx=None, parent_flex_dir fusionne le style de positionnement fixe ci-dessous AVEC le style normal de l'élément (_style_string) : si l'élément est réglé "Masqué" dans ses propriétés, ou si une action "Modifier un élément" le cache plus tard, - ce masquage continue de fonctionner normalement.""" + ce masquage continue de fonctionner normalement. + + Classes Bulma ("modal is-active" / "box") posées en PLUS du style + inline existant, jamais à sa place : tout le positionnement/masquage + critique (position:fixed, z-index, display) reste en inline, qui gagne + toujours sur une règle de classe — si Bulma (chargé depuis un CDN, voir + play.html) ne se charge pas (hors-ligne), la boîte de dialogue continue + de fonctionner exactement pareil, ces classes n'ajoutant qu'un habillage + visuel (ombre, base de police) qui se dégrade sans casser quoi que ce + soit. Pas de ".modal-background" séparé : le voile semi-transparent est + déjà posé en inline sur cette même balise (background:rgba(...)) — un + second calque tout aussi transparent par-dessus n'ajouterait rien.""" attrs_raw = el.get("attributes") or {} box_color = attrs_raw.get("_couleur_boite") or "#1f2430" radius = attrs_raw.get("_arrondi") or "12" @@ -53,7 +64,7 @@ def _render_overlay(el, meta, slug, children_map=None, ctx=None, parent_flex_dir ) child_html = _render_children(el, slug, children_map, ctx) return ( - f'
' - f'
{child_html}
' + f'" ) From 69bafcb5c56a0eb526b3285b5240f63044ececfd Mon Sep 17 00:00:00 2001 From: william Date: Fri, 28 Aug 2026 06:18:19 +0200 Subject: [PATCH 03/16] =?UTF-8?q?Corrige=20le=20texte=20invisible=20dans?= =?UTF-8?q?=20la=20bo=C3=AEte=20de=20dialogue=20(r=C3=A9gression=20du=20st?= =?UTF-8?q?yle=20Bulma)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Régression du commit précédent (d87d6ad, classes Bulma sur la superposition) : la classe ".box" impose elle-même une couleur de texte SOMBRE (pensée pour un fond blanc). Comme aucun widget ne fige de couleur de texte à sa création (default_style_for_widget.py), un titre/texte posé dans la boîte SANS couleur personnalisée héritait de ce gris sombre imposé par Bulma — invisible sur le fond sombre par défaut de cette boîte de dialogue. Ni erreur serveur ni régression de test visible : juste un texte "présent mais invisible", exactement ce qui a été rapporté ("j'ai mis un texte dedans mais il ne se voit pas"). Correctif : la boîte fixe elle-même une couleur de texte claire par défaut (color:#e8eaf0) dans son propre style inline — l'élément le plus proche gagne, donc ça écrase la couleur imposée par la classe Bulma, tout en restant surchargeable : un titre/texte qui personnalise sa propre couleur (réglage "Couleur du texte") continue de l'emporter normalement. Nouveau test de régression, confirmé en échec sur le commit précédent (même style Bulma, pas encore cette couleur) puis au vert avec le correctif. 138 tests au vert au total. Jeu de démo régénéré. Co-Authored-By: Claude Sonnet 5 --- screens/rendering/render_overlay.py | 13 ++++++++++++- tests/test_confort.py | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/screens/rendering/render_overlay.py b/screens/rendering/render_overlay.py index f2ff251e..c573921f 100644 --- a/screens/rendering/render_overlay.py +++ b/screens/rendering/render_overlay.py @@ -60,7 +60,18 @@ def _render_overlay(el, meta, slug, children_map=None, ctx=None, parent_flex_dir # bord à bord peu lisible comme dialogue — 560px reste une largeur # de boîte de dialogue confortable, tout en retombant sur 90% sur un # écran de jeu étroit (mobile/portrait) pour ne jamais déborder. - "padding:24px; width:100%; max-width:min(560px, 90%); max-height:90%; overflow:auto; box-sizing:border-box;" + # color:#e8eaf0 : la classe Bulma ".box" (ajoutée ci-dessous) impose + # elle-même une couleur de texte SOMBRE (pensée pour un fond blanc + # par défaut) — comme aucun widget ne fige de couleur de texte à sa + # création (default_style_for_widget.py), un titre/texte posé dans + # la boîte SANS couleur personnalisée héritait de ce gris sombre + # imposé par Bulma, invisible sur le fond sombre par défaut de cette + # boîte de dialogue (texte "présent mais invisible", pas de bug côté + # utilisateur). On redonne donc ici une couleur claire par défaut, + # que Bulma ne peut plus écraser (élément le plus proche gagne) — + # un texte/titre qui personnalise sa propre couleur reste bien sûr + # prioritaire sur celle-ci. + "color:#e8eaf0; padding:24px; width:100%; max-width:min(560px, 90%); max-height:90%; overflow:auto; box-sizing:border-box;" ) child_html = _render_children(el, slug, children_map, ctx) return ( diff --git a/tests/test_confort.py b/tests/test_confort.py index d62894bc..3b657b4e 100644 --- a/tests/test_confort.py +++ b/tests/test_confort.py @@ -181,6 +181,28 @@ def test_overlay_widget_renders_fullscreen_fixed_box(client, game): assert "forgeOverlayBox" in snippet +def test_overlay_box_default_text_color_survives_the_bulma_box_class(client, game): + """Régression : la classe Bulma ".box" (voir render_overlay.py) impose + elle-même une couleur de texte SOMBRE, pensée pour un fond blanc. Un + texte posé dans la boîte SANS couleur personnalisée (le cas par défaut + — aucun widget ne fige de couleur à sa création, voir + default_style_for_widget.py) héritait donc de ce gris sombre, invisible + sur le fond sombre par défaut de la boîte — "j'ai mis un texte dedans + mais il ne se voit pas", sans la moindre erreur serveur. La boîte doit + donc fixer elle-même une couleur de texte claire par défaut, que Bulma + ne peut plus écraser (élément le plus proche gagne).""" + screen_id = _create_screen(client, game) + overlay_id = _add_element(client, game, screen_id, "superposition") + client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"}) + + resp = client.get(f"/game/{game}/play") + html = resp.data.decode() + idx = html.find("forgeOverlayBox") + assert idx != -1 + box_tag = html[html.rfind(" Date: Fri, 28 Aug 2026 06:30:24 +0200 Subject: [PATCH 04/16] =?UTF-8?q?Corrige=20la=20vraie=20cause=20:=20la=20b?= =?UTF-8?q?o=C3=AEte=20de=20dialogue=20restait=20invisible=20DANS=20L'?= =?UTF-8?q?=C3=89DITEUR=20(masqu=C3=A9e=20par=20d=C3=A9faut)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Après vérification serveur, le texte était bel et bien rendu dans le HTML (couleur correcte incluse) — donc pas un souci de contenu ni de couleur. La vraie cause : le widget "Superposition / boîte de dialogue" démarre MASQUÉ par défaut à sa création (display:none, voir default_style_for_widget.py — pour ne pas couvrir tout l'écran dès qu'on le pose). Ce display:none est écrit tel quel dans le HTML aussi bien en mode jouable QUE dans l'éditeur, puisque le rendu de ce widget est partagé par les deux. Résultat : toute la boîte (et donc son contenu, peu importe le texte ou sa couleur) restait invisible dans le CANEVAS DE L'ÉDITEUR dès l'instant de sa création — impossible d'y voir/positionner visuellement ce qu'on pose dedans tant qu'on n'a pas pensé à basculer manuellement "Visibilité" sur "Visible" (puis à y repenser pour la remettre sur "Masqué" avant de tester en jeu). Correctif (render_overlay.py) : dans l'ÉDITEUR uniquement (détecté via le ctx "_forge_play_mode" déjà posé par list_elements.py pour le mode jouable), un "display:flex" est ajouté en dernier dans le style — gagnant sur le "display:none" par défaut (CSS : la dernière déclaration de la même propriété l'emporte). Le mode JOUABLE, lui, continue de respecter ce réglage normalement (masqué tant qu'aucune action ne l'affiche). Nouveau test, confirmé en échec sur l'ancien code puis au vert avec le correctif : vérifie explicitement que le style se termine par "display:flex;" dans l'éditeur et par "display:none;" en mode jouable. 139 tests au vert au total. Jeu de démo régénéré. Co-Authored-By: Claude Sonnet 5 --- screens/rendering/render_overlay.py | 17 +++++++++++++++++ tests/test_confort.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/screens/rendering/render_overlay.py b/screens/rendering/render_overlay.py index c573921f..d5a55814 100644 --- a/screens/rendering/render_overlay.py +++ b/screens/rendering/render_overlay.py @@ -44,6 +44,23 @@ def _render_overlay(el, meta, slug, children_map=None, ctx=None, parent_flex_dir "align-items:center; justify-content:center; background:rgba(0,0,0,0.6);" ) style = base_overlay_style + " " + _style_string(el, parent_flex_direction=parent_flex_direction) + if not (ctx and ctx.get("_forge_play_mode")): + # Dans l'ÉDITEUR (ctx sans _forge_play_mode — voir list_elements.py, + # posé uniquement en mode JOUABLE), ce widget doit rester visible + # quel que soit son réglage de visibilité statique — "Masqué" par + # défaut à SA création (default_style_for_widget.py), pour ne pas + # couvrir tout l'écran dès qu'on le pose. Sans ce forçage, la boîte + # de dialogue reste display:none dans le canevas de l'éditeur, donc + # invisible dès sa création : impossible d'y voir/positionner son + # contenu tant qu'on n'a pas basculé "Visibilité" sur "Visible" à la + # main (puis pensé à la remettre sur "Masqué" avant de tester). Le + # "display:flex;" ajouté ICI, en dernier dans la chaîne de style, + # gagne sur le "display:none" éventuellement posé plus tôt par + # _style_string (CSS : même propriété déclarée deux fois -> la + # dernière l'emporte). Seul le mode JOUABLE respecte réellement ce + # réglage (ou une action "Modifier un élément → Visibilité" qui le + # change en cours de partie).""" + style += " display:flex;" attr_parts = [f'style="{html_lib.escape(style)}"'] for k, v in attrs.items(): diff --git a/tests/test_confort.py b/tests/test_confort.py index 3b657b4e..983e146c 100644 --- a/tests/test_confort.py +++ b/tests/test_confort.py @@ -203,6 +203,35 @@ def test_overlay_box_default_text_color_survives_the_bulma_box_class(client, gam assert "color:#e8eaf0" in box_tag +def test_overlay_stays_visible_in_the_editor_despite_starting_masked(client, game): + """Régression : une "superposition" démarre MASQUÉE par défaut à sa + création (display:none — voir default_style_for_widget.py, pour ne + pas couvrir tout l'écran dès qu'on la pose). Ce display:none est écrit + tel quel dans le HTML, aussi bien en mode jouable QUE dans l'éditeur — + sans forçage, la boîte de dialogue restait donc invisible dans le + CANEVAS DE L'ÉDITEUR dès l'instant où elle était créée, rendant + impossible d'y voir/positionner visuellement titres/textes/boutons + posés à l'intérieur ("j'ai mis un texte dedans mais il ne se voit pas" + — le texte était là, mais toute la boîte qui le contient était + display:none). Seul le mode JOUABLE doit respecter ce réglage.""" + screen_id = _create_screen(client, game) + overlay_id = _add_element(client, game, screen_id, "superposition") + client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"}) + + edit_html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode() + idx = edit_html.find(f'id="elt-{overlay_id}"') + assert idx != -1 + wrapper_style = re.search(r'style="([^"]*)"', edit_html[edit_html.rfind(" Date: Fri, 28 Aug 2026 06:58:44 +0200 Subject: [PATCH 05/16] =?UTF-8?q?Corrige=20la=20bo=C3=AEte=20de=20dialogue?= =?UTF-8?q?=20pos=C3=A9e=20comme=20=C3=A9l=C3=A9ment=20de=20jeu=20r=C3=A9u?= =?UTF-8?q?tilisable=20:=20conteneur=20vide=20au=20lieu=20du=20dialogue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- filters/element_style_filter.py | 15 ++++++++-- screens/element_types/is_overlay_only.py | 16 +++++++++++ screens/elements/list_elements.py | 10 +++++++ screens/rendering/render_element_html.py | 33 ++++++++++++++++++---- tests/test_confort.py | 35 ++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 screens/element_types/is_overlay_only.py diff --git a/filters/element_style_filter.py b/filters/element_style_filter.py index 4618102b..c824ba82 100644 --- a/filters/element_style_filter.py +++ b/filters/element_style_filter.py @@ -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) + ";" diff --git a/screens/element_types/is_overlay_only.py b/screens/element_types/is_overlay_only.py new file mode 100644 index 00000000..bc611662 --- /dev/null +++ b/screens/element_types/is_overlay_only.py @@ -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"} diff --git a/screens/elements/list_elements.py b/screens/elements/list_elements.py index cc755cae..f047648c 100644 --- a/screens/elements/list_elements.py +++ b/screens/elements/list_elements.py @@ -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 diff --git a/screens/rendering/render_element_html.py b/screens/rendering/render_element_html.py index 909e205d..bf8dcd3e 100644 --- a/screens/rendering/render_element_html.py +++ b/screens/rendering/render_element_html.py @@ -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"]) diff --git a/tests/test_confort.py b/tests/test_confort.py index 983e146c..ab53612e 100644 --- a/tests/test_confort.py +++ b/tests/test_confort.py @@ -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('
Date: Fri, 28 Aug 2026 07:14:51 +0200 Subject: [PATCH 06/16] =?UTF-8?q?Retire=20le=20voile=20plein=20=C3=A9cran?= =?UTF-8?q?=20et=20le=20for=C3=A7age=20de=20visibilit=C3=A9=20de=20l'?= =?UTF-8?q?=C3=A9diteur=20(retour=20utilisateur)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux retours après le dernier correctif (78a373a, qui forçait "display:flex" dans l'éditeur pour que la boîte de dialogue reste visible) : - "je souhaite avoir la main sur la visibilité de la modale sinon elle s'affiche toujours sur la scène, court-circuite ma logique" — le forçage empêchait de vraiment utiliser "Visibilité" pendant l'édition. - "quand j'édite la modale ou quand je la mets dans une scène je souhaite que rien ne soit assombri, l'assombrissement ne se fait que quand la scène est jouée" — le voile plein écran (position:fixed + fond assombri) restait aussi actif dans l'éditeur. Correctif (render_overlay.py) : le voile plein écran ET le forçage de visibilité sont retirés de l'ÉDITEUR — ce widget s'y comporte maintenant comme un CONTENEUR NORMAL (position/taille selon x/y/width/height, aucun voile, réglage "Visibilité" respecté normalement, comme n'importe quel autre widget masqué). Le comportement plein écran/voile/masquage par défaut n'est conservé qu'en mode JOUABLE (ctx["_forge_play_mode"]). En creusant pourquoi "Visible" ne suffisait pas à faire réapparaître la boîte dans l'éditeur (menant l'utilisateur à essayer "Invisible" à la place, visible dans ses captures), trouvé un vrai bug latent dans save_element_controls.py : l'option "Visible" du réglage "Visibilité" ne touche volontairement jamais "display" (pour ne pas écraser le "display:flex" d'un conteneur en disposition ligne/colonne — voir visibility_control.py) — ça fonctionne seulement parce que, pour un widget AVEC un réglage "Disposition interne", celui-ci réaffirme lui-même un display non-"none" au même enregistrement. La "superposition" n'a PAS ce réglage : "display:none" (posé à la création ou par un "Masqué" précédent) restait donc bloqué pour toujours, quel que soit le nombre de fois où "Visible" était ensuite choisi. Corrigé : "Visible" efface aussi "display" pour tout widget SANS réglage "Disposition interne" (safe : les widgets qui EN ont un ne sont pas concernés, donc aucune régression sur leur comportement existant). Tests mis à jour (l'ancien test attendait le forçage, désormais retiré) + nouveau test qui couvre le cycle complet (masqué par défaut -> "Visible" choisi -> apparaît sans voile dans l'éditeur -> voile plein écran retrouvé en mode jouable). 140 tests au vert au total. Co-Authored-By: Claude Sonnet 5 --- screens/elements/save_element_controls.py | 19 +++++ screens/rendering/render_overlay.py | 96 ++++++++++++----------- tests/test_confort.py | 53 ++++++++----- 3 files changed, 103 insertions(+), 65 deletions(-) diff --git a/screens/elements/save_element_controls.py b/screens/elements/save_element_controls.py index a5c66542..2b16837b 100644 --- a/screens/elements/save_element_controls.py +++ b/screens/elements/save_element_controls.py @@ -3,6 +3,7 @@ import json import db from ..widgets.widget_meta import widget_meta +from ..widgets.layout_capable_widgets import LAYOUT_CAPABLE_WIDGETS from .get_element import get_element @@ -61,6 +62,24 @@ def save_element_controls(slug, element_id, form): style.pop(k, None) else: style[k] = v + if ( + control["key"] == "visibilite" and chosen == "visible" + and el["widget"] not in LAYOUT_CAPABLE_WIDGETS + ): + # "Visible" ne touche volontairement pas "display" (voir + # visibility_control.py) : sur un widget qui A un + # réglage "Disposition interne" (LAYOUT_CONTROLS, + # traité avant celui-ci sur le MÊME enregistrement, + # voir widget_meta.py), ce contrôle réaffirme lui-même + # un "display" non-"none" (flex/etc), donc "Visible" n'a + # rien à faire. Mais un widget SANS ce contrôle (ex. + # "superposition") n'a PERSONNE d'autre pour l'effacer : + # un "display:none" laissé par un "Masqué" précédent (ou + # par le réglage par défaut à la création, voir + # default_style_for_widget.py) restait donc bloqué + # masqué pour toujours, quel que soit le nombre de fois + # où "Visible" était ensuite choisi. + style.pop("display", None) continue if ctype == "scale": diff --git a/screens/rendering/render_overlay.py b/screens/rendering/render_overlay.py index d5a55814..a0f1342a 100644 --- a/screens/rendering/render_overlay.py +++ b/screens/rendering/render_overlay.py @@ -7,60 +7,61 @@ from .render_children import _render_children def _render_overlay(el, meta, slug, children_map=None, ctx=None, parent_flex_direction=None): """3.4 (Confort) — overlay/modale réutilisable : une boîte de dialogue - prête à l'emploi, par-dessus TOUT le reste de l'écran, fermeture - manuelle uniquement (aucun clic-en-dehors-pour-fermer volontairement, - conformément au manque documenté). Contrairement aux autres widgets, sa - position ne dépend PAS de x/y/width/height (glissé-déposé sur le - canevas) : `position:fixed; inset:0` la fait toujours couvrir tout - l'écran, quel que soit l'endroit où elle a été posée dans l'éditeur — - seul un voile semi-transparent + une boîte centrée, contenant les - éléments posés à l'intérieur (comme un conteneur normal). + prête à l'emploi, par-dessus TOUT le reste de l'écran EN MODE JOUABLE + UNIQUEMENT (`position:fixed; inset:0` + voile semi-transparent, ignore + x/y/width/height) — fermeture manuelle uniquement (aucun + clic-en-dehors-pour-fermer volontairement, conformément au manque + documenté). - Ouverture/fermeture : PAS de mécanisme dédié — elle réutilise l'action - existante "Modifier un élément → Visibilité" (masquer/rendre visible), - exactement comme n'importe quel autre élément. C'est pour ça qu'on - fusionne le style de positionnement fixe ci-dessous AVEC le style normal - de l'élément (_style_string) : si l'élément est réglé "Masqué" dans ses - propriétés, ou si une action "Modifier un élément" le cache plus tard, - ce masquage continue de fonctionner normalement. + Dans l'ÉDITEUR (ctx sans "_forge_play_mode" — posé uniquement en mode + jouable, voir list_elements.py), ce widget se comporte comme un + CONTENEUR NORMAL : position/taille selon x/y/width/height comme + n'importe quel widget, respect normal de son réglage "Visibilité" + (masqué = invisible dans l'éditeur aussi, comme tout autre widget), + AUCUN voile plein écran. Deux essais précédents corrigés à partir des + retours utilisateur : + - Fond assombri visible pendant l'édition ("je veux que rien ne soit + assombri, l'assombrissement ne se fait que quand la scène est + jouée") : le voile (background:rgba(...)) ne fait donc plus partie + du style de base, il n'est ajouté qu'en mode jouable. + - Visibilité forcée en permanence dans l'éditeur ("je veux avoir la + main sur la visibilité, sinon elle s'affiche toujours sur la scène + et court-circuite ma logique") : le forçage display:flex a donc été + retiré — l'éditeur respecte de nouveau fidèlement le réglage + "Visibilité" (masqué par défaut à la création, pour ne plus couvrir + tout l'écran EN JEU dès qu'on la pose — sans plus aucun rapport avec + l'éditeur, qui ne couvre plus jamais rien). + + Ouverture/fermeture (en JEU) : PAS de mécanisme dédié — elle réutilise + l'action existante "Modifier un élément → Visibilité" (masquer/rendre + visible), exactement comme n'importe quel autre élément. Classes Bulma ("modal is-active" / "box") posées en PLUS du style - inline existant, jamais à sa place : tout le positionnement/masquage - critique (position:fixed, z-index, display) reste en inline, qui gagne - toujours sur une règle de classe — si Bulma (chargé depuis un CDN, voir - play.html) ne se charge pas (hors-ligne), la boîte de dialogue continue - de fonctionner exactement pareil, ces classes n'ajoutant qu'un habillage - visuel (ombre, base de police) qui se dégrade sans casser quoi que ce - soit. Pas de ".modal-background" séparé : le voile semi-transparent est - déjà posé en inline sur cette même balise (background:rgba(...)) — un - second calque tout aussi transparent par-dessus n'ajouterait rien.""" + inline, jamais à sa place, et UNIQUEMENT en mode jouable (voir + ci-dessus) : tout le positionnement/masquage critique (position:fixed, + z-index, display) reste en inline, qui gagne toujours sur une règle de + classe — si Bulma (chargé depuis un CDN, voir play.html) ne se charge + pas (hors-ligne), la boîte de dialogue continue de fonctionner + exactement pareil. Pas de ".modal-background" séparé : le voile + semi-transparent est déjà posé en inline sur cette même balise + (background:rgba(...)) — un second calque tout aussi transparent + par-dessus n'ajouterait rien.""" + is_play_mode = bool(ctx and ctx.get("_forge_play_mode")) attrs_raw = el.get("attributes") or {} box_color = attrs_raw.get("_couleur_boite") or "#1f2430" radius = attrs_raw.get("_arrondi") or "12" attrs = _visible_attrs(el, meta, ctx) - base_overlay_style = ( - "position:fixed; inset:0; z-index:9999; display:flex; " - "align-items:center; justify-content:center; background:rgba(0,0,0,0.6);" - ) - style = base_overlay_style + " " + _style_string(el, parent_flex_direction=parent_flex_direction) - if not (ctx and ctx.get("_forge_play_mode")): - # Dans l'ÉDITEUR (ctx sans _forge_play_mode — voir list_elements.py, - # posé uniquement en mode JOUABLE), ce widget doit rester visible - # quel que soit son réglage de visibilité statique — "Masqué" par - # défaut à SA création (default_style_for_widget.py), pour ne pas - # couvrir tout l'écran dès qu'on le pose. Sans ce forçage, la boîte - # de dialogue reste display:none dans le canevas de l'éditeur, donc - # invisible dès sa création : impossible d'y voir/positionner son - # contenu tant qu'on n'a pas basculé "Visibilité" sur "Visible" à la - # main (puis pensé à la remettre sur "Masqué" avant de tester). Le - # "display:flex;" ajouté ICI, en dernier dans la chaîne de style, - # gagne sur le "display:none" éventuellement posé plus tôt par - # _style_string (CSS : même propriété déclarée deux fois -> la - # dernière l'emporte). Seul le mode JOUABLE respecte réellement ce - # réglage (ou une action "Modifier un élément → Visibilité" qui le - # change en cours de partie).""" - style += " display:flex;" + if is_play_mode: + base_overlay_style = ( + "position:fixed; inset:0; z-index:9999; display:flex; " + "align-items:center; justify-content:center; background:rgba(0,0,0,0.6);" + ) + style = base_overlay_style + " " + _style_string(el, parent_flex_direction=parent_flex_direction) + wrapper_class = "modal is-active" + else: + style = _style_string(el, parent_flex_direction=parent_flex_direction) + wrapper_class = "" attr_parts = [f'style="{html_lib.escape(style)}"'] for k, v in attrs.items(): @@ -91,8 +92,9 @@ def _render_overlay(el, meta, slug, children_map=None, ctx=None, parent_flex_dir "color:#e8eaf0; padding:24px; width:100%; max-width:min(560px, 90%); max-height:90%; overflow:auto; box-sizing:border-box;" ) child_html = _render_children(el, slug, children_map, ctx) + wrapper_class_attr = f'class="{wrapper_class}" ' if wrapper_class else "" return ( - f' +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+ + +
+
+ + diff --git a/templates/play.html b/templates/play.html index b49828f0..82bf95f3 100644 --- a/templates/play.html +++ b/templates/play.html @@ -835,13 +835,61 @@ } } + // Lit la valeur ACTUELLE d'une variable globale (gameData.variables, + // exposé par full_game_payload.py, tenu à jour par refreshRuntimeData() + // après toute action qui en modifie une), pour l'évaluation d'une + // condition — équivalent, côté client, de _resolve_filter_value côté + // serveur (screens/rendering/filter_repeater_rows.py), mais SANS la + // syntaxe utilisée par le Répéteur/la Condition de visibilité (jamais + // nécessaire ici : le nom de la variable est choisi dans un menu + // déroulant, voir screen_edit.html). + function readVariableValue(name) { + const v = (gameData.variables || {})[name]; + return v ? v.value : undefined; + } + + // Navigue dans une valeur JSON (variable de type "objet"/"tableau") + // selon un chemin ".champ"/"[index]" chaînable — équivalent JS de + // _resolve_variable_path (même fichier Python que ci-dessus). Chemin + // vide -> valeur brute inchangée (le cas normal pour une variable + // scalaire). Ne lève jamais : JSON invalide ou chemin qui ne correspond + // à rien -> null, comme côté serveur. + function resolveVariablePath(rawValue, path) { + if (!path) return rawValue; + let current; + try { current = rawValue ? JSON.parse(rawValue) : null; } catch (e) { return null; } + const segmentRe = /\.([^.\[\]]+)|\[(\d+)\]/g; + let m; + while ((m = segmentRe.exec(path)) !== null) { + if (current === null || current === undefined) return null; + current = m[1] !== undefined ? current[m[1]] : current[parseInt(m[2], 10)]; + } + return current === undefined ? null : current; + } + // 2.4 — conditions combinées (ET/OU) : un nœud Condition peut porter une // liste cond_clauses (JSON) en plus de sa clause historique. Un nœud sans // cond_clauses (tous les nœuds créés avant 2.4, ou un nœud à une seule // clause) garde EXACTEMENT son ancien comportement — une seule comparaison. + // + // Chaque clause peut tester soit un champ d'objet (source absente/"objet", + // comportement historique), soit une VARIABLE GLOBALE (source + // "variable" — voir ensure_flow_schema.py pour cond_source/cond_variable/ + // cond_variable_chemin). function evaluateConditionClause(clause) { + const source = clause.source ?? clause.cond_source ?? 'objet'; + const operator = clause.operator ?? clause.cond_operator; + const expected = clause.value ?? clause.cond_value; + if (source === 'variable') { + const varName = clause.variable ?? clause.cond_variable; + const path = clause.variable_chemin ?? clause.cond_variable_chemin; + const varInfo = (gameData.variables || {})[varName]; + const actual = resolveVariablePath(readVariableValue(varName), path); + const fieldType = varInfo && varInfo.type === 'booleen' ? 'booleen' : ''; + return compareValues(actual, operator, expected, fieldType); + } const actual = readFieldValue(clause.definition_id ?? clause.cond_definition_id, clause.row_id ?? clause.cond_row_id, clause.field ?? clause.cond_field); - return compareValues(actual, clause.operator ?? clause.cond_operator, clause.value ?? clause.cond_value, clause.field_type ?? clause.cond_field_type); + return compareValues(actual, operator, expected, clause.field_type ?? clause.cond_field_type); } function evaluateConditionNode(node) { diff --git a/templates/screen_edit.html b/templates/screen_edit.html index 25e9c038..c6333d16 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -219,16 +219,40 @@ + + +

Aucune condition = la ligne la plus récente de cet objet.

+ + {% set clause = {"champ": "", "operateur": "egal", "valeur": ""} %} + {% elif c.type == 'shadow' %}
@@ -1621,8 +1684,9 @@ function initBuilderPanel() { bindPropsAutosave(); bindJaugeDefinitionSelect(); bindDefinitionFieldSelects('field-definition_id', ['field-filtre_champ', 'field-filtre2_champ']); - bindDefinitionFieldSelects('field-data_definition_id', ['field-data_filtre_champ', 'field-data_filtre2_champ']); bindDefinitionFieldSelects('field-visibilite_cond_definition_id', ['field-visibilite_cond_champ']); + bindClauseListDefinitionSelect('field-data_definition_id'); + bindClauseListRows(); initFilterValuePickers(); var visCondModeSel = document.getElementById('field-visibilite_cond_mode'); if (visCondModeSel) toggleVisCondFields(visCondModeSel); @@ -1701,6 +1765,88 @@ function bindDefinitionFieldSelects(defSelId, fieldSelIds) { }); } +// ---------- "Donnée liée" : liste de conditions ILLIMITÉE (champ/opérateur/ +// valeur), combinées entre elles par ET/OU (voir c_clause_list.py, +// save_element_controls.py, resolve_bound_row.py). Chaque ligne existante +// est rendue côté serveur (au chargement du panneau) ; "+ Ajouter une +// condition" clone un