Make the left element-tree panel floating/draggable/closable too, same as the right one

Generalized the floating-panel mechanism from the properties panel (props/propsPanelFloat/propsPanelReopenBtn) into reusable functions keyed by a short id, and applied it to the left "Éléments de cet écran" panel (key "left", panelLeftFloat, panelLeftReopenBtn). Both panels can now be closed independently, letting the canvas take the screen's full width for the most faithful possible preview against the actual /play rendering. Shared CSS (.floatPanel/.floatPanelHeader/.floatPanelBody/.floatPanelReopenBtn with --left/--right position variants) replaces the props-panel-only rules from the previous commit.

Fixed the two querySelector('.builderPanel .elementTree') call sites (used to patch the tree preview into the DOM after an unrelated canvas-only refresh) that would have silently stopped finding the tree once its wrapper's class changed.
This commit is contained in:
william
2026-08-24 15:33:47 +02:00
parent 03ca2723c5
commit 850242d1d4
2 changed files with 93 additions and 68 deletions
+28 -24
View File
@@ -150,33 +150,37 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
toutes les trois occupent exactement la même hauteur (celle qui reste toutes les trois occupent exactement la même hauteur (celle qui reste
disponible), avec leur propre défilement interne si leur contenu dépasse. */ disponible), avec leur propre défilement interne si leur contenu dépasse. */
.builder3{ display:flex; gap:20px; align-items:stretch; margin-top:14px; flex:1 1 auto; min-height:0; } .builder3{ display:flex; gap:20px; align-items:stretch; margin-top:14px; flex:1 1 auto; min-height:0; }
.builderPanel{ flex:0 0 260px; min-width:0; height:100%; overflow-y:auto; }
.builderCanvasArea{ flex:1 1 auto; min-width:0; height:100%; overflow-y:auto; display:flex; flex-direction:column; } .builderCanvasArea{ flex:1 1 auto; min-width:0; height:100%; overflow-y:auto; display:flex; flex-direction:column; }
/* ---------- Panneau de propriétés flottant ---------- */ /* ---------- Panneaux gauche/droite flottants ---------- */
/* Sorti du flux flex de .builder3 (position:fixed) : .builderCanvasArea /* Les deux panneaux (éléments de l'écran / propriétés) sont sortis du flux
(flex:1 1 auto) récupère automatiquement l'espace libéré, sans réglage flex de .builder3 (position:fixed) : .builderCanvasArea (flex:1 1 auto)
supplémentaire — un panneau flottant repositionné par l'utilisateur récupère automatiquement tout l'espace libéré, sans réglage
n'agrandit/ne rétrécit donc jamais le canevas de travers. Position et supplémentaire — les fermer donne donc une prévisualisation aussi large
état ouvert/fermé mémorisés par écran (voir restorePropsPanelState dans que possible, aussi fidèle que possible à ce que "Jouer" affiche
screen_edit.html) pour survivre à un changement de sélection ou un réellement. Position et état ouvert/fermé mémorisés par écran (voir
rechargement. */ floatPanel*() dans screen_edit.html) pour survivre à un changement de
.builderPanelRight{ sélection (qui remplace tout #builder3) ou un rechargement. */
position:fixed; top:90px; right:24px; width:340px; max-height:calc(100vh - 120px); .floatPanel{
position:fixed; top:90px; max-height:calc(100vh - 120px);
min-width:0; display:flex; flex-direction:column; z-index:60; min-width:0; display:flex; flex-direction:column; z-index:60;
background:var(--panel); border:1px solid var(--border); border-radius:10px; background:var(--panel); border:1px solid var(--border); border-radius:10px;
box-shadow:0 12px 40px rgba(0,0,0,.55); box-shadow:0 12px 40px rgba(0,0,0,.55);
} }
.builderPanelRight.hidden{ display:none; } .floatPanel.hidden{ display:none; }
.propsPanelHeader{ .floatPanel--left{ left:24px; width:280px; }
.floatPanel--right{ right:24px; width:340px; }
.floatPanelHeader{
display:flex; align-items:center; justify-content:space-between; gap:8px; display:flex; align-items:center; justify-content:space-between; gap:8px;
padding:8px 10px; border-bottom:1px solid var(--border); background:var(--panel2); padding:8px 10px; border-bottom:1px solid var(--border); background:var(--panel2);
border-radius:10px 10px 0 0; cursor:move; flex:0 0 auto; user-select:none; border-radius:10px 10px 0 0; cursor:move; flex:0 0 auto; user-select:none;
} }
.propsPanelHeader .propsPanelTitle{ font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.05em; } .floatPanelHeader .floatPanelTitle{ font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.05em; }
.propsPanelBody{ padding:12px; overflow-y:auto; min-height:0; flex:1 1 auto; } .floatPanelBody{ padding:12px; overflow-y:auto; min-height:0; flex:1 1 auto; }
#propsPanelReopenBtn{ position:fixed; top:90px; right:24px; z-index:60; display:none; } .floatPanelReopenBtn{ position:fixed; top:90px; z-index:60; display:none; }
#propsPanelReopenBtn.visible{ display:inline-flex; } .floatPanelReopenBtn.visible{ display:inline-flex; }
#panelLeftReopenBtn{ left:24px; }
#propsPanelReopenBtn{ right:24px; }
.widgetGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; } .widgetGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.widgetBtnForm{ margin:0; } .widgetBtnForm{ margin:0; }
@@ -314,7 +318,7 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
.sliderValue{ min-width:44px; text-align:right; } .sliderValue{ min-width:44px; text-align:right; }
input[type=color]{ width:52px; height:34px; padding:2px; } input[type=color]{ width:52px; height:34px; padding:2px; }
.propsPanel .sectionDivider, .builderPanelRight .sectionDivider{ border:none; border-top:1px solid var(--border); margin:14px 0; } .propsPanel .sectionDivider, .floatPanel .sectionDivider{ border:none; border-top:1px solid var(--border); margin:14px 0; }
.actionAddForm{ margin-top:10px; } .actionAddForm{ margin-top:10px; }
.uploadRow{ margin-top:6px; } .uploadRow{ margin-top:6px; }
@@ -330,13 +334,13 @@ code{ background:var(--panel2); border:1px solid var(--border); border-radius:4p
body.builderBody{ height:auto; overflow:visible; } body.builderBody{ height:auto; overflow:visible; }
body.builderBody > main.content{ overflow:visible; } body.builderBody > main.content{ overflow:visible; }
.builder3{ flex-direction:column; flex:none; } .builder3{ flex-direction:column; flex:none; }
.builderPanel, .builderCanvasArea{ flex:none; width:100%; height:auto; max-height:none; overflow:visible; } .builderCanvasArea{ flex:none; width:100%; height:auto; max-height:none; overflow:visible; }
/* Trop peu de largeur pour qu'un panneau flottant ait un sens (il /* Trop peu de largeur pour qu'un panneau flottant ait un sens (il
couvrirait tout l'écran) : il rejoint la pile normale comme les deux couvrirait tout l'écran) : les deux rejoignent la pile normale et
autres colonnes, redimensionnement/déplacement désactivés (voir le restent toujours ouverts/visibles, quel que soit l'état mémorisé —
garde useFloatingPanel() dans screen_edit.html). */ .hidden est ignoré grâce au "!important" sur "display" ci-dessous. */
.builderPanelRight{ position:static; flex:none; width:100%; height:auto; max-height:none; box-shadow:none; top:auto; right:auto; } .floatPanel{ position:static; flex:none; width:100%; height:auto; max-height:none; box-shadow:none; top:auto; left:auto; right:auto; display:flex !important; }
#propsPanelReopenBtn{ display:none !important; } .floatPanelReopenBtn{ display:none !important; }
} }
/* ---------- Logique de la scène : éditeur de flow à nœuds ---------- */ /* ---------- Logique de la scène : éditeur de flow à nœuds ---------- */
+65 -44
View File
@@ -57,8 +57,16 @@
<div class="builder3" id="builder3"> <div class="builder3" id="builder3">
{% set el = elements|selectattr("id", "equalto", selected_id)|first if selected_id else none %} {% 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) --> <!-- Panneau gauche : ajouter un élément (icônes, sans jargon technique) —
<div class="builderPanel"> flottant, déplaçable, fermable, même mécanisme que le panneau droit
(voir floatPanel*() plus bas). -->
<button type="button" id="panelLeftReopenBtn" class="button is-small floatPanelReopenBtn" onclick="openLeftPanel()" title="Rouvrir le panneau des éléments">🧩 Éléments</button>
<div class="floatPanel floatPanel--left" id="panelLeftFloat">
<div class="floatPanelHeader" data-drag-panel="panelLeftFloat" data-drag-key="left">
<span class="floatPanelTitle">⋮⋮ Éléments de cet écran</span>
<button type="button" class="button is-small" onclick="closeLeftPanel()" title="Fermer">✕</button>
</div>
<div class="floatPanelBody">
<h2 class="subtitle is-6">Éléments de cet écran</h2> <h2 class="subtitle is-6">Éléments de cet écran</h2>
{% if top_level_elements %} {% if top_level_elements %}
<div class="elementTree"> <div class="elementTree">
@@ -113,6 +121,7 @@
</div> </div>
</details> </details>
</div> </div>
</div>
<!-- Menu contextuel (clic droit sur un élément de l'arborescence) --> <!-- Menu contextuel (clic droit sur un élément de l'arborescence) -->
<div id="elementContextMenu" class="contextMenu" style="display:none;"> <div id="elementContextMenu" class="contextMenu" style="display:none;">
@@ -144,14 +153,14 @@
<!-- Panneau droit : propriétés de l'élément sélectionné — flottant, <!-- Panneau droit : propriétés de l'élément sélectionné — flottant,
déplaçable à la souris par son en-tête, fermable pour agrandir le déplaçable à la souris par son en-tête, fermable pour agrandir le
canevas (voir bindPropsPanelFloating() plus bas). --> canevas (voir floatPanel*() plus bas). -->
<button type="button" id="propsPanelReopenBtn" class="button is-small" onclick="openPropsPanel()" title="Rouvrir le panneau de propriétés">⚙️ Propriétés</button> <button type="button" id="propsPanelReopenBtn" class="button is-small floatPanelReopenBtn" onclick="openPropsPanel()" title="Rouvrir le panneau de propriétés">⚙️ Propriétés</button>
<div class="builderPanel builderPanelRight" id="propsPanelFloat"> <div class="floatPanel floatPanel--right" id="propsPanelFloat">
<div class="propsPanelHeader" id="propsPanelDragHandle"> <div class="floatPanelHeader" data-drag-panel="propsPanelFloat" data-drag-key="props">
<span class="propsPanelTitle">⋮⋮ Propriétés</span> <span class="floatPanelTitle">⋮⋮ Propriétés</span>
<button type="button" class="button is-small" onclick="closePropsPanel()" title="Fermer">✕</button> <button type="button" class="button is-small" onclick="closePropsPanel()" title="Fermer">✕</button>
</div> </div>
<div class="propsPanelBody"> <div class="floatPanelBody">
{% if not selected_id %} {% if not selected_id %}
<div class="hint" style="padding:20px 0;">Sélectionne un élément (sur l'écran ou dans la liste) pour voir et modifier ses propriétés ici.</div> <div class="hint" style="padding:20px 0;">Sélectionne un élément (sur l'écran ou dans la liste) pour voir et modifier ses propriétés ici.</div>
{% else %} {% else %}
@@ -738,35 +747,38 @@ function restoreTreeCollapsedState() {
}); });
} }
// ---------- Panneau de propriétés : flottant, déplaçable, fermable ---------- // ---------- Panneaux gauche/droite : flottants, déplaçables, fermables ----------
// //
// Sorti du flux (position:fixed en CSS) pour ne jamais rétrécir le canevas // Sortis du flux (position:fixed en CSS) pour ne jamais rétrécir le canevas
// selon où on le pose — le fermer libère toute cette largeur pour une // selon où on les pose — les fermer libère cette largeur pour une
// prévisualisation plus fidèle à ce que "Jouer" affiche réellement (voir // prévisualisation plus fidèle à ce que "Jouer" affiche réellement (voir
// bug remonté : un panneau trop large réduisait le canevas au point de // bug remonté : des panneaux trop larges réduisaient le canevas au point de
// casser la mise en page des onglets). Position et état ouvert/fermé // casser la mise en page des onglets). Un seul jeu de fonctions génériques
// mémorisés par écran pour survivre à un changement de sélection (qui // (key = identifiant court mémorisé dans localStorage, distinct par
// remplace tout #builder3, voir refreshBuilderPanel) ou un rechargement. // panneau) plutôt qu'une copie par panneau. Position et état ouvert/fermé
function propsPanelStateKey() { // survivent à un changement de sélection (qui remplace tout #builder3, voir
return 'forgePropsPanel:' + GAME_SLUG + ':' + SCREEN_ID; // refreshBuilderPanel) ou un rechargement — voir les appels à
// restoreFloatPanelState() dans initBuilderPanel().
function floatPanelStateKey(key) {
return 'forgeFloatPanel:' + GAME_SLUG + ':' + SCREEN_ID + ':' + key;
} }
function savePropsPanelState(state) { function saveFloatPanelState(key, state) {
try { try {
var current = JSON.parse(localStorage.getItem(propsPanelStateKey()) || '{}'); var current = JSON.parse(localStorage.getItem(floatPanelStateKey(key)) || '{}');
localStorage.setItem(propsPanelStateKey(), JSON.stringify(Object.assign(current, state))); localStorage.setItem(floatPanelStateKey(key), JSON.stringify(Object.assign(current, state)));
} catch (e) {} } catch (e) {}
} }
function loadPropsPanelState() { function loadFloatPanelState(key) {
try { return JSON.parse(localStorage.getItem(propsPanelStateKey()) || '{}'); } catch (e) { return {}; } try { return JSON.parse(localStorage.getItem(floatPanelStateKey(key)) || '{}'); } catch (e) { return {}; }
} }
function restorePropsPanelState() { function restoreFloatPanelState(key, panelId, reopenBtnId) {
var panel = document.getElementById('propsPanelFloat'); var panel = document.getElementById(panelId);
var reopenBtn = document.getElementById('propsPanelReopenBtn'); var reopenBtn = document.getElementById(reopenBtnId);
if (!panel) return; if (!panel) return;
var state = loadPropsPanelState(); var state = loadFloatPanelState(key);
if (state.left != null && state.top != null) { if (state.left != null && state.top != null) {
panel.style.left = state.left + 'px'; panel.style.left = state.left + 'px';
panel.style.top = state.top + 'px'; panel.style.top = state.top + 'px';
@@ -777,27 +789,35 @@ function restorePropsPanelState() {
if (reopenBtn) reopenBtn.classList.toggle('visible', !open); if (reopenBtn) reopenBtn.classList.toggle('visible', !open);
} }
function closePropsPanel() { function closeFloatPanel(key, panelId, reopenBtnId) {
var panel = document.getElementById('propsPanelFloat'); var panel = document.getElementById(panelId);
var reopenBtn = document.getElementById('propsPanelReopenBtn'); var reopenBtn = document.getElementById(reopenBtnId);
if (panel) panel.classList.add('hidden'); if (panel) panel.classList.add('hidden');
if (reopenBtn) reopenBtn.classList.add('visible'); if (reopenBtn) reopenBtn.classList.add('visible');
savePropsPanelState({ open: false }); saveFloatPanelState(key, { open: false });
} }
function openPropsPanel() { function openFloatPanel(key, panelId, reopenBtnId) {
var panel = document.getElementById('propsPanelFloat'); var panel = document.getElementById(panelId);
var reopenBtn = document.getElementById('propsPanelReopenBtn'); var reopenBtn = document.getElementById(reopenBtnId);
if (panel) panel.classList.remove('hidden'); if (panel) panel.classList.remove('hidden');
if (reopenBtn) reopenBtn.classList.remove('visible'); if (reopenBtn) reopenBtn.classList.remove('visible');
savePropsPanelState({ open: true }); saveFloatPanelState(key, { open: true });
} }
if (!window.__forgePropsPanelDragBound) { // Raccourcis utilisés directement par les attributs onclick du gabarit.
window.__forgePropsPanelDragBound = true; function closePropsPanel() { closeFloatPanel('props', 'propsPanelFloat', 'propsPanelReopenBtn'); }
function openPropsPanel() { openFloatPanel('props', 'propsPanelFloat', 'propsPanelReopenBtn'); }
function closeLeftPanel() { closeFloatPanel('left', 'panelLeftFloat', 'panelLeftReopenBtn'); }
function openLeftPanel() { openFloatPanel('left', 'panelLeftFloat', 'panelLeftReopenBtn'); }
if (!window.__forgeFloatPanelDragBound) {
window.__forgeFloatPanelDragBound = true;
document.addEventListener('mousedown', function (e) { document.addEventListener('mousedown', function (e) {
if (!e.target.closest('#propsPanelDragHandle') || e.target.closest('button')) return; var handle = e.target.closest('.floatPanelHeader');
var panel = document.getElementById('propsPanelFloat'); if (!handle || e.target.closest('button')) return;
var panel = document.getElementById(handle.dataset.dragPanel);
var key = handle.dataset.dragKey;
if (!panel) return; if (!panel) return;
var rect = panel.getBoundingClientRect(); var rect = panel.getBoundingClientRect();
var startX = e.clientX, startY = e.clientY; var startX = e.clientX, startY = e.clientY;
@@ -814,7 +834,7 @@ if (!window.__forgePropsPanelDragBound) {
function onUp() { function onUp() {
document.removeEventListener('mousemove', onMove); document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp); document.removeEventListener('mouseup', onUp);
savePropsPanelState({ left: parseFloat(panel.style.left), top: parseFloat(panel.style.top) }); saveFloatPanelState(key, { left: parseFloat(panel.style.left), top: parseFloat(panel.style.top) });
} }
document.addEventListener('mousemove', onMove); document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp); document.addEventListener('mouseup', onUp);
@@ -960,8 +980,8 @@ function refreshAfterReorder() {
curCanvas.innerHTML = newCanvas.innerHTML; curCanvas.innerHTML = newCanvas.innerHTML;
applySelectionHighlight(CURRENT_SELECTED_ID); applySelectionHighlight(CURRENT_SELECTED_ID);
} }
var newTree = doc.querySelector('.builderPanel .elementTree'); var newTree = doc.querySelector('#panelLeftFloat .elementTree');
var curTree = document.querySelector('.builderPanel .elementTree'); var curTree = document.querySelector('#panelLeftFloat .elementTree');
if (newTree && curTree) { curTree.innerHTML = newTree.innerHTML; } if (newTree && curTree) { curTree.innerHTML = newTree.innerHTML; }
restoreTreeCollapsedState(); restoreTreeCollapsedState();
}); });
@@ -1016,8 +1036,8 @@ function refreshCanvasOnly() {
// Aperçu du contenu dans l'arborescence de gauche (ex: texte tronqué) — // Aperçu du contenu dans l'arborescence de gauche (ex: texte tronqué) —
// sans toucher au panneau de droite, pour ne jamais faire perdre le // sans toucher au panneau de droite, pour ne jamais faire perdre le
// focus/curseur pendant qu'on modifie un réglage. // focus/curseur pendant qu'on modifie un réglage.
var newTree = doc.querySelector('.builderPanel .elementTree'); var newTree = doc.querySelector('#panelLeftFloat .elementTree');
var curTree = document.querySelector('.builderPanel .elementTree'); var curTree = document.querySelector('#panelLeftFloat .elementTree');
if (newTree && curTree) { curTree.innerHTML = newTree.innerHTML; restoreTreeCollapsedState(); } if (newTree && curTree) { curTree.innerHTML = newTree.innerHTML; restoreTreeCollapsedState(); }
}); });
} }
@@ -1224,7 +1244,8 @@ function initBuilderPanel() {
bindJaugeDefinitionSelect(); bindJaugeDefinitionSelect();
restoreTreeCollapsedState(); restoreTreeCollapsedState();
applySelectionHighlight(CURRENT_SELECTED_ID); applySelectionHighlight(CURRENT_SELECTED_ID);
restorePropsPanelState(); restoreFloatPanelState('props', 'propsPanelFloat', 'propsPanelReopenBtn');
restoreFloatPanelState('left', 'panelLeftFloat', 'panelLeftReopenBtn');
} }
// Jauge : quand on change l'objet de données à suivre SANS quitter le // Jauge : quand on change l'objet de données à suivre SANS quitter le