2012-07-03

在 jQuery 用 mouseenter 與 mouseleave 取代 mouseover 與 mouseout

一般常用的 mouse 進出物件的 Event type 為 mouseover(進入)與 mouseout(離開),但是有兩個難用的地方:
  • 沒有父層物件的概念
  • Event Bubbling
$(function(){
    $('div').mouseover(function(e){
        log(e);
    });
    $('div').mouseout(function(e){
        log(e);
    });
});
function log(e) {
    neil.log(e.type + '(T)' + e.target.tagName + '.' + e.target.id + '(C)' + e.currentTarget.tagName + '.' + e.currentTarget.id + '(R)' + e.relatedTarget.tagName + '.' + e.relatedTarget.id);
}
以上圖結構與程式測試,滑鼠從 body 進入 DIV.a、DIV.b 與 DIV.c,再反向出來,得到以下結果。

-- 進入 DIV.a
mouseover(T)DIV.a(C)DIV.a(R)BODY.

-- 進入 DIV.b
mouseout(T)DIV.a(C)DIV.a(R)DIV.b
mouseover(T)DIV.b(C)DIV.b(R)DIV.a
mouseover(T)DIV.b(C)DIV.a(R)DIV.a -- bubbling

-- 進入 DIV.c
mouseout(T)DIV.b(C)DIV.b(R)DIV.c
mouseout(T)DIV.b(C)DIV.a(R)DIV.c -- bubbling
mouseover(T)DIV.c(C)DIV.c(R)DIV.b
mouseover(T)DIV.c(C)DIV.b(R)DIV.b -- bubbling
mouseover(T)DIV.c(C)DIV.a(R)DIV.b -- bubbling

-- 離開 DIV.c
mouseout(T)DIV.c(C)DIV.c(R)DIV.b
mouseout(T)DIV.c(C)DIV.b(R)DIV.b -- bubbling
mouseout(T)DIV.c(C)DIV.a(R)DIV.b -- bubbling
mouseover(T)DIV.b(C)DIV.b(R)DIV.c
mouseover(T)DIV.b(C)DIV.a(R)DIV.c -- bubbling

-- 離開 DIV.b
mouseout(T)DIV.b(C)DIV.b(R)DIV.a
mouseout(T)DIV.b(C)DIV.a(R)DIV.a -- bubbling
mouseover(T)DIV.a(C)DIV.a(R)DIV.b

-- 離開 DIV.a
mouseout(T)DIV.a(C)DIV.a(R)BODY.

從 BODY 進入 DIV.a 比較沒有問題,奇怪的事情是從進入 DIV.b 開始,不只出現進入 DIV.b 的 mouseover,還出現離開 DIV.a 的 mouseout,離開 DIV.a 進入 DIV.b 嚴格來講是沒錯,但在實務運用上會造成一些困擾,畢竟還是在 DIV.a 裡面啊,另外還多了一個因為泡泡作用而出現的進入 DIV.a 的 mouseover。

好亂啊,多了一個離開 DIV.a,又多了一個進入 DIV.a,到底是進入 DIV.a 還是離開啊,雖然瀏覽器很聰明的知道要先呼叫 mouseout 再呼叫 mouseover,不然就真的離開 DIV.a 了。

接下來從 DIV.b 進入 DIV.c 又更精彩了,離開 DIV.b 進入 DIV.c 加上泡泡,很簡單的一個進入動作,結果產生 5 個 Event。

從 DIV.c 一路離開也是一樣的劇本再來一次,只是進入與離開動作對調吧了。

先來看看實務運用上的困擾,最常遇到的就是下拉選單,需求很簡單,游標移到「Menu」就彈出下拉選單,當游標移出下拉選單(橘色框)就關閉下拉選單。

先看 item 之間沒有留白的狀況。

// 顯示選單
$('div.menus').mouseover(function(e){
    $('div.menu').show();
});
// 隱藏選單
$('div.menu').mouseout(function(e){
    if ($(e.relatedTarget).parents('div.menu').length > 0) {
        return;
    }

    $(this).hide();
});
若沒有紅色部份的判斷,只要游標移到 item 裡,因為「進入a離開b」的邏輯,選單就會被隱藏,永遠也點不到 item。

當游標從下拉選單離開,可能是真的離開,也可能是移到下拉選單裡的 item,所以要避免後者的情況發生,除外邏輯就是去判斷離開選單進入的對象是不是選單的內層物件,若是,表示還在選單裡,這時就不可以隱藏選單,e.relatedTarget 指的就是「進入的對象」,然後用 parents() 判斷「進入的對象」與選單之間是否存在上下關係,若是則不隱藏表單。

再來看看 item 之間有留白的情況,與沒有留白的情況比較會多一個 Event 出現,就是從 menu item 離開進入 menu,進入 menu 沒事,問題出在離開 item 這個 Event 會因「泡泡作用」丟給 menu,導致 menu 多收到一個 mouseout,而之前加的除外邏輯必須為內外層關係才成立,而這時的 e.relatedTarget 指的就是 menu,所以除外邏輯派不上用場。

解決方法一,在除外邏輯加上 menu 的判斷。
$('div.menu').mouseout(function(e){
    if (e.relatedTarget == this || $(e.relatedTarget).parents('div.menu').length > 0) {
        return;
    }
    $(this).hide();
});
解決方法二,制止 item 丟給 menu
$('div.item').mouseout(function(e){
    e.stopPropagation();
    // or
    return false;

});

mouseenter 與 mouseleave

jQuery 帶來更好的解決方法:mouseenter 與 mouseleave,不會有進入 a 就得離開 b 的問題,也不會泡泡往外丟。
$('div.menus').mouseenter(function(e){
    $('div.menu').show();
});
$('div.menu').mouseleave(function(e){
    $(this).hide();
});
hover

jQuery 為 mouseenter 與 mouseleave 提供一個簡單的用法。
$('div.menus').hover(enterHandler, leaveHandler);

或者

$('div.menus').hover(enterAndLeaveHandler);

沒有留言:

張貼留言