2022年4月17日

マウスの移動: mouseover/out, mouseenter/leave

マウスポインタが要素間を移動したときに発生するイベントについて、より詳細に見ていきましょう。

イベント mouseover/mouseout, relatedTarget

mouseover イベントは、マウスポインタが要素上に来たときに発生し、mouseout は、マウスポインタが要素から離れたときに発生します。

これらのイベントは、relatedTarget プロパティを持っているため、特殊です。このプロパティは target を補完します。マウスが1つの要素から別の要素に移動すると、一方の要素が target になり、もう一方の要素が relatedTarget になります。

mouseover の場合

  • event.target – マウスが上に乗った要素です。
  • event.relatedTarget – マウスが来た要素です (relatedTargettarget)。

mouseout の場合は逆になります

  • event.target – マウスが離れた要素です。
  • event.relatedTarget – マウスが移動した、ポインタの下にある新しい要素です (targetrelatedTarget)。

以下の例では、顔とその特徴はそれぞれ別々の要素です。マウスを動かすと、テキストエリアにマウスイベントが表示されます。

各イベントには、targetrelatedTarget の両方の情報が含まれています

結果
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>
relatedTargetnull になる可能性があります

relatedTarget プロパティは null になる可能性があります。

これは正常であり、マウスが別の要素からではなく、ウィンドウの外から来たことを意味します。または、ウィンドウから離れたことを意味します。

コードで event.relatedTarget を使用する場合、この可能性を念頭に置いておく必要があります。 event.relatedTarget.tagName にアクセスすると、エラーが発生します。

要素のスキップ

mousemove イベントは、マウスが移動したときに発生します。しかし、すべてのピクセル移動がイベントを発生させるわけではありません。

ブラウザは、時々マウスの位置を確認します。そして、変化に気づくとイベントを発生させます。

つまり、訪問者がマウスを非常に速く動かしている場合、一部のDOM要素がスキップされる可能性があります

マウスが上記のように #FROM 要素から #TO 要素に非常に速く移動した場合、中間の <div> 要素 (またはその一部) がスキップされる可能性があります。 mouseout イベントが #FROM で発生し、すぐに mouseover#TO で発生する可能性があります。

これはパフォーマンスの向上に役立ちます。なぜなら、中間要素がたくさんある可能性があるからです。それぞれの要素に出入りする処理はしたくありません。

一方、マウスポインタは途中で「訪問」するすべての要素を訪れるわけではないことに注意する必要があります。マウスポインタは「ジャンプ」する可能性があります.

特に、ポインタがウィンドウの外からページの真ん中にジャンプする可能性があります。その場合、relatedTargetnull になります。なぜなら、「どこからともなく」来たからです

以下のテストスタンドで「ライブ」で確認できます。

そのHTMLには、2つのネストされた要素があります。<div id="child"><div id="parent"> の中にあります。マウスをそれらの上で速く動かすと、子divのみがイベントを発生させるか、親divがイベントを発生させるか、またはイベントがまったく発生しない可能性があります。

また、ポインタを子の div に移動し、次に親の div を通ってすばやく下に移動します。移動が十分に速い場合、親要素は無視されます。マウスは親要素に気付かずに通過します。

結果
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>
mouseover が発生した場合、mouseout が発生する必要があります

マウスの動きが速い場合、中間要素は無視される可能性がありますが、確実にわかっていることが1つあります。ポインタが「公式に」要素に入った場合 (mouseover イベントが発生した場合)、要素から離れると常に mouseout が発生します。

子に移動するときのMouseout

mouseout の重要な機能は、ポインタが要素からその子孫に移動したときに発生することです。たとえば、このHTMLでは #parent から #child に移動したときです

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

#parent にいて、ポインタを #child のさらに深いところに移動すると、#parentmouseout が発生します!

これは奇妙に思えるかもしれませんが、簡単に説明できます。

ブラウザのロジックによれば、マウスカーソルは、いつでも _単一の_ 要素(最もネストされた要素で、z-indexが最も高い要素)の上にのみ存在できます。

したがって、マウスカーソルが別の要素(子孫であっても)に移動すると、前の要素から離れます。

イベント処理のもう1つの重要な詳細に注意してください.

子孫の mouseover イベントはバブリングアップします。したがって、#parentmouseover ハンドラがある場合、それはトリガーされます

以下の例でよくわかります。<div id="child"><div id="parent"> の中にあります。 #parent 要素には、イベントの詳細を出力する mouseover/out ハンドラがあります.

マウスを #parent から #child に移動すると、#parent で2つのイベントが発生します

  1. mouseout [target: parent] (親から離れました)、次に
  2. mouseover [target: child] (子に来ました、バブリングしました)。
結果
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>

示されているように、ポインタが #parent 要素から #child に移動すると、親要素で2つのハンドラがトリガーされます: mouseoutmouseover

