¿Cómo detecto un clic fuera de un elemento?

2695

Tengo algunos menús HTML, que muestro por completo cuando un usuario hace clic en el encabezado de estos menús. Me gustaría ocultar estos elementos cuando el usuario hace clic fuera del área de los menús.

¿Es posible algo como esto con jQuery?

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

Note: Using stopPropagation is something that should be avoided as it breaks normal event flow in the DOM. See this CSS Tricks article for more information. Consider using this method instead.

Adjunte un evento de clic al cuerpo del documento que cierra la ventana. Adjunte un evento de clic separado al contenedor que detiene la propagación al cuerpo del documento.

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

$('#menucontainer').click(function(event){
  event.stopPropagation();
});
29
  • 761
    Esto rompe el comportamiento estándar de muchas cosas, incluidos los botones y enlaces, contenidos en #menucontainer. Me sorprende que esta respuesta sea tan popular. Art 12 de junio de 2010 a las 8:00
  • 79
    Esto no interrumpe el comportamiento de nada dentro de #menucontainer, ya que está en la parte inferior de la cadena de propagación para cualquier cosa dentro de él. Eran Galperin 16/06/10 a las 19:55
  • 98
    Es muy bonito, pero no debes usar el $('html').click()cuerpo. El cuerpo siempre tiene la altura de su contenido. Si no hay mucho contenido o la pantalla es muy alta, solo funciona en la parte que ocupa el cuerpo. meo 25 feb 2011 a las 15:35
  • 111
    También me sorprende que esta solución haya obtenido tantos votos. Esto fallará para cualquier elemento externo que tenga stopPropagation jsfiddle.net/Flandre/vaNFw/3Andre 8 de mayo de 2012 a las 12:32
  • 144
    Philip Walton explica muy bien por qué esta respuesta no es la mejor solución: css-tricks.com/dangers-stopping-event-propagationTom 20 de mayo de 2014 a las 10:52
1505

Puede escuchar un evento de clicdocument y luego asegurarse de que #menucontainerno sea un antepasado o el objetivo del elemento en el que se hizo clic usando .closest().

Si no es así, el elemento en el que se hizo clic está fuera del #menucontainery puede ocultarlo de forma segura.

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

Editar - 2017-06-23

También puede limpiar después del detector de eventos si planea descartar el menú y desea dejar de escuchar eventos. Esta función limpiará solo el oyente recién creado, conservando cualquier otro oyente de clics activado document. Con sintaxis ES2015:

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

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

  document.addEventListener('click', outsideClickListener)
}

Editar - 2018-03-11

Para aquellos que no quieren usar jQuery. Aquí está el código anterior en simple 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 

NOTA: Esto se basa en el comentario de Alex para usar en !element.contains(event.target)lugar de la parte jQuery.

Pero element.closest()ahora también está disponible en todos los navegadores principales (la versión de W3C difiere un poco de la de jQuery). Polyfills se puede encontrar aquí: Element.closest ()

Editar - 2020-05-21

En el caso en el que desee que el usuario pueda hacer clic y arrastrar dentro del elemento, luego suelte el mouse fuera del elemento, sin cerrar el elemento:

      ...
      let lastMouseDownX = 0;
      let lastMouseDownY = 0;
      let lastMouseDownWasOutside = false;

      const mouseDownListener = (event: MouseEvent) => {
        lastMouseDownX = event.offsetX
        lastMouseDownY = event.offsetY
        lastMouseDownWasOutside = !$(event.target).closest(element).length
      }
      document.addEventListener('mousedown', mouseDownListener);

Y en outsideClickListener:

const outsideClickListener = event => {
        const deltaX = event.offsetX - lastMouseDownX
        const deltaY = event.offsetY - lastMouseDownY
        const distSq = (deltaX * deltaX) + (deltaY * deltaY)
        const isDrag = distSq > 3
        const isDragException = isDrag && !lastMouseDownWasOutside

        if (!element.contains(event.target) && isVisible(element) && !isDragException) { // or use: event.target.closest(selector) === null
          element.style.display = 'none'
          removeClickListener()
          document.removeEventListener('mousedown', mouseDownListener); // Or add this line to removeClickListener()
        }
    }
