Version en ligne

Tutoriel : Les déplacements de la souris

Table des matières

Les déplacements de la souris
Les coordonnées de la souris
La variable event
Les coordonnées
Position absolue
Fil rouge : gestion des fenêtres
Les coordonnées des blocs
Centrer
Coordonnées
Fil rouge : décalage initial
Les cibles
La propagation des évènements
Récupérer la cible
Avec des classes
Fil rouge : sans ajout d'évènement
Finalisation du script de fenêtres
Une histoire d'ordre
Le vrai style CSS
Maximiser
Cloisonnement
Une liste ordonnée originale !
Code de base
Ajout du script
Le li fantôme
Comptage et camouflage

Les déplacements de la souris

Bonjour, bonjour. :)

Vous avez sûrement déjà vu ces sites web où certaines icônes ou fenêtres sont déplaçables à la souris, ou alors où un texte suit le curseur. Vous vous êtes peut-être demandé comment réaliser ce genre d'effet. Avec du Javascript oui, mais encore ? Le sujet semble parfois complexe car tous les navigateurs ne sont pas compatibles.

Mais ne fuyez pas ! Attendez ! Je vais expliquer (du moins, essayer) clairement comment déplacer des éléments dans une page web grâce aux déplacements de la souris et j'espère bien vous voir devenir des pros de cette technique qu'on appelle parfois le "drag & drop". :soleil:

Vous devriez être déjà assez à l'aise avec le HTML, le Javascript et le DOM pour bien comprendre ce tutoriel. ;)

Les coordonnées de la souris

La variable event

Si vous avez déjà eu l'occasion de travailler dans un repère en maths (nan! ne partez pas !), vous savez sûrement déjà ce que sont les coordonnées.
Les coordonnées sont composées de deux nombres ici :

Image utilisateurVoir un exemple animé

Pour connaître la position de la souris sur l'écran, on va chercher à récupérer ces deux distances. :)

La variable event

Les coordonnées de la souris Les coordonnées

La variable event

La position de la souris se récupère via une variable particulière qui s'appelle event. Mais n'essayez pas d'utiliser cette variable directement : elle n'existe pas d'habitude. Pour l'utiliser, il faut déclencher un évènement.
Tous les évènements qui ont un rapport avec la souris marchent :
onclick, ondbclick, onmousemove, onmousedown, onmouseup, etc.

La variable event indiquera donc la position de la souris au moment du onclick au moment où cet évènement est déclenché (donc à chaque clic, cela va créer une nouvelle variable event).

Trois cas se présentent suivant la façon dont vous associez des évènements aux objets HTML de votre page.

Cas n°1 : assigner un évènement en HTML

Si pour ajouter un évènement à votre page, vous l'écriviez dans le code HTML :

<script type="text/javascript">
function onclick_page()
{
  
}
</script><input type="button" id="bouton_click" onclick="onclick_page();" value="Click!" />

vous avez juste à rajouter cette nouvelle variable :

<script type="text/javascript">
function onclick_page(event)
{
  //ici, on va se servir de la variable event ^^
  alert(event);
}
</script>
<input type="button" id="bouton_click" onclick="onclick_page(event);" value="Click!" />
Cas n°2 : assigner un évènement dynamiquement avec la méthode DOM-0

Le DOM-0 permet d'ajouter des évènements en modifiant une variable :

document.getElementById('bouton_click').onclick = onclick_page;

Il va falloir modifier légèrement le début de la fonction. La plupart des navigateurs transmettent bien une variable event à la fonction, sauf Internet Explorer où la variable event est globale. Pour concilier tous les navigateurs, il va falloir préciser tous les cas possibles :

function onclick_page(event)
{
  if( window.event)
    event = window.event;
  //On peut aussi écrire: event = event || window.event;
  //ici, on va se servir de la variable event ^^
  alert(event);
}
Cas n°3 : assigner un évènement dynamiquement avec addEventListener (DOM-2)

Si vous utilisez la fonction addEventListener (et attachEvent pour Internet Explorer), il n'y a pas grand-chose à modifier :

function onclick_page(event)
{
  //ici, on va se servir de la variable event ^^
  alert(event);
}
var objet = document.getElementById('bouton_click');
if( objet.addEventListener)
  objet.addEventListener('click',onclick_page,true);
else  //pour ie
  objet.attachEvent('onclick',onclick_page);

Personnellement, j'ai écrit une petite fonction pour ne pas avoir à toujours taper cette condition :

