11 juillet 2023

Déplacer la souris : mouseover/out, mouseenter/leave

Plongeont dans plus de détails sur les événements qui se produisent lorsque la souris se déplace entre les éléments.

Events mouseover/mouseout, relatedTarget

L’évènement mouseover est exécuté lorsqu’un pointeur de la souris survole un élément, et mouseout – lorsqu’il le quitte.

Ces événements sont spéciaux, car ils ont la propriété relatedTarget. Cette propriété complète target. Quand une souris quitte un élément pour un autre, l’un d’eux devient target, et l’autre – relatedTarget.

Pour mouseover:

  • event.target – l’élément que survole la souris.
  • event.relatedTarget – voici l’élément d’origine la souris (relatedTargettarget).

A l’inverse du mouseout:

  • event.target – est l’élément que la souris a quitté.
  • event.relatedTarget – est le nouvel élément situé sous le pointeur, celui pour lequel la souris a quitté (targetrelatedTarget).

Dans l’exemple ci-dessous chaque aspect facial est un élément. Lorsque vous déplacez la souris, vous pouvez voir les évènements de souris dans la zone de texte.

Chaque événement contient les informations sur target et relatedTarget :

Résultat
script.js
style.css
index.html
container.onmouseover = container.onmouseout = handler;

function handler(event) {

  function str(el) {
    if (!el) return "null"
    return el.className || el.tagName;
  }

  log.value += event.type + ':  ' +
    'target=' + str(event.target) +
    ',  relatedTarget=' + str(event.relatedTarget) + "\n";
  log.scrollTop = log.scrollHeight;

  if (event.type == 'mouseover') {
    event.target.style.background = 'pink'
  }
  if (event.type == 'mouseout') {
    event.target.style.background = ''
  }
}
body,
html {
  margin: 0;
  padding: 0;
}

#container {
  border: 1px solid brown;
  padding: 10px;
  width: 330px;
  margin-bottom: 5px;
  box-sizing: border-box;
}

#log {
  height: 120px;
  width: 350px;
  display: block;
  box-sizing: border-box;
}

[class^="smiley-"] {
  display: inline-block;
  width: 70px;
  height: 70px;
  border-radius: 50%;
  margin-right: 20px;
}

.smiley-green {
  background: #a9db7a;
  border: 5px solid #92c563;
  position: relative;
}

.smiley-green .left-eye {
  width: 18%;
  height: 18%;
  background: #84b458;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smiley-green .right-eye {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #84b458;
  top: 29%;
  right: 22%;
  float: right;
}

.smiley-green .smile {
  position: absolute;
  top: 67%;
  left: 16.5%;
  width: 70%;
  height: 20%;
  overflow: hidden;
}

.smiley-green .smile:after,
.smiley-green .smile:before {
  content: "";
  position: absolute;
  top: -50%;
  left: 0%;
  border-radius: 50%;
  background: #84b458;
  height: 100%;
  width: 97%;
}

.smiley-green .smile:after {
  background: #84b458;
  height: 80%;
  top: -40%;
  left: 0%;
}

.smiley-yellow {
  background: #eed16a;
  border: 5px solid #dbae51;
  position: relative;
}

.smiley-yellow .left-eye {
  width: 18%;
  height: 18%;
  background: #dba652;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smiley-yellow .right-eye {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #dba652;
  top: 29%;
  right: 22%;
  float: right;
}

.smiley-yellow .smile {
  position: absolute;
  top: 67%;
  left: 19%;
  width: 65%;
  height: 14%;
  background: #dba652;
  overflow: hidden;
  border-radius: 8px;
}

.smiley-red {
  background: #ee9295;
  border: 5px solid #e27378;
  position: relative;
}

.smiley-red .left-eye {
  width: 18%;
  height: 18%;
  background: #d96065;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smiley-red .right-eye {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #d96065;
  top: 29%;
  right: 22%;
  float: right;
}

.smiley-red .smile {
  position: absolute;
  top: 57%;
  left: 16.5%;
  width: 70%;
  height: 20%;
  overflow: hidden;
}

