Administration

Constructeur de menu par glisser-déposer

Le constructeur de menu de QuietCMS offre une interface visuelle complète pour composer la navigation du site : glisser-déposer, niveaux d'imbrication illimités, ajout rapide de pages et catégories existantes, saisie de liens personnalisés. L'ensemble est sérialisé en JSON à la soumission du formulaire, sans rechargement intermédiaire.

Architecture de la liste plate

Plutôt qu'un arbre DOM récursif, le builder maintient une liste plate d'éléments dont chaque nœud porte un attribut data-depth représentant son niveau d'imbrication. Cette approche simplifie le glisser-déposer (tous les éléments sont frères dans le DOM) et la sérialisation (un seul parcours linéaire suffit à reconstruire l'arbre) :

<!-- Représentation DOM interne -->
<div class="mn-item" data-depth="0">Accueil</div>
<div class="mn-item" data-depth="0">Documentation</div>
<div class="mn-item" data-depth="1">Installation</div>
<div class="mn-item" data-depth="1">Architecture</div>
<div class="mn-item" data-depth="0">Blog</div>

Glisser-déposer vertical

Le drag & drop utilise l'API native HTML5 (draggable, dragstart, dragover, drop) sans bibliothèque externe. La poignée (⠿) active l'attribut draggable uniquement pendant le mousedown sur la poignée, ce qui évite les conflits avec les champs de saisie présents dans le même élément.

Un indicateur visuel (ligne bleue de 2,5 px) signale la position de dépôt cible au-dessus ou en dessous d'un élément selon la position verticale du curseur. Après chaque drop, une passe de correction (fixDepthsAfterDrop) vérifie la cohérence des profondeurs : un élément ne peut pas avoir une profondeur supérieure à celle de son prédécesseur + 1, évitant les hiérarchies orphelines.

Contrôles d'indentation

Chaque ligne dispose de deux boutons ← et → pour déplacer l'élément d'un niveau vers la gauche (réduire) ou la droite (imbriquer). L'imbrication est contrainte : il est impossible de sauter un niveau, un élément ne peut être imbriqué que d'un cran au-delà de son prédécesseur. Le déplacement se propage automatiquement aux descendants pour maintenir la cohérence de l'arbre.

La sidebar du builder liste les pages et catégories publiées du site, regroupées par type. Cliquer sur un élément l'ajoute immédiatement en bas de la liste principale avec le libellé et l'URL pré-remplis. Les sections Pages et Catégories sont repliées par défaut (collapsibles au clic) pour économiser l'espace sur les sites avec de nombreuses pages, et se déploient à la demande.

Des liens communs (Accueil, Blog, Contact) et un formulaire de lien personnalisé complètent la sidebar pour les cas non couverts par les contenus existants. La touche Entrée dans les champs du lien personnalisé déclenche l'ajout directement.

Sérialisation en arbre JSON

À la soumission du formulaire, la fonction flatToTree() parcourt la liste plate et reconstruit l'arbre imbriqué attendu par ContentManager::saveMenu() :

function flatToTree(flatItems) {
    const root  = [];
    const stack = [{ depth: -1, children: root }];
    for (const { label, url, depth } of flatItems) {
        const node = { label, url };
        while (stack.length > 1 && stack[stack.length - 1].depth >= depth) {
            stack.pop();
        }
        stack[stack.length - 1].children.push(node);
        node.children = [];
        stack.push({ depth, children: node.children });
    }
    return clean(root); // supprime les children vides
}

Le résultat est encodé en JSON et placé dans un champ <input type="hidden" name="menu_json"> avant soumission. Côté serveur, sanitizeMenuTree() valide récursivement chaque nœud (libellé et URL obligatoires, profondeur maximale 30 niveaux) avant la sauvegarde.

Stockage dans menu.json

Le menu est persisté dans content/menu.json sous forme d'un tableau JSON d'objets récursifs :

[
  { "label": "Accueil", "url": "https://monsite.fr/" },
  {
    "label": "Documentation",
    "url": "https://monsite.fr/documentation",
    "children": [
      { "label": "Installation", "url": "https://monsite.fr/installation" },
      { "label": "Architecture", "url": "https://monsite.fr/blog/categorie/architecture" }
    ]
  }
]

Ce format est lu par ContentManager::getMenu() et transmis directement aux thèmes pour générer la navigation. Les thèmes reçoivent l'arbre complet et gèrent eux-mêmes le rendu des niveaux d'imbrication : sous-menus déroulants au survol sur desktop, tiroir mobile avec scroll lock, accordéon, ou tout autre pattern adapté à leur design.

Articles similaires