12 de julio de 2022

Moviendo el mouse: mouseover/out, mouseenter/leave

Entremos en detalle sobre los eventos que suceden cuando el mouse se mueve entre elementos.

Eventos mouseover/mouseout, relatedTarget

El evento mouseover se produce cuando el cursor del mouse aparece sobre un elemento y mouseout cuando se va.

Estos eventos son especiales porque tienen la propiedad relatedTarget. Esta propiedad complementa a target. Cuando el puntero del mouse deja un elemento por otro, uno de ellos se convierte en target y el otro en relatedTarget.

Para mouseover:

  • event.target – Es el elemento al que se acerca el mouse.
  • event.relatedTarget – Es el elemento de donde proviene el mouse (relatedTargettarget).

Para mouseout sucede al contrario:

  • event.target – Es el elemento que el mouse dejó.
  • event.relatedTarget – es el nuevo elemento bajo el cursor por cuál el cursor dejó al anterior (targetrelatedTarget).

En el siguiente ejemplo, cada cara y sus características son elementos separados. Puedes ver en el área de texto los eventos que ocurren cuando mueves el mouse.

Cada evento tiene la información sobre ambas propiedades: target y relatedTarget:

Resultado
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">¡Los eventos se mostrarán aquí!
</textarea>

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

</body>
</html>
relatedTarget puede ser null

La propiedad relatedTarget puede tener un valor null.

Eso es normal y solo significa que el mouse no vino de otro elemento, sino de la ventana o que salió de la ventana.

Debemos tener en cuenta esa posibilidad cuando usemos event.relatedTarget en nuestro código. Si accedemos a event.relatedTarget.tagName entonces habrá un error.

Saltando elementos

El evento mousemove se activa cuando el mouse se mueve, pero eso no significa que cada píxel nos lleve a un evento.

El navegador verifica la posición del mouse de vez en cuando y si nota cambios entonces activan los eventos.

Eso significa que si el visitante mueve el mouse muy rápido, entonces algunos elementos del DOM podrían estar siendo ignorados:

Si el mouse se mueve muy rápido de los elementos #FROM a #TO, como se muestra arriba, entonces los elementos intermedios <div> (o algunos de ellos) podrían ser ignorados. El evento mouseout se podría activar en #FROM e inmediatamente mouseover en #TO.

Eso es bueno para el rendimiento porque puede haber muchos elementos intermedios. Realmente no queremos procesar todo lo que sucede dentro y fuera de cada uno.

Por otro lado, debemos tener en cuenta que el puntero del mouse no “visita” todos los elementos en el camino. Los puede “saltar”.

En particular, es posible que el puntero salte dentro de la mitad de la página desde la ventana. En ese caso relatedTarget es null, porque vino de “la nada”:

Puedes verlo “en vivo” en un testeador a continuación.

Este HTML tiene dos elementos: el <div id="child"> está adentro del <div id="parent">. Si mueves el mouse rápidamente sobre ellos entonces probablemente solo el div hijo active los eventos, o probablemente el padre, o probablemente no ocurran eventos en lo absoluto.

También prueba a mover el cursor hacia el div hijo y luego muévelo rápidamente hacia abajo a través del padre. Si el movimiento es lo suficientemente rápido entonces el padre será ignorado. El mouse cruzará el elemento padre sin notarlo.

Resultado
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 se activa, deberá haber mouseout

En caso de movimientos rápidos, los elementos intermedios podrían ser ignorados, pero una cosa segura sabemos: si el cursor ingresa “oficialmente” dentro de un elemento(evento mouseover generado), una vez que lo deje obtendremos mouseout.

Mouseout, cuando se deja un elemento por uno anidado.

Una característica importante de mouseout – se activa cuando el cursor se mueve de un elemento hacia su descendiente (elemento anidado o interno). Por ejemplo de #parent a #child en este HTML:

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

Si estamos sobre #parent y luego movemos el cursor hacia dentro de #child, ¡vamos a obtener mouseout en #parent!

Eso puede parecer extraño, pero puede explicarse fácilmente.

De acuerdo con la lógica del navegador, el cursor podría estar sobre un elemento individual en cualquier momento – el anidado y el más alto según el z-index.

