マウスが要素間を移動するときに起こるイベントについての詳細を見ていきましょう。
Mouseover/mouseout, relatedTarget
mouseoever
イベントはマウスポインタが要素の上に来るときに発生し、mouseout
は – そこを離れるときです。
これらのイベントは relatedTarget
を持っているという点で特別です。
mouseover
の場合:
event.target
– はマウスが来た要素です。event.relatedTarget
– は、マウスが来た元の要素です(どこから来たか)。
mouseout
の場合はその逆です:
event.target
– はマウスが離れた要素です。event.relatedTarget
– は新たなポインタの下の要素です(マウスが向かった要素)
下の例では、それぞれの顔が要素です。マウスを移動させると、テキストエリアでイベントが見えます。
各イベントは要素が来た場所や、どこから来たかについての情報を持っています。
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
は null
の可能性がありますrelatedTarget
プロパティは null
の場合があります。
それは正常なことで、単にマウスが別の要素から来たのではなく、ウィンドウの外から来たことを意味します。もしくはウィンドウから出たことを意味します。
我々のコードで event.relatedTarget
を使うときは,その可能性を心に留めておく必要があります。もし event.relatedTarget.tagName
へアクセスすると、エラーになるでしょう。
イベントの頻度
mousemove
イベントはマウスの移動時にトリガされます。しかし、すべてのピクセル単位の移動でイベントが発生する訳ではありません。
ブラウザは時々マウスの位置をチェックします。そして、もし変更に気づいた場合、イベントをトリガします。
つまり、訪問者がマウスをとても速く動かしている場合、DOM 要素はスキップされる可能性があることを意味します。:
もしもマウスが上に書いているように、 #FROM
から #TO
要素へ非常に速く移動する場合、間にある <div>
(やそれら) はスキップされる可能性があります。mouseout
イベントは #FROM
でトリガし、その後 #TO
ですぐに mouseover
をトリガするかもしれません。
実際には、これは間に多くの要素がある場合に役立ちます。 私たちは本当にそれぞれのIn/Outを処理したくはありません。
その反面、マウスがあるイベントから別のイベントへゆっくり移動することは想定できないことに留意する必要があります。そうではなく、それは “ジャンプ” できます。
特に、ウィンドウの外からページ中央にカーソルが移動することもあり得ます。そして、それは “どこからも” 来ていないので、relatedTarget=null
です。:
下のテストスタンドで、実際に確認してみてください。
HTMLは2つのネストされた <div>
要素です。もしマウスをすばやく移動させると、イベントはまったく起きないかもしれません。もしくは赤の div だけ、緑の div だけがイベントをトリガするかもしれません。
また、赤の div
にポインタを移動させ、すばやく緑の div
を通って下に移動してみてください。移動が十分速い場合、親要素は無視されます。
green.onmouseover = green.onmouseout = green.onmousemove = handler;
function handler(event) {
let type = event.type;
while (type < 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;
}
#green {
height: 50px;
width: 160px;
background: green;
}
#red {
height: 20px;
width: 110px;
background: red;
color: white;
font-weight: bold;
padding: 5px;
text-align: center;
margin: 20px;
}
#text {
font-size: 12px;
height: 200px;
width: 360px;
display: block;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="green">
<div id="red">Test</div>
</div>
<input onclick="clearText()" value="Clear" type="button">
<textarea id="text"></textarea>
<script src="script.js"></script>
</body>
</html>
子へ向けて移動するときの “余分な” mouseout
想像してください – マウスポインタが要素に入りました。mouseover
がトリガされました。その後、カーソルが子要素へ行きます。興味深いことは mouseout
がその場合にトリガすることです。カーソルは依然として要素の中にありますが、mouseout
が起きます!
奇妙に見えますが、簡単に説明する事ができます。
ブラウザのロジックによれば、マウスカーソルは常に 単一の 要素 – 最もネストされた要素(及び z-index がトップ) – の上にだけあります。
したがって、別の要素(子孫だとしても)へ行く場合は前の要素を離れます。シンプルです。
下の例で見ることができる面白い結果があります。
赤の <div>
は青の <div>
にネストされています。青の <div>
は以下のテキストにすべてのイベントを記録する mouseover/out
ハンドラを持っています。
青要素に入って、次に赤要素にマウスを移動させてみてください – そしてイベントを見てください。:
function mouselog(event) {
text.value += event.type + ' [target: ' + event.target.className + ']\n'
text.scrollTop = text.scrollHeight
}
.blue {
background: blue;
width: 160px;
height: 160px;
position: relative;
}
.red {
background: red;
width: 100px;
height: 100px;
position: absolute;
left: 30px;
top: 30px;
}
textarea {
height: 100px;
width: 400px;
display: block;
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="blue" onmouseover="mouselog(event)" onmouseout="mouselog(event)">
<div class="red"></div>
</div>
<textarea id="text"></textarea>
<input type="button" onclick="text.value=''" value="Clear">
<script src="script.js"></script>
</body>
</html>
- 青要素に入ると –
mouseover [target: blue]
を得ます。 - 次に、青から赤要素へ移動した後、 –
mouseout [target: blue]
を得ます(親を離れます)。 - …そしてすぐに
mouseover [target: red]
です。
なので、target
を考慮しないハンドラでは、(2)
の mouseout
で親を離れ、(3)
の mouseover
でそこへ戻ってきたように見えます。
要素の出入りの際にいくつかのアクションを実行する場合、多くの余分な “偽の” 実行が発生します。シンプルな物事に対して気づかない可能性があります。複雑な物事に対しては、望ましくない副作用を引き起こす可能性があります。
私たちは、代わりに mouseenter/mouseleave
イベントを使用して修正できます。
イベント mouseenter と mouseleave
イベント mouseenter/mouseleave
は mouseover/mouseout
のようなものです。それらもマウスポインタが要素を出入りするときにトリガされます。
違いが2つあります。:
- 要素内の遷移はカウントされません。
- イベント
mouseenter/mouseleave
はバブルしません。
これらのイベントは直感的に非常に明確です。
ポインタが要素に入るとき – mouseenter
をトリガし、次に要素内でどこに行こうと関係はありません。mouseleave
イベントはカーソルがそこを離れるときにだけトリガします。
同じ例を作りますが、青の <div>
に mouseenter/mouseleave
を置き、同じことをすると – 青の <div>
を入ったり出たりするときのみイベントをトリガするのが分かります。赤の <div>
に行くときや戻るときに余分なイベントはありません。子は無視されます。
function log(event) {
text.value += event.type + ' [target: ' + event.target.id + ']\n';
text.scrollTop = text.scrollHeight;
}
#blue {
background: blue;
width: 160px;
height: 160px;
position: relative;
}
#red {
background: red;
width: 70px;
height: 70px;
position: absolute;
left: 45px;
top: 45px;
}
#text {
display: block;
height: 100px;
width: 400px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="blue" onmouseenter="log(event)" onmouseleave="log(event)">
<div id="red"></div>
</div>
<textarea id="text"></textarea>
<input type="button" onclick="text.value=''" value="Clear">
<script src="script.js"></script>
</body>
</html>
イベント移譲 [$Event delegation]
イベント mouseenter/leave
は非常にシンプルで使いやすいです。しかし、それらはバブルしません。そのため、それらにイベント移譲を使えません。
テーブルセルに対してマウスの出入りを処理したいと想像してください。そして、何百ものセルがあります。
自然な解決策は – <table>
にハンドラを設定し、そこでイベントを処理することです。しかし mouseenter/leave
はバブルしません。したがって、<td>
でこのようなイベントが起きる場合、その <td>
のハンドラだけがそのイベントをキャッチできます。
<table>
上の mouseenter/leave
に対するハンドラは、テーブル全体の出入りでのみトリガします。その内側の遷移に関する情報を取得することはできません。
問題はありません – mouseover/mouseout
を使ってみましょう。
次のようなシンプルなハンドラがあります:
// マウスの下にあるセルをハイライトしましょう
table.onmouseover = function(event) {
let target = event.target;
target.style.background = 'pink';
};
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
};
table.onmouseover = function(event) {
let target = event.target;
target.style.background = 'pink';
text.value += "mouseover " + target.tagName + "\n";
text.scrollTop = text.scrollHeight;
};
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
text.value += "mouseout " + 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;
}
#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>
の子の間で起こる遷移を処理したくありません。
解決策の1つは次のようになります:
- 変数で、現在強調されている
<td>
を覚えます。 mouseover
では – まだ現在の<td>
の中にいる場合はイベントを無視します。mouseout
では – 現在の<td>
を離れなかった場合には無視します。
それは、<td>
の子の間を移動するときの “余分な” イベントをフィルタします。
すべての詳細を含む完全な例を次に示します。:
// <td> under the mouse right now (if any)
let currentElem = null;
table.onmouseover = function(event) {
if (currentElem) {
// before entering a new element, the mouse always leaves the previous one
// if we didn't leave <td> yet, then we're still inside it, so can ignore the event
return;
}
let target = event.target.closest('td');
if (!target || !table.contains(target)) return;
// yeah we're inside <td> now
currentElem = target;
target.style.background = 'pink';
};
table.onmouseout = function(event) {
// if we're outside of any <td> now, then ignore the event
if (!currentElem) return;
// we're leaving the element -- where to? Maybe to a child element?
let relatedTarget = event.relatedTarget;
if (relatedTarget) { // possible: relatedTarget = null
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 element. really.
currentElem.style.background = '';
currentElem = null;
};
#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;
}
#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>
<script src="script.js"></script>
</body>
</html>
カーソルを、テーブルセルやその内側の内外に移動させてみてください。以前の例とは異なり、全体として <td>
だけが強調表示されています。
サマリ
私たちはイベント mouseover
, mouseout
, mousemove
, mouseenter
と mouseleave
を説明しました。
注目すべきことは:
- 速いマウス移動は
mouseover, mousemove, mouseout
に対し、中間要素をスキップすることができます。 - イベント
mouseover/out
とmouserenter/leave
はrelatedTarget
という追加のターゲットを持っています。それは私たちが 来た/行く 要素であり、target
と相補的な要素です。 - イベント
mouseover/out
は親要素から子要素に移動してもトリガされます。 マウスは、一度に1つの要素、つまり最も深い要素を想定します。 - イベント
mouserenter/leave
はバブルしないので、マウスが子要素に行くときにはトリガしません。それらは、マウスが要素全体の内側と外側のどちらに来るのかを追跡します。
コメント
<code>
タグを使ってください。複数行の場合は<pre>
を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。