28
  • 31
    Probé muchas de las otras respuestas, pero solo esta funcionó. Gracias. El código que terminé usando fue este: $ (document) .click (function (event) {if ($ (event.target) .closest ('. Window'). Length == 0) {$ ('. Window' ) .fadeOut ('rápido');}}); Pistos 5 abr 12 a las 19:30
  • 41
    De hecho, terminé optando por esta solución porque admite mejor varios menús en la misma página donde hacer clic en un segundo menú mientras el primero está abierto dejará el primero abierto en la solución stopPropagation. umassthrower 7/04/12 a las 23:41
  • 15
    Excelente respuesta. Este es el camino a seguir cuando tiene varios elementos que desea cerrar. John 8 de mayo de 13 a las 15:23
  • 6
    Esta debería ser la respuesta aceptada debido a la falla que tiene la otra solución con event.stopPropagation (). tomato 25 abr '14 a las 2:45
  • 26
    Sin jQuery - !element.contains(event.target)usando Node.contains ()Alex Ross 31 de julio de 2015 a las 1:56
356

How to detect a click outside an element?

La razón por la que esta pregunta es tan popular y tiene tantas respuestas es que es engañosamente compleja. Después de casi ocho años y decenas de respuestas, estoy realmente sorprendido de ver la poca atención que se le ha dado a la accesibilidad.

I would like to hide these elements when the user clicks outside the menus' area.

Esta es una causa noble y es el problema real . El título de la pregunta, que es lo que la mayoría de las respuestas parecen intentar abordar, contiene una desafortunada pista falsa.

Pista: ¡es la palabra "clic" !

En realidad, no desea vincular controladores de clics.

Si está vinculando controladores de clic para cerrar el cuadro de diálogo, ya ha fallado. La razón por la que ha fallado es que no todos desencadenan clickeventos. Los usuarios que no usen un mouse podrán escapar de su cuadro de diálogo (y su menú emergente es posiblemente un tipo de cuadro de diálogo) presionando Tab, y luego no podrán leer el contenido detrás del cuadro de diálogo sin activar posteriormente un clickevento.

Así que reformulemos la pregunta.

How does one close a dialog when a user is finished with it?

Ésta es la meta. Desafortunadamente, ahora necesitamos vincular el userisfinishedwiththedialogevento, y esa vinculación no es tan sencilla.

Entonces, ¿cómo podemos detectar que un usuario ha terminado de usar un diálogo?

focusout evento

Un buen comienzo es determinar si el foco ha abandonado el cuadro de diálogo.

Sugerencia: tenga cuidado con el blurevento, ¡ blurno se propaga si el evento estaba vinculado a la fase de burbujeo!

jQuery's funcionará focusoutbien. Si no puede usar jQuery, puede usarlo blurdurante la fase de captura:

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

Además, para muchos diálogos, deberá permitir que el contenedor se enfoque. Agregar tabindex="-1"para permitir que el cuadro de diálogo reciba el foco de forma dinámica sin interrumpir el flujo de tabulación.