Entonces si se dirige hacia otro elemento (incluso uno anidado), está dejando al anterior.

Por favor, note otro importante detalle sobre el procesamiento de eventos.

El evento mouseover se aparece en un un elemento anidado (brota o nace, por decirlo así). Entonces si #parent tiene el controlador mouseover, se activa:

Puedes verlo muy bien a continuación: <div id="child"> está dentro de<div id="parent">. Hay controladores mouseover/out en el elemento #parent que arrojan los detalles de los eventos.

Si mueves el mouse de #parent a #child, verás dos eventos sobre #parent:

  1. mouseout [target: parent] (dejó al padre), luego
  2. mouseover [target: child] (vino hacia el hijo, y este evento brotó).
Resultado
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>

Como se muestra, cuando el cursor se mueve del elemento #parent a #child, los dos controladores se activan en el elemento padre: mouseout y mouseover:

parent.onmouseout = function(event) {
  /* event.target: elemento padre  */
};
parent.onmouseover = function(event) {
  /* event.target: elemento hijo (brota) */
};

Si no examinamos event.target dentro de los controladores podría parecer que el cursor dejo el elemento #parent y volvió a él inmediatamente.

Pero ese no es el caso. El cursor aún está sobre el elemento padre, simplemente se adentró más en el elemento hijo.

Si hay algunas acciones al abandonar el elemento padre,por ejemplo: una animación se ejecuta con parent.onmouseout, usualmente no la queremos cuando el cursor se adentre más sobre #parent.

Para evitar esto lo que podemos hacer es checar relatedTarget en el controlador y si el mouse aún permanece dentro del elemento entonces ignorar dicho evento.

Alternativamente podemos usar otros eventos: mouseenter y mouseleave, los cuales cubriremos a continuación, ya que con ellos no hay tales problemas.

Eventos mouseenter y mouseleave

Los eventos mouseenter/mouseleave son como mouseover/mouseout. Se activan cuando el cursor del mouse entra/sale del elemento.

Pero hay dos diferencias importantes:

  1. Las transiciones hacia/desde los descendientes no se cuentan.
  2. Los eventos mouseenter/mouseleave no brotan.

Son eventos extremadamente simples.

Cuando el cursor entra en un elemento mouseenter se activa. La ubicación exacta del cursor dentro del elemento o sus descendientes no importa.

Cuando el cursor deja el elemento mouseleave se activa.

Este ejemplo es similar al anterior, pero ahora el elemento tiene mouseenter/mouseleave en lugar de mouseover/mouseout.

Como puedes ver, los únicos eventos generados son los relacionados con mover el puntero dentro y fuera del elemento superior. No pasa nada cuando el puntero va hacia el descendiente y regresa. Las transiciones entre descendientes se ignoran:

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

Delegación de eventos

Los eventos mouseenter/leave son muy simples de usar. Pero no brotan por sí solos. Por lo tanto no podemos usar la delegación de eventos con ellos.

Imagina que queremos manejar entrada/salida para celdas de tabla y hay cientos de celdas.

La solución natural sería: ajustar el controlador en <table> y manejar los eventos desde ahí. Pero mouseenter/leave no aparece. Entonces si cada evento sucede en <td>, solamente un controlador <td> es capaz de detectarlo.

Los controladores mouseenter/leave en <table> solamente se activan cuando el cursor entra/deja la tabla completa. Es imposible obtener alguna información sobre las transiciones dentro de ella.

Pues usemos mouseover/mouseout.

Comencemos con controladores simples que resaltan el elemento debajo del mouse:

// Resaltemos un elemento debajo del cursor
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
};

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

Aquí se muestran en acción. A medida que el mouse recorre los elementos de esta tabla, se resalta la actual:

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

En nuestro caso nos gustaría manejar las transiciones entre las celdas de la tabla <td>: entradas y salidas de una celda a otra. Otras transiciones, como dentro de una celda o fuera de cualquiera de ellas no nos interesan. Vamos a filtrarlas.

Esto es lo que podemos hacer:

  • Recordar el elemento <td> resaltado actualmente en una variable, llamémosla currentElem.
  • En mouseover ignoraremos el evento si permanecemos dentro del <td> actual.
  • En mouseout ignoraremos el evento si no hemos dejado el <td> actual.

