5. března 2026

Pohyb myší: mouseover/out, mouseenter/leave

Ponořme se nyní do dalších detailů událostí, které nastávají, když se ukazatel myši pohybuje mezi elementy.

Události mouseover/mouseout, relatedTarget

Událost mouseover nastává, když ukazatel myši vstoupí na element, a mouseout nastane, když jej opustí.

Tyto události jsou speciální v tom, že mají vlastnost relatedTarget, která doplňuje vlastnost target. Když se ukazatel přesune z jednoho elementu na druhý, jeden z nich se stane hodnotou target a druhý hodnotou relatedTarget.

Pro mouseover:

  • událost.target – je element, na který ukazatel vstoupil.
  • event.relatedTarget – je element, ze kterého ukazatel přišel (relatedTarget → target).

Pro mouseout je to obráceně:

  • event.target – je element, který ukazatel opustil.
  • event.relatedTarget – je nový element pod ukazatelem, na který ukazatel vstoupil (target → relatedTarget).

V následujícím příkladu jsou každá tvář i její jednotlivé prvky samostatné elementy. Když budete pohybovat myší, uvidíte v textové oblasti události myši.

Každá událost obsahuje informaci o target i o relatedTarget:

Výsledek
script.js
style.css
index.html
kontejner.onmouseover = kontejner.onmouseout = handler;

function handler(událost) {

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

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

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

#kontejner {
  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^="smajlík-"] {
  display: inline-block;
  width: 70px;
  height: 70px;
  border-radius: 50%;
  margin-right: 20px;
}

.smajlík-zelený {
  background: #a9db7a;
  border: 5px solid #92c563;
  position: relative;
}

.smajlík-zelený .levé-oko {
  width: 18%;
  height: 18%;
  background: #84b458;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smajlík-zelený .pravé-oko {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #84b458;
  top: 29%;
  right: 22%;
  float: right;
}

.smajlík-zelený .úsměv {
  position: absolute;
  top: 67%;
  left: 16.5%;
  width: 70%;
  height: 20%;
  overflow: hidden;
}

.smajlík-zelený .úsměv:after,
.smajlík-zelený .úsměv:before {
  content: "";
  position: absolute;
  top: -50%;
  left: 0%;
  border-radius: 50%;
  background: #84b458;
  height: 100%;
  width: 97%;
}

.smajlík-zelený .úsměv:after {
  background: #84b458;
  height: 80%;
  top: -40%;
  left: 0%;
}

.smajlík-žlutý {
  background: #eed16a;
  border: 5px solid #dbae51;
  position: relative;
}

.smajlík-žlutý .levé-oko {
  width: 18%;
  height: 18%;
  background: #dba652;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smajlík-žlutý .pravé-oko {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #dba652;
  top: 29%;
  right: 22%;
  float: right;
}

.smajlík-žlutý .úsměv {
  position: absolute;
  top: 67%;
  left: 19%;
  width: 65%;
  height: 14%;
  background: #dba652;
  overflow: hidden;
  border-radius: 8px;
}

.smajlík-červený {
  background: #ee9295;
  border: 5px solid #e27378;
  position: relative;
}

.smajlík-červený .levé-oko {
  width: 18%;
  height: 18%;
  background: #d96065;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smajlík-červený .pravé-oko {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #d96065;
  top: 29%;
  right: 22%;
  float: right;
}

.smajlík-červený .úsměv {
  position: absolute;
  top: 57%;
  left: 16.5%;
  width: 70%;
  height: 20%;
  overflow: hidden;
}

.smajlík-červený .úsměv:after,
.smajlík-červený .úsměv:before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0%;
  border-radius: 50%;
  background: #d96065;
  height: 100%;
  width: 97%;
}

.smajlík-červený .úsměv: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="kontejner">
    <div class="smajlík-zelený">
      <div class="levé-oko"></div>
      <div class="pravé-oko"></div>
      <div class="úsměv"></div>
    </div>

    <div class="smajlík-žlutý">
      <div class="levé-oko"></div>
      <div class="pravé-oko"></div>
      <div class="úsměv"></div>
    </div>

    <div class="smajlík-červený">
      <div class="levé-oko"></div>
      <div class="pravé-oko"></div>
      <div class="úsměv"></div>
    </div>
  </div>

  <textarea id="log">Zde se zobrazí události!
