Skip to content

Commit

Permalink
feat(keyEvents): support for <div (keyup.enter)="callback()">
Browse files Browse the repository at this point in the history
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 angular#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 angular#523
close angular#1136
  • Loading branch information
divdavem committed Apr 8, 2015
1 parent ca95846 commit 051b2a7
Show file tree
Hide file tree
Showing 8 changed files with 395 additions and 2 deletions.
3 changes: 2 additions & 1 deletion modules/angular2/src/core/application.js
Expand Up @@ -18,6 +18,7 @@ import {ShadowDomStrategy, NativeShadowDomStrategy, EmulatedUnscopedShadowDomStr
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';
Expand Down Expand Up @@ -92,7 +93,7 @@ function _injectorBindings(appComponentType): List<Binding> {
[appViewToken]),
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(
Expand Down
85 changes: 85 additions & 0 deletions modules/angular2/src/dom/browser_adapter.dart
Expand Up @@ -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());
Expand Down Expand Up @@ -202,4 +283,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';
}
}
68 changes: 67 additions & 1 deletion modules/angular2/src/dom/browser_adapter.es6
@@ -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';

Expand All @@ -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());
Expand Down Expand Up @@ -283,4 +326,27 @@ 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)) {
key = 'Unidentified';
} else 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;
}
}
3 changes: 3 additions & 0 deletions modules/angular2/src/dom/dom_adapter.js
Expand Up @@ -255,6 +255,9 @@ export class DomAdapter {
getHref(element): string {
throw _abstract();
}
getEventKey(event): string {
throw _abstract();
}
resolveAndSetHref(element, baseUrl:string, href:string) {
throw _abstract();
}
Expand Down
93 changes: 93 additions & 0 deletions modules/angular2/src/render/dom/events/key_events.js
@@ -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) {
eventName = eventName.toLowerCase();
var parts = eventName.split('.');
var domEventName = ListWrapper.removeAt(parts, 0);
if ((parts.length === 0) || (domEventName !== 'keydown' && 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 (key == ' ') {
key = 'space'; // for readability
} else if (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));
}
};
}
}
69 changes: 69 additions & 0 deletions modules/examples/e2e_test/key_events/key_events_spec.es6
@@ -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('');
});

});
26 changes: 26 additions & 0 deletions modules/examples/src/key_events/index.html
@@ -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 comments on commit 051b2a7

Please sign in to comment.