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 9 var KEY_MODE = 'key'; 10 var SHIFT_MODE = 'shift'; 11 var NUMBER_MODE = 'number'; 12 var SYMBOL_MODE = 'symbol'; 13 var MODES = [ KEY_MODE, SHIFT_MODE, NUMBER_MODE, SYMBOL_MODE ]; 14 var currentMode = KEY_MODE; 15 var MODE_TRANSITIONS = {}; 16 17 MODE_TRANSITIONS[KEY_MODE + SHIFT_MODE] = SHIFT_MODE; 18 MODE_TRANSITIONS[KEY_MODE + NUMBER_MODE] = NUMBER_MODE; 19 MODE_TRANSITIONS[SHIFT_MODE + SHIFT_MODE] = KEY_MODE; 20 MODE_TRANSITIONS[SHIFT_MODE + NUMBER_MODE] = NUMBER_MODE; 21 MODE_TRANSITIONS[NUMBER_MODE + SHIFT_MODE] = SYMBOL_MODE; 22 MODE_TRANSITIONS[NUMBER_MODE + NUMBER_MODE] = KEY_MODE; 23 MODE_TRANSITIONS[SYMBOL_MODE + SHIFT_MODE] = NUMBER_MODE; 24 MODE_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 */ 31 function 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 */ 41 function 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 */ 50 function 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 */ 58 function BaseKey() {} 59 60 BaseKey.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 */ 154 function 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 166 Key.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 */ 192 function 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 200 SvgKey.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 */ 231 function 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 239 SpecialKey.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 */ 262 function ShiftKey(aspect) { 263 this.modeElements_ = {}; 264 this.aspect_ = aspect; 265 this.cellType_ = 'nc'; 266 } 267 268 ShiftKey.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 */ 314 function SymbolKey() { 315 this.modeElements_ = {} 316 this.aspect_ = 1.3; 317 this.cellType_ = 'nc'; 318 } 319 320 SymbolKey.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 */ 355 function DotComKey() { 356 this.modeElements_ = {} 357 this.aspect_ = 1.3; 358 this.cellType_ = 'nc'; 359 } 360 361 DotComKey.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 */ 388 function HideKeyboardKey() { 389 this.modeElements_ = {} 390 this.aspect_ = 1.3; 391 this.cellType_ = 'nc'; 392 } 393 394 HideKeyboardKey.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 */ 425 function Row(position, keys) { 426 this.position_ = position; 427 this.keys_ = keys; 428 this.element_ = null; 429 this.modeElements_ = {}; 430 } 431 432 Row.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 */ 508 var 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 */ 569 var 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 */ 575 function 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 */ 588 function 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 */ 598 var 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 */ 605 function 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 */ 627 function sendKeyFunction(key) { 628 return function() { sendKey(key); } 629 } 630 631 /** 632 * Resize the keyboard according to the new window size. 633 * @return {void} 634 */ 635 window.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 */ 653 window.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