Move element tree above the add-element menus, make containers collapsible

The "Éléments de cet écran" tree now sits above "Ajouter un élément sur
l'écran" / "Mes éléments de jeu" so it's the first thing visible in the
left panel.

Each container row gets a disclosure triangle that collapses/expands its
children, independent from the row's link (which still selects the
element and shows its properties whether collapsed or expanded). The
collapsed state is remembered per element in localStorage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-23 20:17:21 +02:00
co-authored by Claude Sonnet 5
parent bb79f2f93d
commit 386e93d9c6
2 changed files with 78 additions and 23 deletions
+66 -21
View File
@@ -30,15 +30,22 @@
{% endif %}
{% macro render_tree_node(el) %}
<div class="treeNode">
<a class="treeRowLink {{ 'selected' if el.id == selected_id else '' }}"
href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=el.id) }}"
data-element-id="{{ el.id }}"
oncontextmenu="return showElementContextMenu(event, {{ el.id }});">
<span class="treeIcon">{{ widgets.get(el.widget, {}).get('icon', '❔') }}</span>
<span class="treeLabel">{{ el|elabel }}</span>
</a>
{% set kids = children_of.get(el.id, []) %}
{% set kids = children_of.get(el.id, []) %}
<div class="treeNode" data-element-id="{{ el.id }}">
<div class="treeRow">
{% if kids %}
<button type="button" class="treeToggle" data-node-id="{{ el.id }}" aria-label="Réduire / déplier">▾</button>
{% else %}
<span class="treeToggleSpacer"></span>
{% endif %}
<a class="treeRowLink {{ 'selected' if el.id == selected_id else '' }}"
href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=el.id) }}"
data-element-id="{{ el.id }}"
oncontextmenu="return showElementContextMenu(event, {{ el.id }});">
<span class="treeIcon">{{ widgets.get(el.widget, {}).get('icon', '❔') }}</span>
<span class="treeLabel">{{ el|elabel }}</span>
</a>
</div>
{% if kids %}
<div class="treeChildren">
{% for child in kids %}
@@ -53,6 +60,17 @@
{% set el = elements|selectattr("id", "equalto", selected_id)|first if selected_id else none %}
<!-- Panneau gauche : ajouter un élément (icônes, sans jargon technique) -->
<div class="builderPanel">
<h2>Éléments de cet écran</h2>
{% if top_level_elements %}
<div class="elementTree">
{% for el in top_level_elements %}
{{ render_tree_node(el) }}
{% endfor %}
</div>
{% else %}
<p class="hint">Aucun élément pour l'instant.</p>
{% endif %}
<details class="propGroup">
<summary>Ajouter un élément sur l'écran</summary>
<div class="propGroupBody">
@@ -95,17 +113,6 @@
{% endif %}
</div>
</details>
<h2>Éléments de cet écran</h2>
{% if top_level_elements %}
<div class="elementTree">
{% for el in top_level_elements %}
{{ render_tree_node(el) }}
{% endfor %}
</div>
{% else %}
<p class="hint">Aucun élément pour l'instant.</p>
{% endif %}
</div>
<!-- Menu contextuel (clic droit sur un élément de l'arborescence) -->
@@ -579,6 +586,7 @@ function saveGeometry(id, x, y, width, height) {
var SCREEN_EDIT_URL = "{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id) }}";
var CURRENT_SELECTED_ID = {{ selected_id or 'null' }};
var GAME_SLUG = "{{ game.slug }}";
var SCREEN_ID = {{ screen.id }};
// ---------- Menu contextuel (clic droit) de l'arborescence des éléments ----------
//
@@ -626,6 +634,42 @@ if (!window.__forgeContextMenuBound) {
document.addEventListener('scroll', hideElementContextMenu, true);
}
// ---------- Arborescence : replier/déplier un conteneur ----------
//
// Le triangle (.treeToggle) replie/déplie SANS toucher à la sélection : le
// nom de l'élément reste un lien à part (.treeRowLink) qui, lui, sélectionne
// toujours l'élément et affiche ses réglages — cliquer sur un conteneur
// replié donne donc bien accès à ses propriétés, comme demandé. L'état
// replié/déplié est mémorisé par élément dans localStorage pour survivre à
// un changement de sélection ou un rechargement de l'éditeur.
function treeCollapseKey(nodeId) {
return 'forgeTreeCollapsed:' + GAME_SLUG + ':' + SCREEN_ID + ':' + nodeId;
}
function restoreTreeCollapsedState() {
document.querySelectorAll('.elementTree .treeNode').forEach(function (node) {
var id = node.dataset.elementId;
var collapsed = false;
try { collapsed = localStorage.getItem(treeCollapseKey(id)) === '1'; } catch (e) {}
node.classList.toggle('collapsed', collapsed);
});
}
if (!window.__forgeTreeToggleBound) {
window.__forgeTreeToggleBound = true;
document.addEventListener('click', function (e) {
var btn = e.target.closest('.treeToggle');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
var node = btn.closest('.treeNode');
var collapsed = node.classList.toggle('collapsed');
try { localStorage.setItem(treeCollapseKey(node.dataset.elementId), collapsed ? '1' : '0'); } catch (err) {}
});
}
restoreTreeCollapsedState();
// ---------- Navigation dans l'éditeur sans AUCUN rechargement de page ----------
//
// Se déplacer dans le canevas (glisser, redimensionner, cliquer un autre
@@ -676,7 +720,7 @@ function refreshCanvasOnly() {
// focus/curseur pendant qu'on modifie un réglage.
var newTree = doc.querySelector('.builderPanel .elementTree');
var curTree = document.querySelector('.builderPanel .elementTree');
if (newTree && curTree) curTree.innerHTML = newTree.innerHTML;
if (newTree && curTree) { curTree.innerHTML = newTree.innerHTML; restoreTreeCollapsedState(); }
});
}
@@ -880,6 +924,7 @@ function initBuilderPanel() {
bindPropsAutosave();
bindJaugeDefinitionSelect();
restoreTreeCollapsedState();
}
// Jauge : quand on change l'objet de données à suivre SANS quitter le