Menu contextuel HTML sans framework

Sans aucun framework, ajouter un menu contextuel en JavaScript et CSS aux éléments d'une application ou une page Web.

Ce script est compatible avec IE9, Firefox, Chrome, Safari.

Il convient pour remplacer le menu par défaut apparaîssant par un clic du bouton droit de la souris, par un menu spécifique à l'élément de l'interface ainsi cliqué. Si l'on veut juste faire apparaître une fenêtre ou une liste quand la souris passe sur un élément, on utilisera plutôt une infobulle.

On sait que l'on détecte un clic d'un bouton quelconque par l'attribut onClick, mais pour répondre au bouton droit on utilise l'attribut onContextMenu.

La première étape consiste à supprimer le menu contextuel par défaut. Pour cela il suffit de retourner false.

<div oncontextmenu="return false"></div> 

Comme on va en fait appeler une fonction en réponse à l'évènement, il faudra que la fonction retourne false et l'on retourne le résultat de la fonction comme ceci:

<div oncontextmenu="return mafonction()"></div> 

Pour construire le menu contextuel, une fonction génére un calque que l'on remplira dynamiquement avec la liste des items du menu. Par exemple, on ajoute les commandes "Renommer" et "Editer". La feuille de style est incorporée statiquement à la page qui contient le script.

Voici une démonstration du script, suivie par le code source complet.

Démonstration : Cliquez le bouton droit

Le code HTML est simple:

<div oncontextmenu="return monmenu(this)">
  Démonstration : Cliquez le bouton droit
</div> 

Le code JavaScript capture d'abord la position actuelle de la souris pour pour placer le menu contextuel généré au dessus de l'élément correspondant.

La fonction que l'on a définie fonctionne sur tous les navigateurs modernes. Comme le script est destiné à une application, on peut faire l'impasse sur les outils de navigation du passé.

var xMousePosition = 0;
var yMousePosition = 0;
document.onmousemove = function(e)
{
  xMousePosition = e.clientX + window.pageXOffset;
  yMousePosition = e.clientY + window.pageYOffset;
};

Les attributs pageXOffset et pageYOffset permettent de prendre en compte le déroulement de la page (ce qui est en fait inutile si l'interface ne déroule pas dans la fenêtre du navigateur).

Il est encore possible de se passer de cette fonction en plaçant le menu contextuel comme descendant de l'élément correspondant, plutôt que de son conteneur, et de lui donner une position fixe à coté de cet élément.

Le menu est ensuite créé et positionné selon la position de la souris.

Le code JavaScript complet du menu contextuel

var xMousePosition = 0;
var yMousePosition = 0;
document.onmousemove = function(e)
{
  xMousePosition = e.clientX + window.pageXOffset;
  yMousePosition = e.clientY + window.pageYOffset;
};


function rename(element)
{
  alert("Renommer");
}

function edit(element)
{
  alert("Editer");
}

function monmenu(element)
{
  var x = document.getElementById('ctxmenu1');
  if(x) x.parentNode.removeChild(x);

  var d = document.createElement('div');
  d.setAttribute('class', 'ctxmenu');
  d.setAttribute('id', 'ctxmenu1');
  element.parentNode.appendChild(d);
  d.style.left = xMousePosition + "px";
  d.style.top = yMousePosition + "px"; 
  d.onmouseover = function(e) { this.style.cursor = 'pointer'; } 
  d.onclick = function(e) { element.parentNode.removeChild(d);  }
  document.body.onclick = function(e) { element.parentNode.removeChild(d);  }

  var p = document.createElement('p');
  d.appendChild(p);
  p.onclick=function() { rename(element) };
  p.setAttribute('class', 'ctxline');
  p.innerHTML = "Renommer";

  var p2 = document.createElement('p');
  d.appendChild(p2);
  p2.onclick=function() { edit(element) };  
  p2.setAttribute('class', 'ctxline');
  p2.innerHTML = "Editer"; 

  return false;
}

Les fonctions rename() et edit() servent uniquement à la démonstration et seront remplacées par vos propres fonctions.

Ce code reflète deux choix de conception:

  1. Le menu est créé dynamiquement par insertion de nouvelles balises dans le DOM.
  2. Il n'y a pas de fonction d'ajout de lignes, on crée un code spécifique pour chaque commande. C'est suffisant si l'on a peu de menus contextuels dans l'application, mais il faudra développer si on a beaucoup. En fait il est assez facile d'ajouter une ligne par copier/coller et en changeant les données.

Le menu ainsi généré est équivalent au code statique suivant, où l'on voit qu'il serait plus difficile d'associer des évènements aux balises:

<body onclick="document.getElementById('ctxmenu1').parentNode.removeChild(d);">
  <div class="ctxmenu" id="ctxmenu1"
       onmouseover="this.style.cursor = 'pointer'"
       onclick="this.parentNode.parentNode.removeChild(this)">
  <p class="ctxline" onclick="rename(element)">Renommer</p>
  <p class="ctxline" onclick="edit(element)">Editer</p>
</div>

La variable element représente l'objet auquel est associé le menu contextuel. Dans notre exemple, se serait un nom de fichier dans une liste.

Le code CSS complet

.ctxmenu
{
  position:absolute;	
  min-width: 128px;
  height:auto;
  padding: 8px;
  margin:0;
  margin-left:32px;
  margin-top:-16px;
  border: 1px solid #999;
  background: #F8F8F8;
  box-shadow: 2px 2px 2px #AAA;
  z-index:11;
  overflow: visible;
}
.ctxline
{
  display:block;
  margin:0px;
  padding:2px 2px 2px 8px;
  border:1px solid #F8F8F8;
  border-radius:3px;
  font-size:13px;
  font-family:Arial, Helvetica, sans-serif;
  overflow:visible;
}
.ctxline:hover
{
  border:1px solid #BBB;
  background-color: #F0F0F0;
  background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
  background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6);
  background-image: -webkit-gradient(linear, 0 0, 0 100%,
    from(#ffffff), to(#e6e6e6));
  background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
  background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
  background-image: linear-gradient(top, #ffffff, #e6e6e6);
  background-repeat: repeat-x;
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff',
    endColorstr='#e6e6e6', GradientType=0);
}

Certaines propriétés sont essentielles:

Le reste est une question d'apparence.

On a essayé de donner un look assez proche de celui d'un environnement Windows standard. Comme l'apparence des éléments de Windows dépend en fait du thème choisi par l'utilisateur, il se peut que l'apparence ne soit pas celle des menus contextuels sur votre système. Mais il est difficile de faire mieux sur ce plan...

Limitation

Quand j'utilise ce menu contextuel en production il arrive que le navigateur assigne les propriétés left et top de façon incrémentale en ajoutant les valeurs données à celles du conteneur. Cela en fait est dû au <!doctype html> de HTML 5 et ne se produit pas avec les précédents doctypes.
Pour contourner ce problème, on peut ajouter le menu en tant que descendant de l'objet auquel il est associé et positionner le menu de façon statique, avec une valeur négative pour top. Mais on peut aussi changer le doctype.

La meilleure solution est en fait de rendre la position du conteneur statique (ce qui est la valeur par défaut):

#content
{
position:absolute;
left: 218px;
top: 92px;
}

Sera remplacé par :

#content
{
position:static;
margin-left: 218px;
margin-top: 92px;
}

C'est ce qui a été fait pour la page présente. Ainsi les propriétés top et left sont assignées correctement pour le menu.