javascript - tutorial - jquery w3schools




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

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.

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

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