[AngularJS] Incorrect ng-mouseenter ng-mouseleave and Solution


Old version AngularJS (such as 1.1.3) has incorrect implementation of mouseenter and mouseleave directive:

Demo of incorrect implementation

See also this pull request (fix(jqLite): mouseenter/-leave should not trigger on child elements) on AngularJS Github repo for more details.

The following is correct implementation of mouseenter and mouseleave directive:

mouseEnterLeave.js | repository | view raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
'use strict';

/* Directives */


angular.module('pali.mouseEnterLeave', []).
  directive('mouseenter', [function() {
    return {
      restrict: 'A',
      link: function(scope, elm, attrs) {
        /**
         * check if element is targer or the parent of target
         * @param {DOM Element} target
         * @param {DOM Element} element
         * @return {boolean} Return true if element is target or the parent of target
         *                   else return false.
         */
        function checkParent(target, element) {
          if (angular.isUndefined(target)) return false;
          // Chrome and Firefox use parentNode, while Opera use offsetParent
          while(target.parentNode) {
            if( target == element ) return true;
            target = target.parentNode;
          }
          while(target.offsetParent) {
            if( target == element ) return true;
            target = target.offsetParent;
          }
          return false;
        };

        elm.bind('mouseover', function(e) {
          var evt = e || window.event;
          var targetElement = evt.target || evt.srcElement;

          // check if mouse moves inside the element, if yes, return.
          var relTarg = evt.relatedTarget || evt.fromElement;
          if (checkParent(relTarg, elm[0])) return;

          // https://gist.github.com/siongui/a8d9a9003772315e2cba
          if (scope.$root.$$phase)
            scope.$eval(attrs['mouseenter']);
          else
            scope.$apply(attrs['mouseenter']);
        });
      }
    };
  }]).

  directive('mouseleave', [function() {
    return {
      restrict: 'A',
      link: function(scope, elm, attrs) {
        /**
         * check if element is targer or the parent of target
         * @param {DOM Element} target
         * @param {DOM Element} element
         * @return {boolean} Return true if element is target or the parent of target
         *                   else return false.
         */
        function checkParent(target, element) {
          if (angular.isUndefined(target)) return false;
          // Chrome and Firefox use parentNode, while Opera use offsetParent
          while(target.parentNode) {
            if( target == element ) return true;
            target = target.parentNode;
          }
          while(target.offsetParent) {
            if( target == element ) return true;
            target = target.offsetParent;
          }
          return false;
        };

        elm.bind('mouseout', function(e) {
          var evt = e || window.event;
          var targetElement = evt.target || evt.srcElement;

          // check if mouse moves inside the element, if yes, return.
          var relTarg = evt.relatedTarget || evt.toElement;
          if (checkParent(relTarg, elm[0])) return;

          // https://gist.github.com/siongui/a8d9a9003772315e2cba
          if (scope.$root.$$phase)
            scope.$eval(attrs['mouseleave']);
          else
            scope.$apply(attrs['mouseleave']);
        });
      }
    };
  }]);

The usage is the same as ngMouseenter and ngMouseleave, except the name changed to mouseenter and mouseleave. Also do not forget to include this module in your AngularJS application.