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


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 los menús.

¿Es posible algo así con jQuery?

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

2023


origen


Respuestas:


NOTA: Usando stopEventPropagation() es algo que debe evitarse ya que rompe el flujo de eventos normales en el DOM. Ver Este artículo para más información. Considere usar este método en lugar.

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

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

$('#menucontainer').click(function(event){
    event.stopPropagation();
});

1618



Puedes escuchar por un hacer clic evento en document y luego asegúrate #menucontainer no es un ancestro o el objetivo del elemento cliqueado al usar .closest().

Si no es así, entonces el elemento cliqueado está fuera del #menucontainer y puedes esconderlo con seguridad

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

Editar - 2017-06-23

También puede limpiar después del oyente del evento si planea cerrar el menú y desea dejar de escuchar los eventos. Esta función limpiará solo el oyente creado recientemente, preservando cualquier otro oyente de clic en document. Con la sintaxis ES2015:

export function hideOnClickOutside(selector) {
  const outsideClickListener = (event) => {
    if (!$(event.target).closest(selector).length) {
      if ($(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 plain vanillaJS (ECMAScript6).

function hideOnClickOutside(element) {
    const outsideClickListener = event => {
        if (!element.contains(event.target)) { // or use: event.target.closest(selector) === null
            if (isVisible(element)) {
                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 simplemente usar !element.contains(event.target) en 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 jQuery). Los polifills se pueden encontrar aquí: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest


1123



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

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

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

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

Sugerencia: es la palabra "hacer clic"!

En realidad, no desea vincular los manejadores de clics.

Si enlaza los manejadores de clics para cerrar el diálogo, ya ha fallado. La razón por la que has fallado es porque no todos desencadenan click eventos. Los usuarios que no usen un mouse podrán escapar de su diálogo (y su menú emergente es posiblemente un tipo de diálogo) presionando Lengüeta, y entonces no podrán leer el contenido detrás del diálogo sin desencadenar un click evento.

Entonces, reformulemos la pregunta.

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

Este es el objetivo. Desafortunadamente, ahora tenemos que unir el userisfinishedwiththedialog evento, y ese enlace no es tan directo.

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

focusout evento

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

Sugerencia: tenga cuidado con el blur evento, blur no se propaga si el evento estaba ligado a la fase de burbujeo!

jQuery's focusout lo hará bien. Si no puede usar jQuery, puede usar blur durante 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. Añadir tabindex="-1" para permitir que el diálogo reciba el enfoque 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.

El primero es que no se puede hacer clic en el enlace del cuadro de diálogo. Intentar hacer clic en él o tabularlo llevará al cierre del diálogo antes de que la interacción tenga lugar. Esto se debe a que al enfocar el elemento interno se desencadena una focusout evento antes de activar un focusin evento de nuevo.

La solución es poner en cola el cambio de estado en el ciclo de eventos. Esto se puede hacer mediante el uso de setImmediate(...), o setTimeout(..., 0) para navegadores que no son compatibles setImmediate. Una vez en cola, puede ser cancelado por una subsiguiente 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 diálogo no se cerrará cuando se vuelva a presionar el enlace. Esto se debe a que el diálogo pierde el foco, lo que desencadena el comportamiento de cierre, después del cual el clic de enlace desencadena el diálogo para volver 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 se trata de manejar eventos de enfoque en los activadores 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 creía que había terminado al manejar los estados de enfoque, puede hacer más para simplificar la experiencia del usuario.

Suele ser una característica "agradable de tener", pero es común que cuando tiene un menú emergente o modal de cualquier tipo que Esc la tecla lo cerrará

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 diálogo, no necesitará enfocar el 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.


Roles de WAI-ARIA y otro soporte de accesibilidad

Esta respuesta, con suerte, cubre los aspectos básicos del soporte de teclado y mouse accesible para esta función, pero como ya es bastante considerable, voy a evitar cualquier discusión sobre Roles y atributos de WAI-ARIA, Sin embargo, yo altamente recomendamos que los implementadores se refieran a la especificación para obtener detalles sobre qué roles deberían usar y cualquier otro atributo apropiado.


184



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

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

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 como esto:

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

  event.stopPropagation();
});

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


118



$("#menuscontainer").click(function() {
    $(this).focus();
});
$("#menuscontainer").blur(function(){
    $(this).hide();
});

Funciona bien para mí


32



Ahora hay un complemento para eso: eventos fuera (entrada en el blog)

Lo siguiente sucede cuando clickoutside controlador (WLOG) está vinculado a un elemento:

  • el elemento se agrega a una matriz que contiene todos los elementos con clickoutside manejadores
  • un (namespaced) hacer clic controlador está vinculado al documento (si no está ya allí)
  • en cualquier hacer clic en el documento, el clickoutside evento se desencadena para los elementos en esa matriz que no son iguales o un padre de la matriz hacer clic-eventos objetivo
  • adicionalmente, el evento. objetivo para el clickoutside el evento se establece en el elemento en el que el usuario hizo clic (por lo que incluso sabe lo que el usuario hizo clic, no solo que hizo clic afuera)

Entonces, no se detienen eventos de propagación y adicionales hacer clic los manipuladores pueden usarse "arriba" del elemento con el manipulador externo.


31



¡Esto funcionó para mí perfectamente!

$('html').click(function (e) {
    if (e.target.id == 'YOUR-DIV-ID') {
        //do something
    } else {
        //do something
    }
});

26



Después de investigar, encontré tres soluciones de trabajo (Olvidé los enlaces de la página para 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>

24