function addEvent(obj,event,fct)
{
        if( obj.attachEvent)
                obj.attachEvent('on' + event,fct);
        else
                obj.addEventListener(event,fct,true);
}

Comme ça, on pourra utiliser cette fonction à chaque fois que l'on veut rajouter un évènement, proprement. :)

addEvent(document.getElementById('bouton_click'),'click',onclick_page);

Les coordonnées de la souris Les coordonnées

Les coordonnées

La variable event Position absolue

Les coordonnées

Notre but principal est de récupérer les coordonnées de la souris ; pour cela, on a récupéré la variable event et celle-ci possède les propriétés qui nous intéressent : clientX et clientY.

Aller, un premier exemple avec onclick :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>test coordonnées</title>
<script type="text/javascript">function onclick_page(event)
{
  var x = event.clientX;
  var y = event.clientY;
  alert('Vous avez cliqué au point de coordonnés: ' + x + ', ' + y );
}</script>

</head>
<body onclick="onclick_page(event);">

</body>
</html>

Tester ce code

Exercice

Au lieu d'ouvrir une boîte de dialogue (alert), faites en sorte que le message apparaisse dans une zone de texte dans la page web. Ensuite, remplacez l'évènement onclick par un évènement onmousemove. Les coordonnées vont être mises à jour à chaque déplacement de la souris !

Correction

Si vous avez besoin d'un moyen pour connaître l'état d'une variable (pour déboguer), il faudra utiliser des fonctions comme console.debug (pour firebug).

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>test coordonnées</title>
<script type="text/javascript">
function souris(event)
{
  var x = event.clientX;
  var y = event.clientY;
  document.getElementById('coordonnes').value = x + ', ' + y;
}
</script>

</head>
<body onmousemove="souris(event);">
  <div id="info">
    Coordonnées de la souris : <input type="text" id="coordonnes" value="inconnues" readonly="readonly" />
  </div>
</body>
</html>

Tester ce code


La variable event Position absolue

Position absolue

Les coordonnées Fil rouge : gestion des fenêtres

Position absolue

On peut placer une balise HTML exactement là où on le souhaite grâce au style CSS et à l'utilisation des positions absolues. Il suffit alors d'indiquer à l'aide des propriétés left et top l'abscisse et l'ordonnée de l'élément.
Par exemple, pour placer le div qui porte l'id info (dans l'exemple précédent), à 30 pixels du bord gauche et à 100 pixels en dessous de la page, on ferait :

#info
{
  position: absolute;
  left: 30px;
  top: 100px;
}

Oui, oui, mais on va faire ce placement en Javascript grâce au DOM !

Petits rappels

Modifiez le dernier exemple pour qu'en plus d'afficher les coordonnées de la souris, il déplace le <div id="info"> au même endroit que la souris.

Correction

Bon, avec tous les indices que je vous ai donnés, ça n'a pas dû être très dur. :p

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>test coordonnées</title>
<style type="text/css">
#info
{
        background-color: #DDD;
        border: 1px solid #000;
}
</style>
<script type="text/javascript">
function souris(event)
{
  var x = event.clientX;
  var y = event.clientY;
  document.getElementById('coordonnes').value = x + ', ' + y;
  //Nouveau code :
  var element = document.getElementById('info');
  element.style.position = 'absolute';
  element.style.left = x + 'px';
  element.style.top = y + 'px';
}
</script>

</head>
<body onmousemove="souris(event);">
        <div id="info">
                Coordonnées de la souris : <input type="text" id="coordonnes" value="inconnues" readonly="readonly" />
        </div>
</body>
</html>

Tester : le bloc suit la souris


Les coordonnées Fil rouge : gestion des fenêtres

Fil rouge : gestion des fenêtres

Position absolue Les coordonnées des blocs

Fil rouge : gestion des fenêtres

On va commencer notre script qui imite des fenêtres. Il nous faut déjà un bloc (div) jouant le rôle de fenêtre déplaçable ainsi qu'un autre bloc pour la barre de titre. Enfin, un petit bouton pour minimiser pourrait également servir.

On obtient donc un code HTML de ce genre :

<div id="fenetre">
  <div class="barre" onmousedown="start_drag(document.getElementById('fenetre'), event);">
    <button class="close" onclick="document.getElementById('fenetre').parentNode.removeChild(document.getElementById('fenetre'));">X</button>
    Titre
  </div>
  Contenu de la page !!
