Como detecto um clique fora de um elemento?

2486

Eu tenho alguns menus HTML, que mostro completamente quando um usuário clica no cabeçalho desses menus. Gostaria de ocultar esses elementos quando o usuário clica fora da área de menus.

É possível algo assim com o jQuery?

$("#menuscontainer").clickOutsideThisElement(function() {
    // Hide the menus
});
Sergio del Amo
fonte
47
Aqui está uma amostra dessa estratégia: jsfiddle.net/tedp/aL7Xe/1
Ted
18
Como Tom mencionou, convém ler css-tricks.com/dangers-stopping-event-propagation antes de usar essa abordagem. Essa ferramenta jsfiddle é bem legal.
Jon Coombs
3
obter uma referência para o elemento e, em seguida, event.target e, finalmente = ou == ambos, em seguida, executar o código em conformidade ..!
Rohit Kumar
Eu recomendo o uso github.com/gsantiago/jquery-clickout :)
Rômulo M. Farias
2
Solução Vanilla JS com event.targete sem event.stopPropagation .
lowtechsun

Respostas:

1812

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

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 no corpo do documento.

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

$('#menucontainer').click(function(event){
    event.stopPropagation();
});
Eran Galperin
fonte
708
Isso quebra o comportamento padrão de muitas coisas, incluindo botões e links, contidos no #menucontainer. Estou surpreso que esta resposta seja tão popular.
Art
75
Isso não quebra o comportamento de nada dentro de #menucontainer, pois está no final da cadeia de propagação para qualquer coisa dentro dele.
Eran Galperin
94
é muito bonito, mas você $('html').click()não deve usar o corpo. O corpo sempre tem a altura do seu conteúdo. Não há muito conteúdo ou a tela é muito alta, funciona apenas na parte preenchida pelo corpo.
meo
103
Também estou surpreso que esta solução tenha tantos votos. Isto irá falhar por qualquer fora elemento que tem stopPropagation jsfiddle.net/Flandre/vaNFw/3
Andre
140
Philip Walton explica muito bem por que esta resposta não é a melhor solução: css-tricks.com/dangers-stopping-event-propagation
Tom
1387

Você pode ouvir um evento de cliquedocument e, em seguida, verifique se #menucontainernão é um ancestral ou o destino do elemento clicado usando .closest().

Caso contrário, o elemento clicado está fora do #menucontainere você pode ocultá-lo com segurança.

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

Editar - 23-06-2017

Você também pode limpar após o ouvinte de eventos se planejar descartar o menu e desejar parar de ouvir os eventos. Esta função limpará apenas o ouvinte recém-criado, preservando outros ouvintes de clique document. Com a sintaxe do 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 quem não quer usar o jQuery. Aqui está o código acima em plain vanillaJS (ECMAScript6).

function hideOnClickOutside(element) {
    const outsideClickListener = event => {
        if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
          element.style.display = 'none'
          removeClickListener()
        }
    }

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

    document.addEventListener('click', outsideClickListener)
}

const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ) // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js 

NOTA: Isso é baseado no comentário de Alex para usar apenas em !element.contains(event.target)vez da parte do jQuery.

Mas element.closest()agora também está disponível em todos os principais navegadores (a versão W3C difere um pouco da jQuery). Os polyfills podem ser encontrados aqui: Element.closest ()

Edição - 2020-05-21

No caso em que você deseja que o usuário possa clicar e arrastar dentro do elemento, solte o mouse fora do elemento, sem fechar o 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);

E em 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
fonte
28
Tentei muitas das outras respostas, mas apenas essa funcionou. Obrigado. O código que acabei usando foi o seguinte: $ (document) .click (function (event) {if ($ (event.target) .closest ('. Window'). Length == 0) {$ ('. Window' ) .fadeOut ('rápido');}});
Pistos
39
Na verdade, acabei optando por esta solução porque ela suporta melhor vários menus na mesma página, onde clicar em um segundo menu enquanto um primeiro é aberto deixará o primeiro aberto na solução stopPropagation.
Umassthrower
13
Excelente resposta. Este é o caminho a seguir quando você tem vários itens que deseja fechar.
João
5
Essa deve ser a resposta aceita devido à falha que a outra solução possui com event.stopPropagation ().
tomate
20
Sem jQuery - !element.contains(event.target)usando Node.contains ()
Alex Ross
304

Como detectar um clique fora de um elemento?

