Corrige la nav pjax, panneau déplaçable/redimensionnable pour éditer un objet
1) Bug pjax trouvé : swapDocument() cherchait "header.topbar", qui n'a jamais existé (c'est un <nav>, pas un <header>) — la barre de navigation du jeu (ajoutée après pjax.js) n'était donc jamais mise à jour pendant une navigation pjax : absente en arrivant sur un jeu sans Ctrl+F5, et inversement laissée en place en revenant sur l'accueil où elle n'a rien à faire. Fix : tout ce qui est hors <main> mais doit changer d'une page à l'autre (topbar + barre du jeu) est regroupé dans un nouveau conteneur stable #pageChrome, que pjax.js remplace en bloc — plus fiable qu'un sélecteur qui ne correspondait à rien. CSS (flex:0 0 auto des layouts plein-écran) mis à jour en conséquence. 2) Renommages demandés : onglet/panneau "Écrans du jeu" -> "Écrants", "Éléments de jeu" -> "Templates" (tab, titre de panneau, bouton "+ Créer un template", état vide, infobulle, confirmation de suppression). 3) Le crayon ✏️ sur une ligne d'objet ouvre désormais un panneau déplaçable ET redimensionnable (nouveau coin de redimensionnement générique, .floatPanelResizeHandle) au lieu de naviguer vers object_edit.html (retirée) — un panneau par objet, pré-rendu et caché par défaut. Reprend telles quelles les fonctionnalités de l'ancienne page : renommer l'objet, ajouter un champ (ligne compacte), modifier/supprimer un champ existant (tableau dense déjà repris pour "Nouvel objet"), supprimer l'objet. Les routes de champs (object_field_ add/edit/delete) et object_edit lui-même redirigent maintenant vers le tableau de bord avec ?edit=<id>, pour rouvrir automatiquement le bon panneau après l'action plutôt que de le fermer silencieusement. makeFloatPanelDraggable()/makeFloatPanelResizable() généralisées pour être partagées entre "Nouvel objet" et les panneaux d'édition, plutôt que du code dupliqué par panneau. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e1216e99d5
commit
b8dfdcb205
@@ -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,
|
||||
|
||||
@@ -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/<slug>/objects/<int:definition_id>/edit", methods=["GET", "POST"])
|
||||
def object_edit(slug, definition_id):
|
||||
game = db.game_meta(slug)
|
||||
"""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=<id>)."""
|
||||
if request.method == "POST":
|
||||
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]
|
||||
|
||||
if request.method == "POST":
|
||||
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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -8,4 +8,4 @@ from core.flask_app import app
|
||||
@app.route("/game/<slug>/objects/<int:definition_id>/fields/<int:field_id>/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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
+7
-6
@@ -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
|
||||
* <title>, 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);
|
||||
|
||||
+10
-2
@@ -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%); }
|
||||
|
||||
+14
-1
@@ -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>
|
||||
|
||||
+220
-22
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user