// 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).