20 dicembre 2021

Movimenti del mouse: mouseover/out, mouseenter/leave

Entriamo nel dettaglio degli eventi generati quando il mouse si sposta tra gli elementi.

Eventi mouseover/mouseout, relatedTarget

L’evento mouseover viene generato quando il puntatore del mouse passa su un elemento, e mouseout – quando lo abbandona.

Sono eventi particolari, perché posseggono la proprietà relatedTarget. Questa proprietà è complementare rispetto a target. Quando il mouse passa da un elemento a un altro, uno di questi diventa il target, e l’altro – relatedTarget.

Per mouseover:

  • event.target – é l’elemento appena raggiunto dal mouse.
  • event.relatedTarget – è l’elemento appena abbandonato dal mouse (relatedTargettarget).

Per mouseout, invece, è esattamente il contrario:

  • event.target – è l’elemento appena lasciato dal mouse.
  • event.relatedTarget – è il nuovo elemento sotto il puntatore (targetrelatedTarget).

Nel seguente esempio, ogni faccia e le sue proprietà sono elementi separati. Al movimento del mouse, corrispondono degli eventi che vengono descritti nell’area di testo.

Ogni evento contiene entrambe le informazioni sia del target che del relatedTarget:

Risultato
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">Events will show up here!
</textarea>

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

</body>
</html>
relatedTarget può essere null

La proprietà relatedTarget può essere null.

È normale e significa solo che il mouse non proviene da un altro elemento della UI, ma esternamente rispetto alla finestra. Oppure può significare che l’ha appena lasciata (per l’evento mouseover).

Dobbiamo tenere a mente questa eventualità, quando coinvolgiamo event.relatedTarget nel nostro codice, perché in queste condizioni, nel tentativo di accedere ad event.relatedTarget.tagName, andremmo incontro ad un errore.

Saltare elementi

L’evento mousemove viene attivato dal movimento del mouse. Tuttavia, ciò non significa che ogni pixel porterà ad un evento.

Il browser controlla la posizione del mouse di tanto in tanto. E se in questo frangente noterà qualche cambiamento, allora genererà degli eventi.

Ne consegue che, se l’utente muovesse il mouse molto velocemente, potrebbero essere “saltati” alcuni elementi del DOM:

Se il mouse si muovesse molto velocemente, passando dagli elementi #FROM a #TO appena illustrati, gli elementi <div> intermedi (o alcuni di essi) potrebbero essere ignorati. L’evento mouseout potrebbe essere generato su #FROM ed il successivo mouseover immediatamente su #TO.

Questo è sicuramente ottimo in termini di prestazioni, dal momento che potrebbero esserci tanti elementi intermedi. Non vogliamo veramente elaborare l’entrata e uscita per ognuno di essi.

D’altra parte, dovremmo anche tenere a mente che il puntatore del mouse non “visita” tutti gli elementi lungo il suo cammino. Può appunto “saltare” elementi.

In particolare, è possibile che il puntatore arrivi direttamente al centro della pagina, provenendo dall’esterno della finestra. In questo caso relatedTarget sarebbe null, non venendo da “nessuna parte”:

Possiamo testare dal “vivo” il concetto, nel seguente banco di prova.

Questo HTML ha due elementi nidificati: un <div id="child"> dentro un <div id="parent">. Muovendo il mouse velocemente su di loro, potrebbe accadere che l’evento venga generato solo dal div figlio, o magari solo dal genitore, oppure ancora, potrebbe non esserci alcun evento.

Inoltre, si potrebbe provare a spostare il puntatore dentro il div figlio, e poi subito dopo, muoverlo velocemente attraverso il genitore. Se il movimento è abbastanza veloce, allora l’elemento genitore potrebbe essere ignorato. In questo caso, il mouse attraverserebbe l’elemento genitore senza nemmeno “notarlo”.

Risultato
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>
Se viene generato mouseover, allora deve esserci mouseout

Come appena detto, per movimenti sufficientemente rapidi, gli elementi intermedi potrebbero essere ignorati, ma una cosa è certa: se il puntatore entra “ufficialmente” dentro un elemento (quindi è stato generato l’evento mouseover), allora dopo averlo lasciato otterremo sempre un mouseout.

Mouseout quando si abbandona il genitore per un elemento figlio

Un’importante caratteristica di mouseout è che – viene generato quando il puntatore si muove da un elemento verso un suo discendente, ad esempio da #parent verso #child come nel seguente HTML:

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

Se siamo su #parent e spostiamo il mouse del tutto dentro #child, otteniamo un mouseout on #parent!

A prima vista può sembrare strano, ma la spiegazione è molto semplice.