</div>

J'ai déjà rajouté un évènement onmousedown sur la barre de titre, qui va appeler notre fonction et déclencher le début du déplacement.

On stockera l'élément en cours de déplacement dans une variable : dragged (c'est moi qui ai décidé de l'appeler comme ça :p ) et on va avoir besoin de deux autres évènements :

Je vous laisse essayer un peu avant de vous montrer comment j'ai fait (il y a sûrement plusieurs méthodes).

Correction

Ci-dessous le style CSS ajouté ; sûrement une question de goût et le mien est assez mauvais.
Voilà mon résultat (avec les artistes que vous êtes, je suis sûr que vous avez fait mieux :p ) :

Image utilisateur
#fenetre
{
        position: absolute;
        width: 400px;
        height: 200px;
        background-color: #DDD;
        border: 1px solid #000;
}

.close
{
        float: right;
        background-color: #EEE;
}

.barre
{
        width: 100%;
        height: 30px;
        background-color: #00D;
        color: #FFF;
        cursor: hand;
}

Pour le code Javascript, j'ai décidé de commencer à rajouter les évènements (onmousemove et onmousedown) dynamiquement sans avoir à toucher au code HTML.
Pour "capter" le maximum de déplacement, on va fixer ces évènements sur l'élément HTML le plus haut possible : document.

var dragged = null; //L'élément en cours de drag
//Lorsque dragged = null, il n'y a rien en cours de déplacement


function start_drag(objet,event)
{
  dragged = objet; //On le place comme objet en cours
}

function drag_onmousemove(event)  //Lorsque la souris se déplace
{
  if( dragged ) //s'il n'y a pas d'élément en cours de déplacement, inutile de le déplacer :) 
  {
    var x = event.clientX;
    var y = event.clientY;
    dragged.style.position = 'absolute';
    dragged.style.left = x + 'px';
    dragged.style.top = y + 'px';
  }
}

function drag_onmouseup(event)  //Lorsque le bouton de la souris est relâché
{
  dragged = null; //On arrête le drag & drop
}

//Ma petite fonction "magique" pour ajouter des évènements
function addEvent(obj,event,fct)
{
  if( obj.attachEvent)
     obj.attachEvent('on' + event,fct);
  else
     obj.addEventListener(event,fct,true);
}

//On ajoute les deux évènements avec la méthode
addEvent(document,'mousemove',drag_onmousemove);
addEvent(document,'mouseup',drag_onmouseup);

Tester la première version du script de fenêtres

Comportement par défaut

Un bug assez gênant apparaît pendant le déplacement : le texte est sélectionné, et c'est assez moche comme effet. :-°
De la part du navigateur, c'est assez logique : quand on enfonce un bouton de la souris et qu'on la déplace, c'est pour sélectionner du texte, c'est ce qu'il doit se passer par défaut.
Ici, ce n'est pas ce qu'on souhaite faire et il faut donc annuler ce comportement par défaut :

//des lignes à rajouter dans start_drag:
event.returnValue = false; //Pour Internet Explorer
if( event.preventDefault ) event.preventDefault();

(Cela ressemble beaucoup à return false; que l'on voit souvent.)

Prendre en compte le scroll

Si vous avez eu l'occasion de tester ce code et que vous avez scrollé (bougé l'ascenseur, vertical ou horizontal), vous avez sûrement remarqué un décalage . Il faut donc rajouter à x et y la position actuelle des ascenseurs (horizontaux et verticaux):

var x = event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft);
var y = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop);

Et voilà, on sait déplacer un bloc en fonction des mouvements de la souris mais ...

Oui mais... la souris reste toujours accrochée au même endroit, c'est nul !! :(

Il faut garder le même décalage entre la souris et le bloc tout au long du déplacement !


Position absolue Les coordonnées des blocs

Les coordonnées des blocs

Fil rouge : gestion des fenêtres Centrer

On a vu comment connaître la position de la souris mais non la position (initiale) d'un élément.
Dans la plupart des cas, il faut effectuer un décalage vers la gauche et le haut, donc enlever (soustraire) aux coordonnées de la souris une certaine valeur.

On peut procéder par tâtonnement. Par exemple pour décaler de 100 et 60 pixels :

element.style.left = x - 100 + 'px';
element.style.top = y - 60 + 'px';

Mais ça serait mieux avec des valeurs exactes.

Image utilisateur

