Как я могу обнаружить щелчок за пределами элемента? - PullRequest
2251 голосов
/ 30 сентября 2008

У меня есть несколько HTML-меню, которые отображаются полностью, когда пользователь нажимает на заголовок этих меню. Я хотел бы скрыть эти элементы, когда пользователь щелкает за пределами области меню.

Возможно ли что-то подобное с jQuery?

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

Ответы [ 76 ]

1709 голосов
/ 30 сентября 2008

ПРИМЕЧАНИЕ. Использование stopEventPropagation() - это то, чего следует избегать, поскольку оно нарушает нормальный поток событий в DOM. См. эту статью для получения дополнительной информации. Попробуйте использовать этот метод вместо

Прикрепить событие щелчка к телу документа, которое закрывает окно. Прикрепите отдельное событие щелчка к контейнеру, которое останавливает распространение в теле документа.

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

$('#menucontainer').click(function(event){
    event.stopPropagation();
});
1261 голосов
/ 12 июня 2010

Вы можете прослушать событие click на document, а затем убедиться, что #menucontainer не является предком или целью выбранного элемента, используя .closest().

Если это не так, то выбранный элемент находится за пределами #menucontainer, и его можно смело скрывать.

$(document).click(function(event) { 
  $target = $(event.target);
  if(!$target.closest('#menucontainer').length && 
  $('#menucontainer').is(":visible")) {
    $('#menucontainer').hide();
  }        
});

Редактировать - 2017-06-23

Вы также можете очистить после прослушивателя событий, если вы планируете закрыть меню и хотите прекратить прослушивание событий. Эта функция будет очищать только недавно созданного прослушивателя, сохраняя все остальные прослушиватели щелчков на document. С синтаксисом ES2015:

export function hideOnClickOutside(selector) {
  const outsideClickListener = (event) => {
    $target = $(event.target);
    if (!$target.closest(selector).length && $(selector).is(':visible')) {
        $(selector).hide();
        removeClickListener();
    }
  }

  const removeClickListener = () => {
    document.removeEventListener('click', outsideClickListener)
  }

  document.addEventListener('click', outsideClickListener)
}

Редактировать - 2018-03-11

Для тех, кто не хочет использовать jQuery. Вот приведенный выше код на простом vanillaJS (ECMAScript6).

function hideOnClickOutside(element) {
    const outsideClickListener = event => {
        if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
          element.style.display = 'none'
          removeClickListener()
        }
    }

    const removeClickListener = () => {
        document.removeEventListener('click', outsideClickListener)
    }

    document.addEventListener('click', outsideClickListener)
}

const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ) // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js 

Примечание: Это основано на комментарии Алекса, чтобы просто использовать !element.contains(event.target) вместо части jQuery.

Но element.closest() теперь также доступен во всех основных браузерах (версия W3C немного отличается от версии jQuery). Полифилы можно найти здесь: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest

245 голосов
/ 12 июля 2016

Как обнаружить щелчок снаружи элемента?

Причина, по которой этот вопрос так популярен и имеет так много ответов, заключается в том, что он обманчиво сложен. После почти восьми лет и десятков ответов я искренне удивлен, увидев, как мало внимания уделяется доступности.

Я хотел бы скрыть эти элементы, когда пользователь щелкает за пределами области меню.

Это благородная причина и является актуальной проблемой. Название вопроса & ndash; как представляется, большинство ответов пытается ответить на него, содержит неудачную красную сельдь.

Подсказка: это слово "щелчок" !

На самом деле вы не хотите связывать обработчики кликов.

Если вы связываете обработчики щелчков, чтобы закрыть диалоговое окно, вы уже потерпели неудачу. Причина, по которой вы потерпели неудачу, заключается в том, что не все запускают click события. Пользователи, не использующие мышь, смогут выйти из вашего диалога (и ваше всплывающее меню, возможно, является типом диалога), нажав Tab , и они не смогут читать содержимое, стоящее за диалоговое окно без последующего запуска события click.

Итак, давайте перефразируем вопрос.

