javascript - tutorial - jquery w3schools




Como faço para detectar um clique fora de um elemento? (20)

Eu tenho alguns menus HTML, que mostro completamente quando um usuário clica na cabeça desses menus. Eu gostaria de esconder esses elementos quando o usuário clica fora da área dos menus.

É algo assim possível com jQuery?

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

Como detectar um clique fora de um elemento?

A razão pela qual esta pergunta é tão popular e tem tantas respostas é que é enganosamente complexa. Depois de quase oito anos e dúzias de respostas, fico genuinamente surpreso ao ver quão pouco cuidado foi dado à acessibilidade.

Eu gostaria de esconder esses elementos quando o usuário clica fora da área dos menus.

Esta é uma causa nobre e é o problema real . O título da pergunta - que é o que a maioria das respostas parece tentar abordar - contém um infeliz arenque vermelho.

Dica: é a palavra "clique" !

Você não quer realmente ligar os manipuladores de cliques.

Se você estiver vinculando os manipuladores de clique para fechar a caixa de diálogo, você já falhou. A razão pela qual você falhou é que nem todos acionam eventos de click . Os usuários que não usam um mouse poderão escapar de sua caixa de diálogo (e seu menu pop-up é indiscutivelmente um tipo de caixa de diálogo) pressionando Tab , e eles não conseguirão ler o conteúdo por trás da caixa de diálogo sem dispararem subseqüentemente um click evento.

Então, vamos reformular a questão.

Como alguém fecha uma caixa de diálogo quando um usuário termina?

Esse é o objetivo. Infelizmente, agora precisamos vincular o evento userisfinishedwiththedialog e essa vinculação não é tão direta.

Então, como podemos detectar que um usuário terminou de usar um diálogo?

evento de focusout

Um bom começo é determinar se o foco deixou o diálogo.

Sugestão: tenha cuidado com o evento de blur , o blur não se propaga se o evento estiver ligado à fase de bolhas!

O focusout do jQuery vai fazer muito bem. Se você não pode usar o jQuery, então você pode usar blur durante a fase de captura:

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

Além disso, para muitos diálogos, você precisará permitir que o contêiner ganhe foco. Adicione tabindex="-1" para permitir que o diálogo receba o foco dinamicamente sem interromper o fluxo de tabulação.

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

Se você jogar com essa demonstração por mais de um minuto, deverá começar a ver os problemas rapidamente.

A primeira é que o link na caixa de diálogo não é clicável. Tentar clicar nele ou na guia levará ao fechamento da caixa de diálogo antes que a interação ocorra. Isso ocorre porque a focalização do elemento interno aciona um evento de focusout antes de acionar um evento focusin novamente.

A correção é enfileirar a mudança de estado no loop de eventos. Isso pode ser feito usando setImmediate(...) ou setTimeout(..., 0) para navegadores que não suportam setImmediate . Depois de enfileirado, ele pode ser cancelado por um focusin subsequente:

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

A segunda questão é que a caixa de diálogo não fechará quando o link for pressionado novamente. Isso ocorre porque a caixa de diálogo perde o foco, acionando o comportamento de fechamento, após o qual o clique do link aciona a caixa de diálogo para reabrir.

Semelhante à edição anterior, o estado de foco precisa ser gerenciado. Dado que a alteração de estado já foi enfileirada, é apenas uma questão de manipular eventos de foco nos acionadores de diálogo:

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

Se você pensou que estava lidando com os estados de foco, há mais coisas a fazer para simplificar a experiência do usuário.

Este é frequentemente um recurso "interessante", mas é comum que quando você tem um modal ou pop-up de qualquer tipo que a tecla Esc irá fechá-lo.

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>

Se você souber que tem elementos focáveis ​​na caixa de diálogo, não precisará focar a caixa de diálogo diretamente. Se você estiver criando um menu, poderá focar o primeiro item de menu.

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.

Funções de WAI-ARIA e outro suporte de acessibilidade

Esta resposta, esperançosamente, cobre os fundamentos do suporte acessível a teclado e mouse para este recurso, mas como já é bastante considerável, evitarei qualquer discussão sobre os atributos e funções do WAI-ARIA , porém eu recomendo que os implementadores se refiram à especificação para detalhes. em quais papéis eles devem usar e quaisquer outros atributos apropriados.


NOTA: O uso de stopEventPropagation() é algo que deve ser evitado, pois interrompe o fluxo normal de eventos no DOM. Veja este artigo para mais informações. Considere usar esse método .

Anexe um evento de clique ao corpo do documento que fecha a janela. Anexe um evento de clique separado ao contêiner que interrompe a propagação para o corpo do documento.

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

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

Agora há um plugin para isso: eventos externos ( post no blog )

O seguinte acontece quando um manipulador de clique (WLOG) é vinculado a um elemento:

  • o elemento é adicionado a um array que contém todos os elementos com manipuladores clickoutside
  • um manipulador de cliques ( namespaced ) está vinculado ao documento (se não estiver lá)
  • em qualquer clique no documento, o evento clickoutside é acionado para os elementos nessa matriz que não são iguais ou um pai do destino click- event
  • Além disso, o event.target para o evento clickoutside é definido para o elemento em que o usuário clicou (para que você saiba o que o usuário clicou, não apenas que ele clicou fora)

Portanto, nenhum evento é interrompido da propagação e manipuladores de cliques adicionais podem ser usados ​​"acima" do elemento com o manipulador externo.


As outras soluções aqui não funcionaram para mim, então eu tive que usar:

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

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

Não há problema em css-tricks.com/dangers-stopping-event-propagation e suporta melhor vários menus na mesma página, onde clicar em um segundo menu enquanto o primeiro é aberto deixará o primeiro aberto na solução stopPropagation.