À la fin de ce tutoriel, on saura comment centrer un bloc au-dessus de la souris et comment conserver le décalage initial entre la souris et nos "fenêtres".

Centrer

Les coordonnées des blocs Coordonnées

Centrer

On veut centrer le bloc autour de la souris lors du déplacement.

Il faut donc connaître la largeur et hauteur du bloc pour les diviser par 2 et obtenir le décalage permettant de centrer le bloc.

Citation

Cool, et bah on a juste à utiliser style.width et style.heigth. :-°

Non! J'ai une meilleure méthode à vous proposer !

Premièrement, si on s'appuie sur le style CSS, on n'est pas sûr qu'il s'agisse de la bonne largeur (elle peut ne pas être définie ou le texte peut déborder). Il vaut mieux utiliser les propriétés offsetWidth et offsetHeight.

Je vous laisse modifier le code de notre script de fenêtre pour que le bloc reste centré :

x -= dragged.offsetWidth / 2; //On décale vers la gauche, donc -
y -= dragged.offsetHeight / 2;

Tester cet exemple : la souris reste au centre de la fenêtre


Les coordonnées des blocs Coordonnées

Coordonnées

Centrer Fil rouge : décalage initial

Coordonnées

Comment connaître la position initiale d'un bloc ? On pourrait utiliser .style.top et .style.left mais si je vous en parle, c'est qu'il y a forcément mieux. :-°

On va utiliser offsetLeft et offsetTop qui nous donnent les coordonnées x et y d'un élément HTML par rapport à son conteneur (offsetParent).
Là où ça se complique, c'est que le conteneur lui-même possède des propriétés offsetLeft et offsetTop par rapport à un autre conteneur ! :p

Image utilisateur

Il faut donc tout additionner en remontant de conteneur en conteneur jusqu'à arriver à la page entière elle-même.

var elementX = 0;
var elementY = 0;
var element = objet;
while(element)
{
  //On rajoute des coordonnées
  elementX += element.offsetLeft;
  elementY += element.offsetTop;
  element = element.offsetParent;
}

En réalité, on va s'arrêter dans la boucle dès que l'on trouve un premier élément en position absolue.

do //On utilise un do-while au lieu d'un while car la cible elle-même est en position absolue (vu qu'on va la déplacer)
{
  //On rajoute des coordonnées
  elementX += element.offsetLeft;
  elementY += element.offsetTop;
  element = element.offsetParent;
}while(element && element.style.position != 'absolute');

Centrer Fil rouge : décalage initial

Fil rouge : décalage initial

Coordonnées Les cibles

Fil rouge : décalage initial

Maintenant que l'on possède les coordonnées, on peut connaître le décalage initial qui existe entre la souris et le bloc lui-même. Décalage qu'il va falloir conserver au cours du déplacement :

Image utilisateur

Pas besoin de plus d'explications je pense, vous pouvez essayer par vous-mêmes (ou... si vous êtes paresseux, regarder directement comment faire :-° ).

Correction

Ce décalage, on le calcule au départ (fonction start_drag) et on le stocke dans deux variables (dX et dY).
On le réapplique ensuite lors du déplacement.

//Partie du code ayant changé
var dX, dY;

function start_drag(objet,event)
{
  dragged = objet;

  event.returnValue = false;
  if( event.preventDefault ) event.preventDefault();
        
  //Coordonnées de la souris
  var x = event.clientX + (document.documentElement.scrollLeft + document.body.scrollLeft);
  var y = event.clientY + (document.documentElement.scrollTop + document.body.scrollTop);
        

  //Coordonnées de l'élément
  var eX = 0;
  var eY = 0;
  var element = objet;
  do
  {
    eX += element.offsetLeft;
    eY += element.offsetTop;
    element = element.offsetParent;
  } while( element && element.style.position != 'absolute');

  //Calcul du décallage
  dX = x - eX;
  dY = y - eY;

}

function drag_onmousemove(event) 
{
  if( dragged ) 
  {
    var x = event.clientX + (document.documentElement.scrollLeft + document.body.scrollLeft);
    var y = event.clientY + (document.documentElement.scrollTop + document.body.scrollTop);
                
    //On applique le décalage
    x -= dX;
    y -= dY;

    dragged.style.position = 'absolute';
    dragged.style.left = x + 'px';
    dragged.style.top = y + 'px';
  }
}

Notre petite fenêtre en cours de déplacement :magicien:

On sait récupérer les coordonnées de la souris, des éléments, leur décalage. À partir de là, on peut imaginer plein d'applications. :) Il faut surtout un peu d'imagination et... parfois un peu de mathématiques !
Mais comme vous avez pu le voir, la plupart du temps, c'est surtout des additions, soustractions, multiplications et divisions. :-°


Coordonnées Les cibles

Les cibles

Fil rouge : décalage initial La propagation des évènements

Dans tous nos scripts précédents, on était obligés de mettre des évènements onmousedown partout, sur chaque élément que l'on voulait déplacer. En règle générale, il est normalement déconseillé de mélanger le Javascript avec le code HTML à l'intérieur d'une page, car il faut séparer le contenu (HTML), de la présentation (CSS) et du comportement (Javascript).

Alors, au chargement de la page, on pourrait assigner dynamiquement chaque évènement sur chaque bloc :

function rendre_deplacable(objet)
{
  addEvent(objet,'mousedown',function (event) {
    start_drag(objet, event);
  });
}

window.onload = function ()
{
  var elements = document.getElementsByTagName('div'); //on va rendre déplaçables toutes les div
  for( var i = 0; i < elements.length; i++)
  {
    rendre_deplacable(elements.item(i));
  }
}

Mais ensuite, si on rajoute de nouveaux div dans notre page (dynamiquement), ils ne seront pas déplaçables ?

Eh non. :( À chaque nouveau div, il faudra rajouter l'évènement Javascript qui va avec. On pourrait utiliser la fonction rendre_deplacable() mais on va apprendre ici une nouvelle méthode.

La propagation des évènements

Les cibles Récupérer la cible

La propagation des évènements

Essayons d'imbriquer deux éléments qui réagissent tous deux au même évènement :

<div onmousedown="start_drag(this,event);" style="width: 500px; height: 500px; background-color: green;">
  <div onmousedown="start_drag(this,event);" style="width: 200px; height: 200px; background-color: yellow;">
  </div>
</div>

Tester cet exemple qui contient un bogue

On a placé un évènement onmousedown sur le div jaune, pourtant, il ne se déplace jamais ! C'est toujours le vert qui bouge !?

C'est dû à la propagation des évènements. Lorsqu'on clique sur le div jaune, on clique également sur le div vert conteneur. La fonction start_drag est donc immédiatement appelée pour le div jaune puis pour le vert.

Image utilisateur

La fonctiononmousedown du bloc jaune est d'abord lue, puis celle du div vert, puis le onmousedown présent sur la page entière.
Pour éviter, cette propagation, appelée "bubbling" (bouillonnement) car l'évènement se propage comme une bulle, il faut rajouter une ligne à la fin de start_drag :

event.cancelBubble = true;

Et voilà l'exemple précédent fonctionnel


Les cibles Récupérer la cible

Récupérer la cible

La propagation des évènements Avec des classes

Récupérer la cible

On a vu que les évènements se propagent jusqu'à arriver sur l'objet document. Soyons fainéants (les programmeurs sont des fainéants :p ) et attendons qu'il arrive jusqu'au document plutôt que de placer des onmousedown partout.

C'est là qu'il falloir utiliser une autre propriété de l'objet event :) Sur la plupart des navigateurs, on utilisera event.target
alors que sur Internet Explorer, il faudra utiliser event.srcElement.

Pour un code qui marche partout, on va donc créer une variable universelle :

var target = event.target || event.srcElement

Grâce à elle, on peut savoir sur quel élément précis on a cliqué (ou bougé) :

document.onclick = function (event)
{
  event = event || window.event;
  var target = event.target || event.srcElement;
  
  var type;
  if(target.nodeType == 1)
    type = 'Tag: ' + target.tagName;
  else
    type = target;
   alert('Vous avez cliqué sur ' + type);
};

Ce script permet de savoir sur quelle balise on a cliqué

Grâce à cela, on peut lancer directement un déplacement de bloc, sans avoir à ajouter un évènement sur le bloc en question :

addEvent(document,'mousedown',function (event)
{
  var target = event.target || event.srcElement;
  start_drag(target, event);
});

Ce code démarre un déplacement de la balise HTML situé juste en dessous de la souris

Ce n'est pas exactement ce qu'on veut faire mais on va voir comment corriger ce bogue. ;)


La propagation des évènements Avec des classes

Avec des classes

Récupérer la cible Fil rouge : sans ajout d'évènement

Avec des classes

Il va donc falloir trouver un moyen d'identifier les objets HTML que l'on a le droit de bouger et ceux qui sont fixes. Je vous propose donc d'utiliser les classes pour identifier ce qui est déplaçable.

On va placer une classe sur les éléments de la page qui ont le droit d'être déplacés. Par exemple, on va mettre la classe deplacable.
Lorsqu'on clique sur un élément, on clique aussi sur son parent. Il va falloir tester l'existence de la classe sur tous les parents de la cible jusqu'à en trouver un qui possède la classe que l'on désire ou si on n'en trouve aucun, ne rien faire :

addEvent(document,'mousedown',function (event)
{
  var target = event.target || event.srcElement;
  
  var element = target;
  while(element)
  {
    if( element.className && element.className == 'deplacable')
    {
      start_drag(element, event);
      element = false; //On stoppe la boucle : sinon, on va se retrouver avec le même problème que le bubbling car le déplacement va aussi se déclencher pour les parents 
    }
    else
      element = element.parentNode; //Sinon, on continue à remonter dans les ancêtres de la cible
  }

});

Tester : pensez aussi à rajouter class="deplacable" sur le div de la fenêtre.

Plusieurs classes

Et si on profitait du fait qu'il est possible de mettre plusieurs classes sur un même tag HTML ? Il suffit de séparer par un espace :

<div class="classe1 classe2 classe3"></div>

Avec une regex, on peut savoir si tel ou tel objet possède telle classe :

if( bloc.className && bloc.className.match(/\bclasse1\b/g) )
{
  alert( 'Ce bloc possède bien la classe "classe1" !');
}

(Les \b sont des assertions pour indiquer un espace entre deux mots. ;) )
Comme on peut mettre plusieurs classes, on peut indiquer d'un côté le comportement Javascript de l'élément, et d'un autre côté l'apparence qu'il aura grâce au style CSS.


Récupérer la cible Fil rouge : sans ajout d'évènement

Fil rouge : sans ajout d'évènement

Avec des classes Finalisation du script de fenêtres

Fil rouge : sans ajout d'évènement

Nous allons modifier notre script de "fausses fenêtres" pour le rendre plus universel, nous n'aurons plus besoin de taper une seule ligne de code Javascript dans notre code HTML. Juste rajouter des classes pour identifier chaque élément de la fenêtre.

Je vous propose cette liste de classes :

(On pourrait par exemple faire un système de barre des tâches, mais honnêtement, je suis trop paresseux ; je n'ai donc pas proposé le bouton "réduire" :p .)

Correction

On possède déjà une fonction start_drag mais il nous faut aussi des fonctions pour fermer et maximiser les fenêtres.

function min_max(fenetre)
{
  if( fenetre.style.width != '100%' && fenetre.style.height != '100%') //si la fenêtre n'est pas déjà maximisée
  {
     fenetre.style.width = '100%'; //maximum largeur et hauteur
     fenetre.style.height = '100%';
     fenetre.style.position = 'absolute';
     fenetre.style.left = 0;  //à partir du coin en haut à gauche
     fenetre.style.top = 0;
  }
  else
  {
    fenetre.style.width = '';
    fenetre.style.height = '';
  }
}

function close(fenetre)
{
  fenetre.parentNode.removeChild(fenetre);  //On peut enlever le bloc du document
  //ou: fenetre.style.display = 'none';     //ou alors le cacher avec du style CSS
}

Une fois ces fonctions créées, il faut rajouter un évènement onmousedown sur la page entière pour tout récupérer. On récupère dans un premier temps la fenêtre mais aussi les boutons pour savoir quelle action on doit faire.

function drag_onmousedown (event)
{
  var target = event.target || event.srcElement;
  
  //On commence par trouver la fenêtre elle-même
  var fenetre = target;
  while( fenetre)
  {
    if( fenetre.className && fenetre.className.match(/bwindow-baseb/g))
    {
       break; //On arrête la boucle
    }
    fenetre = fenetre.parentNode;
  }
  if( !fenetre) //Si on est sortis de la boucle mais qu'on n'a trouvé aucune fenêtre, on abandonne
    return;

  //Maintenant, on part à la recherche d'un bouton déclencheur
  var element = target;
  while(element)
  {
    if( element.className)
    {
      if( element.className.match(/\bwindow-close\b/g))
      {
        close(fenetre);
        break;
      }
      else if( element.className.match(/\bwindow-min-max\b/g) )
      {
        min_max(fenetre);
        break;
      }
      else if( element.className.match(/\bwindow-move\b/g) )
      {
        start_drag(fenetre, event);
        break;
      }
    }
    element = element.parentNode;
  }
}
addEvent(document,'mousedown',drag_onmousedown);

J'ai légèrement modifié le code de la feuille de style pour ne plus faire référence à des id mais uniquement à des classes. On peut alors créer plusieurs fenêtres avec un simple copier-coller sans avoir de problèmes au niveau des id qui doivent être uniques.

En assignant un seul évènement Javascript : onmousedown sur la page entière, on a pu créer un système qui déplace juste certains blocs.
Il n'y a plus de code Javascript parasite à l'intérieur du code HTML, les attributs classes permettent de dire comment va se comporter tel ou tel élément.

Notre système sépare le contenu de la présentation et du comportement. :)

