javascript - tutorial - jquery教學




如何檢測元素外的點擊? (20)

我有一些HTML菜單,當用戶點擊這些菜單的頭部時,我會完全顯示。 當用戶點擊菜單區域外時,我想隱藏這些元素。

jQuery可以這樣嗎?

$("#menuscontainer").clickOutsideThisElement(function() {
    // Hide the menus
});

如何檢測元素外的點擊?

這個問題如此受歡迎並且答案如此之多的原因在於它看起來很複雜。 經過近八年的時間和幾十個答案,我真的很驚訝地看到對可訪問性的關注度很低。

當用戶點擊菜單區域外時,我想隱藏這些元素。

這是一個崇高的事業,也是實際問題。 問題的標題 - 這是大多數答案似乎試圖解決的問題 - 包含一個不幸的紅鯡魚。

提示:這是“點擊”這個詞!

您實際上並不想綁定點擊處理程序。

如果您綁定了點擊處理程序以關閉對話框,那麼您已經失敗了。 你失敗的原因是不是每個人都會觸發click事件。 通過按Tab鍵 ,不使用鼠標的用戶將能夠轉義對話框(並且您的彈出菜單可以說是一種對話框),然後他們將無法在不隨後觸發click情況下讀取對話框後面的內容事件。

讓我們重新解釋一下這個問題。

如何在用戶完成對話時關閉對話框?

這是目標。 不幸的是,現在我們需要綁定userisfinishedwiththedialog事件,並且綁定不是那麼簡單。

那麼我們怎樣才能檢測到用戶已經完成了對話框的使用?

focusout事件

一個好的開始是確定焦點是否已離開對話框。

提示:小心blur事件,如果事件被綁定到冒泡階段, blur不會傳播!

jQuery的focusout會很好。 如果你不能使用jQuery,那麼你可以在捕獲階段使用blur

element.addEventListener('blur', ..., true);
//                       use capture: ^^^^

此外,對於許多對話框,您需要允許容器獲得焦點。 添加tabindex="-1"以允許對話框動態接收焦點,而不會中斷標籤流。

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on('focusout', function () {
  $(this).removeClass('active');
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

如果您使用該演示超過一分鐘,您應該很快就會看到問題。

第一個是對話框中的鏈接不可點擊。 嘗試單擊它或選項卡到它將導致對話關閉在交互發生之前。 這是因為聚焦內部元素會在再次觸發focusout事件之前觸發focusin事件。

修復是在事件循環上對狀態更改進行排隊。 這可以通過對不支持setImmediate瀏覽器使用setImmediate(...)setTimeout(..., 0)setImmediate 。 排隊後,可以通過後續focusin取消:

$('.submenu').on({
  focusout: function (e) {
    $(this).data('submenuTimer', setTimeout(function () {
      $(this).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function (e) {
    clearTimeout($(this).data('submenuTimer'));
  }
});

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  }
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

第二個問題是再次按下鏈接時對話框不會關閉。 這是因為對話框失去焦點,觸發關閉行為,之後鏈接單擊觸發對話框重新打開。

與前一個問題類似,需要管理焦點狀態。 鑑於狀態更改已經排隊,只需在對話框觸發器上處理焦點事件:

這應該看起來很熟悉
$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  }
});

$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

Esc

如果您認為自己是通過處理焦點狀態來完成的,那麼您可以採取更多措施來簡化用戶體驗。

這通常是一個“很好的”功能,但是當你有任何類型的模態或彈出窗口時, Esc鍵會將其關閉,這是很常見的。

keydown: function (e) {
  if (e.which === 27) {
    $(this).removeClass('active');
    e.preventDefault();
  }
}

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  },
  keydown: function (e) {
    if (e.which === 27) {
      $(this).removeClass('active');
      e.preventDefault();
    }
  }
});

$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

