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

2487

Tengo algunos menús HTML, que se muestran completamente 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 menús.

¿Es posible algo así con jQuery?

$("#menuscontainer").clickOutsideThisElement(function() {
    // Hide the menus
});
Sergio del Amo
fuente
47
Aquí hay una muestra de esta estrategia: jsfiddle.net/tedp/aL7Xe/1
Ted
18
Como mencionó Tom, querrá leer css-tricks.com/dangers-stopping-event-propagation antes de usar este enfoque. Sin embargo, esa herramienta jsfiddle es bastante genial.
Jon Coombs
3
obtener una referencia al elemento y luego event.target, y finalmente! = o == ambos ejecutan el código en consecuencia ..
Rohit Kumar
Recomiendo usar github.com/gsantiago/jquery-clickout :)
Rômulo M. Farias
2
Solución JS de vainilla con event.targety sin event.stopPropagation .
lowtechsun

Respuestas:

1812

NOTA: El uso stopEventPropagation()es algo que debe evitarse ya que interrumpe el flujo normal de eventos en el DOM. Vea este artículo para más información. Considere usar este método en su lugar

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();
});
Eran Galperin
fuente
708
Esto rompe el comportamiento estándar de muchas cosas, incluidos botones y enlaces, contenidos en #menucontainer. Me sorprende que esta respuesta sea tan popular.
Arte
75
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
94
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 llena el cuerpo.
meo
103
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/3
Andre
140
Philip Walton explica muy bien por qué esta respuesta no es la mejor solución: css-tricks.com/dangers-stopping-event-propagation
Tom
1387

Puede escuchar un evento de clicdocument y luego asegurarse de que #menucontainerno sea un antepasado o el objetivo del elemento cliqueado mediante .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) { 
  $target = $(event.target);
  if(!$target.closest('#menucontainer').length && 
  $('#menucontainer').is(":visible")) {
    $('#menucontainer').hide();
  }        
});

Editar - 2017-06-23

También puede limpiar después del escucha de eventos si planea cerrar el menú y desea dejar de escuchar eventos. Esta función limpiará solo el oyente recién creado, preservando cualquier otro oyente de clics document. Con la sintaxis 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)
}

Editar - 2018-03-11

Para aquellos que no quieren usar jQuery. Aquí está el código anterior en vanillaJS simple (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 usarlo en !element.contains(event.target)lugar de la parte jQuery.

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

Editar - 2020-05-21

En el caso en 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()
        }
    }
Arte
fuente
28
Intenté 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
39
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 está abierto un primero dejará el primero abierto en la solución stopPropagation.
umassthrower
13
Excelente respuesta Este es el camino a seguir cuando tiene varios elementos que desea cerrar.
John
55
Esta debería ser la respuesta aceptada debido a la falla que tiene la otra solución con event.stopPropagation ().
tomate
20
Sin jQuery - !element.contains(event.target)usando Node.contains ()
Alex Ross
305

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

La razón por la que esta pregunta es tan popular y tiene tantas respuestas es porque es engañosamente compleja. Después de casi ocho años y docenas de respuestas, estoy realmente sorprendido de ver cuán poco se ha prestado atención a la accesibilidad.

Me gustaría ocultar estos elementos cuando el usuario hace clic fuera del área de menús.

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 un desafortunado arenque rojo.

Sugerencia: es la palabra "clic" !

En realidad, no desea vincular los controladores de clics.

Si vincula los 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 diálogo (y su menú emergente podría decirse que es un tipo de diálogo) presionando Tab, y luego no podrán leer el contenido detrás del diálogo sin activar posteriormente un clickevento.

Así que reformulemos la pregunta.

¿Cómo se cierra un diálogo cuando un usuario termina con él?

Este es el objetivo. 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 salido del diálogo.

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

jQuery focusoutlo hará bien. Si no puede usar jQuery, puede usarlo blurdurante la fase de captura:

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

Además, para muchos cuadros de diálogo deberá permitir que el contenedor se enfoque. Agregar tabindex="-1"para permitir que el cuadro de diálogo reciba el foco dinámicamente 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 juegas con esa demo por más de un minuto, deberías comenzar a ver rápidamente los problemas.

El primero es que no se puede hacer clic en el enlace del cuadro de diálogo. Si intentas hacer clic en él o presionarlo, se cerrará el cuadro de diálogo antes de que tenga lugar la interacción. Esto se debe a que enfocar el elemento interno desencadena un focusoutevento antes de activarlo focusinnuevamente.

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 cola, puede ser cancelado por un posterior 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'));
  }
});

El segundo problema es que el diálogo no se cerrará cuando se presione nuevamente el enlace. Esto se debe a que el cuadro de diálogo pierde el foco, desencadenando el comportamiento de cierre, después de lo cual el clic en el enlace hace que el cuadro de diálogo se vuelva a abrir.