La première partie du tutoriel est terminée. On sait déplacer un objet, pour qu'il suive la souris. On a vu comment obtenir un décalage et aussi comment connaître l'élément qui est survolé par la souris.

Pour faire des scripts plus évolués, c'est surtout une histoire d'innovation (ou plutôt d'imagination ? :p ).


Avec des classes Finalisation du script de fenêtres

Finalisation du script de fenêtres

Fil rouge : sans ajout d'évènement Une histoire d'ordre

Ce dernier chapitre sur la gestion des fenêtres n'est pas essentiel pour la suite. Nous allons juste faire quelques petites améliorations pour avoir un script terminé totalement.
Passez votre chemin si vous n'êtes pas intéressés par le système de fenêtres ou si vous voulez juste vous servir du script sans rien comprendre, lisez la conclusion. :p

Une histoire d'ordre

Finalisation du script de fenêtres Le vrai style CSS

Une histoire d'ordre

On peut créer toute une armée de fenêtres uniquement avec les classes et les styles CSS. Mais il se pose un dernier problème (si vous avez testé le dernier exemple) : la fenêtre en cours de déplacement ne passe pas au-dessus des autres.

Pour régler l'ordre de superposition des blocs, on utilise la propriété CSS z-index. Plus z-index est grand, plus il est placé devant les autres blocs. Pour faire revenir un bloc au premier plan, il faut connaître le z-index maximum sur la page pour placer notre nouveau bloc au-dessus des autres.

On va donc faire une boucle pour récupérer le plus grand z-index et en trouver un encore plus grand :

//à rajouter dans drag_onmousedown
var elements = document.getElementsByTagName('*'); //On récupère tous les éléments de la page
var zIndex = 0;
for( var i=0; i < elements.length; i++)
{
  zIndex = Math.max(zIndex,elements[i].style.zIndex);
}
fenetre.style.zIndex = zIndex + 1; //toujours plus haut que le plus haut

Tester : les blocs passent les uns au-dessus des autres


Finalisation du script de fenêtres Le vrai style CSS

Le vrai style CSS

Une histoire d'ordre Maximiser

Le vrai style CSS

Je vous avais dit que les styles CSS left et top indiquent la position horizontale et verticale du bloc par rapport au premier ancêtre en position absolue. C'est pour cela qu'on avait mis la condition :

element.style.position != 'absolute'

Seulement, cette condition est fausse car la propriété style ne prend pas en compte les feuilles de style externes.
Voilà un exemple :

<style type="text/css">
#bloc
{
  position: absolute;
}</style>
<!-- ... -->
<div id="bloc">contenu</div>
<button onclick="alert(document.getElementById('bloc').style.position);">Tester</button>

Le alert va afficher une chaîne vide "", car position a été indiqué dans la feuille de style et non pas dans l'attribut HTML style.

Il nous faut donc récupérer la véritable valeur du style (ici, la position), et je vous propose cette fonction (sans vraiment vous expliquer comment elle fonctionne :p ) :

//utilisation : au lieu d'écrire element.style.position, on écrira getCssStyleValue(element,"position");
function getCssStyleValue(element /*element html*/, style/*style recherché*/)
{
  if( element.currentStyle )
  {
    return element.currentStyle[style];
  }
  else
  {
    return window.getComputedStyle(element,null).getPropertyValue(style);
  }
}

Notre boucle qui calcule le décalage horizontal (variable eX) et vertical (variable eY) devient donc :

//Coordonnées de l'élément
  var eX = 0;
  var eY = 0;
  var element = objet;
  do
  {
    eX += element.offsetLeft;
    eY += element.offsetTop;
    element = element.offsetParent;
  } while( element && getCssStyleValue(element, 'position') != 'absolute'); //<--

Pensez aussi à copier-coller la fonction getCssStyleValue dans votre code source ;) .