$('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>

Si juega con esa demostración durante más de un minuto, debería comenzar a ver problemas rápidamente.

La primera es que no se puede hacer clic en el enlace del cuadro de diálogo. Si intenta hacer clic en él o en la pestaña, se cerrará el cuadro de diálogo antes de que se produzca la interacción. Esto se debe a que enfocar el elemento interno desencadena un focusoutevento antes de desencadenar un focusinevento nuevamente.

La solución es poner en cola el cambio de estado en el bucle de eventos. Esto se puede hacer usando setImmediate(...)o setTimeout(..., 0)para navegadores que no son compatibles setImmediate. Una vez en la cola, puede ser cancelado por un siguiente 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>

El segundo problema es que el cuadro de diálogo no se cierra cuando se vuelve a presionar el enlace. Esto se debe a que el cuadro de diálogo pierde el foco, lo que desencadena el comportamiento de cierre, después de lo cual el clic en el vínculo hace que el cuadro de diálogo se vuelva a abrir.

Al igual que en el problema anterior, es necesario administrar el estado de enfoque. Dado que el cambio de estado ya se ha puesto en cola, solo es cuestión de gestionar los eventos de enfoque en los activadores del cuadro de diálogo:

Esto debería parecer familiar
$('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 llave

Si pensaba que había terminado con el manejo de los estados de enfoque, hay más que puede hacer para simplificar la experiencia del usuario.

Esta es a menudo una característica "agradable de tener", pero es común que cuando tienes una ventana emergente o modal de cualquier tipo, la Escclave la cierre.

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>

Si sabe que tiene elementos enfocables dentro del cuadro de diálogo, no necesitará enfocar el cuadro de diálogo directamente. Si está creando un menú, puede enfocar el primer elemento del menú en su lugar.

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.

Funciones de WAI-ARIA y otro soporte de accesibilidad

Con suerte, esta respuesta cubre los conceptos básicos de la compatibilidad con el teclado y el mouse accesibles para esta función, pero como ya es bastante considerable, evitaré cualquier discusión sobre los roles y atributos de WAI-ARIA ; sin embargo, recomiendo encarecidamente que los implementadores se refieran a la especificación para obtener más detalles. sobre qué roles deben usar y cualquier otro atributo apropiado.

15
  • 38
    Esta es la respuesta más completa, teniendo en cuenta las explicaciones y la accesibilidad. Creo que esta debería ser la respuesta aceptada, ya que la mayoría de las otras respuestas solo manejan el clic y son solo un fragmento de código eliminado sin ninguna explicación. Cyrille 29/10/2016 a las 14:39
  • 5
    Increíble, bien explicado. Acabo de usar este enfoque en un componente React y funcionó perfectamente graciasDavid Lavieri 18 de noviembre de 2016 a las 3:26
  • 3
    @zzzzBov gracias por la respuesta en profundidad, estoy tratando de lograrlo en Vanilla JS, y estoy un poco perdido con todas las cosas de jquery. ¿Hay algo similar en Vanilla JS? HendrikEng 4 de enero de 2017 a las 3:33
  • 2
    @zzzzBov no, no estoy buscando que escribas una versión libre de jQuery, por supuesto, intentaré hacerlo y supongo que lo mejor es hacer una nueva pregunta aquí si realmente me atasco. Muchas gracias de nuevo. HendrikEng 4 de enero de 2017 a las 12:19
  • 8
    Si bien este es un gran método para detectar clics en menús desplegables personalizados u otras entradas, nunca debería ser el método preferido para modales o ventanas emergentes por dos razones. El modal se cerrará cuando el usuario cambie a otra pestaña o ventana, o abra un menú contextual, lo cual es realmente molesto. Además, el evento de 'clic' se dispara con el mouse hacia arriba, mientras que el evento de 'focusout' se activa en el instante en que presiona el mouse hacia abajo. Normalmente, un botón solo realiza una acción si presiona el botón del mouse y luego lo suelta. La forma adecuada y accesible de hacer esto para los modales es agregar un botón de cierre con tabulación. Kevin 6 de agosto de 2017 a las 13:11
158

Las otras soluciones aquí no funcionaron para mí, así que tuve que usar:

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

Editar: variante de JavaScript simple (2021-03-31)

Utilicé este método para manejar el cierre de un menú desplegable al hacer clic fuera de él.

Primero, creé un nombre de clase personalizado para todos los elementos del componente. Este nombre de clase se agregará a todos los elementos que componen el widget de menú.

const className = `dropdown-${Date.now()}-${Math.random() * 100}`;

Creo una función para verificar los clics y el nombre de la clase del elemento en el que se hizo clic. Si el elemento en el que se hizo clic no contiene el nombre de la clase personalizada que generé anteriormente, debería establecer la showbandera en falsey el menú se cerrará.

const onClickOutside = (e) => {
  if (!e.target.className.includes(className)) {
    show = false;
  }
};

Luego adjunté el controlador de clic al objeto de la ventana.

// add when widget loads
window.addEventListener("click", onClickOutside);

... y finalmente algo de limpieza

// remove listener when destroying the widget
window.removeEventListener("click", onClickOutside);
8
  • Publiqué otro ejemplo práctico de cómo usar event.target para evitar activar otro widget de interfaz de usuario de Jquery, haga clic fuera de los controladores html al incrustarlos en su propio cuadro emergente: la mejor manera de obtener el objetivo originalJoey T 2/07/12 a las 18:13
  • 45
    Esto funcionó para mí, excepto que agregué && !$(event.target).parents("#foo").is("#foo")dentro de la IFdeclaración para que los elementos secundarios no cierren el menú cuando se hace clic. honyovk 26/09/12 a las 19:57
  • 1
    No puedo encontrar algo mejor que: // Estamos fuera de $ (event.target) .parents ('# foo'). Length == 0AlexG 15/12/2013 a las 11:12
  • 3
    La mejora concisa para manejar el anidamiento profundo es usar .is('#foo, #foo *'), pero no recomiendo enlazar controladores de clics para resolver este problema . zzzzBov 19 de septiembre de 2016 a las 16:03
  • 4
    !$(event.target).closest("#foo").lengthsería mejor y obvia la necesidad de la adición de @ honyovk. tvanc 24 de mayo de 2018 a las 23:30
132

Tengo una aplicación que funciona de manera similar al ejemplo de Eran, excepto que adjunto el evento de clic al cuerpo cuando abro el menú ... algo así:

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

  event.stopPropagation();
});