</textarea>

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

</body>
</html>
relatedTarget může být null

Vlastnost relatedTarget může být null.

To se běžně stává a znamená to jen to, že ukazatel nepřišel z jiného elementu, ale zvnějšku okna, nebo že okno opustil.

Na tuto možnost bychom měli pamatovat, když používáme událost.relatedTarget v našem kódu. Přistoupíme-li k událost.relatedTarget.tagName, nastane chyba.

Přeskakování elementů

Událost mousemove se spustí, když je ukazatel přesunut. To však neznamená, že tuto událost spustí každý pixel, přes který se ukazatel přesune.

Prohlížeč si jednou za čas zkontroluje pozici myši, a pokud zjistí, že se změnila, vyvolá události.

To znamená, že když návštěvník pohybuje myší příliš rychle, může některé DOM elementy přeskočit:

Jestliže se myš pohybuje velmi rychle z elementu #OD do elementu #DO, jak je zobrazeno výše, může přeskočit mezilehlé elementy <div> (nebo některé z nich). Může se spustit událost mouseout na #OD a hned poté mouseover na #DO.

To zlepšuje výkon, jelikož mezilehlých elementů může být mnoho. Opravdu nechceme zpracovávat vstup na a výstup z každého z nich.

Na druhou stranu bychom měli mít na paměti, že ukazatel myši „nenavštíví“ všechny elementy po cestě a může přes ně „skákat“.

Konkrétně je možné, že ukazatel skočí zvnějšku okna přímo doprostřed stránky. V takovém případě relatedTarget je null, protože přichází „odnikud“:

Můžete si to vyzkoušet „naživo“ na testovacím příkladu níže.

Jeho HTML kód obsahuje dva vnořené elementy: <div id="dítě"> se nachází uvnitř <div id="rodič">. Pokud nad nimi budete rychle pohybovat myší, možná vyvolá události jen dětský div, možná jen rodičovský a možná vůbec žádné události nenastanou.

Zkuste také přesunout ukazatel na dětský div a pak jej přesunout rychle dolů přes rodičovský. Bude-li pohyb dostatečně rychlý, bude rodičovský element ignorován. Ukazatel ho přeskočí bez povšimnutí.

Výsledek
script.js
style.css
index.html
let rodič = document.getElementById('rodič');
rodič.onmouseover = rodič.onmouseout = rodič.onmousemove = handler;

function handler(událost) {
  let typ = událost.type;
  while (typ.length < 11) typ += ' ';

  log(typ + " target=" + událost.target.id)
  return false;
}


function vymažText() {
  text.value = "";
  posledníZpráva = "";
}

let časPosledníZprávy = 0;
let posledníZpráva = "";
let početOpakování = 1;

function log(zpráva) {
  if (časPosledníZprávy == 0) časPosledníZprávy = new Date();

  let čas = new Date();

  if (čas - časPosledníZprávy > 500) {
    zpráva = '------------------------------\n' + zpráva;
  }

  if (zpráva === posledníZpráva) {
    početOpakování++;
    if (početOpakování == 2) {
      text.value = text.value.trim() + ' x 2\n';
    } else {
      text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + početOpakování + "\n";
    }

  } else {
    početOpakování = 1;
    text.value += zpráva + "\n";
  }

  text.scrollTop = text.scrollHeight;

  časPosledníZprávy = čas;
  posledníZpráva = zpráva;
}
#rodič {
  background: #99C0C3;
  width: 160px;
  height: 120px;
  position: relative;
}

#dítě {
  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="rodič">rodič
    <div id="dítě">dítě</div>
  </div>
  <textarea id="text"></textarea>
  <input onclick="vymažText()" value="Vymazat" type="button">

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

</body>

</html>
Když se spustila mouseover, musí být i mouseout