Similar al problema anterior, el estado de enfoque necesita ser administrado. Dado que el cambio de estado ya se ha puesto en cola, solo es cuestión de manejar eventos de enfoque en los disparadores 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'));  
  }
});


Esc llave

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

Esto es a menudo una característica "agradable de tener", pero es común que cuando tenga un modo o ventana emergente de cualquier tipo, la Escclave la cierre.

keydown: function (e) {
  if (e.which === 27) {
    $(this).removeClass('active');
    e.preventDefault();
  }
}


Si sabe que tiene elementos enfocables dentro del diálogo, no necesitará enfocar el diálogo directamente. Si está creando un menú, podría enfocar el primer elemento del menú.

click: function (e) {
  $(this.hash)
    .toggleClass('submenu--active')
    .find('a:first')
    .focus();
  e.preventDefault();
}


Roles WAI-ARIA y otro soporte de accesibilidad

Es de esperar que esta respuesta cubra los aspectos básicos del soporte accesible de teclado y mouse para esta función, pero como ya es bastante considerable, voy a evitar cualquier discusión sobre los roles y atributos de WAI-ARIA , sin embargo, recomiendo encarecidamente que los implementadores consulten la especificación para obtener más detalles. sobre qué roles deben usar y cualquier otro atributo apropiado.

revs zzzzBov
fuente
30
Esta es la respuesta más completa, con explicaciones y accesibilidad en mente. 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 fragmentos de código que se eliminan sin ninguna explicación.
Cyrille
44
Increíble, bien explicado. Acabo de utilizar este enfoque en un componente React y funcionó perfectamente gracias
David Lavieri
2
@zzzzBov gracias por la respuesta profunda, 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
2
@zzzzBov no, no estoy buscando que escribas una versión sin jQuery, por supuesto, intentaré hacerlo y supongo que lo mejor es hacer una nueva pregunta aquí si realmente me quedo atascado. Muchas gracias de nuevo.
HendrikEng
77
Si bien este es un excelente método para detectar clics de 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, que es realmente molesto. Además, el evento 'clic' se dispara con el mouse hacia arriba, mientras que el evento '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 manera adecuada y accesible de hacer esto para los modales es agregar un botón de cierre tabulable.
Kevin
144

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

if(!$(event.target).is('#foo'))
{
    // hide menu
}
Dennis
fuente
Publiqué otro ejemplo práctico de cómo usar event.target para evitar activar otros controladores html de Jquery UI al hacer clic en los controladores html al incrustarlos en su propio cuadro emergente: La mejor manera de obtener el Objetivo original
Joey T
43
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ú al hacer clic.
honyovk
1
No puedo encontrar nada mejor que: // Estamos fuera de $ (event.target) .parents ('# foo').
Length
3
La mejora concisa para manejar el anidamiento profundo es usar .is('#foo, #foo *'), pero no recomiendo vincular manejadores de clics para resolver este problema .
zzzzBov
1
!$(event.target).closest("#foo").lengthsería mejor y obvia la necesidad de agregar a @ honyovk.
tvanc
127

Tengo una aplicación que funciona de manera similar al ejemplo de Eran, excepto que adjunto el evento click 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

Joe Lencioni
fuente
99
pero si hace clic en el menú en sí, luego fuera, no va a funcionar :)
VSYNC
3
Ayuda a poner event.stopProgagantion () antes de vincular el escucha de clics al cuerpo.
Jasper Kennis el
44
El problema con esto es que "uno" se aplica al método jQuery de agregar eventos a una matriz varias veces. Por lo tanto, si hace clic en el menú para abrirlo más de una vez, el evento vuelve a vincularse al cuerpo e intenta ocultar el menú varias veces. Se debe aplicar una prueba de fallos para solucionar este problema.
marksyzm
Después de enlazar usando .one- dentro del $('html')controlador - escriba a $('html').off('click').
Cody
44
@Cody No creo que eso ayude. El onecontrolador llamará automáticamente off(como se muestra en los documentos de jQuery).
Mariano Desanze
44

Después de la investigación, he encontrado tres soluciones de trabajo (olvidé los enlaces de la página como referencia)

Primera solución

<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 solución

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

Tercera solución

<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>
Rameez Rami
fuente
8
La tercera solución es, con mucho, la forma más elegante de verificar. Tampoco implica ninguna sobrecarga de jQuery. Muy agradable. Ayudó mucho Gracias.
dbarth
Estoy tratando de que la tercera solución funcione con múltiples elementos document.getElementsByClassName, si alguien tiene una pista, por favor comparta.
lowtechsun
@lowtechsun Tendrías que recorrer para verificar cada uno.
Donnie D'Amato
Realmente me gustó la tercera solución, sin embargo, el clic se dispara antes de que mi div haya comenzado a mostrar qué lo oculta nuevamente, ¿alguna idea de por qué?
indiehjaerta
El tercero permite console.log, claro, pero no te permite cerrar realmente configurando display a none, porque lo hará antes de que se muestre el menú. ¿Tienes una solución para esto?
RolandiXor
40
$("#menuscontainer").click(function() {
    $(this).focus();
});
$("#menuscontainer").blur(function(){
    $(this).hide();
});