Más información sobre la one()función de jQuery

7
  • 10
    pero luego, si hace clic en el menú en sí, entonces afuera, no funcionará :)vsync 17/08/09 a las 16:46
  • 3
    Ayuda poner event.stopProgagantion () antes de vincular el oyente de clics al cuerpo. Jasper Kennis 19/04/12 a las 10:05
  • 4
    El problema con esto es que "uno" se aplica al método jQuery de agregar eventos a una matriz varias veces. Entonces, si hace clic en el menú para abrirlo más de una vez, el evento se vincula al cuerpo nuevamente e intenta ocultar el menú varias veces. Se debe aplicar una protección contra fallas para solucionar este problema. marksyzm 24/06/2013 a las 11:01
  • Después de vincular usando .one, dentro del $('html')controlador, escriba un $('html').off('click'). Cody 13/07/2015 a las 18:01
  • 4
    @Cody No creo que eso ayude. El onecontrolador llamará automáticamente off(como se muestra en los documentos de jQuery). Mariano Desanze 4 dic 2015 a las 18:12
52

Es 2020 y puedes usar event.composedPath()

De: https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath

The composedPath() method of the Event interface returns the event’s path, which is an array of the objects on which listeners will be invoked.

const target = document.querySelector('#myTarget')

document.addEventListener('click', (event) => {
  const withinBoundaries = event.composedPath().includes(target)

  if (withinBoundaries) {
    target.innerText = 'Click happened inside element'
  } else {
    target.innerText = 'Click happened **OUTSIDE** element'
  } 
})
/* just to make it good looking. you don't need this */
#myTarget {
  margin: 50px auto;
  width: 500px;
  height: 500px;
  background: gray;
  border: 10px solid black;
}
<div id="myTarget">
  click me (or not!)
</div>
3
  • 2
    Vaya, nunca supe sobre este método en el objeto de evento, y también en Vanilla JS. ¡Gracias por compartir! lislis 15 nov.20 a las 6:09
  • si desea hacerlo en línea, puede simplemente<div style="width:100%;border:1px solid black" onclick="console.log('Outside?',event.composedPath()[0]===this)"><span>Inside</span> Outside</div>YoniXw 13/12/20 a las 18:37
  • 1
    He estado buscando esto durante mucho tiempo. ¡Gracias! Omar Omeiri 5 de agosto a las 17:23
51

Después de investigar, encontré tres soluciones que funcionan (olvidé los enlaces de la página como referencia)

Primera solucion

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

Segunda solucion

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

Tercera solucion

