Clic angulaire en dehors d'un événement élément

Je sais qu'il y a de nombreuses questions pour poser une question similaire. Mais personne ne répond vraiment à mon problème.

J'essaie de créer une directive qui exécutera une expression lorsque le clic de la souris est en dehors de l'élément actuel.

Pourquoi j'ai-je besoin de cette fonction? Je crée une application, dans cette application, il y a 3 menus déroulants, 5 liste déroulante (comme choisis). Toutes ces directives sont angulaires. Supposons que toutes ces directives sont différentes. Nous avons donc 8 directives. Et tous ont besoin d'une même fonction: lorsque vous cliquez sur l'élément, devez cacher la liste déroulante.

J'ai 2 solutions pour cela, mais les deux ont eu problème:

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

Voici un exemple pour la solution A: cliquez ici

Lorsque vous cliquez sur le premier menu déroulant, puis sur le travail, puis cliquez sur la deuxième entrée, le premier doit se cacher, mais pas.

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

Voici un exemple pour la solution B: cliquez ici

La solution A fonctionne s'il n'y a qu'une directive dans la page, mais pas dans mon application. Parce que cela empêche de faire des bouillonnements, d'abord lorsque je clique sur la liste déroulante1, affiche la liste déroulante1, puis cliquez sur la liste déroulante2, cliquez sur l'événement, prévoyez-la, alors la liste déroulante1 s'affiche encore, même si je clique sur la liste déroulante1.

La solution B fonctionne dans mon application que j'utilise maintenant. Mais le problème est que cela cause un problème de performance. Trop d'événement de clic doit être géré sur chaque clic sur n'importe où dans l'application. Dans mon cas actuel, il y a 8 liens d'événement de clic avec un document, donc chaque clic exécute 8 fonctions. Ce qui rend mon application très lente, surtout dans IE8.

Donc, y a-t-il une meilleure solution pour cela? Merci

Je n'utiliserais pas event.stopPropagation () car cela provoque exactement le type de problèmes que vous voyez dans la solution A. Si possible, je voudrais également utiliser des événements flous et concentrés. Lorsque votre menu déroulant est attaché à une entrée, vous pouvez le fermer lorsque l'entrée perd la mise au point.

Cependant, gérer les événements de clic sur le document n'est pas si grave non plus, alors si vous souhaitez éviter de manipuler le même événement à plusieurs reprises, débranchez-le du document lorsqu'il n'est plus nécessaire. En plus de l'expression évaluée en cliquant en dehors du menu déroulant, la directive doit savoir si elle est active ou non:

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

Lorsque vous utilisez la directive, il suffit de fournir une autre expression comme ceci:

 <your-dropdown click-anywhere-but-here="close()" is-active="isDropdownOpen()"></your-dropdown> 

Je n'ai pas testé votre fonction onClick. Je suppose que cela fonctionne comme prévu. J'espère que cela t'aides.

Vous devez utiliser ngBlur et ngFocus pour afficher ou masquer vos menus déroulants. Quand quelqu'un clique, il se concentre davantage, il devient flou.

Aussi, reportez-vous à cette question Comment définir le focus sur le champ de saisie? Pour se concentrer sur AngularJS.

EDIT: Pour chaque directive (menu déroulant ou liste, appelons-le Y), vous devrez le montrer lorsque vous cliquez sur un élément (appelons-le X) et vous devez le cacher lorsque vous cliquez n'importe où en dehors de Y (à l'exception de X évidemment). Y a une propriété isYvisisble. Donc, quand quelqu'un clique sur X (ng-clic), définissez "isYvisible" pour être vrai et définissez Focus on Y. Lorsque quelqu'un clique sur Y (ng-blur), vous définissez "isYvisible" comme faux, il se cache. Vous devez partager une variable ("isYvisible") entre deux éléments / directives différents et vous pouvez utiliser la portée du contrôleur ou des services pour le faire. Il y a d'autres solutions de rechange à cela, mais cela ne fait pas partie de la question.

Votre solution A est la plus correcte, mais vous devez ajouter un autre paramètre à la directive pour le suivi si elle est ouverte:

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

Voici une solution que j'ai utilisée qui n'a besoin que de l'événement du clic (disponible en $ événement dans la directive ngClick). Je voulais un menu avec des éléments qui, lorsqu'ils seraient cliqués, seraient:

  • Basculer l'affichage d'un sous-menu
  • Cacher tout autre sous-menu s'il était affiché
  • Masquer le sous-menu si un clic s'est produit à l'extérieur.

Ce code définit la classe 'active' sur l'élément de menu afin qu'il puisse être utilisé pour afficher ou cacher son sous-menu

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

Voici une solution que j'utilise (possible une réponse un peu tardive, mais j'espère que cela peut être utile pour les autres personnes qui ont traversé cela)

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

@ Réponse lex82 est bonne et constitue la base de cette réponse, mais la mine diffère de quelques façons:

  1. C'est dans TypeScript
  2. Il supprime la liaison de clic lorsque la portée est détruite, ce qui signifie que vous n'avez pas à gérer la liaison de clic séparément avec une propriété
  3. Le délai d'attente garantit que si l'objet avec click-out on est créé via un événement de souris, que le même événement de souris ne déclenche pas par inadvertance le mécanisme de fermeture

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