Při rychlém pohybu myší mohou být mezilehlé elementy ignorovány, ale jedno víme jistě: jestliže ukazatel „oficiálně“ vstoupil na element (byla generována událost mouseover), pak při jeho opuštění vždy obdržíme mouseout.

Událost mouseout při opuštění dítěte

Důležitou vlastností události mouseout je, že se spustí, když se ukazatel přemístí z elementu na jeho potomka, např. v následujícím HTML kódu z #rodič na #dítě:

<div id="rodič">
  <div id="dítě">...</div>
</div>

Pokud jsme na #rodič a přesuneme ukazatel hlouběji na #dítě, pak dostaneme mouseout na #rodič!

Může to vypadat divně, ale má to jednoduché vysvětlení.

Podle logiky prohlížeče může být ukazatel myši pouze na jediném elementu současně – na tom nejvnořenějším a podle z-indexu vrchním.

Když se tedy přesune na jiný element (třeba i na potomka), opustí předcházející.

Prosíme všimněte si dalšího důležitého detailu zpracování událostí.

Událost mouseover na potomku bublá. Jestliže tedy #rodič má handler mouseover, spustí se:

Na následujícím příkladu to vidíte velmi dobře: <div id="dítě"> je uvnitř <div id="rodič">. Na elementu #rodič jsou handlery mouseover/out, které vypisují detaily událostí.

Když přesunete myš z #rodič na #dítě, uvidíte na #rodič dvě události:

  1. mouseout [target: rodič] (opuštění rodiče), pak
  2. mouseover [target: dítě] (příchod na dítě, probublala).
Výsledek
script.js
style.css
index.html
function mouselog(událost) {
  let d = new Date();
  text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${událost.type} [target: ${událost.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
  text.scrollTop = text.scrollHeight;
}
#rodič {
  background: #99C0C3;
  width: 160px;
  height: 120px;
  position: relative;
}

#dítě {
  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="rodič" onmouseover="mouselog(event)" onmouseout="mouselog(event)">rodič
    <div id="dítě">dítě</div>
  </div>

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

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

</body>

</html>

Jak je vidět, když se ukazatel přemístí z elementu #rodič na element #dítě, spustí se na rodičovském elementu dvě události: mouseout a mouseover:

rodič.onmouseout = function(událost) {
  /* událost.target: rodičovský element */
};
rodič.onmouseover = function(událost) {
  /* událost.target: dětský element (probublala) */
};

Pokud v handlerech neprozkoumáme událost.target, může to vypadat tak, že ukazatel myši opustil element #rodič a hned pak se na něj vrátil.

To však není tento případ! Ukazatel je neustále na rodiči, jen se přemístil hlouběji na dětský element.

Pokud se při opuštění rodičovského elementu mají provést nějaké akce, např. v rodič.onmouseout se má spustit animace, pak je zpravidla nechceme, když se ukazatel jen přemístí hlouběji dovnitř #rodič.

Abychom se tomu vyhnuli, můžeme v handleru prověřovat relatedTarget, a pokud je myš stále uvnitř elementu, budeme takovou událost ignorovat.

Alternativně můžeme použít jiné události: mouseenter a mouseleave, které nyní probereme. Ty tento problém nemají.

Události mouseenter a mouseleave

Události mouseenter/mouseleave se podobají mouseover/mouseout. Spustí se, když ukazatel myši vstoupí na/opustí element.

Jsou mezi nimi však dva důležité rozdíly:

  1. Přesuny uvnitř elementu, na potomky a z nich, se nepočítají.
  2. Události mouseenter/mouseleave nebublají.

Tyto události jsou extrémně jednoduché.

Když ukazatel vstoupí na element, spustí se mouseenter. Na přesné poloze ukazatele uvnitř elementu nebo jeho potomků nezáleží.

Když ukazatel opustí element, spustí se mouseleave.

Následující příklad se podobá výše uvedenému, ale vrchní element nyní obsahuje mouseenter/mouseleave místo mouseover/mouseout.

Jak vidíte, vygenerují se jedině události, které se vztahují k přesunu ukazatele na vrchní element a z něj. Když se ukazatel přemístí na jeho dítě a zpět, nic se nestane. Přesuny mezi potomky jsou ignorovány.

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

#dítě {
  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="rodič" onmouseenter="mouselog(event)" onmouseleave="mouselog(event)">rodič
    <div id="dítě">dítě</div>
  </div>

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

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

</body>

</html>

Delegování událostí

Události mouseenter/leave jsou velice jednoduché a snadno se používají, ale nebublají. Nemůžeme tedy na ně použít delegování událostí.

Představme si, že chceme zpracovávat vstup myši na buňky tabulky a jejich opuštění. A těch jsou stovky.

Přirozené řešení by bylo nastavit handler na <table> a zpracovávat události v něm. Jenže mouseenter/leave nebublají. Když se tedy taková událost stane na <td>, dokáže ji zachytit jedině handler na tomto <td>.

Handlery pro mouseenter/leave na <table> se spustí jen tehdy, když ukazatel vstoupí na celou tabulku nebo ji opustí. O přesunech uvnitř ní nemůžeme získat žádné informace.

Použijeme tedy mouseover/mouseout.

Začněme s jednoduchými handlery, které zvýrazňují element pod ukazatelem:

// zvýrazníme element pod ukazatelem
table.onmouseover = function(událost) {
  let cíl = událost.target;
  cíl.style.background = 'pink';
};

table.onmouseout = function(událost) {
  let cíl = událost.target;
  cíl.style.background = '';
};

Zde je vidíte v akci. Když se myš přesunuje po elementech této tabulky, aktuální element se zvýrazní:

Výsledek
script.js
style.css
index.html
tabulka.onmouseover = function(událost) {
  let cíl = událost.target;
  cíl.style.background = 'pink';

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

tabulka.onmouseout = function(událost) {
  let cíl = událost.target;
  cíl.style.background = '';

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

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

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

#tabulka .nw {
  background: #999;
}

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

#tabulka .ne {
  background: #ff6;
}

#tabulka .w {
  background: #ff0;
}

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

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

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

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

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

