• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 08/01/2013

Une liste ordonnée originale !

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Un élément souvent bien pratique dans les formulaires manque à la panoplie des balises (x)html : les listes ordonnées. Par exemple, pour choisir l'ordre dans lequel des menus apparaissent, ou alors établir une liste des préférences ou des priorités d'un visiteur.

Des solutions en html pur, sans javascript, existent, mais ne sont pas toujours très pratiques.

Ainsi, notre formulaire sera accessible, avec ou sans javascript :) !

Code de base

Sans javascript et juste avec du html, on pourrait bricoler un formulaire de ce genre :

<ol class="liste">
  <li>
    <input class="ordre" type="text" id="accueil" name="accueil" value="1" />
    <label for="accueil">Accueil</label>
  </li>
  <li>
    <input class="ordre" type="text" id="forum" name="forum" value="2" />
    <label for="forum">Forum</label>
  </li>
  <li>
    <input class="ordre" type="text" id="livredor" name="livredor" value="3" />
    <label for="livredor">Livre d'or</label>
  </li>
  <li>
    <input class="ordre" type="text" id="options" name="options" value="4" />
    <label for="options">Mes options</label>
  </li>
  <li>
    <input class="ordre" type="text" id="faq" name="faq" value="5" />
    <label for="faq">Faq</label>
  </li>
</ol>

(la classe "ordre" est à placer sur l'input qui indique la position dans la liste)

On va aussi décorer un peu cette liste avec du code css :

ol.liste
{
  width: 400px;
  border: 1px solid #111;
  background-color: #CCF;
  padding: 0; margin: 0;
  list-style: none;
}

ol.liste li
{
  margin: 10px 10px 10px 10px;
  border: 1px solid #222;
  background-color: #EEF;
  height: 30px;
}

ol.liste input.ordre
{
  width: 3em;
}

Cliquez ici pour voir le rendu, uniquement avec du code html.

Chaque input contient l'ordre d'affichage de chaque élément et l'utilisateur peut les modifier pour changer l'ordre.

Ce sera également au script serveur de réordonner ensuite la liste, de mettre à jour la base de données, mais ce n'est pas ici le but du tutoriel.

Ajout du script

Notre script va beaucoup ressembler à notre bon vieux code de gestions de "fausses fenêtres".
On aura besoin des fonctions :

  • onmousedown, chargée de chercher les classes concernées et commencer le déplacement.

  • onmousemove, pour déplacer le div en cours de déplacement.

  • onmouseup, qui va arrêter le déplacement.

Et des variables :

  • dX et dY, les décalages entre le haut du bloc en déplacement et la souris

  • dragged, l'objet en déplacement (objet li).

  • liste, la liste ordonnée (objet ol, className="liste") qui contient dragged.

Dans un premier temps, on se contentera d'enlever la position absolue du bloc en déplacement lors du relâchement de la souris. Cela aura pour effet de faire revenir le bloc à sa position initiale. Comme cette partie a un petit air de déjà-vu (le code ressemble fortement à celui du chapitre précédent :p ), je vous laisse essayer de faire ce script ;) .

Correction

La fonction de click de souris est un peu plus compliquée: il faut récupérer deux balises à partir de leur className. Mais finalement le code n'est pas méchant, quoique un peu long... :-°

//addEvent et getCssStyleValue sont définies comme dans les chapitres précédents

var dragged = null; //balise li en cours de déplacement
var liste = null; //balise ol en cours de modification

var dX, dY; //Décalages

function list_onmousedown(event)
{
	var target = event.target || event.srcElement;
	
	//S'il y a déjà un li en déplacement, on "simule" un évènement onmouseup en premier
	if( dragged ) list_onmouseup(event);

	//A la recherche d'une balise ol class="liste"
	var element = target;
	while(element)
	{
		if( element == null ) //si element = null, alors on n'a rien trouvé, on quitte cette fonction
			return;
		else if( element.className && element.className.match(/\bliste\b/) )
			break;
		element = element.parentNode;
	}
	liste = element;

	//Reste maintenant à trouver le "li" déplacé
	var element = target;
	while(element)
	{
		if( element == liste) //On est remonté jusqu'à la liste elle-même, cela signifie que l'on n'a pas cliqué sur une balise li
			return;
		else if ( element.tagName && element.tagName.toLowerCase() == 'li' )
			break;
		element = element.parentNode;
	}
	dragged = element;
	
	//On annule le comportement par défaut:
	event.returnValue = false;
	event.preventDefault && event.preventDefault();
	
	//On calcule les décalages
	dX = event.clientX + document.documentElement.scrollLeft + document.body.scrollLeft;
	dY = event.clientY + document.documentElement.scrollTop + document.body.scrollTop;
	var element = dragged;
	do
	{
		dX -= element.offsetLeft;
		dY -= element.offsetTop;
		element = element.offsetParent;
	} while( element && getCssStyleValue(element, 'position') != 'top');
	
	dragged.style.width = dragged.offsetWidth + 'px';
	dragged.style.height = dragged.offsetHeight + 'px';

	//On simule un premier déplacement
	list_onmousemove(event);
}

Tester ! Pas d'inquiétude, on va s'occuper de l'insertion !
Il y quand-même quelques remarques sur ce code :

  • Afin de corriger quelques petits bugs (par exemple, un bloc qui reste en position absolue), on simule le déplacement de la souris ou le relâchement du bouton en appelant nous-mêmes certaines fonctions lignes 12 et 58.

  • J'utilise tagName.toLowerCase() pour être sûr que le tag est comparable à "li" ( "LI" != "li") ligne 33.

  • Lorsqu'un élément est en position absolue, il perd sa largeur (voici le problème).
    Ce n'est pas très beau alors on fixe manuellement la largeur (et la hauteur, tant qu'on y est) lignes 54-55.