<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>
8
  • 9
    La tercera solución es, con mucho, la forma más elegante de comprobar. Tampoco implica ninguna sobrecarga de jQuery. Muy agradable. Ayudó mucho. Gracias. dbarth 27/07/2016 a las 14:49
  • Estoy tratando de hacer que la tercera solución funcione con múltiples elementos document.getElementsByClassName, si alguien tiene una pista, por favor compártala. lowtechsun 16 de enero de 2017 a las 8:42
  • @lowtechsun Tendría que recorrer para verificar cada uno. Donnie D'Amato 13/02/2017 a las 17:28
  • Realmente me gustó la tercera solución, sin embargo, el clic se activa antes de que mi div haya comenzado a mostrar cuál lo oculta nuevamente, ¿alguna idea de por qué? indiehjaerta 14/04/18 a las 0:49
  • El tercero permite console.log, claro, pero no te permite cerrar realmente configurando display en none, porque hará esto antes de que se muestre el menú. ¿Tienes una solución para esto? RolandiXor 9/10/2018 a las 11:32
42
$("#menuscontainer").click(function() {
    $(this).focus();
});
$("#menuscontainer").blur(function(){
    $(this).hide();
});

Funciona para mí muy bien.

5
  • 4
    Este es el que yo usaría. Puede que no sea perfecto, pero como programador aficionado, es lo suficientemente simple como para entenderlo con claridad. kevtrout 8 de febrero de 2010 a las 16:05
  • el desenfoque es un movimiento fuera de #menucontainerla pregunta se trataba de un clicborrel 6 de agosto de 2013 a las 13:22
  • 4
    @borrel blur no es un movimiento fuera del contenedor. Desenfocar es lo opuesto al enfoque, estás pensando en mouseout. Esta solución funcionó particularmente bien para mí cuando estaba creando texto "hacer clic para editar" en el que cambiaba de un lado a otro entre el texto sin formato y los campos de entrada en los clics. parker.sikand 10/10/2013 a las 22:08
  • Tuve que agregar más tabindex="-1"para #menuscontainerque funcionara. Parece que si coloca una etiqueta de entrada dentro del contenedor y hace clic en ella, el contenedor se oculta. tyrion 12/08/2014 a las 13:17
  • el evento mouseleave es más adecuado para menús y contenedores (ref: w3schools.com/jquery/… )Evgeniya Manolova 19/10/15 a las 1:35
38

Ahora hay un complemento para eso: eventos externos ( publicación de blog )

El siguiente sucede cuando un clickoutside controlador (WLOG) está unido a un elemento:

  • el elemento se agrega a una matriz que contiene todos los elementos con manejadores de clickoutside
  • un controlador de clic ( espacio de nombres ) está vinculado al documento (si no está ya allí)
  • en cualquier clic en el documento, el evento clickoutside se activa para aquellos elementos en esa matriz que no son iguales o un padre del objetivo de eventos de clic
  • Además, el evento event.target para el evento clickoutside se establece en el elemento en el que el usuario hizo clic (por lo que incluso sabe en qué hizo clic el usuario, no solo que hizo clic en el exterior).

Por lo tanto, no se detiene la propagación de eventos y se pueden usar controladores de clic adicionales "encima" del elemento con el controlador externo.

2
  • Buen complemento. El enlace de "eventos externos" está muerto, mientras que el enlace de la publicación de blog está vivo y proporciona un complemento muy útil para eventos de tipo "clickoutside". También tiene licencia del MIT. TechNyquist 11/04/2017 a las 6:44
  • Gran complemento. Funcionó perfectamente. El uso es así:$( '#element' ).on( 'clickoutside', function( e ) { .. } );Gavin 21/03/18 a las 13:48
33

¡Esto funcionó para mí perfectamente!

$('html').click(function (e) {
    if (e.target.id == 'YOUR-DIV-ID') {
        //do something
    } else {
        //do something
    }
});
1
  • Esta solución funcionó bien para lo que estaba haciendo, donde todavía necesitaba que el evento de clic se disparara, gracias. Mike 23/07/12 a las 2:10