Une histoire d'ordre Maximiser

Maximiser

Le vrai style CSS Cloisonnement

Maximiser

Le script pour maximiser / minimiser était assez simplet, en voilà une version améliorée qui conserve les anciennes positions et dimensions initiales (avant maximisation) :

(en fait, il n'y a pas grand-chose à expliquer, le code parle de lui-même)

//Nouvelle fonction min_max
function min_max(fenetre)
{
  if( ! fenetre.max )
  {
    fenetre.max = true;
    fenetre.oldTop = fenetre.style.top;
    fenetre.oldLeft = fenetre.style.left;
    fenetre.oldWidth = fenetre.style.width;
    fenetre.oldHeight = fenetre.style.height;
     
    fenetre.style.top = 0;
    fenetre.style.left = 0;
    fenetre.style.width = '100%';
    fenetre.style.height = '100%';
  }
  else
  {
    fenetre.max = false;
    fenetre.style.top = fenetre.oldTop;
    fenetre.style.left = fenetre.oldLeft;
    fenetre.style.width = fenetre.oldWidth;
    fenetre.style.height = fenetre.oldHeight;
  }
}
 
//À modifier aussi, pour empêcher le déplacement lorsque maximisé :
//Au début de start_drag():
function start_drag(objet,event)
{
  if( objet.max) return; //on quitte la fonction
 
  dragged = objet;
  //[...]

Tester :)


Le vrai style CSS Cloisonnement

Cloisonnement

Maximiser Une liste ordonnée originale !

Cloisonnement

Pour éviter les conflits entre plusieurs scripts, on a utilisé addEventLister (et attachEvent). Les variables et les fonctions que l'on utilise ne sont pas utilisées (de l'extérieur) car on se base uniquement sur les classes ; il n'est donc pas utile de rendre les variables globales et on évitera ainsi des problèmes de compatibilité avec d'autres scripts.

Les variables (et fonctions) déclarées à l'intérieur d'une fonction sont inaccessibles de l'extérieur : c'est le principe des cloisons en Javascript. On va créer une fonction anonyme qui va contenir tout notre code. Et cette fonction anonyme sera lue immédiatement :

(function () { //Début du cloisonnement
  
  //Code invisible de l'extérieur
 
})(); //Fin du cloisonnement

Plus d'informations sur Alsacreations.

Notre script est fini ! :)
On aurait pu aussi gérer le redimensionnement mais c'est assez répétitif à coder (droite, gauche, haut, bas, diagonales...) et pas très dur, alors je vous laisse le faire. :p

Récapitulons le fonctionnement de ce script.
Il fonctionne sur la base des classes (attribut class en HTML) que l'on place sur chaque balise HTML.
Il y a au total 4 classes :

Ce qui donne par exemple le code HTML suivant :

<div class="window-base">
  <div class="window-move" >
    <button class="window-close">X</button>
    <button class="window-min-max">[]</button>
    Barre de titre
  </div>
  Contenu
</div>

Avec le code CSS suivant :

.window-base
{
        position: absolute;
        width: 400px;
        height: 200px;
        background-color: #DDD;
        border: 1px solid #000;
}
 
.window-close , .window-min-max
{
        float: right;
        background-color: #EEE;
}
 
.window-move
{
        width: 100%;
        height: 30px;
        background-color: #00D;
        color: #FFF;
        cursor: move;
}

Pour utiliser le script dans la page, il suffit de l'insérer (dans la partie <head>) avec le code HTML :

<script type="text/javascript" src="fenetres.js"></script>

Et voilà le fichier complet.


Maximiser Une liste ordonnée originale !

Une liste ordonnée originale !

Cloisonnement Code de base

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

Une liste ordonnée originale ! Ajout du script

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.


Une liste ordonnée originale ! Ajout du script

Ajout du script

Code de base Le li fantôme

Ajout du script

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

Et des variables :

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 :

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);

Code de base Le li fantôme

Le li fantôme

Ajout du script Comptage et camouflage

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 :

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).


Ajout du script Comptage et camouflage

Comptage et camouflage

Le li fantôme

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

Le li fantôme