#tabulka .zvýrazni {
  background: red;
}
<!DOCTYPE HTML>
<html>

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

<body>


  <table id="tabulka">
    <tr>
      <th colspan="3">Tabulka <em>Pa-kua</em>: směr, element, barva, význam</th>
    </tr>
    <tr>
      <td class="nw"><strong>Severozápad</strong>
        <br>Kov
        <br>Stříbrná
        <br>Stáří
      </td>
      <td class="n"><strong>Sever</strong>
        <br>Voda
        <br>Modrá
        <br>Změna
      </td>
      <td class="ne"><strong>Severovýchod</strong>
        <br>Země
        <br>Žlutá
        <br>Směr
      </td>
    </tr>
    <tr>
      <td class="w"><strong>Západ</strong>
        <br>Kov
        <br>Zlatá
        <br>Mládí
      </td>
      <td class="c"><strong>Střed</strong>
        <br>Všechny
        <br>Fialová
        <br>Harmonie
      </td>
      <td class="e"><strong>Východ</strong>
        <br>Dřevo
        <br>Modrá
        <br>Budoucnost
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Jihozápad</strong>
        <br>Země
        <br>Hnědá
        <br>Klid
      </td>
      <td class="s"><strong>Jih</strong>
        <br>Oheň
        <br>Oranžová
        <br>Sláva
      </td>
      <td class="se"><strong>Jihovýchod</strong>
        <br>Dřevo
        <br>Zelená
        <br>Láska
      </td>
    </tr>

  </table>

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

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

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

</body>
</html>

V našem případě bychom chtěli zpracovávat přesuny mezi buňkami tabulky <td>: vstup na buňku a její opuštění. Jiné přesuny, například uvnitř jedné buňky nebo mimo buňky, nás nezajímají. Odfiltrujme je.

Můžeme dělat následující:

  • Zapamatujeme si aktuálně zvýrazněnou <td> v proměnné, nazveme ji aktuálníElem.
  • Při mouseover budeme tuto událost ignorovat, jestliže jsme stále uvnitř aktuální <td>.
  • Při mouseout ji budeme ignorovat, jestliže neopustíme aktuální <td>.