.smiley-red .smile:after,
.smiley-red .smile:before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0%;
  border-radius: 50%;
  background: #d96065;
  height: 100%;
  width: 97%;
}

.smiley-red .smile:after {
  background: #d96065;
  height: 80%;
  top: 60%;
  left: 0%;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="container">
    <div class="smiley-green">
      <div class="left-eye"></div>
      <div class="right-eye"></div>
      <div class="smile"></div>
    </div>

    <div class="smiley-yellow">
      <div class="left-eye"></div>
      <div class="right-eye"></div>
      <div class="smile"></div>
    </div>

    <div class="smiley-red">
      <div class="left-eye"></div>
      <div class="right-eye"></div>
      <div class="smile"></div>
    </div>
  </div>

  <textarea id="log"> Les évènements apparaîtront ici!
</textarea>

  <script src="script.js"></script>

</body>
</html>
relatedTarget peut être null

La propriété relatedTarget peut être null.

C’est normal et cela signifie simplement que la souris de provient pas d’un autre élément, mais hors de la fenêtre Windows. Ou bien qu’elle a quitté la fenêtre Windows.

Nous devons garder cette éventualité à l’esprit lorsqu’on utilise event.relatedTarget dans notre code. Si nous accédons a la propriété event.relatedTarget.tagName, alors il y aura une erreur.

Ignorer des éléments

L’évènement mousemove se déclenche lorsque la souris se déplace. Mais cela ne signifie pas chaque pixel mène a un évènement.

Le navigateur vérifie la position de la souris de temps en temps. Et s’il remarque des changements, déclenche les événements.

Cela signifie que si le visiteur déplace la souris très rapidement, certains éléments DOM peuvent être ignorés :

Si la souris se déplace très rapidement de #FROM aux éléments #TO telle que décrite en haut, alors le <div> intermédiaire (ou certains d’entre eux) peuvent être sautés. L’évènement mouseout peut être déclenche sur #FROM et ensuite immédiatement le mouseover sur #TO.

C’est bon pour la performance, car s’il peut y avoir beaucoup d’éléments intermédiaires. Nous ne voulons pas vraiment traiter dans et hors de chacun d’entre eux.

D’autre part, nous devons garder à l’esprit que le pointeur de la souris ne “visite” pas tous les éléments le long du chemin. Il peut “sauter”.

En particulier, il est possible que le pointeur saute directement au centre de la page depuis l’extérieur de la fenêtre. Dans ce cas relatedTarget est null, parce qu’il venait de “nulle part” :

Vous pouvez le vérifier “en direct” sur un banc d’essai ci-dessous.

Son code HTML comporte deux éléments imbriqués: la <div id="child"> est à l’intérieur de <div id="parent">. Si vous déplacez rapidement la souris dessus, alors peut-être que seule la div enfant déclenchera les événements, ou peut-être la div parent, ou peut-être qu’il n’y aura aucun événement.

Déplacez également le pointeur dans la div enfant, puis le déplacer rapidement en dehors à travers le parent. Si le mouvement est assez rapide, l’élément parent est ignoré. La souris traversera l’élément parent sans le remarquer.

Résultat
script.js
style.css
index.html
let parent = document.getElementById('parent');
parent.onmouseover = parent.onmouseout = parent.onmousemove = handler;

function handler(event) {
  let type = event.type;
  while (type.length < 11) type += ' ';

  log(type + " target=" + event.target.id)
  return false;
}


function clearText() {
  text.value = "";
  lastMessage = "";
}

let lastMessageTime = 0;
let lastMessage = "";
let repeatCounter = 1;

function log(message) {
  if (lastMessageTime == 0) lastMessageTime = new Date();

  let time = new Date();

  if (time - lastMessageTime > 500) {
    message = '------------------------------\n' + message;
  }

  if (message === lastMessage) {
    repeatCounter++;
    if (repeatCounter == 2) {
      text.value = text.value.trim() + ' x 2\n';
    } else {
      text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + repeatCounter + "\n";
    }

  } else {
    repeatCounter = 1;
    text.value += message + "\n";
  }

  text.scrollTop = text.scrollHeight;

  lastMessageTime = time;
  lastMessage = message;
}
#parent {
  background: #99C0C3;
  width: 160px;
  height: 120px;
  position: relative;
}

