diff --git a/routes/elements/__init__.py b/routes/elements/__init__.py index 673286f5..fae00da0 100644 --- a/routes/elements/__init__.py +++ b/routes/elements/__init__.py @@ -2,6 +2,7 @@ from . import ( element_add, element_add_child, element_geometry, + element_reorder, element_save, element_delete, element_duplicate, diff --git a/routes/elements/element_reorder.py b/routes/elements/element_reorder.py new file mode 100644 index 00000000..a3125505 --- /dev/null +++ b/routes/elements/element_reorder.py @@ -0,0 +1,22 @@ +from flask import request, jsonify, abort + +import screens + +from core.flask_app import app + + +@app.route("/game//elements//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}) diff --git a/screens/__init__.py b/screens/__init__.py index 75af2633..e40b97f5 100644 --- a/screens/__init__.py +++ b/screens/__init__.py @@ -75,6 +75,7 @@ from .elements.get_element import get_element from .elements.add_element import add_element from .elements.add_tab import add_tab 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.delete_element import delete_element from .elements.duplicate_element import duplicate_element diff --git a/screens/elements/reorder_element.py b/screens/elements/reorder_element.py new file mode 100644 index 00000000..5c926e56 --- /dev/null +++ b/screens/elements/reorder_element.py @@ -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() diff --git a/static/style.css b/static/style.css index f5b76177..f1cf730a 100644 --- a/static/style.css +++ b/static/style.css @@ -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; } .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); } +.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{ position:fixed; z-index:500; background:var(--panel); border:1px solid var(--border); diff --git a/templates/screen_edit.html b/templates/screen_edit.html index b834aa96..92e8a6cf 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -32,7 +32,7 @@ {% macro render_tree_node(el) %} {% set kids = children_of.get(el.id, []) %}
-
+
{% if kids %} {% else %} @@ -738,6 +738,101 @@ if (!window.__forgeTreeToggleBound) { 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 ---------- // // Se déplacer dans le canevas (glisser, redimensionner, cliquer un autre