Replace the icon widget's plain text field with a visual searchable picker

The properties panel's "Icône" setting was a raw text input expecting a Font Awesome class name typed by hand — not something a non-developer using Forge Engine should have to know. It's now a button showing the current icon that opens a modal with the same searchable icon grid as the add-gallery; picking one replaces the selected element's icon and autosaves immediately, no typing required. The class name is still stored the same way (a hidden input feeding the existing generic attr:class control), so no backend changes were needed.
This commit is contained in:
william
2026-08-24 19:26:35 +02:00
parent cc0442ef5f
commit 9cc6704e08
+73 -1
View File
@@ -178,6 +178,32 @@
<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 : n'importe quelle autre classe Font Awesome 5 reste utilisable en la tapant directement dans le réglage "Icône".</p> <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 : n'importe quelle autre classe Font Awesome 5 reste utilisable en la tapant directement dans le réglage "Icône".</p>
</div> </div>
</details> </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
celle de l'élément sélectionné au lieu d'en ajouter un nouveau. Une
seule instance partagée (pas besoin de la refaire par élément). -->
<div class="modal" id="iconPickerModal">
<div class="modal-background" onclick="closeIconPicker()"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Choisir une icône</p>
<button class="delete" aria-label="Fermer" onclick="closeIconPicker()"></button>
</header>
<section class="modal-card-body">
<input type="text" class="input is-small" placeholder="Rechercher une icône..." oninput="filterIconPickerModal(this.value)">
<div class="iconGallery" id="iconPickerGrid" style="max-height:50vh;">
{% for slug in fontawesome_icons %}
<button type="button" class="iconGalleryTile" data-icon-search="{{ slug }}"
title="fa-{{ slug }}" onclick="chooseIcon('fa-solid fa-{{ slug }}')">
<i class="fa-solid fa-{{ slug }}"></i>
</button>
{% endfor %}
</div>
</section>
</div>
</div>
{% 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 %}
@@ -343,7 +369,15 @@
</div> </div>
{% else %} {% else %}
<label class="label is-small">{{ c.label }}</label> <label class="label is-small">{{ c.label }}</label>
{% if c.type in ('text', 'url') %} {% if c.key == 'icon_class' %}
<div class="control">
<button type="button" class="button is-fullwidth" onclick="openIconPicker()">
<span class="icon"><i id="iconPickerPreview" class="{{ c.value or c.default }}"></i></span>
<span>Changer l'icône</span>
</button>
<input type="hidden" name="ctrl_{{ c.key }}" id="field-{{ c.key }}" value="{{ c.value or c.default }}">
</div>
{% elif c.type in ('text', 'url') %}
<div class="control"> <div class="control">
<input class="input {{ 'uploadTextField' if c.allow_upload else '' }}" type="text" name="ctrl_{{ c.key }}" value="{{ c.value }}" placeholder="{{ c.placeholder or '' }}" id="field-{{ c.key }}"> <input class="input {{ 'uploadTextField' if c.allow_upload else '' }}" type="text" name="ctrl_{{ c.key }}" value="{{ c.value }}" placeholder="{{ c.placeholder or '' }}" id="field-{{ c.key }}">
</div> </div>
@@ -886,6 +920,44 @@ function filterIconGallery(query) {
}); });
} }
// ---------- Sélecteur visuel d'icône (réglage "Icône" d'un élément déjà
// posé) — même liste que la galerie d'ajout ci-dessus, mais choisir une
// icône ici remplace celle de l'élément SÉLECTIONNÉ (champ caché
// "field-icon_class") au lieu d'en créer un nouveau. ----------
function openIconPicker() {
var modal = document.getElementById('iconPickerModal');
if (modal) modal.classList.add('is-active');
}
function closeIconPicker() {
var modal = document.getElementById('iconPickerModal');
if (modal) modal.classList.remove('is-active');
}
function chooseIcon(iconClass) {
var input = document.getElementById('field-icon_class');
var preview = document.getElementById('iconPickerPreview');
if (input) input.value = iconClass;
if (preview) preview.className = iconClass;
closeIconPicker();
submitPropsForm();
}
function filterIconPickerModal(query) {
var q = query.trim().toLowerCase();
document.querySelectorAll('#iconPickerGrid .iconGalleryTile').forEach(function (tile) {
var match = !q || tile.dataset.iconSearch.indexOf(q) !== -1;
tile.classList.toggle('iconHidden', !match);
});
}
if (!window.__forgeIconPickerEscBound) {
window.__forgeIconPickerEscBound = true;
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeIconPicker();
});
}
if (!window.__forgeIconGalleryDragBound) { if (!window.__forgeIconGalleryDragBound) {
window.__forgeIconGalleryDragBound = true; window.__forgeIconGalleryDragBound = true;
var draggedIconClass = null; var draggedIconClass = null;