#child {
  background: #FFDE99;
  width: 50%;
  height: 50%;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

textarea {
  height: 140px;
  width: 300px;
  display: block;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="parent">parent
    <div id="child">child</div>
  </div>
  <textarea id="text"></textarea>
  <input onclick="clearText()" value="Clear" type="button">

  <script src="script.js"></script>

</body>

</html>
Si mouseover est déclenché, il doit y avoir mouseout

En cas de mouvements rapides de la souris, les éléments intermédiaires peuvent être ignorés, mais une chose est sûre : si le pointeur est “officiellement” entré dans un élément (événement mouseover généré), alors en le quittant, on aura toujours mouseout.

Mouseout en quittant pour un enfant

Une caractéristique importante de mouseout – il se déclenche lorsque le pointeur se déplace d’un élément à son descendant, par ex. de #parent à #enfant dans ce code HTML :

<div id="parent">
  <div id="child">...</div>
</div>

Si nous sommes sur #parent, puis déplaçons le pointeur plus profondément dans #enfant, nous obtenons mouseout sur #parent !

Cela semble étrange, mais peut être facilement expliqué.

Selon la logique du navigateur, le curseur de la souris ne peut survoler qu’un seul élément à tout moment – l’élément le plus imbriqué et le plus élevé par z-index.

Donc, s’il passe à un autre élément (même un descendant), alors il quitte le précédent.

Veuillez noter un autre détail important du traitement de l’événement.

L’événement mouseover sur un descendant “bubble up” (remonte). Donc, si #parent a un gestionnaire mouseover, il se déclenche :

Vous pouvez le voir très bien dans l’exemple ci-dessous : <div id="child "> est à l’intérieur de <div id="parent">. Il existe des gestionnaires mouseover/out sur l’élément #parent qui fournissent les détails de l’événement.

Si vous déplacez la souris de #parent à #enfant, vous voyez deux événements sur #parent :

  1. mouseout [target: parent] (quitte le parent), alors
  2. mouseover [target: child] (est arrivé à l’enfant, bubbled).
Résultat
script.js
style.css
index.html
function mouselog(event) {
  let d = new Date();
  text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
  text.scrollTop = text.scrollHeight;
}
#parent {
  background: #99C0C3;
  width: 160px;
  height: 120px;
  position: relative;
}

#child {
  background: #FFDE99;
  width: 50%;
  height: 50%;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

textarea {
  height: 140px;
  width: 300px;
  display: block;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="parent" onmouseover="mouselog(event)" onmouseout="mouselog(event)">parent
    <div id="child">child</div>
  </div>

  <textarea id="text"></textarea>
  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>

</html>

Comme indiqué, lorsque le pointeur passe de l’élément #parent à l’élément #child, deux gestionnaires se déclenchent sur l’élément parent : mouseout et mouseover:

parent.onmouseout = function(event) {
  /* event.target: élément parent */
};
parent.onmouseover = function(event) {
  /* event.target: élément enfant */
};

Si nous n’examinons pas event.target à l’intérieur des gestionnaires, il peut alors sembler que le pointeur de la souris a quitté l’élément #parent, puis est immédiatement revenu dessus.

Mais ce n’est pas le cas! Le pointeur se trouve toujours sur le parent, il s’est déplacé plus profondément dans l’élément enfant.

S’il y a des actions lorsque vous quittez l’élément parent, par exemple une animation qui s’exécute dans parent.onmouseout, nous ne le souhaitons généralement pas lorsque le pointeur va plus en profondeur dans #parent.

Pour l’éviter, nous pouvons vérifier relatedTarget dans le gestionnaire et, si la souris est toujours dans l’élément, alors ignorer cet événement.

Alternativement, nous pouvons utiliser d’autres événements : mouseenter et mouseleave, que nous allons couvrir maintenant, car ils n’ont pas ce genre de problèmes.

Evènements mouseenter and mouseleave

Les évènements mouseenter/mouseleave sont comme mouseover/mouseout. Ils se déclenchent lorsque le pointeur de la souris entre/sort de l’élément.

Mais il y a deux différences importantes :

  1. Les transitions à l’intérieur de l’élément, vers/depuis les descendants, ne sont pas comptées.
  2. Les évènements mouseenter/mouseleave ne “bubble” pas.

Ces événements sont extrêmement simples.

Quand le pointeur entre dans un élément – mouseenter se déclenche. L’emplacement exact du pointeur à l’intérieur de l’élément ou de ses descendants n’a pas d’importance.

Quand le pointeur quitte un élément – mouseleave se déclenche.

Cet exemple est similaire à celui ci-dessus, mais maintenant l’élément supérieur a mouseenter/mouseleave au lieu de mouseover/mouseout.

Comme vous pouvez le voir, les seuls événements générés sont ceux liés au déplacement du pointeur dans et hors de l’élément supérieur. Rien ne se passe lorsque le pointeur se dirige vers l’enfant et vice-versa. Les transitions entre les descendants sont ignorées.

Résultat
script.js
style.css
index.html
function mouselog(event) {
  let d = new Date();
  text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
  text.scrollTop = text.scrollHeight;
}
#parent {
  background: #99C0C3;
  width: 160px;
  height: 120px;
  position: relative;
}