Následuje příklad kódu, který počítá se všemi možnými situacemi:

// <td> pod ukazatelem právě teď (pokud nějaká je)
let aktuálníElem = null;

tabulka.onmouseover = function(událost) {
  // před vstupem na nový element myš vždy opustí předchozí
  // jestliže aktuálníElem je nastaven, neopustili jsme předchozí <td>,
  // jde o mouseover uvnitř něj, tuto událost budeme ignorovat
  if (aktuálníElem) return;

  let cíl = událost.target.closest('td');

  // nepřesunuli jsme se na <td> - ignorovat
  if (!cíl) return;

  // přesunuli jsme se na <td>, ale mimo naši tabulku (to je možné v případě vnořených tabulek)
  // ignorovat
  if (!tabulka.contains(cíl)) return;

  // hurá! vstoupili jsme na novou <td>
  aktuálníElem = cíl;
  přiPříchodu(aktuálníElem);
};


tabulka.onmouseout = function(událost) {
  // pokud jsme nyní mimo jakoukoli <td>, budeme tuto událost ignorovat
  // to je pravděpodobně pohyb uvnitř tabulky, ale mimo <td>,
  // např. z <tr> na jinou <tr>
  if (!aktuálníElem) return;

  // opouštíme element - kam? Možná na potomka?
  let cílPohybu = událost.relatedTarget;

  while (cílPohybu) {
    // projdeme řetězec rodičů a prověříme to - pokud jsme stále uvnitř aktuálníElem,
    // je to vnitřní přesun - budeme ho ignorovat
    if (cílPohybu == aktuálníElem) return;

    cílPohybu = cílPohybu.parentNode;
  }

  // opravdu jsme opustili <td>
  přiOdchodu(aktuálníElem);
  aktuálníElem = null;
};

// veškeré funkce pro zpracování vstupu/opuštění elementu
function přiPříchodu(elem) {
  elem.style.background = 'pink';

  // zobrazíme to v textarea
  text.value += `dovnitř -> ${aktuálníElem.tagName}.${aktuálníElem.className}\n`;
  text.scrollTop = 1e6;
}

function přiOdchodu(elem) {
  elem.style.background = '';

  // zobrazíme to v textarea
  text.value += `ven <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}

Opakujeme, že jeho důležité prvky jsou:

  1. Pro zpracování vstupu na/opuštění jakékoli <td> v tabulce používá delegování událostí. Je tedy založen na mouseover/out, ne na mouseenter/leave, která nebublá, a tedy neumožňuje delegování.
  2. Další události, například přesun mezi potomky <td>, jsou odfiltrovány, takže funkce přiPříchodu/přiOdchodu se spustí jen tehdy, když ukazatel vstoupí na celou <td> nebo ji opustí.

Zde je celý příklad se všemi detaily:

Výsledek
script.js
style.css
index.html
// <td> pod ukazatelem právě teď (pokud nějaká je)
let aktuálníElem = null;

tabulka.onmouseover = function(událost) {
  // před vstupem na nový element myš vždy opustí předchozí
  // jestliže aktuálníElem je nastaven, neopustili jsme předchozí <td>,
  // jde o mouseover uvnitř něj, tuto událost budeme ignorovat
  if (aktuálníElem) return;

  let cíl = událost.target.closest('td');

  // nepřesunuli jsme se na <td> - ignorovat
  if (!cíl) return;

  // přesunuli jsme se na <td>, ale mimo naši tabulku (to je možné v případě vnořených tabulek)
  // ignorovat
  if (!tabulka.contains(cíl)) return;

  // hurá! vstoupili jsme na novou <td>
  aktuálníElem = cíl;
  přiPříchodu(aktuálníElem);
};


tabulka.onmouseout = function(událost) {
  // pokud jsme nyní mimo jakoukoli <td>, budeme tuto událost ignorovat
  // to je pravděpodobně pohyb uvnitř tabulky, ale mimo <td>,
  // např. z <tr> na jinou <tr>
  if (!aktuálníElem) return;

  // opouštíme element - kam? Možná na potomka?
  let cílPohybu = událost.relatedTarget;

  while (cílPohybu) {
    // projdeme řetězec rodičů a prověříme to - pokud jsme stále uvnitř aktuálníElem,
    // je to vnitřní přesun - budeme ho ignorovat
    if (cílPohybu == aktuálníElem) return;

    cílPohybu = cílPohybu.parentNode;
  }

  // opravdu jsme opustili <td>
  přiOdchodu(aktuálníElem);
  aktuálníElem = null;
};

// veškeré funkce pro zpracování vstupu/opuštění elementu
function přiPříchodu(elem) {
  elem.style.background = 'pink';

  // zobrazíme to v textarea
  text.value += `dovnitř -> ${aktuálníElem.tagName}.${aktuálníElem.className}\n`;
  text.scrollTop = 1e6;
}

function přiOdchodu(elem) {
  elem.style.background = '';

  // zobrazíme to v textarea
  text.value += `ven <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}
