Comment générer un menu persistent simple
avec PHP, CSS et jQuery

Sur une page Web, un menu de navigation est un élément d'interface graphique présentant une liste de liens sur lesquels l'usager peut "cliquer" pour se rendre à une destination. Ils sont très communs sur les pages Web et ils permettent de naviguer vers d'autres pages d'un site.

À titre d'exercice, j'ai codé un menu très simple affiché à gauche de ce texte. Il est généré par du code PHP et activé sur le fureteur du client par du code jQuery. Comme je voulais garder la liste des liens dans un fichier texte appelé "fichier structure", j'ai dû utiliser PHP pour transformer cette liste en une liste que les fureteurs vont reconnaître comme un menu (Étape 1). Pour rendre ce menu interactif, j'ai utilisé jQuery (Étape 2). Finalement, un fichier CSS gère la mise en forme du menu(Étape 3).

Code PHP

Cette première étape s'exécute sur le serveur avant la livraison de la page au client. Comme je l'ai dit plus haut, la liste des liens est emmagasinée dans un ou plusieurs fichiers sur le serveur sous la forme suivante que j'appelle le fichier Structure:

.|Premier parent|#|
..|Premier enfant|[lien1]|
..|Deuxième enfant|[lien2]|
.|Deuxième parent|#|
..|Troisième enfant|[lien3]|
..|Quatrième enfant|[lien4]|
..|Troisième parent|#|
...|Cinquième enfant|[lien5]|
..|Quatrième parent|#|
...|Sixième enfant|[lien6]|
.|Cinquième parent|#|
..|Septième enfant|[lien7]|

Le caractère "|" est un séparateur et chaque ligne comporte trois parties: le niveau indiqué par le nombre de points successifs, le titre de l'item de menu et le lien vers la cible.

Travail à faire

Pour en faire un menu affichable sur une page HTML, il faut qu'il prenne la forme suivante que je vais appeler la "forme canonique":

<ul id="nav" class="noprint">
  <li><a class="noIcon" href="#">Premier parent</a>
    <ul class="sub">
      <li><a href="[lien1]">Premier enfant</a></li>
      <li><a href="[lien2]">Deuxième enfant</a></li>
    </ul>
  </li>
  <li><a href="#">Deuxième parent</a>
    <ul class="sub">
      <li><a href="[lien3]">Troisième enfant</a></li>
      <li><a href="[lien4]">Quatrième enfant</a></li>
      <li><a href="#">Troisième parent</a>
        <ul class="sub">
          <li><a href="[lien5]">Cinquième enfant</a></li>
        </ul>
      </li>
      <li><a href="#">Quatrième parent</a>
        <ul class="sub">
          <li><a href="[lien6]">Sixième enfant</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li ><a href="#">Cinquième parent</a>
    <ul class="sub">
      <li ><a href="[lien7]">Septième enfant</a></li>
    </ul>
  </li>
</ul>	

Lancement de l'objet GTRO_jQuery_Menu

C'est ici que PHP effectue la transformation des liens des fichiers "Structure" vers la forme canonique. Elle se fait en deux étapes: la collecte des données et la transformation vers la forme canonique affichable par HTML.

Le code du menu est "orienté objet" et il est fortement inspiré du code utilisé par PHP Layers Menu System (PHPLM). Il est appelé comme suit:

$mid = new GTRO_jQuery_Menu();
$mid->setStructureFile($myDirPath . 'menu.txt'); // collecte des donnée source
echo $mid->parseStructure('gtromenu'); // transformation vers la forme canonique

où menu.txt contient le fichier structure. Après ces énoncés, l'objet GTRO-jQuery_Menu est prêt à être affiché.

Collecte des données

Ici, PHP ouvre le fichier structure ($tree_file) et le transcrit dans la variable $Structure par regroupements de 4096 octets.

function setStructureFile($tree_file)
// Prend les données source du menu dans le fichier structure et l'insère dans la variable $Structure
{
  if (!($fd = fopen($tree_file, 'r'))){ // ouvre le fichier structure
    $this->error("setMenuStructureFile: incapable d'ouvrir le fichier structure $tree_file.");
    return false;
  }
  $this->Structure = '';
  while ($buffer = fgets($fd, 4096)) // divise le contenu du fichier en groupes de 4096 octets
    $this->Structure .= $buffer;
  fclose($fd);
  if ($this->Structure == ''){
    $this->error("setStructureFile: $tree_file is vide.");
    return false;
  }
  return true;
}