#child {
  background: #FFDE99;
  width: 50%;
  height: 50%;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

textarea {
  height: 140px;
  width: 300px;
  display: block;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="parent" onmouseenter="mouselog(event)" onmouseleave="mouselog(event)">parent
    <div id="child">child</div>
  </div>

  <textarea id="text"></textarea>
  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>

</html>

Délégation des évènements

Les évènements mouseenter/leave sont très simple et facile à utiliser. Mais il ne remonte pas. Donc nous ne pouvons pas utiliser la délégation d’évènements sur eux.

Imaginez qu’on veuille gérer les évènements de souris enter/leave pour les cellules d’un tableau et qu’il ait une centaine de cellules.

La solution naturelle serait de définir le gestionnaire sur <table> et d’y traiter les événements. Mais mouseenter/leave ne “bubble” pas. Donc, si un tel événement se produit sur <td>, alors seul un gestionnaire sur ce <td> est capable de l’attraper.

Les gestionnaires pour mouseenter/leave sur <table> ne se déclenche que lorsque le pointeur entre/sort du tableau dans son ensemble. Il est impossible d’obtenir des informations sur les transitions à l’intérieur.

Alors, utilisons mouseover/mouseout.

Commençons par des gestionnaires simples qui mettent en évidence l’élément sous la souris :

// mettons en évidence un élément sous le pointeur
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';
};

Les voici en action. Lorsque la souris se déplace sur les éléments de ce tableau, celui qui est survolé est mis en évidence:

Résultat
script.js
style.css
index.html
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';

  text.value += `over -> ${target.tagName}\n`;
  text.scrollTop = text.scrollHeight;
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';

  text.value += `out <- ${target.tagName}\n`;
  text.scrollTop = text.scrollHeight;
};
#text {
  display: block;
  height: 100px;
  width: 456px;
}

#table th {
  text-align: center;
  font-weight: bold;
}

#table td {
  width: 150px;
  white-space: nowrap;
  text-align: center;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 12px;
  cursor: pointer;
}

#table .nw {
  background: #999;
}

#table .n {
  background: #03f;
  color: #fff;
}

#table .ne {
  background: #ff6;
}

#table .w {
  background: #ff0;
}

#table .c {
  background: #60c;
  color: #fff;
}

#table .e {
  background: #09f;
  color: #fff;
}

#table .sw {
  background: #963;
  color: #fff;
}

#table .s {
  background: #f60;
  color: #fff;
}

#table .se {
  background: #0c3;
  color: #fff;
}