A mí me funciona bien.

usuario212621
fuente
3
Este es el que usaría. Puede que no sea perfecto, pero como programador de pasatiempos, es lo suficientemente simple como para entenderlo claramente.
kevtrout
el desenfoque es un movimiento fuera de la #menucontainerpregunta sobre un clic
borrel
44
@borrel blur no es un movimiento fuera del contenedor. El desenfoque es lo opuesto al foco, estás pensando en el mouseout. Esta solución funcionó particularmente bien para mí cuando estaba creando texto de "hacer clic para editar", donde intercambiaba entre texto plano y campos de entrada en los clics.
parker.siky
Tuve que agregar tabindex="-1"al #menuscontainerpara que funcione. Parece que si coloca una etiqueta de entrada dentro del contenedor y hace clic en él, el contenedor se oculta.
tyrion
el evento mouseleave es más adecuado para menús y contenedores (ref: w3schools.com/jquery/… )
Evgenia Manolova
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 controladores de clic de salida
  • un controlador de clics ( espacio de nombres ) está vinculado al documento (si aún no está allí)
  • en cualquier clic en el documento, el evento clickoutside se activa para aquellos elementos en esa matriz que no son iguales o primarios del destino de eventos de clic
  • además, event.target para el evento clickoutside se establece en el elemento en el que hizo clic el usuario (para que sepa qué hizo clic el usuario, no solo que hizo clic afuera)

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.

Wolfram
fuente
Bonito complemento. El enlace de "eventos externos" está inactivo, mientras que el enlace de la publicación de blog está activo y proporciona un complemento muy útil para eventos de tipo "clickoutside". También tiene licencia MIT.
TechNyquist
Gran complemento Funcionó a la perfección. El uso es así:$( '#element' ).on( 'clickoutside', function( e ) { .. } );
Gavin
33

¡Esto funcionó para mí perfectamente!

$('html').click(function (e) {
    if (e.target.id == 'YOUR-DIV-ID') {
        //do something
    } else {
        //do something
    }
});
srinath
fuente
Esta solución funcionó bien para lo que estaba haciendo, donde todavía necesitaba el evento de clic para aparecer, gracias.
Mike
26

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

Al no encontrar respuestas satisfactorias arriba me impulsó a escribir esta publicación de blog el otro día. Para los más pedantes, hay una serie de problemas para 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 desenlazar 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 utiliza event.stopPropogation () en un evento de clic, ningún otro elemento de su página puede tener una función de hacer clic para cerrar.
  3. Adjuntar un controlador de eventos de clic al elemento del cuerpo indefinidamente no es una solución eficaz
  4. Al comparar el objetivo del evento y sus padres con el creador del controlador, se supone que lo que desea es cerrar el menú cuando hace clic en él, cuando lo que realmente quiere es cerrarlo cuando hace clic en cualquier lugar de la página.
  5. Escuchar eventos en el elemento body hará que su código sea más frágil. Un estilo tan inocente como esto lo rompería:body { margin-left:auto; margin-right: auto; width:960px;}
34m0
fuente
2
"Si hace clic en el menú, o fuera del menú, ¿debería cerrarse?" no siempre. Cancelar un clic arrastrando un elemento aún activará un clic a nivel de 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 "clic de salida" que permitiría hacer clic internamente.
zzzzBov
25

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

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

    })
});
Ben B
fuente
25

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

El script anterior ocultará divsi divse activa fuera del evento click.

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

Jitendra Damor
fuente
22

Solución1

En lugar de usar event.stopPropagation () que puede tener algunos efectos secundarios, solo defina una variable de bandera simple y agregue una ifcondición. Probé esto y trabajé 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";
    }
});

Solution2

Con solo una ifcondición simple :

$(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
    }
});
Iman Sedighi
fuente
Usé esta solución con una bandera booleana y también es buena con un DOm articulado y también si dentro de #menucontainer hay muchos otros elementos
Migio B
La solución 1 funciona mejor, porque maneja casos cuando el destino del clic se elimina del DOM cuando el evento de tiempo se propaga al documento.
Alice
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á dentro del área del menú.

Chris MacDonald
fuente
18

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, enlace un controlador de clic al cuerpo que se oculta #menuscontainersolo si el objetivo (del clic) no es hijo de él.

Chu Yeow
fuente
17

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 al hacer clic en un segundo menú mientras está abierto uno primero, dejará el primero abierto en la solución stopPropagation.

