| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview A simple, English virtual keyboard implementation. |
| */ |
| |
| var KEY_MODE = 'key'; |
| var SHIFT_MODE = 'shift'; |
| var NUMBER_MODE = 'number'; |
| var SYMBOL_MODE = 'symbol'; |
| var MODES = [ KEY_MODE, SHIFT_MODE, NUMBER_MODE, SYMBOL_MODE ]; |
| var currentMode = KEY_MODE; |
| var MODE_TRANSITIONS = {}; |
| |
| MODE_TRANSITIONS[KEY_MODE + SHIFT_MODE] = SHIFT_MODE; |
| MODE_TRANSITIONS[KEY_MODE + NUMBER_MODE] = NUMBER_MODE; |
| MODE_TRANSITIONS[SHIFT_MODE + SHIFT_MODE] = KEY_MODE; |
| MODE_TRANSITIONS[SHIFT_MODE + NUMBER_MODE] = NUMBER_MODE; |
| MODE_TRANSITIONS[NUMBER_MODE + SHIFT_MODE] = SYMBOL_MODE; |
| MODE_TRANSITIONS[NUMBER_MODE + NUMBER_MODE] = KEY_MODE; |
| MODE_TRANSITIONS[SYMBOL_MODE + SHIFT_MODE] = NUMBER_MODE; |
| MODE_TRANSITIONS[SYMBOL_MODE + NUMBER_MODE] = KEY_MODE; |
| |
| /** |
| * Transition the mode according to the given transition. |
| * @param {string} transition The transition to take. |
| * @return {void} |
| */ |
| function transitionMode(transition) { |
| currentMode = MODE_TRANSITIONS[currentMode + transition]; |
| } |
| |
| /** |
| * Plain-old-data class to represent a character. |
| * @param {string} display The HTML to be displayed. |
| * @param {string} id The key identifier for this Character. |
| * @constructor |
| */ |
| function Character(display, id) { |
| this.display = display; |
| this.keyIdentifier = id; |
| } |
| |
| /** |
| * Convenience function to make the keyboard data more readable. |
| * @param {string} display Both the display and id for the created Character. |
| */ |
| function C(display) { |
| return new Character(display, display); |
| } |
| |
| /** |
| * An abstract base-class for all keys on the keyboard. |
| * @constructor |
| */ |
| function BaseKey() {} |
| |
| BaseKey.prototype = { |
| /** |
| * The aspect ratio of this key. |
| * @type {number} |
| */ |
| aspect_: 1, |
| |
| /** |
| * The cell type of this key. Determines the background colour. |
| * @type {string} |
| */ |
| cellType_: '', |
| |
| /** |
| * @return {number} The aspect ratio of this key. |
| */ |
| get aspect() { |
| return this.aspect_; |
| }, |
| |
| /** |
| * Set the position, a.k.a. row, of this key. |
| * @param {string} position The position. |
| * @return {void} |
| */ |
| set position(position) { |
| for (var i in this.modeElements_) { |
| this.modeElements_[i].classList.add(this.cellType_ + 'r' + position); |
| } |
| }, |
| |
| /** |
| * Returns the amount of padding for the top of the key. |
| * @param {string} mode The mode for the key. |
| * @param {number} height The height of the key. |
| * @return {number} Padding in pixels. |
| */ |
| getPadding: function(mode, height) { |
| return Math.floor(height / 3.5); |
| }, |
| |
| /** |
| * Size the DOM elements of this key. |
| * @param {string} mode The mode to be sized. |
| * @param {number} height The height of the key. |
| * @return {void} |
| */ |
| sizeElement: function(mode, height) { |
| var padding = this.getPadding(mode, height); |
| var border = 1; |
| var margin = 5; |
| var width = Math.floor(height * this.aspect_); |
| |
| var extraHeight = margin + padding + 2 * border; |
| var extraWidth = margin + 2 * border; |
| |
| this.modeElements_[mode].style.width = (width - extraWidth) + 'px'; |
| this.modeElements_[mode].style.height = (height - extraHeight) + 'px'; |
| this.modeElements_[mode].style.marginLeft = margin + 'px'; |
| this.modeElements_[mode].style.fontSize = (height / 3.5) + 'px'; |
| this.modeElements_[mode].style.paddingTop = padding + 'px'; |
| }, |
| |
| /** |
| * Resize all modes of this key based on the given height. |
| * @param {number} height The height of the key. |
| * @return {void} |
| */ |
| resize: function(height) { |
| for (var i in this.modeElements_) { |
| this.sizeElement(i, height); |
| } |
| }, |
| |
| /** |
| * Create the DOM elements for the given keyboard mode. Must be overridden. |
| * @param {string} mode The keyboard mode to create elements for. |
| * @param {number} height The height of the key. |
| * @return {Element} The top-level DOM Element for the key. |
| */ |
| makeDOM: function(mode, height) { |
| throw new Error('makeDOM not implemented in BaseKey'); |
| }, |
| }; |
| |
| /** |
| * A simple key which displays Characters. |
| * @param {Character} key The Character for KEY_MODE. |
| * @param {Character} shift The Character for SHIFT_MODE. |
| * @param {Character} num The Character for NUMBER_MODE. |
| * @param {Character} symbol The Character for SYMBOL_MODE. |
| * @constructor |
| * @extends {BaseKey} |
| */ |
| function Key(key, shift, num, symbol) { |
| this.modeElements_ = {}; |
| this.aspect_ = 1; // ratio width:height |
| this.cellType_ = ''; |
| |
| this.modes_ = {}; |
| this.modes_[KEY_MODE] = key; |
| this.modes_[SHIFT_MODE] = shift; |
| this.modes_[NUMBER_MODE] = num; |
| this.modes_[SYMBOL_MODE] = symbol; |
| } |
| |
| Key.prototype = { |
| __proto__: BaseKey.prototype, |
| |
| /** @inheritDoc */ |
| makeDOM: function(mode, height) { |
| this.modeElements_[mode] = document.createElement('div'); |
| this.modeElements_[mode].textContent = this.modes_[mode].display; |
| this.modeElements_[mode].className = 'key'; |
| |
| this.sizeElement(mode, height); |
| |
| this.modeElements_[mode].onclick = |
| sendKeyFunction(this.modes_[mode].keyIdentifier); |
| |
| return this.modeElements_[mode]; |
| } |
| }; |
| |
| /** |
| * A key which displays an SVG image. |
| * @param {number} aspect The aspect ratio of the key. |
| * @param {string} className The class that provides the image. |
| * @param {string} keyId The key identifier for the key. |
| * @constructor |
| * @extends {BaseKey} |
| */ |
| function SvgKey(aspect, className, keyId) { |
| this.modeElements_ = {}; |
| this.aspect_ = aspect; |
| this.cellType_ = 'nc'; |
| this.className_ = className; |
| this.keyId_ = keyId; |
| } |
| |
| SvgKey.prototype = { |
| __proto__: BaseKey.prototype, |
| |
| /** @inheritDoc */ |
| getPadding: function(mode, height) { return 0; }, |
| |
| /** @inheritDoc */ |
| makeDOM: function(mode, height) { |
| this.modeElements_[mode] = document.createElement('div'); |
| this.modeElements_[mode].className = 'key'; |
| |
| var img = document.createElement('div'); |
| img.className = 'image-key ' + this.className_; |
| this.modeElements_[mode].appendChild(img); |
| |
| this.modeElements_[mode].onclick = sendKeyFunction(this.keyId_); |
| |
| this.sizeElement(mode, height); |
| |
| return this.modeElements_[mode]; |
| } |
| }; |
| |
| /** |
| * A Key that remains the same through all modes. |
| * @param {number} aspect The aspect ratio of the key. |
| * @param {string} content The display text for the key. |
| * @param {string} keyId The key identifier for the key. |
| * @constructor |
| * @extends {BaseKey} |
| */ |
| function SpecialKey(aspect, content, keyId) { |
| this.modeElements_ = {}; |
| this.aspect_ = aspect; |
| this.cellType_ = 'nc'; |
| this.content_ = content; |
| this.keyId_ = keyId; |
| } |
| |
| SpecialKey.prototype = { |
| __proto__: BaseKey.prototype, |
| |
| /** @inheritDoc */ |
| makeDOM: function(mode, height) { |
| this.modeElements_[mode] = document.createElement('div'); |
| this.modeElements_[mode].textContent = this.content_; |
| this.modeElements_[mode].className = 'key'; |
| |
| this.modeElements_[mode].onclick = sendKeyFunction(this.keyId_); |
| |
| this.sizeElement(mode, height); |
| |
| return this.modeElements_[mode]; |
| } |
| }; |
| |
| /** |
| * A shift key. |
| * @param {number} aspect The aspect ratio of the key. |
| * @constructor |
| * @extends {BaseKey} |
| */ |
| function ShiftKey(aspect) { |
| this.modeElements_ = {}; |
| this.aspect_ = aspect; |
| this.cellType_ = 'nc'; |
| } |
| |
| ShiftKey.prototype = { |
| __proto__: BaseKey.prototype, |
| |
| /** @inheritDoc */ |
| getPadding: function(mode, height) { |
| if (mode == NUMBER_MODE || mode == SYMBOL_MODE) { |
| return BaseKey.prototype.getPadding.call(this, mode, height); |
| } |
| return 0; |
| }, |
| |
| /** @inheritDoc */ |
| makeDOM: function(mode, height) { |
| this.modeElements_[mode] = document.createElement('div'); |
| |
| if (mode == KEY_MODE || mode == SHIFT_MODE) { |
| var shift = document.createElement('div'); |
| shift.className = 'image-key shift'; |
| this.modeElements_[mode].appendChild(shift); |
| } else if (mode == NUMBER_MODE) { |
| this.modeElements_[mode].textContent = 'more'; |
| } else if (mode == SYMBOL_MODE) { |
| this.modeElements_[mode].textContent = '#123'; |
| } |
| |
| if (mode == SHIFT_MODE || mode == SYMBOL_MODE) { |
| this.modeElements_[mode].className = 'moddown key'; |
| } else { |
| this.modeElements_[mode].className = 'key'; |
| } |
| |
| this.sizeElement(mode, height); |
| |
| this.modeElements_[mode].onclick = function() { |
| transitionMode(SHIFT_MODE); |
| setMode(currentMode); |
| }; |
| return this.modeElements_[mode]; |
| }, |
| }; |
| |
| /** |
| * The symbol key: switches the keyboard into symbol mode. |
| * @constructor |
| * @extends {BaseKey} |
| */ |
| function SymbolKey() { |
| this.modeElements_ = {} |
| this.aspect_ = 1.3; |
| this.cellType_ = 'nc'; |
| } |
| |
| SymbolKey.prototype = { |
| __proto__: BaseKey.prototype, |
| |
| /** @inheritDoc */ |
| makeDOM: function(mode, height) { |
| this.modeElements_[mode] = document.createElement('div'); |
| |
| if (mode == KEY_MODE || mode == SHIFT_MODE) { |
| this.modeElements_[mode].textContent = '#123'; |
| } else if (mode == NUMBER_MODE || mode == SYMBOL_MODE) { |
| this.modeElements_[mode].textContent = 'abc'; |
| } |
| |
| if (mode == NUMBER_MODE || mode == SYMBOL_MODE) { |
| this.modeElements_[mode].className = 'moddown key'; |
| } else { |
| this.modeElements_[mode].className = 'key'; |
| } |
| |
| this.sizeElement(mode, height); |
| |
| this.modeElements_[mode].onclick = function() { |
| transitionMode(NUMBER_MODE); |
| setMode(currentMode); |
| }; |
| |
| return this.modeElements_[mode]; |
| } |
| }; |
| |
| /** |
| * The ".com" key. |
| * @constructor |
| * @extends {BaseKey} |
| */ |
| function DotComKey() { |
| this.modeElements_ = {} |
| this.aspect_ = 1.3; |
| this.cellType_ = 'nc'; |
| } |
| |
| DotComKey.prototype = { |
| __proto__: BaseKey.prototype, |
| |
| /** @inheritDoc */ |
| makeDOM: function(mode, height) { |
| this.modeElements_[mode] = document.createElement('div'); |
| this.modeElements_[mode].textContent = '.com'; |
| this.modeElements_[mode].className = 'key'; |
| |
| this.sizeElement(mode, height); |
| |
| this.modeElements_[mode].onclick = function() { |
| sendKey('.'); |
| sendKey('c'); |
| sendKey('o'); |
| sendKey('m'); |
| }; |
| |
| return this.modeElements_[mode]; |
| } |
| }; |
| |
| /** |
| * The key that hides the keyboard. |
| * @constructor |
| * @extends {BaseKey} |
| */ |
| function HideKeyboardKey() { |
| this.modeElements_ = {} |
| this.aspect_ = 1.3; |
| this.cellType_ = 'nc'; |
| } |
| |
| HideKeyboardKey.prototype = { |
| __proto__: BaseKey.prototype, |
| |
| /** @inheritDoc */ |
| getPadding: function(mode, height) { return 0; }, |
| |
| /** @inheritDoc */ |
| makeDOM: function(mode, height) { |
| this.modeElements_[mode] = document.createElement('div'); |
| this.modeElements_[mode].className = 'key'; |
| |
| var hide = document.createElement('div'); |
| hide.className = 'image-key hide'; |
| this.modeElements_[mode].appendChild(hide); |
| |
| this.sizeElement(mode, height); |
| |
| this.modeElements_[mode].onclick = function() { |
| // TODO(bryeung): need a way to cancel the keyboard |
| }; |
| |
| return this.modeElements_[mode]; |
| } |
| }; |
| |
| /** |
| * A container for keys. |
| * @param {number} position The position of the row (0-3). |
| * @param {Array.<BaseKey>} keys The keys in the row. |
| * @constructor |
| */ |
| function Row(position, keys) { |
| this.position_ = position; |
| this.keys_ = keys; |
| this.element_ = null; |
| this.modeElements_ = {}; |
| } |
| |
| Row.prototype = { |
| /** |
| * Get the total aspect ratio of the row. |
| * @return {number} The aspect ratio relative to a height of 1 unit. |
| */ |
| get aspect() { |
| var total = 0; |
| for (var i = 0; i < this.keys_.length; ++i) { |
| total += this.keys_[i].aspect; |
| } |
| return total; |
| }, |
| |
| /** |
| * Create the DOM elements for the row. |
| * @return {Element} The top-level DOM Element for the row. |
| */ |
| makeDOM: function(height) { |
| this.element_ = document.createElement('div'); |
| this.element_.className = 'row'; |
| for (var i = 0; i < MODES.length; ++i) { |
| var mode = MODES[i]; |
| this.modeElements_[mode] = document.createElement('div'); |
| this.modeElements_[mode].style.display = 'none'; |
| this.element_.appendChild(this.modeElements_[mode]); |
| } |
| |
| for (var j = 0; j < this.keys_.length; ++j) { |
| var key = this.keys_[j]; |
| for (var i = 0; i < MODES.length; ++i) { |
| this.modeElements_[MODES[i]].appendChild(key.makeDOM(MODES[i]), height); |
| } |
| } |
| |
| for (var i = 0; i < MODES.length; ++i) { |
| var clearingDiv = document.createElement('div'); |
| clearingDiv.style.clear = 'both'; |
| this.modeElements_[MODES[i]].appendChild(clearingDiv); |
| } |
| |
| for (var i = 0; i < this.keys_.length; ++i) { |
| this.keys_[i].position = this.position_; |
| } |
| |
| return this.element_; |
| }, |
| |
| /** |
| * Shows the given mode. |
| * @param {string} mode The mode to show. |
| * @return {void} |
| */ |
| showMode: function(mode) { |
| for (var i = 0; i < MODES.length; ++i) { |
| this.modeElements_[MODES[i]].style.display = 'none'; |
| } |
| this.modeElements_[mode].style.display = 'block'; |
| }, |
| |
| /** |
| * Resizes all keys in the row according to the global size. |
| * @param {number} height The height of the key. |
| * @return {void} |
| */ |
| resize: function(height) { |
| for (var i = 0; i < this.keys_.length; ++i) { |
| this.keys_[i].resize(height); |
| } |
| }, |
| }; |
| |
| /** |
| * All keys for the rows of the keyboard. |
| * NOTE: every row below should have an aspect of 12.6. |
| * @type {Array.<Array.<BaseKey>>} |
| */ |
| var KEYS = [ |
| [ |
| new SvgKey(1, 'tab', 'Tab'), |
| new Key(C('q'), C('Q'), C('1'), C('`')), |
| new Key(C('w'), C('W'), C('2'), C('~')), |
| new Key(C('e'), C('E'), C('3'), new Character('<', 'LessThan')), |
| new Key(C('r'), C('R'), C('4'), new Character('>', 'GreaterThan')), |
| new Key(C('t'), C('T'), C('5'), C('[')), |
| new Key(C('y'), C('Y'), C('6'), C(']')), |
| new Key(C('u'), C('U'), C('7'), C('{')), |
| new Key(C('i'), C('I'), C('8'), C('}')), |
| new Key(C('o'), C('O'), C('9'), C('\'')), |
| new Key(C('p'), C('P'), C('0'), C('|')), |
| new SvgKey(1.6, 'backspace', 'Backspace') |
| ], |
| [ |
| new SymbolKey(), |
| new Key(C('a'), C('A'), C('!'), C('+')), |
| new Key(C('s'), C('S'), C('@'), C('=')), |
| new Key(C('d'), C('D'), C('#'), C(' ')), |
| new Key(C('f'), C('F'), C('$'), C(' ')), |
| new Key(C('g'), C('G'), C('%'), C(' ')), |
| new Key(C('h'), C('H'), C('^'), C(' ')), |
| new Key(C('j'), C('J'), new Character('&', 'Ampersand'), C(' ')), |
| new Key(C('k'), C('K'), C('*'), C('#')), |
| new Key(C('l'), C('L'), C('('), C(' ')), |
| new Key(C('\''), C('\''), C(')'), C(' ')), |
| new SvgKey(1.3, 'return', 'Enter') |
| ], |
| [ |
| new ShiftKey(1.6), |
| new Key(C('z'), C('Z'), C('/'), C(' ')), |
| new Key(C('x'), C('X'), C('-'), C(' ')), |
| new Key(C('c'), C('C'), C('\''), C(' ')), |
| new Key(C('v'), C('V'), C('"'), C(' ')), |
| new Key(C('b'), C('B'), C(':'), C('.')), |
| new Key(C('n'), C('N'), C(';'), C(' ')), |
| new Key(C('m'), C('M'), C('_'), C(' ')), |
| new Key(C('!'), C('!'), C('{'), C(' ')), |
| new Key(C('?'), C('?'), C('}'), C(' ')), |
| new Key(C('/'), C('/'), C('\\'), C(' ')), |
| new ShiftKey(1) |
| ], |
| [ |
| new SvgKey(1.3, 'mic', ''), |
| new DotComKey(), |
| new SpecialKey(1.3, '@', '@'), |
| // TODO(bryeung): the spacebar needs to be a little bit more stretchy, |
| // since this row has only 7 keys (as opposed to 12), the truncation |
| // can cause it to not be wide enough. |
| new SpecialKey(4.8, ' ', 'Spacebar'), |
| new SpecialKey(1.3, ',', ','), |
| new SpecialKey(1.3, '.', '.'), |
| new HideKeyboardKey() |
| ] |
| ]; |
| |
| /** |
| * All of the rows in the keyboard. |
| * @type {Array.<Row>} |
| */ |
| var allRows = []; // Populated during start() |
| |
| /** |
| * Calculate the height of the row based on the size of the page. |
| * @return {number} The height of each row, in pixels. |
| */ |
| function getRowHeight() { |
| var x = window.innerWidth; |
| var y = window.innerHeight; |
| return (x > kKeyboardAspect * y) ? |
| (height = Math.floor(y / 4)) : |
| (height = Math.floor(x / (kKeyboardAspect * 4))); |
| } |
| |
| /** |
| * Set the keyboard mode. |
| * @param {string} mode The new mode. |
| * @return {void} |
| */ |
| function setMode(mode) { |
| for (var i = 0; i < allRows.length; ++i) { |
| allRows[i].showMode(mode); |
| } |
| } |
| |
| /** |
| * The keyboard's aspect ratio. |
| * @type {number} |
| */ |
| var kKeyboardAspect = 3.3; |
| |
| /** |
| * Send the given key to chrome, via the experimental extension API. |
| * @param {string} key The key to send. |
| * @return {void} |
| */ |
| function sendKey(key) { |
| if (!chrome.experimental) { |
| console.log(key); |
| return; |
| } |
| |
| var keyEvent = {'type': 'keydown', 'keyIdentifier': key}; |
| if (currentMode == SHIFT_MODE) |
| keyEvent['shiftKey'] = true; |
| |
| chrome.experimental.input.sendKeyboardEvent(keyEvent); |
| keyEvent['type'] = 'keyup'; |
| chrome.experimental.input.sendKeyboardEvent(keyEvent); |
| |
| // TODO(bryeung): deactivate shift after a successful keypress |
| } |
| |
| /** |
| * Create a closure for the sendKey function. |
| * @param {string} key The parameter to sendKey. |
| * @return {void} |
| */ |
| function sendKeyFunction(key) { |
| return function() { sendKey(key); } |
| } |
| |
| /** |
| * Resize the keyboard according to the new window size. |
| * @return {void} |
| */ |
| window.onresize = function() { |
| var height = getRowHeight(); |
| var newX = document.documentElement.clientWidth; |
| |
| // All rows should have the same aspect, so just use the first one |
| var totalWidth = Math.floor(height * allRows[0].aspect); |
| var leftPadding = Math.floor((newX - totalWidth) / 2); |
| document.getElementById('b').style.paddingLeft = leftPadding + 'px'; |
| |
| for (var i = 0; i < allRows.length; ++i) { |
| allRows[i].resize(height); |
| } |
| } |
| |
| /** |
| * Init the keyboard. |
| * @return {void} |
| */ |
| window.onload = function() { |
| var body = document.getElementById('b'); |
| for (var i = 0; i < KEYS.length; ++i) { |
| allRows.push(new Row(i, KEYS[i])); |
| } |
| |
| for (var i = 0; i < allRows.length; ++i) { |
| body.appendChild(allRows[i].makeDOM(getRowHeight())); |
| allRows[i].showMode(KEY_MODE); |
| } |
| |
| window.onresize(); |
| } |
| |
| // TODO(bryeung): would be nice to leave less gutter (without causing |
| // rendering issues with floated divs wrapping at some sizes). |