A razão pela qual essa pergunta é tão popular e tem tantas respostas é que ela é enganosamente complexa. Depois de quase oito anos e dezenas de respostas, estou genuinamente surpreso ao ver quão pouco cuidado foi dado à acessibilidade.

Gostaria de ocultar esses elementos quando o usuário clica fora da área de menus.

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

Dica: é a palavra "clique" !

Na verdade, você não deseja vincular manipuladores de cliques.

Se você está vinculando manipuladores de clique para fechar a caixa de diálogo, você já falhou. O motivo pelo qual você falhou é que nem todos acionam clickeventos. Os usuários que não usarem o mouse poderão escapar da sua caixa de diálogo (e seu menu pop-up é sem dúvida um tipo de caixa de diálogo) pressionando Tabe, em seguida, não poderão ler o conteúdo por trás da caixa de diálogo sem disparar um clickevento posteriormente .

Então, vamos reformular a pergunta.

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

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

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

focusout evento

Um bom começo é determinar se o foco saiu da caixa de diálogo.

Dica: tenha cuidado com o blurevento, blurnão se propaga se o evento foi vinculado à fase de bolhas!

jQuery focusoutvai fazer muito bem. Se você não pode usar o jQuery, pode usar blurdurante a fase de captura:

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

Além disso, em muitas caixas de diálogo, você precisará permitir que o contêiner ganhe foco. Adicionar tabindex="-1"para permitir que o diálogo receba 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, deve começar a ver rapidamente os problemas.

A primeira é que o link na caixa de diálogo não é clicável. Tentar clicar nele ou na guia dele levará ao fechamento do diálogo antes que a interação ocorra. Isso ocorre porque o foco no elemento interno aciona um focusoutevento antes de acioná-lo focusinnovamente.

A correção é enfileirar a alteração de estado no loop de eventos. Isso pode ser feito usando setImmediate(...)ou setTimeout(..., 0)para navegadores que não suportam setImmediate. Uma vez na fila, ele pode ser cancelado por um subseqüente 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 segunda questão é que a caixa de diálogo não será fechada 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 no link aciona a caixa de diálogo para reabrir.

Semelhante à edição anterior, o estado do foco precisa ser gerenciado. Dado que a mudança de estado já foi enfileirada, é apenas uma questão de lidar com eventos de foco nos gatilhos da caixa 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'));  
  }
});


Esc chave

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

Geralmente, é um recurso "bom de ter", mas é comum que, quando você tem um modal ou pop-up de qualquer tipo, a Escchave o feche.

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


Se você souber que possui elementos de foco 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 do menu.

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


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

Esperamos que esta resposta cubra o básico do suporte acessível de teclado e mouse para esse recurso, mas como já é bastante considerável, evitarei qualquer discussão sobre as funções e atributos do WAI-ARIA , no entanto, recomendo vivamente que os implementadores consultem a especificação para obter detalhes sobre quais funções eles devem usar e quaisquer outros atributos apropriados.

zzzzBov
fonte
29
Esta é a resposta mais completa, com explicações e acessibilidade em mente. Eu acho que essa deve ser a resposta aceita, já que a maioria das outras respostas trata apenas de cliques e são apenas trechos de código descartados sem explicações.
Cyrille
4
Incrível, bem explicado. Eu apenas usei essa abordagem em um Reagir Component e funcionou perfeitamente graças
David Lavieri
2
@zzzzBov obrigado pela resposta profunda, estou tentando alcançá-lo em vanilla JS, e estou um pouco perdido com todas as coisas de jquery. existe algo semelhante no vanilla js?
precisa saber é o seguinte
2
@zzzzBov não, eu não estou olhando para você escrever uma versão livre de jQuery, é claro, vou tentar fazer isso e acho que o melhor é fazer uma nova pergunta aqui, se eu realmente ficar preso. Muito obrigado novamente.
precisa saber é o seguinte
7
Embora este seja um ótimo método para detectar cliques em listas suspensas personalizadas ou outras entradas, nunca deve ser o método preferido para modais ou pop-ups por dois motivos. O modal será fechado quando o usuário alternar para outra guia ou janela ou abrir um menu de contexto, o que é realmente irritante. Além disso, o evento 'click' é acionado com o mouse para cima, enquanto o evento 'focusout' dispara no instante em que você pressiona o mouse. Normalmente, um botão só executa uma ação se você pressionar o botão do mouse e depois soltá-lo. A maneira adequada e acessível de fazer isso para os modais é adicionar um botão Fechar com tabulação.
Kevin
144

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

