Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

fix(jqLite): properly deregister the listeners for mouseenter/mouseleave #12799

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions src/jqLite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -831,6 +859,7 @@ forEach({
}
});

jqLiteOn(element, relatedType, listenerFn);
} else {
if (type !== '$destroy') {
addEventListenerFn(element, type, handle);
Expand Down
3 changes: 2 additions & 1 deletion src/ngScenario/browserTrigger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions test/jqLiteSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' +
Expand Down