Detectar clic fuera del elemento (JavaScript vainilla)

84

He buscado una buena solución en todas partes, pero no puedo encontrar una que no use jQuery .

¿Existe una forma normal entre navegadores (sin hacks extraños o código fácil de descifrar) para detectar un clic fuera de un elemento (que puede tener o no hijos)?

Tiberiu-Ionuț Stan
fuente
¿Establecer un controlador de eventos onclick en la etiqueta del cuerpo haría el trabajo?
Benjamin Toueg

Respuestas:

192

Agregue un detector de eventos documenty utilícelo Node.contains()para encontrar si el objetivo del evento (que es el elemento más interno en el que se hace clic) está dentro del elemento especificado. Funciona incluso en 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
fuente
14
Me parece la solución más elegante.
Marian
3
Es la mejor solución que he visto en mi vida.
HelloWorld
25

Debe manejar el evento de clic en el nivel del documento. En el objeto de evento, tiene una targetpropiedad, el elemento DOM más interno en el que se hizo clic. Con esto se revisa y recorre sus padres hasta el elemento del documento, si uno de ellos es su elemento observado.

Vea el ejemplo en 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";
});

Como siempre, esto no es compatible con navegadores defectuosos debido a addEventListener / attachEvent, pero funciona así.

Se hace clic en un niño, cuando no es event.target, pero uno de sus padres es el elemento observado (simplemente estoy contando el nivel para esto). También puede tener una var booleana, si el elemento se encuentra o no, para no devolver el controlador desde dentro de la cláusula for. Mi ejemplo se limita a que el controlador solo termina, cuando nada coincide.

Agregando compatibilidad entre navegadores, generalmente lo hago así:

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

Este es un código compatible con varios navegadores para adjuntar un detector / manejador de eventos, incluida la reescritura thisen IE, para que sea el elemento, como lo hace jQuery para sus manejadores de eventos. Hay muchos argumentos para tener en cuenta algunos bits de jQuery;)

metadings
fuente
4
¿Qué pasa si el usuario hace clic en un elemento secundario del elemento?
Tiberiu-Ionuț Stan
3
Con esta solución, verifica la targetpropiedad (que, como dice metadings, será el elemento DOM más interno) y todos sus elementos principales. O llega al elemento observado, lo que significa que el usuario hizo clic dentro de él, o llega al elemento del documento, en cuyo caso hizo clic fuera. Alternativamente, puede agregar dos detectores de clics, uno en el elemento del documento y otro en el elemento observado, que usa event.stopPropagation()/ cancelBubblepara que el detector del elemento del documento solo se active con un clic fuera del elemento observado.
JimmiTh
2
Ese es un uso inteligente de for. Por curiosidad, ¿habría alguna razón para no comparar un elemento de una variable con el objetivo en el evento (usando una comparación estricta)?
Tiberiu-Ionuț Stan
@ Tiberiu-IonuțStan sí, si crea este 'reloj' usando una referencia en lugar de la identificación 'x', le sugiero que use element === seenElement usando triple igual.
metadings
Tu solución funciona muy bien. Este puede ser un problema personal, pero prefiero compartirlo. Tengo la misma necesidad, pero un botón al que se hace clic muestra un div que, cuando el usuario hace clic en el exterior, debe estar oculto. El problema con esta solución es que el div nunca se vuelve a mostrar. Bifurqué su código aquí para ver un ejemplo: jsfiddle.net/bx5hnL2h
Alexandre Schmidt
7

Qué tal esto:

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
fuente
0

Para ocultar el elemento haciendo clic fuera de él, generalmente aplico un código tan simple:

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
fuente
0

Otro enfoque muy simple y rápido para este problema es mapear la matriz de pathen el objeto de evento devuelto por el oyente. Si el nombre ido classde su elemento coincide con uno de los de la matriz, el clic está dentro de su elemento.

(Esta solución puede ser útil si no desea obtener el elemento directamente (por ejemplo:, document.getElementById('...')por ejemplo, en una aplicación reactjs / nextjs, en ssr ..).

Aquí hay un ejemplo:

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

¡Espero que esta respuesta te ayude! (Avíseme si mi solución no es una buena solución o si ve algo para mejorar).

hpapier
fuente
¿Qué pasa con los elementos anidados?
Tiberiu-Ionuț Stan
@ Tiberiu-IonuțStan tienes acceso a toda la lista de nodos, desde el padre hasta los hijos, para que puedas manipularlos fácilmente.
hpapier
De repente, tu solución ya no es simple.
Tiberiu-Ionuț Stan
@ Tiberiu-IonuțStan ¿Por qué? Personalmente, encuentro que esta solución es la más simple en un entorno React o JS complicado. No necesita ninguna referencia, no necesita implementar una solución si el dom hace tiempo para cargar, usted tiene más control.
hpapier
0

Investigué mucho para encontrar un método mejor. El método JavaScript .contains va de forma recursiva en DOM para comprobar si contiene el objetivo o no. Lo usé en uno de los proyectos de reacción, pero cuando el DOM de reacción cambia en el estado establecido, el método .contains no funciona. Entonces se me ocurrió esta solución

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

TRABAJANDO DEMO jsfiddle

Amrendra Kumar
fuente