【问题标题】:Directive that fires an event when clicking outside of the element在元素外部单击时触发事件的指令
【发布时间】:2013-12-09 18:54:02
【问题描述】:

我知道有很多问题问类似的事情。但是没有人真正解决我的问题。

我正在尝试构建一个指令,当鼠标在当前元素外单击时将执行一个表达式。

为什么我需要这个功能?我正在构建一个应用程序,在这个应用程序中,有 3 个下拉菜单,5 个下拉列表(如选择)。所有这些都是角度指令。让我们假设所有这些指令都是不同的。所以我们有 8 个指令。而且它们都需要一个相同的功能:当点击元素的外部时,需要隐藏下拉菜单。

我有 2 个解决方案,但都遇到了问题:

解决方案 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);
      })
    }
  }
})

这里是解决方案 A 的示例:click here

当你点击第一个下拉菜单,然后工作,然后点击第二个输入,第一个应该隐藏但不是。

解决方案 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
}]);

这里是解决方案 B 的示例:click here

如果页面中只有一个指令但我的应用程序中没有,则解决方案 A 有效。因为它是防止冒泡,所以首先当我点击 dropdown1 时,显示 dropdown1,然后点击 dropdown2,点击事件被阻止,所以即使我在 dropdown1 之外点击,dropdown1 仍然显示在那里。

解决方案 B 在我现在使用的应用程序中运行。但问题是它会导致性能问题。在应用程序中的任何位置每次单击都会处理太多的单击事件。在我目前的情况下,有 8 个单击事件与文档绑定,因此每次单击执行 8 个函数。这导致我的应用程序非常缓慢,尤其是在 IE8 中。

那么有没有更好的解决方案呢?谢谢

【问题讨论】:

  • 如果你提供使用 Plunker/Fiddle 会很棒。
  • 在演示中提供一些基本示例。这肯定看起来比它应该的更复杂。
  • 解决方案 B 正在污染全局范围。如果这是故意的,那真是糟糕的做法。如果不是,它可能是您的一些问题的根源。
  • 您使用的 angularJS 版本是什么?如果我正确理解了您的要求,我已经给出了一个可行的解决方案。
  • 各位大佬,例子加了。

标签: javascript angularjs


【解决方案1】:

您应该使用ngBlurngFocus 来显示或隐藏您的下拉菜单。当有人点击它时,它会变得专注,否则它会变得模糊。

另外,请参考这个问题How to set focus on input field? 在 AngularJS 中设置焦点。

编辑: 对于每个指令(下拉菜单或列表,我们称之为 Y),您必须在单击元素时显示它(我们称之为 X),并且在单击 Y 之外的任何位置时需要隐藏它(显然不包括 X) . Y 有属性 isYvisisble。 因此,当有人单击 X(ng-click)时,将“isYvisible”设置为 true 并将焦点设置在 Y 上。 当有人在 Y (ng-blur) 之外单击时,您将“isYvisible”设置为 false,它会被隐藏。 您需要在两个不同的元素/指令之间共享一个变量(“isYvisible”),您可以使用控制器或服务的范围来做到这一点。还有其他替代方案,但这不在问题范围内。

【讨论】:

  • 当下拉触发器是一个输入或某个具有焦点事件的元素时。这是一个很好的解决方案。但我想做的是建立一个独立的指令。所以我不能假设总是有一个焦点事件。不确定建立独立指令的想法是好是坏。也许我一开始就错了。
  • 绝妙优雅的解决方案 :)
【解决方案2】:

我不会使用 event.stopPropagation(),因为它会导致您在解决方案 A 中看到的那种问题。如果可能的话,我也会求助于模糊和聚焦事件。当您的下拉菜单附加到输入时,您可以在输入失去焦点时关闭它。

不过,在文档上处理点击事件也不是那么糟糕,所以如果您想避免多次处理同一个点击事件,只需在不再需要时将其与文档解除绑定即可。除了在下拉菜单外点击时被评估的表达式,指令还需要知道它是否处于活动状态:

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

使用指令时,只需提供另一个这样的表达式:

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

我没有测试过你的 onClick 功能。我认为它按预期工作。希望这会有所帮助。

【讨论】:

  • 非常好的方法,但值得一提的是,我必须在作用域上设置 onClick 函数以使其适用于指令的两个实例。
  • 不错的解决方案,我只需要删除 jQuery 依赖项,而不是:var isChild = $(element).has(event.target).length &gt; 0; 你可以使用:var isChild = element[0].contains(event.target);
【解决方案3】:

您的解决方案 A 是最正确的,但您应该在指令中添加另一个参数以跟踪它是否打开:

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

【讨论】:

    【解决方案4】:
    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);
      };
    }
    

    【讨论】:

      【解决方案5】:

      这是我使用的一个解决方案,它只需要点击事件(在 ngClick 指令中以 $event 的形式提供)。我想要一个带有项目的菜单,单击时会:

      • 切换子菜单的显示
      • 如果显示任何其他子菜单,则隐藏它
      • 如果在外部发生点击,则隐藏子菜单。

      此代码在菜单项上设置类“活动”,以便可以用来显示或隐藏它的子菜单

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

      【讨论】:

        【解决方案6】:

        这是我正在使用的一个解决方案(可能会晚一点回答,但希望对经历过这个问题的其他人有所帮助)

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

        【讨论】:

          【解决方案7】:

          @lex82 的答案很好,构成了这个答案的基础,但我的答案在几个方面有所不同:

          1. 在 TypeScript 中
          2. 它会在作用域被破坏时移除点击绑定,这意味着您不必使用属性单独管理点击绑定
          3. 超时确保如果带有click-out 的对象是通过鼠标事件创建的,那么相同的鼠标事件实际上不会无意中触发关闭机制

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

          【讨论】:

            【解决方案8】:

            比大多数赞成的答案更简单的版本,对我来说它更清晰并且工作得很好!

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

            【讨论】:

            • 如果他们点击另一个 'clickAnywhereButHere' 指令,这将不起作用。
            • @Jordash,如果您的要求是在页面上有多个这样的指令,您可以使用此代码。例如,您可以创建一些基于元素 ID 作为键的元素映射,并将其“点击状态”保存为值,而不是将状态保存到一个属性。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-10-28
            • 1970-01-01
            相关资源
            最近更新 更多