feat(events): adds support for bubbling native events (^event).
Refactor - move DomEventManager into its own plugin.
This commit is contained in:
parent
2f015cc336
commit
6ad2c18277
|
@ -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 {ShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy';
|
||||||
import {XHR} from 'angular2/src/core/compiler/xhr/xhr';
|
import {XHR} from 'angular2/src/core/compiler/xhr/xhr';
|
||||||
import {XHRImpl} from 'angular2/src/core/compiler/xhr/xhr_impl';
|
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 {HammerGesturesPlugin} from 'angular2/src/core/events/hammer_gestures';
|
||||||
import {Binding} from 'angular2/src/di/binding';
|
import {Binding} from 'angular2/src/di/binding';
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ function _injectorBindings(appComponentType): List<Binding> {
|
||||||
[appViewToken]),
|
[appViewToken]),
|
||||||
bind(LifeCycle).toFactory((exceptionHandler) => new LifeCycle(exceptionHandler, null, assertionsEnabled()),[ExceptionHandler]),
|
bind(LifeCycle).toFactory((exceptionHandler) => new LifeCycle(exceptionHandler, null, assertionsEnabled()),[ExceptionHandler]),
|
||||||
bind(EventManager).toFactory((zone) => {
|
bind(EventManager).toFactory((zone) => {
|
||||||
var plugins = [new HammerGesturesPlugin()];
|
var plugins = [new HammerGesturesPlugin(), new DomEventsPlugin()];
|
||||||
return new EventManager(plugins, zone);
|
return new EventManager(plugins, zone);
|
||||||
}, [VmTurnZone]),
|
}, [VmTurnZone]),
|
||||||
bind(ShadowDomStrategy).toValue(new NativeShadowDomStrategy()),
|
bind(ShadowDomStrategy).toValue(new NativeShadowDomStrategy()),
|
||||||
|
|
|
@ -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 {DOM, Element} from 'angular2/src/facade/dom';
|
||||||
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||||
import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone';
|
import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone';
|
||||||
|
|
||||||
|
var BUBBLE_SYMBOL = '^';
|
||||||
|
|
||||||
export class EventManager {
|
export class EventManager {
|
||||||
_plugins: List<EventManagerPlugin>;
|
_plugins: List<EventManagerPlugin>;
|
||||||
_zone: VmTurnZone;
|
_zone: VmTurnZone;
|
||||||
|
@ -16,13 +18,13 @@ export class EventManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(element: Element, eventName: string, handler: Function) {
|
addEventListener(element: Element, eventName: string, handler: Function) {
|
||||||
var plugin = this._findPluginFor(eventName);
|
var shouldSupportBubble = eventName[0] == BUBBLE_SYMBOL;
|
||||||
|
if (shouldSupportBubble) {
|
||||||
if (isPresent(plugin)) {
|
eventName = StringWrapper.substring(eventName, 1);
|
||||||
plugin.addEventListener(element, eventName, handler);
|
|
||||||
} else {
|
|
||||||
this._addNativeEventListener(element, eventName, handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var plugin = this._findPluginFor(eventName);
|
||||||
|
plugin.addEventListener(element, eventName, handler, shouldSupportBubble);
|
||||||
}
|
}
|
||||||
|
|
||||||
getZone(): VmTurnZone {
|
getZone(): VmTurnZone {
|
||||||
|
@ -37,30 +39,56 @@ export class EventManager {
|
||||||
return plugin;
|
return plugin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
throw new BaseException(`No event manager plugin found for event ${eventName}`);
|
||||||
}
|
|
||||||
|
|
||||||
_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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventManagerPlugin {
|
export class EventManagerPlugin {
|
||||||
manager: EventManager;
|
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 {
|
supports(eventName: string): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(element: Element, eventName: string, handler: Function) {
|
addEventListener(element: Element, eventName: string, handler: Function,
|
||||||
|
shouldSupportBubble: boolean) {
|
||||||
throw "not implemented";
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ class HammerGesturesPlugin extends HammerGesturesPluginCommon {
|
||||||
return true;
|
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();
|
var zone = this.manager.getZone();
|
||||||
eventName = eventName.toLowerCase();
|
eventName = eventName.toLowerCase();
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@ export class HammerGesturesPlugin extends HammerGesturesPluginCommon {
|
||||||
return true;
|
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();
|
var zone = this.manager.getZone();
|
||||||
eventName = eventName.toLowerCase();
|
eventName = eventName.toLowerCase();
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {View} from 'angular2/src/core/compiler/view';
|
||||||
import {ViewContainer} from 'angular2/src/core/compiler/view_container';
|
import {ViewContainer} from 'angular2/src/core/compiler/view_container';
|
||||||
import {reflector} from 'angular2/src/reflection/reflection';
|
import {reflector} from 'angular2/src/reflection/reflection';
|
||||||
import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone';
|
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
|
@proxy
|
||||||
@IMPLEMENTS(ViewContainer)
|
@IMPLEMENTS(ViewContainer)
|
||||||
|
@ -439,7 +439,8 @@ export function main() {
|
||||||
var view, ctx, called, receivedEvent, dispatchedEvent;
|
var view, ctx, called, receivedEvent, dispatchedEvent;
|
||||||
|
|
||||||
function createViewAndContext(protoView) {
|
function createViewAndContext(protoView) {
|
||||||
view = createView(protoView, new EventManager([], new FakeVmTurnZone()));
|
view = createView(protoView,
|
||||||
|
new EventManager([new DomEventsPlugin()], new FakeVmTurnZone()));
|
||||||
ctx = view.context;
|
ctx = view.context;
|
||||||
called = 0;
|
called = 0;
|
||||||
receivedEvent = null;
|
receivedEvent = null;
|
||||||
|
|
|
@ -1,19 +1,34 @@
|
||||||
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib';
|
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 {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone';
|
||||||
import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection';
|
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() {
|
export function main() {
|
||||||
|
var domEventPlugin;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
domEventPlugin = new DomEventsPlugin();
|
||||||
|
});
|
||||||
|
|
||||||
describe('EventManager', () => {
|
describe('EventManager', () => {
|
||||||
|
|
||||||
it('should delegate event bindings to plugins', () => {
|
it('should delegate event bindings to plugins', () => {
|
||||||
var element = el('<div></div>');
|
var element = el('<div></div>');
|
||||||
var handler = (e) => e;
|
var handler = (e) => e;
|
||||||
var plugin = new FakeEventManagerPlugin(['click']);
|
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);
|
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('<div></div>');
|
||||||
|
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', () => {
|
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());
|
var manager = new EventManager([plugin1, plugin2], new FakeVmTurnZone());
|
||||||
manager.addEventListener(element, 'click', clickHandler);
|
manager.addEventListener(element, 'click', clickHandler);
|
||||||
manager.addEventListener(element, 'dblclick', dblClickHandler);
|
manager.addEventListener(element, 'dblclick', dblClickHandler);
|
||||||
expect(MapWrapper.contains(plugin1._eventHandlers, 'click')).toBe(false);
|
expect(MapWrapper.contains(plugin1._nonBubbleEventHandlers, 'click')).toBe(false);
|
||||||
expect(MapWrapper.get(plugin2._eventHandlers, 'click')).toBe(clickHandler);
|
expect(MapWrapper.get(plugin2._nonBubbleEventHandlers, 'click')).toBe(clickHandler);
|
||||||
expect(MapWrapper.contains(plugin2._eventHandlers, 'dblclick')).toBe(false);
|
expect(MapWrapper.contains(plugin2._nonBubbleEventHandlers, 'dblclick')).toBe(false);
|
||||||
expect(MapWrapper.get(plugin1._eventHandlers, 'dblclick')).toBe(dblClickHandler);
|
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('<div></div>');
|
var element = el('<div></div>');
|
||||||
|
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('<div><div></div></div>');
|
||||||
|
var child = DOM.firstChild(element);
|
||||||
var dispatchedEvent = DOM.createMouseEvent('click');
|
var dispatchedEvent = DOM.createMouseEvent('click');
|
||||||
var receivedEvent = null;
|
var receivedEvent = null;
|
||||||
var handler = (e) => { receivedEvent = e; };
|
var handler = (e) => { receivedEvent = e; };
|
||||||
var plugin = new FakeEventManagerPlugin(['dblclick']);
|
var manager = new EventManager([domEventPlugin], new FakeVmTurnZone());
|
||||||
var manager = new EventManager([plugin], new FakeVmTurnZone());
|
|
||||||
manager.addEventListener(element, 'click', handler);
|
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('<div><div></div></div>');
|
||||||
|
// 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);
|
expect(receivedEvent).toBe(dispatchedEvent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -47,19 +87,22 @@ export function main() {
|
||||||
|
|
||||||
class FakeEventManagerPlugin extends EventManagerPlugin {
|
class FakeEventManagerPlugin extends EventManagerPlugin {
|
||||||
_supports: List<string>;
|
_supports: List<string>;
|
||||||
_eventHandlers: Map;
|
_nonBubbleEventHandlers: Map;
|
||||||
|
_bubbleEventHandlers: Map;
|
||||||
constructor(supports: List<string>) {
|
constructor(supports: List<string>) {
|
||||||
super();
|
super();
|
||||||
this._supports = supports;
|
this._supports = supports;
|
||||||
this._eventHandlers = MapWrapper.create();
|
this._nonBubbleEventHandlers = MapWrapper.create();
|
||||||
|
this._bubbleEventHandlers = MapWrapper.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
supports(eventName: string): boolean {
|
supports(eventName: string): boolean {
|
||||||
return ListWrapper.contains(this._supports, eventName);
|
return ListWrapper.contains(this._supports, eventName);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(element: Element, eventName: string, handler: Function) {
|
addEventListener(element: Element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
|
||||||
MapWrapper.set(this._eventHandlers, eventName, handler);
|
MapWrapper.set(shouldSupportBubble ? this._bubbleEventHandlers : this._nonBubbleEventHandlers,
|
||||||
|
eventName, handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue