From 2895b4c068c194f1a24afd982568ce1055d4002b Mon Sep 17 00:00:00 2001 From: Michael Benford Date: Fri, 8 Nov 2013 00:38:04 -0200 Subject: [PATCH 1/2] fix(jqLite): fix event.stopImmediatePropagation() so it works as expected jqLite doesn't override the default implementation of event.stopImmediatePropagation() and so it doesn't work as expected, i.e, it doesn't prevent the rest of the event handlers from being executed. --- src/jqLite.js | 32 +++++++++++++++--- test/jqLiteSpec.js | 84 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 73b76f930739..cdde2f1c0855 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -692,7 +692,6 @@ forEach({ function createEventHandler(element, events) { var eventHandler = function (event, type) { - // jQuery specific api event.isDefaultPrevented = function() { return event.defaultPrevented; @@ -703,13 +702,34 @@ function createEventHandler(element, events) { if (!eventFnsLength) return; + if (isUndefined(event.immediatePropagationStopped)) { + var originalStopImmediatePropagation = event.stopImmediatePropagation; + event.stopImmediatePropagation = function() { + event.immediatePropagationStopped = true; + + if (event.stopPropagation) { + event.stopPropagation(); + } + + if (originalStopImmediatePropagation) { + originalStopImmediatePropagation.call(event); + } + }; + } + + event.isImmediatePropagationStopped = function() { + return event.immediatePropagationStopped === true; + }; + // 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++) { - eventFns[i].call(element, event); + if (!event.isImmediatePropagationStopped()) { + eventFns[i].call(element, event); + } } }; @@ -910,11 +930,12 @@ forEach({ var eventFns = events && events[eventName]; if (eventFns) { - // Create a dummy event to pass to the handlers dummyEvent = { preventDefault: function() { this.defaultPrevented = true; }, isDefaultPrevented: function() { return this.defaultPrevented === true; }, + stopImmediatePropagation: function() { this.immediatePropagationStopped = true; }, + isImmediatePropagationStopped: function() { return this.immediatePropagationStopped === true; }, stopPropagation: noop, type: eventName, target: element @@ -930,9 +951,10 @@ forEach({ handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; forEach(eventFnsCopy, function(fn) { - fn.apply(element, handlerArgs); + if (!dummyEvent.isImmediatePropagationStopped()) { + fn.apply(element, handlerArgs); + } }); - } } }, function(fn, name){ diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index 3be21571f808..01b46f8ae2c4 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -1102,6 +1102,58 @@ describe('jqLite', function() { browserTrigger(a, 'click'); }); + it('should stop triggering handlers when stopImmediatePropagation is called', function() { + var element = jqLite(a), + clickSpy1 = jasmine.createSpy('clickSpy1'), + clickSpy2 = jasmine.createSpy('clickSpy2').andCallFake(function(event) { event.stopImmediatePropagation(); }), + clickSpy3 = jasmine.createSpy('clickSpy3'), + clickSpy4 = jasmine.createSpy('clickSpy4'); + + element.on('click', clickSpy1); + element.on('click', clickSpy2); + element.on('click', clickSpy3); + element[0].addEventListener('click', clickSpy4); + + browserTrigger(element, 'click'); + + expect(clickSpy1).toHaveBeenCalled(); + expect(clickSpy2).toHaveBeenCalled(); + expect(clickSpy3).not.toHaveBeenCalled(); + expect(clickSpy4).not.toHaveBeenCalled(); + }); + + it('should execute stopPropagation when stopImmediatePropagation is called', function() { + var element = jqLite(a), + clickSpy = jasmine.createSpy('clickSpy'); + + clickSpy.andCallFake(function(event) { + spyOn(event, 'stopPropagation'); + event.stopImmediatePropagation(); + expect(event.stopPropagation).toHaveBeenCalled(); + }); + + element.on('click', clickSpy); + + browserTrigger(element, 'click'); + expect(clickSpy).toHaveBeenCalled(); + }); + + it('should have event.isImmediatePropagationStopped method', function() { + var element = jqLite(a), + clickSpy = jasmine.createSpy('clickSpy'); + + clickSpy.andCallFake(function(event) { + expect(event.isImmediatePropagationStopped()).toBe(false); + event.stopImmediatePropagation(); + expect(event.isImmediatePropagationStopped()).toBe(true); + }); + + element.on('click', clickSpy); + + browserTrigger(element, 'click'); + expect(clickSpy).toHaveBeenCalled(); + }); + describe('mouseenter-mouseleave', function() { var root, parent, sibling, child, log; @@ -1784,7 +1836,6 @@ describe('jqLite', function() { expect(event.isDefaultPrevented()).toBe(true); }); - it('should support handlers that deregister themselves', function() { var element = jqLite('poke'), clickSpy = jasmine.createSpy('click'), @@ -1821,6 +1872,37 @@ describe('jqLite', function() { expect(actualEvent.target).toEqual(element[0]); expect(actualEvent.type).toEqual('click'); }); + + it('should stop triggering handlers when stopImmediatePropagation is called', function () { + var element = jqLite(a), + clickSpy1 = jasmine.createSpy('clickSpy1'), + clickSpy2 = jasmine.createSpy('clickSpy2').andCallFake(function(event) { event.stopImmediatePropagation(); }), + clickSpy3 = jasmine.createSpy('clickSpy3'); + + element.on('click', clickSpy1); + element.on('click', clickSpy2); + element.on('click', clickSpy3); + + element.triggerHandler('click'); + + expect(clickSpy1).toHaveBeenCalled(); + expect(clickSpy2).toHaveBeenCalled(); + expect(clickSpy3).not.toHaveBeenCalled(); + }); + + it('should have event.isImmediatePropagationStopped method', function() { + var element = jqLite(a), + clickSpy = jasmine.createSpy('clickSpy'), + event; + + element.on('click', clickSpy); + element.triggerHandler('click'); + event = clickSpy.mostRecentCall.args[0]; + + expect(event.isImmediatePropagationStopped()).toBe(false); + event.stopImmediatePropagation(); + expect(event.isImmediatePropagationStopped()).toBe(true); + }); }); From aec9070f11a03800b0373a9ab15f82d2013bce3a Mon Sep 17 00:00:00 2001 From: Michael Benford Date: Mon, 21 Jul 2014 00:30:01 -0300 Subject: [PATCH 2/2] test(jqLite): Refactor test for isDefaultPrevent Refactor the spec for isDefaultPrevent method so it fails if the existing expectations aren't executed. --- test/jqLiteSpec.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index 01b46f8ae2c4..9601c9afd6e9 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -1091,7 +1091,10 @@ describe('jqLite', function() { }); it('should have event.isDefaultPrevented method', function() { - jqLite(a).on('click', function(e) { + var element = jqLite(a), + clickSpy = jasmine.createSpy('clickSpy'); + + clickSpy.andCallFake(function(e) { expect(function() { expect(e.isDefaultPrevented()).toBe(false); e.preventDefault(); @@ -1099,7 +1102,10 @@ describe('jqLite', function() { }).not.toThrow(); }); + element.on('click', clickSpy); + browserTrigger(a, 'click'); + expect(clickSpy).toHaveBeenCalled(); }); it('should stop triggering handlers when stopImmediatePropagation is called', function() {