Pour les deux autres fonctions, c'est la routine :soleil: :

function list_onmousemove(event)
{
	if( dragged)
	{
		dragged.style.position = 'absolute';
		dragged.style.left = event.clientX + document.documentElement.scrollLeft + document.body.scrollLeft - dX + 'px';
		dragged.style.top = event.clientY + document.documentElement.scrollTop + document.body.scrollTop - dY + 'px';
	}
}

function list_onmouseup(event)
{
	if( dragged)
	{
		dragged.style.position = dragged.style.width = dragged.style.height = '';
		dragged = null;
	}
}
addEvent(document,'mousedown',list_onmousedown);
addEvent(document,'mousemove',list_onmousemove);
addEvent(document,'mouseup',list_onmouseup);

Le li fantôme

Image utilisateur

Pour ce genre de drag&drop, il est utile de savoir où le bloc en déplacement va atterrir.

Ce bloc "fantôme" va aussi nous servir pour placer facilement le li en déplacement au bon endroit à la fin du drag&drop (il suffira d'utiliser la méthode dom replaceChild). De façon totalement originale ^^ , j'ai nommé la variable correspondante ghost :

var ghost = document.createElement('li');
//Pour le différencier un peu des vraies "div"
ghost.style.backgroundColor = 'transparent';
ghost.style.borderStyle = 'dashed';

On pourrait aussi lui attribuer une classe spéciale (className="ghost" par exemple) et modifier la feuille de style.

Insertion initiale

Pour placer ce fantôme, on va utiliser la méthode dom insertBefore qui s'utilise comme suit :

parent.insertBefore(element_a_inserer, element_de_reference);

Il faut l'insérer dans la liste au moment du début du déplacement (onmousedown) :

liste.insertBefore(ghost, dragged); //On insère le fantôme juste avant le div que l'on déplace

Déplacement

Ensuite, à chaque déplacement de la souris (onmousemove), il va falloir déplacer de nouveau ce bloc fantôme, pour indiquer la position d'arrivée du drag&drop. On va encore utiliser insertBefore.

Mais avant quel bloc faut-il l'insérer ?

Pour le savoir, on va comparer le offsetTop appartenant au bloc aux autres offsetTop, et on s'arrêtera au premier qui est supérieur.

Exemple
Image utilisateur

On parcourt la liste de haut en bas :

  • Le premier bloc est le fantôme, on l'ignore.

  • Le bloc 2 est situé plus haut que le curseur, on passe.

  • Le bloc 3 est situé plus haut que le curseur, on passe (bis).

  • Le bloc 4 est situé plus bas, on s'arrête ici.

Tout cette succession se résume par une boucle, et une condition d'arrêt (plutôt longue !) :

var avant = null; //La balise que l'on va utiliser pour insertBefore()
//On va parcourir tous les enfants direct de notre liste
for( var i = 0; i < liste.childNodes.length; i++)
{
  var el = liste.childNodes.item(i);

  //Premièrement il faut s'assurer qu'il s'agit bien d'une balise (tagName) puis d'une balise li
  //Ensuite, on exclut de nos tests la balise ghost elle-même ainsi que la balise en cours de déplacement
  //Enfin, on vérifie les valeurs de offsetTop
  if( el.tagName.toLowerCase() == 'li' && el != dragged && el != ghost && el.offsetTop > dragged.offsetTop )
  {
    avant = el;
    break; //On a trouvé où insérer le fantôme, on arrête la boucle
  }
}

Et si l'élément qui se déplace est situé tout en bas, et qu'il n'y a rien après ? On ne peut pas trouver de div pour insérer avant ?

Dans ce cas, la variable avant vaut null et on utilisera la fonction appendChild à la place :

liste.removeChild(ghost); //Il faut déjà l'enlever, pour pouvoir le mettre ailleurs
if( avant == null )
  liste.appendChild(ghost);
else
  liste.insertBefore(ghost, avant);

Dernière optimisation : il serait bête d'enlever la balise fantôme pour la remettre au même endroit, ça va clignoter (beurk !). On peut utiliser la propriété ghost.nextSibling (next sibling = noeud/balise suivante) pour vérifier qu'elle n'est pas déjà au bon endroit :

if( avant != ghost.nextSibling )  //Si ghost est déjà bien placé
{
  liste.removeChild(ghost);
  if( avant == null )
    liste.appendChild(ghost);
  else
    liste.insertBefore(ghost, avant);
}

Dans le cas où avant = null, cela marche aussi car nextSibling vaut aussi null.

Remplacement final

On a passé le plus dur :)
Lorsque l'on relâche la souris (drag_onmouseup), il n'y a plus qu'à remplacer le bloc fantôme par le vrai bloc. Inutile de faire un dessin :p pour expliquer comment marche la fonction replaceChild ("remplacer nœud") :

