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>
554 lines
27 KiB
HTML
554 lines
27 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ game.name }} — Forge Engine{% endblock %}
|
|
{% block content_class %}content-wide{% endblock %}
|
|
{% block content %}
|
|
|
|
<!-- Une seule fenêtre, un onglet par catégorie (même principe que les
|
|
onglets "Écran / Logique / Timeline" de l'éditeur d'écran, voir
|
|
.builderTabs/.builderTabBtn/.builderTabPanel dans style.css) — pour
|
|
naviguer entre écrans/objets/éléments de jeu/variables sans jamais
|
|
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')">🖥️ É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')">🧩 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>
|
|
|
|
<!-- ---------- Écrans ---------- -->
|
|
<div class="builderTabPanel active" data-tab-panel="screens">
|
|
{% if request.args.get('blocked_screen') %}
|
|
<div class="warningBanner">
|
|
⚠️ Suppression impossible : au moins un autre écran a un bouton qui cible précisément celui-ci. Retire d'abord cette action.
|
|
</div>
|
|
{% endif %}
|
|
<div class="dashPanelHeader">
|
|
<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;">
|
|
<input type="text" name="name" placeholder="Nom du nouvel écran (ex. Accueil, Scène 1...)" required autocomplete="off">
|
|
<button type="submit" class="primary">Créer</button>
|
|
</form>
|
|
{% if screen_list %}
|
|
<div class="fieldsTableWrap">
|
|
<table class="table is-fullwidth is-hoverable fieldsTable">
|
|
<thead><tr><th>#</th><th>Nom</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for s in screen_list %}
|
|
<tr>
|
|
<td>{{ loop.index }}</td>
|
|
<td>
|
|
<a href="{{ url_for('screen_edit', slug=game.slug, screen_id=s.id) }}">{{ s.name }}</a>
|
|
{% if loop.first %} <span class="tag is-info is-light">★ Départ</span>{% endif %}
|
|
</td>
|
|
<td class="fieldsTableActions">
|
|
<form method="post" action="{{ url_for('screen_move', slug=game.slug, screen_id=s.id) }}"><input type="hidden" name="direction" value="up"><button type="submit" class="button is-small" {{ 'disabled' if loop.first else '' }}>↑</button></form>
|
|
<form method="post" action="{{ url_for('screen_move', slug=game.slug, screen_id=s.id) }}"><input type="hidden" name="direction" value="down"><button type="submit" class="button is-small" {{ 'disabled' if loop.last else '' }}>↓</button></form>
|
|
<a class="button is-small" href="{{ url_for('screen_edit', slug=game.slug, screen_id=s.id) }}">✏️</a>
|
|
<form method="post" action="{{ url_for('screen_delete', slug=game.slug, screen_id=s.id) }}"
|
|
onsubmit="return confirm('Supprimer l\'écran « {{ s.name }} » et tous ses éléments ?');">
|
|
<button type="submit" class="button is-small is-danger">🗑️</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="hint">Aucun écran pour l'instant.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- ---------- Objets ---------- -->
|
|
<div class="builderTabPanel" data-tab-panel="objects">
|
|
<div class="dashPanelHeader">
|
|
<h2>Objets</h2>
|
|
<button type="button" class="button is-small primary" onclick="openNewObjectPanel()">+ Nouvel objet</button>
|
|
</div>
|
|
{% if definitions %}
|
|
<div class="fieldsTableWrap">
|
|
<table class="table is-fullwidth is-hoverable fieldsTable">
|
|
<thead><tr><th>Nom</th><th>Table SQL</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for d in definitions %}
|
|
<tr>
|
|
<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">
|
|
<button type="button" class="button is-small" onclick="openObjectEditPanel({{ d.id }})" title="Modifier cet objet">✏️</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="hint">Aucun objet pour l'instant.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- ---------- Templates ---------- -->
|
|
<div class="builderTabPanel" data-tab-panel="element_types">
|
|
<div class="dashPanelHeader">
|
|
<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">
|
|
<input type="text" name="name" placeholder="Nom (ex. Personnage, Carte mail...)" required autocomplete="off">
|
|
<select name="bound_definition_id">
|
|
<option value="">(aucun objet lié)</option>
|
|
{% for d in definitions %}
|
|
<option value="{{ d.id }}">{{ d.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button type="submit" class="primary">Créer</button>
|
|
</form>
|
|
{% if element_types %}
|
|
<div class="fieldsTableWrap">
|
|
<table class="table is-fullwidth is-hoverable fieldsTable">
|
|
<thead><tr><th>Nom</th><th>Usage</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for t in element_types %}
|
|
<tr>
|
|
<td>{{ t.icon or '🧩' }} {{ t.name }}</td>
|
|
<td class="has-text-grey">
|
|
{{ element_type_usage.get(t.id, 0) }} instance(s)
|
|
{% if t.bound_definition_id %}
|
|
{% for d in definitions %}{% if d.id == t.bound_definition_id %} · « {{ d.name }} »{% endif %}{% endfor %}
|
|
{% 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 ce template">✏️</a>
|
|
<form method="post" action="{{ url_for('element_type_delete', slug=game.slug, element_type_id=t.id) }}"
|
|
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>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="hint">Aucun template pour l'instant.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- ---------- Variables ---------- -->
|
|
<div class="builderTabPanel" data-tab-panel="variables">
|
|
<div class="dashPanelHeader">
|
|
<h2>Variables</h2>
|
|
<button type="button" class="button is-small primary" onclick="toggleDashCreate('variables')">+ Créer une variable</button>
|
|
</div>
|
|
<form method="post" action="{{ url_for('create_global_var', slug=game.slug) }}" class="dashCreateBar" id="dashCreate-variables" style="display:none;">
|
|
<input type="text" name="name" placeholder="Nom (ex. porte_ouverte)" required autocomplete="off">
|
|
<select name="type">
|
|
{% for key, val in variable_types.items() %}
|
|
<option value="{{ key }}">{{ val.label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<input type="text" name="default_value" placeholder="Valeur de départ">
|
|
<button type="submit" class="primary">Créer</button>
|
|
</form>
|
|
{% if variables %}
|
|
{% for v in variables %}
|
|
<form method="post" action="{{ url_for('global_var_edit', slug=game.slug, var_id=v.id) }}" id="varEditForm{{ v.id }}"></form>
|
|
{% endfor %}
|
|
<div class="fieldsTableWrap">
|
|
<table class="table is-fullwidth is-hoverable fieldsTable">
|
|
<thead><tr><th>Nom</th><th>Type</th><th>Valeur</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for v in variables %}
|
|
<tr>
|
|
<td>{{ v.name }}</td>
|
|
<td>
|
|
<select form="varEditForm{{ v.id }}" name="type" aria-label="Type de « {{ v.name }} »">
|
|
{% for key, val in variable_types.items() %}
|
|
<option value="{{ key }}" {{ 'selected' if v.type == key else '' }}>{{ val.label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</td>
|
|
<td><input type="text" form="varEditForm{{ v.id }}" name="value" value="{{ v.value or '' }}" aria-label="Valeur de « {{ v.name }} »"></td>
|
|
<td class="fieldsTableActions">
|
|
<button type="submit" form="varEditForm{{ v.id }}" class="button is-small">Enregistrer</button>
|
|
<form method="post" action="{{ url_for('global_var_delete', slug=game.slug, var_id=v.id) }}"
|
|
onsubmit="return confirm('Supprimer la variable « {{ v.name }} » ? Toute condition de visibilité ou action qui la référence cessera de fonctionner.');">
|
|
<button type="submit" class="button is-small is-danger">🗑️</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="hint">Aucune variable pour l'instant.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="dangerZone dashDangerZone">
|
|
<h3>Paramètres du jeu</h3>
|
|
<form method="post" action="{{ url_for('game_rename', slug=game.slug) }}">
|
|
<label for="rename">Renommer le jeu</label>
|
|
<div class="fieldRow">
|
|
<input type="text" id="rename" name="game_name" value="{{ game.name }}" required autocomplete="off">
|
|
<button type="submit">Renommer</button>
|
|
</div>
|
|
</form>
|
|
<form method="post" action="{{ url_for('game_delete', slug=game.slug) }}" class="actionsRow"
|
|
onsubmit="return confirm('Supprimer définitivement le jeu « {{ game.name }} », son dossier et sa base de données ? Cette action est irréversible.');">
|
|
<button type="submit" class="danger">🗑️ Supprimer ce jeu</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- ---------- Panneau déplaçable : nouvel objet ---------- -->
|
|
<!-- Réutilise .floatPanel/.floatPanelHeader/.floatPanelBody (éditeur
|
|
d'écran, voir style.css) pour la même sensation de fenêtre flottante
|
|
qu'ailleurs dans l'outil, avec sa propre variante centrée
|
|
(.floatPanel--center) et son propre glisser-déposer minimal (pas de
|
|
position persistée en localStorage ici, contrairement aux panneaux de
|
|
l'éditeur d'écran — inutile pour un panneau ouvert ponctuellement).
|
|
Contenu repris de l'ancienne page "Définir un nouvel objet"
|
|
(object_form.html, retirée), mais avec un gabarit de ligne dense —
|
|
une vraie ligne de tableau (comme "Champs existants" dans
|
|
object_edit.html) plutôt qu'une grosse carte par champ — pensé pour
|
|
ajouter beaucoup de champs d'affilée sans que le panneau explose en
|
|
hauteur. -->
|
|
<div class="floatPanel floatPanel--center hidden" id="newObjectPanel">
|
|
<div class="floatPanelHeader" id="newObjectPanelHeader">
|
|
<span class="floatPanelTitle">⋮⋮ Nouvel objet</span>
|
|
<button type="button" class="button is-small" onclick="closeNewObjectPanel()">✕</button>
|
|
</div>
|
|
<div class="floatPanelBody">
|
|
<form method="post" action="{{ url_for('object_new', slug=game.slug) }}" id="objectForm" class="formPassthrough">
|
|
<div class="formField">
|
|
<label for="object_name">Nom de l'objet (ex. Personnage, Niveau, Question...)</label>
|
|
<input type="text" id="object_name" name="object_name" required autocomplete="off">
|
|
</div>
|
|
|
|
<div class="dashPanelHeader" style="margin-top:14px;">
|
|
<h2>Champs</h2>
|
|
<button type="button" class="button is-small" id="btnAddField">+ Ajouter un champ</button>
|
|
</div>
|
|
<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 id="fieldsList"></tbody>
|
|
</table>
|
|
</div>
|
|
<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>
|
|
<td>
|
|
<div class="select is-small">
|
|
<select name="field_type[]" class="fieldTypeSelect">
|
|
{% for key, val in field_types.items() %}
|
|
<option value="{{ key }}">{{ val.label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="select is-small relationTarget" style="display:none;">
|
|
<select name="field_relation[]">
|
|
{% for d in definitions %}
|
|
<option value="{{ d.id }}">{{ d.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<span class="has-text-grey relationPlaceholder">—</span>
|
|
</td>
|
|
<td>
|
|
<input class="input is-small boundsTarget" type="number" step="any" name="field_min[]" placeholder="0" style="display:none;">
|
|
<span class="has-text-grey boundsPlaceholder">—</span>
|
|
</td>
|
|
<td>
|
|
<input class="input is-small boundsTarget" type="number" step="any" name="field_max[]" placeholder="100" style="display:none;">
|
|
<span class="has-text-grey boundsPlaceholder">—</span>
|
|
</td>
|
|
<td>
|
|
<div class="select is-small">
|
|
<select name="field_required[]">
|
|
<option value="0">Non</option>
|
|
<option value="1">Oui</option>
|
|
</select>
|
|
</div>
|
|
</td>
|
|
<td class="fieldsTableActions"><button type="button" class="button is-small is-danger btnRemoveField">🗑️</button></td>
|
|
</tr>
|
|
</template>
|
|
|
|
<script src="{{ url_for('static', filename='object_form.js') }}"></script>
|
|
<script>
|
|
function switchDashTab(tab) {
|
|
document.querySelectorAll('#dashTabs .builderTabBtn').forEach(function (btn) {
|
|
btn.classList.toggle('active', btn.dataset.tab === tab);
|
|
});
|
|
document.querySelectorAll('.builderTabPanel').forEach(function (panel) {
|
|
panel.classList.toggle('active', panel.dataset.tabPanel === tab);
|
|
});
|
|
}
|
|
function toggleDashCreate(tab) {
|
|
var bar = document.getElementById('dashCreate-' + tab);
|
|
bar.style.display = bar.style.display === 'none' ? 'flex' : 'none';
|
|
if (bar.style.display === 'flex') {
|
|
var firstInput = bar.querySelector('input, select');
|
|
if (firstInput) firstInput.focus();
|
|
}
|
|
}
|
|
|
|
// Onglet initial : ?tab=... (utilisé par les liens "crée-en un" ailleurs
|
|
// dans l'outil, ex. l'éditeur d'écran, pour arriver directement sur le bon
|
|
// onglet plutôt que de laisser deviner où cliquer).
|
|
(function () {
|
|
var requestedTab = {{ (request.args.get('tab') or '')|tojson }};
|
|
if (requestedTab) switchDashTab(requestedTab);
|
|
})();
|
|
|
|
// ---------- 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;
|
|
header.addEventListener('mousedown', function (e) {
|
|
if (e.target.closest('button')) return;
|
|
dragging = true;
|
|
var rect = panel.getBoundingClientRect();
|
|
panel.style.left = rect.left + 'px';
|
|
panel.style.top = rect.top + 'px';
|
|
panel.style.right = 'auto';
|
|
panel.style.transform = 'none';
|
|
offsetX = e.clientX - rect.left;
|
|
offsetY = e.clientY - rect.top;
|
|
document.body.style.userSelect = 'none';
|
|
});
|
|
document.addEventListener('mousemove', function (e) {
|
|
if (!dragging) return;
|
|
panel.style.left = (e.clientX - offsetX) + 'px';
|
|
panel.style.top = (e.clientY - offsetY) + 'px';
|
|
});
|
|
document.addEventListener('mouseup', function () {
|
|
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 %}
|