27

Una solución simple para la situación es:

$(document).mouseup(function (e)
{
    var container = $("YOUR SELECTOR"); // Give you class or ID

    if (!container.is(e.target) &&            // If the target of the click is not the desired div or section
        container.has(e.target).length === 0) // ... nor a descendant-child of the container
    {
        container.hide();
    }
});

La secuencia de comandos anterior ocultará divsi divse activa el evento de clic fuera .

Puede ver el siguiente blog para obtener más información: http://www.codecanal.com/detect-click-outside-div-using-javascript/

0
26

No creo que lo que realmente necesita es cerrar el menú cuando el usuario hace clic fuera; lo que necesita es que el menú se cierre cuando el usuario haga clic en cualquier parte de la página. Si hace clic en el menú, o fuera del menú, debería cerrarse, ¿verdad?

No encontrar respuestas satisfactorias arriba me llevó a escribir esta publicación de blog el otro día. Para los más pedantes, hay una serie de trampas a tener en cuenta:

  1. Si adjunta un controlador de eventos de clic al elemento del cuerpo en el momento del clic, asegúrese de esperar el segundo clic antes de cerrar el menú y desvincular el evento. De lo contrario, el evento de clic que abrió el menú aparecerá en el oyente que tiene que cerrar el menú.
  2. Si usa event.stopPropogation () en un evento de clic, ningún otro elemento de su página puede tener una función de clic en cualquier lugar para cerrar.
  3. Adjuntar un controlador de eventos de clic al elemento del cuerpo de forma indefinida no es una solución eficaz
  4. Comparar el destino del evento y sus padres con el creador del controlador supone que lo que quieres es cerrar el menú cuando haces clic en él, cuando lo que realmente quieres es cerrarlo cuando haces clic en cualquier parte de la página.
  5. Escuchar eventos en el elemento del cuerpo hará que su código sea más frágil. Un estilo tan inocente como este lo rompería:body { margin-left:auto; margin-right: auto; width:960px;}
1
  • 3
    "Si hace clic en el menú, o fuera del menú, debería cerrarse, ¿verdad?" no siempre. Cancelar un clic arrastrando un elemento aún activará un clic en el nivel del documento, pero la intención no sería continuar cerrando el menú. También hay muchos otros tipos de cuadros de diálogo que podrían usar el comportamiento de "hacer clic" que permitiría hacer clic internamente. zzzzBov 11/07/2016 a las 23:18
26

Como dijo otro cartel, hay muchas trampas, especialmente si el elemento que está mostrando (en este caso un menú) tiene elementos interactivos. Encontré que el siguiente método es bastante sólido:

$('#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);
        }

    })
});
0
21

Verifique el destino del evento de clic de la ventana (debe propagarse a la ventana, siempre que no se capture en ningún otro lugar) y asegúrese de que no sea ninguno de los elementos del menú. Si no es así, entonces estás fuera de tu menú.

O verifique la posición del clic y vea si está contenido dentro del área del menú.

0
21

Solución1

En lugar de usar event.stopPropagation () que puede tener algunos efectos secundarios, simplemente defina una variable de bandera simple y agregue una ifcondición. Probé esto y funcioné correctamente sin ningún efecto secundario de stopPropagation:

var flag = "1";
$('#menucontainer').click(function(event){
    flag = "0"; // flag 0 means click happened in the area where we should not do any action
});

$('html').click(function() {
    if(flag != "0"){
        // Hide the menus if visible
    }
    else {
        flag = "1";
    }
});

Solución2

Con solo una simple ifcondición:

$(document).on('click', function(event){
    var container = $("#menucontainer");
    if (!container.is(event.target) &&            // If the target of the click isn't the container...
        container.has(event.target).length === 0) // ... nor a descendant of the container
    {
        // Do whatever you want to do when click is outside the element
    }
});
2
  • Usé esta solución con una bandera booleana y es buena también con un DOm articulado y también si dentro de #menucontainer hay muchos otros elementosMigio B 28/02/15 a las 20:28
  • La solución 1 funciona mejor porque maneja los casos en los que el destino del clic se elimina del DOM en el momento en que el evento se propaga al documento. Alice 18 feb.20 a las 14:23
