Directive qui déclenche un événement en cliquant en dehors de l’élément

Je sais qu’il y a beaucoup de questions à poser. Mais personne ne résout vraiment mon problème.

J’essaie de construire une directive qui exécutera une expression lorsque le clic de la souris en dehors de l’élément actuel.

Pourquoi j’ai besoin de cette fonction? Je construis une application, dans cette application, il y a 3 menu déroulant, 5 liste déroulante (comme choisi). Toutes ces directives sont angulars. 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 le côté de l’élément, vous devez masquer la liste déroulante.

J’ai 2 solutions pour cela, mais les deux ont un problème:

Solution A:

app.directive('clickAnywhereButHere', function($document){ return { ressortingct: '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 de solution A: cliquez ici

Lorsque vous cliquez sur la première liste déroulante, puis que vous travaillez, puis cliquez sur la deuxième entrée, la première doit être masquée, 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 l’exemple de la solution B: cliquez ici

Solution Travailler s’il n’y a qu’une seule directive dans la page, mais pas dans mon application. Parce que cela empêche la formation de bulles, donc d’abord lorsque je clique sur dropdown1, affichez dropdown1, puis cliquez sur dropdown2, click event be prevent, donc dropdown1 affiche toujours là même si je clique en dehors de la liste déroulante1.

Solution B fonctionnant dans mon application que j’utilise maintenant. Mais le problème est que cela cause un problème de performance. Trop de clics d’événements sont traités à chaque clic sur l’application. Dans mon cas actuel, il y a 8 événements de liaison avec le document, donc chaque clic exécute 8 fonctions. Ce qui cause mon application très lente, surtout dans IE8.

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

Je n’utiliserais pas event.stopPropagation () car il provoque exactement le type de problèmes que vous voyez dans la solution A. Si possible, je recourrais aussi à des événements flous et focalisés. Lorsque votre liste déroulante est associée à une entrée, vous pouvez la fermer lorsque l’entrée perd le focus.

Cependant, la gestion des événements de clic sur le document n’est pas si mauvaise non plus. Donc, si vous souhaitez éviter de gérer plusieurs fois le même événement de clic, supprimez-le simplement du document lorsqu’il n’est plus nécessaire. En plus de l’expression en cours d’évaluation en cliquant en dehors de la liste déroulante, 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, fournissez simplement une autre expression comme celle-ci:

  

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 listes déroulantes. Quand quelqu’un clique dessus, il se concentre sinon il devient flou.

Référez-vous également à cette question Comment définir le focus sur le champ de saisie? pour mettre au point dans 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 que vous devez le masquer lorsque vous cliquez en dehors de Y (sauf X évidemment). Y a la propriété isYvisisble. Donc, quand quelqu’un clique sur X (ng-click), alors “isYvisible” est vrai et définit Focus sur Y. Quand quelqu’un clique en dehors de Y (ng-blur), alors vous définissez “isYvisible” comme faux. Vous devez partager une variable (“isYvisible”) entre deux éléments / directives différents et vous pouvez utiliser la scope du contrôleur ou des services pour le faire. Il y a d’autres alternatives à cela, mais cela ne relève pas de la question.

Votre solution A est la plus correcte, mais vous devez append 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’utilise (peut-être une réponse un peu tardive, mais j’espère que cela sera utile pour les autres)

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

Une version un peu plus simple que la réponse la plus populaire, pour moi c’est plus clair et ça marche très bien!

 app.directive('clickAnywhereButHere', function() { return { ressortingct : '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); }; } } }; }); 

Voici une solution que j’ai utilisée qui ne nécessite que l’événement du clic (disponible en tant qu’événement $ dans la directive ngClick). Je voulais un menu avec des éléments qui, une fois cliqué:

  • basculer l’affichage d’un sous-menu
  • masquer tout autre sous-menu s’il était affiché
  • cacher le sous-menu si un clic a eu lieu à l’extérieur.

Ce code définit la classe ‘active’ sur l’élément de menu afin de pouvoir afficher ou masquer 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; } }); } } 

@ lex82 answer is good, et forme la base de cette réponse mais la mienne diffère de plusieurs manières:

  1. Son en TypeScript
  2. Il supprime la liaison de clic lorsque l’étendue 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’object avec click-out est créé via un événement de souris, le même événement de souris ne déclenche pas le mécanisme de fermeture par inadvertance.

     export interface IClickOutDirectiveScope extends angular.IScope { clickOut: Function; } export class ClickOutDirective implements angular.IDirective { public ressortingct = "A"; public scope = { clickOut: "&" } public link: ($scope: IClickOutDirectiveScope, element: angular.IAugmentedJQuery, attrs: angular.IAtsortingbutes) => void; constructor($timeout: angular.ITimeoutService, $document: angular.IDocumentService) { ClickOutDirective.prototype.link = ($scope: IClickOutDirectiveScope, $element: angular.IAugmentedJQuery, attrs: ng.IAtsortingbutes) => { 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());