if(!$(event.target).is('#foo'))
{
    // hide menu
}
Dennis
fonte
Publiquei outro exemplo prático de como usar o event.target para evitar o acionamento de outros manipuladores html fora da interface do usuário do Jquery ao incorporá-los em sua própria caixa pop-up: Melhor maneira de obter o destino original
Joey T
43
Isso funcionou para mim, exceto que eu adicionei && !$(event.target).parents("#foo").is("#foo")dentro da IFinstrução para que quaisquer elementos filhos não fechem o menu quando clicados.
honyovk
1
Não é possível encontrar melhor que: // Estamos fora de $ (event.target) .parents ('# foo'). Length == 0
AlexG
3
O aprimoramento conciso para lidar com o aninhamento profundo é usar .is('#foo, #foo *'), mas eu não recomendo manipuladores de clique de ligação para resolver esse problema .
ZzzzBov 19/09/16
1
!$(event.target).closest("#foo").lengthseria melhor e evita a necessidade da adição de @ honyovk.
tvanc
127

Eu tenho um aplicativo que funciona de maneira semelhante ao exemplo de Eran, exceto que eu anexo 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 one()função do jQuery

Joe Lencioni
fonte
9
mas, em seguida, se você clicar sobre o próprio menu, em seguida, do lado de fora, não vai funcionar :)
vsync
3
Ajuda a colocar event.stopProgagantion () antes de vincular o ouvinte de clique ao corpo.
Jasper Kennis
4
O problema é que "one" se aplica ao método jQuery de adicionar eventos a uma matriz várias vezes. Portanto, se você clicar no menu para abri-lo mais de uma vez, o evento será vinculado ao corpo novamente e tentará ocultar o menu várias vezes. Um antivírus deve ser aplicado para corrigir esse problema.
marksyzm
Após a ligação usando .one- dentro do $('html')manipulador - escreva a $('html').off('click').
Cody
4
@ Cody Eu não acho que isso ajuda. O onemanipulador chamará automaticamente off(como é mostrado nos documentos do jQuery).
Mariano Desanze 04/04
44