如果您知道對話框中有可聚焦元素,則無需直接對焦對話框。 如果您正在構建菜單,則可以改為聚焦第一個菜單項。

click: function (e) {
  $(this.hash)
    .toggleClass('submenu--active')
    .find('a:first')
    .focus();
  e.preventDefault();
}

$('.menu__link').on({
  click: function (e) {
    $(this.hash)
      .toggleClass('submenu--active')
      .find('a:first')
      .focus();
    e.preventDefault();
  },
  focusout: function () {
    $(this.hash).data('submenuTimer', setTimeout(function () {
      $(this.hash).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('submenuTimer'));  
  }
});

$('.submenu').on({
  focusout: function () {
    $(this).data('submenuTimer', setTimeout(function () {
      $(this).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('submenuTimer'));
  },
  keydown: function (e) {
    if (e.which === 27) {
      $(this).removeClass('submenu--active');
      e.preventDefault();
    }
  }
});
.menu {
  list-style: none;
  margin: 0;
  padding: 0;
}
.menu:after {
  clear: both;
  content: '';
  display: table;
}
.menu__item {
  float: left;
  position: relative;
}

.menu__link {
  background-color: lightblue;
  color: black;
  display: block;
  padding: 0.5em 1em;
  text-decoration: none;
}
.menu__link:hover,
.menu__link:focus {
  background-color: black;
  color: lightblue;
}

.submenu {
  border: 1px solid black;
  display: none;
  left: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  position: absolute;
  top: 100%;
}
.submenu--active {
  display: block;
}

.submenu__item {
  width: 150px;
}

.submenu__link {
  background-color: lightblue;
  color: black;
  display: block;
  padding: 0.5em 1em;
  text-decoration: none;
}

.submenu__link:hover,
.submenu__link:focus {
  background-color: black;
  color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="menu">
  <li class="menu__item">
    <a class="menu__link" href="#menu-1">Menu 1</a>
    <ul class="submenu" id="menu-1" tabindex="-1">
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
    </ul>
  </li>
  <li class="menu__item">
    <a  class="menu__link" href="#menu-2">Menu 2</a>
    <ul class="submenu" id="menu-2" tabindex="-1">
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
    </ul>
  </li>
</ul>
lorem ipsum <a href="http://example.com/">dolor</a> sit amet.

WAI-ARIA角色和其他輔助功能支持

這個答案有希望涵蓋這個功能的可訪問鍵盤和鼠標支持的基礎知識,但由於它已經相當大,我將避免任何關於WAI-ARIA角色和屬性的討論,但我強烈建議實施者參考規範了解詳情他們應該使用什麼角色以及任何其他適當的屬性。


注意:使用stopEventPropagation()是應該避免的,因為它會破壞DOM中的正常事件流。 有關更多信息,請參閱此文章 。 請考慮使用此方法 。

將單擊事件附加到關閉窗口的文檔正文。 將單獨的單擊事件附加到容器,該容器將停止傳播到文檔正文。

$(window).click(function() {
//Hide the menus if visible
});

$('#menucontainer').click(function(event){
    event.stopPropagation();
});

Upvote為最流行的答案,但添加

&& (e.target != $('html').get(0)) // ignore the scrollbar

因此,單擊滾動條不會[隱藏或任何]目標元素。


作為變種:

var $menu = $('#menucontainer');
$(document).on('click', function (e) {

    // If element is opened and click target is outside it, hide it
    if ($menu.is(':visible') && !$menu.is(e.target) && !$menu.has(e.target).length) {
        $menu.hide();
    }
});

css-tricks.com/dangers-stopping-event-propagation沒有問題,並且更好地支持同一頁面上的多個菜單,在第一個打開時單擊第二個菜單將在stopPropagation解決方案中保留第一個菜單。


功能:

$(function() {
    $.fn.click_inout = function(clickin_handler, clickout_handler) {
        var item = this;
        var is_me = false;
        item.click(function(event) {
            clickin_handler(event);
            is_me = true;
        });
        $(document).click(function(event) {
            if (is_me) {
                is_me = false;
            } else {
                clickout_handler(event);
            }
        });
        return this;
    }
});

用法:

this.input = $('<input>')
    .click_inout(
        function(event) { me.ShowTree(event); },
        function() { me.Hide(); }
    )
    .appendTo(this.node);

功能很簡單:

ShowTree: function(event) {
    this.data_span.show();
}
Hide: function() {
    this.data_span.hide();
}

如果您正在編寫IE和FF 3. *的腳本,並且您只想知道某個框區域內是否發生了單擊,您還可以使用以下內容:

this.outsideElementClick = function(objEvent, objElement){   
var objCurrentElement = objEvent.target || objEvent.srcElement;
var blnInsideX = false;
var blnInsideY = false;

if (objCurrentElement.getBoundingClientRect().left >= objElement.getBoundingClientRect().left && objCurrentElement.getBoundingClientRect().right <= objElement.getBoundingClientRect().right)
    blnInsideX = true;

if (objCurrentElement.getBoundingClientRect().top >= objElement.getBoundingClientRect().top && objCurrentElement.getBoundingClientRect().bottom <= objElement.getBoundingClientRect().bottom)
    blnInsideY = true;

if (blnInsideX && blnInsideY)
    return false;
else
    return true;}

我不認為你真正需要的是當用戶點擊外面時關閉菜單; 您需要的是當用戶點擊頁面上的任何位置時關閉菜單。 如果您單擊菜單,或關閉菜單,它應該關閉嗎?

上面沒有找到滿意的答案促使我前幾天寫這篇博文 。 對於更迂腐的人來說,有許多值得注意的問題:

  1. 如果在單擊時將click事件處理程序附加到body元素,請確保在關閉菜單之前等待第二次單擊,並取消綁定事件。 否則,打開菜單的click事件將冒泡到必須關閉菜單的偵聽器。
  2. 如果對click事件使用event.stopPropogation(),則頁面中沒有其他元素可以具有click-anywhere-to-close功能。
  3. 將click事件處理程序無限期地附加到body元素不是一個高性能的解決方案
  4. 將事件的目標及其父項與處理程序的創建者進行比較假定您單擊它時關閉菜單,當您單擊頁面上的任何位置時,您真正想要的是關閉它。
  5. 在body元素上偵聽事件會使代碼更加脆弱。 造型像無辜一樣會破壞它: body { margin-left:auto; margin-right: auto; width:960px;} body { margin-left:auto; margin-right: auto; width:960px;}

我在一些jQuery日曆插件中找到了這個方法。

function ClickOutsideCheck(e)
{
  var el = e.target;
  var popup = $('.popup:visible')[0];
  if (popup==undefined)
    return true;

  while (true){
    if (el == popup ) {
      return true;
    } else if (el == document) {
      $(".popup").hide();
      return false;
    } else {
      el = $(el).parent()[0];
    }
  }
};

$(document).bind('mousedown.popup', ClickOutsideCheck);

我最終做了這樣的事情:

$(document).on('click', 'body, #msg_count_results .close',function() {
    $(document).find('#msg_count_results').remove();
});
$(document).on('click','#msg_count_results',function(e) {
    e.preventDefault();
    return false;
});

我在新容器中有一個關閉按鈕,用於最終用戶友好的UI用途。我不得不使用return false以便不通過。當然,在那裡有一個A HREF帶你到某個地方會很好,或者你可以調用一些ajax的東西。無論哪種方式,它對我都有效。正是我想要的。


我有一個類似於Eran示例的應用程序,除了我在打開菜單時將click事件附加到正文...有點像這樣:

$('#menucontainer').click(function(event) {
  $('html').one('click',function() {
    // Hide the menus
  });

  event.stopPropagation();
});

有關jQuery的one()函數的更多信息


正如另一張海報所說,有很多陷阱,特別是如果您正在顯示的元素(在這種情況下是菜單)具有交互元素。 我發現以下方法相當健壯:

$('#menuscontainer').click(function(event) {
    //your code that shows the menus fully

    //now set up an event listener so that clicking anywhere outside will close the menu
    $('html').click(function(event) {
        //check up the tree of the click target to check whether user has clicked outside of menu
        if ($(event.target).parents('#menuscontainer').length==0) {
            // your code to hide menu

            //this event listener has done its job so we can unbind it.
            $(this).unbind(event);
        }

    })
});

為了更容易使用,以及更具表現力的代碼,我為此創建了一個jQuery插件:

$('div.my-element').clickOut(function(target) { 
    //do something here... 
});

注意:target是用戶實際單擊的元素。但是回調仍然在原始元素的上下文中執行,因此您可以像在jQuery回調中所期望的那樣利用

插入:

$.fn.clickOut = function (parent, fn) {
    var context = this;
    fn = (typeof parent === 'function') ? parent : fn;
    parent = (parent instanceof jQuery) ? parent : $(document);

    context.each(function () {
        var that = this;
        parent.on('click', function (e) {
            var clicked = $(e.target);
            if (!clicked.is(that) && !clicked.parents().is(that)) {
                if (typeof fn === 'function') {
                    fn.call(that, clicked);
                }
            }
        });

    });
    return context;
};

默認情況下,單擊事件偵聽器放置在文檔上。但是,如果要限制事件偵聽器作用域,則可以傳入表示父級別元素的jQuery對象,該父級別元素將是將要偵聽單擊的頂級父級。這可以防止不必要的文檔級事件偵聽器。顯然,除非提供的父元素是初始元素的父元素,否則它將不起作用。

使用如下:

$('div.my-element').clickOut($('div.my-parent'), function(target) { 
    //do something here...
});

相反,使用流動中斷,模糊/焦點事件或任何其他棘手的技術,只需將事件流與元素的親緣關係匹配:

$(document).on("click.menu-outside", function(event){
    // Test if target and it's parent aren't #menuscontainer
    // That means the click event occur on other branch of document tree
    if(!$(event.target).parents().andSelf().is("#menuscontainer")){
        // Click outisde #menuscontainer
        // Hide the menus (but test if menus aren't already hidden)
    }
});

要刪除單擊外部事件偵聽器,只需:

$(document).off("click.menu-outside");

經過研究,我找到了三個有效的解決方案(我忘了頁面鏈接供參考)

第一解決方案

<script>
    //The good thing about this solution is it doesn't stop event propagation.

    var clickFlag = 0;
    $('body').on('click', function () {
        if(clickFlag == 0) {
            console.log('hide element here');
            /* Hide element here */
        }
        else {
            clickFlag=0;
        }
    });
    $('body').on('click','#testDiv', function (event) {
        clickFlag = 1;
        console.log('showed the element');
        /* Show the element */
    });
</script>

二解決方案

<script>
    $('body').on('click', function(e) {
        if($(e.target).closest('#testDiv').length == 0) {
           /* Hide dropdown here */
        }
    });
</script>

第三種方案

<script>
    var specifiedElement = document.getElementById('testDiv');
    document.addEventListener('click', function(event) {
        var isClickInside = specifiedElement.contains(event.target);
        if (isClickInside) {
          console.log('You clicked inside')
        }
        else {
          console.log('You clicked outside')
        }
    });
</script>

這是我解決這個問題的方法:

$(document).ready(function() {
  $('#user-toggle').click(function(e) {
    $('#user-nav').toggle();
    e.stopPropagation();
  });

  $('body').click(function() {
    $('#user-nav').hide(); 
  });

  $('#user-nav').click(function(e){
    e.stopPropagation();
  });
});

這是針對未來觀眾的vanilla JavaScript解決方案。

單擊文檔中的任何元素後,如果切換了單擊元素的id,或者未隱藏隱藏元素且隱藏元素不包含單擊元素,則切換元素。

(function () {
    "use strict";
    var hidden = document.getElementById('hidden');
    document.addEventListener('click', function (e) {
        if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
    }, false);
})();

(function () {
    "use strict";
    var hidden = document.getElementById('hidden');
    document.addEventListener('click', function (e) {
        if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
    }, false);
})();
<a href="javascript:void(0)" id="toggle">Toggle Hidden Div</a>
<div id="hidden" style="display: none;">This content is normally hidden. click anywhere other than this content to make me disappear</div>

如果您要在同一頁面上進行多次切換,可以使用以下內容:

  1. 將類名添加hidden到可折疊項。
  2. 單擊文檔後,關閉所有不包含單擊元素但未隱藏的隱藏元素
  3. 如果單擊的元素是切換,則切換指定的元素。

(function () {
    "use strict";
    var hiddenItems = document.getElementsByClassName('hidden'), hidden;
    document.addEventListener('click', function (e) {
        for (var i = 0; hidden = hiddenItems[i]; i++) {
            if (!hidden.contains(e.target) && hidden.style.display != 'none')
                hidden.style.display = 'none';
        }
        if (e.target.getAttribute('data-toggle')) {
            var toggle = document.querySelector(e.target.getAttribute('data-toggle'));
            toggle.style.display = toggle.style.display == 'none' ? 'block' : 'none';
        }
    }, false);
})();
<a href="javascript:void(0)" data-toggle="#hidden1">Toggle Hidden Div</a>
<div class="hidden" id="hidden1" style="display: none;" data-hidden="true">This content is normally hidden</div>
<a href="javascript:void(0)" data-toggle="#hidden2">Toggle Hidden Div</a>
<div class="hidden" id="hidden2" style="display: none;" data-hidden="true">This content is normally hidden</div>
<a href="javascript:void(0)" data-toggle="#hidden3">Toggle Hidden Div</a>
<div class="hidden" id="hidden3" style="display: none;" data-hidden="true">This content is normally hidden</div>


這裡的其他解決方案對我不起作用所以我不得不使用:

if(!$(event.target).is('#foo'))
{
    // hide menu
}

在文檔上掛鉤單擊事件偵聽器。在事件監聽器中,您可以查看事件對象,特別是event.target以查看單擊了哪個元素:

$(document).click(function(e){
    if ($(e.target).closest("#menuscontainer").length == 0) {
        // .closest can help you determine if the element 
        // or one of its ancestors is #menuscontainer
        console.log("hide");
    }
});

該事件有一個名為event.path的屬性,該屬性是“樹中所有祖先的靜態有序列表”。要檢查事件是源自特定DOM元素還是其中一個子元素,只需檢查該特定DOM元素的路徑。它還可以用於通過邏輯OR檢查some函數中的元素檢查來檢查多個元素。

$("body").click(function() {
  target = document.getElementById("main");
  flag = event.path.some(function(el, i, arr) {
    return (el == target)
  })
  if (flag) {
    console.log("Inside")
  } else {
    console.log("Outside")
  }
});
#main {
  display: inline-block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main">
  <ul>
    <li>Test-Main</li>
    <li>Test-Main</li>
    <li>Test-Main</li>
    <li>Test-Main</li>
    <li>Test-Main</li>
  </ul>
</div>
<div id="main2">
  Outside Main
</div>

所以對於你的情況它應該是

$("body").click(function() {
  target = $("#menuscontainer")[0];
  flag = event.path.some(function(el, i, arr) {
    return (el == target)
  });
  if (!flag) {
    // Hide the menus
  }
});

$("#menuscontainer").click(function() {
    $(this).focus();
});
$("#menuscontainer").blur(function(){
    $(this).hide();
});

對我來說就好了。





jquery