Skip to content

Commit

Permalink
fix(jqLite): deregister special mouseenter / mouseleave events co…
Browse files Browse the repository at this point in the history
…rrectly

Closes angular#12795
Closes angular#12799
  • Loading branch information
petebacondarwin authored and gkalpak committed Nov 23, 2015
1 parent 6ad15c4 commit 14c2cda
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 32 deletions.
80 changes: 49 additions & 31 deletions src/jqLite.js
Expand Up @@ -313,17 +313,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]);
}
});
}
}
Expand Down Expand Up @@ -779,14 +785,17 @@ 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);
}

for (var i = 0; i < eventFnsLength; i++) {
if (!event.isImmediatePropagationStopped()) {
eventFns[i].call(element, event);
handlerWrapper(element, event, eventFns[i]);
}
}
};
Expand All @@ -797,6 +806,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
Expand Down Expand Up @@ -825,35 +850,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);
}
}
},

Expand Down
3 changes: 2 additions & 1 deletion src/ngScenario/browserTrigger.js
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
54 changes: 54 additions & 0 deletions test/jqLiteSpec.js
Expand Up @@ -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'),
Expand Down

0 comments on commit 14c2cda

Please sign in to comment.