Após a pesquisa, encontrei três soluções de trabalho (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>
Rameez Rami
fonte
8
A terceira solução é de longe a maneira mais elegante de verificar. Também não envolve nenhuma sobrecarga do jQuery. Muito agradável. Ele ajudou muito. Obrigado.
dbarth
Estou tentando obter a terceira solução com vários elementos document.getElementsByClassName, se alguém tiver uma idéia, compartilhe.
lowtechsun
@lowtechsun Você teria que fazer um loop para verificar cada um.
Donnie D'Amato
Gostei muito da terceira solução, no entanto, o clique é acionado antes que minha div comece a mostrar o que a oculta novamente, alguma idéia do porquê?
Indiehjaerta 14/04/19
O terceiro permite o console.log, com certeza, mas não permite que você feche definindo display como none - porque ele fará isso antes que o menu seja exibido. Você tem uma solução para isso?
RolandiXor 9/10
40
$("#menuscontainer").click(function() {
    $(this).focus();
});
$("#menuscontainer").blur(function(){
    $(this).hide();
});

Funciona para mim muito bem.

user212621
fonte
3
Este é o que eu usaria. Pode não ser perfeito, mas como programador de hobby, é simples o suficiente para entender claramente.
Kevtrout
borrão é um movimento fora do #menucontainerque a pergunta era sobre um clique
borrel
4
@borrel blur não é um movimento para fora do contêiner. O desfoque é o oposto do foco, você está pensando em passar o mouse. Essa solução funcionou particularmente bem para mim quando eu estava criando um texto "clique para editar", onde alternava entre texto sem formatação e campos de entrada nos cliques.
Parker.sikand
Eu tive que adicionar tabindex="-1"ao #menuscontainerpara fazê-lo funcionar. Parece que se você colocar uma tag de entrada dentro do contêiner e clicar nele, o contêiner fica oculto.
precisa saber é
o evento mouseleave é mais adequado para os menus e os recipientes (ref: w3schools.com/jquery/... )
Evgenia Manolova
38

Agora existe um plugin para isso: eventos externos ( postagem no blog )

Acontece o seguinte quando um clickoutside manipulador (WLOG) está ligada a um elemento:

  • o elemento é adicionado a uma matriz que contém todos os elementos com manipuladores externos
  • um manipulador de cliques (com espaço para nome ) está vinculado ao documento (se ainda 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 os pais do destino dos eventos de clique
  • Além disso, o event.target do evento clickoutside é definido como o elemento em que o usuário clicou (para que você saiba o que o usuário clicou, não apenas o que clicou fora)

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

Wolfram
fonte
Bom plugin. O link em "eventos externos" está inoperante, enquanto o link de postagem do blog está ativo e fornece um plug-in muito útil para eventos do tipo "clique fora". Também é licenciado pelo MIT.
TechNyquist
Ótimo plugin. Funcionou perfeitamente. O uso é assim:$( '#element' ).on( 'clickoutside', function( e ) { .. } );
Gavin
33

Isso funcionou para mim perfeitamente !!

$('html').click(function (e) {
    if (e.target.id == 'YOUR-DIV-ID') {
        //do something
    } else {
        //do something
    }
});
srinath
fonte
Essa solução funcionou bem para o que eu estava fazendo, onde eu ainda precisava que o evento click aparecesse, obrigado.
23412 Mike
26

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 sair do menu, ele deve fechar, certo?

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

  1. Se você anexar um manipulador de eventos de clique ao elemento body no momento do clique, aguarde o segundo clique antes de fechar o menu e desvincular o evento. Caso contrário, o evento de clique que abriu o menu passará para o ouvinte que precisa fechar o menu.
  2. Se você usar event.stopPropogation () em um evento de clique, nenhum outro elemento da sua página poderá ter um recurso de clique em qualquer lugar para fechar.
  3. Anexar um manipulador de eventos de clique ao elemento body indefinidamente não é uma solução de alto desempenho
  4. A comparação do destino do evento e de seus pais com o criador do manipulador pressupõe que o que você deseja é fechar o menu ao clicar nele, quando o que você realmente deseja é fechá-lo quando clicar em qualquer lugar da página.
  5. Ouvir eventos no elemento body tornará seu código mais frágil. Estilo tão inocente quanto isso quebraria:body { margin-left:auto; margin-right: auto; width:960px;}
34m0
fonte
2
"Se você clicar no menu ou sair do menu, ele deve fechar, certo?" nem sempre. Cancelar um clique arrastando um elemento ainda acionará um clique no nível do documento, mas a intenção não seria continuar fechando o menu. Também existem muitos outros tipos de caixas de diálogo que poderiam usar o comportamento de "clique" que permitiria clicar internamente.
zzzzBov
25

Como outro pôster disse, existem muitas dicas, especialmente se o elemento que você está exibindo (neste caso, um menu) possui elementos interativos. Eu achei o seguinte método 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);
        }

    })
});
benb
fonte
25

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 ocultará o evento divfora do divclique é acionado.

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

Jitendra Damor
fonte
22

Solução1

Em vez de usar event.stopPropagation () que pode ter alguns efeitos colaterais, basta definir uma variável de flag simples e adicionar uma ifcondição. Eu testei isso e funcionou corretamente, sem nenhum efeito colateral do 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";
    }
});

Solução2

Com apenas uma ifcondição simples :

$(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
fonte
Eu usei esta solução com um sinalizador booleano E também é bom com um DOM articulada e também se #menucontainer dentro, há uma série de outros elementos
Migio B
A solução 1 funciona melhor porque lida com casos em que o destino do clique é removido do DOM no momento em que o evento é propagado para o documento.
Alice
21

Verifique o destino do evento de clique na janela (ele deve se propagar para a janela, desde que não seja capturado em nenhum outro lugar) e verifique se ele não é um dos elementos do menu. Caso contrário, você está fora do seu menu.

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

Chris MacDonald
fonte
18

Eu tive sucesso com algo assim:

var $menuscontainer = ...;

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

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

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

A lógica é: quando #menuscontainermostrado, vincule um manipulador de cliques ao corpo que oculta #menuscontainerapenas se o destino (do clique) não for filho dele.

Chu Yeow
fonte
17

Como uma 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á nenhum problema em interromper a propagação de eventos e suporta melhor vários menus na mesma página, onde clicar em um segundo menu enquanto um primeiro é aberto deixará o primeiro aberto na solução stopPropagation.

Bohdan Lyzanets
fonte
16

O evento possui 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 de seus filhos, basta verificar o caminho para esse elemento DOM específico. Também pode ser usado para verificar vários elementos, logicamente ORa verificação de 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;
  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>

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
  }
});
Dan Philip
fonte
1
Não funciona no Edge.
Legends
event.pathnão é uma coisa.
Andrew
14

