Move icon gallery to the left panel, drop the redundant Icône tile, add drag-into-container in the tree
- The "icone" widget no longer appears as a generic tile in "Ajouter un élément sur l'écran" / "Ajouter DANS ce conteneur" — the icon gallery (now living under "Ajouter un élément sur l'écran" in the left panel instead of the right one) is the only way to add one, always pre-set to the icon you picked. - The element tree now supports dragging an element onto a container row to move it inside (last child), from anywhere in the tree — not just reordering within the same parent. Hovering a container row splits it into before/after/inside zones (thirds) when dragging a sibling, or "inside only" when dragging from elsewhere in the tree. New move_element_to_container() rejects non-container targets and cycles (dropping a container into itself or one of its own descendants) silently, mirroring reorder_element's existing safety pattern. Verified end to end: gallery renders in the left panel with no icone tile in the widget grids, and the move endpoint correctly reparents, rejects a cycle, and rejects a non-container target. Full suite green (89).
This commit is contained in:
+80
-42
@@ -31,7 +31,7 @@
|
||||
{% macro render_tree_node(el) %}
|
||||
{% set kids = children_of.get(el.id, []) %}
|
||||
<div class="treeNode" data-element-id="{{ el.id }}">
|
||||
<div class="treeRow" draggable="true" data-parent-id="{{ el.parent_id or '' }}">
|
||||
<div class="treeRow" draggable="true" data-parent-id="{{ el.parent_id or '' }}" data-is-container="{{ '1' if el.widget in container_widgets else '' }}">
|
||||
{% if kids %}
|
||||
<button type="button" class="treeToggle" data-node-id="{{ el.id }}" aria-label="Réduire / déplier">▾</button>
|
||||
{% else %}
|
||||
@@ -82,7 +82,7 @@
|
||||
<summary>Ajouter un élément sur l'écran</summary>
|
||||
<div class="propGroupBody">
|
||||
<div class="widgetGrid">
|
||||
{% for wkey, w in widgets.items() %}
|
||||
{% for wkey, w in widgets.items() if wkey != 'icone' %}
|
||||
<form method="post" action="{{ url_for('element_add', slug=game.slug, screen_id=screen.id) }}" class="widgetBtnForm">
|
||||
<input type="hidden" name="widget" value="{{ wkey }}">
|
||||
<button type="submit" class="widgetBtn" title="{{ w.label }}">
|
||||
@@ -95,6 +95,27 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Galerie d'icônes — pas un widget parmi d'autres dans la grille
|
||||
ci-dessus (voir "if wkey != 'icone'") : cliquer/glisser une icône ICI
|
||||
pose directement un élément Icône déjà réglé sur cette icône précise,
|
||||
plutôt qu'un élément générique qu'il faudrait ensuite configurer. -->
|
||||
<details class="propGroup">
|
||||
<summary>🔤 Icônes</summary>
|
||||
<div class="propGroupBody">
|
||||
<input type="text" class="input is-small" id="iconGallerySearch" placeholder="Rechercher une icône..." oninput="filterIconGallery(this.value)">
|
||||
<div class="iconGallery" id="iconGallery">
|
||||
{% for slug in fontawesome_icons %}
|
||||
<button type="button" class="iconGalleryTile" draggable="true"
|
||||
data-icon-class="{{ slug }}" data-icon-search="{{ slug }}"
|
||||
title="{{ slug }}" onclick="addIconElement('{{ slug }}')">
|
||||
<i class="icon-svg" style="--icon-url:url('/static/icons/{{ slug }}.svg');"></i>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="help">Glisse une icône sur le canevas, ou clique dessus pour l'ajouter — sa couleur et sa taille se règlent ensuite dans ses propriétés ("Icône"). Sélection non exhaustive : d'autres icônes pourront être ajoutées à cette liste plus tard si besoin.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Menu rétractable dédié aux éléments de jeu personnalisés (Personnage,
|
||||
Outil, IA...) : chacun a désormais SA PROPRE tuile, avec son nom,
|
||||
plutôt qu'une unique tuile générique "Élément de jeu" qui ajoutait
|
||||
@@ -161,23 +182,6 @@
|
||||
<button type="button" class="button is-small" onclick="closePropsPanel()" title="Fermer">✕</button>
|
||||
</div>
|
||||
<div class="floatPanelBody">
|
||||
<details class="propGroup">
|
||||
<summary>🔤 Icônes</summary>
|
||||
<div class="propGroupBody">
|
||||
<input type="text" class="input is-small" id="iconGallerySearch" placeholder="Rechercher une icône..." oninput="filterIconGallery(this.value)">
|
||||
<div class="iconGallery" id="iconGallery">
|
||||
{% for slug in fontawesome_icons %}
|
||||
<button type="button" class="iconGalleryTile" draggable="true"
|
||||
data-icon-class="{{ slug }}" data-icon-search="{{ slug }}"
|
||||
title="{{ slug }}" onclick="addIconElement('{{ slug }}')">
|
||||
<i class="icon-svg" style="--icon-url:url('/static/icons/{{ slug }}.svg');"></i>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="help">Glisse une icône sur le canevas, ou clique dessus pour l'ajouter — sa couleur et sa taille se règlent ensuite dans ses propriétés ("Icône"). Sélection non exhaustive : d'autres icônes pourront être ajoutées à cette liste plus tard si besoin.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Sélecteur visuel d'icône, ouvert depuis le réglage "Icône" d'un
|
||||
élément Icône déjà posé (voir plus bas dans la boucle des contrôles)
|
||||
— même galerie que ci-dessus, mais choisir une icône ici REMPLACE
|
||||
@@ -230,7 +234,7 @@
|
||||
<summary>📥 Ajouter DANS ce conteneur</summary>
|
||||
<div class="propGroupBody">
|
||||
<div class="widgetGrid">
|
||||
{% for wkey, w in widgets.items() %}
|
||||
{% for wkey, w in widgets.items() if wkey != 'icone' %}
|
||||
<form method="post" action="{{ url_for('element_add_child', slug=game.slug, parent_id=el.id) }}" class="widgetBtnForm">
|
||||
<input type="hidden" name="widget" value="{{ wkey }}">
|
||||
<button type="submit" class="widgetBtn" title="{{ w.label }}">
|
||||
@@ -1041,27 +1045,53 @@ if (!window.__forgeTreeToggleBound) {
|
||||
|
||||
restoreTreeCollapsedState();
|
||||
|
||||
// ---------- Arborescence : réordonner par glisser-déposer ----------
|
||||
// ---------- Arborescence : réordonner ou déplacer dans un conteneur 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.
|
||||
// Deux gestes distincts, selon où on relâche sur une ligne cible :
|
||||
// - au-dessus/en dessous d'une ligne DU MÊME CONTENEUR (même parent, ou
|
||||
// toutes deux à la racine) : change leur ordre (voir reorder_element.py).
|
||||
// - sur le MILIEU d'une ligne dont le widget est un conteneur (n'importe où
|
||||
// dans l'arborescence, même un autre parent) : pose l'élément DEDANS, en
|
||||
// dernier enfant (voir move_element_to_container.py) — c'est ce qui
|
||||
// permet de ranger un élément existant dans un conteneur sans repasser
|
||||
// par "Ajouter DANS ce conteneur".
|
||||
// É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 || '' };
|
||||
return {
|
||||
elementId: node ? node.dataset.elementId : null,
|
||||
parentId: row.dataset.parentId || '',
|
||||
isContainer: row.dataset.isContainer === '1',
|
||||
};
|
||||
}
|
||||
|
||||
function clearTreeDropIndicators() {
|
||||
document.querySelectorAll('.treeRow.dropBefore, .treeRow.dropAfter').forEach(function (r) {
|
||||
r.classList.remove('dropBefore', 'dropAfter');
|
||||
document.querySelectorAll('.treeRow.dropBefore, .treeRow.dropAfter, .treeRow.dropInside').forEach(function (r) {
|
||||
r.classList.remove('dropBefore', 'dropAfter', 'dropInside');
|
||||
});
|
||||
}
|
||||
|
||||
// Zone touchée par le curseur dans la ligne cible : avec les frères et
|
||||
// sœurs, avant/après seulement (moitié haute/basse) ; sur un conteneur,
|
||||
// une bande centrale "dedans" s'ajoute (tiers haut/milieu/bas) — sauf si ce
|
||||
// conteneur n'est PAS un frère/sœur du même parent, où seul "dedans" a un
|
||||
// sens (il n'y a pas d'ordre à changer avec un élément d'un autre parent).
|
||||
function treeDropZone(row, clientY, sameParent, isContainer) {
|
||||
var rect = row.getBoundingClientRect();
|
||||
var ratio = (clientY - rect.top) / rect.height;
|
||||
if (!sameParent) return isContainer ? 'inside' : null;
|
||||
if (!isContainer) return ratio < 0.5 ? 'before' : 'after';
|
||||
if (ratio < 1 / 3) return 'before';
|
||||
if (ratio > 2 / 3) return 'after';
|
||||
return 'inside';
|
||||
}
|
||||
|
||||
if (!window.__forgeTreeDragBound) {
|
||||
window.__forgeTreeDragBound = true;
|
||||
|
||||
@@ -1089,31 +1119,39 @@ if (!window.__forgeTreeDragBound) {
|
||||
if (!row || treeDragElementId == null) return;
|
||||
var info = treeRowDropInfo(row);
|
||||
if (info.elementId == null || String(info.elementId) === String(treeDragElementId)) return;
|
||||
if (info.parentId !== treeDragParentId) return;
|
||||
var sameParent = info.parentId === treeDragParentId;
|
||||
var zone = treeDropZone(row, e.clientY, sameParent, info.isContainer);
|
||||
if (!zone) 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');
|
||||
row.classList.add(zone === 'before' ? 'dropBefore' : zone === 'after' ? 'dropAfter' : 'dropInside');
|
||||
});
|
||||
|
||||
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 sameParent = info.parentId === treeDragParentId;
|
||||
var zone = treeDropZone(row, e.clientY, sameParent, info.isContainer);
|
||||
if (!zone) return;
|
||||
e.preventDefault();
|
||||
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);
|
||||
if (zone === 'inside') {
|
||||
fetch('/game/{{ game.slug }}/elements/' + draggedId + '/move-to-container', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ parent_id: info.elementId })
|
||||
}).then(refreshAfterReorder);
|
||||
} else {
|
||||
fetch('/game/{{ game.slug }}/elements/' + draggedId + '/reorder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ target_id: info.elementId, position: zone })
|
||||
}).then(refreshAfterReorder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user