parent.onmouseout = function(event) {
  /* event.target: parent element */
};
parent.onmouseover = function(event) {
  /* event.target: child element (bubbled) */
};

ハンドラ内で event.target を調べない場合、マウスポインタが #parent 要素から離れて、すぐにその上に戻ってきたように見えるかもしれません.

しかし、そうではありません!ポインタはまだ親の上にあります。子要素のさらに深いところに移動しただけです。

親要素から離れると何らかのアクションがある場合、たとえば、parent.onmouseout でアニメーションが実行される場合、ポインタが #parent のさらに深いところに移動したときには、通常はそれを望みません。

これを回避するには、ハンドラで relatedTarget をチェックし、マウスがまだ要素内にある場合は、そのようなイベントを無視できます.

または、次に説明する mouseentermouseleave イベントを使用することもできます。これらのイベントにはそのような問題はありません.

イベント mouseenter と mouseleave

イベント mouseenter/mouseleavemouseover/mouseout に似ています。マウスポインタが要素に入るとき/出るときに発生します。

ただし、2つの重要な違いがあります

  1. 要素内、子孫との間の遷移はカウントされません。
  2. イベント mouseenter/mouseleave はバブリングしません。

これらのイベントは非常にシンプルです。

ポインタが要素に入ると – mouseenter が発生します。要素内またはその子孫内のポインタの正確な位置は関係ありません.

ポインタが要素から離れると – mouseleave が発生します.

この例は上記の例に似ていますが、最上位要素には mouseover/mouseout の代わりに mouseenter/mouseleave があります.

ご覧のとおり、生成される唯一のイベントは、ポインタが最上位要素に出入りすることに関連するイベントです。ポインタが子に移動して戻ってきても何も起こりません。子孫間の遷移は無視されます

結果
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>

イベント делегирование

イベント mouseenter/leave は非常にシンプルで使い勝手が良いです.しかし、それらはバブルしません。そのため、イベント委任をそれらに使用することはできません.

テーブルセルのマウスの進入/離脱を処理したいとします。そして、何百ものセルがあります.

自然な解決策は、<table> にハンドラを設定し、そこでイベントを処理することです。しかし、mouseenter/leave はバブルしません。そのため、そのようなイベントが <td> で発生した場合、その <td> のハンドラのみがそれをキャッチできます.

<table>mouseenter/leave のハンドラは、ポインタがテーブル全体に出入りする場合にのみトリガーされます。その内部の遷移に関する情報を取得することはできません。

そこで、mouseover/mouseout を使用しましょう。

マウスの下にある要素を強調表示する単純なハンドラから始めましょう

// let's highlight an element under the pointer
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
};

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

それらが動作している様子です.マウスがこのテーブルの要素上を移動すると、現在の要素が強調表示されます

結果
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>

この場合、テーブルセル <td> 間の遷移(セルへの進入とセルからの離脱)を処理します。セル内やセル外の他の遷移には関心がありません。それらをフィルタリングしましょう.

私たちができることは次のとおりです

  • 現在強調表示されている <td> を変数に記憶します。それを currentElem と呼ぶことにしましょう.
  • mouseover では、現在の <td> 内にいる場合はイベントを無視します。
  • mouseout では、現在の <td> から離れていない場合は無視します.

考えられるすべての状況を考慮したコードの例を次に示します

// <td> under the mouse right now (if any)
let currentElem = null;

table.onmouseover = function(event) {
  // before entering a new element, the mouse always leaves the previous one
  // if currentElem is set, we didn't leave the previous <td>,
  // that's a mouseover inside it, ignore the event
  if (currentElem) return;

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

  // we moved not into a <td> - ignore
  if (!target) return;

  // moved into <td>, but outside of our table (possible in case of nested tables)
  // ignore
  if (!table.contains(target)) return;

  // hooray! we entered a new <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function(event) {
  // if we're outside of any <td> now, then ignore the event
  // that's probably a move inside the table, but out of <td>,
  // e.g. from <tr> to another <tr>
  if (!currentElem) return;

  // we're leaving the element – where to? Maybe to a descendant?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // go up the parent chain and check – if we're still inside currentElem
    // then that's an internal transition – ignore it
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // we left the <td>. really.
  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;
}

もう一度、重要な機能は次のとおりです

  1. イベント委任を使用して、テーブル内の任意の <td> の進入/離脱を処理します。そのため、バブルせず、委任を許可しない mouseenter/leave の代わりに mouseover/out に依存しています。
  2. <td> の子孫間を移動するなどの余分なイベントはフィルタリングされるため、ポインタが <td> 全体から離れるか入るときにのみ onEnter/Leave が実行されます.

すべての詳細を含む完全な例を次に示します

