1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview A simple, English virtual keyboard implementation.
7 */
8
9var KEY_MODE = 'key';
10var SHIFT_MODE = 'shift';
11var NUMBER_MODE = 'number';
12var SYMBOL_MODE = 'symbol';
13var MODES = [ KEY_MODE, SHIFT_MODE, NUMBER_MODE, SYMBOL_MODE ];
14var currentMode = KEY_MODE;
15var MODE_TRANSITIONS = {};
16
17MODE_TRANSITIONS[KEY_MODE + SHIFT_MODE] = SHIFT_MODE;
18MODE_TRANSITIONS[KEY_MODE + NUMBER_MODE] = NUMBER_MODE;
19MODE_TRANSITIONS[SHIFT_MODE + SHIFT_MODE] = KEY_MODE;
20MODE_TRANSITIONS[SHIFT_MODE + NUMBER_MODE] = NUMBER_MODE;
21MODE_TRANSITIONS[NUMBER_MODE + SHIFT_MODE] = SYMBOL_MODE;
22MODE_TRANSITIONS[NUMBER_MODE + NUMBER_MODE] = KEY_MODE;
23MODE_TRANSITIONS[SYMBOL_MODE + SHIFT_MODE] = NUMBER_MODE;
24MODE_TRANSITIONS[SYMBOL_MODE + NUMBER_MODE] = KEY_MODE;
25
26/**
27 * Transition the mode according to the given transition.
28 * @param {string} transition The transition to take.
29 * @return {void}
30 */
31function transitionMode(transition) {
32  currentMode = MODE_TRANSITIONS[currentMode + transition];
33}
34
35/**
36 * Plain-old-data class to represent a character.
37 * @param {string} display The HTML to be displayed.
38 * @param {string} id The key identifier for this Character.
39 * @constructor
40 */
41function Character(display, id) {
42  this.display = display;
43  this.keyIdentifier = id;
44}
45
46/**
47 * Convenience function to make the keyboard data more readable.
48 * @param {string} display Both the display and id for the created Character.
49 */
50function C(display) {
51  return new Character(display, display);
52}
53
54/**
55 * An abstract base-class for all keys on the keyboard.
56 * @constructor
57 */
58function BaseKey() {}
59
60BaseKey.prototype = {
61  /**
62   * The aspect ratio of this key.
63   * @type {number}
64   */
65  aspect_: 1,
66
67  /**
68   * The cell type of this key.  Determines the background colour.
69   * @type {string}
70   */
71  cellType_: '',
72
73  /**
74   * @return {number} The aspect ratio of this key.
75   */
76  get aspect() {
77    return this.aspect_;
78  },
79
80  /**
81   * Set the position, a.k.a. row, of this key.
82   * @param {string} position The position.
83   * @return {void}
84   */
85  set position(position) {
86    for (var i in this.modeElements_) {
87      this.modeElements_[i].classList.add(this.cellType_ + 'r' + position);
88    }
89  },
90
91  /**
92   * Returns the amount of padding for the top of the key.
93   * @param {string} mode The mode for the key.
94   * @param {number} height The height of the key.
95   * @return {number} Padding in pixels.
96   */
97  getPadding: function(mode, height) {
98    return Math.floor(height / 3.5);
99  },
100
101  /**
102   * Size the DOM elements of this key.
103   * @param {string} mode The mode to be sized.
104   * @param {number} height The height of the key.
105   * @return {void}
106   */
107  sizeElement: function(mode, height) {
108    var padding = this.getPadding(mode, height);
109    var border = 1;
110    var margin = 5;
111    var width = Math.floor(height * this.aspect_);
112
113    var extraHeight = margin + padding + 2 * border;
114    var extraWidth = margin + 2 * border;
115
116    this.modeElements_[mode].style.width = (width - extraWidth) + 'px';
117    this.modeElements_[mode].style.height = (height - extraHeight) + 'px';
118    this.modeElements_[mode].style.marginLeft = margin + 'px';
119    this.modeElements_[mode].style.fontSize = (height / 3.5) + 'px';
120    this.modeElements_[mode].style.paddingTop = padding + 'px';
121  },
122
123  /**
124   * Resize all modes of this key based on the given height.
125   * @param {number} height The height of the key.
126   * @return {void}
127   */
128  resize: function(height) {
129    for (var i in this.modeElements_) {
130      this.sizeElement(i, height);
131    }
132  },
133
134  /**
135   * Create the DOM elements for the given keyboard mode.  Must be overridden.
136   * @param {string} mode The keyboard mode to create elements for.
137   * @param {number} height The height of the key.
138   * @return {Element} The top-level DOM Element for the key.
139   */
140  makeDOM: function(mode, height) {
141    throw new Error('makeDOM not implemented in BaseKey');
142  },
143};
144
145/**
146 * A simple key which displays Characters.
147 * @param {Character} key The Character for KEY_MODE.
148 * @param {Character} shift The Character for SHIFT_MODE.
149 * @param {Character} num The Character for NUMBER_MODE.
150 * @param {Character} symbol The Character for SYMBOL_MODE.
151 * @constructor
152 * @extends {BaseKey}
153 */
154function Key(key, shift, num, symbol) {
155  this.modeElements_ = {};
156  this.aspect_ = 1;  // ratio width:height
157  this.cellType_ = '';
158
159  this.modes_ = {};
160  this.modes_[KEY_MODE] = key;
161  this.modes_[SHIFT_MODE] = shift;
162  this.modes_[NUMBER_MODE] = num;
163  this.modes_[SYMBOL_MODE] = symbol;
164}
165
166Key.prototype = {
167  __proto__: BaseKey.prototype,
168
169  /** @inheritDoc */
170  makeDOM: function(mode, height) {
171    this.modeElements_[mode] = document.createElement('div');
172    this.modeElements_[mode].textContent = this.modes_[mode].display;
173    this.modeElements_[mode].className = 'key';
174
175    this.sizeElement(mode, height);
176
177    this.modeElements_[mode].onclick =
178        sendKeyFunction(this.modes_[mode].keyIdentifier);
179
180    return this.modeElements_[mode];
181  }
182};
183
184/**
185 * A key which displays an SVG image.
186 * @param {number} aspect The aspect ratio of the key.
187 * @param {string} className The class that provides the image.
188 * @param {string} keyId The key identifier for the key.
189 * @constructor
190 * @extends {BaseKey}
191 */
192function SvgKey(aspect, className, keyId) {
193  this.modeElements_ = {};
194  this.aspect_ = aspect;
195  this.cellType_ = 'nc';
196  this.className_ = className;
197  this.keyId_ = keyId;
198}
199
200SvgKey.prototype = {
201  __proto__: BaseKey.prototype,
202
203  /** @inheritDoc */
204  getPadding: function(mode, height) { return 0; },
205
206  /** @inheritDoc */
207  makeDOM: function(mode, height) {
208    this.modeElements_[mode] = document.createElement('div');
209    this.modeElements_[mode].className = 'key';
210
211    var img = document.createElement('div');
212    img.className = 'image-key ' + this.className_;
213    this.modeElements_[mode].appendChild(img);
214
215    this.modeElements_[mode].onclick = sendKeyFunction(this.keyId_);
216
217    this.sizeElement(mode, height);
218
219    return this.modeElements_[mode];
220  }
221};
222
223/**
224 * A Key that remains the same through all modes.
225 * @param {number} aspect The aspect ratio of the key.
226 * @param {string} content The display text for the key.
227 * @param {string} keyId The key identifier for the key.
228 * @constructor
229 * @extends {BaseKey}
230 */
231function SpecialKey(aspect, content, keyId) {
232  this.modeElements_ = {};
233  this.aspect_ = aspect;
234  this.cellType_ = 'nc';
235  this.content_ = content;
236  this.keyId_ = keyId;
237}
238
239SpecialKey.prototype = {
240  __proto__: BaseKey.prototype,
241
242  /** @inheritDoc */
243  makeDOM: function(mode, height) {
244    this.modeElements_[mode] = document.createElement('div');
245    this.modeElements_[mode].textContent = this.content_;
246    this.modeElements_[mode].className = 'key';
247
248    this.modeElements_[mode].onclick = sendKeyFunction(this.keyId_);
249
250    this.sizeElement(mode, height);
251
252    return this.modeElements_[mode];
253  }
254};
255
256/**
257 * A shift key.
258 * @param {number} aspect The aspect ratio of the key.
259 * @constructor
260 * @extends {BaseKey}
261 */
262function ShiftKey(aspect) {
263  this.modeElements_ = {};
264  this.aspect_ = aspect;
265  this.cellType_ = 'nc';
266}
267
268ShiftKey.prototype = {
269  __proto__: BaseKey.prototype,
270
271  /** @inheritDoc */
272  getPadding: function(mode, height) {
273    if (mode == NUMBER_MODE || mode == SYMBOL_MODE) {
274      return BaseKey.prototype.getPadding.call(this, mode, height);
275    }
276    return 0;
277  },
278
279  /** @inheritDoc */
280  makeDOM: function(mode, height) {
281    this.modeElements_[mode] = document.createElement('div');
282
283    if (mode == KEY_MODE || mode == SHIFT_MODE) {
284      var shift = document.createElement('div');
285      shift.className = 'image-key shift';
286      this.modeElements_[mode].appendChild(shift);
287    } else if (mode == NUMBER_MODE) {
288      this.modeElements_[mode].textContent = 'more';
289    } else if (mode == SYMBOL_MODE) {
290      this.modeElements_[mode].textContent = '#123';
291    }
292
293    if (mode == SHIFT_MODE || mode == SYMBOL_MODE) {
294      this.modeElements_[mode].className = 'moddown key';
295    } else {
296      this.modeElements_[mode].className = 'key';
297    }
298
299    this.sizeElement(mode, height);
300
301    this.modeElements_[mode].onclick = function() {
302      transitionMode(SHIFT_MODE);
303      setMode(currentMode);
304    };
305    return this.modeElements_[mode];
306  },
307};
308
309/**
310 * The symbol key: switches the keyboard into symbol mode.
311 * @constructor
312 * @extends {BaseKey}
313 */
314function SymbolKey() {
315  this.modeElements_ = {}
316  this.aspect_ = 1.3;
317  this.cellType_ = 'nc';
318}
319
320SymbolKey.prototype = {
321  __proto__: BaseKey.prototype,
322
323  /** @inheritDoc */
324  makeDOM: function(mode, height) {
325    this.modeElements_[mode] = document.createElement('div');
326
327    if (mode == KEY_MODE || mode == SHIFT_MODE) {
328      this.modeElements_[mode].textContent = '#123';
329    } else if (mode == NUMBER_MODE || mode == SYMBOL_MODE) {
330      this.modeElements_[mode].textContent = 'abc';
331    }
332
333    if (mode == NUMBER_MODE || mode == SYMBOL_MODE) {
334      this.modeElements_[mode].className = 'moddown key';
335    } else {
336      this.modeElements_[mode].className = 'key';
337    }
338
339    this.sizeElement(mode, height);
340
341    this.modeElements_[mode].onclick = function() {
342      transitionMode(NUMBER_MODE);
343      setMode(currentMode);
344    };
345
346    return this.modeElements_[mode];
347  }
348};
349
350/**
351 * The ".com" key.
352 * @constructor
353 * @extends {BaseKey}
354 */
355function DotComKey() {
356  this.modeElements_ = {}
357  this.aspect_ = 1.3;
358  this.cellType_ = 'nc';
359}
360
361DotComKey.prototype = {
362  __proto__: BaseKey.prototype,
363
364  /** @inheritDoc */
365  makeDOM: function(mode, height) {
366    this.modeElements_[mode] = document.createElement('div');
367    this.modeElements_[mode].textContent = '.com';
368    this.modeElements_[mode].className = 'key';
369
370    this.sizeElement(mode, height);
371
372    this.modeElements_[mode].onclick = function() {
373      sendKey('.');
374      sendKey('c');
375      sendKey('o');
376      sendKey('m');
377    };
378
379    return this.modeElements_[mode];
380  }
381};
382
383/**
384 * The key that hides the keyboard.
385 * @constructor
386 * @extends {BaseKey}
387 */
388function HideKeyboardKey() {
389  this.modeElements_ = {}
390  this.aspect_ = 1.3;
391  this.cellType_ = 'nc';
392}
393
394HideKeyboardKey.prototype = {
395  __proto__: BaseKey.prototype,
396
397  /** @inheritDoc */
398  getPadding: function(mode, height) { return 0; },
399
400  /** @inheritDoc */
401  makeDOM: function(mode, height) {
402    this.modeElements_[mode] = document.createElement('div');
403    this.modeElements_[mode].className = 'key';
404
405    var hide = document.createElement('div');
406    hide.className = 'image-key hide';
407    this.modeElements_[mode].appendChild(hide);
408
409    this.sizeElement(mode, height);
410
411    this.modeElements_[mode].onclick = function() {
412      // TODO(bryeung): need a way to cancel the keyboard
413    };
414
415    return this.modeElements_[mode];
416  }
417};
418
419/**
420 * A container for keys.
421 * @param {number} position The position of the row (0-3).
422 * @param {Array.<BaseKey>} keys The keys in the row.
423 * @constructor
424 */
425function Row(position, keys) {
426  this.position_ = position;
427  this.keys_ = keys;
428  this.element_ = null;
429  this.modeElements_ = {};
430}
431
432Row.prototype = {
433  /**
434   * Get the total aspect ratio of the row.
435   * @return {number} The aspect ratio relative to a height of 1 unit.
436   */
437  get aspect() {
438    var total = 0;
439    for (var i = 0; i < this.keys_.length; ++i) {
440      total += this.keys_[i].aspect;
441    }
442    return total;
443  },
444
445  /**
446   * Create the DOM elements for the row.
447   * @return {Element} The top-level DOM Element for the row.
448   */
449  makeDOM: function(height) {
450    this.element_ = document.createElement('div');
451    this.element_.className = 'row';
452    for (var i = 0; i < MODES.length; ++i) {
453      var mode = MODES[i];
454      this.modeElements_[mode] = document.createElement('div');
455      this.modeElements_[mode].style.display = 'none';
456      this.element_.appendChild(this.modeElements_[mode]);
457    }
458
459    for (var j = 0; j < this.keys_.length; ++j) {
460      var key = this.keys_[j];
461      for (var i = 0; i < MODES.length; ++i) {
462        this.modeElements_[MODES[i]].appendChild(key.makeDOM(MODES[i]), height);
463      }
464    }
465
466    for (var i = 0; i < MODES.length; ++i) {
467      var clearingDiv = document.createElement('div');
468      clearingDiv.style.clear = 'both';
469      this.modeElements_[MODES[i]].appendChild(clearingDiv);
470    }
471
472    for (var i = 0; i < this.keys_.length; ++i) {
473      this.keys_[i].position = this.position_;
474    }
475
476    return this.element_;
477  },
478
479  /**
480   * Shows the given mode.
481   * @param {string} mode The mode to show.
482   * @return {void}
483   */
484  showMode: function(mode) {
485    for (var i = 0; i < MODES.length; ++i) {
486      this.modeElements_[MODES[i]].style.display = 'none';
487    }
488    this.modeElements_[mode].style.display = 'block';
489  },
490
491  /**
492   * Resizes all keys in the row according to the global size.
493   * @param {number} height The height of the key.
494   * @return {void}
495   */
496  resize: function(height) {
497    for (var i = 0; i < this.keys_.length; ++i) {
498      this.keys_[i].resize(height);
499    }
500  },
501};
502
503/**
504 * All keys for the rows of the keyboard.
505 * NOTE: every row below should have an aspect of 12.6.
506 * @type {Array.<Array.<BaseKey>>}
507 */
508var KEYS = [
509  [
510    new SvgKey(1, 'tab', 'Tab'),
511    new Key(C('q'), C('Q'), C('1'), C('`')),
512    new Key(C('w'), C('W'), C('2'), C('~')),
513    new Key(C('e'), C('E'), C('3'), new Character('<', 'LessThan')),
514    new Key(C('r'), C('R'), C('4'), new Character('>', 'GreaterThan')),
515    new Key(C('t'), C('T'), C('5'), C('[')),
516    new Key(C('y'), C('Y'), C('6'), C(']')),
517    new Key(C('u'), C('U'), C('7'), C('{')),
518    new Key(C('i'), C('I'), C('8'), C('}')),
519    new Key(C('o'), C('O'), C('9'), C('\'')),
520    new Key(C('p'), C('P'), C('0'), C('|')),
521    new SvgKey(1.6, 'backspace', 'Backspace')
522  ],
523  [
524    new SymbolKey(),
525    new Key(C('a'), C('A'), C('!'), C('+')),
526    new Key(C('s'), C('S'), C('@'), C('=')),
527    new Key(C('d'), C('D'), C('#'), C(' ')),
528    new Key(C('f'), C('F'), C('$'), C(' ')),
529    new Key(C('g'), C('G'), C('%'), C(' ')),
530    new Key(C('h'), C('H'), C('^'), C(' ')),
531    new Key(C('j'), C('J'), new Character('&', 'Ampersand'), C(' ')),
532    new Key(C('k'), C('K'), C('*'), C('#')),
533    new Key(C('l'), C('L'), C('('), C(' ')),
534    new Key(C('\''), C('\''), C(')'), C(' ')),
535    new SvgKey(1.3, 'return', 'Enter')
536  ],
537  [
538    new ShiftKey(1.6),
539    new Key(C('z'), C('Z'), C('/'), C(' ')),
540    new Key(C('x'), C('X'), C('-'), C(' ')),
541    new Key(C('c'), C('C'), C('\''), C(' ')),
542    new Key(C('v'), C('V'), C('"'), C(' ')),
543    new Key(C('b'), C('B'), C(':'), C('.')),
544    new Key(C('n'), C('N'), C(';'), C(' ')),
545    new Key(C('m'), C('M'), C('_'), C(' ')),
546    new Key(C('!'), C('!'), C('{'), C(' ')),
547    new Key(C('?'), C('?'), C('}'), C(' ')),
548    new Key(C('/'), C('/'), C('\\'), C(' ')),
549    new ShiftKey(1)
550  ],
551  [
552    new SvgKey(1.3, 'mic', ''),
553    new DotComKey(),
554    new SpecialKey(1.3, '@', '@'),
555    // TODO(bryeung): the spacebar needs to be a little bit more stretchy,
556    // since this row has only 7 keys (as opposed to 12), the truncation
557    // can cause it to not be wide enough.
558    new SpecialKey(4.8, ' ', 'Spacebar'),
559    new SpecialKey(1.3, ',', ','),
560    new SpecialKey(1.3, '.', '.'),
561    new HideKeyboardKey()
562  ]
563];
564
565/**
566 * All of the rows in the keyboard.
567 * @type {Array.<Row>}
568 */
569var allRows = [];  // Populated during start()
570
571/**
572 * Calculate the height of the row based on the size of the page.
573 * @return {number} The height of each row, in pixels.
574 */
575function getRowHeight() {
576  var x = window.innerWidth;
577  var y = window.innerHeight;
578  return (x > kKeyboardAspect * y) ?
579      (height = Math.floor(y / 4)) :
580      (height = Math.floor(x / (kKeyboardAspect * 4)));
581}
582
583/**
584 * Set the keyboard mode.
585 * @param {string} mode The new mode.
586 * @return {void}
587 */
588function setMode(mode) {
589  for (var i = 0; i < allRows.length; ++i) {
590    allRows[i].showMode(mode);
591  }
592}
593
594/**
595 * The keyboard's aspect ratio.
596 * @type {number}
597 */
598var kKeyboardAspect = 3.3;
599
600/**
601 * Send the given key to chrome, via the experimental extension API.
602 * @param {string} key The key to send.
603 * @return {void}
604 */
605function sendKey(key) {
606  if (!chrome.experimental) {
607    console.log(key);
608    return;
609  }
610
611  var keyEvent = {'type': 'keydown', 'keyIdentifier': key};
612  if (currentMode == SHIFT_MODE)
613    keyEvent['shiftKey'] = true;
614
615  chrome.experimental.input.sendKeyboardEvent(keyEvent);
616  keyEvent['type'] = 'keyup';
617  chrome.experimental.input.sendKeyboardEvent(keyEvent);
618
619  // TODO(bryeung): deactivate shift after a successful keypress
620}
621
622/**
623 * Create a closure for the sendKey function.
624 * @param {string} key The parameter to sendKey.
625 * @return {void}
626 */
627function sendKeyFunction(key) {
628  return function() { sendKey(key); }
629}
630
631/**
632 * Resize the keyboard according to the new window size.
633 * @return {void}
634 */
635window.onresize = function() {
636  var height = getRowHeight();
637  var newX = document.documentElement.clientWidth;
638
639  // All rows should have the same aspect, so just use the first one
640  var totalWidth = Math.floor(height * allRows[0].aspect);
641  var leftPadding = Math.floor((newX - totalWidth) / 2);
642  document.getElementById('b').style.paddingLeft = leftPadding + 'px';
643
644  for (var i = 0; i < allRows.length; ++i) {
645    allRows[i].resize(height);
646  }
647}
648
649/**
650 * Init the keyboard.
651 * @return {void}
652 */
653window.onload = function() {
654  var body = document.getElementById('b');
655  for (var i = 0; i < KEYS.length; ++i) {
656    allRows.push(new Row(i, KEYS[i]));
657  }
658
659  for (var i = 0; i < allRows.length; ++i) {
660    body.appendChild(allRows[i].makeDOM(getRowHeight()));
661    allRows[i].showMode(KEY_MODE);
662  }
663
664  window.onresize();
665}
666
667// TODO(bryeung): would be nice to leave less gutter (without causing
668// rendering issues with floated divs wrapping at some sizes).
669