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

Commit f5aa207

Browse files
fix(jqLite): deregister special mouseenter / mouseleave events correctly
Closes #12795 Closes #12799
1 parent 4412fe2 commit f5aa207

File tree

3 files changed

+105
-32
lines changed

3 files changed

+105
-32
lines changed

src/jqLite.js

+49-31
Original file line numberDiff line numberDiff line change
@@ -313,17 +313,23 @@ function jqLiteOff(element, type, fn, unsupported) {
313313
delete events[type];
314314
}
315315
} else {
316-
forEach(type.split(' '), function(type) {
316+
317+
var removeHandler = function(type) {
318+
var listenerFns = events[type];
317319
if (isDefined(fn)) {
318-
var listenerFns = events[type];
319320
arrayRemove(listenerFns || [], fn);
320-
if (listenerFns && listenerFns.length > 0) {
321-
return;
322-
}
323321
}
322+
if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) {
323+
removeEventListenerFn(element, type, handle);
324+
delete events[type];
325+
}
326+
};
324327

325-
removeEventListenerFn(element, type, handle);
326-
delete events[type];
328+
forEach(type.split(' '), function(type) {
329+
removeHandler(type);
330+
if (MOUSE_EVENT_MAP[type]) {
331+
removeHandler(MOUSE_EVENT_MAP[type]);
332+
}
327333
});
328334
}
329335
}
@@ -779,14 +785,17 @@ function createEventHandler(element, events) {
779785
return event.immediatePropagationStopped === true;
780786
};
781787

788+
// Some events have special handlers that wrap the real handler
789+
var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper;
790+
782791
// Copy event handlers in case event handlers array is modified during execution.
783792
if ((eventFnsLength > 1)) {
784793
eventFns = shallowCopy(eventFns);
785794
}
786795

787796
for (var i = 0; i < eventFnsLength; i++) {
788797
if (!event.isImmediatePropagationStopped()) {
789-
eventFns[i].call(element, event);
798+
handlerWrapper(element, event, eventFns[i]);
790799
}
791800
}
792801
};
@@ -797,6 +806,22 @@ function createEventHandler(element, events) {
797806
return eventHandler;
798807
}
799808

809+
function defaultHandlerWrapper(element, event, handler) {
810+
handler.call(element, event);
811+
}
812+
813+
function specialMouseHandlerWrapper(target, event, handler) {
814+
// Refer to jQuery's implementation of mouseenter & mouseleave
815+
// Read about mouseenter and mouseleave:
816+
// http://www.quirksmode.org/js/events_mouse.html#link8
817+
var related = event.relatedTarget;
818+
// For mousenter/leave call the handler if related is outside the target.
819+
// NB: No relatedTarget if the mouse left/entered the browser window
820+
if (!related || (related !== target && !jqLiteContains.call(target, related))) {
821+
handler.call(target, event);
822+
}
823+
}
824+
800825
//////////////////////////////////////////
801826
// Functions iterating traversal.
802827
// These functions chain results into a single
@@ -825,35 +850,28 @@ forEach({
825850
var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type];
826851
var i = types.length;
827852

828-
while (i--) {
829-
type = types[i];
853+
var addHandler = function(type, specialHandlerWrapper, noEventListener) {
830854
var eventFns = events[type];
831855

832856
if (!eventFns) {
833-
events[type] = [];
834-
835-
if (type === 'mouseenter' || type === 'mouseleave') {
836-
// Refer to jQuery's implementation of mouseenter & mouseleave
837-
// Read about mouseenter and mouseleave:
838-
// http://www.quirksmode.org/js/events_mouse.html#link8
839-
840-
jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) {
841-
var target = this, related = event.relatedTarget;
842-
// For mousenter/leave call the handler if related is outside the target.
843-
// NB: No relatedTarget if the mouse left/entered the browser window
844-
if (!related || (related !== target && !jqLiteContains.call(target, related))) {
845-
handle(event, type);
846-
}
847-
});
848-
849-
} else {
850-
if (type !== '$destroy') {
851-
addEventListenerFn(element, type, handle);
852-
}
857+
eventFns = events[type] = [];
858+
eventFns.specialHandlerWrapper = specialHandlerWrapper;
859+
if (type !== '$destroy' && !noEventListener) {
860+
addEventListenerFn(element, type, handle);
853861
}
854-
eventFns = events[type];
855862
}
863+
856864
eventFns.push(fn);
865+
};
866+
867+
while (i--) {
868+
type = types[i];
869+
if (MOUSE_EVENT_MAP[type]) {
870+
addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper);
871+
addHandler(type, undefined, true);
872+
} else {
873+
addHandler(type);
874+
}
857875
}
858876
},
859877