liste.replaceChild(dragged, ghost);  //remplace ghost par dragged (attention à l'ordre des paramètres)

Tester le script avec les trois ajouts de code (onmousedown, onmousemove et onmouseup).

Comptage et camouflage

Les balises li ont été déplacées mais les champs input à l'intérieur n'ont pas été changés pour indiquer le nouvel ordre. Comme l'ordre d'affichage des éléments correspond (en position non absolue !) à l'ordre des éléments dans l'arbre html, il suffit de les compter dans l'ordre :

var inputs = liste.getElementsByTagName('input');
var n = 1; //Compteur
for(var i = 0; i < inputs.length; i++)
{
  if( inputs.item(i).className.match(/\bordre\b/) )
  {
    inputs.item(i).value = n++;
  }
}

Tester cette version avec décompte.
Ce code, qui doit s'exécuter après avoir remplacé le fantôme par le li déplacé, prend toutes les balises input, qui contiennent des informations d'ordre (class="ordre") et renumérote tout. C'est un peu bourrin, mais après tous les efforts que l'on a fourni, on peut bien se le permettre :-° .

Une fois que l'on a vérifié que ce script marche, les champs input deviennent inutiles (visuellement parlant). On va donc les cacher, avec javascript et cette nouvelle fonction pour rajouter des règles css dans la page :

//Fonction "magique" pour ajouter une règle css:
function insertCss(selector,rule)
{
  if( document.styleSheets && document.styleSheets[0] )
  {
    var feuille = document.styleSheets[0];
    if( feuille.insertRule )  //internet explorer
        feuille.insertRule(selector + " { " + rule + " } ", feuille.cssRules.length);
    else if( feuille.addRule )  //Pour firefox
        feuille.addRule(selector,rule);
  }
  else  //Pour le reste
  {
        var ss        =       document.createElement('style');
        ss.setAttribute('type','text/css');
        ss.appendChild(document.createTextNode(selector + " { " + rule + " } ") );
        document.getElementsByTagName('head')[0].appendChild(ss);
  }
}

Elle s'utilise ainsi :

insertCss('.ordre','display: none;');
//On en profite pour rajouter un curseur "spécial" pour le drag&drop
insertCss('.liste li', 'cursor: move;');

Tester le code final.

On peut remarquer que sans javascript, le formulaire fonctionnera toujours et c'est bien la force de ce système : on s'est basé sur un code html qui marchait, et on a rajouté du javascript autour sans toucher à la structure de la page ! Les styles css propres au javascript, pour cacher les input, ont été rajoutés par javascript. On ne risque ainsi pas de cacher des éléments nécessaires à l'utilisation de la page.

Si vous avez déjà utilisé des framework javascript qui réalisent ce genre de scripts, vous aurez remarqué qu'en général ces considérations sont mises de côtés : les pages webs nécessitent toujours javascript pour fonctionner.

Je ne blâme pas ces framework pour cela, car s'affranchir de ces contraintes permet de faire des pages "web 2.0" beaucoup plus puissantes et plus simplement surtout mais je tenais toutefois à montrer une autre méthode, plus artisanale :pirate: .

Idées d'amélioration
  • Faire un style css plus joli !

  • Rendre l'élément déplacé transparent.

  • Faire une petite animation, pour le déplacement d'un élément vers sa position finale.

  • Faire du drag&drop entre plusieurs listes.

  • Vérifier si l'on peut imbriquer les listes (après quelques petites corrections, ça fonctionne !).

Exemple de certificat de réussite
Exemple de certificat de réussite