Javascript  |  669行  |  17.53 KB

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