La construction d'un arbre HTML est une opération fastidieuse. Pour la simplifier, la librairieJmp.Web dispose de la classe Jmp.Controls.Tree. Cet article donne une procédure à partir d'un exemple permettant la création d'un tel arbre. Nous allons l'illustre ici par un exemple. Ce dernier consiste à créer l'arbre HTML à partir d'un arbre n-aire "ordinaire" (classe Jmp.Data.SerializableTree).
Pour rappel, la classe SerializableTree représente le nœud d'un arbre. Ce nœud est affecté d'un identifiant unique (entier long) auquel on peut "accrocher" une objet qui implémente l'interface Serializable. Ce nœud possède une collection d'un nombre quelconque "d'enfants" qui a leur tour peuvent avoir un nombre quelconques d'enfants.
La construction d'un arbre reposant sur la classe SerializableTree n'est pas l'objet de cet article mais elle ne présente pas de grosse difficulté. Nous supposerons que nous détenons un arbre de ce type à représenter dans une page HTML.
Voici alors la procédure à suivre :
// Ajout de l'image-lien de pliage d'un noeud
Image imgExpanded = new Image();
imgExpanded.getCommonAttribute().setTitle(_lcl.getString("ToCollapse"));
imgExpanded.setImageLink(String.format("%s/images/triangle-down.png", _acn.getSiteURL()));;
imgExpanded.setDimensions(new Point(16,16));
String sTag = "";
if (cmd != null)
sTag = String.format("cmd=%s&scm=eoc", cmd.name());
ImageLink ilnCollapse = new ImageLink(String.format("%s?tag=%s", _acn.getApplicationURL(), sTag), imgExpanded);
tre.setCollaspeCommand(ilnCollapse);Notez que l'invocation de la méthode n'est pas obligatoire. Si elle n'est pas invoquée, aucune icône de pliage ou de déploiement ne sera ajoutée à l'arbre.// Ajout de l'image-lien de dépliage d'un noeud
Image imgCollapsed = new Image();
imgCollapsed.getCommonAttribute().setTitle(_lcl.getString("ToExpand"));
imgCollapsed.setImageLink(String.format("%s/images/triangle-right.png", _acn.getSiteURL()));
imgCollapsed.setDimensions(new Point(16,16));
sTag = "";
if (cmd != null)
sTag = String.format("cmd=%s&scm=eoc", cmd.name());
ImageLink ilnExpand = new ImageLink(String.format("%s?tag=%s", _acn.getApplicationURL(), sTag), imgCollapsed);
tre.setExpandCommand(ilnExpand);Là encre, la présence de ce lien-image n'est pas obligatoire.// Construction des branches à partir des noeuds racine
lst.stream().forEach(srl ->
{
Node nde = new Node(tre, srl.getID());
nde.setData(srl);
nde.setLabel(tlb.buildLabel(nde));
if (srl.isExpanded())
nde.expand();
else
nde.collapse();
nde.setLeafIcon(aimgNode[0]);
nde.setExpandedIcon(aimgNode[1]);
nde.setCollapsedIcon(aimgNode[2]);
tre.addChild(nde);
buildBranch(srl, nde, tlb, aimgNode);
});Bien que ce code paraisse simple, il masque l'ensemble des difficultés. Nous allons le détailler. L'idée générale est de transformer l'arbre initiale en un arbre HTML en substituant aux nœuds de classe SerializableTree des nœuds de classe Node "accrochés" aux même endroits que ceux de l'arbre initial. Notez que pour ne pas perdre d'information sur le nœud initial, nous l'accrochons en tant que donnée utilisateur au nœud HTML : nde.setData(srl); (le paramètre est le nœud initial)./**
* Construit la branche "concrète" (affichable en HTML) d'une branche issue d'un arbre virtuel.
* @param srlParent Noeud virtuel parent.
* @param ndeParent Noeud concret parent.
* @param tlb Fabrique des libellés de l'arbre.
* @param aimgNode Images adapté à l'état du noeud. 0 : feuille, 1 : déplié, 2 ou (> 1) : replié.
*/
private void buildBranch(SerializableTree srlParent, Node ndeParent, ITreeLabelBuilder tlb, Image[] aimgNode)
{
srlParent.getChildren().stream().forEach(srl ->
{
Node nde = new Node(ndeParent.getTree(), srl.getID());
nde.setData(srl);
nde.setLabel(tlb.buildLabel(nde));
nde.setLeafIcon(aimgNode[0]);
nde.setExpandedIcon(aimgNode[1]);
nde.setCollapsedIcon(aimgNode[2]);
if (srl.isExpanded())
nde.expand();
else
nde.collapse();
ndeParent.addChild(nde);
// Récursion
buildBranch(srl, nde, tlb, aimgNode);
});
}La récursivité s'interrompt lorsque le nœud n'a pas d'enfant.Toutes ces étapes accomplies, nous possédons un arbre HTML de classe Tree qui est le reflet de l'arbre initial de classe SerializableTree. Il suffit d'invoquer sa méthode toHtml5 pour le sérialiser sous code HTML5.
Ces explications montent également qu'il est assez aisé de créer un arbre HTML from scratch (on crée des objets de classe Node que l'on accroche aux différents autres nœuds de l'arbre. Le lecteur attentif, peut s'interroger sur la nécessité de passer par une classe Tree alors que l'on aurait pu se contenter d'un objet de classe Node comme "racine". Cela tient au fait qu'en HTML, on a assez souvent besoin de présenter des branches n'ayant pas une racine commune (un arbre multi-racines en quelques sortes). De plus, la classe Tree permet de factoriser les comportements spécifiques à l'arbre et non au nœud (comme les icônes de pliage ou de déploiement).
Bien entendu, il est assez facile de transposer ce code à n'importe quel arbre non réalisé sur des nœuds de classe SerializableTree.
Nous allons regrouper ces fragments de code en une méthode générique qui permet de transposer assez simplement un arbre de classe SerializableTree en un arbre de classe Tree. Cet extrait est tiré d'une application réelle. Sa généricité peut donc encore être nettement améliorée de manière à ne faire un méthode statique de portée très générale.
/**
* Construit l'arbre affichable via HTML qui correspond à l'arbre passé en paramètre.
* @param lst Liste des noeuds racine de l'arbre virtuel.
* @param tlb Fabrique des libellés de l'arbre.
* @param sTreeID Identifiant de l'arbre.
* @param aimgNode Images adapté à l'état du noeud. 0 : feuille, 1 : déplié, 2 ou (> 1) : replié.
* @param cmd Commande à ajouter aux liens des boutons de pliage / dépliage des noeuds (pas d'ajout si objet nul).
* @return Arbre capable de s'afficher en HTML.
*/
public Tree buildTree(
List<SerializableTree> lst, ITreeLabelBuilder tlb, String sTreeID, Image[] aimgNode, Command cmd)
{
if (tlb == null)
throw new IllegalArgumentException("ITreeLabelBuilder tlb = null");
if (lst == null)
return null;
Tree tre = new Tree(sTreeID);
// Ajout de l'image-lien de pliage d'un noeud
Image imgExpanded = new Image();
imgExpanded.getCommonAttribute().setTitle(_lcl.getString("ToCollapse"));
imgExpanded.setImageLink(String.format("%s/images/triangle-down.png", _acn.getSiteURL()));;
imgExpanded.setDimensions(new Point(16,16));
String sTag = "";
if (cmd != null)
sTag = String.format("cmd=%s&scm=eoc", cmd.name());
sTag = AbstractPageBuilder.cipherHttpParameters(_acn, sTag);
ImageLink ilnCollapse = new ImageLink(String.format("%s?tag=%s", _acn.getApplicationURL(), sTag), imgExpanded);
tre.setCollaspeCommand(ilnCollapse);
// Ajout de l'image-lien de dépliage d'un noeud
Image imgCollapsed = new Image();
imgCollapsed.getCommonAttribute().setTitle(_lcl.getString("ToExpand"));
imgCollapsed.setImageLink(String.format("%s/images/triangle-right.png", _acn.getSiteURL()));
imgCollapsed.setDimensions(new Point(16,16));
sTag = "";
if (cmd != null)
sTag = String.format("cmd=%s&scm=eoc", cmd.name());
sTag = AbstractPageBuilder.cipherHttpParameters(_acn, sTag);
ImageLink ilnExpand = new ImageLink(String.format("%s?tag=%s", _acn.getApplicationURL(), sTag), imgCollapsed);
tre.setExpandCommand(ilnExpand);
// Construction des branches à partir des noeuds racine
lst.stream().forEach(srl ->
{
Node nde = new Node(tre, srl.getID());
nde.setData(srl);
nde.setLabel(tlb.buildLabel(nde));
if (srl.isExpanded())
nde.expand();
else
nde.collapse();
nde.setLeafIcon(aimgNode[0]);
nde.setExpandedIcon(aimgNode[1]);
nde.setCollapsedIcon(aimgNode[2]);
tre.addChild(nde);
buildBranch(srl, nde, tlb, aimgNode);
});
return tre;
}En vue d'améliorer la généricité de cette méthode, on note que ce code contient "en dur" un certain nombre de choses qui pourraient être converties en paramètres ou éléments de contexte (comme l'URL des icônes de pliage/déploiement ou les paramètres de commande du pliage/déploiement du nœud). L'attribut _lcl est un objet de traduction des chaînes de caractères. On aura intérêt à en faire un paramètre ou un objet global ce qui permettrait de rendre cette méthode statique.
Ce code s'appuie sur les librairies PiApplications Jmp.jar et Jmp.Web.jar.
Pour être complet, nous livrons le code de l'interface ITreeLabelBuilder.
import Jmp.Web.Controls.IWebComponent;
import Jmp.Web.Controls.Node;
/**
* Interface implémentée par toute classe de construction d'un libellé du noeud d'un arbre affichable.
* @author (c) PiApplications 2014.
*/
public interface ITreeLabelBuilder
{
/**
* Construit le libellé d'un noeud d'arbre à afficher.
* @param nde Noeud dont le libellé doit être affiché.
* @return Libellé d'un noeud d'arbre à afficher.
*/
IWebComponent buildLabel(Node nde);
}Vous noterez qu'il s'agit d'une interface fonctionnelle ce qui autorise à remplacer le paramètre ITreeLabelBuilder tlb dans la méthode buildTree par une expression lambda.
Il ne reste plus qu'à implémenter cette interface pour obtenir le contrôle HTML qui représentera le libellé d'un nœud dans un contexte donné.
La classe Tree est la tête d'un arbre limité à une collection de nœuds "racine" (objets de classeJmp.Web.Controls.Node). Chaque nœud dispose ensuite d'une filiation d'autres objets de classes Node et ainsi de suite. Cette classe Node implémente l'interface IWebComponent.
On ajoute un nœud racine à l'arbre en invoquant la méthode addChild de la classe Tree. Cette méthode a deux prototypes dont l'un est trompeur. Il donne l'impression d'accepter un paramètre qui se limite à implémenter l'interface IWebComponent (ce que fait la classe Node) mais en fait, quelque soit les prototypes, ils attendent l'ajout d'un objet de classe ou dérivant de la classe Node. Le prototype admettant un paramètre de type IWebComponent est simplement imposé par l'héritage de la classe Tree (AbstractWebComponent).
La représentation HTML du nœud (son libellé) se fait au moyen de la méthode Node.setLabel(IWebComponent). Le contrôle qui implémente l'interface IWebComponent peut ici être quelconque tant qu'il est compatible de l'ajout à une cellule d'un tableau.
Rédaction par Jean-Marie Piatte (1983-2021)