diff --git a/src/jqLite.js b/src/jqLite.js index 2a36b315a8ce..aadd0a29c371 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -138,6 +138,15 @@ var MOZ_HACK_REGEXP = /^moz([A-Z])/; var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"}; var jqLiteMinErr = minErr('jqLite'); +/** + * Returns the key under which the related event-type listener is stored on the original listener + * function. See the special handling of mouseenter/mouseleave events in `jqLiteOn/Off`. + * @param relatedEventType The type of the related event + */ +function getRelatedListenerKey(relatedEventType) { + return '$$' + relatedEventType + 'Listener'; +} + /** * Converts snake_case to camelCase. * Also there is special case for Moz prefix starting with upper case letter. @@ -300,12 +309,28 @@ function jqLiteOff(element, type, fn, unsupported) { } } else { forEach(type.split(' '), function(type) { - if (isDefined(fn)) { - var listenerFns = events[type]; - arrayRemove(listenerFns || [], fn); - if (listenerFns && listenerFns.length > 0) { - return; - } + var relatedType = MOUSE_EVENT_MAP[type]; + var relatedListenerKey = relatedType && getRelatedListenerKey(relatedType); + var isDefinedFn = isDefined(fn); + + var allListenerFnsForType = events[type] || []; + var allListenerFnsForRelatedType = (relatedType && events[relatedType]) || []; + var listenerFnsToRemove = isDefinedFn ? [fn] : allListenerFnsForType; + + // Remove the "related" listeners (if any) + if (allListenerFnsForRelatedType.length) { + forEach(listenerFnsToRemove, function(fn) { + var relatedListenerFn = fn[relatedListenerKey]; + arrayRemove(allListenerFnsForRelatedType, relatedListenerFn); + }); + + if (!allListenerFnsForRelatedType.length) jqLiteOff(element, relatedType); + } + + // Remove the listener or all listeners for `type` + if (isDefinedFn) { + arrayRemove(allListenerFnsForType, fn); + if (allListenerFnsForType.length) return; } removeEventListenerFn(element, type, handle); @@ -822,7 +847,10 @@ forEach({ // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 - jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) { + // We need to keep track of the actual listener + var relatedType = MOUSE_EVENT_MAP[type]; + var relatedListenerKey = getRelatedListenerKey(relatedType); + var listenerFn = fn[relatedListenerKey] || (fn[relatedListenerKey] = function(event) { var 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 @@ -831,6 +859,7 @@ forEach({ } }); + jqLiteOn(element, relatedType, listenerFn); } else { if (type !== '$destroy') { addEventListenerFn(element, type, handle); diff --git a/src/ngScenario/browserTrigger.js b/src/ngScenario/browserTrigger.js index f3c22fe5ff62..79a70934063e 100644 --- a/src/ngScenario/browserTrigger.js +++ b/src/ngScenario/browserTrigger.js @@ -15,6 +15,7 @@ if (!element) return; eventData = eventData || {}; + var relatedTarget = eventData.relatedTarget || element; var keys = eventData.keys; var x = eventData.x; var y = eventData.y; @@ -84,7 +85,7 @@ x = x || 0; y = y || 0; evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'), - pressed('alt'), pressed('shift'), pressed('meta'), 0, element); + pressed('alt'), pressed('shift'), pressed('meta'), 0, relatedTarget); } /* we're unable to change the timeStamp value directly so this diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index 83d531bc18bc..6a9c12f07f0f 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -1426,6 +1426,26 @@ describe('jqLite', function() { }); + it('should correctly deregister the mouseenter/mouseleave listeners', function() { + var aElem = jqLite(a); + var onMouseenter = jasmine.createSpy('onMouseenter'); + var onMouseleave = jasmine.createSpy('onMouseleave'); + + aElem.on('mouseenter', onMouseenter); + aElem.on('mouseleave', onMouseleave); + aElem.off('mouseenter', onMouseenter); + aElem.off('mouseleave', onMouseleave); + aElem.on('mouseenter', onMouseenter); + aElem.on('mouseleave', onMouseleave); + + browserTrigger(a, 'mouseover', {relatedTarget: b}); + expect(onMouseenter).toHaveBeenCalledOnce(); + + browserTrigger(a, 'mouseout', {relatedTarget: b}); + expect(onMouseleave).toHaveBeenCalledOnce(); + }); + + describe('native listener deregistration', function() { it('should deregister the native listener when all jqLite listeners for given type are gone ' +