L'objet GTRO_jQuery_Menu contient aussi une fonction appelée appendStructureFile() qui ajoute le contenu d'un nombre à déterminer de fichier "structure". Le code est le même à l'exception de l'énoncé 8 qui est $this->Structure .= ''; (noter le . avant le signe =) permettant ainsi l'ajout d'un ou de quelques fichiers "structure" à la variable $Structure.

Transformation vers la forme canonique

Une fois la variable $Structure bien remplie, il faut maintenant l'analyser et construire la forme canonique pour que le fureteur puisse l'afficher.

Dans une première phase, le code lit la variable $Structure ligne par ligne et remplit un tableau associatif appelé $Tree qui, pour chaque ligne de $Structure contient le niveau, le titre et le lien de chaque noeud.

function parseStructure($menu_name = '')
// $menu_name identifie le menu ==> on peut en gérer plusieurs
// Transforme le contenu de $Structure en sa forme canonique
{		
  // Initialisation des variables utilisées par la fonction: supprimée de ce code  
  while ($Structure != '') // lit $Structure and crée le tableau associatif $nodes 
  {
    $before_cr = strcspn($Structure, "\n");// = #char before \n
    $buffer = substr($Structure, 0, $before_cr); // extraction d'une chaîne/ligne de $Structure
    $Structure = substr($Structure, $before_cr+1); // qui est ensuite supprimée
    if (substr($buffer, 0, 1) == '#') 
      continue;	// suprime les commetaires
    $tmp = rtrim($buffer); // nettoyage de $buffer
    $node = explode('|', $tmp); // la chaîne est coupée en trois segments
    $Tree[$cnt]['level'] = strlen($node[0]); // le niveau du noeud
    $Tree[$cnt]['caption'] = $node[1];       // le titre du noeud
    $Tree[$cnt]['href'] = $node[2];          // le lien du noeud
    $cnt++;
  } // fin de while ($Structure != '')

À ce moment-ci, la variable $Structure est vide et le tableau associatif $Tree est rempli. Il faut maintenant en faire l'analyse et générer la forme canonique du menu que les fureteurs pourront afficher.

  $LastItem[$menu_name] = count($Tree);
  $nodesCount = $LastItem[$menu_name];
  $Tree[$LastItem[$menu_name]+1]['level'] = 0;
  $output = '<ul id="nav" class="noprint">'; // ouvre le menu id="nav"
		
  for ($cnt=$FirstItem[$menu_name]; $cnt <= $LastItem[$menu_name]; $cnt++){
    $text = '';
    $Tree[$cnt]['caption'] = stripslashes($Tree[$cnt]['caption']);
    $Tree[$cnt]['haschildren'] = true; //Identification des noeuds parents et enfants
    if ($Tree[$cnt]['href'] == '#'){ // si c'est un noeud parent
      $Tree[$cnt]['isfolder'] = true;
      if (($cnt < $LastItem[$menu_name]) && ($Tree[$cnt]['isfolder'] == true) && ($Tree[$cnt+1]['level'] == $Tree[$cnt]['level']))
      $Tree[$cnt]['haschildren'] = false;			
    }
    else 			
      $Tree[$cnt]['isfolder'] = false;
				
    if ($cnt > 1) { // pour chaque noeud après le premier
      $delta = $Tree[$cnt-1]['level']-$Tree[$cnt]['level']; // calcule le # niveaux vers le bas
      for ($i=1; $i <= $delta; $i++)
        $text .= $backOneLevel; // ajoute autant de '</ul></li>'.chr(13) qu'il en faut 
    } // ($cnt > 1)
			
    if ($Tree[$cnt]['isfolder'] == true) { // output  parent lines of code
      $text .= '<li class="parent"  id="N'.$cnt.'" rel="'.$Tree[$cnt]['level'].'"><a href="'.$Tree[$cnt]['href'].'">'.$spaces.$Tree[$cnt]['caption'].'</a>';
      if ($Tree[$cnt]['haschildren'] == true)
        $text .= $upOneLevel;
    }
    else 
      $text .= '<li class="child" id="N'.$cnt.'" rel="'.$Tree[$cnt]['level'].'"><a href="'.$Tree[$cnt]['href'].'">'.$Tree[$cnt]['caption'].'</a></li>';
    
    $output .= chr(13).$text; // ajoute le noeud au code
    $LastLevel= $Tree[$cnt]['level'];
  } // fin de la boucle for($cnt ...
		
  $delta = $LastLevel;
  for ($i=1; $i < $delta; $i++)
    $output .= chr(13).'</ul></li>';

  $output .= '</ul>'; // ferme le menu
  return $output;
}