Eu encontrei esse método em algum plugin de calendário 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
fonte
13

Aqui está a solução de baunilha JavaScript para futuros espectadores.

Ao clicar em qualquer elemento do documento, se o ID do elemento clicado for alternado ou o elemento oculto não estiver oculto e o elemento oculto não contiver o elemento clicado, alterne o 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);
})();

Se você tiver várias alternâncias na mesma página, poderá usar algo como isto:

  1. Adicione o nome da classe hiddenao item recolhível.
  2. Ao clicar no documento, feche todos os elementos ocultos que não contêm o elemento clicado e não estão ocultos
  3. Se o elemento clicado for uma alternância, alterne o 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>

user4639281
fonte
13

Estou surpreso que ninguém realmente reconheceu o 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
fonte
Você está sentindo falta da minha resposta, eu acho. stackoverflow.com/a/47755925/6478359
Muhammet Can TONBUL
Essa deve ser a resposta aceita, direta ao ponto. Obrigado!!!
Lucky Lam
8

Se você estiver criando scripts para o IE e o FF 3. * e quiser apenas saber se o clique ocorreu em uma determinada área da caixa, você também pode 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
fonte
8

Em vez de usar interrupção de fluxo, evento de desfoque / foco ou qualquer outra técnica complicada, basta combinar o fluxo de eventos 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 evento, basta:

$(document).off("click.menu-outside");
mems
fonte
Para mim, essa foi a melhor solução. Usei-o em um retorno de chamada após a animação, então eu precisava do evento de desanexação em algum lugar. +1
qwertzman
Há uma verificação redundante aqui. se o elemento é realmente #menuscontainervocê ainda está passando por seus pais. você deve primeiro verificar isso e, se não for esse elemento, suba na árvore do DOM.
vsync
Certo ! Você pode alterar a condição para if(!($(event.target).is("#menuscontainer") || $(event.target).parents().is("#menuscontainer"))){. É uma otimização pequena, mas ocorre apenas algumas vezes na vida útil do programa: para cada clique, se o click.menu-outsideevento estiver registrado. É mais longo (+32 caracteres) e não usar o método de encadeamento
MEMS
8

Usar:

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

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

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

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

Se alguém curioso aqui é a solução javascript (es6):

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

e es5, apenas no caso de:

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

});

Walt
fonte
7

Aqui está uma solução simples por javascript puro. Está atualizado com o 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
fonte
"Atualizar com o ES6" é uma afirmação bastante ousada, quando a única coisa atualizada com o ES6 é fazer em () => {}vez de function() {}. O que você tem lá é classificado como JavaScript simples com um toque do ES6.
MortenMoulder
@MortenMoulder: Ya. É apenas para atenção, embora seja realmente o ES6. Mas basta olhar para a solução. Eu acho isso bom.
Duannx 18/11/19
É JS baunilha e funciona para o destino do evento removido do DOM (por exemplo, quando o valor do pop-up interno é selecionado, fechando-o imediatamente). +1 de mim!
Alice
6

Conecte um ouvinte de evento de clique no documento. Dentro do ouvinte de evento, você pode olhar para 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");
    }
});
Salman A
fonte
6

Eu usei abaixo script e feito com 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
    }
})

Abaixo encontre o 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>

Você pode ler o tutorial aqui

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

Se você clicar no documento, oculte um determinado elemento, a menos que você clique no mesmo elemento.

Rowan
fonte
5

Voto positivo para a resposta mais popular, mas adicione

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

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

bbe
fonte
5

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 em que o usuário realmente clicou. Mas o retorno de chamada ainda é executado no contexto do elemento original, para que você possa utilizá- lo conforme o 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ê deseja limitar o escopo do ouvinte de eventos, pode transmitir um objeto jQuery que representa um elemento de nível pai que será o pai principal no qual os cliques serão ouvidos. Isso evita ouvintes de eventos desnecessários no nível do documento. Obviamente, ele não funcionará, a menos que o elemento pai fornecido seja um pai do seu elemento inicial.

Use assim:

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