feat(keyEvents): support for <div (keyup.enter)="callback()">
This commit adds a plugin for the event manager, to allow a key name to be appended to the event name (for keyup and keydown events), so that the callback is only called for that key. Here are some examples: (keydown.shift.enter) (keyup.space) (keydown.control.shift.a) (keyup.f1) Key names mostly follow the DOM Level 3 event key values: http://www.w3.org/TR/DOM-Level-3-Events-key/#key-value-tables There are some limitations to be worked on (cf details in https://github.com/angular/angular/pull/1136) but for now, this implementation is reliable for the following keys (by "reliable" I mean compatible with Chrome and Firefox and not depending on the keyboard layout): - alt, control, shift, meta (those keys can be combined with other keys) - tab, enter, backspace, pause, scrolllock, capslock, numlock - insert, delete, home, end, pageup, pagedown - arrowup, arrowdown, arrowleft, arrowright - latin letters (a-z), function keys (f1-f12) - numbers on the numeric keypad (but those keys are not correctly simulated by Chromedriver) There is a sample to play with in examples/src/key_events/. close #523 close #1136
This commit is contained in:
parent
f45281a10a
commit
8fa1539bac
|
@ -18,6 +18,7 @@ import {EmulatedUnscopedShadowDomStrategy} from 'angular2/src/render/dom/shadow_
|
|||
import {XHR} from 'angular2/src/services/xhr';
|
||||
import {XHRImpl} from 'angular2/src/services/xhr_impl';
|
||||
import {EventManager, DomEventsPlugin} from 'angular2/src/render/dom/events/event_manager';
|
||||
import {KeyEventsPlugin} from 'angular2/src/render/dom/events/key_events';
|
||||
import {HammerGesturesPlugin} from 'angular2/src/render/dom/events/hammer_gestures';
|
||||
import {Binding} from 'angular2/src/di/binding';
|
||||
import {ComponentUrlMapper} from 'angular2/src/core/compiler/component_url_mapper';
|
||||
|
@ -81,7 +82,7 @@ function _injectorBindings(appComponentType): List<Binding> {
|
|||
[appComponentRefToken]),
|
||||
bind(LifeCycle).toFactory((exceptionHandler) => new LifeCycle(exceptionHandler, null, assertionsEnabled()),[ExceptionHandler]),
|
||||
bind(EventManager).toFactory((zone) => {
|
||||
var plugins = [new HammerGesturesPlugin(), new DomEventsPlugin()];
|
||||
var plugins = [new HammerGesturesPlugin(), new KeyEventsPlugin(), new DomEventsPlugin()];
|
||||
return new EventManager(plugins, zone);
|
||||
}, [VmTurnZone]),
|
||||
bind(ShadowDomStrategy).toFactory(
|
||||
|
|
|
@ -14,6 +14,87 @@ class _IdentitySanitizer implements NodeTreeSanitizer {
|
|||
|
||||
final _identitySanitizer = new _IdentitySanitizer();
|
||||
|
||||
final _keyCodeToKeyMap = const {
|
||||
8: 'Backspace',
|
||||
9: 'Tab',
|
||||
12: 'Clear',
|
||||
13: 'Enter',
|
||||
16: 'Shift',
|
||||
17: 'Control',
|
||||
18: 'Alt',
|
||||
19: 'Pause',
|
||||
20: 'CapsLock',
|
||||
27: 'Escape',
|
||||
32: ' ',
|
||||
33: 'PageUp',
|
||||
34: 'PageDown',
|
||||
35: 'End',
|
||||
36: 'Home',
|
||||
37: 'ArrowLeft',
|
||||
38: 'ArrowUp',
|
||||
39: 'ArrowRight',
|
||||
40: 'ArrowDown',
|
||||
45: 'Insert',
|
||||
46: 'Delete',
|
||||
65: 'a',
|
||||
66: 'b',
|
||||
67: 'c',
|
||||
68: 'd',
|
||||
69: 'e',
|
||||
70: 'f',
|
||||
71: 'g',
|
||||
72: 'h',
|
||||
73: 'i',
|
||||
74: 'j',
|
||||
75: 'k',
|
||||
76: 'l',
|
||||
77: 'm',
|
||||
78: 'n',
|
||||
79: 'o',
|
||||
80: 'p',
|
||||
81: 'q',
|
||||
82: 'r',
|
||||
83: 's',
|
||||
84: 't',
|
||||
85: 'u',
|
||||
86: 'v',
|
||||
87: 'w',
|
||||
88: 'x',
|
||||
89: 'y',
|
||||
90: 'z',
|
||||
91: 'OS',
|
||||
93: 'ContextMenu',
|
||||
96: '0',
|
||||
97: '1',
|
||||
98: '2',
|
||||
99: '3',
|
||||
100: '4',
|
||||
101: '5',
|
||||
102: '6',
|
||||
103: '7',
|
||||
104: '8',
|
||||
105: '9',
|
||||
106: '*',
|
||||
107: '+',
|
||||
109: '-',
|
||||
110: '.',
|
||||
111: '/',
|
||||
112: 'F1',
|
||||
113: 'F2',
|
||||
114: 'F3',
|
||||
115: 'F4',
|
||||
116: 'F5',
|
||||
117: 'F6',
|
||||
118: 'F7',
|
||||
119: 'F8',
|
||||
120: 'F9',
|
||||
121: 'F10',
|
||||
122: 'F11',
|
||||
123: 'F12',
|
||||
144: 'NumLock',
|
||||
145: 'ScrollLock'
|
||||
};
|
||||
|
||||
class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
||||
static void makeCurrent() {
|
||||
setRootDomAdapter(new BrowserDomAdapter());
|
||||
|
@ -203,4 +284,8 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
|||
String getHref(AnchorElement element) {
|
||||
return element.href;
|
||||
}
|
||||
String getEventKey(KeyboardEvent event) {
|
||||
int keyCode = event.keyCode;
|
||||
return _keyCodeToKeyMap.containsKey(keyCode) ? _keyCodeToKeyMap[keyCode] : 'Unidentified';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
import {isBlank, isPresent} from 'angular2/src/facade/lang';
|
||||
import {setRootDomAdapter} from './dom_adapter';
|
||||
import {GenericBrowserDomAdapter} from './generic_browser_adapter';
|
||||
|
||||
|
@ -9,6 +9,49 @@ var _attrToPropMap = {
|
|||
'tabindex': 'tabIndex'
|
||||
};
|
||||
|
||||
const DOM_KEY_LOCATION_NUMPAD = 3;
|
||||
|
||||
// Map to convert some key or keyIdentifier values to what will be returned by getEventKey
|
||||
var _keyMap = {
|
||||
// The following values are here for cross-browser compatibility and to match the W3C standard
|
||||
// cf http://www.w3.org/TR/DOM-Level-3-Events-key/
|
||||
'\b': 'Backspace',
|
||||
'\t': 'Tab',
|
||||
'\x7F': 'Delete',
|
||||
'\x1B': 'Escape',
|
||||
'Del': 'Delete',
|
||||
'Esc': 'Escape',
|
||||
'Left': 'ArrowLeft',
|
||||
'Right': 'ArrowRight',
|
||||
'Up': 'ArrowUp',
|
||||
'Down':'ArrowDown',
|
||||
'Menu': 'ContextMenu',
|
||||
'Scroll' : 'ScrollLock',
|
||||
'Win': 'OS'
|
||||
};
|
||||
|
||||
// There is a bug in Chrome for numeric keypad keys:
|
||||
// https://code.google.com/p/chromium/issues/detail?id=155654
|
||||
// 1, 2, 3 ... are reported as A, B, C ...
|
||||
var _chromeNumKeyPadMap = {
|
||||
'A': '1',
|
||||
'B': '2',
|
||||
'C': '3',
|
||||
'D': '4',
|
||||
'E': '5',
|
||||
'F': '6',
|
||||
'G': '7',
|
||||
'H': '8',
|
||||
'I': '9',
|
||||
'J': '*',
|
||||
'K': '+',
|
||||
'M': '-',
|
||||
'N': '.',
|
||||
'O': '/',
|
||||
'\x60': '0',
|
||||
'\x90': 'NumLock'
|
||||
};
|
||||
|
||||
export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
||||
static makeCurrent() {
|
||||
setRootDomAdapter(new BrowserDomAdapter());
|
||||
|
@ -286,4 +329,28 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
|||
getHref(el:Element): string {
|
||||
return el.href;
|
||||
}
|
||||
getEventKey(event): string {
|
||||
var key = event.key;
|
||||
if (isBlank(key)) {
|
||||
key = event.keyIdentifier;
|
||||
// keyIdentifier is defined in the old draft of DOM Level 3 Events implemented by Chrome and Safari
|
||||
// cf http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/events.html#Events-KeyboardEvents-Interfaces
|
||||
if (isBlank(key)) {
|
||||
return 'Unidentified';
|
||||
}
|
||||
if (key.startsWith('U+')) {
|
||||
key = String.fromCharCode(parseInt(key.substring(2), 16));
|
||||
if (event.location === DOM_KEY_LOCATION_NUMPAD && _chromeNumKeyPadMap.hasOwnProperty(key)) {
|
||||
// There is a bug in Chrome for numeric keypad keys:
|
||||
// https://code.google.com/p/chromium/issues/detail?id=155654
|
||||
// 1, 2, 3 ... are reported as A, B, C ...
|
||||
key = _chromeNumKeyPadMap[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_keyMap.hasOwnProperty(key)) {
|
||||
key = _keyMap[key];
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,6 +258,9 @@ export class DomAdapter {
|
|||
getHref(element): string {
|
||||
throw _abstract();
|
||||
}
|
||||
getEventKey(event): string {
|
||||
throw _abstract();
|
||||
}
|
||||
resolveAndSetHref(element, baseUrl:string, href:string) {
|
||||
throw _abstract();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {isPresent, isBlank, StringWrapper, RegExpWrapper, BaseException, NumberWrapper} from 'angular2/src/facade/lang';
|
||||
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {EventManagerPlugin} from './event_manager';
|
||||
|
||||
var modifierKeys = ['alt', 'control', 'meta', 'shift'];
|
||||
var modifierKeyGetters = {
|
||||
'alt': (event) => event.altKey,
|
||||
'control': (event) => event.ctrlKey,
|
||||
'meta': (event) => event.metaKey,
|
||||
'shift': (event) => event.shiftKey
|
||||
}
|
||||
|
||||
export class KeyEventsPlugin extends EventManagerPlugin {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
supports(eventName: string): boolean {
|
||||
return isPresent(KeyEventsPlugin.parseEventName(eventName));
|
||||
}
|
||||
|
||||
addEventListener(element, eventName: string, handler: Function,
|
||||
shouldSupportBubble: boolean) {
|
||||
var parsedEvent = KeyEventsPlugin.parseEventName(eventName);
|
||||
|
||||
var outsideHandler = KeyEventsPlugin.eventCallback(element, shouldSupportBubble,
|
||||
StringMapWrapper.get(parsedEvent, 'fullKey'), handler, this.manager.getZone());
|
||||
|
||||
this.manager.getZone().runOutsideAngular(() => {
|
||||
DOM.on(element, StringMapWrapper.get(parsedEvent, 'domEventName'), outsideHandler);
|
||||
});
|
||||
}
|
||||
|
||||
static parseEventName(eventName: string) /* {'domEventName': string, 'fullKey': string} */ {
|
||||
eventName = eventName.toLowerCase();
|
||||
var parts = eventName.split('.');
|
||||
var domEventName = ListWrapper.removeAt(parts, 0);
|
||||
if ((parts.length === 0) || !(StringWrapper.equals(domEventName, 'keydown') || StringWrapper.equals(domEventName, 'keyup'))) {
|
||||
return null;
|
||||
}
|
||||
var key = ListWrapper.removeLast(parts);
|
||||
|
||||
var fullKey = '';
|
||||
ListWrapper.forEach(modifierKeys, (modifierName) => {
|
||||
if (ListWrapper.contains(parts, modifierName)) {
|
||||
ListWrapper.remove(parts, modifierName);
|
||||
fullKey += modifierName + '.';
|
||||
}
|
||||
});
|
||||
fullKey += key;
|
||||
|
||||
if (parts.length != 0 || key.length === 0) {
|
||||
// returning null instead of throwing to let another plugin process the event
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
'domEventName': domEventName,
|
||||
'fullKey': fullKey
|
||||
};
|
||||
}
|
||||
|
||||
static getEventFullKey(event): string {
|
||||
var fullKey = '';
|
||||
var key = DOM.getEventKey(event);
|
||||
key = key.toLowerCase();
|
||||
if (StringWrapper.equals(key, ' ')) {
|
||||
key = 'space'; // for readability
|
||||
} else if (StringWrapper.equals(key, '.')) {
|
||||
key = 'dot'; // because '.' is used as a separator in event names
|
||||
}
|
||||
ListWrapper.forEach(modifierKeys, (modifierName) => {
|
||||
if (modifierName != key) {
|
||||
var modifierGetter = StringMapWrapper.get(modifierKeyGetters, modifierName);
|
||||
if (modifierGetter(event)) {
|
||||
fullKey += modifierName + '.';
|
||||
}
|
||||
}
|
||||
});
|
||||
fullKey += key;
|
||||
return fullKey;
|
||||
}
|
||||
|
||||
static eventCallback(element, shouldSupportBubble, fullKey, handler, zone) {
|
||||
return (event) => {
|
||||
var correctElement = shouldSupportBubble || event.target === element;
|
||||
if (correctElement && KeyEventsPlugin.getEventFullKey(event) === fullKey) {
|
||||
zone.run(() => handler(event));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib';
|
||||
import {KeyEventsPlugin} from 'angular2/src/render/dom/events/key_events';
|
||||
|
||||
export function main() {
|
||||
describe('KeyEvents', () => {
|
||||
|
||||
it('should ignore unrecognized events', () => {
|
||||
expect(KeyEventsPlugin.parseEventName('keydown')).toEqual(null);
|
||||
expect(KeyEventsPlugin.parseEventName('keyup')).toEqual(null);
|
||||
expect(KeyEventsPlugin.parseEventName('keydown.unknownmodifier.enter')).toEqual(null);
|
||||
expect(KeyEventsPlugin.parseEventName('keyup.unknownmodifier.enter')).toEqual(null);
|
||||
expect(KeyEventsPlugin.parseEventName('unknownevent.control.shift.enter')).toEqual(null);
|
||||
expect(KeyEventsPlugin.parseEventName('unknownevent.enter')).toEqual(null);
|
||||
});
|
||||
|
||||
it('should correctly parse event names', () => {
|
||||
// key with no modifier
|
||||
expect(KeyEventsPlugin.parseEventName('keydown.enter')).toEqual({
|
||||
'domEventName': 'keydown',
|
||||
'fullKey': 'enter'
|
||||
});
|
||||
expect(KeyEventsPlugin.parseEventName('keyup.enter')).toEqual({
|
||||
'domEventName': 'keyup',
|
||||
'fullKey': 'enter'
|
||||
});
|
||||
|
||||
// key with modifiers:
|
||||
expect(KeyEventsPlugin.parseEventName('keydown.control.shift.enter')).toEqual({
|
||||
'domEventName': 'keydown',
|
||||
'fullKey': 'control.shift.enter'
|
||||
});
|
||||
expect(KeyEventsPlugin.parseEventName('keyup.control.shift.enter')).toEqual({
|
||||
'domEventName': 'keyup',
|
||||
'fullKey': 'control.shift.enter'
|
||||
});
|
||||
|
||||
// key with modifiers in a different order:
|
||||
expect(KeyEventsPlugin.parseEventName('keydown.shift.control.enter')).toEqual({
|
||||
'domEventName': 'keydown',
|
||||
'fullKey': 'control.shift.enter'
|
||||
});
|
||||
expect(KeyEventsPlugin.parseEventName('keyup.shift.control.enter')).toEqual({
|
||||
'domEventName': 'keyup',
|
||||
'fullKey': 'control.shift.enter'
|
||||
});
|
||||
|
||||
// key that is also a modifier:
|
||||
expect(KeyEventsPlugin.parseEventName('keydown.shift.control')).toEqual({
|
||||
'domEventName': 'keydown',
|
||||
'fullKey': 'shift.control'
|
||||
});
|
||||
expect(KeyEventsPlugin.parseEventName('keyup.shift.control')).toEqual({
|
||||
'domEventName': 'keyup',
|
||||
'fullKey': 'shift.control'
|
||||
});
|
||||
|
||||
expect(KeyEventsPlugin.parseEventName('keydown.control.shift')).toEqual({
|
||||
'domEventName': 'keydown',
|
||||
'fullKey': 'control.shift'
|
||||
});
|
||||
expect(KeyEventsPlugin.parseEventName('keyup.control.shift')).toEqual({
|
||||
'domEventName': 'keyup',
|
||||
'fullKey': 'control.shift'
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
var testUtil = require('angular2/src/test_lib/e2e_util');
|
||||
describe('key_events', function () {
|
||||
|
||||
var URL = 'examples/src/key_events/index.html';
|
||||
|
||||
afterEach(testUtil.verifyNoBrowserErrors);
|
||||
beforeEach(() => {
|
||||
browser.get(URL);
|
||||
});
|
||||
|
||||
it('should display correct key names', function() {
|
||||
var firstArea = element.all(by.css('.sample-area')).get(0);
|
||||
expect(firstArea.getText()).toBe('(none)');
|
||||
|
||||
// testing different key categories:
|
||||
firstArea.sendKeys(protractor.Key.ENTER);
|
||||
expect(firstArea.getText()).toBe('enter');
|
||||
|
||||
firstArea.sendKeys(protractor.Key.SHIFT, protractor.Key.ENTER);
|
||||
expect(firstArea.getText()).toBe('shift.enter');
|
||||
|
||||
firstArea.sendKeys(protractor.Key.CONTROL, protractor.Key.SHIFT, protractor.Key.ENTER);
|
||||
expect(firstArea.getText()).toBe('control.shift.enter');
|
||||
|
||||
firstArea.sendKeys(' ');
|
||||
expect(firstArea.getText()).toBe('space');
|
||||
|
||||
firstArea.sendKeys('a');
|
||||
expect(firstArea.getText()).toBe('a');
|
||||
|
||||
firstArea.sendKeys(protractor.Key.CONTROL, 'b');
|
||||
expect(firstArea.getText()).toBe('control.b');
|
||||
|
||||
firstArea.sendKeys(protractor.Key.F1);
|
||||
expect(firstArea.getText()).toBe('f1');
|
||||
|
||||
firstArea.sendKeys(protractor.Key.ALT, protractor.Key.F1);
|
||||
expect(firstArea.getText()).toBe('alt.f1');
|
||||
|
||||
firstArea.sendKeys(protractor.Key.CONTROL, protractor.Key.F1);
|
||||
expect(firstArea.getText()).toBe('control.f1');
|
||||
|
||||
// There is an issue with protractor.Key.NUMPAD0 (and other NUMPADx):
|
||||
// chromedriver does not correctly set the location property on the event to
|
||||
// specify that the key is on the numeric keypad (event.location = 3)
|
||||
// so the following test fails:
|
||||
// firstArea.sendKeys(protractor.Key.NUMPAD0);
|
||||
// expect(firstArea.getText()).toBe('0');
|
||||
});
|
||||
|
||||
it('should correctly react to the specified key', function() {
|
||||
var secondArea = element.all(by.css('.sample-area')).get(1);
|
||||
secondArea.sendKeys(protractor.Key.SHIFT, protractor.Key.ENTER);
|
||||
expect(secondArea.getText()).toEqual('You pressed shift.enter!');
|
||||
});
|
||||
|
||||
it('should not react to incomplete keys', function() {
|
||||
var secondArea = element.all(by.css('.sample-area')).get(1);
|
||||
secondArea.sendKeys(protractor.Key.ENTER);
|
||||
expect(secondArea.getText()).toEqual('');
|
||||
});
|
||||
|
||||
it('should not react to keys with more modifiers', function() {
|
||||
var secondArea = element.all(by.css('.sample-area')).get(1);
|
||||
secondArea.sendKeys(protractor.Key.CONTROL, protractor.Key.SHIFT, protractor.Key.ENTER);
|
||||
expect(secondArea.getText()).toEqual('');
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<title>Key events</title>
|
||||
<style>
|
||||
.sample-area {
|
||||
text-align: center;
|
||||
margin: 5px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #d0d0d0;
|
||||
}
|
||||
.sample-area:focus {
|
||||
border: 1px solid blue;
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<key-events-app>
|
||||
Loading...
|
||||
</key-events-app>
|
||||
|
||||
$SCRIPTS$
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,50 @@
|
|||
import {bootstrap, Component, Template} from 'angular2/angular2';
|
||||
import {KeyEventsPlugin} from 'angular2/src/render/dom/events/key_events';
|
||||
|
||||
// 2 imports for the Dart version:
|
||||
import {reflector} from 'angular2/src/reflection/reflection';
|
||||
import {ReflectionCapabilities} from 'angular2/src/reflection/reflection_capabilities';
|
||||
|
||||
@Component({
|
||||
selector: 'key-events-app',
|
||||
})
|
||||
@Template({
|
||||
inline: `Click in the following area and press a key to display its name:<br>
|
||||
<div (keydown)="onKeyDown($event)" class="sample-area" tabindex="0">{{lastKey}}</div><br>
|
||||
Click in the following area and press shift.enter:<br>
|
||||
<div
|
||||
(keydown.shift.enter)="onShiftEnter($event)"
|
||||
(click)="resetShiftEnter()"
|
||||
class="sample-area"
|
||||
tabindex="0"
|
||||
>{{shiftEnter ? 'You pressed shift.enter!' : ''}}</div>`
|
||||
})
|
||||
class KeyEventsApp {
|
||||
lastKey: string;
|
||||
shiftEnter: boolean;
|
||||
|
||||
constructor() {
|
||||
this.lastKey = '(none)';
|
||||
this.shiftEnter = false;
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
this.lastKey = KeyEventsPlugin.getEventFullKey(event);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
onShiftEnter(event) {
|
||||
this.shiftEnter = true;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
resetShiftEnter() {
|
||||
this.shiftEnter = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function main() {
|
||||
reflector.reflectionCapabilities = new ReflectionCapabilities(); // for the Dart version
|
||||
bootstrap(KeyEventsApp);
|
||||
}
|
Loading…
Reference in New Issue