Add drag-and-drop reordering of sibling elements in the tree panel
Elements can now be reordered within the same container by dragging a row above or below another in the left-hand element tree.
This commit is contained in:
@@ -2,6 +2,7 @@ from . import (
|
|||||||
element_add,
|
element_add,
|
||||||
element_add_child,
|
element_add_child,
|
||||||
element_geometry,
|
element_geometry,
|
||||||
|
element_reorder,
|
||||||
element_save,
|
element_save,
|
||||||
element_delete,
|
element_delete,
|
||||||
element_duplicate,
|
element_duplicate,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from flask import request, jsonify, abort
|
||||||
|
|
||||||
|
import screens
|
||||||
|
|
||||||
|
from core.flask_app import app
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/game/<slug>/elements/<int:element_id>/reorder", methods=["POST"])
|
||||||
|
def element_reorder(slug, element_id):
|
||||||
|
"""Appelé en AJAX quand on glisse un élément au-dessus/en dessous d'un
|
||||||
|
autre DANS LA MÊME ARBORESCENCE (panneau de gauche) pour changer l'ordre
|
||||||
|
des éléments d'un même conteneur — voir reorder_element."""
|
||||||
|
element = screens.get_element(slug, element_id)
|
||||||
|
if not element:
|
||||||
|
abort(404)
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
target_id = int(data["target_id"])
|
||||||
|
position = data.get("position")
|
||||||
|
if position not in ("before", "after"):
|
||||||
|
abort(400)
|
||||||
|
screens.reorder_element(slug, element["screen_id"], element_id, target_id, position)
|
||||||
|
return jsonify({"ok": True})
|
||||||
@@ -75,6 +75,7 @@ from .elements.get_element import get_element
|
|||||||
from .elements.add_element import add_element
|
from .elements.add_element import add_element
|
||||||
from .elements.add_tab import add_tab
|
from .elements.add_tab import add_tab
|
||||||
from .elements.update_element_geometry import update_element_geometry
|
from .elements.update_element_geometry import update_element_geometry
|
||||||
|
from .elements.reorder_element import reorder_element
|
||||||
from .elements.save_element_controls import save_element_controls
|
from .elements.save_element_controls import save_element_controls
|
||||||
from .elements.delete_element import delete_element
|
from .elements.delete_element import delete_element
|
||||||
from .elements.duplicate_element import duplicate_element
|
from .elements.duplicate_element import duplicate_element
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import db
|
||||||
|
|
||||||
|
|
||||||
|
def reorder_element(slug, screen_id, element_id, target_id, position):
|
||||||
|
"""Déplace element_id juste avant ("before") ou juste après ("after")
|
||||||
|
target_id, DANS LE MÊME CONTENEUR (les deux doivent avoir le même
|
||||||
|
parent_id — sinon on ne touche à rien, pour ne jamais reparenter par
|
||||||
|
erreur via le glisser-déposer de l'arborescence). L'ordre est porté par
|
||||||
|
z_index, partagé par tous les éléments de l'écran (pas juste les frères
|
||||||
|
et sœurs) : on renumérote donc l'écran entier à partir de l'ordre actuel
|
||||||
|
(z_index, id) pour rester cohérent partout, pas seulement dans ce
|
||||||
|
conteneur."""
|
||||||
|
conn = db.connect(slug)
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT id, parent_id FROM _screen_elements WHERE screen_id = ? ORDER BY z_index, id",
|
||||||
|
(screen_id,),
|
||||||
|
).fetchall()
|
||||||
|
by_id = {r["id"]: r["parent_id"] for r in rows}
|
||||||
|
if element_id not in by_id or target_id not in by_id:
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
if by_id[element_id] != by_id[target_id]:
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
order = [r["id"] for r in rows]
|
||||||
|
order.remove(element_id)
|
||||||
|
target_index = order.index(target_id)
|
||||||
|
insert_at = target_index + 1 if position == "after" else target_index
|
||||||
|
order.insert(insert_at, element_id)
|
||||||
|
|
||||||
|
conn.executemany(
|
||||||
|
"UPDATE _screen_elements SET z_index = ? WHERE id = ?",
|
||||||
|
[(i + 1, eid) for i, eid in enumerate(order)],
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
@@ -195,6 +195,9 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
|||||||
.treeIcon{ flex:0 0 auto; font-size:12px; }
|
.treeIcon{ flex:0 0 auto; font-size:12px; }
|
||||||
.treeLabel{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
.treeLabel{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
.treeChildren{ display:flex; flex-direction:column; margin-left:10px; padding-left:8px; border-left:1px dashed var(--border); }
|
.treeChildren{ display:flex; flex-direction:column; margin-left:10px; padding-left:8px; border-left:1px dashed var(--border); }
|
||||||
|
.treeRow.dragging{ opacity:0.4; }
|
||||||
|
.treeRow.dropBefore{ box-shadow: inset 0 2px 0 0 var(--accent); }
|
||||||
|
.treeRow.dropAfter{ box-shadow: inset 0 -2px 0 0 var(--accent); }
|
||||||
|
|
||||||
.contextMenu{
|
.contextMenu{
|
||||||
position:fixed; z-index:500; background:var(--panel); border:1px solid var(--border);
|
position:fixed; z-index:500; background:var(--panel); border:1px solid var(--border);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
{% macro render_tree_node(el) %}
|
{% macro render_tree_node(el) %}
|
||||||
{% set kids = children_of.get(el.id, []) %}
|
{% set kids = children_of.get(el.id, []) %}
|
||||||
<div class="treeNode" data-element-id="{{ el.id }}">
|
<div class="treeNode" data-element-id="{{ el.id }}">
|
||||||
<div class="treeRow">
|
<div class="treeRow" draggable="true" data-parent-id="{{ el.parent_id or '' }}">
|
||||||
{% if kids %}
|
{% if kids %}
|
||||||
<button type="button" class="treeToggle" data-node-id="{{ el.id }}" aria-label="Réduire / déplier">▾</button>
|
<button type="button" class="treeToggle" data-node-id="{{ el.id }}" aria-label="Réduire / déplier">▾</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -738,6 +738,101 @@ if (!window.__forgeTreeToggleBound) {
|
|||||||
|
|
||||||
restoreTreeCollapsedState();
|
restoreTreeCollapsedState();
|
||||||
|
|
||||||
|
// ---------- Arborescence : réordonner par glisser-déposer ----------
|
||||||
|
//
|
||||||
|
// Glisser une ligne au-dessus/en dessous d'une autre ligne DU MÊME
|
||||||
|
// CONTENEUR (même parent, ou toutes deux à la racine) change leur ordre —
|
||||||
|
// voir screens/elements/reorder_element.py. Écouteurs posés une seule fois
|
||||||
|
// sur document (délégation) : ils survivent au remplacement de #builder3
|
||||||
|
// par refreshBuilderPanel/refreshCanvasOnly, pas besoin de les rebrancher.
|
||||||
|
var treeDragElementId = null;
|
||||||
|
var treeDragParentId = null;
|
||||||
|
|
||||||
|
function treeRowDropInfo(row) {
|
||||||
|
var node = row.closest('.treeNode');
|
||||||
|
return { elementId: node ? node.dataset.elementId : null, parentId: row.dataset.parentId || '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTreeDropIndicators() {
|
||||||
|
document.querySelectorAll('.treeRow.dropBefore, .treeRow.dropAfter').forEach(function (r) {
|
||||||
|
r.classList.remove('dropBefore', 'dropAfter');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.__forgeTreeDragBound) {
|
||||||
|
window.__forgeTreeDragBound = true;
|
||||||
|
|
||||||
|
document.addEventListener('dragstart', function (e) {
|
||||||
|
var row = e.target.closest('.elementTree .treeRow');
|
||||||
|
if (!row) return;
|
||||||
|
var info = treeRowDropInfo(row);
|
||||||
|
treeDragElementId = info.elementId;
|
||||||
|
treeDragParentId = info.parentId;
|
||||||
|
row.classList.add('dragging');
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
try { e.dataTransfer.setData('text/plain', String(info.elementId)); } catch (err) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragend', function (e) {
|
||||||
|
var row = e.target.closest('.elementTree .treeRow');
|
||||||
|
if (row) row.classList.remove('dragging');
|
||||||
|
clearTreeDropIndicators();
|
||||||
|
treeDragElementId = null;
|
||||||
|
treeDragParentId = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragover', function (e) {
|
||||||
|
var row = e.target.closest('.elementTree .treeRow');
|
||||||
|
if (!row || treeDragElementId == null) return;
|
||||||
|
var info = treeRowDropInfo(row);
|
||||||
|
if (info.elementId == null || String(info.elementId) === String(treeDragElementId)) return;
|
||||||
|
if (info.parentId !== treeDragParentId) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
clearTreeDropIndicators();
|
||||||
|
var rect = row.getBoundingClientRect();
|
||||||
|
var before = (e.clientY - rect.top) < rect.height / 2;
|
||||||
|
row.classList.add(before ? 'dropBefore' : 'dropAfter');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('drop', function (e) {
|
||||||
|
var row = e.target.closest('.elementTree .treeRow');
|
||||||
|
clearTreeDropIndicators();
|
||||||
|
if (!row || treeDragElementId == null) return;
|
||||||
|
e.preventDefault();
|
||||||
|
var info = treeRowDropInfo(row);
|
||||||
|
if (info.elementId == null || String(info.elementId) === String(treeDragElementId)) return;
|
||||||
|
if (info.parentId !== treeDragParentId) return;
|
||||||
|
var rect = row.getBoundingClientRect();
|
||||||
|
var position = (e.clientY - rect.top) < rect.height / 2 ? 'before' : 'after';
|
||||||
|
var draggedId = treeDragElementId;
|
||||||
|
fetch('/game/{{ game.slug }}/elements/' + draggedId + '/reorder', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ target_id: info.elementId, position: position })
|
||||||
|
}).then(refreshAfterReorder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAfterReorder() {
|
||||||
|
var url = SCREEN_EDIT_URL + (CURRENT_SELECTED_ID ? ('?selected=' + CURRENT_SELECTED_ID) : '');
|
||||||
|
return fetch(url, { headers: { 'X-Requested-With': 'pjax' } })
|
||||||
|
.then(function (r) { return r.text(); })
|
||||||
|
.then(function (html) {
|
||||||
|
var doc = new DOMParser().parseFromString(html, 'text/html');
|
||||||
|
var newCanvas = doc.getElementById('canvas');
|
||||||
|
var curCanvas = document.getElementById('canvas');
|
||||||
|
if (newCanvas && curCanvas) {
|
||||||
|
curCanvas.innerHTML = newCanvas.innerHTML;
|
||||||
|
applySelectionHighlight(CURRENT_SELECTED_ID);
|
||||||
|
}
|
||||||
|
var newTree = doc.querySelector('.builderPanel .elementTree');
|
||||||
|
var curTree = document.querySelector('.builderPanel .elementTree');
|
||||||
|
if (newTree && curTree) { curTree.innerHTML = newTree.innerHTML; }
|
||||||
|
restoreTreeCollapsedState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Navigation dans l'éditeur sans AUCUN rechargement de page ----------
|
// ---------- Navigation dans l'éditeur sans AUCUN rechargement de page ----------
|
||||||
//
|
//
|
||||||
// Se déplacer dans le canevas (glisser, redimensionner, cliquer un autre
|
// Se déplacer dans le canevas (glisser, redimensionner, cliquer un autre
|
||||||
|
|||||||
Reference in New Issue
Block a user