Coerentemente con la logica del browser, il puntatore del mouse può stare sopra un solo elemento per volta – il più annidato e con il valore di z-index più alto.

Quindi, se il puntatore si sposta su un altro elemento (anche nel caso di un suo discendente), allora lascia il precedente.

È bene porre la nostra attenzione ad un importante dettaglio sull’elaborazione dell’evento.

L’evento mouseover su un discendente “sale verso l’alto” (bubbling). Quindi, se #parent avesse un gestore mouseover, questo verrebbe attivato:

Possiamo notare il fenomeno palesarsi nel seguente esempio: <div id="child"> è dentro <div id="parent">. Abbiamo dei gestori mouseover/out sull’elemento #parent che generano dettagli sugli eventi.

Spostando il mouse da #parent a #child, è possibile notare due eventi su #parent:

  1. mouseout [target: parent] (indica che ha lasciato il genitore), e poi
  2. mouseover [target: child] (ci dice che è arrivato sul figlio, con il bubbling).
Risultato
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>

Si nota bene che quando il puntatore si muove dall’elemento #parent al #child, vengono attivati due gestori sull’elemento genitore: mouseout e mouseover:

parent.onmouseout = function(event) {
  /* event.target: elemento genitore */
};
parent.onmouseover = function(event) {
  /* event.target: elemento figlio (bubbled) */
};

Se non analizzassimo event.target dentro i gestori, potremmo essere portati a pensare che il puntatore lasci l’elemento genitore per poi di nuovo rientrarci subito dopo.

Ma non è così. Il puntatore è ancora sul genitore, solo che si è mosso più internamente, dentro l’elemento figlio.

Se vi fossero degli eventi generati dall’abbandono dell’elemento genitore, ad esempio un’animazione eseguita al parent.onmouseout, nella maggioranza dei casi non vorremmo che si attivassero se il puntatore entrasse in profondità nel #parent andando dentro il figlio (anzi, in generale vorremmo che si attivassero solo quando il puntatore va all’esterno dell’area del #parent).

Quindi, per evitare questo, possiamo controllare relatedTarget nel gestore, e se il mouse è ancora dentro l’elemento, ignoriamo del tutto l’evento.

In alternativa, possiamo usare altri eventi: mouseenter e mouseleave, che affronteremo proprio adesso, che non sono affetti da queste problematiche.

Eventi mouseenter e mouseleave

Gli eventi mouseenter/mouseleave si comportano come mouseover/mouseout. Vengono attivati quando il puntatore del mouse entra/lascia l’elemento.

Ma hanno due importanti differenze:

  1. Le transizioni dentro l’elemento, da e verso i discendenti, non vengono considerate.
  2. Gli eventi mouseenter/mouseleave non “risalgono” in bubbling.

Sono eventi estremamente semplici.

Quando il puntatore entra su un elemento – viene generato mouseenter. La posizione esatta del puntatore dentro l’elemento o dei suoi discendenti è del tutto irrilevante.

Quando il puntatore lascia un elemento – viene generato mouseleave.

L’esempio seguente è simile al precedente, solo che in questo caso l’elemento superiore è associato a mouseenter/mouseleave piuttosto che mouseover/mouseout.

Come possiamo notare, gli unici eventi generati sono quelli relativi al movimento del puntatore del mouse dentro e fuori dall’elemento superiore. Se il puntatore va dentro l’elemento figlio, non succede nulla. Le transizioni tra i figli vengono ignorate.

Risultato
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>

Event delegation

Gli eventi mouseenter/leave sono molto semplici e facili da usare. Ma, abbiamo detto, non seguono la logica del bubbling. Quindi, con questi eventi non potremo mai usare la event delegation.

Ora immaginiamo di voler gestire il movimento del mouse, sia in entrata che in uscita dalle celle di una tabella. E immaginiamo anche che questa tabella abbia centinaia di celle.

La soluzione più naturale sarebbe quella di – impostare un gestore su <table> per elaborare lì gli eventi. Ma il problema è che mouseenter/leave non fanno bubbling. Quindi se questi eventi avvengono su <td>, solo un gestore su <td> potrà catturarli.

I gestori per mouseenter/leave sulla <table> verrebbero generati solo se il puntatore entrasse ed uscisse dalla tabella, e sarebbe impossibile ottenere informazioni sugli spostamenti al suo interno.

Quindi, saremmo costretti ad usare mouseover/mouseout.

Cominciamo con dei semplici gestori che evidenziano gli elementi sotto il mouse:

// evidenziamo un elemento sotto il puntatore
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
};

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