Как закрыть диалоговое окно, когда пользователь покончил с ним?

Это цель. К сожалению, теперь нам нужно связать событие userisfinishedwiththedialog, и это связывание не так просто.

Итак, как мы можем определить, что пользователь закончил использовать диалог?

focusout событие

Хорошее начало - определить, покинул ли фокус диалог.

Подсказка: будьте осторожны с событием blur, blur не распространяется, если событие было привязано к фазе барботирования!

jQuery's 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(...) или 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 , однако я 1138 * весьма рекомендует, чтобы разработчики обращались к спецификации за подробностями о том, какие роли они должны использовать, и о любых других соответствующих атрибутах.

136 голосов
/ 07 июля 2009

Другие решения здесь не сработали, поэтому мне пришлось использовать:

if(!$(event.target).is('#foo'))
{
    // hide menu
}
123 голосов
/ 30 сентября 2008

У меня есть приложение, которое работает аналогично примеру Эрана, за исключением того, что я прикрепляю событие click к телу при открытии меню ... Вроде как:

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

  event.stopPropagation();
});

Дополнительная информация о функции jQuery one()

38 голосов
/ 17 ноября 2009
$("#menuscontainer").click(function() {
    $(this).focus();
});
$("#menuscontainer").blur(function(){
    $(this).hide();
});

У меня работает просто отлично.

37 голосов
/ 05 апреля 2010

Теперь для этого есть плагин: внешние события ( сообщение в блоге )

Следующее происходит, когда обработчик clickoutside (WLOG) привязан к элементу:

  • элемент добавляется в массив, который содержит все элементы с clickoutside обработчиками
  • a ( namespaced ) click Обработчик привязан к документу (если его там еще нет)
  • при любом щелчке в документе событие clickoutside * запускается для тех элементов в этом массиве, которые не равны или являются родительскими для click мишень событий
  • дополнительно, event.target для события clickoutside устанавливается на элемент, на который нажал пользователь (так что вы даже знаете, что пользователь нажал, а не только то, что он нажал снаружи)

Таким образом, никакие события не останавливаются от распространения, и дополнительные обработчики click могут использоваться "над" элементом с внешним обработчиком.

36 голосов
/ 02 ноября 2015

После исследования я нашел три рабочих решения (я забыл ссылки на страницы для справки)

Первое решение

<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>
30 голосов
/ 04 июня 2012

Это отлично сработало для меня !!

$('html').click(function (e) {
    if (e.target.id == 'YOUR-DIV-ID') {
        //do something
    } else {
        //do something
    }
});
25 голосов
/ 19 мая 2011

Не думаю, что вам действительно нужно закрывать меню, когда пользователь нажимает снаружи; вам нужно, чтобы меню закрывалось, когда пользователь щелкает в любом месте на странице. Если вы нажмете на меню, или от меню оно должно закрыться правильно?

Не найдя удовлетворительных ответов выше, я предложил написать в этом блоге на днях. Для более педантичных есть несколько уловок, на которые стоит обратить внимание:

  1. Если вы прикрепляете обработчик события click к элементу body во время щелчка, обязательно дождитесь второго щелчка, прежде чем закрыть меню и отменить привязку события. В противном случае событие щелчка, открывшее меню, будет пузыриться до слушателя, который должен закрыть меню.
  2. Если вы используете event.stopPropogation () для события щелчка, никакие другие элементы на вашей странице не могут иметь функцию щелчка в любом месте, чтобы закрыть ее.
  3. Добавление обработчика события click к элементу body на неопределенный срок не является эффективным решением
  4. Сравнение цели события и его родителей с создателем обработчика предполагает, что вы хотите закрыть меню, когда вы щелкаете по нему, когда то, что вы действительно хотите, это закрыть его, когда вы щелкаете в любом месте страницы.
  5. Прослушивание событий в элементе body сделает ваш код более хрупким. Стиль, столь невинный, как это сломало бы это: body { margin-left:auto; margin-right: auto; width:960px;}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...