diff --git a/src/jqLite.js b/src/jqLite.js index 80680ac55469..34010092c0b0 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -307,17 +307,23 @@ function jqLiteOff(element, type, fn, unsupported) { delete events[type]; } } else { - forEach(type.split(' '), function(type) { + + var removeHandler = function(type) { + var listenerFns = events[type]; if (isDefined(fn)) { - var listenerFns = events[type]; arrayRemove(listenerFns || [], fn); - if (listenerFns && listenerFns.length > 0) { - return; - } } + if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) { + removeEventListenerFn(element, type, handle); + delete events[type]; + } + }; - removeEventListenerFn(element, type, handle); - delete events[type]; + forEach(type.split(' '), function(type) { + removeHandler(type); + if (MOUSE_EVENT_MAP[type]) { + removeHandler(MOUSE_EVENT_MAP[type]); + } }); } } @@ -772,6 +778,9 @@ function createEventHandler(element, events) { return event.immediatePropagationStopped === true; }; + // Some events have special handlers that wrap the real handler + var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper; + // Copy event handlers in case event handlers array is modified during execution. if ((eventFnsLength > 1)) { eventFns = shallowCopy(eventFns); @@ -779,7 +788,7 @@ function createEventHandler(element, events) { for (var i = 0; i < eventFnsLength; i++) { if (!event.isImmediatePropagationStopped()) { - eventFns[i].call(element, event); + handlerWrapper(element, event, eventFns[i]); } } }; @@ -790,6 +799,22 @@ function createEventHandler(element, events) { return eventHandler; } +function defaultHandlerWrapper(element, event, handler) { + handler.call(element, event); +} + +function specialMouseHandlerWrapper(target, event, handler) { + // Refer to jQuery's implementation of mouseenter & mouseleave + // Read about mouseenter and mouseleave: + // http://www.quirksmode.org/js/events_mouse.html#link8 + var 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 && !jqLiteContains.call(target, related))) { + handler.call(target, event); + } +} + ////////////////////////////////////////// // Functions iterating traversal. // These functions chain results into a single @@ -818,35 +843,28 @@ forEach({ var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; var i = types.length; - while (i--) { - type = types[i]; + var addHandler = function(type, specialHandlerWrapper, noEventListener) { var eventFns = events[type]; if (!eventFns) { - events[type] = []; - - if (type === 'mouseenter' || type === 'mouseleave') { - // Refer to jQuery's implementation of mouseenter & mouseleave - // Read about mouseenter and mouseleave: - // http://www.quirksmode.org/js/events_mouse.html#link8 - - jqLiteOn(element, MOUSE_EVENT_MAP[type], 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 - if (!related || (related !== target && !jqLiteContains.call(target, related))) { - handle(event, type); - } - }); - - } else { - if (type !== '$destroy') { - addEventListenerFn(element, type, handle); - } + eventFns = events[type] = []; + eventFns.specialHandlerWrapper = specialHandlerWrapper; + if (type !== '$destroy' && !noEventListener) { + addEventListenerFn(element, type, handle); } - eventFns = events[type]; } + eventFns.push(fn); + }; + + while (i--) { + type = types[i]; + if (MOUSE_EVENT_MAP[type]) { + addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper); + addHandler(type, undefined, true); + } else { + addHandler(type); + } } }, 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 0fccd1e67c78..98c91c8a42c8 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -1431,6 +1431,60 @@ 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(); + }); + + + it('should call a `mouseenter/leave` listener only once when `mouseenter/leave` and `mouseover/out` ' + + 'are triggered simultaneously', function() { + var aElem = jqLite(a); + var onMouseenter = jasmine.createSpy('mouseenter'); + var onMouseleave = jasmine.createSpy('mouseleave'); + + aElem.on('mouseenter', onMouseenter); + aElem.on('mouseleave', onMouseleave); + + browserTrigger(a, 'mouseenter', {relatedTarget: b}); + browserTrigger(a, 'mouseover', {relatedTarget: b}); + expect(onMouseenter).toHaveBeenCalledOnce(); + + browserTrigger(a, 'mouseleave', {relatedTarget: b}); + browserTrigger(a, 'mouseout', {relatedTarget: b}); + expect(onMouseleave).toHaveBeenCalledOnce(); + }); + + it('should call a `mouseenter/leave` listener when manually triggering the event', function() { + var aElem = jqLite(a); + var onMouseenter = jasmine.createSpy('mouseenter'); + var onMouseleave = jasmine.createSpy('mouseleave'); + + aElem.on('mouseenter', onMouseenter); + aElem.on('mouseleave', onMouseleave); + + aElem.triggerHandler('mouseenter'); + expect(onMouseenter).toHaveBeenCalledOnce(); + + aElem.triggerHandler('mouseleave'); + expect(onMouseleave).toHaveBeenCalledOnce(); + }); + + it('should deregister specific listener within the listener and call subsequent listeners', function() { var aElem = jqLite(a), clickSpy = jasmine.createSpy('click'),