#text {
  display: block;
  height: 100px;
  width: 456px;
}

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

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

#tabulka .nw {
  background: #999;
}

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

#tabulka .ne {
  background: #ff6;
}

#tabulka .w {
  background: #ff0;
}

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

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

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

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

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

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

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

<body>


  <table id="tabulka">
    <tr>
      <th colspan="3">Tabulka <em>Pa-kua</em>: směr, element, barva, význam</th>
    </tr>
    <tr>
      <td class="nw"><strong>Severozápad</strong>
        <br>Kov
        <br>Stříbrná
        <br>Stáří
      </td>
      <td class="n"><strong>Sever</strong>
        <br>Voda
        <br>Modrá
        <br>Změna
      </td>
      <td class="ne"><strong>Severovýchod</strong>
        <br>Země
        <br>Žlutá
        <br>Směr
      </td>
    </tr>
    <tr>
      <td class="w"><strong>Západ</strong>
        <br>Kov
        <br>Zlatá
        <br>Mládí
      </td>
      <td class="c"><strong>Střed</strong>
        <br>Všechny
        <br>Fialová
        <br>Harmonie
      </td>
      <td class="e"><strong>Východ</strong>
        <br>Dřevo
        <br>Modrá
        <br>Budoucnost
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Jihozápad</strong>
        <br>Země
        <br>Hnědá
        <br>Klid
      </td>
      <td class="s"><strong>Jih</strong>
        <br>Oheň
        <br>Oranžová
        <br>Sláva
      </td>
      <td class="se"><strong>Jihovýchod</strong>
        <br>Dřevo
        <br>Zelená
        <br>Láska
      </td>
    </tr>

  </table>

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

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

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

</body>
</html>

Zkuste si přesunovat ukazatel dovnitř a ven z buněk tabulky a uvnitř nich. Rychle nebo pomalu, na tom nezáleží. Zvýrazní se jen <td> jako celek, na rozdíl od výše uvedeného příkladu.

Shrnutí

Probrali jsme události mouseover, mouseout, mousemove, mouseenter a mouseleave.

Je dobré si zapamatovat následující:

  • Při rychlém pohybu myší mohou být přeskočeny mezilehlé elementy.
  • Události mouseover/out a mouseenter/leave mají další vlastnost: relatedTarget. To je element, ze kterého přicházíme / na který odcházíme, doplněk k target.

Události mouseover/out se spustí i tehdy, když se přesuneme z rodičovského elementu na dětský. Prohlížeč předpokládá, že ukazatel může být pouze na jednom elementu současně – na tom nejhlubším.

Události mouseenter/leave se v tomto ohledu liší: spustí se jen tehdy, když myš vstoupí na celý element nebo jej opustí. Kromě toho nebublají.

Úlohy

důležitost: 5

Napište kód v JavaScriptu, který zobrazí tooltip nad elementem obsahujícím atribut data-tooltip. Textem tooltipu by měla být hodnota tohoto atributu.

