javascript - hacer - ocultar div click fuera




¿Cómo detecto un clic fuera de un elemento? (20)

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

¿Es algo así posible con jQuery?

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

¿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 que es engañosamente compleja. Después de casi ocho años y docenas de respuestas, estoy realmente sorprendido de ver lo poco que se ha prestado atención a la accesibilidad.

Me gustaría ocultar estos elementos cuando el usuario haga clic fuera del área de los 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 parece tratar de abordar, contiene una desafortunada pista falsa.

Pista: es la palabra "click" !

En realidad, no desea enlazar controladores de clics.

Si está enlazando los controladores de clic para cerrar el cuadro de diálogo, ya ha fallado. La razón por la que has fallado es que no todos activan los eventos de click . Los usuarios que no utilicen un mouse podrán escapar de su cuadro de diálogo (y su menú emergente es posiblemente un tipo de diálogo) presionando la tecla Tab , y luego no podrán leer el contenido detrás del cuadro de diálogo sin disparar un click posteriormente evento.

Así que vamos a reformular la pregunta.

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

Este es el objetivo. Desafortunadamente, ahora debemos vincular al userisfinishedwiththedialog evento de userisfinishedwiththedialog , y ese enlace no es tan sencillo.

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

evento de focusout

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

Sugerencia: tenga cuidado con el evento de blur , el blur no se propaga si el evento estaba vinculado a la fase de propagación.

focusout jQuery hará bien. Si no puede usar jQuery, entonces 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. Agregue tabindex="-1" para permitir que el cuadro de diálogo reciba un enfoque dinámico 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 durante más de un minuto, deberías comenzar a ver rápidamente los problemas.

La primera es que el enlace en el diálogo no se puede hacer clic. Si intenta hacer clic en él o en la pestaña, se cerrará el cuadro de diálogo antes de que tenga lugar la interacción. Esto se debe a que enfocar el elemento interno activa un evento de focusout antes de focusin activar un evento de focusin .

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 los navegadores que no admiten setImmediate . Una vez en cola, puede ser cancelado por un focusin subsiguiente:

$('.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 cuadro de diálogo pierde el enfoque, lo que desencadena el comportamiento de cierre, después de lo cual el clic del enlace activa el cuadro de diálogo para volver a abrir.

Al igual que en la edición anterior, el estado de enfoque debe ser administrado. Dado que el cambio de estado ya se ha puesto en cola, es solo una cuestión de manejar los 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>

Tecla esc

Si creía que había terminado manejando los estados de enfoque, hay mucho 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 tenga un modal o una ventana emergente de cualquier tipo, la tecla Esc la 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 enfocados 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();
}

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

Esperamos que esta respuesta cubra los aspectos básicos de la compatibilidad con el teclado y el mouse accesibles para esta función, pero como ya es bastante importante, evitaré cualquier discusión sobre los roles y atributos de WAI-ARIA. en qué roles deben usar y cualquier otro atributo apropiado.


NOTA: El uso de stopEventPropagation() es algo que se debe evitar ya que interrumpe el flujo de eventos normal 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();
});

¡Esto me funcionó perfectamente!

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

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

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

  • el elemento se agrega a una matriz que contiene todos los elementos con controladores de clickoutside
  • un controlador de clic ( namespaced ) 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 elemento primario del objetivo click -events
  • adicionalmente, el evento.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 fuera)

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


Como dijo otro póster, hay muchos errores, 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);
        }

    })
});

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 problemas para css-tricks.com/dangers-stopping-event-propagation y es mejor admitir 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.


Encontré este método en algún plugin 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);

Función:

$(function() {
    $.fn.click_inout = function(clickin_handler, clickout_handler) {
        var item = this;
        var is_me = false;
        item.click(function(event) {
            clickin_handler(event);
            is_me = true;
        });
        $(document).click(function(event) {
            if (is_me) {
                is_me = false;
            } else {
                clickout_handler(event);
            }
        });
        return this;
    }
});

Uso:

this.input = $('<input>')
    .click_inout(
        function(event) { me.ShowTree(event); },
        function() { me.Hide(); }
    )
    .appendTo(this.node);

Y las funciones son muy simples:

ShowTree: function(event) {
    this.data_span.show();
}
Hide: function() {
    this.data_span.hide();
}

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

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

No creo que lo que realmente necesite es cerrar el menú cuando el usuario haga clic fuera; 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, ¿verdad?

Al no encontrar respuestas satisfactorias en el párrafo anterior, tuve que escribir este blog el otro día. Para los más pedantes, hay una serie de errores para tomar nota de:

  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 de desenlazar el evento. De lo contrario, el evento de clic que abrió el menú aumentará hasta 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. Al comparar el objetivo del evento y sus padres con el creador del controlador, se supone que lo que desea es cerrar el menú al hacer clic en él, cuando lo que realmente desea es cerrarlo cuando hace clic en cualquier lugar 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 esto lo rompería: body { margin-left:auto; margin-right: auto; width:960px;} body { margin-left:auto; margin-right: auto; width:960px;}

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ú ... Un poco como esto:

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

  event.stopPropagation();
});

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


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á el div si se desencadena el evento div click.

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


Verifique el destino del evento de clic en 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 lo es, entonces estás fuera de tu menú.

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


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

$(document).off("click.menu-outside");

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

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

Nota: target es el elemento que el usuario realmente hizo clic. Pero la devolución de llamada aún se ejecuta en el contexto del elemento original, por lo que puede utilizar esto como se 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 primario que será el principal superior al que se escucharán los clics. Esto evita escuchas de eventos de nivel de documento innecesario. Obviamente, no funcionará a menos que el elemento padre provisto sea un padre de su elemento inicial.

Utilizar como tal:

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

Si está ejecutando 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;}

Upvote para la respuesta más popular, pero agrega

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

por lo tanto, un clic en una barra de desplazamiento no [esconde o lo que sea] su elemento objetivo.


El evento tiene una propiedad llamada event.path del elemento que es una "lista estática ordenada de todos sus ancestros en orden de árbol" . Para verificar si un evento se originó desde un elemento DOM específico o uno de sus hijos, simplemente verifique la ruta para ese elemento DOM específico. También se puede usar para verificar múltiples elementos al ingresar lógicamente ORla verificación de elementos 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;
}
<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>

Así que 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
  }
});

Implementamos una solución, en parte basada en un comentario de un usuario anterior, que funciona perfectamente para nosotros. Lo usamos para ocultar un cuadro de búsqueda / resultados al hacer clic fuera de esos elementos, excluyendo el elemento que originalmente.

// HIDE SEARCH BOX IF CLICKING OUTSIDE
$(document).click(function(event){ 
    // IF NOT CLICKING THE SEARCH BOX OR ITS CONTENTS OR SEARCH ICON 
    if ($("#search-holder").is(":visible") && !$(event.target).is("#search-holder *, #search")) {
        $("#search-holder").fadeOut('fast');
        $("#search").removeClass('active');
    }
});

También verifica si el cuadro de búsqueda también es visible primero, y en nuestro caso, también está eliminando una clase activa en el botón de búsqueda de ocultar / mostrar.


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

Funciona para mí muy bien.





jquery