Skip to content

Commit d5c981b

Browse files
committed
fix(jqLite): properly deregister the listeners for mouseenter/mouseleave
Fixes angular#12795
1 parent e36fbb7 commit d5c981b

File tree

2 files changed

+56
-7
lines changed

2 files changed

+56
-7
lines changed

src/jqLite.js

+36-7
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ var MOZ_HACK_REGEXP = /^moz([A-Z])/;
138138
var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"};
139139
var jqLiteMinErr = minErr('jqLite');
140140

141+
/**
142+
* Returns the key under which the related event-type listener is stored on the original listener
143+
* function. See the special handling of mouseenter/mouseleave events in `jqLiteOn/Off`.
144+
* @param relatedEventType The type of the related event
145+
*/
146+
function getRelatedListenerKey(relatedEventType) {
147+
return '$$' + relatedEventType + 'Listener';
148+
}
149+
141150
/**
142151
* Converts snake_case to camelCase.
143152
* Also there is special case for Moz prefix starting with upper case letter.
@@ -300,12 +309,28 @@ function jqLiteOff(element, type, fn, unsupported) {
300309
}
301310
} else {
302311
forEach(type.split(' '), function(type) {
303-
if (isDefined(fn)) {
304-
var listenerFns = events[type];
305-
arrayRemove(listenerFns || [], fn);
306-
if (listenerFns && listenerFns.length > 0) {
307-
return;
308-
}
312+
var relatedType = MOUSE_EVENT_MAP[type];
313+
var relatedListenerKey = relatedType && getRelatedListenerKey(relatedType);
314+
var isDefinedFn = isDefined(fn);
315+
316+
var allListenerFnsForType = events[type] || [];
317+
var allListenerFnsForRelatedType = (relatedType && events[relatedType]) || [];
318+
var listenerFnsToRemove = isDefinedFn ? [fn] : allListenerFnsForType;
319+
320+
// Remove the "related" listeners (if any)
321+
if (allListenerFnsForRelatedType.length) {
322+
forEach(listenerFnsToRemove, function(fn) {
323+
var relatedListenerFn = fn[relatedListenerKey];
324+
arrayRemove(allListenerFnsForRelatedType, relatedListenerFn);
325+
});
326+
327+
if (!allListenerFnsForRelatedType.length) jqLiteOff(element, relatedType);
328+
}
329+
330+
// Remove the listener or all listeners for `type`
331+
if (isDefinedFn) {
332+
arrayRemove(allListenerFnsForType, fn);
333+
if (allListenerFnsForType.length) return;
309334
}
310335

311336
removeEventListenerFn(element, type, handle);
@@ -822,7 +847,10 @@ forEach({
822847
// Read about mouseenter and mouseleave:
823848
// http://www.quirksmode.org/js/events_mouse.html#link8
824849

825-
jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) {
850+
// We need to keep track of the actual listener
851+
var relatedType = MOUSE_EVENT_MAP[type];
852+
var relatedListenerKey = getRelatedListenerKey(relatedType);
853+
var listenerFn = fn[relatedListenerKey] || (fn[relatedListenerKey] = function(event) {
826854
var target = this, related = event.relatedTarget;
827855
// For mousenter/leave call the handler if related is outside the target.
828856
// NB: No relatedTarget if the mouse left/entered the browser window
@@ -831,6 +859,7 @@ forEach({
831859
}
832860
});
833861

862+
jqLiteOn(element, relatedType, listenerFn);
834863
} else {
835864
if (type !== '$destroy') {
836865
addEventListenerFn(element, type, handle);

test/jqLiteSpec.js

+20
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,26 @@ describe('jqLite', function() {
14261426
});
14271427

14281428

1429+
it('should correctly deregister the mouseenter/mouseleave listeners', function() {
1430+
var aElem = jqLite(a);
1431+
var onMouseenter = jasmine.createSpy('onMouseenter');
1432+
var onMouseleave = jasmine.createSpy('onMouseleave');
1433+
1434+
aElem.on('mouseenter', onMouseenter);
1435+
aElem.on('mouseleave', onMouseleave);
1436+
aElem.off('mouseenter', onMouseenter);
1437+
aElem.off('mouseleave', onMouseleave);
1438+
aElem.on('mouseenter', onMouseenter);
1439+
aElem.on('mouseleave', onMouseleave);
1440+
1441+
browserTrigger(a, 'mouseover', {relatedTarget: b});
1442+
expect(onMouseenter).toHaveBeenCalledOnce();
1443+
1444+
browserTrigger(a, 'mouseout', {relatedTarget: b});
1445+
expect(onMouseleave).toHaveBeenCalledOnce();
1446+
});
1447+
1448+
14291449
describe('native listener deregistration', function() {
14301450

14311451
it('should deregister the native listener when all jqLite listeners for given type are gone ' +

0 commit comments

Comments
 (0)