Skip to content

Commit 8fa1539

Browse files
committed
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 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
1 parent f45281a commit 8fa1539

File tree

9 files changed

+465
-2
lines changed

9 files changed

+465
-2
lines changed

modules/angular2/src/core/application.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {EmulatedUnscopedShadowDomStrategy} from 'angular2/src/render/dom/shadow_
1818
import {XHR} from 'angular2/src/services/xhr';
1919
import {XHRImpl} from 'angular2/src/services/xhr_impl';
2020
import {EventManager, DomEventsPlugin} from 'angular2/src/render/dom/events/event_manager';
21+
import {KeyEventsPlugin} from 'angular2/src/render/dom/events/key_events';
2122
import {HammerGesturesPlugin} from 'angular2/src/render/dom/events/hammer_gestures';
2223
import {Binding} from 'angular2/src/di/binding';
2324
import {ComponentUrlMapper} from 'angular2/src/core/compiler/component_url_mapper';
@@ -81,7 +82,7 @@ function _injectorBindings(appComponentType): List<Binding> {
8182
[appComponentRefToken]),
8283
bind(LifeCycle).toFactory((exceptionHandler) => new LifeCycle(exceptionHandler, null, assertionsEnabled()),[ExceptionHandler]),
8384
bind(EventManager).toFactory((zone) => {
84-
var plugins = [new HammerGesturesPlugin(), new DomEventsPlugin()];
85+
var plugins = [new HammerGesturesPlugin(), new KeyEventsPlugin(), new DomEventsPlugin()];
8586
return new EventManager(plugins, zone);
8687
}, [VmTurnZone]),
8788
bind(ShadowDomStrategy).toFactory(

modules/angular2/src/dom/browser_adapter.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,87 @@ class _IdentitySanitizer implements NodeTreeSanitizer {
1414

1515
final _identitySanitizer = new _IdentitySanitizer();
1616

17+
final _keyCodeToKeyMap = const {
18+
8: 'Backspace',
19+
9: 'Tab',
20+
12: 'Clear',
21+
13: 'Enter',
22+
16: 'Shift',
23+
17: 'Control',
24+
18: 'Alt',
25+
19: 'Pause',
26+
20: 'CapsLock',
27+
27: 'Escape',
28+
32: ' ',
29+
33: 'PageUp',
30+
34: 'PageDown',
31+
35: 'End',
32+
36: 'Home',
33+
37: 'ArrowLeft',
34+
38: 'ArrowUp',
35+
39: 'ArrowRight',
36+
40: 'ArrowDown',
37+
45: 'Insert',
38+
46: 'Delete',
39+
65: 'a',
40+
66: 'b',
41+
67: 'c',
42+
68: 'd',
43+
69: 'e',
44+
70: 'f',
45+
71: 'g',
46+
72: 'h',
47+
73: 'i',
48+
74: 'j',
49+
75: 'k',
50+
76: 'l',
51+
77: 'm',
52+
78: 'n',
53+
79: 'o',
54+
80: 'p',
55+
81: 'q',
56+
82: 'r',
57+
83: 's',
58+
84: 't',
59+
85: 'u',
60+
86: 'v',
61+
87: 'w',
62+
88: 'x',
63+
89: 'y',
64+
90: 'z',
65+
91: 'OS',
66+
93: 'ContextMenu',
67+
96: '0',
68+
97: '1',
69+
98: '2',
70+
99: '3',
71+
100: '4',
72+
101: '5',
73+
102: '6',
74+
103: '7',
75+
104: '8',
76+
105: '9',
77+
106: '*',
78+
107: '+',
79+
109: '-',
80+
110: '.',
81+
111: '/',
82+
112: 'F1',
83+
113: 'F2',
84+
114: 'F3',
85+
115: 'F4',
86+
116: 'F5',
87+
117: 'F6',
88+
118: 'F7',
89+
119: 'F8',
90+
120: 'F9',
91+
121: 'F10',
92+
122: 'F11',
93+
123: 'F12',
94+
144: 'NumLock',
95+
145: 'ScrollLock'
96+
};
97+
1798
class BrowserDomAdapter extends GenericBrowserDomAdapter {
1899
static void makeCurrent() {
19100
setRootDomAdapter(new BrowserDomAdapter());
@@ -203,4 +284,8 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
203284
String getHref(AnchorElement element) {
204285
return element.href;
205286
}
287+
String getEventKey(KeyboardEvent event) {
288+
int keyCode = event.keyCode;
289+
return _keyCodeToKeyMap.containsKey(keyCode) ? _keyCodeToKeyMap[keyCode] : 'Unidentified';
290+
}
206291
}

modules/angular2/src/dom/browser_adapter.es6

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
2-
import {isPresent} from 'angular2/src/facade/lang';
2+
import {isBlank, isPresent} from 'angular2/src/facade/lang';
33
import {setRootDomAdapter} from './dom_adapter';
44
import {GenericBrowserDomAdapter} from './generic_browser_adapter';
55

@@ -9,6 +9,49 @@ var _attrToPropMap = {
99
'tabindex': 'tabIndex'
1010
};
1111

12+
const DOM_KEY_LOCATION_NUMPAD = 3;
13+
14+
// Map to convert some key or keyIdentifier values to what will be returned by getEventKey
15+
var _keyMap = {
16+
// The following values are here for cross-browser compatibility and to match the W3C standard
17+
// cf http://www.w3.org/TR/DOM-Level-3-Events-key/
18+
'\b': 'Backspace',
19+
'\t': 'Tab',
20+
'\x7F': 'Delete',
21+
'\x1B': 'Escape',
22+
'Del': 'Delete',
23+
'Esc': 'Escape',
24+
'Left': 'ArrowLeft',
25+
'Right': 'ArrowRight',
26+
'Up': 'ArrowUp',
27+
'Down':'ArrowDown',
28+
'Menu': 'ContextMenu',
29+
'Scroll' : 'ScrollLock',
30+
'Win': 'OS'
31+
};
32+
33+
// There is a bug in Chrome for numeric keypad keys:
34+
// https://code.google.com/p/chromium/issues/detail?id=155654
35+
// 1, 2, 3 ... are reported as A, B, C ...
36+
var _chromeNumKeyPadMap = {
37+
'A': '1',
38+
'B': '2',
39+
'C': '3',
40+
'D': '4',
41+
'E': '5',
42+
'F': '6',
43+
'G': '7',
44+
'H': '8',
45+
'I': '9',
46+
'J': '*',
47+
'K': '+',
48+
'M': '-',
49+
'N': '.',
50+
'O': '/',
51+
'\x60': '0',
52+
'\x90': 'NumLock'
53+
};
54+
1255
export class BrowserDomAdapter extends GenericBrowserDomAdapter {
1356
static makeCurrent() {
1457
setRootDomAdapter(new BrowserDomAdapter());
@@ -286,4 +329,28 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
286329
getHref(el:Element): string {
287330
return el.href;
288331
}
332+
getEventKey(event): string {
333+
var key = event.key;
334+
if (isBlank(key)) {
335+
key = event.keyIdentifier;
336+
// keyIdentifier is defined in the old draft of DOM Level 3 Events implemented by Chrome and Safari
337+
// cf http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/events.html#Events-KeyboardEvents-Interfaces
338+
if (isBlank(key)) {
339+
return 'Unidentified';
340+
}
341+
if (key.startsWith('U+')) {
342+
key = String.fromCharCode(parseInt(key.substring(2), 16));
343+
if (event.location === DOM_KEY_LOCATION_NUMPAD && _chromeNumKeyPadMap.hasOwnProperty(key)) {
344+
// There is a bug in Chrome for numeric keypad keys:
345+
// https://code.google.com/p/chromium/issues/detail?id=155654
346+
// 1, 2, 3 ... are reported as A, B, C ...
347+
key = _chromeNumKeyPadMap[key];
348+
}
349+
}
350+
}
351+
if (_keyMap.hasOwnProperty(key)) {
352+
key = _keyMap[key];
353+
}
354+
return key;
355+
}
289356
}

modules/angular2/src/dom/dom_adapter.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ export class DomAdapter {
258258
getHref(element): string {
259259
throw _abstract();
260260
}
261+
getEventKey(event): string {
262+
throw _abstract();
263+
}
261264
resolveAndSetHref(element, baseUrl:string, href:string) {
262265
throw _abstract();
263266
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {DOM} from 'angular2/src/dom/dom_adapter';
2+
import {isPresent, isBlank, StringWrapper, RegExpWrapper, BaseException, NumberWrapper} from 'angular2/src/facade/lang';
3+
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
4+
import {EventManagerPlugin} from './event_manager';
5+
6+
var modifierKeys = ['alt', 'control', 'meta', 'shift'];
7+
var modifierKeyGetters = {
8+
'alt': (event) => event.altKey,
9+
'control': (event) => event.ctrlKey,
10+
'meta': (event) => event.metaKey,
11+
'shift': (event) => event.shiftKey
12+
}
13+
14+
export class KeyEventsPlugin extends EventManagerPlugin {
15+
constructor() {
16+
super();
17+
}
18+
19+
supports(eventName: string): boolean {
20+
return isPresent(KeyEventsPlugin.parseEventName(eventName));
21+
}
22+
23+
addEventListener(element, eventName: string, handler: Function,
24+
shouldSupportBubble: boolean) {
25+
var parsedEvent = KeyEventsPlugin.parseEventName(eventName);
26+
27+
var outsideHandler = KeyEventsPlugin.eventCallback(element, shouldSupportBubble,
28+
StringMapWrapper.get(parsedEvent, 'fullKey'), handler, this.manager.getZone());
29+
30+
this.manager.getZone().runOutsideAngular(() => {
31+
DOM.on(element, StringMapWrapper.get(parsedEvent, 'domEventName'), outsideHandler);
32+
});
33+
}
34+
35+
static parseEventName(eventName: string) /* {'domEventName': string, 'fullKey': string} */ {
36+
eventName = eventName.toLowerCase();
37+
var parts = eventName.split('.');
38+
var domEventName = ListWrapper.removeAt(parts, 0);
39+
if ((parts.length === 0) || !(StringWrapper.equals(domEventName, 'keydown') || StringWrapper.equals(domEventName, 'keyup'))) {
40+
return null;
41+
}
42+
var key = ListWrapper.removeLast(parts);
43+
44+
var fullKey = '';
45+
ListWrapper.forEach(modifierKeys, (modifierName) => {
46+
if (ListWrapper.contains(parts, modifierName)) {
47+
ListWrapper.remove(parts, modifierName);
48+
fullKey += modifierName + '.';
49+
}
50+
});
51+
fullKey += key;
52+
53+
if (parts.length != 0 || key.length === 0) {
54+
// returning null instead of throwing to let another plugin process the event
55+
return null;
56+
}
57+
58+
return {
59+
'domEventName': domEventName,
60+
'fullKey': fullKey
61+
};
62+
}
63+
64+
static getEventFullKey(event): string {
65+
var fullKey = '';
66+
var key = DOM.getEventKey(event);
67+
key = key.toLowerCase();
68+
if (StringWrapper.equals(key, ' ')) {
69+
key = 'space'; // for readability
70+
} else if (StringWrapper.equals(key, '.')) {
71+
key = 'dot'; // because '.' is used as a separator in event names
72+
}
73+
ListWrapper.forEach(modifierKeys, (modifierName) => {
74+
if (modifierName != key) {
75+
var modifierGetter = StringMapWrapper.get(modifierKeyGetters, modifierName);
76+
if (modifierGetter(event)) {
77+
fullKey += modifierName + '.';
78+
}
79+
}
80+
});
81+
fullKey += key;
82+
return fullKey;
83+
}
84+
85+
static eventCallback(element, shouldSupportBubble, fullKey, handler, zone) {
86+
return (event) => {
87+
var correctElement = shouldSupportBubble || event.target === element;
88+
if (correctElement && KeyEventsPlugin.getEventFullKey(event) === fullKey) {
89+
zone.run(() => handler(event));
90+
}
91+
};
92+
}
93+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib';
2+
import {KeyEventsPlugin} from 'angular2/src/render/dom/events/key_events';
3+
4+
export function main() {
5+
describe('KeyEvents', () => {
6+
7+
it('should ignore unrecognized events', () => {
8+
expect(KeyEventsPlugin.parseEventName('keydown')).toEqual(null);
9+
expect(KeyEventsPlugin.parseEventName('keyup')).toEqual(null);
10+
expect(KeyEventsPlugin.parseEventName('keydown.unknownmodifier.enter')).toEqual(null);
11+
expect(KeyEventsPlugin.parseEventName('keyup.unknownmodifier.enter')).toEqual(null);
12+
expect(KeyEventsPlugin.parseEventName('unknownevent.control.shift.enter')).toEqual(null);
13+
expect(KeyEventsPlugin.parseEventName('unknownevent.enter')).toEqual(null);
14+
});
15+
16+
it('should correctly parse event names', () => {
17+
// key with no modifier
18+
expect(KeyEventsPlugin.parseEventName('keydown.enter')).toEqual({
19+
'domEventName': 'keydown',
20+
'fullKey': 'enter'
21+
});
22+
expect(KeyEventsPlugin.parseEventName('keyup.enter')).toEqual({
23+
'domEventName': 'keyup',
24+
'fullKey': 'enter'
25+
});
26+
27+
// key with modifiers:
28+
expect(KeyEventsPlugin.parseEventName('keydown.control.shift.enter')).toEqual({
29+
'domEventName': 'keydown',
30+
'fullKey': 'control.shift.enter'
31+
});
32+
expect(KeyEventsPlugin.parseEventName('keyup.control.shift.enter')).toEqual({
33+
'domEventName': 'keyup',
34+
'fullKey': 'control.shift.enter'
35+
});
36+
37+
// key with modifiers in a different order:
38+
expect(KeyEventsPlugin.parseEventName('keydown.shift.control.enter')).toEqual({
39+
'domEventName': 'keydown',
40+
'fullKey': 'control.shift.enter'
41+
});
42+
expect(KeyEventsPlugin.parseEventName('keyup.shift.control.enter')).toEqual({
43+
'domEventName': 'keyup',
44+
'fullKey': 'control.shift.enter'
45+
});
46+
47+
// key that is also a modifier:
48+
expect(KeyEventsPlugin.parseEventName('keydown.shift.control')).toEqual({
49+
'domEventName': 'keydown',
50+
'fullKey': 'shift.control'
51+
});
52+
expect(KeyEventsPlugin.parseEventName('keyup.shift.control')).toEqual({
53+
'domEventName': 'keyup',
54+
'fullKey': 'shift.control'
55+
});
56+
57+
expect(KeyEventsPlugin.parseEventName('keydown.control.shift')).toEqual({
58+
'domEventName': 'keydown',
59+
'fullKey': 'control.shift'
60+
});
61+
expect(KeyEventsPlugin.parseEventName('keyup.control.shift')).toEqual({
62+
'domEventName': 'keyup',
63+
'fullKey': 'control.shift'
64+
});
65+
66+
});
67+
68+
});
69+
}

0 commit comments

Comments
 (0)