Обнаружение щелчка вне элемента (обычный JavaScript)

85

Я везде искал хорошее решение, но не могу найти того, в котором не используется jQuery .

Есть ли кроссбраузерный, нормальный способ (без странных взломов или легко взломанного кода) для обнаружения щелчка вне элемента (который может иметь или не иметь дочерних элементов)?

Тибериу-Ионуй Стан
источник
Будет ли установка обработчика событий onclick в теге body сработать?
Benjamin Toueg 07

Ответы:

202

Добавьте прослушиватель событий documentи используйте Node.contains()его, чтобы определить, находится ли цель события (который является самым внутренним элементом, вызывающим щелчок) внутри указанного вами элемента. Работает даже в IE5

var specifiedElement = document.getElementById('a');

//I'm using "click" but it works with any event
document.addEventListener('click', function(event) {
  var isClickInside = specifiedElement.contains(event.target);

  if (!isClickInside) {
    //the click was outside the specifiedElement, do something
  }
});

Fregante
источник
14
Мне кажется, это самое элегантное решение.
Мариан
3
Это лучшее решение, которое я когда-либо видел.
HelloWorld
25

Вам необходимо обработать событие щелчка на уровне документа. В объекте события у вас есть targetсвойство, самый внутренний элемент DOM, на который был выполнен щелчок. С его помощью вы проверяете себя и подходите к его родителям до элемента документа, если один из них является вашим наблюдаемым элементом.

См. Пример на jsFiddle

document.addEventListener("click", function (e) {
  var level = 0;
  for (var element = e.target; element; element = element.parentNode) {
    if (element.id === 'x') {
      document.getElementById("out").innerHTML = (level ? "inner " : "") + "x clicked";
      return;
    }
    level++;
  }
  document.getElementById("out").innerHTML = "not x clicked";
});

Как всегда, из-за addEventListener / attachEvent это несовместимо с кросс-браузерами, но работает вот так.

Щелкается дочерний элемент, когда не event.target, но один из его родителей является наблюдаемым элементом (я просто считаю уровень для этого). У вас также может быть логическая переменная, если элемент найден или нет, чтобы не возвращать обработчик из предложения for. Мой пример ограничивается тем, что обработчик завершает работу только тогда, когда ничего не совпадает.

Добавляя кроссбраузерность, я обычно делаю это так:

var addEvent = function (element, eventName, fn, useCapture) {
  if (element.addEventListener) {
    element.addEventListener(eventName, fn, useCapture);
  }
  else if (element.attachEvent) {
    element.attachEvent(eventName, function (e) {
      fn.apply(element, arguments);
    }, useCapture);
  }
};

Это кроссбраузерный код для присоединения прослушивателя / обработчика событий, включая перезапись thisв IE, в качестве элемента, как это делает jQuery для своих обработчиков событий. Есть много аргументов, чтобы иметь в виду некоторые кусочки jQuery;)

метадинги
источник
4
Что, если пользователь щелкнет дочерний элемент элемента?
Tiberiu-Ionuț Stan
3
Используя это решение, вы проверяете targetсвойство (которое, как сказано в метаданных, будет самым внутренним элементом DOM) и все его родительские элементы. Либо вы дойдете до наблюдаемого элемента, что означает, что пользователь щелкнул внутри него, либо вы достигнете элемента документа, и в этом случае он / она щелкнет снаружи. В качестве альтернативы вы можете добавить два прослушивателя щелчка - один на элементе документа, а другой на наблюдаемом элементе, который использует event.stopPropagation()/, cancelBubbleчтобы прослушиватель на элементе документа запускался только щелчком вне наблюдаемого элемента.
JimmiTh 07
2
Это какое-то умное использование for. Из любопытства, есть ли причина не сравнивать элемент из переменной с целью в событии (с использованием строгого сравнения)?
Tiberiu-Ionuț Stan
@ Tiberiu-IonuțStan: да, если вы создаете эти «часы», используя ссылку вместо идентификатора «x», я предлагаю вам использовать element === watchedElement с тройным равенством.
метадинги
Ваше решение отлично работает. Это может быть личная проблема, но я предпочитаю ею поделиться. У меня такая же потребность, но нажатие кнопки показывает div, который, когда пользователь щелкает снаружи, должен быть скрыт. Проблема с этим решением в том, что div больше не отображается. Я разветвил ваш код для примера: jsfiddle.net/bx5hnL2h
Александр Шмидт
7

