diff --git a/routes/games/game_dashboard.py b/routes/games/game_dashboard.py index 886b08a5..3f679ddc 100644 --- a/routes/games/game_dashboard.py +++ b/routes/games/game_dashboard.py @@ -16,12 +16,19 @@ def game_dashboard(slug): liens.""" game = db.game_meta(slug) definitions = db.list_definitions(slug) + # Détail complet (avec ses champs) de chaque objet, pour le panneau + # d'édition déplaçable (voir game_dashboard.html) ouvert par le crayon + # ✏️ de l'onglet Objets — un panneau par objet, pré-rendu ici et + # simplement montré/masqué en JS, plutôt qu'un aller-retour serveur à + # l'ouverture (voir openObjectEditPanel()). + definitions_full = {d["id"]: db.get_definition(slug, d["id"]) for d in definitions} screen_list = screens.list_screens(slug) element_types = screens.list_element_types(slug) element_type_usage = {t["id"]: screens.element_type_usage_count(slug, t["id"]) for t in element_types} variables = db.list_global_variables(slug) return render_template( "game_dashboard.html", game=game, definitions=definitions, + definitions_full=definitions_full, screen_list=screen_list, element_types=element_types, element_type_usage=element_type_usage, variables=variables, variable_types=db.GLOBAL_VARIABLE_TYPES, field_types=db.FIELD_TYPES, diff --git a/routes/objects/object_edit.py b/routes/objects/object_edit.py index d96d1ea1..4ee56339 100644 --- a/routes/objects/object_edit.py +++ b/routes/objects/object_edit.py @@ -1,4 +1,4 @@ -from flask import render_template, request, redirect, url_for, abort +from flask import request, redirect, url_for, abort import db @@ -7,22 +7,18 @@ from core.flask_app import app @app.route("/game//objects//edit", methods=["GET", "POST"]) def object_edit(slug, definition_id): - game = db.game_meta(slug) - definition = db.get_definition(slug, definition_id) - if not definition: - abort(404) - existing_definitions = [d for d in db.list_definitions(slug) if d["id"] != definition_id] - + """Renommer un objet + gérer ses champs se fait désormais depuis un + panneau déplaçable/redimensionnable sur le tableau de bord (voir + game_dashboard.html, ouvert par le crayon ✏️ de l'onglet Objets) plutôt + qu'une page dédiée — cette route ne fait donc plus que traiter le + renommage. Un GET direct redirige vers le tableau de bord, en rouvrant + le panneau de cet objet (?edit=).""" if request.method == "POST": + definition = db.get_definition(slug, definition_id) + if not definition: + abort(404) new_name = request.form.get("object_name", "").strip() if new_name and new_name != definition["name"]: db.rename_definition(slug, definition_id, new_name) - return redirect(url_for("object_edit", slug=slug, definition_id=definition_id)) - return render_template( - "object_edit.html", - game=game, - definition=definition, - existing_definitions=existing_definitions, - field_types=db.FIELD_TYPES, - ) + return redirect(url_for("game_dashboard", slug=slug, tab="objects", edit=definition_id)) diff --git a/routes/objects/object_field_add.py b/routes/objects/object_field_add.py index 49a6afd7..6fecc0e4 100644 --- a/routes/objects/object_field_add.py +++ b/routes/objects/object_field_add.py @@ -17,4 +17,4 @@ def object_field_add(slug, definition_id): "min_value": request.form.get("field_min") or None, "max_value": request.form.get("field_max") or None, }) - return redirect(url_for("object_edit", slug=slug, definition_id=definition_id)) + return redirect(url_for("game_dashboard", slug=slug, tab="objects", edit=definition_id)) diff --git a/routes/objects/object_field_delete.py b/routes/objects/object_field_delete.py index 4e2a719a..c6247f77 100644 --- a/routes/objects/object_field_delete.py +++ b/routes/objects/object_field_delete.py @@ -8,4 +8,4 @@ from core.flask_app import app @app.route("/game//objects//fields//delete", methods=["POST"]) def object_field_delete(slug, definition_id, field_id): db.delete_field(slug, definition_id, field_id) - return redirect(url_for("object_edit", slug=slug, definition_id=definition_id)) + return redirect(url_for("game_dashboard", slug=slug, tab="objects", edit=definition_id)) diff --git a/routes/objects/object_field_edit.py b/routes/objects/object_field_edit.py index b77c799d..8066a2e2 100644 --- a/routes/objects/object_field_edit.py +++ b/routes/objects/object_field_edit.py @@ -17,4 +17,4 @@ def object_field_edit(slug, definition_id, field_id): min_value=request.form.get("field_min") or None, max_value=request.form.get("field_max") or None, ) - return redirect(url_for("object_edit", slug=slug, definition_id=definition_id)) + return redirect(url_for("game_dashboard", slug=slug, tab="objects", edit=definition_id)) diff --git a/static/pjax.js b/static/pjax.js index 3aa18bac..7ed7377b 100644 --- a/static/pjax.js +++ b/static/pjax.js @@ -5,8 +5,9 @@ * générées en Jinja) — on intercepte simplement chaque clic sur un lien * interne et chaque soumission de formulaire interne, on va chercher la * page suivante nous-mêmes en arrière-plan (fetch), et on ne remplace que - * , la classe du <body>, l'en-tête (fil d'Ariane) et le contenu - * principal (<main>) — au lieu de laisser le navigateur tout recharger. + * <title>, la classe du <body>, #pageChrome (topbar + barre de navigation + * du jeu, voir base.html) et le contenu principal (<main>) — au lieu de + * laisser le navigateur tout recharger. * L'URL affichée et l'historique (bouton "précédent") restent corrects * grâce à history.pushState/popstate. * @@ -77,10 +78,10 @@ currentMain.className = newMain.className; currentMain.innerHTML = newMain.innerHTML; - var newHeader = doc.querySelector("header.topbar"); - var currentHeader = document.querySelector("header.topbar"); - if (newHeader && currentHeader) { - currentHeader.innerHTML = newHeader.innerHTML; + var newChrome = doc.querySelector("#pageChrome"); + var currentChrome = document.querySelector("#pageChrome"); + if (newChrome && currentChrome) { + currentChrome.innerHTML = newChrome.innerHTML; } mergeHeadAssets(doc.head); diff --git a/static/style.css b/static/style.css index 80dbdd33..e0c0d947 100644 --- a/static/style.css +++ b/static/style.css @@ -104,7 +104,7 @@ button.danger{ color:var(--danger); border-color:rgba(226,104,95,.4); } a beaucoup de champs, pour garder une vue d'ensemble sans perdre le formulaire "Ajouter un champ" hors de l'écran. */ body.objectEditBody{ display:flex; flex-direction:column; height:100vh; margin:0; overflow:hidden; } -body.objectEditBody > .topbar, body.objectEditBody > .gameNavBar{ flex:0 0 auto; } +body.objectEditBody > #pageChrome{ flex:0 0 auto; } body.objectEditBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; display:flex; flex-direction:column; } .content-objectEdit{ width:100%; display:flex; flex-direction:column; min-height:0; } @@ -139,7 +139,7 @@ body.objectEditBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden ce sont les zones internes (panneaux, écran, panneau de logique) qui défilent chacune de leur côté si besoin. */ body.builderBody{ display:flex; flex-direction:column; height:100vh; margin:0; overflow:hidden; } -body.builderBody > .topbar, body.builderBody > .gameNavBar{ flex:0 0 auto; } +body.builderBody > #pageChrome{ flex:0 0 auto; } body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; display:flex; flex-direction:column; } .content-builder{ @@ -190,6 +190,14 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d box-shadow:0 12px 40px rgba(0,0,0,.55); } .floatPanel.hidden{ display:none; } +/* Coin de redimensionnement (voir wireFloatPanelResize() dans + game_dashboard.html) — un panneau devient redimensionnable simplement en + lui ajoutant ce coin, JS mis à part rien d'autre à faire côté CSS. */ +.floatPanelResizeHandle{ + position:absolute; right:0; bottom:0; width:16px; height:16px; cursor:nwse-resize; + background:linear-gradient(135deg, transparent 50%, var(--border) 50%, var(--border) 60%, transparent 60%, transparent 70%, var(--border) 70%, var(--border) 80%, transparent 80%); + border-radius:0 0 10px 0; +} .floatPanel--left{ left:24px; width:280px; } .floatPanel--right{ right:24px; width:340px; } .floatPanel--center{ left:50%; width:560px; max-width:calc(100vw - 48px); transform:translateX(-50%); } diff --git a/templates/base.html b/templates/base.html index b73a183c..63ac44dc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,6 +9,18 @@ {% block extra_head %}{% endblock %} </head> <body class="{% block body_class %}{% endblock %}"> +<!-- #pageChrome : tout ce qui est en dehors de <main> mais doit quand même + changer d'une page à l'autre (barre du jeu comprise) — un seul bloc + stable pour que pjax.js puisse le remplacer d'un coup à chaque + navigation (voir swapDocument() dans pjax.js). Avant l'ajout de ce + conteneur, pjax.js cherchait un <header class="topbar"> qui n'a jamais + existé (la barre est un <nav>, pas un <header>) : la barre de + navigation du jeu (ajoutée après pjax.js) ne changeait donc jamais + pendant la navigation pjax — absente en arrivant sur un jeu tant qu'on + ne rechargeait pas la page en entier (Ctrl+F5), et inversement + laissée en place en revenant sur l'accueil (liste des jeux) où elle + n'a pourtant rien à faire. --> +<div id="pageChrome"> <nav class="navbar topbar" role="navigation" aria-label="main navigation"> <div class="navbar-brand"> <a href="{{ url_for('index') }}" class="navbar-item brand"><strong>🛠️ Forge Engine</strong></a> @@ -25,10 +37,11 @@ délibérément le DERNIER lien — une action à part (ouvre l'aperçu jouable dans un nouvel onglet), pas une page de gestion de plus. --> <nav class="gameNavBar" aria-label="Navigation du jeu"> - <a href="{{ url_for('game_dashboard', slug=game.slug) }}" class="gameNavLink {{ 'active' if request.path == '/game/' ~ game.slug else '' }}" title="Écrans, objets, éléments de jeu, variables">📊 Tableau de bord</a> + <a href="{{ url_for('game_dashboard', slug=game.slug) }}" class="gameNavLink {{ 'active' if request.path == '/game/' ~ game.slug else '' }}" title="Écrants, objets, templates, variables">📊 Tableau de bord</a> <a href="{{ url_for('game_play', slug=game.slug) }}" target="_blank" class="gameNavLink" title="Aperçu jouable du jeu">▶️ Jouer</a> </nav> {% endif %} +</div> <main class="content {% block content_class %}{% endblock %}"> {% block content %}{% endblock %} </main> diff --git a/templates/game_dashboard.html b/templates/game_dashboard.html index 5feb20a3..5870b6d3 100644 --- a/templates/game_dashboard.html +++ b/templates/game_dashboard.html @@ -10,9 +10,9 @@ changer de page, avec à chaque fois un vrai tableau (dense, colonnes nettes) plutôt que des cartes compactes. --> <div class="builderTabs" id="dashTabs"> - <button type="button" class="builderTabBtn active" data-tab="screens" onclick="switchDashTab('screens')">🖥️ Écrans du jeu <span class="dashTabCount">{{ screen_list|length }}</span></button> + <button type="button" class="builderTabBtn active" data-tab="screens" onclick="switchDashTab('screens')">🖥️ Écrants <span class="dashTabCount">{{ screen_list|length }}</span></button> <button type="button" class="builderTabBtn" data-tab="objects" onclick="switchDashTab('objects')">🗂️ Objets <span class="dashTabCount">{{ definitions|length }}</span></button> - <button type="button" class="builderTabBtn" data-tab="element_types" onclick="switchDashTab('element_types')">🧩 Éléments de jeu <span class="dashTabCount">{{ element_types|length }}</span></button> + <button type="button" class="builderTabBtn" data-tab="element_types" onclick="switchDashTab('element_types')">🧩 Templates <span class="dashTabCount">{{ element_types|length }}</span></button> <button type="button" class="builderTabBtn" data-tab="variables" onclick="switchDashTab('variables')">🔧 Variables <span class="dashTabCount">{{ variables|length }}</span></button> </div> @@ -24,7 +24,7 @@ </div> {% endif %} <div class="dashPanelHeader"> - <h2>Écrans du jeu</h2> + <h2>Écrants</h2> <button type="button" class="button is-small primary" onclick="toggleDashCreate('screens')">+ Créer un écran</button> </div> <form method="post" action="{{ url_for('screens_new', slug=game.slug) }}" class="dashCreateBar" id="dashCreate-screens" style="display:none;"> @@ -78,7 +78,7 @@ <td><a href="{{ url_for('object_view', slug=game.slug, definition_id=d.id) }}">{{ d.name }}</a></td> <td class="has-text-grey">{{ d.table_name }}</td> <td class="fieldsTableActions"> - <a class="button is-small" href="{{ url_for('object_edit', slug=game.slug, definition_id=d.id) }}" title="Modifier cet objet">✏️</a> + <button type="button" class="button is-small" onclick="openObjectEditPanel({{ d.id }})" title="Modifier cet objet">✏️</button> </td> </tr> {% endfor %} @@ -90,11 +90,11 @@ {% endif %} </div> -<!-- ---------- Éléments de jeu ---------- --> +<!-- ---------- Templates ---------- --> <div class="builderTabPanel" data-tab-panel="element_types"> <div class="dashPanelHeader"> - <h2>Éléments de jeu</h2> - <button type="button" class="button is-small primary" onclick="toggleDashCreate('element_types')">+ Créer un élément de jeu</button> + <h2>Templates</h2> + <button type="button" class="button is-small primary" onclick="toggleDashCreate('element_types')">+ Créer un template</button> </div> <form method="post" action="{{ url_for('element_types', slug=game.slug) }}" class="dashCreateBar" id="dashCreate-element_types" style="display:none;"> <input type="text" name="icon" value="🧩" maxlength="4" style="max-width:60px;" title="Icône"> @@ -122,9 +122,9 @@ {% endif %} </td> <td class="fieldsTableActions"> - <a class="button is-small" href="{{ url_for('screen_edit', slug=game.slug, screen_id=t.template_screen_id) }}" title="Modifier le contenu de cet élément de jeu">✏️</a> + <a class="button is-small" href="{{ url_for('screen_edit', slug=game.slug, screen_id=t.template_screen_id) }}" title="Modifier le contenu de ce template">✏️</a> <form method="post" action="{{ url_for('element_type_delete', slug=game.slug, element_type_id=t.id) }}" - onsubmit="return confirm('Supprimer l\'élément de jeu « {{ t.name }} » du catalogue (et tout son contenu) ?');"> + onsubmit="return confirm('Supprimer le template « {{ t.name }} » du catalogue (et tout son contenu) ?');"> <button type="submit" class="button is-small is-danger" {{ 'disabled title="Utilisé sur un écran — retire-le des écrans d\'abord"' if element_type_usage.get(t.id, 0) > 0 else '' }}>🗑️</button> </form> </td> @@ -134,7 +134,7 @@ </table> </div> {% else %} - <p class="hint">Aucun élément de jeu pour l'instant.</p> + <p class="hint">Aucun template pour l'instant.</p> {% endif %} </div> @@ -245,8 +245,136 @@ <div class="actionsRow"><button type="submit" class="primary">Créer l'objet</button></div> </form> </div> + <div class="floatPanelResizeHandle" id="newObjectPanelResizeHandle"></div> </div> +<!-- ---------- Panneaux déplaçables : modifier un objet (un par objet) ---------- --> +<!-- Un panneau par objet, pré-rendu ici et caché par défaut (voir + openObjectEditPanel()) — évite un aller-retour serveur à l'ouverture. + Remplace l'ancienne page object_edit.html (retirée) : même renommage, + même formulaire "Ajouter un champ" (une ligne compacte, pas la grosse + carte d'origine), et le même tableau "Champs existants" repris tel + quel (déjà dense et sobre). --> +{% for d in definitions %} +{% set definition = definitions_full[d.id] %} +{% set existing_definitions = definitions|rejectattr('id', 'equalto', d.id)|list %} +<div class="floatPanel floatPanel--center hidden objectEditPanel" data-definition-id="{{ d.id }}" id="objectEditPanel-{{ d.id }}"> + <div class="floatPanelHeader"> + <span class="floatPanelTitle">⋮⋮ {{ definition.name }}</span> + <button type="button" class="button is-small" onclick="closeObjectEditPanel({{ d.id }})">✕</button> + </div> + <div class="floatPanelBody"> + <form method="post" action="{{ url_for('object_edit', slug=game.slug, definition_id=d.id) }}" class="field has-addons"> + <div class="control is-expanded"> + <label class="label" for="object_name-{{ d.id }}">Nom de l'objet</label> + <input class="input" type="text" id="object_name-{{ d.id }}" name="object_name" value="{{ definition.name }}" required> + </div> + <div class="control" style="align-self:flex-end;"> + <button type="submit" class="button">Renommer</button> + </div> + </form> + <p class="help mb-3">Table SQL : {{ definition.table_name }}</p> + + <h2>Ajouter un champ</h2> + <form method="post" action="{{ url_for('object_field_add', slug=game.slug, definition_id=d.id) }}" class="dashCreateBar objectAddFieldBar"> + <input type="text" name="field_name" placeholder="Nom du champ" required autocomplete="off"> + <select name="field_type" class="fieldTypeSelect"> + {% for key, val in field_types.items() %} + <option value="{{ key }}">{{ val.label }}</option> + {% endfor %} + </select> + <select name="field_relation" class="relationTarget" style="display:none;"> + {% for od in existing_definitions %} + <option value="{{ od.id }}">{{ od.name }}</option> + {% endfor %} + </select> + <input type="number" step="any" name="field_min" placeholder="Mini" class="boundsTarget" style="display:none;"> + <input type="number" step="any" name="field_max" placeholder="Maxi" class="boundsTarget" style="display:none;"> + <select name="field_required"> + <option value="0">Non obligatoire</option> + <option value="1">Obligatoire</option> + </select> + <button type="submit" class="primary">+ Ajouter</button> + </form> + + <h2>Champs existants</h2> + {% if definition.fields %} + {% for f in definition.fields %} + <form method="post" action="{{ url_for('object_field_edit', slug=game.slug, definition_id=d.id, field_id=f.id) }}" id="fieldEditForm{{ f.id }}"></form> + <form method="post" action="{{ url_for('object_field_delete', slug=game.slug, definition_id=d.id, field_id=f.id) }}" id="fieldDeleteForm{{ f.id }}" + onsubmit="return confirm('Retirer le champ « {{ f.name }} » ? Les données déjà enregistrées dans ce champ seront perdues.');"></form> + {% endfor %} + <div class="fieldsTableWrap"> + <table class="table is-fullwidth is-hoverable fieldsTable"> + <thead> + <tr><th>Nom</th><th>Type</th><th>Objet lié</th><th>Mini</th><th>Maxi</th><th>Obligatoire</th><th></th></tr> + </thead> + <tbody> + {% for f in definition.fields %} + <tr> + <td><input class="input is-small" type="text" form="fieldEditForm{{ f.id }}" name="field_name" value="{{ f.name }}" aria-label="Nom du champ « {{ f.name }} »" required></td> + <td>{{ field_types.get(f.type, {}).get('label', f.type) }}</td> + <td> + {% if f.type == 'relation' %} + <div class="select is-small"> + <select form="fieldEditForm{{ f.id }}" name="field_relation" aria-label="Objet lié pour « {{ f.name }} »"> + {% for od in existing_definitions %} + <option value="{{ od.id }}" {{ 'selected' if od.id == f.relation_definition_id else '' }}>{{ od.name }}</option> + {% endfor %} + </select> + </div> + {% else %} + <span class="has-text-grey">—</span> + {% endif %} + </td> + <td> + {% if f.type in ('nombre_entier', 'nombre_decimal') %} + <input class="input is-small" type="number" step="any" form="fieldEditForm{{ f.id }}" name="field_min" value="{{ f.min_value if f.min_value is not none else '' }}" placeholder="ex. 0" aria-label="Valeur mini pour « {{ f.name }} »"> + {% else %} + <span class="has-text-grey">—</span> + {% endif %} + </td> + <td> + {% if f.type in ('nombre_entier', 'nombre_decimal') %} + <input class="input is-small" type="number" step="any" form="fieldEditForm{{ f.id }}" name="field_max" value="{{ f.max_value if f.max_value is not none else '' }}" placeholder="ex. 100" aria-label="Valeur maxi pour « {{ f.name }} »"> + {% else %} + <span class="has-text-grey">—</span> + {% endif %} + </td> + <td> + <div class="select is-small"> + <select form="fieldEditForm{{ f.id }}" name="field_required" aria-label="« {{ f.name }} » obligatoire ?"> + <option value="0" {{ 'selected' if not f.required else '' }}>Non</option> + <option value="1" {{ 'selected' if f.required else '' }}>Oui</option> + </select> + </div> + </td> + <td class="fieldsTableActions"> + <button type="submit" form="fieldEditForm{{ f.id }}" class="button is-small">Enregistrer</button> + <button type="submit" form="fieldDeleteForm{{ f.id }}" class="button is-small is-danger">🗑️</button> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + <p class="help">Mini/Maxi (bornage automatique) : laisser vide = pas de limite.</p> + {% else %} + <p class="help">Aucun champ pour l'instant.</p> + {% endif %} + + <div class="dangerZone"> + <h3>Supprimer cet objet</h3> + <form method="post" action="{{ url_for('object_delete', slug=game.slug, definition_id=d.id) }}" + onsubmit="return confirm('Supprimer définitivement l\'objet « {{ definition.name }} », sa table et toutes ses données ? Cette action est irréversible.');"> + <button type="submit" class="danger">🗑️ Supprimer l'objet « {{ definition.name }} »</button> + </form> + </div> + </div> + <div class="floatPanelResizeHandle"></div> +</div> +{% endfor %} + <template id="fieldRowTemplate"> <tr> <td><input class="input is-small" type="text" name="field_name[]" placeholder="ex. nom, pv..." required></td> @@ -316,19 +444,12 @@ function toggleDashCreate(tab) { if (requestedTab) switchDashTab(requestedTab); })(); -// ---------- Panneau déplaçable "Nouvel objet" ---------- -(function () { - var panel = document.getElementById('newObjectPanel'); - var header = document.getElementById('newObjectPanelHeader'); +// ---------- Panneaux flottants : glisser-déposer + redimensionnement ---------- +// Générique — utilisé aussi bien par "Nouvel objet" que par un panneau +// d'édition d'objet (un par objet, voir plus bas) : mêmes gestes partout +// dans l'outil plutôt qu'une variante par panneau. +function makeFloatPanelDraggable(panel, header) { var dragging = false, offsetX = 0, offsetY = 0; - - window.openNewObjectPanel = function () { - panel.classList.remove('hidden'); - }; - window.closeNewObjectPanel = function () { - panel.classList.add('hidden'); - }; - header.addEventListener('mousedown', function (e) { if (e.target.closest('button')) return; dragging = true; @@ -350,6 +471,83 @@ function toggleDashCreate(tab) { dragging = false; document.body.style.userSelect = ''; }); +} +function makeFloatPanelResizable(panel, handle) { + var resizing = false, startX = 0, startY = 0, startW = 0, startH = 0; + handle.addEventListener('mousedown', function (e) { + resizing = true; + var rect = panel.getBoundingClientRect(); + startX = e.clientX; startY = e.clientY; + startW = rect.width; startH = rect.height; + // Un coin qui redimensionne doit d'abord "détacher" le panneau de sa + // largeur/hauteur fixées par la variante CSS (--center...) — sinon la + // largeur en % ou le max-width continueraient à l'emporter sur le + // redimensionnement manuel. + panel.style.width = startW + 'px'; + panel.style.maxWidth = 'none'; + document.body.style.userSelect = 'none'; + e.preventDefault(); + }); + document.addEventListener('mousemove', function (e) { + if (!resizing) return; + panel.style.width = Math.max(320, startW + (e.clientX - startX)) + 'px'; + panel.style.height = Math.max(200, startH + (e.clientY - startY)) + 'px'; + }); + document.addEventListener('mouseup', function () { + resizing = false; + document.body.style.userSelect = ''; + }); +} + +// ---------- Panneau déplaçable "Nouvel objet" ---------- +(function () { + var panel = document.getElementById('newObjectPanel'); + makeFloatPanelDraggable(panel, document.getElementById('newObjectPanelHeader')); + makeFloatPanelResizable(panel, document.getElementById('newObjectPanelResizeHandle')); + window.openNewObjectPanel = function () { panel.classList.remove('hidden'); }; + window.closeNewObjectPanel = function () { panel.classList.add('hidden'); }; +})(); + +// ---------- Panneaux déplaçables "Modifier un objet" (un par objet) ---------- +(function () { + var NUMERIC_FIELD_TYPES = ['nombre_entier', 'nombre_decimal']; + document.querySelectorAll('.objectEditPanel').forEach(function (panel) { + makeFloatPanelDraggable(panel, panel.querySelector('.floatPanelHeader')); + makeFloatPanelResizable(panel, panel.querySelector('.floatPanelResizeHandle')); + + // Formulaire "Ajouter un champ" : une ligne fixe (pas de liste + // extensible comme dans le panneau "Nouvel objet"), donc pas besoin + // d'espace réservé "—" pour garder des colonnes alignées — un simple + // masquage suffit ici. + var addBar = panel.querySelector('.objectAddFieldBar'); + var typeSelect = addBar.querySelector('.fieldTypeSelect'); + var relationTarget = addBar.querySelector('.relationTarget'); + var boundsTargets = addBar.querySelectorAll('.boundsTarget'); + function syncAddFieldVisibility() { + relationTarget.style.display = typeSelect.value === 'relation' ? '' : 'none'; + var isNumeric = NUMERIC_FIELD_TYPES.indexOf(typeSelect.value) !== -1; + boundsTargets.forEach(function (el) { el.style.display = isNumeric ? '' : 'none'; }); + } + typeSelect.addEventListener('change', syncAddFieldVisibility); + syncAddFieldVisibility(); + }); + window.openObjectEditPanel = function (definitionId) { + document.querySelectorAll('.objectEditPanel').forEach(function (panel) { + panel.classList.toggle('hidden', panel.dataset.definitionId !== String(definitionId)); + }); + }; + window.closeObjectEditPanel = function (definitionId) { + var panel = document.querySelector('.objectEditPanel[data-definition-id="' + definitionId + '"]'); + if (panel) panel.classList.add('hidden'); + }; + + // Réouverture automatique après un aller-retour serveur (ajout/édition/ + // suppression d'un champ, renommage...) : ?edit=<id>, posé par les + // routes concernées (voir routes/objects/) sur leur redirection vers ce + // tableau de bord — sans ça, chaque action fermait le panneau et + // obligeait à recliquer sur le crayon pour voir le résultat. + var requestedEdit = {{ (request.args.get('edit') or '')|tojson }}; + if (requestedEdit) openObjectEditPanel(requestedEdit); })(); </script> {% endblock %} diff --git a/templates/object_edit.html b/templates/object_edit.html deleted file mode 100644 index a7cd4718..00000000 --- a/templates/object_edit.html +++ /dev/null @@ -1,189 +0,0 @@ -{% extends "base.html" %} -{% block title %}Modifier {{ definition.name }} — {{ game.name }}{% endblock %} -{% block content_class %}content-wide content-objectEdit{% endblock %} -{% block body_class %}objectEditBody{% endblock %} -{% block content %} -<h1 class="title">Modifier l'objet « {{ definition.name }} »</h1> -<p class="help mb-4">Table SQL : {{ definition.table_name }}</p> - -<form method="post" class="field has-addons"> - <div class="control is-expanded"> - <label class="label" for="object_name">Nom de l'objet</label> - <input class="input" type="text" id="object_name" name="object_name" value="{{ definition.name }}" required> - </div> - <div class="control" style="align-self:flex-end;"> - <button type="submit" class="button">Renommer</button> - </div> -</form> - -<div class="twoCol"> - <!-- Colonne de gauche : ajouter un champ + supprimer l'objet --> - <div class="colLeft"> - <h2 class="subtitle">Ajouter un champ</h2> - <form method="post" action="{{ url_for('object_field_add', slug=game.slug, definition_id=definition.id) }}" class="box" id="addFieldForm"> - <div class="field"> - <label class="label">Nom du champ</label> - <div class="control"><input class="input" type="text" name="field_name" placeholder="ex. nom, description, pv..." required></div> - </div> - - <div class="field"> - <label class="label">Type</label> - <div class="control"> - <div class="select is-fullwidth"> - <select name="field_type" id="newFieldType"> - {% for key, val in field_types.items() %} - <option value="{{ key }}">{{ val.label }}</option> - {% endfor %} - </select> - </div> - </div> - </div> - - <div id="newFieldRelationWrap" class="field" style="display:none;"> - <label class="label">Objet lié</label> - <div class="control"> - <div class="select is-fullwidth"> - <select name="field_relation"> - {% for d in existing_definitions %} - <option value="{{ d.id }}">{{ d.name }}</option> - {% endfor %} - </select> - </div> - </div> - {% if not existing_definitions %} - <p class="help">Aucun autre objet défini.</p> - {% endif %} - </div> - - <div id="newFieldBoundsWrap" class="columns" style="display:none;"> - <div class="column"> - <div class="field"> - <label class="label">Valeur mini (optionnel)</label> - <div class="control"><input class="input" type="number" step="any" name="field_min" placeholder="ex. 0"></div> - </div> - </div> - <div class="column"> - <div class="field"> - <label class="label">Valeur maxi (optionnel)</label> - <div class="control"><input class="input" type="number" step="any" name="field_max" placeholder="ex. 100"></div> - </div> - </div> - </div> - - <div class="field"> - <label class="label">Obligatoire ?</label> - <div class="control"> - <div class="select is-fullwidth"> - <select name="field_required"> - <option value="0">Non</option> - <option value="1">Oui</option> - </select> - </div> - </div> - </div> - - <div class="field"><button type="submit" class="button is-primary">+ Ajouter ce champ</button></div> - </form> - - <div class="box has-background-danger-dark mt-4"> - <h3 class="title is-6 has-text-danger-light">Supprimer cet objet</h3> - <form method="post" action="{{ url_for('object_delete', slug=game.slug, definition_id=definition.id) }}" - onsubmit="return confirm('Supprimer définitivement l\'objet « {{ definition.name }} », sa table et toutes ses données ? Cette action est irréversible.');"> - <button type="submit" class="button is-danger">🗑️ Supprimer l'objet « {{ definition.name }} »</button> - </form> - </div> - </div> - - <!-- Colonne de droite : champs existants, chacun modifiable --> - <div class="colRight"> - <h2 class="subtitle">Champs existants</h2> - {% if definition.fields %} - {% for f in definition.fields %} - <form method="post" action="{{ url_for('object_field_edit', slug=game.slug, definition_id=definition.id, field_id=f.id) }}" id="fieldEditForm{{ f.id }}"></form> - <form method="post" action="{{ url_for('object_field_delete', slug=game.slug, definition_id=definition.id, field_id=f.id) }}" id="fieldDeleteForm{{ f.id }}" - onsubmit="return confirm('Retirer le champ « {{ f.name }} » ? Les données déjà enregistrées dans ce champ seront perdues.');"></form> - {% endfor %} - <div class="fieldsTableWrap"> - <table class="table is-fullwidth is-hoverable fieldsTable"> - <thead> - <tr> - <th>Nom du champ</th> - <th>Type</th> - <th>Objet lié</th> - <th>Mini</th> - <th>Maxi</th> - <th>Obligatoire ?</th> - <th></th> - </tr> - </thead> - <tbody> - {% for f in definition.fields %} - <tr> - <td><input class="input is-small" type="text" form="fieldEditForm{{ f.id }}" name="field_name" value="{{ f.name }}" aria-label="Nom du champ « {{ f.name }} »" required></td> - <td>{{ field_types.get(f.type, {}).get('label', f.type) }}</td> - <td> - {% if f.type == 'relation' %} - <div class="select is-small"> - <select form="fieldEditForm{{ f.id }}" name="field_relation" aria-label="Objet lié pour « {{ f.name }} »"> - {% for d in existing_definitions %} - <option value="{{ d.id }}" {{ 'selected' if d.id == f.relation_definition_id else '' }}>{{ d.name }}</option> - {% endfor %} - </select> - </div> - {% else %} - <span class="has-text-grey">—</span> - {% endif %} - </td> - <td> - {% if f.type in ('nombre_entier', 'nombre_decimal') %} - <input class="input is-small" type="number" step="any" form="fieldEditForm{{ f.id }}" name="field_min" value="{{ f.min_value if f.min_value is not none else '' }}" placeholder="ex. 0" aria-label="Valeur mini pour « {{ f.name }} »"> - {% else %} - <span class="has-text-grey">—</span> - {% endif %} - </td> - <td> - {% if f.type in ('nombre_entier', 'nombre_decimal') %} - <input class="input is-small" type="number" step="any" form="fieldEditForm{{ f.id }}" name="field_max" value="{{ f.max_value if f.max_value is not none else '' }}" placeholder="ex. 100" aria-label="Valeur maxi pour « {{ f.name }} »"> - {% else %} - <span class="has-text-grey">—</span> - {% endif %} - </td> - <td> - <div class="select is-small"> - <select form="fieldEditForm{{ f.id }}" name="field_required" aria-label="« {{ f.name }} » obligatoire ?"> - <option value="0" {{ 'selected' if not f.required else '' }}>Non</option> - <option value="1" {{ 'selected' if f.required else '' }}>Oui</option> - </select> - </div> - </td> - <td class="fieldsTableActions"> - <button type="submit" form="fieldEditForm{{ f.id }}" class="button is-small">Enregistrer</button> - <button type="submit" form="fieldDeleteForm{{ f.id }}" class="button is-small is-danger">🗑️</button> - </td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - <p class="help">Mini/Maxi (bornage automatique) : laisser vide = pas de limite. S'applique à "Donner cette valeur", "Augmenter de..." et "Diminuer de..." dans le graphe de logique.</p> - {% else %} - <p class="help">Aucun champ pour l'instant.</p> - {% endif %} - </div> -</div> - -<script> - (function(){ - var sel = document.getElementById('newFieldType'); - var relWrap = document.getElementById('newFieldRelationWrap'); - var boundsWrap = document.getElementById('newFieldBoundsWrap'); - var NUMERIC_TYPES = ['nombre_entier', 'nombre_decimal']; - function sync(){ - relWrap.style.display = sel.value === 'relation' ? 'block' : 'none'; - boundsWrap.style.display = NUMERIC_TYPES.indexOf(sel.value) !== -1 ? 'flex' : 'none'; - } - sel.addEventListener('change', sync); - sync(); - })(); -</script> -{% endblock %}