diff --git a/modules/angular2/src/core/application.js b/modules/angular2/src/core/application.js index 6a77497298..161ee1bdb3 100644 --- a/modules/angular2/src/core/application.js +++ b/modules/angular2/src/core/application.js @@ -16,7 +16,7 @@ import {LifeCycle} from 'angular2/src/core/life_cycle/life_cycle'; import {ShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {XHR} from 'angular2/src/core/compiler/xhr/xhr'; import {XHRImpl} from 'angular2/src/core/compiler/xhr/xhr_impl'; -import {EventManager} from 'angular2/src/core/events/event_manager'; +import {EventManager, DomEventsPlugin} from 'angular2/src/core/events/event_manager'; import {HammerGesturesPlugin} from 'angular2/src/core/events/hammer_gestures'; import {Binding} from 'angular2/src/di/binding'; @@ -75,7 +75,7 @@ function _injectorBindings(appComponentType): List { [appViewToken]), bind(LifeCycle).toFactory((exceptionHandler) => new LifeCycle(exceptionHandler, null, assertionsEnabled()),[ExceptionHandler]), bind(EventManager).toFactory((zone) => { - var plugins = [new HammerGesturesPlugin()]; + var plugins = [new HammerGesturesPlugin(), new DomEventsPlugin()]; return new EventManager(plugins, zone); }, [VmTurnZone]), bind(ShadowDomStrategy).toValue(new NativeShadowDomStrategy()), diff --git a/modules/angular2/src/core/events/event_manager.js b/modules/angular2/src/core/events/event_manager.js index ae6e2aa401..58fe234c05 100644 --- a/modules/angular2/src/core/events/event_manager.js +++ b/modules/angular2/src/core/events/event_manager.js @@ -1,8 +1,10 @@ -import {isBlank, BaseException, isPresent} from 'angular2/src/facade/lang'; +import {isBlank, BaseException, isPresent, StringWrapper} from 'angular2/src/facade/lang'; import {DOM, Element} from 'angular2/src/facade/dom'; import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone'; +var BUBBLE_SYMBOL = '^'; + export class EventManager { _plugins: List; _zone: VmTurnZone; @@ -16,13 +18,13 @@ export class EventManager { } addEventListener(element: Element, eventName: string, handler: Function) { - var plugin = this._findPluginFor(eventName); - - if (isPresent(plugin)) { - plugin.addEventListener(element, eventName, handler); - } else { - this._addNativeEventListener(element, eventName, handler); + var shouldSupportBubble = eventName[0] == BUBBLE_SYMBOL; + if (shouldSupportBubble) { + eventName = StringWrapper.substring(eventName, 1); } + + var plugin = this._findPluginFor(eventName); + plugin.addEventListener(element, eventName, handler, shouldSupportBubble); } getZone(): VmTurnZone { @@ -37,30 +39,56 @@ export class EventManager { return plugin; } } - return null; - } - - _addNativeEventListener(element: Element, eventName: string, handler: Function) { - this._zone.runOutsideAngular(() => { - DOM.on(element, eventName, (event) => { - if (event.target === element) { - this._zone.run(function() { - handler(event); - }); - } - }); - }); + throw new BaseException(`No event manager plugin found for event ${eventName}`); } } export class EventManagerPlugin { manager: EventManager; + // We are assuming here that all plugins support bubbled and non-bubbled events. + // That is equivalent to having supporting $event.target + // The bubbling flag (currently ^) is stripped before calling the supports and + // addEventListener methods. supports(eventName: string): boolean { return false; } - addEventListener(element: Element, eventName: string, handler: Function) { + addEventListener(element: Element, eventName: string, handler: Function, + shouldSupportBubble: boolean) { throw "not implemented"; } } + +export class DomEventsPlugin extends EventManagerPlugin { + manager: EventManager; + + // This plugin should come last in the list of plugins, because it accepts all + // events. + supports(eventName: string): boolean { + return true; + } + + addEventListener(element: Element, eventName: string, handler: Function, + shouldSupportBubble: boolean) { + var outsideHandler = shouldSupportBubble ? + DomEventsPlugin.bubbleCallback(element, handler, this.manager._zone) : + DomEventsPlugin.sameElementCallback(element, handler, this.manager._zone); + + this.manager._zone.runOutsideAngular(() => { + DOM.on(element, eventName, outsideHandler); + }); + } + + static sameElementCallback(element, handler, zone) { + return (event) => { + if (event.target === element) { + zone.run(() => handler(event)); + } + }; + } + + static bubbleCallback(element, handler, zone) { + return (event) => zone.run(() => handler(event)); + } +} diff --git a/modules/angular2/src/core/events/hammer_gestures.dart b/modules/angular2/src/core/events/hammer_gestures.dart index f5404f6080..89c07e728c 100644 --- a/modules/angular2/src/core/events/hammer_gestures.dart +++ b/modules/angular2/src/core/events/hammer_gestures.dart @@ -18,7 +18,8 @@ class HammerGesturesPlugin extends HammerGesturesPluginCommon { return true; } - addEventListener(Element element, String eventName, Function handler) { + addEventListener(Element element, String eventName, Function handler, bool shouldSupportBubble) { + if (shouldSupportBubble) throw new BaseException('Hammer.js plugin does not support bubbling gestures.'); var zone = this.manager.getZone(); eventName = eventName.toLowerCase(); diff --git a/modules/angular2/src/core/events/hammer_gestures.es6 b/modules/angular2/src/core/events/hammer_gestures.es6 index c4d10397ed..87c6cc2a1a 100644 --- a/modules/angular2/src/core/events/hammer_gestures.es6 +++ b/modules/angular2/src/core/events/hammer_gestures.es6 @@ -17,7 +17,8 @@ export class HammerGesturesPlugin extends HammerGesturesPluginCommon { return true; } - addEventListener(element:Element, eventName:string, handler:Function) { + addEventListener(element:Element, eventName:string, handler:Function, shouldSupportBubble: boolean) { + if (shouldSupportBubble) throw new BaseException('Hammer.js plugin does not support bubbling gestures.'); var zone = this.manager.getZone(); eventName = eventName.toLowerCase(); diff --git a/modules/angular2/test/core/compiler/view_spec.js b/modules/angular2/test/core/compiler/view_spec.js index dcbbc37dc1..b30e57137e 100644 --- a/modules/angular2/test/core/compiler/view_spec.js +++ b/modules/angular2/test/core/compiler/view_spec.js @@ -15,7 +15,7 @@ import {View} from 'angular2/src/core/compiler/view'; import {ViewContainer} from 'angular2/src/core/compiler/view_container'; import {reflector} from 'angular2/src/reflection/reflection'; import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone'; -import {EventManager} from 'angular2/src/core/events/event_manager'; +import {EventManager, DomEventsPlugin} from 'angular2/src/core/events/event_manager'; @proxy @IMPLEMENTS(ViewContainer) @@ -439,7 +439,8 @@ export function main() { var view, ctx, called, receivedEvent, dispatchedEvent; function createViewAndContext(protoView) { - view = createView(protoView, new EventManager([], new FakeVmTurnZone())); + view = createView(protoView, + new EventManager([new DomEventsPlugin()], new FakeVmTurnZone())); ctx = view.context; called = 0; receivedEvent = null; diff --git a/modules/angular2/test/core/events/event_manager_spec.js b/modules/angular2/test/core/events/event_manager_spec.js index 95b17fd024..9c85b7e568 100644 --- a/modules/angular2/test/core/events/event_manager_spec.js +++ b/modules/angular2/test/core/events/event_manager_spec.js @@ -1,19 +1,34 @@ import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib'; -import {EventManager, EventManagerPlugin} from 'angular2/src/core/events/event_manager'; +import {EventManager, EventManagerPlugin, DomEventsPlugin} from 'angular2/src/core/events/event_manager'; import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone'; import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection'; -import {DOM, Element} from 'angular2/src/facade/dom'; +import {DOM, Element, document} from 'angular2/src/facade/dom'; export function main() { + var domEventPlugin; + + beforeEach(() => { + domEventPlugin = new DomEventsPlugin(); + }); + describe('EventManager', () => { it('should delegate event bindings to plugins', () => { var element = el('
'); var handler = (e) => e; var plugin = new FakeEventManagerPlugin(['click']); - var manager = new EventManager([plugin], new FakeVmTurnZone()); + var manager = new EventManager([plugin, domEventPlugin], new FakeVmTurnZone()); manager.addEventListener(element, 'click', handler); - expect(MapWrapper.get(plugin._eventHandlers, 'click')).toBe(handler); + expect(MapWrapper.get(plugin._nonBubbleEventHandlers, 'click')).toBe(handler); + }); + + it('should delegate bubbling events to plugins', () => { + var element = el('
'); + var handler = (e) => e; + var plugin = new FakeEventManagerPlugin(['click']); + var manager = new EventManager([plugin, domEventPlugin], new FakeVmTurnZone()); + manager.addEventListener(element, '^click', handler); + expect(MapWrapper.get(plugin._bubbleEventHandlers, 'click')).toBe(handler); }); it('should delegate event bindings to the first plugin supporting the event', () => { @@ -25,21 +40,46 @@ export function main() { var manager = new EventManager([plugin1, plugin2], new FakeVmTurnZone()); manager.addEventListener(element, 'click', clickHandler); manager.addEventListener(element, 'dblclick', dblClickHandler); - expect(MapWrapper.contains(plugin1._eventHandlers, 'click')).toBe(false); - expect(MapWrapper.get(plugin2._eventHandlers, 'click')).toBe(clickHandler); - expect(MapWrapper.contains(plugin2._eventHandlers, 'dblclick')).toBe(false); - expect(MapWrapper.get(plugin1._eventHandlers, 'dblclick')).toBe(dblClickHandler); + expect(MapWrapper.contains(plugin1._nonBubbleEventHandlers, 'click')).toBe(false); + expect(MapWrapper.get(plugin2._nonBubbleEventHandlers, 'click')).toBe(clickHandler); + expect(MapWrapper.contains(plugin2._nonBubbleEventHandlers, 'dblclick')).toBe(false); + expect(MapWrapper.get(plugin1._nonBubbleEventHandlers, 'dblclick')).toBe(dblClickHandler); }); - it('should fall back to native events when no plugin can handle the event', () => { + it('should throw when no plugin can handle the event', () => { var element = el('
'); + var plugin = new FakeEventManagerPlugin(['dblclick']); + var manager = new EventManager([plugin], new FakeVmTurnZone()); + expect(() => manager.addEventListener(element, 'click', null)) + .toThrowError('No event manager plugin found for event click'); + }); + + it('by default events are only caught on same element', () => { + var element = el('
'); + var child = DOM.firstChild(element); var dispatchedEvent = DOM.createMouseEvent('click'); var receivedEvent = null; var handler = (e) => { receivedEvent = e; }; - var plugin = new FakeEventManagerPlugin(['dblclick']); - var manager = new EventManager([plugin], new FakeVmTurnZone()); + var manager = new EventManager([domEventPlugin], new FakeVmTurnZone()); manager.addEventListener(element, 'click', handler); - DOM.dispatchEvent(element, dispatchedEvent); + DOM.dispatchEvent(child, dispatchedEvent); + + expect(receivedEvent).toBe(null); + }); + + it('bubbled events are caught when fired from a child', () => { + var element = el('
'); + // Workaround for https://bugs.webkit.org/show_bug.cgi?id=122755 + DOM.appendChild(document.body, element); + + var child = DOM.firstChild(element); + var dispatchedEvent = DOM.createMouseEvent('click'); + var receivedEvent = null; + var handler = (e) => { receivedEvent = e; }; + var manager = new EventManager([domEventPlugin], new FakeVmTurnZone()); + manager.addEventListener(element, '^click', handler); + DOM.dispatchEvent(child, dispatchedEvent); + expect(receivedEvent).toBe(dispatchedEvent); }); }); @@ -47,19 +87,22 @@ export function main() { class FakeEventManagerPlugin extends EventManagerPlugin { _supports: List; - _eventHandlers: Map; + _nonBubbleEventHandlers: Map; + _bubbleEventHandlers: Map; constructor(supports: List) { super(); this._supports = supports; - this._eventHandlers = MapWrapper.create(); + this._nonBubbleEventHandlers = MapWrapper.create(); + this._bubbleEventHandlers = MapWrapper.create(); } supports(eventName: string): boolean { return ListWrapper.contains(this._supports, eventName); } - addEventListener(element: Element, eventName: string, handler: Function) { - MapWrapper.set(this._eventHandlers, eventName, handler); + addEventListener(element: Element, eventName: string, handler: Function, shouldSupportBubble: boolean) { + MapWrapper.set(shouldSupportBubble ? this._bubbleEventHandlers : this._nonBubbleEventHandlers, + eventName, handler); } }