Wykryj element zewnętrzny kliknięcia (waniliowy JavaScript)

85

Wszędzie szukałem dobrego rozwiązania, ale nie mogę znaleźć takiego, które nie korzysta z jQuery .

Czy istnieje normalny sposób (bez dziwnych hacków lub łatwego do złamania kodu) między przeglądarkami, aby wykryć kliknięcie poza elementem (który może mieć dzieci lub nie)?

Tiberiu-Ionuț Stan
źródło
Czy ustawienie procedury obsługi zdarzeń onclick w tagu body wykona zadanie?
Benjamin Toueg,

Odpowiedzi:

203

Dodaj detektor zdarzeń do documenti użyj go, Node.contains()aby dowiedzieć się, czy cel zdarzenia (czyli najbardziej kliknięty element w środku) znajduje się wewnątrz określonego elementu. Działa nawet w 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
źródło
14
Wydaje mi się, że to najbardziej eleganckie rozwiązanie.
Marian
3
To najlepsze rozwiązanie, jakie kiedykolwiek widziałem.
HelloWorld
25

Musisz obsłużyć zdarzenie kliknięcia na poziomie dokumentu. W obiekcie zdarzenia masz targetwłaściwość, najbardziej wewnętrzny element DOM, który został kliknięty. Dzięki temu sprawdzasz się i podchodzisz do jego elementów nadrzędnych, aż do elementu dokumentu, jeśli jeden z nich jest Twoim obserwowanym elementem.

Zobacz przykład na 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";
});

Jak zawsze, to nie jest kompatybilne z różnymi przeglądarkami z powodu addEventListener / attachEvent, ale działa w ten sposób.

Dziecko jest klikane, gdy nie jest to event.target, ale jeden z jego rodziców jest obserwowanym elementem (po prostu liczę dla tego poziom). Możesz również mieć zmienną boolowską, jeśli element zostanie znaleziony lub nie, aby nie zwracać procedury obsługi z klauzuli for. Mój przykład ogranicza się do tego, że program obsługi kończy pracę tylko wtedy, gdy nic nie pasuje.

Dodając kompatybilność z różnymi przeglądarkami, zwykle robię to w ten sposób:

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);
  }
};

Jest to kod kompatybilny z różnymi przeglądarkami do dołączania detektora / obsługi zdarzeń, włącznie z przepisywaniem thisw IE, jako element, tak jak robi to jQuery dla swoich programów obsługi zdarzeń. Istnieje wiele argumentów, aby mieć na uwadze kilka bitów jQuery;)

metadings
źródło
4
Co by się stało, gdyby użytkownik kliknął element podrzędny elementu?
Tiberiu-Ionuț Stan,
3
Korzystając z tego rozwiązania, sprawdzasz plik target właściwość (która, jak mówią metadania, będzie najbardziej wewnętrznym elementem DOM) - i wszystkie jej elementy nadrzędne. Albo dotrzesz do obserwowanego elementu, co oznacza, że ​​użytkownik kliknął w nim, albo do elementu dokumentu, w którym to przypadku kliknął na zewnątrz. Alternatywnie, możesz dodać dwa detektory kliknięć - jeden na elemencie dokumentu, a drugi na obserwowanym elemencie, który używa event.stopPropagation()/ cancelBubbletak, że detektor elementu dokumentu jest uruchamiany tylko przez kliknięcie poza obserwowanym elementem.
JimmiTh
2
To sprytne użycie for. Czy z ciekawości byłby jakiś powód, aby nie porównywać elementu ze zmiennej z celem zdarzenia (przy użyciu ścisłego porównania)?
Tiberiu-Ionuț Stan
@ Tiberiu-IonuțStan tak, jeśli utworzysz ten „zegarek” przy użyciu odniesienia zamiast identyfikatora „x”, sugeruję użycie elementu === watchedElement przy użyciu potrójnych równych.
metadings,
Twoje rozwiązanie działa świetnie. To może być sprawa osobista, ale wolę się nią podzielić. Mam tę samą potrzebę, ale kliknięty przycisk pokazuje element div, który, gdy użytkownik kliknie na zewnątrz, powinien być ukryty. Problem z tym rozwiązaniem polega na tym, że element div nigdy więcej nie jest wyświetlany. Tutaj rozwidliłem twój kod na przykład: jsfiddle.net/bx5hnL2h
Alexandre Schmidt
7

Co powiesz na to:

jsBin demo

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');
} 
Akhil Sekharan
źródło
0

Aby ukryć element, klikając poza nim, zwykle stosuję taki prosty kod:

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);

Aleks Sid
źródło
0

Innym bardzo prostym i szybkim podejściem do tego problemu jest odwzorowanie tablicy pathna obiekt zdarzenia zwracany przez nasłuchiwanie. Jeśli nazwa idlub classelementu pasuje do jednego z elementów w tablicy, kliknięcie znajduje się wewnątrz elementu.

(To rozwiązanie może być przydatne, jeśli nie chcesz pobierać elementu bezpośrednio (np .: document.getElementById('...')na przykład w aplikacji actjs / nextjs, w ssr ..).

Oto przykład:

   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..
    });

Mam nadzieję, że ta odpowiedź Ci pomoże! (Daj mi znać, jeśli moje rozwiązanie nie jest dobrym rozwiązaniem lub jeśli widzisz coś do poprawy).

mocniejszy
źródło
A co z elementami zagnieżdżonymi?
Tiberiu-Ionuț Stan
@ Tiberiu-IonuțStan masz dostęp do całej listy węzłów, od rodzica po dzieci, dzięki czemu możesz łatwo nimi manipulować.
hpapier
Twoje rozwiązanie nagle przestaje być proste.
Tiberiu-Ionuț Stan
@ Tiberiu-IonuțStan Dlaczego? Osobiście uważam, że to rozwiązanie jest najprostsze w środowisku React lub skomplikowanym środowisku JS. Nie potrzebujesz żadnego ref, nie musisz wdrażać rozwiązania, jeśli dom ma czas na załadowanie, masz większą kontrolę.
hpapier
0

Zrobiłem dużo badań, aby znaleźć lepszą metodę. Metoda JavaScript .contains przechodzi rekurencyjnie w DOM, aby sprawdzić, czy zawiera cel, czy nie. Użyłem go w jednym z projektów react, ale kiedy reaguj na zmiany DOM na ustawiony stan, metoda .contains nie działa. Więc wymyśliłem to rozwiązanie

//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')
  }
})

PRACA DEMO jsfiddle

Amrendra Kumar
źródło