🌳 Démo interactive
Double-clic pour renommer, clic droit pour le menu contextuel, glisser-déposer pour déplacer, Ctrl+clic pour multi-sélectionner. À droite, un second arbre alimenté par des données système en direct : le composant affiche ce qu'on lui câble.
Arborescence de fichiers, ou toute donnée à afficher en arbre
Déplacement virtuel- Glissez un nœud sur un dossier, ou entre deux lignes.
Chargement de l'arbre…
Données système
En directNavigateur, écran, réseau, et l'état de l'arbre de gauche : chaque valeur est un nœud mis à jour par set_text().
L'arbre en liste déroulante
Le même composant tient dans un champ de formulaire : un <select> qui déroule une hiérarchie au lieu d'une liste plate, avec filtre. Le nœud choisi remplit un champ caché, envoyé avec le formulaire.
Valeur envoyée : famille_id = (vide)
🗄️ Côté serveur
Le composant affiche et manipule l'arbre dans le navigateur, mais n'enregistre rien. Pour qu'un déplacement, un renommage ou une création survive au rechargement, il faut un code serveur : il sert l'arbre en JSON (en entier, ou branche par branche pour le chargement paresseux), reçoit les événements move_node, rename_node, create_node, delete_node et dnd_drop, vérifie les droits et met la base à jour.
Un arbre stocké en représentation intervallaire
En base, chaque nœud garde son parent_id et reçoit deux bornes, gauche (bg) et droite (bd), numérotées par un parcours en profondeur. Un nœud contient tous ceux dont les bornes tombent entre les siennes : la hiérarchie se lit par une simple comparaison de nombres, sans requête récursive.
| Besoin | Avec les bornes |
|---|---|
| Une branche entière (dossier et tous ses sous-dossiers) | WHERE bg BETWEEN :bg AND :bd : une requête, quelle que soit la profondeur. |
| Le chemin d'un résultat de recherche (fil d'Ariane) | WHERE bg < :bg AND bd > :bd ORDER BY bg : tous les ancêtres d'un coup. |
| Le nombre de descendants | (bd - bg - 1) / 2, sans aucune requête. |
| Feuille ou dossier ? | bd = bg + 1 : c'est une feuille. |
| Refuser un déplacement en boucle | La cible tombe-t-elle entre les bornes du nœud déplacé ? Une comparaison suffit. |
| Des totaux par branche (articles d'une famille et de ses sous-familles) | Une jointure sur l'intervalle, en une seule requête. |
| L'arbre dans l'ordre d'affichage | ORDER BY bg, servi par un index sur la borne gauche. |
La contrepartie porte sur l'écriture : déplacer un nœud renumérote des bornes. Sur des arbres de quelques centaines de nœuds, on recalcule tout l'arbre dans une transaction, ce qui reste simple et sûr. La lecture, de loin l'opération la plus fréquente, reste rapide.
Demander un devis
Décrivez votre arbre (quelles données, combien de nœuds, quelle base) et ce qu'il doit enregistrer. Un code de validation vous est envoyé par e-mail pour confirmer votre adresse.
📦 Installation
npm install @synapxlab/jstree
Ou via CDN (ESM, aucune installation) :
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@synapxlab/jstree/dist/style.css">
<script type="module">
import { JsTree } from 'https://cdn.jsdelivr.net/npm/@synapxlab/jstree/dist/jstree.es.js';
</script>
⚡ Démarrage rapide
import { JsTree } from '@synapxlab/jstree';
import '@synapxlab/jstree/style.css';
const tree = new JsTree('#tree', {
plugins: ['wholerow', 'contextmenu', 'dnd', 'search', 'state', 'types'],
core: {
multiple: true,
check_callback: true,
data: [
{ id: '1', text: 'Racine', parent: '#', state: { opened: true } },
{ id: '2', text: 'Documents', parent: '1' },
{ id: '3', text: 'Rapport.pdf', parent: '2' },
],
},
});
const tree = new JsTree('#tree', {
core: {
// Chargement paresseux : appelé à l'ouverture d'un nœud
data: {
url: (node) => `/api/nodes?parent=${node.id}`,
},
},
});
📖 API & méthodes
Le constructeur prend un sélecteur (ou un élément) et un objet d'options. L'instance expose des méthodes de manipulation des nœuds.
| Méthode | Description |
|---|---|
edit(id) | Renommage inline du nœud. |
create_node(parentId, opts) | Crée un nœud enfant, renvoie son id. |
delete_node(id) | Supprime un nœud (et ses enfants). |
move_node(id, parentId) | Déplace un nœud sous un autre parent. |
open_node(id) / close_node(id) | Ouvre / referme un nœud. |
open_all() / close_all() | Déplie / replie tout l'arbre (ou la branche passée en argument). |
set_text(id, texte) | Change le libellé sans redessiner l'arbre — utile pour des valeurs en direct. |
select_node(id) | Sélectionne un nœud (multi-sélection si activée). |
search(terme) | Filtre l'arbre (plugin search). |
{ id, text, parent, icon?, state? }. parent: '#' désigne la racine. Format plat (liste) ou imbriqué (children).
🧩 Plugins
wholerow
Surlignage de la ligne entière au survol et à la sélection.
contextmenu
Menu clic droit personnalisable (renommer, créer, supprimer…).
dnd
Glisser-déposer pour réorganiser et déplacer les nœuds.
search
Filtrage et mise en évidence des nœuds correspondants.
state
Persistance de l'état (ouverture, sélection) en localStorage.
types
Types de nœuds avec icônes et règles (dossier, fichier…).
dnd ne déplace pas seulement les nœuds de l'arbre. N'importe quel élément de la page (ligne de tableau, carte, fichier d'une liste de documents) peut être glissé dans un dossier : la source émet dnd_start.vakata sur l'arbre avec les données de son choix, l'arbre indique le dossier visé pendant le geste, puis émet dnd_drop avec external: true, le dossier cible et ces données. À votre code de les ranger, en mémoire ou côté serveur.
📡 Événements
Les événements sont émis comme CustomEvent natifs sur l'élément conteneur, suffixés .jstree.
tree.element.addEventListener('select_node.jstree', (e) => {
console.log('Sélectionné :', e.detail.node.id, e.detail.node.text);
});
Événements disponibles : select_node, deselect_node, create_node, rename_node, delete_node, move_node, open_node, close_node. Le plugin dnd ajoute dnd_start, dnd_stop, dnd_cancel et dnd_refused (dépôt refusé par move_node).