单击元素外部时触发事件的指令

时间:2013-11-25 07:00:24

标签: javascript angularjs

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

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

为什么我需要这个功能?我正在构建一个应用程序,在这个应用程序中,有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

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

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

那么有更好的解决方案吗?感谢

8 个答案:

答案 0 :(得分:32)

我不会使用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功能。我认为它按预期工作。希望这会有所帮助。

答案 1 :(得分:9)

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

另外,请参阅此问题How to set focus on input field?以在AngularJS中设置焦点。

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

答案 2 :(得分: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);
  })
}

答案 3 :(得分:3)

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

答案 4 :(得分:2)

这是我正在使用的解决方案(可能有点迟到,但希望对其他人有所帮助)

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

答案 5 :(得分:2)

比最热门的答案更简单的版本,对我来说它更清晰,工作得很好!

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

答案 6 :(得分:1)

这是我使用的解决方案,只需要点击事件(在ngClick指令中作为$ event提供)。我想要一个包含项目的菜单,点击后会:

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

此代码设置类&#39;活跃&#39;在菜单项上,以便可用于显示或隐藏它的子菜单

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

答案 7 :(得分:1)

@ lex82答案很好,并形成了这个答案的基础,但我的方法有所不同:

  1. 在TypeScript中
  2. 当范围被销毁时,它会删除点击装订,这意味着您不必单独使用属性管理点击装订
  3. 超时确保如果通过鼠标事件创建了click-out on的对象,那么相同的鼠标事件实际上不会无意中触发关闭机制

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