src/ngScenario/browserTrigger.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
if (!element) return;
1616

1717
eventData = eventData || {};
18+
var relatedTarget = eventData.relatedTarget || element;
1819
var keys = eventData.keys;
1920
var x = eventData.x;
2021
var y = eventData.y;
@@ -84,7 +85,7 @@
8485
x = x || 0;
8586
y = y || 0;
8687
evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'),
87-
pressed('alt'), pressed('shift'), pressed('meta'), 0, element);
88+
pressed('alt'), pressed('shift'), pressed('meta'), 0, relatedTarget);
8889
}
8990

9091
/* we're unable to change the timeStamp value directly so this

test/jqLiteSpec.js

+54
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,60 @@ describe('jqLite', function() {
14311431
});
14321432

14331433

1434+
it('should correctly deregister the mouseenter/mouseleave listeners', function() {
1435+
var aElem = jqLite(a);
1436+
var onMouseenter = jasmine.createSpy('onMouseenter');
1437+
var onMouseleave = jasmine.createSpy('onMouseleave');
1438+
1439+
aElem.on('mouseenter', onMouseenter);
1440+
aElem.on('mouseleave', onMouseleave);
1441+
aElem.off('mouseenter', onMouseenter);
1442+
aElem.off('mouseleave', onMouseleave);
1443+
aElem.on('mouseenter', onMouseenter);
1444+
aElem.on('mouseleave', onMouseleave);
1445+
1446+
browserTrigger(a, 'mouseover', {relatedTarget: b});
1447+
expect(onMouseenter).toHaveBeenCalledOnce();
1448+
1449+
browserTrigger(a, 'mouseout', {relatedTarget: b});
1450+
expect(onMouseleave).toHaveBeenCalledOnce();
1451+
});
1452+
1453+
1454+
it('should call a `mouseenter/leave` listener only once when `mouseenter/leave` and `mouseover/out` '
1455+
+ 'are triggered simultaneously', function() {
1456+
var aElem = jqLite(a);
1457+
var onMouseenter = jasmine.createSpy('mouseenter');
1458+
var onMouseleave = jasmine.createSpy('mouseleave');
1459+
1460+
aElem.on('mouseenter', onMouseenter);
1461+
aElem.on('mouseleave', onMouseleave);
1462+
1463+
browserTrigger(a, 'mouseenter', {relatedTarget: b});
1464+
browserTrigger(a, 'mouseover', {relatedTarget: b});
1465+
expect(onMouseenter).toHaveBeenCalledOnce();
1466+
1467+
browserTrigger(a, 'mouseleave', {relatedTarget: b});
1468+
browserTrigger(a, 'mouseout', {relatedTarget: b});
1469+
expect(onMouseleave).toHaveBeenCalledOnce();
1470+
});
1471+
1472+
it('should call a `mouseenter/leave` listener when manually triggering the event', function() {
1473+
var aElem = jqLite(a);
1474+
var onMouseenter = jasmine.createSpy('mouseenter');
1475+
var onMouseleave = jasmine.createSpy('mouseleave');
1476+
1477+
aElem.on('mouseenter', onMouseenter);
1478+
aElem.on('mouseleave', onMouseleave);
1479+
1480+
aElem.triggerHandler('mouseenter');
1481+
expect(onMouseenter).toHaveBeenCalledOnce();
1482+
1483+
aElem.triggerHandler('mouseleave');
1484+
expect(onMouseleave).toHaveBeenCalledOnce();
1485+
});
1486+
1487+
14341488
it('should deregister specific listener within the listener and call subsequent listeners', function() {
14351489
var aElem = jqLite(a),
14361490
clickSpy = jasmine.createSpy('click'),

0 commit comments

Comments
 (0)