|
|
|
@@ -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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|