Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(60)

Side by Side Diff: tools/dom/src/KeyboardEventController.dart

Issue 12419011: Modern-ify KeyEvent handling. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: \ Created 7 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 part of html;
6
7 /**
8 * Works with KeyboardEvent and KeyEvent to determine how to expose information
9 * about Key(board)Events. This class functions like an EventListenerList, and
10 * provides a consistent interface for the Dart
11 * user, despite the fact that a multitude of browsers that have varying
12 * keyboard default behavior.
13 *
14 * This class is very much a work in progress, and we'd love to get information
15 * on how we can make this class work with as many international keyboards as
16 * possible. Bugs welcome!
17 */
18 class KeyboardEventController {
19 // This code inspired by Closure's KeyHandling library.
20 // http://closure-library.googlecode.com/svn/docs/closure_goog_events_keyhandl er.js.source.html
21
22 /**
23 * The set of keys that have been pressed down without seeing their
24 * corresponding keyup event.
25 */
26 List<KeyboardEvent> _keyDownList;
27
28 /** The set of functions that wish to be notified when a KeyEvent happens. */
29 List<Function> _callbacks;
30
31 /** The type of KeyEvent we are tracking (keyup, keydown, keypress). */
32 String _type;
33
34 /** The element we are watching for events to happen on. */
35 EventTarget _target;
36
37 // The distance to shift from upper case alphabet Roman letters to lower case.
38 final int _ROMAN_ALPHABET_OFFSET = "a".codeUnits[0] - "A".codeUnits[0];
39
40 StreamSubscription _keyUpSubscription, _keyDownSubscription,
41 _keyPressSubscription;
42
43 /**
44 * An enumeration of key identifiers currently part of the W3C draft for DOM3
45 * and their mappings to keyCodes.
46 * http://www.w3.org/TR/DOM-Level-3-Events/keyset.html#KeySet-Set
47 */
48 static Map<String, int> _keyIdentifier = {
49 'Up': KeyCode.UP,
50 'Down': KeyCode.DOWN,
51 'Left': KeyCode.LEFT,
52 'Right': KeyCode.RIGHT,
53 'Enter': KeyCode.ENTER,
54 'F1': KeyCode.F1,
55 'F2': KeyCode.F2,
56 'F3': KeyCode.F3,
57 'F4': KeyCode.F4,
58 'F5': KeyCode.F5,
59 'F6': KeyCode.F6,
60 'F7': KeyCode.F7,
61 'F8': KeyCode.F8,
62 'F9': KeyCode.F9,
63 'F10': KeyCode.F10,
64 'F11': KeyCode.F11,
65 'F12': KeyCode.F12,
66 'U+007F': KeyCode.DELETE,
67 'Home': KeyCode.HOME,
68 'End': KeyCode.END,
69 'PageUp': KeyCode.PAGE_UP,
70 'PageDown': KeyCode.PAGE_DOWN,
71 'Insert': KeyCode.INSERT
72 };
73
74 /** Named constructor to add an onKeyPress event listener to our handler. */
75 KeyboardEventController.keypress(EventTarget target) {
76 _KeyboardEventController(target, 'keypress');
77 }
78
79 /** Named constructor to add an onKeyUp event listener to our handler. */
80 KeyboardEventController.keyup(EventTarget target) {
81 _KeyboardEventController(target, 'keyup');
82 }
83
84 /** Named constructor to add an onKeyDown event listener to our handler. */
85 KeyboardEventController.keydown(EventTarget target) {
86 _KeyboardEventController(target, 'keydown');
87 }
88
89 /**
90 * General constructor, performs basic initialization for our improved
91 * KeyboardEvent controller.
92 */
93 _KeyboardEventController(EventTarget target, String type) {
94 _callbacks = [];
95 _type = type;
96 _target = target;
97 }
98
99 /**
100 * Hook up all event listeners under the covers so we can estimate keycodes
101 * and charcodes when they are not provided.
102 */
103 void _initializeAllEventListeners() {
104 _keyDownList = [];
105 if (_keyDownSubscription == null) {
106 _keyDownSubscription = Element.keyDownEvent.forTarget(
107 _target, useCapture: true).listen(processKeyDown);
108 _keyPressSubscription = Element.keyPressEvent.forTarget(
109 _target, useCapture: true).listen(processKeyUp);
110 _keyUpSubscription = Element.keyUpEvent.forTarget(
111 _target, useCapture: true).listen(processKeyPress);
112 }
113 }
114
115 /** Add a callback that wishes to be notified when a KeyEvent occurs. */
116 void add(void callback(KeyEvent)) {
117 if (_callbacks.length == 0) {
118 _initializeAllEventListeners();
119 }
120 _callbacks.add(callback);
121 }
122
123 /**
124 * Notify all callback listeners that a KeyEvent of the relevant type has
125 * occurred.
126 */
127 bool _dispatch(KeyEvent event) {
128 if (event.type == _type) {
129 // Make a copy of the listeners in case a callback gets removed while
130 // dispatching from the list.
131 List callbacksCopy = new List.from(_callbacks);
132 for(var callback in callbacksCopy) {
133 callback(event);
134 }
135 }
136 }
137
138 /** Remove the given callback from the listeners list. */
139 void remove(void callback(KeyEvent)) {
140 var index = _callbacks.indexOf(callback);
141 if (index != -1) {
142 _callbacks.removeAt(index);
143 }
144 if (_callbacks.length == 0) {
145 // If we have no listeners, don't bother keeping track of keypresses.
146 _keyDownSubscription.cancel();
147 _keyDownSubscription = null;
148 _keyPressSubscription.cancel();
149 _keyPressSubscription = null;
150 _keyUpSubscription.cancel();
151 _keyUpSubscription = null;
152 }
153 }
154
155 /** Determine if caps lock is one of the currently depressed keys. */
156 bool get _capsLockOn =>
157 _keyDownList.any((var element) => element.keyCode == KeyCode.CAPS_LOCK);
158
159 /**
160 * Given the previously recorded keydown key codes, see if we can determine
161 * the keycode of this keypress [event]. (Generally browsers only provide
162 * charCode information for keypress events, but with a little
163 * reverse-engineering, we can also determine the keyCode.) Returns
164 * KeyCode.UNKNOWN if the keycode could not be determined.
165 */
166 int _determineKeyCodeForKeypress(KeyboardEvent event) {
167 // Note: This function is a work in progress. We'll expand this function
168 // once we get more information about other keyboards.
169 for (var prevEvent in _keyDownList) {
170 if (prevEvent._shadowCharCode == event.charCode) {
171 return prevEvent.keyCode;
172 }
173 if ((event.shiftKey || _capsLockOn) && event.charCode >= "A".codeUnits[0]
174 && event.charCode <= "Z".codeUnits[0] && event.charCode +
175 _ROMAN_ALPHABET_OFFSET == prevEvent._shadowCharCode) {
176 return prevEvent.keyCode;
177 }
178 }
179 return KeyCode.UNKNOWN;
180 }
181
182 /**
183 * Given the charater code returned from a keyDown [event], try to ascertain
184 * and return the corresponding charCode for the character that was pressed.
185 * This information is not shown to the user, but used to help polyfill
186 * keypress events.
187 */
188 int _findCharCodeKeyDown(KeyboardEvent event) {
189 if (event.keyLocation == 3) { // Numpad keys.
190 switch (event.keyCode) {
191 case KeyCode.NUM_ZERO:
192 // Even though this function returns _charCodes_, for some cases the
193 // KeyCode == the charCode we want, in which case we use the keycode
194 // constant for readability.
195 return KeyCode.ZERO;
196 case KeyCode.NUM_ONE:
197 return KeyCode.ONE;
198 case KeyCode.NUM_TWO:
199 return KeyCode.TWO;
200 case KeyCode.NUM_THREE:
201 return KeyCode.THREE;
202 case KeyCode.NUM_FOUR:
203 return KeyCode.FOUR;
204 case KeyCode.NUM_FIVE:
205 return KeyCode.FIVE;
206 case KeyCode.NUM_SIX:
207 return KeyCode.SIX;
208 case KeyCode.NUM_SEVEN:
209 return KeyCode.SEVEN;
210 case KeyCode.NUM_EIGHT:
211 return KeyCode.EIGHT;
212 case KeyCode.NUM_NINE:
213 return KeyCode.NINE;
214 case KeyCode.NUM_MULTIPLY:
215 return 42; // Char code for *
216 case KeyCode.NUM_PLUS:
217 return 43; // +
218 case KeyCode.NUM_MINUS:
219 return 45; // -
220 case KeyCode.NUM_PERIOD:
221 return 46; // .
222 case KeyCode.NUM_DIVISION:
223 return 47; // /
224 }
225 } else if (event.keyCode >= 65 && event.keyCode <= 90) {
226 // Set the "char code" for key down as the lower case letter. Again, this
227 // will not show up for the user, but will be helpful in estimating
228 // keyCode locations and other information during the keyPress event.
229 return event.keyCode + _ROMAN_ALPHABET_OFFSET;
230 }
231 switch(event.keyCode) {
232 case KeyCode.SEMICOLON:
233 return KeyCode.FF_SEMICOLON;
234 case KeyCode.EQUALS:
235 return KeyCode.FF_EQUALS;
236 case KeyCode.COMMA:
237 return 44; // Ascii value for ,
238 case KeyCode.DASH:
239 return 45; // -
240 case KeyCode.PERIOD:
241 return 46; // .
242 case KeyCode.SLASH:
243 return 47; // /
244 case KeyCode.APOSTROPHE:
245 return 96; // `
246 case KeyCode.OPEN_SQUARE_BRACKET:
247 return 91; // [
248 case KeyCode.BACKSLASH:
249 return 92; // \
250 case KeyCode.CLOSE_SQUARE_BRACKET:
251 return 93; // ]
252 case KeyCode.SINGLE_QUOTE:
253 return 39; // '
254 }
255 return event.keyCode;
256 }
257
258 /**
259 * Returns true if the key fires a keypress event in the current browser.
260 */
261 bool _firesKeyPressEvent(KeyEvent event) {
262 if (!Device.isIE && !Device.isWebKit) {
263 return true;
264 }
265
266 if (Device.userAgent.contains('Mac') && event.altKey) {
267 return KeyCode.isCharacterKey(event.keyCode);
268 }
269
270 // Alt but not AltGr which is represented as Alt+Ctrl.
271 if (event.altKey && !event.ctrlKey) {
272 return false;
273 }
274
275 // Saves Ctrl or Alt + key for IE and WebKit, which won't fire keypress.
276 if (!event.shiftKey &&
277 (_keyDownList.last.keyCode == KeyCode.CTRL ||
278 _keyDownList.last.keyCode == KeyCode.ALT ||
279 Device.userAgent.contains('Mac') &&
280 _keyDownList.last.keyCode == KeyCode.META)) {
281 return false;
282 }
283
284 // Some keys with Ctrl/Shift do not issue keypress in WebKit.
285 if (Device.isWebKit && event.ctrlKey && event.shiftKey && (
286 event.keyCode == KeyCode.BACKSLASH ||
287 event.keyCode == KeyCode.OPEN_SQUARE_BRACKET ||
288 event.keyCode == KeyCode.CLOSE_SQUARE_BRACKET ||
289 event.keyCode == KeyCode.TILDE ||
290 event.keyCode == KeyCode.SEMICOLON || event.keyCode == KeyCode.DASH ||
291 event.keyCode == KeyCode.EQUALS || event.keyCode == KeyCode.COMMA ||
292 event.keyCode == KeyCode.PERIOD || event.keyCode == KeyCode.SLASH ||
293 event.keyCode == KeyCode.APOSTROPHE ||
294 event.keyCode == KeyCode.SINGLE_QUOTE)) {
295 return false;
296 }
297
298 switch (event.keyCode) {
299 case KeyCode.ENTER:
300 // IE9 does not fire keypress on ENTER.
301 return !Device.isIE;
302 case KeyCode.ESC:
303 return !Device.isWebKit;
304 }
305
306 return KeyCode.isCharacterKey(event.keyCode);
307 }
308
309 /**
310 * Normalize the keycodes to the IE KeyCodes (this is what Chrome, IE, and
311 * Opera all use).
312 */
313 int _normalizeKeyCodes(KeyboardEvent event) {
314 // Note: This may change once we get input about non-US keyboards.
315 if (Device.isFirefox) {
316 switch(event.keyCode) {
317 case KeyCode.FF_EQUALS:
318 return KeyCode.EQUALS;
319 case KeyCode.FF_SEMICOLON:
320 return KeyCode.SEMICOLON;
321 case KeyCode.MAC_FF_META:
322 return KeyCode.META;
323 case KeyCode.WIN_KEY_FF_LINUX:
324 return KeyCode.WIN_KEY;
325 }
326 }
327 return event.keyCode;
328 }
329
330 /** Handle keydown events. */
331 void processKeyDown(KeyboardEvent e) {
332 // Ctrl-Tab and Alt-Tab can cause the focus to be moved to another window
333 // before we've caught a key-up event. If the last-key was one of these
334 // we reset the state.
335 if (_keyDownList.length > 0 &&
336 (_keyDownList.last.keyCode == KeyCode.CTRL && !e.ctrlKey ||
337 _keyDownList.last.keyCode == KeyCode.ALT && !e.altKey ||
338 Device.userAgent.contains('Mac') &&
339 _keyDownList.last.keyCode == KeyCode.META && !e.metaKey)) {
340 _keyDownList = [];
341 }
342
343 var event = new KeyEvent(e);
344 event._shadowKeyCode = _normalizeKeyCodes(event);
345 // Technically a "keydown" event doesn't have a charCode. This is
346 // calculated nonetheless to provide us with more information in giving
347 // as much information as possible on keypress about keycode and also
348 // charCode.
349 event._shadowCharCode = _findCharCodeKeyDown(event);
350 if (_keyDownList.length > 0 && event.keyCode != _keyDownList.last.keyCode &&
351 !_firesKeyPressEvent(event)) {
352 // Some browsers have quirks not firing keypress events where all other
353 // browsers do. This makes them more consistent.
354 processKeyPress(event);
355 }
356 _keyDownList.add(event);
357 _dispatch(event);
358 }
359
360 /** Handle keypress events. */
361 void processKeyPress(KeyboardEvent event) {
362 var e = new KeyEvent(event);
363 // IE reports the character code in the keyCode field for keypress events.
364 // There are two exceptions however, Enter and Escape.
365 if (Device.isIE) {
366 if (e.keyCode == KeyCode.ENTER || e.keyCode == KeyCode.ESC) {
367 e._shadowCharCode = 0;
368 } else {
369 e._shadowCharCode = e.keyCode;
370 }
371 } else if (Device.isOpera) {
372 // Opera reports the character code in the keyCode field.
373 e._shadowCharCode = KeyCode.isCharacterKey(e.keyCode) ? e.keyCode : 0;
374 }
375 // Now we guestimate about what the keycode is that was actually
376 // pressed, given previous keydown information.
377 e._shadowKeyCode = _determineKeyCodeForKeypress(e);
378
379 // Correct the key value for certain browser-specific quirks.
380 if (e._shadowKeyIdentifier != null &&
381 _keyIdentifier.containsKey(e._shadowKeyIdentifier)) {
382 // This is needed for Safari Windows because it currently doesn't give a
383 // keyCode/which for non printable keys.
384 e._shadowKeyCode = _keyIdentifier[e._shadowKeyIdentifier];
385 }
386 e._shadowAltKey = _keyDownList.any((var element) => element.altKey);
387 _dispatch(e);
388 }
389
390 /** Handle keyup events. */
391 void processKeyUp(KeyboardEvent event) {
392 var e = new KeyEvent(event);
393 KeyboardEvent toRemove = null;
394 for (var key in _keyDownList) {
395 if (key.keyCode == e.keyCode) {
396 toRemove = key;
397 }
398 }
399 if (toRemove != null) {
400 _keyDownList =
401 _keyDownList.where((element) => element != toRemove).toList();
402 } else if (_keyDownList.length > 0) {
403 // This happens when we've reached some international keyboard case we
404 // haven't accounted for or we haven't correctly eliminated all browser
405 // inconsistencies. Filing bugs on when this is reached is welcome!
406 _keyDownList.removeLast();
407 }
408 _dispatch(e);
409 }
410 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698