結果
script.js
style.css
index.html
// <td> under the mouse right now (if any)
let currentElem = null;

table.onmouseover = function(event) {
  // before entering a new element, the mouse always leaves the previous one
  // if currentElem is set, we didn't leave the previous <td>,
  // that's a mouseover inside it, ignore the event
  if (currentElem) return;

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

  // we moved not into a <td> - ignore
  if (!target) return;

  // moved into <td>, but outside of our table (possible in case of nested tables)
  // ignore
  if (!table.contains(target)) return;

  // hooray! we entered a new <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function(event) {
  // if we're outside of any <td> now, then ignore the event
  // that's probably a move inside the table, but out of <td>,
  // e.g. from <tr> to another <tr>
  if (!currentElem) return;

  // we're leaving the element – where to? Maybe to a descendant?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // go up the parent chain and check – if we're still inside currentElem
    // then that's an internal transition – ignore it
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // we left the <td>. really.
  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>

テーブルセルの内外にカーソルを移動してみてください。速くても遅くても関係ありません。前の例とは異なり、<td> 全体のみが強調表示されます.

まとめ

イベント mouseovermouseoutmousemovemouseentermouseleave について説明しました。

次の点に注意してください

  • マウスの動きが速いと、中間要素がスキップされる可能性があります。
  • イベントmouseover/outmouseenter/leaveには、追加のプロパティrelatedTargetがあります。これは、targetを補完する、マウスが移動元/移動先の要素です。

イベントmouseover/outは、親要素から子要素に移動した場合でも発生します。ブラウザは、マウスが一度に1つの要素(最も深い要素)のみに重なっていると想定します。

イベントmouseenter/leaveはその点で異なります。マウスが要素全体に出入りしたときのみ発生します。また、バブリングしません。

タスク

重要度: 5

属性data-tooltipを持つ要素の上にツールチップを表示するJavaScriptを書いてください。この属性の値がツールチップのテキストになります。

これはタスクツールチップの動作と似ていますが、ここでは注釈付き要素を入れ子にすることができます。最も深くネストされたツールチップが表示されます。

同時に表示できるツールチップは1つだけです。

例えば

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

iframeの結果

タスクのサンドボックスを開く。

重要度: 5

訪問者がマウスを要素の*上*に移動させた場合にのみツールチップを表示し、*通過*させた場合は表示しない関数を作成してください。

言い換えれば、訪問者がマウスを要素に移動してそこで停止した場合 - ツールチップを表示します。ただマウスを通過させただけなら、不要です。誰が余計な点滅を望むでしょうか?

技術的には、要素上でのマウスの速度を測定し、速度が遅い場合は「要素上」に来たと想定してツールチップを表示し、速度が速い場合は無視することができます。

そのため、汎用オブジェクトnew HoverIntent(options)を作成します。

そのoptions

  • elem - 追跡する要素。
  • over - マウスが要素に到達した場合に呼び出す関数:つまり、ゆっくり移動するか、要素上で停止した場合です。
  • out - マウスが要素から離れたときに呼び出す関数(overが呼び出された場合)。

ツールチップにそのようなオブジェクトを使用する例

// a sample tooltip
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";

// the object will track mouse and call 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();
  }
});

デモ

マウスを「時計」の上にすばやく移動させると何も起こらず、ゆっくり移動させたり、時計の上で停止させたりすると、ツールチップが表示されます。

注意:カーソルが時計のサブ要素間を移動しても、ツールチップは「点滅」しません。

テスト付きのサンドボックスを開く。

アルゴリズムは単純に見えます

  1. 要素にonmouseover/outハンドラーを配置します。ここではonmouseenter/leaveも使用できますが、汎用性が低く、委任を導入すると機能しません。
  2. マウスカーソルが要素に入ったときに、mousemoveで速度の測定を開始します。
  3. 速度が遅い場合は、overを実行します。
  4. 要素から出ていくときに、overが実行された場合は、outを実行します。

しかし、速度をどのように測定するのでしょうか?

最初のアイデアは、100msごとに関数を実行し、以前の座標と新しい座標の間の距離を測定することです。距離が小さい場合、速度は遅いです。

残念ながら、JavaScriptで「現在のマウス座標」を取得する方法はありません。getCurrentMouseCoordinates()のような関数はありません。

座標を取得する唯一の方法は、mousemoveなどのマウスイベントをリッスンし、イベントオブジェクトから座標を取得することです。

そこで、mousemoveにハンドラーを設定して座標を追跡し、記憶させます。そして、100msごとにそれらを比較します。

追伸 解答テストでは、ツールチップが正しく機能するかどうかを確認するためにdispatchEventを使用していることに注意してください。

テスト付きの解答をサンドボックスで開く。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善のための提案がある場合は、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • いくつかのコードを挿入するには、<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepenなど)を使用してください。