Bohdan Lyzanets
fuente
16

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 elementos secundarios, simplemente verifique la ruta para ese elemento DOM específico. También se puede usar para verificar múltiples elementos al lógicamente ORla verificación 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 su 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
  }
});
Dan Philip
fuente
1
No funciona en Edge.
Leyendas
event.pathno es una cosa
Andrew
14

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

Aquí está la solución JavaScript vainilla para futuros espectadores.

Al hacer clic en cualquier elemento del documento, si la identificación del elemento seleccionado se alterna o si el elemento oculto no está oculto y el elemento oculto no contiene el elemento seleccionado, alterne 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);
})();

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 contengan el elemento seleccionado y que no estén ocultos
  3. Si el elemento en el que se hizo clic es un conmutador, alternar 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>

usuario4639281
fuente
13

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>

Jovanni G
fuente
Te falta mi respuesta, creo. stackoverflow.com/a/47755925/6478359
Muhammet Can TONBUL
Esta debería ser la respuesta aceptada, directamente al grano. ¡¡¡Gracias!!!
Lucky Lam
8

Si está creando scripts para IE y FF 3. * y solo desea saber si el clic se produjo dentro de un área de cuadro determinada, también podría 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;}
Erik
fuente
8

En lugar de utilizar la interrupción de flujo, el evento de desenfoque / enfoque o cualquier otra técnica complicada, simplemente combine el flujo de eventos 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 haga clic fuera del detector de eventos, simplemente:

$(document).off("click.menu-outside");
mems
fuente
Para mí esta fue la mejor solución. Lo usé en una devolución de llamada después de la animación, por lo que necesitaba el evento de separación en algún momento. +1
qwertzman
Hay una verificación redundante aquí. si el elemento es de hecho #menuscontainertodavía estás pasando por sus padres. primero debes verificar eso, y si no es ese elemento, sube el árbol DOM.
vsync
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 utiliza el método de encadenamiento
mems
8

Utilizar:

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

Si alguien curioso aquí es la solución de 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';
}

});

Walt
fuente
7

Aquí hay una solución simple de 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;
})
Duannx
fuente
"Actualizar con ES6" es una afirmación bastante audaz, cuando lo único que está haciendo con ES6 es hacer en () => {}lugar de hacerlo function() {}. Lo que tienes allí está clasificado como JavaScript simple con un toque de ES6.
MortenMoulder
@MortenMoulder: Ya. Es solo para llamar la atención, aunque en realidad es ES6. Pero solo mira la solución. Yo creo que es bueno.
Duannx
Es vanilla JS y funciona para el objetivo de evento eliminado de DOM (por ejemplo, cuando se selecciona el valor de la ventana emergente interna, cerrando inmediatamente la ventana emergente). +1 de mi parte!
Alice
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 event.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");
    }
});
Salman A
fuente
6

He usado el siguiente script y 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í

Rinto George
fuente
5
$(document).click(function() {
    $(".overlay-window").hide();
});
$(".overlay-window").click(function() {
    return false;
});

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

Serbal
fuente
5

Vota por la respuesta más popular, pero agrega

&& (e.target != $('html').get(0)) // ignore the scrollbar

entonces, un clic en una barra de desplazamiento no [oculta o lo que sea] su elemento objetivo.

bbe
fuente
5

Para un uso más fácil y un código más expresivo, creé un complemento jQuery para esto:

$('div.my-element').clickOut(function(target) { 
    //do something here... 
});

Nota: target es el elemento en el que el usuario hizo clic. Pero la devolución de llamada todavía se ejecuta en el contexto del elemento original, por lo que puede utilizar esto como esperaría en una devolución de llamada jQuery.

Enchufar:

$.fn.clickOut = function (parent, fn) {
    var context = this;
    fn = (typeof parent === 'function') ? parent : fn;
    parent = (parent instanceof jQuery) ? parent : $(document);

    context.each(function () {
        var that = this;
        parent.on('click', function (e) {
            var clicked = $(e.target);
            if (!clicked.is(that) && !clicked.parents().is(that)) {
                if (typeof fn === 'function') {
                    fn.call(that, clicked);
                }
            }
        });

    });
    return context;
};

De forma predeterminada, el detector de eventos de clic se coloca en el documento. Sin embargo, si desea limitar el alcance del detector de eventos, puede pasar un objeto jQuery que represente un elemento de nivel principal que será el principal superior en el que se escucharán los clics. Esto evita escuchas de eventos a nivel de documento innecesarios. Obviamente, no funcionará a menos que el elemento principal proporcionado sea un elemento primario de su elemento inicial.

Use así:

$('div.my-element').clickOut($('div.my-parent'), function(target) { 
    //do something here...
});
Matt Goodwin
fuente