Aquí hay un ejemplo de código que explica todas las situaciones posibles:

// Los elementos <td> bajo el maouse justo ahora(si es que hay)
let currentElem = null;

table.onmouseover = function(event) {
  // antes de ingresar un uevo elemento, el mouse siempre abandonará al anterior
  // si currentElem está establecido, no abandonamos el <td> anterior,
  // hay un mouseover dentro de él, ignoramos el evento
  if (currentElem) return;

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

  // si no hay movimientos dentro de un <td> - lo ignoramos
  if (!target) return;

  //si hay movimientos dentro de un <td>, pero afuera de una tabla(posiblemente en caso de tablas anidadas)
  // lo ignoramos
  if (!table.contains(target)) return;

  // ¡Genial! ingresamos a un nuevo <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function(event) {
  // si estamos afuera de algún <td> ahora, entonces ignoramos el evento
  // puede haber movimientos dentro de una tabla, pero fuera de <td>,
  // por ejemplo: de un <tr> a otro <tr>
  if (!currentElem) return;

  // abandonamos el elemento – ¿pero hacia dónde? ¿podría ser hacia un descendiente?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // vamos a la cadena de padres y verificamos – si aún estamos dentro de currentElem
    // entonces hay una transición interna – la ignoramos
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // abandonamos el <td>.
  onLeave(currentElem);
  currentElem = null;
};

// algunas funciones para manejar entradas/salidas de un elemento
function onEnter(elem) {
  elem.style.background = 'pink';

  // lo mostramos en el área de texto
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

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

  // lo mostramos en el area de texto
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}

Una vez más, las características importantes son:

  1. Utilizar la delegación de eventos para manejar la entrada/salida de cualquier <td> dentro de la tabla. Pues depende de mouseover/out en lugar de mouseenter/leave que no broten y por lo tanto no permita ninguna delegación.
  2. Los eventos adicionales, como moverse entre descendientes de <td> son filtrados, así que onEnter/Leave solamente se ejecuta si el cursor ingresa a <td> o lo deja absolutamente.

Aquí está el ejemplo completo con todos los detalles:

Resultado
script.js
style.css
index.html
// Los elementos <td> bajo el maouse justo ahora(si es que hay)
let currentElem = null;

table.onmouseover = function(event) {
  // antes de ingresar un uevo elemento, el mouse siempre abandonará al anterior
  // si currentElem está establecido, no abandonamos el <td> anterior,
  // hay un mouseover dentro de él, ignoramos el evento
  if (currentElem) return;

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

  // si no hay movimientos dentro de un <td> - lo ignoramos
  if (!target) return;

  //si hay movimientos dentro de un <td>, pero afuera de una tabla(posiblemente en caso de tablas anidadas)
  // lo ignoramos
  if (!table.contains(target)) return;

  // ¡Genial! ingresamos a un nuevo <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function(event) {
  // si estamos afuera de algún <td> ahora, entonces ignoramos el evento
  // puede haber movimientos dentro de una tabla, pero fuera de <td>,
  // por ejemplo: de un <tr> a otro <tr>
  if (!currentElem) return;

  // abandonamos el elemento – ¿pero hacia dónde? ¿podría ser hacia un descendiente?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // vamos a la cadena de padres y verificamos – si aún estamos dentro de currentElem
    // entonces hay una transición interna – la ignoramos
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // abandonamos el <td>.
  onLeave(currentElem);
  currentElem = null;
};