#table .highlight {
  background: red;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>


  <table id="table">
    <tr>
      <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
    </tr>
    <tr>
      <td class="nw"><strong>Northwest</strong>
        <br>Metal
        <br>Silver
        <br>Elders
      </td>
      <td class="n"><strong>North</strong>
        <br>Water
        <br>Blue
        <br>Change
      </td>
      <td class="ne"><strong>Northeast</strong>
        <br>Earth
        <br>Yellow
        <br>Direction
      </td>
    </tr>
    <tr>
      <td class="w"><strong>West</strong>
        <br>Metal
        <br>Gold
        <br>Youth
      </td>
      <td class="c"><strong>Center</strong>
        <br>All
        <br>Purple
        <br>Harmony
      </td>
      <td class="e"><strong>East</strong>
        <br>Wood
        <br>Blue
        <br>Future
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Southwest</strong>
        <br>Earth
        <br>Brown
        <br>Tranquility
      </td>
      <td class="s"><strong>South</strong>
        <br>Fire
        <br>Orange
        <br>Fame
      </td>
      <td class="se"><strong>Southeast</strong>
        <br>Wood
        <br>Green
        <br>Romance
      </td>
    </tr>

  </table>

  <textarea id="text"></textarea>

  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>
</html>

Ces gestionnaires fonctionnent lorsqu’on se déplace sur n’importe quel élément dans le tableau.

Dans notre cas, nous aimerions gérer les transitions entre les cellules du tableau <td>: entrer dans une cellule et la quitter. Les autres transitions, comme à l’intérieur de la cellule ou à l’extérieur de celles-ci, ne nous intéressent pas. Filtrons-les.

Voici ce que nous pouvons faire :

  • Mémoriser le actuellement sélectionné dans une variable, appelons le currentElem.
  • Sur mouseover – ignorer l’événement si nous sommes toujours dans le <td> actuel.
  • Sur mouseout – ignorer si nous n’avons pas quitté le <td> actuel.

Voici un exemple de code qui prend en compte toutes les situations possibles :

// <td> sous la souris actuellement(le cas échéant)
let currentElem = null;

table.onmouseover = function(event) {
  // avant d'entrer dans un nouvel élément, la souris quitte toujours le précédent
  // Si currentElem est défini, nous n'avons pas quitté le précédent <td>,
  // c'est un mouseover à l'intérieur, ignore l'événement
  if (currentElem) return;

  let target = event.target.closest("td");

  // nous ne sommes pas passés dans un <td> - ignorer
  if (!target) return;

  // déplacé dans <td>, mais en dehors de notre tableau (possible en cas de tableaux imbriquées)
  // ignorer
  if (!table.contains(target)) return;

  // hourra! nous sommes entrés dans un nouveau <td>
  currentElem = target;
  onEnter(currentElem);
};

table.onmouseout = function(event) {
  // si nous sommes en dehors de tout <td> maintenant, alors ignorez l'événement
  // c'est probablement un mouvement à l'intérieur du tableau, mais en dehors des <td>,
  // par exemple. d'un <tr> à un autre <tr>
  if (!currentElem) return;

  // nous quittons l'élément - où aller ? Peut-être à un descendant ?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // monte dans la chaîne parente et vérifie - si nous sommes toujours dans currentElem
    // alors c'est une transition interne - l'ignorer
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // nous avons quitté le <td>. vraiment.
  onLeave(currentElem);
  currentElem = null;
};