Code jQuery

Pour que le menu réagisse aux actions de l'utilisateur, je devais utiliser un langage script" s'exécutant sur le fureteur du client. J'ai choisi Javascript et sa bibliothèque jQuery.

Pour commencer, je me suis assuré que les commandes jQuery se feraient une fois la page complètement chargée. Ceci se fait comme suit:

$(document).ready(function( ) 
{
  // insérer le code jQuery ici.
}

HTTP n'a pas d'état

En informatique, un protocole sans état est un protocole qui traite chaque demande comme une transaction indépendante sans relation avec les précédentes. Ça simplifie le travail du serveur qui n'a pas besoin d'allouer dynamiquement de l'espace pour y conserver des informations concernant la session en cours (il peut y avoir un grand nombre de clients connectés).

Le Web est régi par le protocole "Hypertext Transfer Protocol (HTTP)" qui est un protocole sans état. Il va sans dire que, lors d'un chargement de page, la nouvelle page chargée ne se rappelle pas du tout de ce qui s'est passé dans la page précédente. Cependant, HTTP offre deux méthodes permettant à une nouvelle page de se souvenir: l'usage de formulaires ($_GET et $_POST) et des "cookies".

Dans le cas qui nous occupe, lors d'un chargement de page, le menu ne se rappellerait de rien de son état dans la page précédente. Tous les noeuds "parent" seraient tous fermés. Pour que le menu se rappelle de son état précédent, j'ai donc employé des "cookies".

Dans les faits, lorsque l'utilisateur clique sur un noeud parent, son contenu s'ouvre et crée un "cookie" y correspondant de sorte que lors du chargement suivant de la page, ce noeud reste ouvert. Dans le code commenté qui suit

// ce code associe un cookie avec les menus ouverts 
$('#nav li > a').click(function(){
  if (!$(this).parent().children('ul.sub').length == 0) // teste si le noeud a un enfant ul.sub
    $(this).toggleClass('closeit').toggleClass('openit'); // change l'icône ouvert/fermé
  var menu = $(this).next('.sub');
  if(menu.is(':visible')) {
    menu.fadeOut(1000); // Cache les enfants du noeud parent
    $.cookie("gtro-" + $(this).parent().attr("id"), null, { path: '/'}); // supprime le cookie
  } else {
    menu.fadeIn(1000);  // Affiche les enfants du noeud parent 
    $.cookie("gtro-" + $(this).parent().attr("id"), true, {  path: '/', expires:1 }); // génère le cookie
  }
});

si un noeud parent est cliqué (et qu'il a des enfants), sa classe alterne de "ouvert" avec l'icône "closeit" à "fermé" avec l'icône "openit". Si le clic ouvre le noeud, un cookie lui correspondant et accessible sur tout le domaine est créé pour une journée. Si le clic ferme le noeud, le cookie lui correspondant est supprimé.

Persistance du menu

Lors du chargement d'une page, le code qui suit affiche le menu. Grâce aux "cookies" associés aux menus ouverts, le menu devient persistent et se rappelle de son état antérieur. Le code commenté qui suit explique ce qui se passe.

// sélection de tous les noeuds parents ayant des enfants
$('#nav ul.sub').each(function(){
		if ($.cookie("gtro-" + $(this).parent().attr("id")) == null) { //s'il n'y a pas de cookie correspondant
			$(this).hide(); // cache les enfants
			$(this).parent().children('a').addClass('openit'); // et lui attribue l'icône 'openit'
		} else // si non le garder en vue et lui attribuer l'icône 'closeit'
     $(this).parent().children('a').addClass('closeit'); 
	});

Affichage supplémentaire des titres de la page en cours

Afin de compléter le menu, j'ai ajouté un noeud parent supplémentaire qui affiche les titres de tous les balises <h1> et <h2> de la page.

// affiche les titres <h1> et <h2> de la page à la suite du menu
$('h1, h2').each(function(index, value){
  $('#local').append('<li class="child"><a href="#anchor-'+index+'">'+$(this).html()+'</a></li>');
  $(this).html('<a name="anchor-'+index+'">'+$(this).html()+'</a>');
})

Le problème avec ce code vient de ce que, si la cible est déjà munie d'un ancre, le lien ne fonctionne pas. Pour corriger le problème, il faut donc tester s'il y a déjà un ancre sur la cible. Si c'est le cas, on ne doit pas en créer un autre mais bien l'utiliser.

Pour y arriver, j'ai modifié le code comme suit

// affiche les titres des éléments <h1> et <h2> de la page à la suite du menu
$('h1, h2').each(function(index, value){
  if ($(this).children('a').length == 0) { // teste la présence d'un ancre pré-existant
    $('#local').append('<li class="child"><a href="#anchor-'+index+'">'+$(this).html()+'</a></li>');
    $(this).html('<a name="anchor-'+index+'"></a>'+$(this).html());
  } else { // utilise l'ancre pré-existant
    var attrName = $(this).children('a').attr('name'); // trouve la valeur de name et l'utilise
    $('#local').append('<li class="child"><a href="#'+attrName+'">'+$(this).text()+'</a></li>');
  }
})

Affichage du menu

Finalement, lorsque la page vient d'être chargée, le menu est invisible. Ce qui est visible, c'est un rectangle noir sur lequel est écrit le mot "Menu". Le code commenté qui gère l'apparition ou la disparition du menu est le suivant:

$('#menus').hide(); // cache le menu
$('#menu').visible; // affiche un rectangle noir avec la mention "Menu"
	
$('#menu').click(function(){ // un clic sur le rectangle noir
  $('#menus').show('slide'); // affiche le menu et
  $(this).hide(); // fait disaraître le rectangle noir
});
	
$('#content').click(function(){ // un clic n'importe où sur la page en dehors du menu
  $('#menu').show('slide'); // fait apparaître le rectangle noir
  $('#menus').hide(); // et fait apparaître le rectangle noir
});

Adaptation du menu à la grandeur de la fenêtre

Je voulais que le menu, une fois affiché, occupe toute la hauteur de la fenêtre du fureteur quitte à ce que des barres de défilement apparaissent lorsque le contenu dépasse la hauteur de la fenêtre.

// Adaptation du menu à la hauteur de la fenêtre
$(window).bind("load resize", function() {
  $("#menus").css("height", $(window).height() - 30);
});

Le code qui précède lie les action load et resize de Windows avec l'action qui consiste à ajuster la hauteur du menu avec la hauteur de la fenêtre. En d'autre termes, lors du chargement et du redimensionnement de la fenêtre du fureteur, le menu s'ajuste à la hauteur de la fenêtre.

Mise en forme

Pour ce menu, la mise en forme est gérée par un fichier CSS appelé menus.css. Dans ce fichier,

#menus {
	position:fixed; /* en position "fixed" */
	top: 10px; /* à 10 pixels du haut de la fenêtre */
	left: 20px; /* et à 20 pixels du côté gauche de la fenêtre */
	color:#FFF; /* la couleur de la police est blanche */
	background-color: #000; /* l'arrière-plan est noir
	max-width: 300px; /* le menu a une largeux maximum de 300 pixels */
	font-size: 10pt; /* la police a 10 points */
	overflow:auto; /* met une barre de défilement horizontale ou verticale au besoin */
}

le menu représenté par une <div id="menus"> formatté comme il est expliqué dans les commentaires du code. L'affichage des icônes closeit [] et openit [] sont gérées comme suit:

#nav li.parent a.closeit{
  cursor: pointer;
  background: url(../images/close.png) no-repeat 0% 100%;
  margin-bottom: 4px;
  padding-right: 19px;
}

#nav li.parent a.openit{
  background: url(../images/open.png) no-repeat 0% 100%;
  margin-bottom: 4px;
  padding-right: 19px;
  cursor: pointer;
}

Finalement, la mise en forme suivante est nécessaire pour que les ... n'apparaîssent pas dans l'affichage.

#menus a 
{ 
	text-decoration:none; // supprime les icônes préfixes de la liste non ordonnée 
	text-align: left;
	color: #FFF;
}

Discussion

Je reconnais que le sujet est élémentaire mais je l'ai développé pour le rendre clair pour quiconque ne l'a jamais fait.

 

 


Pour toute question ou commentaire
E-Mail
page modifiée le 12 février 2014 à 15:54:31. []