Depois de pesquisar eu encontrei três soluções de trabalho (eu esqueci os links da página para referência)

Primeira solução

<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 solução

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

Terceira solução

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

Eu não acho que você realmente precise fechar o menu quando o usuário clicar fora; o que você precisa é que o menu seja fechado quando o usuário clicar em qualquer lugar da página. Se você clicar no menu, ou fora do menu, deve fechar certo?

Não encontrando respostas satisfatórias acima me levou a escrever este post no outro dia. Para os mais pedantes, há várias dicas para anotar:

  1. Se você anexar um manipulador de eventos de clique ao elemento body no momento do clique, certifique-se de aguardar o segundo clique antes de fechar o menu e de desassociar o evento. Caso contrário, o evento de clique que abriu o menu será enviado ao ouvinte que precisa fechar o menu.
  2. Se você usar event.stopPropogation () em um evento click, nenhum outro elemento na sua página poderá ter um recurso click-anywhere-to-close.
  3. Anexar um manipulador de eventos de clique ao elemento do corpo indefinidamente não é uma solução de desempenho
  4. Comparando o alvo do evento, e seus pais ao criador do manipulador, assume que o que você quer é fechar o menu quando você clica nele, quando o que você realmente quer é fechá-lo quando você clica em qualquer lugar da página.
  5. Ouvir eventos no elemento body tornará seu código mais frágil. Estilizar tão inocente como isso iria quebrá-lo: body { margin-left:auto; margin-right: auto; width:960px;} body { margin-left:auto; margin-right: auto; width:960px;}

Eu tenho um aplicativo que funciona de forma semelhante ao exemplo de Eran, exceto que eu anexar o evento click ao corpo quando abro o menu ... Mais ou menos assim:

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

  event.stopPropagation();
});

Mais informações sobre a função one() do jQuery


Isso funcionou para mim perfeitamente !!

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

Uma solução simples para a situação é:

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

O script acima irá esconder o div se fora do evento de clique div é acionado.

Você pode ver o seguinte blog para obter mais informações: http://www.codecanal.com/detect-click-outside-div-using-javascript/


Verifique o destino do evento de clique da janela (ele deve se propagar para a janela, desde que não seja capturado em outro local) e verifique se não é nenhum dos elementos do menu. Se não for, então você está fora do seu menu.

Ou verifique a posição do clique e veja se ele está contido na área do menu.


Você pode ouvir um evento de clique no document e, em seguida, certificar-se de que #menucontainer não seja um ancestral ou o destino do elemento clicado usando .closest() .

Se não estiver, o elemento clicado estará fora do #menucontainer e você poderá ocultá-lo com segurança.

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

Editar - 2017-06-23

Você também pode limpar após o ouvinte de eventos se planeja descartar o menu e deseja parar de ouvir eventos. Essa função limpará apenas o ouvinte recém-criado, preservando qualquer outro ouvinte de clique no document . Com a sintaxe do 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 aqueles que não querem usar o jQuery. Aqui está o código acima em 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: Isto é baseado no comentário do Alex para apenas usar !element.contains(event.target) invés da parte do jQuery.

Mas o element.closest() agora também está disponível em todos os principais navegadores (a versão do W3C difere um pouco da versão do jQuery). Polyfills pode ser encontrado aqui: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest


Em vez disso, usando interrupção de fluxo, evento de blur / focus ou qualquer outra técnica complicada, simplesmente combine o fluxo de evento com o parentesco do 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 remover o clique fora do ouvinte de eventos, simplesmente:

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

Esta é a minha solução para este problema:

$(document).ready(function() {
  $('#user-toggle').click(function(e) {
    $('#user-nav').toggle();
    e.stopPropagation();
  });

  $('body').click(function() {
    $('#user-nav').hide(); 
  });

  $('#user-nav').click(function(e){
    e.stopPropagation();
  });
});

Função:

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

E as funções são muito simples:

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

Para um uso mais fácil e um código mais expressivo, criei um plugin jQuery para isso:

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

Nota: target é o elemento que o usuário realmente clicou. Mas o retorno de chamada ainda é executado no contexto do elemento original, portanto, você pode utilizar isso como esperado em um retorno de chamada do jQuery.

Plugar:

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

Por padrão, o ouvinte de evento de clique é colocado no documento. No entanto, se você quiser limitar o escopo do ouvinte de evento, poderá transmitir um objeto jQuery representando um elemento de nível pai que será o pai superior no qual os cliques serão ouvidos. Isso evita ouvintes de eventos de nível de documento desnecessários. Obviamente, não funcionará a menos que o elemento pai fornecido seja pai de seu elemento inicial.

Use assim:

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

Vota para a resposta mais popular, mas adiciona

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

então, um clique em uma barra de rolagem não [oculta ou o que quer que seja] seu elemento de destino.


Anexe um ouvinte de evento de clique no documento. Dentro do ouvinte de eventos, você pode examinar o objeto de evento , em particular, o event.target para ver em qual elemento foi clicado:

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

O evento tem uma propriedade chamada event.path do elemento que é uma "lista ordenada estática de todos os seus ancestrais em ordem de árvore" . Para verificar se um evento se originou de um elemento DOM específico ou de um dos seus filhos, basta verificar o caminho para esse elemento DOM específico. Ele também pode ser usado para verificar vários elementos logicamente ORao verificar o elemento na somefunção.

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

Então, para o seu caso, deve ser

$("body").click(function() {
  target = $("#menuscontainer")[0];
  flag = event.path.some(function(el, i, arr) {
    return (el == target)
  });
  if (!flag) {
    // Hide the menus
  }
});

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

Funciona para mim muito bem.





jquery