Podobá se to úloze Podpora tooltipu, ale elementy s tooltipem tentokrát mohou být vnořené. Zobrazí se ten nejhlouběji vnořený tooltip.

Může se zobrazit jen jeden tooltip současně.

Příklad:

<div data-tooltip="Zde – je interiér domu" id="dům">
  <div data-tooltip="Zde – je střecha" id="střecha"></div>
  ...
  <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Číst dál…">Najeďte na mě</a>
</div>

Výsledek v rámu:

Otevřít pískoviště pro úlohu.

důležitost: 5

Napište funkci, která zobrazí tooltip nad elementem, jen když návštěvník přesune ukazatel myši na něj, ale ne přes něj.

Jinými slovy, jestliže návštěvník přesune ukazatel nad element a zastaví se tam, tooltip se zobrazí. Ale pokud jen přesune myš přes element jinam, pak to není třeba – kdo by chtěl další blikání?

Technicky můžeme změřit rychlost ukazatele nad elementem, a pokud je nízká, pak předpokládáme, že ukazatel vstoupil „na element“ a zobrazíme tooltip. Pokud je vysoká, budeme ukazatel ignorovat.

Vytvořte k tomu univerzální objekt new Setrvání(možnosti).

Jeho možnosti:

  • elem – sledovaný element.
  • over – funkce, která bude volána, když ukazatel vstoupí na element, tedy když se nad ním pohybuje pomalu nebo se na něm zastavil.
  • out – funkce, která bude volána, když ukazatel opustí element (pokud byla předtím volána over).

Příklad použití takového objektu s tooltipem:

// příklad tooltipu
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";

// objekt bude sledovat myš a volat over/out
new Setrvání({
  elem,
  over() {
    tooltip.style.left = elem.getBoundingClientRect().left + 'px';
    tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px';
    document.body.append(tooltip);
  },
  out() {
    tooltip.remove();
  }
});

Demo:

Jestliže pohybujete myší nad „hodinami“ rychle, nic se neděje. Pokud jí pohybujete pomalu nebo se nad nimi zastavíte, zobrazí se tooltip.

Prosíme všimněte si, že tooltip „neblikne“, když se kurzor přesune mezi podelementy hodin.

Otevřít pískoviště s testy.

Algoritmus vypadá jednoduše:

  1. Umístíme na element handlery onmouseover/out. Můžeme zde použít i onmouseenter/leave, ale ty jsou méně univerzální a kdybychom zavedli delegování, nefungovaly by.
  2. Když na element vstoupí ukazatel myši, začneme měřit rychlost v mousemove.
  3. Pokud je rychlost nízká, spustíme over.
  4. Když opustíme element a bylo spuštěno over, spustíme out.

Jak ale měřit rychlost?

Prvním nápadem může být spustit každých 100 ms funkci, která změří vzdálenost mezi předchozími a současnými souřadnicemi. Bude-li malá, bude rychlost nízká.

Naneštěstí v JavaScriptu není žádný způsob, jak zjistit „aktuální souřadnice myši“. Neexistuje žádná funkce jako getCurrentMouseCoordinates().

Jediný způsob, jak zjistit souřadnice, je naslouchat událostem myši, např. mousemove, a zjišťovat souřadnice z objektu události.

Vytvoříme tedy handler pro mousemove, aby sledoval souřadnice a pamatoval si je. A pak je jednou za 100 ms budeme porovnávat.

P.S. Prosíme všimněte si, že testy v řešení používají dispatchEvent, aby viděly, zda tooltip funguje správně.

Otevřít řešení s testy na pískovišti.

Mapa tutoriálu

Komentáře

přečtěte si před komentováním…
  • Máte-li návrhy na zlepšení, vytvořte prosím issue na GitHubu nebo pull request místo komentáře.
  • Pokud v článku něčemu nerozumíte, napište prosím, čemu přesně a na kterém místě.
  • Pro vložení několika slov kódu použijte značku <code>, pro několik řádků je obalte značkou <pre>, pro více než 10 řádků vložte odkaz na pískoviště (plnkr, jsbin, codepen…)