diff --git a/src/jqLite.js b/src/jqLite.js index cf63c5df5952..0abae3d78f1a 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -607,23 +607,43 @@ forEach({ if (!eventFns) { if (type == 'mouseenter' || type == 'mouseleave') { - var counter = 0; + var contains = document.body.contains || document.body.compareDocumentPosition ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; - events.mouseenter = []; - events.mouseleave = []; + events[type] = []; + + // Refer to jQuery's implementation of mouseenter & mouseleave + // Read about mouseenter and mouseleave: + // http://www.quirksmode.org/js/events_mouse.html#link8 + var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"} + bindFn(element, eventmap[type], function(event) { + var ret, target = this, related = event.relatedTarget; + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !contains(target, related)) ){ + handle(event, type); + } - bindFn(element, 'mouseover', function(event) { - counter++; - if (counter == 1) { - handle(event, 'mouseenter'); - } - }); - bindFn(element, 'mouseout', function(event) { - counter --; - if (counter == 0) { - handle(event, 'mouseleave'); - } }); + } else { addEventListenerFn(element, type, handle); events[type] = []; diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index b1fa6b058207..089ae78cd591 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -775,13 +775,9 @@ describe('jqLite', function() { parent.bind('mouseenter', function() { log += 'parentEnter;'; }); parent.bind('mouseleave', function() { log += 'parentLeave;'; }); - parent.mouseover = function() { browserTrigger(parent, 'mouseover'); }; - parent.mouseout = function() { browserTrigger(parent, 'mouseout'); }; child.bind('mouseenter', function() { log += 'childEnter;'; }); child.bind('mouseleave', function() { log += 'childLeave;'; }); - child.mouseover = function() { browserTrigger(child, 'mouseover'); }; - child.mouseout = function() { browserTrigger(child, 'mouseout'); }; }); afterEach(function() { @@ -790,20 +786,49 @@ describe('jqLite', function() { it('should fire mouseenter when coming from outside the browser window', function() { if (window.jQuery) return; - parent.mouseover(); + var browserMoveTrigger = function(from, to){ + var fireEvent = function(type, element, relatedTarget){ + var msie = parseInt((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1]); + if (msie < 9){ + var evnt = document.createEventObject(); + evnt.srcElement = element; + evnt.relatedTarget = relatedTarget; + element.fireEvent('on' + type, evnt); + return; + }; + var evnt = document.createEvent('MouseEvents'), + originalPreventDefault = evnt.preventDefault, + appWindow = window, + fakeProcessDefault = true, + finalProcessDefault; + + evnt.preventDefault = function() { + fakeProcessDefault = false; + return originalPreventDefault.apply(evnt, arguments); + }; + + var x = 0, y = 0; + evnt.initMouseEvent(type, true, true, window, 0, x, y, x, y, false, false, + false, false, 0, relatedTarget); + + element.dispatchEvent(evnt); + }; + fireEvent('mouseout', from[0], to[0]); + fireEvent('mouseover', to[0], from[0]); + }; + + browserMoveTrigger(root, parent); expect(log).toEqual('parentEnter;'); - child.mouseover(); - expect(log).toEqual('parentEnter;childEnter;'); - child.mouseover(); + browserMoveTrigger(parent, child); expect(log).toEqual('parentEnter;childEnter;'); - child.mouseout(); - expect(log).toEqual('parentEnter;childEnter;'); - child.mouseout(); + browserMoveTrigger(child, parent); expect(log).toEqual('parentEnter;childEnter;childLeave;'); - parent.mouseout(); + + browserMoveTrigger(parent, root); expect(log).toEqual('parentEnter;childEnter;childLeave;parentLeave;'); + }); }); });