// algunas funciones para manejar entradas/salidas de un elemento
function onEnter(elem) {
  elem.style.background = 'pink';

  // lo mostramos en el área de texto
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

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

  // lo mostramos en el area de texto
  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>

Intenta mover el cursor dentro y fuera de las celdas de la tabla y dentro de cada una de ellas. Rápido o lento – no importa --. Solo se ilumina <td> como un todo, a diferencia del ejemplo anterior.

Resumen

Hemos cubierto mouseover, mouseout, mousemove, mouseenter ymouseleave.

Estas cosas son buenas de destacar:

  • Un movimiento rápido del mouse puede omitir elementos intermedios.
  • Los eventos mouseover/out y mouseenter/leave tienen una propiedad adicional: relatedTarget. Es el elemento de donde venimos o hacia donde vamos, complementario con target.

Los eventos mouseover/out se activan incluso cuando vamos de un elemento padre a su descendiente. El navegador asume que de el mouse solo puede estar sobre un elemento a la vez – el más interno.

Los eventos mouseenter/leave son diferentes en ese aspecto: solo se activan cuando el mouse viene hacia el elemento o lo deja como un todo. Así que no se aparecen de repente.

Tareas

importancia: 5

Escribe JavaScript que muestre un tooltip sobre un elemento con el atributo data-tooltip. El valor de este atributo debe convertirse en el texto del tooltip.

Es como la tarea Comportamiento: Tooltip, pero aquí los elementos anotados se pueden anidar. Los tooltips más internos se muestran.

Solamente un tooltip puede aparecer a la vez.

Por ejemplo:

<div data-tooltip="Aquí – está el interior de la casa" id="house">
  <div data-tooltip="Aquí – está el techo" id="roof"></div>
  ...
  <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Continúa leyendo…">Colócate sobre mi</a>
</div>

El resultado en el iframe:

Abrir un entorno controlado para la tarea.

Escribe una función que muestre un tooltip sobre un elemento solamente si el visitante mueve el mouse hacia él, pero no a través de él.

En otras palabras, si el visitante mueve el mouse hacia el elemento y para ahí, muestra el tooltip. Y si solamente mueve el mouse a través, entonces no lo necesitamos. ¿Quién quiere parpadeos extra?

Técnicamente, podemos medir la velocidad del mouse sobre el elemento, y si es lenta podemos asumir que el mouse viene “sobre el elemento” y mostramos el tooltip, si es rápida – entonces lo ignoramos.

Hay que crear un objeto universal new HoverIntent(options) para ello.

Sus options:

  • elem – elemento a seguir.
  • over – una función a llamar si el el mouse viene hacia el elemento: o sea, si viene lentamente o para sobre él.
  • out – una función a llamar cuando el mouse abandona el elemento (si over fue llamado).

Un ejemplo de dicho objeto siendo usado para el tooltip:

//  Un tooltip de muestra
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";

// El objeto va a rastrear al mouse y llamar a 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();
  }
});

El demo:

Si mueves el mouse sobre el “reloj” rápido no pasará nada, y si lo haces lento o paras sobre él entonces habrá un tooltip.

Toma en cuenta que el tooltip no “parpadea” cuando el cursor se mueve entre subelementos del reloj.

Abrir en entorno controlado con pruebas.

El algoritmo se ve simple:

  1. Coloca los controladores onmouseover/out en el elemento. Aquí también podemos usar onmouseenter/leave, pero son menos universales, no funcionan si introducimos delegaciones.
  2. Cuando el cursor ingrese al elemento debes medir la velocidad en mousemove.
  3. Si la velocidad es lenta hay que ejecutar over.
  4. Si estamos saliendo del elemento, y over ya se había ejecutado, ahora ejecutamos out.

¿Pero cómo mediremos la velocidad?

La primera idea puede ser: correr una función cada 100ms y medir la distancia entre la coordenada anterior y la actual. Si es pequeña entonces la velocidad fue rápida.

Desafortunadamente no hay manera para obtener las coordenadas actuales del mouse en JavaScript. No existe algo así como getCurrentMouseCoordinates().

La única manera es registrando los eventos del mouse, como mousemove, y tomar las coordenadas del objeto del evento.

Entonces configuremos un mousemove para registrar las coordenadas y recordarlas. Y entonces las comparamos, una por cada 100ms.

PD. Toma nota: El test de la solución usa dispatchEvent para ver si el tooltip funciona bien.

Abrir la solución con pruebas en un entorno controlado.

Mapa del Tutorial

Comentarios

lea esto antes de comentar…
  • Si tiene sugerencias sobre qué mejorar, por favor enviar una propuesta de GitHub o una solicitud de extracción en lugar de comentar.
  • Si no puede entender algo en el artículo, por favor explique.
  • Para insertar algunas palabras de código, use la etiqueta <code>, para varias líneas – envolverlas en la etiqueta <pre>, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)