Diretiva que aciona um evento ao clicar fora do elemento

Eu sei que há muitas perguntas perguntando coisa semelhante. Mas ninguém realmente resolve o meu problema.

Eu estou tentando construir uma diretiva que irá executar uma expressão quando o mouse clica fora do elemento atual.

Por que eu preciso dessa function? Estou construindo um aplicativo, neste aplicativo, há 3 menu suspenso, 5 lista suspensa (como escolhido). Todas estas são diretivas angulares. Vamos supor que todas essas diretivas sejam diferentes. Então nós temos 8 diretivas. E todos eles precisam de uma mesma function: quando clicar no lado do elemento, é necessário ocultar o menu suspenso.

Eu tenho 2 soluções para isso, mas ambos tem problema:

Solução A:

app.directive('clickAnywhereButHere', function($document){ return { restrict: 'A', link: function(scope, elem, attr, ctrl) { elem.bind('click', function(e) { // this part keeps it from firing the click on the document. e.stopPropagation(); }); $document.bind('click', function() { // magic here. scope.$apply(attr.clickAnywhereButHere); }) } } }) 

Aqui está um exemplo para a solução A: clique aqui

Quando você clica no primeiro menu suspenso, em seguida, trabalhando, em seguida, clique em segunda input, o primeiro deve esconder, mas não.

Solução B:

 app.directive('clickAnywhereButHere', ['$document', function ($document) { directiveDefinitionObject = { link: { pre: function (scope, element, attrs, controller) { }, post: function (scope, element, attrs, controller) { onClick = function (event) { var isChild = element.has(event.target).length > 0; var isSelf = element[0] == event.target; var isInside = isChild || isSelf; if (!isInside) { scope.$apply(attrs.clickAnywhereButHere) } } $document.click(onClick) } } } return directiveDefinitionObject }]); 

Aqui está um exemplo para a solução B: clique aqui

Solução A trabalhando se houver apenas uma diretiva na página, mas não no meu aplicativo. Porque ele evita borbulhar, então primeiro quando clico em dropdown1, mostro dropdown1, depois clico em dropdown2, clico em event be prevent, então dropdown1 ainda está lá, mesmo que eu clique fora do dropdown1.

Solução B trabalhando no meu aplicativo que estou usando agora. Mas a questão é que isso causa um problema de desempenho. Um número excessivo de cliques será processado em cada clique em qualquer lugar do aplicativo. No meu caso atual, há 8 links de evento de clique com o documento, portanto, cada clique executa 8 funções. O que causa meu aplicativo muito lento, especialmente no IE8.

Então, existe alguma solução melhor para isso? obrigado

Eu não usaria event.stopPropagation (), uma vez que causa exatamente o tipo de problemas que você vê na solução A. Se possível, eu também recorria a events de desfoque e foco. Quando sua lista suspensa é anexada a uma input, você pode fechá-la quando a input perde o foco.

No entanto, lidar com events de cliques no documento também não é tão ruim, portanto, se você quiser evitar manipular o mesmo evento de cliques várias vezes, basta desvinculá-lo do documento quando ele não for mais necessário. Além da expressão que está sendo avaliada ao clicar fora do menu suspenso, a diretiva precisa saber se está ativa ou não:

 app.directive('clickAnywhereButHere', ['$document', function ($document) { return { link: function postLink(scope, element, attrs) { var onClick = function (event) { var isChild = $(element).has(event.target).length > 0; var isSelf = element[0] == event.target; var isInside = isChild || isSelf; if (!isInside) { scope.$apply(attrs.clickAnywhereButHere) } } scope.$watch(attrs.isActive, function(newValue, oldValue) { if (newValue !== oldValue && newValue == true) { $document.bind('click', onClick); } else if (newValue !== oldValue && newValue == false) { $document.unbind('click', onClick); } }); } }; }]); 

Ao usar a diretiva, apenas forneça outra expressão como esta:

  

Eu não testei sua function onClick. Eu suponho que funciona como esperado. Espero que isto ajude.

Você deve usar ngBlur e ngFocus para mostrar ou ocultar seus dropdowns. Quando alguém clica nele, então fica focado ou fica borrado.

Além disso, consulte esta questão Como definir o foco no campo de input? para definir o foco no AngularJS.

EDIT: Para cada diretiva (menu suspenso ou lista, vamos chamá-lo Y) você terá que mostrá-lo quando você clica em um elemento (vamos chamá-lo X) e você precisa escondê-lo quando você clica em qualquer lugar fora Y (excluindo X obviamente). Y tem propriedade isvisível. Então, quando alguém clica em X (ng-click), defina “isYvisible” para ser verdadeiro e defina Focus em Y. Quando alguém clica fora de Y (ng-blur), então você define “isYvisible” como falso. Você precisa compartilhar uma variável (“isYvisible”) entre dois elementos / diretivas diferentes e você pode usar o escopo do controlador ou dos serviços para fazer isso. Existem outras alternativas para isso também, mas isso está fora do escopo da questão.

Sua solução A é a mais correta, mas você deve adicionar outro parâmetro à diretiva para rastrear se ela está aberta:

 link: function(scope, elem, attr, ctrl) { elem.bind('click', function(e) { // this part keeps it from firing the click on the document. if (isOpen) { e.stopPropagation(); } }); $document.bind('click', function() { // magic here. isOpen = false; scope.$apply(attr.clickAnywhereButHere); }) } 
 post: function ($scope, element, attrs, controller) { element.on("click", function(){ console.log("in element Click event"); $scope.onElementClick = true; $document.on("click", $scope.onClick); }); $scope.onClick = function (event) { if($scope.onElementClick && $scope.open) { $scope.onElementClick = false; return; } $scope.open = false; $scope.$apply(attrs.clickAnywhereButHere) $document.off("click", $scope.onClick); }; } 

Aqui está uma solução que estou usando (possível resposta um pouco atrasada, mas espero que seja útil para os outros que passam por isso)

  link: function (scope, element, attr) { var clickedOutsite = false; var clickedElement = false; $(document).mouseup(function (e) { clickedElement = false; clickedOutsite = false; }); element.on("mousedown", function (e) { clickedElement = true; if (!clickedOutsite && clickedElement) { scope.$apply(function () { //user clicked the element scope.codeCtrl.elementClicked = true; }); } }); $(document).mousedown(function (e) { clickedOutsite = true; if (clickedOutsite && !clickedElement) { scope.$apply(function () { //user clicked outsite the element scope.codeCtrl.elementClicked = false; }); } }); } 

Uma versão um pouco mais simples do que a maioria das respostas vencedoras, para mim é mais claro e funciona muito bem!

 app.directive('clickAnywhereButHere', function() { return { restrict : 'A', link: { post: function(scope, element, attrs) { element.on("click", function(event) { scope.elementClicked = event.target; $(document).on("click", onDocumentClick); }); var onDocumentClick = function (event) { if(scope.elementClicked === event.target) { return; } scope.$apply(attrs.clickAnywhereButHere); $(document).off("click", onDocumentClick); }; } } }; }); 

Aqui está uma solução que eu usei e que só precisa do evento do click (disponível como $ event na diretiva ngClick). Eu queria um menu com itens que, quando clicado, faria:

  • alterna a exibição de um submenu
  • esconder qualquer outro submenu se ele fosse exibido
  • ocultar o submenu se um clique ocorreu do lado de fora.

Este código define a class ‘active’ no item de menu para que possa ser usado para mostrar ou ocultar seu submenu

 // this could also be inside a directive's link function. // each menu element will contain data-ng-click="onMenuItemClick($event)". // $event is the javascript event object made available by ng-click. $scope.onMenuItemClick = function(menuElementEvent) { var menuElement = menuElementEvent.currentTarget, clickedElement = menuElementEvent.target, offRootElementClick; // where we will save angular's event unbinding function if (menuElement !== clickedElement) { return; } if (menuElement.classList.contains('active')) { menuElement.classList.remove('active'); // if we were listening for outside clicks, stop offRootElementClick && offRootElementClick(); offRootElementClick = undefined; } else { menuElement.classList.add('active'); // listen for any click inside rootElement. // angular's bind returns a function that can be used to stop listening // I used $rootElement, but use $document if your angular app is nested in the document offRootElementClick = $rootElement.bind('click', function(rootElementEvent) { var anyClickedElement = rootElementEvent.target; // if it's not a child of the menuElement, close the submenu if(!menuElement.contains(anyClickedElement)) { menuElement.classList.remove('active'); // and stop outside listenting offRootElementClick && offRootElementClick(); offOutsideClick = undefined; } }); } } 

@ lex82 resposta é boa, e constitui a base desta resposta, mas o meu difere de algumas maneiras:

  1. Está no TypeScript
  2. Remove a binding de cliques quando o escopo é destruído, o que significa que você não precisa gerenciar a associação de cliques separadamente com uma propriedade
  3. O tempo limite garante que, se o object com click-out for criado por meio de um evento de mouse, que o mesmo evento de mouse não acione inadvertidamente o mecanismo de fechamento

     export interface IClickOutDirectiveScope extends angular.IScope { clickOut: Function; } export class ClickOutDirective implements angular.IDirective { public restrict = "A"; public scope = { clickOut: "&" } public link: ($scope: IClickOutDirectiveScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes) => void; constructor($timeout: angular.ITimeoutService, $document: angular.IDocumentService) { ClickOutDirective.prototype.link = ($scope: IClickOutDirectiveScope, $element: angular.IAugmentedJQuery, attrs: ng.IAttributes) => { var onClick = (event: JQueryEventObject) => { var isChild = $element[0].contains(event.target); var isSelf = $element[0] === event.target; var isInside = isChild || isSelf; if (!isInside) { if ($scope.clickOut) { $scope.$apply(() => { $scope.clickOut(); }); } } } $timeout(() => { $document.bind("click", onClick); }, 500); $scope.$on("$destroy", () => { $document.unbind("click", onClick); }); } } static factory(): ng.IDirectiveFactory { const directive = ($timeout: angular.ITimeoutService, $document: angular.IDocumentService) => new ClickOutDirective($timeout, $document); directive.$inject = ["$timeout", "$document"]; return directive; } } angular.module("app.directives") .directive("clickOut", ClickOutDirective.factory());