Как насчет этого:

jsBin демонстрация

document.onclick = function(event){
  var hasParent = false;
    for(var node = event.target; node != document.body; node = node.parentNode)
    {
      if(node.id == 'div1'){
        hasParent = true;
        break;
      }
    }
  if(hasParent)
    alert('inside');
  else
    alert('outside');
} 
Ахил Сехаран
источник
0

Чтобы скрыть элемент щелчком вне его, я обычно применяю такой простой код:

var bodyTag = document.getElementsByTagName('body');
var element = document.getElementById('element'); 
function clickedOrNot(e) {
	if (e.target !== element) {
		// action in the case of click outside 
		bodyTag[0].removeEventListener('click', clickedOrNot, true);
	}	
}
bodyTag[0].addEventListener('click', clickedOrNot, true);

Алекс Сид
источник
0

Еще один очень простой и быстрый подход к этой проблеме - сопоставить массив path с объектом события, возвращаемым слушателем. Если имя idили classвашего элемента совпадает с одним из названий в массиве, щелчок находится внутри вашего элемента.

(Это решение может быть полезно, если вы не хотите получать элемент напрямую (например: document.getElementById('...')например, в приложении reactjs / nextjs, в ssr ..).

Вот пример:

   document.addEventListener('click', e => {
      let clickedOutside = true;

      e.path.forEach(item => {
        if (!clickedOutside)
          return;

        if (item.className === 'your-element-class')
          clickedOutside = false;
      });

      if (clickedOutside)
        // Make an action if it's clicked outside..
    });

Надеюсь, этот ответ вам поможет! (Сообщите мне, если мое решение не является хорошим решением или если вы заметите, что нужно улучшить.)

hpapier
источник
А как насчет вложенных элементов?
Tiberiu-Ionuț Stan
@ Tiberiu-IonuțStan, у вас есть доступ ко всему списку узлов, от родительского до дочернего, поэтому вы можете легко ими управлять.
hpapier
Внезапно ваше решение перестало быть простым.
Tiberiu-Ionuț Stan
@ Tiberiu-IonuțStan Почему? Лично я считаю, что это решение является самым простым в среде React или сложной JS-среде. Не нужно никаких ссылок, не нужно реализовывать решение, если у dom есть время для загрузки, у вас больше контроля.
hpapier
0

Я провел много исследований, чтобы найти лучший метод. Метод JavaScript .contains рекурсивно используется в DOM, чтобы проверить, содержит ли он цель или нет. Я использовал его в одном из проектов реагирования, но при реагировании на изменения DOM в установленном состоянии метод .contains не работает. Итак, я придумал это решение

//Basic Html snippet
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
<div id="mydiv">
  <h2>
    click outside this div to test
  </h2>
  Check click outside 
</div>
</body>
</html>


//Implementation in Vanilla javaScript
const node = document.getElementById('mydiv')
//minor css to make div more obvious
node.style.width = '300px'
node.style.height = '100px'
node.style.background = 'red'

let isCursorInside = false

//Attach mouseover event listener and update in variable
node.addEventListener('mouseover', function() {
  isCursorInside = true
  console.log('cursor inside')
})

/Attach mouseout event listener and update in variable
node.addEventListener('mouseout', function() {
    isCursorInside = false
  console.log('cursor outside')
})


document.addEventListener('click', function() {
  //And if isCursorInside = false it means cursor is outside 
    if(!isCursorInside) {
    alert('Outside div click detected')
  }
})

РАБОЧАЯ ДЕМО jsfiddle

Амрендра Кумар
источник