19

He tenido éxito con algo como esto:

var $menuscontainer = ...;

$('#trigger').click(function() {
  $menuscontainer.show();

  $('body').click(function(event) {
    var $target = $(event.target);

    if ($target.parents('#menuscontainer').length == 0) {
      $menuscontainer.hide();
    }
  });
});

La lógica es: cuando #menuscontainerse muestra, vincule un controlador de clic al cuerpo que se oculta #menuscontainersolo si el objetivo (del clic) no es un hijo de él.

0
19

Me sorprende que nadie haya reconocido el focusoutevento:

var button = document.getElementById('button');
button.addEventListener('click', function(e){
  e.target.style.backgroundColor = 'green';
});
button.addEventListener('focusout', function(e){
  e.target.style.backgroundColor = '';
});
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>
  <button id="button">Click</button>
</body>
</html>
3
  • Te estás perdiendo mi respuesta, creo. stackoverflow.com/a/47755925/6478359Muhammet Can TONBUL 14/06/19 a las 13:35
  • Supongo que nadie sugirió focusout porque la pregunta se trata de hacer clic afuera, no de alejar el mouse. Al igual que con otros, necesito específicamente un clic y pasar el mouse. Pero esto será útil para algunos :)James 6/07/20 a las 12:51
  • Si el usuario abre el menú y usa la tecla TAB para moverse a los elementos del menú (sin hacer clic en ellos), usar su código hace que el menú se cierre solo porque se enfoca en los elementos del menú. Y creo que no es buena idea. Mohsen Haeri 10 de enero a las 8:01
17

El evento tiene una propiedad llamada event.path del elemento que es una "lista ordenada estática de todos sus antepasados ​​en orden de árbol" . Para verificar si un evento se originó a partir de un elemento DOM específico o uno de sus hijos, simplemente verifique la ruta de ese elemento DOM específico. También se puede utilizar para comprobar varios elementos mediante ORla comprobación lógica del elemento en la somefunción.

$("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;
  background:yellow;
}
<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>

Entonces, para tu caso, debería ser

$("body").click(function() {
  target = $("#menuscontainer")[0];
  flag = event.path.some(function(el, i, arr) {
    return (el == target)
  });
  if (!flag) {
    // Hide the menus
  }
});
1
  • event.pathno es una cosa. Andrew 25 de mayo de 2020 a las 19:09
16

Como variante:

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

No tiene ningún problema para detener la propagación de eventos y admite mejor varios menús en la misma página donde hacer clic en un segundo menú mientras el primero está abierto dejará el primero abierto en la solución stopPropagation.

14

Encontré este método en algún complemento de calendario de 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);
0
13

Aquí está la solución básica de JavaScript para futuros espectadores.

Al hacer clic en cualquier elemento dentro del documento, si la identificación del elemento en el que se hizo clic está activada, o si el elemento oculto no está oculto y el elemento oculto no contiene el elemento en el que se hizo clic, cambie el elemento.

