Files
Forge-Engine/templates/game_dashboard.html
T
williamandClaude Sonnet 5 1b24df9546 Éditeur à champs pour les variables Objet/Tableau (plus de JSON à taper)
L'édition en JSON brut (textarea) n'était pas adaptée à un public non
développeur. Remplacée par un panneau "Configurer la valeur" (déplaçable/
redimensionnable/plein écran, comme les autres) avec une ligne par clé
(Objet) ou par élément (Tableau) : un champ Clé (Objet seulement), un
type (Texte/Nombre/Oui-Non) et une valeur en texte libre — le JSON est
reconstruit automatiquement au clic sur "OK", jamais tapé à la main.

Un seul panneau partagé (#varValuePanel) pour toutes les variables : à
l'ouverture (openVarValuePanel(targetInputId, mode)), reconstruit une
ligne par entrée déjà présente dans le JSON actuel de l'<input> ciblé —
un objet/tableau imbriqué en valeur est préservé tel quel (JSON.stringify
en texte opaque) plutôt que perdu, même si cette valeur précise n'est pas
éditable via une ligne (portée volontairement limitée aux valeurs
simples : suffisant pour une configuration ou une liste, sans la
complexité d'un éditeur JSON récursif).

- Barre "+ Créer une variable" : le champ de valeur devient en lecture
  seule pour Objet/Tableau, avec un bouton "🔧 Configurer" à côté.
- Ligne d'une variable existante : la cellule Valeur montre un aperçu en
  lecture seule + le même bouton "🔧 Configurer" (au lieu du textarea
  JSON du commit précédent).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 14:41:37 +02:00

1110 lines
56 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><button type="button" class="linkLikeButton" onclick="openObjectEditPanel({{ d.id }})">{{ d.name }}</button></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 id="newVarType" name="type" onchange="onVarTypeSelectChange(this)">
{% for key, val in variable_types.items() %}
<option value="{{ key }}">{{ val.label }}</option>
{% endfor %}
</select>
<input type="text" id="newVarValue" name="default_value" placeholder="Valeur de départ">
<button type="button" id="newVarConfigureBtn" class="button is-small" style="display:none;" onclick="openVarValuePanel('newVarValue', document.getElementById('newVarType').value)">🔧 Configurer la valeur</button>
<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>
{% if v.type in ('objet', 'tableau') %}
<div class="fieldRow" style="align-items:center;">
<input type="text" form="varEditForm{{ v.id }}" name="value" id="varValue-{{ v.id }}" value="{{ v.value or '' }}" readonly aria-label="Valeur de « {{ v.name }} »">
<button type="button" class="button is-small" onclick="openVarValuePanel('varValue-{{ v.id }}', '{{ v.type }}')">🔧 Configurer</button>
</div>
{% else %}
<input type="text" form="varEditForm{{ v.id }}" name="value" value="{{ v.value or '' }}" aria-label="Valeur de « {{ v.name }} »">
{% endif %}
</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>
<!-- ---------- Panneau "Configurer la valeur" (variable Objet/Tableau) ---------- -->
<!-- Un seul panneau partagé (pas un par variable) : une seule valeur à la
fois peut être en cours d'édition. openVarValuePanel(targetInputId,
mode) mémorise vers quel <input> écrire le JSON reconstruit (celui de
la barre de création, ou celui, en lecture seule, de la ligne d'une
variable existante) — pas de JSON à taper à la main : une ligne par
clé (Objet) ou par élément (Tableau), avec un type (Texte/Nombre/
Oui-Non) qui pilote comment la valeur est encodée en JSON. Ne gère
que des valeurs SIMPLES (pas d'objet/tableau imbriqué) — largement
suffisant pour une configuration ou une liste, et ça reste lisible
pour qui ne connaît pas JSON. -->
<div class="floatPanel floatPanel--center hidden" id="varValuePanel">
<div class="floatPanelHeader" id="varValuePanelHeader">
<span class="floatPanelTitle" id="varValuePanelTitle">⋮⋮ Configurer la valeur</span>
<button type="button" class="button is-small floatPanelFullscreenBtn" title="Plein écran">⛶</button>
<button type="button" class="button is-small" onclick="closeVarValuePanel()">✕</button>
</div>
<div class="floatPanelBody">
<p class="hint mb-2" id="varValuePanelHint"></p>
<button type="button" class="button is-outlined is-fullwidth mb-2" onclick="addVarValueRow()">+ Ajouter</button>
<div id="varValueRows"></div>
<div class="actionsRow"><button type="button" class="primary" onclick="applyVarValuePanel()">OK — utiliser cette valeur</button></div>
</div>
<div class="floatPanelResizeHandle" id="varValuePanelResizeHandle"></div>
</div>
<template id="varValueRowTemplate">
<div class="fieldRowCard box varValueRow">
<div class="fieldRow">
<div class="formField varValueKeyField">
<label>Clé</label>
<input type="text" class="varValueKey" placeholder="ex. nom, degats...">
</div>
<div class="formField">
<label>Type</label>
<select class="varValueType">
<option value="texte">Texte</option>
<option value="nombre">Nombre</option>
<option value="booleen">Oui / Non</option>
</select>
</div>
<div class="formField" style="flex:2;">
<label>Valeur</label>
<input type="text" class="varValueValue" placeholder="ex. Alice, 12, Oui...">
</div>
</div>
<button type="button" class="button is-small is-danger varValueRemoveBtn">🗑️ Retirer</button>
</div>
</template>
<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 floatPanelFullscreenBtn" title="Plein écran">⛶</button>
<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 floatPanelFullscreenBtn" title="Plein écran">⛶</button>
<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>
{% if referenced_by_definition.get(d.id) %}
<div class="warningBanner">
⚠️ D'autres objets ont une relation vers « {{ definition.name }} » :
{{ referenced_by_definition[d.id]|map(attribute='name')|join(', ') }}.
La suppression de cet objet est bloquée tant que ces relations existent.
</div>
{% endif %}
<!-- Sous-onglets Champs/Données de CE panneau — classes distinctes de
.builderTabs/.builderTabBtn/.builderTabPanel (onglets du tableau de
bord lui-même) pour ne jamais interférer avec eux : switchDashTab()
cible ces classes globalement, alors que switchObjectPanelTab()
reste scopé au panneau de CET objet (plusieurs objets = plusieurs
jeux de sous-onglets indépendants sur la même page). -->
<div class="builderTabs objectPanelTabs">
<button type="button" class="builderTabBtn active" data-optab="fields" onclick="switchObjectPanelTab(this, 'fields')">Champs <span class="dashTabCount">{{ definition.fields|length }}</span></button>
<button type="button" class="builderTabBtn" data-optab="data" onclick="switchObjectPanelTab(this, 'data')">Données <span class="dashTabCount">{{ rows_by_definition[d.id]|length }}</span></button>
</div>
<div class="objectPanelTabPanel active" data-optab-panel="fields">
<div class="dashPanelHeader">
<h2>Champs existants</h2>
<button type="button" class="button is-small primary" onclick="openAddFieldPanel({{ d.id }})">+ Ajouter</button>
</div>
{% 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>
<div class="objectPanelTabPanel" data-optab-panel="data">
{% set blocked_row_id = request.args.get('blocked_row') if request.args.get('edit') == d.id|string else none %}
{% if blocked_row_id %}
<div class="warningBanner">
⚠️ Impossible de supprimer l'entrée #{{ blocked_row_id }} : elle est référencée ailleurs. Modifie ou supprime d'abord ces entrées.
</div>
{% endif %}
<div class="dashPanelHeader">
<h2>Données enregistrées</h2>
<div style="display:flex; gap:6px;">
{% if definition.fields %}
<div class="columnFilterWrap">
<button type="button" class="button is-small" onclick="toggleColumnFilterMenu({{ d.id }})">🔧 Colonnes</button>
<div class="columnFilterMenu hidden" id="columnFilterMenu-{{ d.id }}">
{% for f in definition.fields %}
<label class="checkbox columnFilterOption"><input type="checkbox" checked data-col="field-{{ f.id }}" onchange="toggleDataColumn({{ d.id }}, 'field-{{ f.id }}', this.checked)"> {{ f.name }}</label>
{% endfor %}
</div>
</div>
<button type="button" class="button is-small primary" onclick="openAddEntryPanel({{ d.id }})">+ Ajouter</button>
{% endif %}
</div>
</div>
{% if definition.fields %}
{% set rows = rows_by_definition[d.id] %}
{% if rows %}
{% for r in rows %}
<form method="post" action="{{ url_for('data_edit', slug=game.slug, definition_id=d.id, row_id=r.id) }}" id="dataEditForm{{ r.id }}"></form>
<form method="post" action="{{ url_for('data_delete', slug=game.slug, definition_id=d.id, row_id=r.id) }}" id="dataDeleteForm{{ r.id }}"
onsubmit="return confirm('Supprimer définitivement l\'entrée #{{ r.id }} ?');"></form>
{% endfor %}
<div class="fieldsTableWrap">
<table class="table is-fullwidth is-hoverable dataTable fieldsTable" id="dataTable-{{ d.id }}">
<thead>
<tr><th>#</th>{% for f in definition.fields %}<th data-col="field-{{ f.id }}">{{ f.name }}</th>{% endfor %}<th></th></tr>
</thead>
<tbody>
{% for r in rows %}
<tr data-row-id="{{ r.id }}">
<td>{{ r.id }}</td>
{% for f in definition.fields %}
{% set col = f.name|colname %}
<td data-col="field-{{ f.id }}" data-field-label="{{ f.name }}">
{% if f.type == 'texte' %}
<input class="input is-small" type="text" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
{% elif f.type == 'texte_long' %}
<textarea class="textarea is-small" form="dataEditForm{{ r.id }}" name="{{ col }}" rows="1">{{ r[col] or '' }}</textarea>
{% elif f.type in ('nombre_entier', 'nombre_decimal') %}
<input class="input is-small" type="number" step="{{ '1' if f.type == 'nombre_entier' else 'any' }}" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] if r[col] is not none else '' }}">
{% elif f.type == 'booleen' %}
<label class="checkbox"><input type="checkbox" form="dataEditForm{{ r.id }}" name="{{ col }}" {{ 'checked' if r[col] else '' }}></label>
{% elif f.type == 'date' %}
<input class="input is-small" type="date" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
{% elif f.type == 'relation' %}
<div class="select is-small">
<select form="dataEditForm{{ r.id }}" name="{{ col }}_id">
<option value="">—</option>
{% for opt in relation_options_by_definition[d.id].get(f.id, []) %}
<option value="{{ opt.id }}" {{ 'selected' if r[col ~ '_id'] == opt.id else '' }}>{{ opt.label }}</option>
{% endfor %}
</select>
</div>
{% endif %}
</td>
{% endfor %}
<td class="fieldsTableActions">
<button type="button" class="button is-small" title="Voir cette entrée en détail" onclick="openRowDetailPanel({{ d.id }}, {{ r.id }})">👁️</button>
<button type="submit" form="dataEditForm{{ r.id }}" class="button is-small">Enregistrer</button>
<button type="submit" form="dataDeleteForm{{ r.id }}" class="button is-small is-danger">🗑️</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="hint">Aucune donnée enregistrée pour l'instant.</p>
{% endif %}
{% else %}
<p class="hint">Ajoute d'abord au moins un champ pour pouvoir enregistrer des données.</p>
{% endif %}
</div>
<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>
<!-- Panneaux "Ajouter" (champ / entrée) de CET objet — déplaçables et
redimensionnables comme les autres, ouverts par le bouton "+ Ajouter"
du panneau ci-dessus. Formulaire vertical (un champ par ligne,
étiquette au-dessus) plutôt que la barre en ligne d'origine : au-delà
d'une poignée de champs, une seule ligne devenait illisible (voir
capture d'écran fournie par l'utilisateur — 10 champs alignés sur une
largeur d'écran normale). -->
<div class="floatPanel floatPanel--center hidden addFieldPanel" data-definition-id="{{ d.id }}" id="addFieldPanel-{{ d.id }}">
<div class="floatPanelHeader">
<span class="floatPanelTitle">⋮⋮ Ajouter un champ — {{ definition.name }}</span>
<button type="button" class="button is-small floatPanelFullscreenBtn" title="Plein écran">⛶</button>
<button type="button" class="button is-small" onclick="closeAddFieldPanel({{ d.id }})">✕</button>
</div>
<div class="floatPanelBody">
<form method="post" action="{{ url_for('object_field_add', slug=game.slug, definition_id=d.id) }}">
<div class="formField">
<label for="new_field_name-{{ d.id }}">Nom du champ</label>
<input type="text" id="new_field_name-{{ d.id }}" name="field_name" placeholder="ex. nom, pv, description..." required autocomplete="off">
</div>
<div class="formField">
<label for="new_field_type-{{ d.id }}">Type</label>
<select id="new_field_type-{{ d.id }}" name="field_type" class="fieldTypeSelect">
{% for key, val in field_types.items() %}
<option value="{{ key }}">{{ val.label }}</option>
{% endfor %}
</select>
</div>
<div class="formField relationTarget" style="display:none;">
<label for="new_field_relation-{{ d.id }}">Objet lié</label>
<select id="new_field_relation-{{ d.id }}" name="field_relation">
{% for od in existing_definitions %}
<option value="{{ od.id }}">{{ od.name }}</option>
{% endfor %}
</select>
</div>
<div class="fieldRow boundsTarget" style="display:none;">
<div class="formField">
<label for="new_field_min-{{ d.id }}">Valeur mini</label>
<input type="number" step="any" id="new_field_min-{{ d.id }}" name="field_min" placeholder="ex. 0">
</div>
<div class="formField">
<label for="new_field_max-{{ d.id }}">Valeur maxi</label>
<input type="number" step="any" id="new_field_max-{{ d.id }}" name="field_max" placeholder="ex. 100">
</div>
</div>
<div class="formField">
<label for="new_field_required-{{ d.id }}">Obligatoire ?</label>
<select id="new_field_required-{{ d.id }}" name="field_required">
<option value="0">Non</option>
<option value="1">Oui</option>
</select>
</div>
<div class="actionsRow"><button type="submit" class="primary">+ Ajouter le champ</button></div>
</form>
</div>
<div class="floatPanelResizeHandle"></div>
</div>
{% if definition.fields %}
<div class="floatPanel floatPanel--center hidden addEntryPanel" data-definition-id="{{ d.id }}" id="addEntryPanel-{{ d.id }}">
<div class="floatPanelHeader">
<span class="floatPanelTitle">⋮⋮ Ajouter une entrée — {{ definition.name }}</span>
<button type="button" class="button is-small floatPanelFullscreenBtn" title="Plein écran">⛶</button>
<button type="button" class="button is-small" onclick="closeAddEntryPanel({{ d.id }})">✕</button>
</div>
<div class="floatPanelBody">
<form method="post" action="{{ url_for('data_new', slug=game.slug, definition_id=d.id) }}">
{% for f in definition.fields %}
{% set col = f.name|colname %}
<div class="formField">
<label for="new_entry_{{ col }}-{{ d.id }}">{{ f.name }}{% if f.required %} *{% endif %}</label>
{% if f.type == 'texte' %}
<input type="text" id="new_entry_{{ col }}-{{ d.id }}" name="{{ col }}" {{ 'required' if f.required else '' }}>
{% elif f.type == 'texte_long' %}
<textarea id="new_entry_{{ col }}-{{ d.id }}" name="{{ col }}" rows="4" {{ 'required' if f.required else '' }}></textarea>
{% elif f.type == 'nombre_entier' %}
<input type="number" step="1" id="new_entry_{{ col }}-{{ d.id }}" name="{{ col }}" {{ 'required' if f.required else '' }}>
{% elif f.type == 'nombre_decimal' %}
<input type="number" step="any" id="new_entry_{{ col }}-{{ d.id }}" name="{{ col }}" {{ 'required' if f.required else '' }}>
{% elif f.type == 'booleen' %}
<label class="checkbox"><input type="checkbox" id="new_entry_{{ col }}-{{ d.id }}" name="{{ col }}"> Oui</label>
{% elif f.type == 'date' %}
<input type="date" id="new_entry_{{ col }}-{{ d.id }}" name="{{ col }}" {{ 'required' if f.required else '' }}>
{% elif f.type == 'relation' %}
<select id="new_entry_{{ col }}-{{ d.id }}" name="{{ col }}_id" {{ 'required' if f.required else '' }}>
<option value="">—</option>
{% for opt in relation_options_by_definition[d.id].get(f.id, []) %}
<option value="{{ opt.id }}">{{ opt.label }}</option>
{% endfor %}
</select>
{% if not relation_options_by_definition[d.id].get(f.id) %}
<p class="help">Aucune entrée n'existe encore pour l'objet lié.</p>
{% endif %}
{% endif %}
</div>
{% endfor %}
<div class="actionsRow"><button type="submit" class="primary">+ Ajouter l'entrée</button></div>
</form>
</div>
<div class="floatPanelResizeHandle"></div>
</div>
<!-- Panneau de détail d'UNE entrée (voir openRowDetailPanel()) — un seul
panneau PAR OBJET (pas un par ligne, pour ne jamais alourdir la page
même avec beaucoup d'entrées) : son contenu est reconstruit à
l'ouverture en lisant directement la ligne correspondante du tableau
"Données enregistrées" (valeurs à jour, y compris une modification
pas encore enregistrée). Champ par ligne, étiquette bien visible —
pensé pour un objet avec beaucoup de colonnes, illisible en tableau
serré (voir la barre "🔧 Colonnes" pour l'inverse : cacher des
colonnes plutôt que tout regarder en détail). -->
<div class="floatPanel floatPanel--center hidden rowDetailPanel" data-definition-id="{{ d.id }}" id="rowDetailPanel-{{ d.id }}">
<div class="floatPanelHeader">
<span class="floatPanelTitle rowDetailPanelTitle">⋮⋮ Entrée — {{ definition.name }}</span>
<button type="button" class="button is-small floatPanelFullscreenBtn" title="Plein écran">⛶</button>
<button type="button" class="button is-small" onclick="closeRowDetailPanel({{ d.id }})">✕</button>
</div>
<div class="floatPanelBody rowDetailPanelBody"></div>
<div class="floatPanelResizeHandle"></div>
</div>
{% endif %}
{% 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();
}
}
// Barre "+ Créer une variable" : pour un type Objet/Tableau, la valeur de
// départ se configure via le panneau à champs (voir openVarValuePanel) —
// la saisie libre n'a alors plus de sens, remplacée par un aperçu en
// lecture seule + le bouton "🔧 Configurer".
function onVarTypeSelectChange(sel) {
var valueInput = document.getElementById('newVarValue');
var configureBtn = document.getElementById('newVarConfigureBtn');
var structured = sel.value === 'objet' || sel.value === 'tableau';
valueInput.readOnly = structured;
configureBtn.style.display = structured ? '' : 'none';
if (structured && !valueInput.value) valueInput.value = sel.value === 'objet' ? '{}' : '[]';
}
// 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;
// Glisser un panneau en plein écran doit d'abord l'en faire sortir —
// sinon left/top posés juste en dessous seraient aussitôt repris en
// main par bottom/right encore actifs (voir makeFloatPanelFullscreenable).
if (panel.classList.contains('floatPanel--fullscreen')) panel._fsToggle.exit();
var rect = panel.getBoundingClientRect();
panel.style.left = rect.left + 'px';
panel.style.top = rect.top + 'px';
panel.style.right = 'auto';
panel.style.bottom = '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;
// Même remarque que pour le glisser-déposer : sortir du plein écran
// d'abord, sinon bottom/right encore actifs reprendraient la main sur
// la largeur/hauteur posées ci-dessous.
if (panel.classList.contains('floatPanel--fullscreen')) panel._fsToggle.exit();
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 = '';
});
}
// Plein écran (voir switchDashTab pour l'esprit général : chaque panneau
// s'ouvre en plein écran par défaut — plus pratique dès qu'il y a plus de
// quelques champs — avec un bouton pour revenir à sa taille/position
// précédente, mémorisée le temps de la session (pas persistée). Renvoie un
// objet {enter, exit} et pose panel._fsToggle dessus, pour que le code qui
// OUVRE le panneau (openXxxPanel ci-dessous) puisse appeler enter() sans
// avoir à re-chercher le bouton.
function makeFloatPanelFullscreenable(panel, toggleBtn) {
var saved = null;
function enter() {
saved = {
left: panel.style.left, top: panel.style.top, right: panel.style.right,
width: panel.style.width, height: panel.style.height,
maxWidth: panel.style.maxWidth, transform: panel.style.transform,
};
panel.classList.add('floatPanel--fullscreen');
panel.style.left = '12px'; panel.style.top = '12px'; panel.style.right = '12px'; panel.style.bottom = '12px';
panel.style.width = 'auto'; panel.style.height = 'auto'; panel.style.maxWidth = 'none'; panel.style.transform = 'none';
toggleBtn.title = 'Quitter le plein écran';
}
function exit() {
panel.classList.remove('floatPanel--fullscreen');
panel.style.bottom = '';
if (saved) {
panel.style.left = saved.left; panel.style.top = saved.top; panel.style.right = saved.right;
panel.style.width = saved.width; panel.style.height = saved.height;
panel.style.maxWidth = saved.maxWidth; panel.style.transform = saved.transform;
}
toggleBtn.title = 'Plein écran';
}
toggleBtn.addEventListener('click', function () {
if (panel.classList.contains('floatPanel--fullscreen')) exit(); else enter();
});
panel._fsToggle = { enter: enter, exit: exit };
return panel._fsToggle;
}
// ---------- Panneau déplaçable "Nouvel objet" ----------
(function () {
var panel = document.getElementById('newObjectPanel');
makeFloatPanelDraggable(panel, document.getElementById('newObjectPanelHeader'));
makeFloatPanelResizable(panel, document.getElementById('newObjectPanelResizeHandle'));
makeFloatPanelFullscreenable(panel, panel.querySelector('.floatPanelFullscreenBtn'));
window.openNewObjectPanel = function () { panel.classList.remove('hidden'); panel._fsToggle.enter(); };
window.closeNewObjectPanel = function () { panel.classList.add('hidden'); };
})();
// ---------- Panneaux déplaçables "Modifier un objet" (un par objet) ----------
(function () {
document.querySelectorAll('.objectEditPanel').forEach(function (panel) {
makeFloatPanelDraggable(panel, panel.querySelector('.floatPanelHeader'));
makeFloatPanelResizable(panel, panel.querySelector('.floatPanelResizeHandle'));
makeFloatPanelFullscreenable(panel, panel.querySelector('.floatPanelFullscreenBtn'));
});
window.openObjectEditPanel = function (definitionId, subtab) {
document.querySelectorAll('.objectEditPanel').forEach(function (panel) {
var isTarget = panel.dataset.definitionId === String(definitionId);
panel.classList.toggle('hidden', !isTarget);
if (isTarget) panel._fsToggle.enter();
});
if (subtab) {
var panel = document.querySelector('.objectEditPanel[data-definition-id="' + definitionId + '"]');
var btn = panel && panel.querySelector('.objectPanelTabs [data-optab="' + subtab + '"]');
if (btn) switchObjectPanelTab(btn, subtab);
}
};
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 ou d'une entrée, renommage...) : ?edit=<id>
// (et ?subtab=data pour rouvrir sur le bon sous-onglet), posés 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 pour voir le résultat.
var requestedEdit = {{ (request.args.get('edit') or '')|tojson }};
var requestedSubtab = {{ (request.args.get('subtab') or '')|tojson }};
if (requestedEdit) openObjectEditPanel(requestedEdit, requestedSubtab);
})();
// ---------- Panneaux déplaçables "Ajouter un champ"/"Ajouter une entrée" ----------
// Un panneau par objet et par action (voir game_dashboard.html, ouverts
// par le bouton "+ Ajouter" du panneau "Modifier un objet") — formulaire
// vertical, un champ par ligne : au-delà d'une poignée de champs, une
// seule ligne (l'ancienne barre en ligne) devenait illisible.
(function () {
var NUMERIC_FIELD_TYPES = ['nombre_entier', 'nombre_decimal'];
document.querySelectorAll('.addFieldPanel').forEach(function (panel) {
makeFloatPanelDraggable(panel, panel.querySelector('.floatPanelHeader'));
makeFloatPanelResizable(panel, panel.querySelector('.floatPanelResizeHandle'));
makeFloatPanelFullscreenable(panel, panel.querySelector('.floatPanelFullscreenBtn'));
var typeSelect = panel.querySelector('.fieldTypeSelect');
var relationTarget = panel.querySelector('.relationTarget');
var boundsTarget = panel.querySelector('.boundsTarget');
function syncAddFieldVisibility() {
relationTarget.style.display = typeSelect.value === 'relation' ? '' : 'none';
boundsTarget.style.display = NUMERIC_FIELD_TYPES.indexOf(typeSelect.value) !== -1 ? '' : 'none';
}
typeSelect.addEventListener('change', syncAddFieldVisibility);
syncAddFieldVisibility();
});
document.querySelectorAll('.addEntryPanel').forEach(function (panel) {
makeFloatPanelDraggable(panel, panel.querySelector('.floatPanelHeader'));
makeFloatPanelResizable(panel, panel.querySelector('.floatPanelResizeHandle'));
makeFloatPanelFullscreenable(panel, panel.querySelector('.floatPanelFullscreenBtn'));
});
window.openAddFieldPanel = function (definitionId) {
var panel = document.querySelector('.addFieldPanel[data-definition-id="' + definitionId + '"]');
if (panel) { panel.classList.remove('hidden'); panel._fsToggle.enter(); }
};
window.closeAddFieldPanel = function (definitionId) {
var panel = document.querySelector('.addFieldPanel[data-definition-id="' + definitionId + '"]');
if (panel) panel.classList.add('hidden');
};
window.openAddEntryPanel = function (definitionId) {
var panel = document.querySelector('.addEntryPanel[data-definition-id="' + definitionId + '"]');
if (panel) { panel.classList.remove('hidden'); panel._fsToggle.enter(); }
};
window.closeAddEntryPanel = function (definitionId) {
var panel = document.querySelector('.addEntryPanel[data-definition-id="' + definitionId + '"]');
if (panel) panel.classList.add('hidden');
};
})();
// ---------- Filtre de colonnes (onglet Données) ----------
function toggleColumnFilterMenu(definitionId) {
var menu = document.getElementById('columnFilterMenu-' + definitionId);
if (!menu) return;
var opening = menu.classList.contains('hidden');
// Un seul menu de filtre ouvert à la fois, tous objets confondus — évite
// d'en laisser traîner un ouvert par-dessus le panneau d'un autre objet.
document.querySelectorAll('.columnFilterMenu').forEach(function (m) { m.classList.add('hidden'); });
if (opening) menu.classList.remove('hidden');
}
document.addEventListener('click', function (e) {
if (e.target.closest('.columnFilterWrap')) return;
document.querySelectorAll('.columnFilterMenu').forEach(function (m) { m.classList.add('hidden'); });
});
function toggleDataColumn(definitionId, colKey, visible) {
var table = document.getElementById('dataTable-' + definitionId);
if (!table) return;
table.querySelectorAll('[data-col="' + colKey + '"]').forEach(function (cell) {
cell.style.display = visible ? '' : 'none';
});
}
// ---------- Panneau de détail d'une entrée (un par objet) ----------
(function () {
document.querySelectorAll('.rowDetailPanel').forEach(function (panel) {
makeFloatPanelDraggable(panel, panel.querySelector('.floatPanelHeader'));
makeFloatPanelResizable(panel, panel.querySelector('.floatPanelResizeHandle'));
makeFloatPanelFullscreenable(panel, panel.querySelector('.floatPanelFullscreenBtn'));
});
// Reconstruit le contenu du panneau à chaque ouverture, en lisant
// directement la ligne correspondante du tableau "Données enregistrées"
// déjà affiché — pas d'aller-retour serveur, et ça reflète une
// modification tout juste saisie mais pas encore "Enregistrer"ée.
window.openRowDetailPanel = function (definitionId, rowId) {
var panel = document.querySelector('.rowDetailPanel[data-definition-id="' + definitionId + '"]');
var table = document.getElementById('dataTable-' + definitionId);
if (!panel || !table) return;
var row = table.querySelector('tr[data-row-id="' + rowId + '"]');
if (!row) return;
var body = panel.querySelector('.rowDetailPanelBody');
body.innerHTML = '';
var idField = document.createElement('div');
idField.className = 'rowDetailField';
idField.innerHTML = '<div class="rowDetailLabel">#</div><div class="rowDetailValue">' + rowId + '</div>';
body.appendChild(idField);
row.querySelectorAll('td[data-field-label]').forEach(function (cell) {
var label = cell.dataset.fieldLabel;
var control = cell.querySelector('input, textarea, select');
var value = '—';
if (control) {
if (control.type === 'checkbox') value = control.checked ? 'Oui' : 'Non';
else if (control.tagName === 'SELECT') value = control.options[control.selectedIndex] ? control.options[control.selectedIndex].text : '—';
else value = control.value || '—';
}
var field = document.createElement('div');
field.className = 'rowDetailField';
var labelEl = document.createElement('div');
labelEl.className = 'rowDetailLabel';
labelEl.textContent = label;
var valueEl = document.createElement('div');
valueEl.className = 'rowDetailValue';
valueEl.textContent = value;
field.appendChild(labelEl);
field.appendChild(valueEl);
body.appendChild(field);
});
panel.classList.remove('hidden');
panel._fsToggle.enter();
};
window.closeRowDetailPanel = function (definitionId) {
var panel = document.querySelector('.rowDetailPanel[data-definition-id="' + definitionId + '"]');
if (panel) panel.classList.add('hidden');
};
})();
// Sous-onglets Champs/Données d'UN panneau précis (voir plus haut) — scopé
// au panneau ancêtre du bouton cliqué, jamais global (plusieurs objets
// peuvent chacun avoir leurs sous-onglets sur leur propre panneau, en même
// temps, sur la même page).
function switchObjectPanelTab(btn, tab) {
var panel = btn.closest('.objectEditPanel');
panel.querySelectorAll('.objectPanelTabs .builderTabBtn').forEach(function (b) {
b.classList.toggle('active', b.dataset.optab === tab);
});
panel.querySelectorAll('.objectPanelTabPanel').forEach(function (p) {
p.classList.toggle('active', p.dataset.optabPanel === tab);
});
}
// ---------- Panneau "Configurer la valeur" (variable Objet/Tableau) ----------
// Un seul panneau partagé (voir le commentaire sur son HTML) : à
// l'ouverture, mémorise vers quel <input> (targetId) le JSON reconstruit
// devra être réécrit, construit une ligne par clé/élément déjà présent
// dans le JSON actuel de cet <input>, puis "OK" sérialise les lignes et
// referme.
(function () {
var panel = document.getElementById('varValuePanel');
var rowsWrap = document.getElementById('varValueRows');
var rowTemplate = document.getElementById('varValueRowTemplate');
var title = document.getElementById('varValuePanelTitle');
var hint = document.getElementById('varValuePanelHint');
makeFloatPanelDraggable(panel, document.getElementById('varValuePanelHeader'));
makeFloatPanelResizable(panel, document.getElementById('varValuePanelResizeHandle'));
makeFloatPanelFullscreenable(panel, panel.querySelector('.floatPanelFullscreenBtn'));
function valueTypeOf(v) {
if (typeof v === 'boolean') return 'booleen';
if (typeof v === 'number') return 'nombre';
return 'texte'; // texte, et repli pour un objet/tableau imbriqué (non éditable ici, voir addRowFrom)
}
function valueTextOf(v, type) {
if (type === 'booleen') return v ? 'Oui' : 'Non';
if (typeof v === 'object' && v !== null) return JSON.stringify(v); // imbriqué : préservé tel quel, en texte opaque
return v == null ? '' : String(v);
}
function coerceRowValue(type, text) {
if (type === 'nombre') { var n = parseFloat(text); return isNaN(n) ? 0 : n; }
if (type === 'booleen') return ['oui', 'vrai', '1', 'true'].indexOf(text.trim().toLowerCase()) !== -1;
return text;
}
function addRowFrom(key, value) {
var clone = rowTemplate.content.cloneNode(true);
var row = clone.querySelector('.varValueRow');
var type = valueTypeOf(value);
if (key !== null) row.querySelector('.varValueKey').value = key;
row.querySelector('.varValueType').value = type;
row.querySelector('.varValueValue').value = valueTextOf(value, type);
row.querySelector('.varValueRemoveBtn').addEventListener('click', function () { row.remove(); });
rowsWrap.appendChild(clone);
}
window.addVarValueRow = function () {
addRowFrom(panel.dataset.mode === 'tableau' ? null : '', '');
};
window.openVarValuePanel = function (targetInputId, mode) {
var input = document.getElementById(targetInputId);
if (!input) return;
panel.dataset.target = targetInputId;
panel.dataset.mode = mode;
rowsWrap.innerHTML = '';
var parsed;
try { parsed = JSON.parse(input.value || (mode === 'objet' ? '{}' : '[]')); } catch (e) { parsed = mode === 'objet' ? {} : []; }
if (mode === 'tableau') {
title.textContent = '⋮⋮ Configurer le tableau';
hint.textContent = 'Une ligne par élément — valeurs simples uniquement (pas de tableau/objet imbriqué).';
(Array.isArray(parsed) ? parsed : []).forEach(function (v) { addRowFrom(null, v); });
} else {
title.textContent = '⋮⋮ Configurer l\'objet';
hint.textContent = 'Une ligne par clé — valeurs simples uniquement (pas de tableau/objet imbriqué).';
Object.keys(parsed || {}).forEach(function (k) { addRowFrom(k, parsed[k]); });
}
if (!rowsWrap.children.length) window.addVarValueRow();
panel.classList.remove('hidden');
panel._fsToggle.enter();
};
window.closeVarValuePanel = function () {
panel.classList.add('hidden');
};
window.applyVarValuePanel = function () {
var mode = panel.dataset.mode;
var result = mode === 'tableau' ? [] : {};
rowsWrap.querySelectorAll('.varValueRow').forEach(function (row) {
var type = row.querySelector('.varValueType').value;
var value = coerceRowValue(type, row.querySelector('.varValueValue').value);
if (mode === 'tableau') {
result.push(value);
} else {
var key = row.querySelector('.varValueKey').value.trim();
if (key) result[key] = value;
}
});
var input = document.getElementById(panel.dataset.target);
if (input) {
input.value = JSON.stringify(result);
input.dispatchEvent(new Event('input', { bubbles: true }));
}
closeVarValuePanel();
};
})();
</script>
{% endblock %}