diff --git a/packages/platform-browser/src/dom/events/dom_events.ts b/packages/platform-browser/src/dom/events/dom_events.ts index 3eec5178fc..735846d505 100644 --- a/packages/platform-browser/src/dom/events/dom_events.ts +++ b/packages/platform-browser/src/dom/events/dom_events.ts @@ -35,6 +35,10 @@ const ANGULAR = 'ANGULAR'; const NATIVE_ADD_LISTENER = 'addEventListener'; const NATIVE_REMOVE_LISTENER = 'removeEventListener'; +// use the same symbol string which is used in zone.js +const stopSymbol = '__zone_symbol__propagationStopped'; +const stopMethodSymbol = '__zone_symbol__stopImmediatePropagation'; + const blackListedEvents: string[] = (typeof Zone !== 'undefined') && (Zone as any)[__symbol__('BLACK_LISTED_EVENTS')]; let blackListedMap: {[eventName: string]: string}; @@ -81,6 +85,11 @@ const globalListener = function(event: Event) { // itself or others const copiedTasks = taskDatas.slice(); for (let i = 0; i < copiedTasks.length; i++) { + // if other listener call event.stopImmediatePropagation + // just break + if ((event as any)[stopSymbol] === true) { + break; + } const taskData = copiedTasks[i]; if (taskData.zone !== Zone.current) { // only use Zone.run when Zone.current not equals to stored zone @@ -94,7 +103,33 @@ const globalListener = function(event: Event) { @Injectable() export class DomEventsPlugin extends EventManagerPlugin { - constructor(@Inject(DOCUMENT) doc: any, private ngZone: NgZone) { super(doc); } + constructor(@Inject(DOCUMENT) doc: any, private ngZone: NgZone) { + super(doc); + + this.patchEvent(); + } + + private patchEvent() { + if (!Event || !Event.prototype) { + return; + } + if ((Event.prototype as any)[stopMethodSymbol]) { + // already patched by zone.js + return; + } + const delegate = (Event.prototype as any)[stopMethodSymbol] = + Event.prototype.stopImmediatePropagation; + Event.prototype.stopImmediatePropagation = function() { + if (this) { + this[stopSymbol] = true; + } + + // should call native delegate in case + // in some enviroment part of the application + // will not use the patched Event + delegate && delegate.apply(this, arguments); + }; + } // This plugin should come last in the list of plugins, because it accepts all // events. diff --git a/packages/platform-browser/test/dom/events/event_manager_spec.ts b/packages/platform-browser/test/dom/events/event_manager_spec.ts index 480de63bd1..e1a405e9b9 100644 --- a/packages/platform-browser/test/dom/events/event_manager_spec.ts +++ b/packages/platform-browser/test/dom/events/event_manager_spec.ts @@ -152,6 +152,42 @@ export function main() { expect(receivedEvents).toEqual([]); }); + it('should support event.stopImmediatePropagation', () => { + const Zone = (window as any)['Zone']; + + const element = el('
'); + getDOM().appendChild(doc.body, element); + const dispatchedEvent = getDOM().createMouseEvent('click'); + let receivedEvents: any[] /** TODO #9100 */ = []; + let receivedZones: any[] = []; + const handler1 = (e: any /** TODO #9100 */) => { + receivedEvents.push(e); + receivedZones.push(Zone.current.name); + e.stopImmediatePropagation(); + }; + const handler2 = (e: any /** TODO #9100 */) => { + receivedEvents.push(e); + receivedZones.push(Zone.current.name); + }; + const manager = new EventManager([domEventPlugin], new FakeNgZone()); + + let remover1 = null; + let remover2 = null; + Zone.root.run(() => { remover1 = manager.addEventListener(element, 'click', handler1); }); + Zone.root.fork({name: 'test'}).run(() => { + remover2 = manager.addEventListener(element, 'click', handler2); + }); + getDOM().dispatchEvent(element, dispatchedEvent); + expect(receivedEvents).toEqual([dispatchedEvent]); + expect(receivedZones).toEqual([Zone.root.name]); + + receivedEvents = []; + remover1 && remover1(); + remover2 && remover2(); + getDOM().dispatchEvent(element, dispatchedEvent); + expect(receivedEvents).toEqual([]); + }); + it('should handle event correctly when one handler remove itself ', () => { const Zone = (window as any)['Zone'];