// any functions to handle entering/leaving an element
function onEnter(elem) {
  elem.style.background = "pink";

  // show that in textarea
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

function onLeave(elem) {
  elem.style.background = "";

  // show that in textarea
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}

Les caractéristiques importantes sont les suivantes:

  1. On utilise la délégation d’événements pour gérer l’entrée/sortie de tout <td> à l’intérieur de la table. On s’appuie donc sur le mouseover/out au lieu du mouseenter/leave qui ne fait pas de bulles et ne permet donc aucune délégation…
  2. Les événements supplémentaires, tels que le déplacement entre les descendants de <td> sont filtrés, de sorte que onEnter/Leave ne fonctionne que si le pointeur quitte ou entre dans <td> dans son ensemble.

Voici l’exemple complet avec tous les détails:

Résultat
script.js
style.css
index.html
// <td> sous la souris actuellement(le cas échéant)
let currentElem = null;

table.onmouseover = function(event) {
  // avant d'entrer dans un nouvel élément, la souris quitte toujours le précédent
  // Si currentElem est défini, nous n'avons pas quitté le précédent <td>,
  // c'est un mouseover à l'intérieur, ignore l'événement
  if (currentElem) return;

  let target = event.target.closest("td");

  // nous ne sommes pas passés dans un <td> - ignorer
  if (!target) return;

  // déplacé dans <td>, mais en dehors de notre tableau (possible en cas de tableaux imbriquées)
  // ignorer
  if (!table.contains(target)) return;

  // hourra! nous sommes entrés dans un nouveau <td>
  currentElem = target;
  onEnter(currentElem);
};

table.onmouseout = function(event) {
  // si nous sommes en dehors de tout <td> maintenant, alors ignorez l'événement
  // c'est probablement un mouvement à l'intérieur du tableau, mais en dehors des <td>,
  // par exemple. d'un <tr> à un autre <tr>
  if (!currentElem) return;

  // nous quittons l'élément - où aller ? Peut-être à un descendant ?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // monte dans la chaîne parente et vérifie - si nous sommes toujours dans currentElem
    // alors c'est une transition interne - l'ignorer
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // nous avons quitté le <td>. vraiment.
  onLeave(currentElem);
  currentElem = null;
};

// any functions to handle entering/leaving an element
function onEnter(elem) {
  elem.style.background = "pink";

  // show that in textarea
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

function onLeave(elem) {
  elem.style.background = "";

  // show that in textarea
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}
#text {
  display: block;
  height: 100px;
  width: 456px;
}

#table th {
  text-align: center;
  font-weight: bold;
}

#table td {
  width: 150px;
  white-space: nowrap;
  text-align: center;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 12px;
  cursor: pointer;
}

#table .nw {
  background: #999;
}

#table .n {
  background: #03f;
  color: #fff;
}

#table .ne {
  background: #ff6;
}

#table .w {
  background: #ff0;
}

#table .c {
  background: #60c;
  color: #fff;
}

#table .e {
  background: #09f;
  color: #fff;
}

#table .sw {
  background: #963;
  color: #fff;
}

#table .s {
  background: #f60;
  color: #fff;
}

#table .se {
  background: #0c3;
  color: #fff;
}

#table .highlight {
  background: red;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>


  <table id="table">
    <tr>
      <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
    </tr>
    <tr>
      <td class="nw"><strong>Northwest</strong>
        <br>Metal
        <br>Silver
        <br>Elders
      </td>
      <td class="n"><strong>North</strong>
        <br>Water
        <br>Blue
        <br>Change
      </td>
      <td class="ne"><strong>Northeast</strong>
        <br>Earth
        <br>Yellow
        <br>Direction
      </td>
    </tr>
    <tr>
      <td class="w"><strong>West</strong>
        <br>Metal
        <br>Gold
        <br>Youth
      </td>
      <td class="c"><strong>Center</strong>
        <br>All
        <br>Purple
        <br>Harmony
      </td>
      <td class="e"><strong>East</strong>
        <br>Wood
        <br>Blue
        <br>Future
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Southwest</strong>
        <br>Earth
        <br>Brown
        <br>Tranquility
      </td>
      <td class="s"><strong>South</strong>
        <br>Fire
        <br>Orange
        <br>Fame
      </td>
      <td class="se"><strong>Southeast</strong>
        <br>Wood
        <br>Green
        <br>Romance
      </td>
    </tr>

  </table>

  <textarea id="text"></textarea>

  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>
</html>

Essayez de déplacer le curseur dans et hors des cellules du tableau et à l’intérieur de celles-ci. Rapide ou lent – peu importe. Seul <td> dans son ensemble est mis en surbrillance, contrairement à l’exemple précédent.

Résumé

Nous avons abordé les évènements mouseover, mouseout, mousemove, mouseenter et mouseleave.