(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>

Si va a tener varios conmutadores en la misma página, puede usar algo como esto:

  1. Agregue el nombre de la clase hiddenal elemento plegable.
  2. Al hacer clic en el documento, cierre todos los elementos ocultos que no contienen el elemento en el que se hizo clic y no están ocultos
  3. Si el elemento en el que se hizo clic es un alternar, alterne el elemento especificado.

(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>
8

Si está realizando un script para IE y FF 3. * y solo desea saber si el clic se produjo dentro de un área determinada del cuadro, también puede usar algo como:

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

En lugar de utilizar la interrupción del flujo, el evento de desenfoque / enfoque o cualquier otra técnica complicada, simplemente haga coincidir el flujo del evento con el parentesco del elemento:

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

Para eliminar el detector de eventos de clic fuera, simplemente:

$(document).off("click.menu-outside");
3
  • Para mí esta fue la mejor solución. Lo usé en una devolución de llamada después de la animación, así que necesitaba separar el evento en algún lugar. +1qwertzman 29 de mayo de 2014 a las 11:09
  • Aquí hay un cheque redundante. si el elemento es de hecho #menuscontainer, todavía estás pasando por sus padres. primero debe verificar eso, y si no es ese elemento, luego suba al árbol DOM. vsync 25 de agosto de 2014 a las 14:25
  • Derecha ! Puede cambiar la condición a if(!($(event.target).is("#menuscontainer") || $(event.target).parents().is("#menuscontainer"))){. Es una pequeña optimización, pero ocurre solo unas pocas veces en la vida de su programa: por cada clic, si el click.menu-outsideevento está registrado. Es más largo (+32 caracteres) y no usa el método de encadenamientomems 25 de agosto de 2014 a las 14:40
8

Usar:

var go = false;
$(document).click(function(){
    if(go){
        $('#divID').hide();
        go = false;
    }
})

$("#divID").mouseover(function(){
    go = false;
});

$("#divID").mouseout(function (){
    go = true;
});

$("btnID").click( function(){
    if($("#divID:visible").length==1)
        $("#divID").hide(); // Toggle
    $("#divID").show();
});
7

Si alguien tiene curiosidad aquí es la solución javascript (es6):

window.addEventListener('mouseup', e => {
        if (e.target != yourDiv && e.target.parentNode != yourDiv) {
            yourDiv.classList.remove('show-menu');
            //or yourDiv.style.display = 'none';
        }
    })

y es5, por si acaso:

window.addEventListener('mouseup', function (e) {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
    yourDiv.classList.remove('show-menu'); 
    //or yourDiv.style.display = 'none';
}

});

7

Aquí hay una solución simple por javascript puro. Está actualizado con ES6 :

var isMenuClick = false;
var menu = document.getElementById('menuscontainer');
document.addEventListener('click',()=>{
    if(!isMenuClick){
       //Hide the menu here
    }
    //Reset isMenuClick 
    isMenuClick = false;
})
menu.addEventListener('click',()=>{
    isMenuClick = true;
})
3
  • "Actualizado con ES6" es una afirmación bastante audaz, cuando lo único que está actualizado con ES6 es hacer en () => {}lugar de function() {}. Lo que tienes allí se clasifica como JavaScript simple con un toque de ES6. MortenMoulder 17/11/2017 a las 12:41
  • @MortenMoulder: Sí. Es solo para llamar la atención, aunque en realidad es ES6. Pero mire la solución. Yo creo que es bueno. Duannx 18/11/2017 a las 2:40
  • Es vainilla JS y funciona para el objetivo del evento eliminado de DOM (por ejemplo, cuando se selecciona el valor de la ventana emergente interna, se cierra inmediatamente la ventana emergente). +1 de mi parte! Alice 18 feb.20 a las 14:29
7

He usado el siguiente script y lo he hecho con jQuery.

jQuery(document).click(function(e) {
    var target = e.target; //target div recorded
    if (!jQuery(target).is('#tobehide') ) {
        jQuery(this).fadeOut(); //if the click element is not the above id will hide
    }
})

A continuación, encuentre el código HTML

<div class="main-container">
<div> Hello I am the title</div>
<div class="tobehide">I will hide when you click outside of me</div>
</div>

Puedes leer el tutorial aquí

7

Solución 2020 que utiliza el método más cercano a la API JS nativa .

document.addEventListener('click', ({ target }) => {
  if (!target.closest('.el1, .el2, #el3')) {
    alert('click outside')
  }
})

1
  • más cercano no es compatible con IESteven Mark Ford 10 de septiembre de 2020 a las 1:19
6
$(document).click(function() {
    $(".overlay-window").hide();
});
$(".overlay-window").click(function() {
    return false;
});

Si hace clic en el documento, oculte un elemento determinado, a menos que haga clic en ese mismo elemento.

6

Enganche un detector de eventos de clic en el documento. Dentro del detector de eventos, puede mirar el objeto del evento , en particular, el evento.target para ver en qué elemento se hizo clic:

$(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");
    }
});