Eccoli in azione. Quando il mouse si sposta attraverso gli elementi di questa tabella, vengono evidenziati:

Risultato
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>

Nel nostro caso vogliamo gestire i passaggi tra le celle della tabella <td>: cioè quando entra in una cella lasciandone un’altra. Gli altri passaggi, come quelli all’interno o fuori da ogni altra celle, non ci interessano e li filtriamo.

Ecco come possiamo fare:

  • Memorizzare l’attuale <td> evidenziata in una variabile, che chiameremo currentElem.
  • Al mouseover – ignorarlo se siamo ancora dentro l’elemento <td> corrente.
  • Al mouseout – ignorarlo se non abbiamo lasciato il <td> corrente.

Ecco un esempio di codice che tiene conto di tutte le combinazioni:

// <td> sotto il mouse proprio adesso (ove previsto)
let currentElem = null;

table.onmouseover = function (event) {
  // prima di entrare su un nuovo elemento, il mouse abbandona quello precedente
  // se currentElem e' impostato, non abbiamo abbandonato il precedente <td>,
  // il mouse è ancora dentro, ignoriamo l'evento
  if (currentElem) return;

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

  // ci siamo spostati ma non dentro un td <td> - ignoriamo
  if (!target) return;

  // ci siamo spostati dentro un <td>, ma fuori dalla nostra tabella (possibile in casi di tabelle annidate)
  // ignoriamo
  if (!table.contains(target)) return;

  // evviva! siamo entrati dentro un nuovo <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function (event) {
  // se adesso siamo fuori da qualunque <td>, ignoriamo l'evento
  // si tratta probabilmente di un movimento dentro la tabella, ma fuori dal <td>,
  // ad esempio da un <tr> a un altro <tr>
  if (!currentElem) return;

  // stiamo abbandonando l'elemento – verso dove? Forse un nodo discendente?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // risale la "catena" dei nodi genitori e controlla - se siamo ancora dentro currentElem
    // si tratta di uno spostamento interno - lo ignoriamo
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // abbiamo lasciato il <td>. per davvero.
  onLeave(currentElem);
  currentElem = null;
};

// qualsiasi funzione per gestire l'entrata e l'uscita da un elemento
function onEnter(elem) {
  elem.style.background = 'pink';

  // lo mostra nella textarea
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

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

  // lo mostra nella textarea
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}

Ancora una volta, le caratteristiche da tenere in considerazione sono:

  1. Usare la event delegation per gestire l’entrata/uscita dai <td> dentro la tabella. Quindi usare mouseover/out piuttosto che mouseenter/leave, in quanto questi ultimi, non facendo bubbling, non permetterebbero la event delegation.
  2. Gli eventi aggiuntivi, come lo spostamento del mouse tra gli elementi discendenti di <td> vanno esclusi, in modo da eseguire onEnter/Leave solo quando il puntatore entra o abbandona del tutto il <td>.

Ecco un esempio con tutti i dettagli:

Risultato
script.js
style.css
index.html
// <td> sotto il mouse proprio adesso (ove previsto)
let currentElem = null;

table.onmouseover = function (event) {
  // prima di entrare su un nuovo elemento, il mouse abbandona quello precedente
  // se currentElem e' impostato, non abbiamo abbandonato il precedente <td>,
  // il mouse è ancora dentro, ignoriamo l'evento
  if (currentElem) return;

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

  // ci siamo spostati ma non dentro un td <td> - ignoriamo
  if (!target) return;

  // ci siamo spostati dentro un <td>, ma fuori dalla nostra tabella (possibile in casi di tabelle annidate)
  // ignoriamo
  if (!table.contains(target)) return;

  // evviva! siamo entrati dentro un nuovo <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function (event) {
  // se adesso siamo fuori da qualunque <td>, ignoriamo l'evento
  // si tratta probabilmente di un movimento dentro la tabella, ma fuori dal <td>,
  // ad esempio da un <tr> a un altro <tr>
  if (!currentElem) return;

  // stiamo abbandonando l'elemento – verso dove? Forse un nodo discendente?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // risale la "catena" dei nodi genitori e controlla - se siamo ancora dentro currentElem
    // si tratta di uno spostamento interno - lo ignoriamo
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // abbiamo lasciato il <td>. per davvero.
  onLeave(currentElem);
  currentElem = null;
};