Ces choses sont bonnes à noter :

  • Un mouvement rapide de la souris peut ignorer les éléments intermédiaires.
  • Les évènements mouseover/out et mouseenter/leave ont une propriété supplémentaire : relatedTarget. C’est l’élément duquel nous venons de/à, complémentaire à target.

Les évènements mouseover/out se déclenchent même lorsque nous passons de l’élément parent à un élément enfant. Le navigateur suppose que la souris ne peut survoler qu’un seul élément à la fois, le plus profond.

Les évènements mouseenter/leave sont différents à cet égard : ils ne se déclenchent que lorsque la souris entre et sort de l’élément dans son ensemble. En outre, ils ne “bubble” pas.

Exercices

importance: 5

Write JavaScript that shows a tooltip over an element with the attribute data-tooltip. The value of this attribute should become the tooltip text.

That’s like the task Comportement info-bulle, but here the annotated elements can be nested. The most deeply nested tooltip is shown.

Only one tooltip may show up at the same time.

For instance:

<div data-tooltip="Here – is the house interior" id="house">
  <div data-tooltip="Here – is the roof" id="roof"></div>
  ...
  <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a>
</div>

The result in iframe:

Open a sandbox for the task.

importance: 5

Ecrivez une fonction qui montre une info-bulle sur un élément seulement si le visiteur déplace la souris sur celui-ci, et non pas en le traversant.

En d’autres termes, si l’usager déplace la souris sur l’élément et s’arrête dessus – afficher l’info bulle. Et si le visiteur déplace seulement la souris en traversant rapidement l’élément, pas besoin de le faire, qui a besoin d’un clignotement supplémentaire ?

Techniquement, nous pouvons mesurer la vitesse de la souris sur un élément, et si elle est lente alors nous pouvons assumer qu’elle arrive “sur l’élément” et monter l’info-bulle, si elle est rapide – alors on l’ignore.

Créer un objet universel new HoverIntent(options) pour cela.

Ses options :

  • elem – l’élément à surveiller.
  • over – une fonction à appeler si la souris arrive sur l’élément : c’est-à-dire qu’elle se déplace lentement ou s’arrête dessus.
  • out – une fonction à appeler quand la souris quitte l’élément (si over était appelé).

Un exemple d’usage d’un tel objet pour l’info-bulle:

// un example d'info-bulle
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";

// L’objet va  suivre la souris et appeler les fonctions over/out
new HoverIntent({
  elem,
  over() {
    tooltip.style.left = elem.getBoundingClientRect().left + 'px';
    tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px';
    document.body.append(tooltip);
  },
  out() {
    tooltip.remove();
  }
});

La demo:

Si vous déplacez la souris rapidement sur la “montre” alors rien ne se passe, et si vous le faites lentement ou que vous vous arrêtez, alors il y aura une info-bulle.

Notez bien: l’info-bulle ne “clignote” pas lorsque le curseur se déplace entre la montre et les sous éléments.

Open a sandbox with tests.

The algorithm looks simple:

  1. Put onmouseover/out handlers on the element. Also can use onmouseenter/leave here, but they are less universal, won’t work if we introduce delegation.
  2. When a mouse cursor entered the element, start measuring the speed on mousemove.
  3. If the speed is slow, then run over.
  4. When we’re going out of the element, and over was executed, run out.

But how to measure the speed?

The first idea can be: run a function every 100ms and measure the distance between previous and new coordinates. If it’s small, then the speed is small.

Unfortunately, there’s no way to get “current mouse coordinates” in JavaScript. There’s no function like getCurrentMouseCoordinates().

The only way to get coordinates is to listen for mouse events, like mousemove, and take coordinates from the event object.

So let’s set a handler on mousemove to track coordinates and remember them. And then compare them, once per 100ms.

P.S. Please note: the solution tests use dispatchEvent to see if the tooltip works right.

Ouvrez la solution avec des tests dans une sandbox.

Carte du tutoriel

Commentaires

lire ceci avant de commenter…
  • Si vous avez des améliorations à suggérer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de préciser.
  • Pour insérer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)