// qualsiasi funzione per gestire l'entrata e l'uscita da un elemento
function onEnter(elem) {
  elem.style.background = 'pink';

  // lo mostra nella textarea
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

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

  // lo mostra nella 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>

Proviamo a spostare il cursore dentro e fuori dalla celle della tabella ed anche al loro interno. Velocemente o lentamente – è irrilevante. Solo l’intero <td> deve essere evidenziato, diversamente da quanto fatto nell’esempio precedente.

Riepilogo

Abbiamo visto gli eventi mouseover, mouseout, mousemove, mouseenter e mouseleave.

Queste sono le cose da evidenziare:

  • Un rapido movimento del mouse può fare ignorare gli elementi intermedi.
  • Gli eventi mouseover/out e mouseenter/leave posseggono una proprietà aggiuntiva: relatedTarget sarà l’elemento dal quale stiamo uscendo, o nel quale stiamo entrando, ed è complementare a target.

Gli eventi mouseover/out vengono generati anche quando andiamo dall’elemento genitore all’elemento figlio. Il browser assume che il mouse può stare su un solo elemento alla volta – quello più annidato.

Gli eventi mouseenter/leave sono differenti da questo punto di vista: vengono scaturiti solo quando il mouse entra o esce del tutto da un elemento. Inoltre non sono soggetti al bubbling.

Esercizi

importanza: 5

Scrivete del codice JavaScript che mostri un tooltip su un elemento con un attributo data-tooltip. Il valore di questo attributo dovrebbe rappresentare il testo del tooltip.

Questo compito è come quello di Comportamento tooltip, con la differenza che qui gli elementi delle annotazioni possono essere annidati. Deve essere mostrato il tooltip più annidato.

Può essere mostrato solo un tooltip alla volta.

Per esempio:

<div data-tooltip="Qui – l'interno della casa" id="house">
  <div data-tooltip="Qui – il tetto" id="roof"></div>
  ...
  <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Leggi…">Hover su di me</a>
</div>

Il risultato dell’iframe:

Apri una sandbox per l'esercizio.

importanza: 5

Scrivete una funzione che mostri un tooltip su un elemento solo se l’utente sposta il mouse su di esso, e non attraverso di esso.

In altre parole, se il visitatore sposta il mouse su questo elemento e si ferma lì – mostra il tooltip. Se invece ha solo spostato il mouse passandoci sopra, non ce n’è bisogno, d’altronde chi mai vorrebbe altri elementi lampeggianti non desiderati?

Tecnicamente, possiamo misurare la velocità del mouse su un elemento, e se è abbastanza lento possiamo supporre che sta arrivando proprio “sull’elemento”, mostrando il tooltip, se è troppo veloce – lo ignoriamo.

Creare un oggetto universale new HoverIntent(options) utile allo scopo.

Le opzioni possibili options:

  • elem – elemento da tracciare.
  • over – una funzione da chiamare se il mouse arriva sull’elemento: ossia, se si muove lentamente o se si ferma sull’elemento.
  • out – una funzione da chiamare quando il mouse abbandona l’elemento (se è stato chiamato over).

Ecco un esempio dell’uso di questo oggetto per il tooltip:

// un tooltip di esempio
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";

// l'oggetto tiene traccia del mouse e chiama 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:

Muovendo il mouse oltre la velocità di “clock” non succede nulla, facendolo lentamente o fermandocisi sopra, viene mostrato il tooltip.

Nota bene: il tooltip non “lampeggia” quando il cursore si muove tra i sottoelementi dell’orologio.

Apri una sandbox con i test.

L’algoritmo è semplice:

  1. Impostare dei gestori onmouseover/out sull’elemento. Qui si possono anche usare onmouseenter/leave, però sono meno universali, e non funzionerebbero se introducessimo l’uso della delegation.
  2. Quando il puntatore è entrato dentro l’elemento, si comincia a misurare la velocità al mousemove.
  3. Se la velocità è lenta, eseguire over.
  4. Quando si esce fuori dall’elemento, ed è stato eseguito over, eseguire out.

Ma come misurare la velocità?

La prima strategia potrebbe essere: eseguire una funzione ogni 100ms e misurare la distanza tra le vecchie e nuove coordinate. Se fosse piccola, anche la velocità lo sarebbe.

Sfortunatamente, non c’è modo di ricavare “le coordinate attuali del mouse” in JavaScript. Non esistono funzioni come getCurrentMouseCoordinates().

L’unico modo è di mettersi in ascolto sugli eventi del mouse, come mousemove, e prendere le coordinate dall’oggetto evento.

Quindi impostiamo un gestore su mousemove per tenere traccia delle coordinate e memorizzarle, per poi confrontarle ogni 100ms.

P.S.: Nota bene: i test della soluzione fanno uso di dispatchEvent per vedere se il tooltip funziona bene.

Apri la soluzione con i test in una sandbox.

Mappa del tutorial