Source: input-sim.js

import { KEYS, keyBindingsForPlatform } from './keybindings';

/**
 * Enum for text direction affinity.
 *
 * @const
 * @enum {number}
 * @private
 */
var Affinity = {
  UPSTREAM: 0,
  DOWNSTREAM: 1,
  NONE: null
};

/**
 * Tests is string passed in is a single word.
 *
 * @param {string} chr
 * @returns {boolean}
 * @private
 */
function isWordChar(chr) {
  return chr && /^\w$/.test(chr);
}

/**
 * Checks if char to the left of {index} in {string}
 * is a break (non-char).
 *
 * @param {string} text
 * @param {number} index
 * @returns {boolean}
 * @private
 */
function hasLeftWordBreakAtIndex(text, index) {
  if (index === 0) {
    return true;
  } else {
    return !isWordChar(text[index - 1]) && isWordChar(text[index]);
  }
}

/**
 * Checks if char to the right of {index} in {string}
 * is a break (non-char).
 *
 * @param {string} text
 * @param {number} index
 * @returns {boolean}
 * @private
 */
function hasRightWordBreakAtIndex(text, index) {
  if (index === text.length - 1) {
    return true;
  } else {
    return isWordChar(text[index]) && !isWordChar(text[index + 1]);
  }
}

class Input {
 /**
   * Sets up the initial properties of the TextField and
   * sets  up the event listeners
   *
   * @param {string} value
   * @param {Object} range ({start: 0, length: 0})
   */
  constructor(value, range) {
    this._value = '';
    this._selectedRange = {
      start: 0,
      length: 0
    };
    this.shouldCancelEvents = true;
    this.selectionAffinity = Affinity.NONE;

    if(value) {
      this.setText(value);
    }
    if(range) {
      this.setSelectedRange(range);
    }
    this._buildKeybindings();
  }

  /**
   * Clears all characters in the existing selection.
   *
   * @example
   *     // 12|34567|8
   *     clearSelection();
   *     // 12|8
   *
   */
  clearSelection() {
    this.replaceSelection('');
  }

  /**
   * Deletes backward one character or clears a non-empty selection.
   *
   * @example
   *
   *     // |What's up, doc?
   *     deleteBackward(event);
   *     // |What's up, doc?
   *
   *     // What'|s up, doc?
   *     deleteBackward(event);
   *     // What|s up, doc?
   *
   *     // |What's| up, doc?
   *     deleteBackward(event);
   *     // | up, doc?
   */
  deleteBackward(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    if (range.length === 0) {
      range.start--;
      range.length++;
      this.setSelectedRange(range);
    }
    this.clearSelection();
  }

  /**
   * Deletes backward one word or clears a non-empty selection.
   *
   * @example
   *     // |What's up, doc?
   *     deleteWordBackward(event);
   *     // |What's up, doc?
   *
   *     // What'|s up, doc?
   *     deleteWordBackward(event);
   *     // |s up, doc?
   *
   *     // |What's| up, doc?
   *     deleteWordBackward(event);
   *     // | up, doc?
   */
  deleteWordBackward(event) {
    if (this.hasSelection()) {
      this.deleteBackward(event);
    } else {
      this._handleEvent(event);
      var range = this.selectedRange();
      var start = this._lastWordBreakBeforeIndex(range.start);
      range.length += range.start - start;
      range.start = start;
      this.setSelectedRange(range);
      this.clearSelection();
    }
  }

  /**
   * Deletes backward one character, clears a non-empty selection, or decomposes
   * an accented character to its simple form.
   *
   * @TODO Make this work as described.
   *
   * @example
   *     // |fiancée
   *     deleteBackwardByDecomposingPreviousCharacter(event);
   *     // |What's up, doc?
   *
   *     // fianc|é|e
   *     deleteBackwardByDecomposingPreviousCharacter(event);
   *     // fianc|e
   *
   *     // fiancé|e
   *     deleteBackwardByDecomposingPreviousCharacter(event);
   *     // fiance|e
   *
   */
  deleteBackwardByDecomposingPreviousCharacter(event) {
    this.deleteBackward(event);
  }

  /**
   * Deletes all characters before the cursor or clears a non-empty selection.
   *
   * @example
   *     // The quick |brown fox.
   *     deleteBackwardToBeginningOfLine(event);
   *     // |brown fox.
   *
   *     // The |quick |brown fox.
   *     deleteBackwardToBeginningOfLine(event);
   *     // The brown fox.
   *
   */
  deleteBackwardToBeginningOfLine(event) {
    if (this.hasSelection()) {
      this.deleteBackward(event);
    } else {
      this._handleEvent(event);
      var range = this.selectedRange();
      range.length = range.start;
      range.start = 0;
      this.setSelectedRange(range);
      this.clearSelection();
    }
  }

  /**
   * Deletes forward one character or clears a non-empty selection.
   *
   * @example
   *     // What's up, doc?|
   *     deleteForward(event);
   *     // What's up, doc?|
   *
   *     // What'|s up, doc?
   *     deleteForward(event);
   *     // What'| up, doc?
   *
   *     // |What's| up, doc?
   *     deleteForward(event);
   *     // | up, doc?
   *
   */
  deleteForward(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    if (range.length === 0) {
      range.length++;
      this.setSelectedRange(range);
    }
    this.clearSelection();
  }

  /**
   * Deletes forward one word or clears a non-empty selection.
   *
   * @example
   *     // What's up, doc?|
   *     deleteWordForward(event);
   *     // What's up, doc?|
   *
   *     // What's |up, doc?
   *     deleteWordForward(event);
   *     // What's |, doc?
   *
   *     // |What's| up, doc?
   *     deleteWordForward(event);
   *     // | up, doc?
   */
  deleteWordForward(event) {
    if (this.hasSelection()) {
      return this.deleteForward(event);
    } else {
      this._handleEvent(event);
      var range = this.selectedRange();
      var end = this._nextWordBreakAfterIndex(range.start + range.length);
      this.setSelectedRange({
        start: range.start,
        length: end - range.start
      });
      this.clearSelection();
    }
  }

  handleEvent(event) {
    if(typeof event === 'undefined') {
      throw new Error('cannot handle and event that isn\'t passed');
    }
    var action = this._bindings.actionForEvent(event);
    if(action) this[action](event);
    return action;
  }

  /**
   * Determines whether this field has any selection.
   *
   * @returns {boolean} true if there is at least one character selected
   */
  hasSelection() {
    return this.selectedRange().length !== 0;
  }

  /**
   * Handles the back tab key.
   *
   */
  insertBackTab() {}

  /**
   * Handles a key event could be trying to end editing.
   *
   */
  insertNewline() {}

  /**
   * Handles the tab key.
   *
   */
  insertTab() {}

  /**
   * Handles a event that is trying to insert a character.
   *
   * @param {string} text
   */
  insertText(text) {
    var range;
    if (this.hasSelection()) {
      this.clearSelection();
    }

    this.replaceSelection(text);
    range = this.selectedRange();
    range.start += range.length;
    range.length = 0;
    this.setSelectedRange(range);
  }

  /**
   * Moves the cursor up, which because this is a single-line text field, means
   * moving to the beginning of the value.
   *
   * @example
   *     // Hey guys|
   *     moveUp(event);
   *     // |Hey guys
   *
   *     // Hey |guys|
   *     moveUp(event);
   *     // |Hey guys
   *
   * @param {Event} event
   */
  moveUp(event) {
    this._handleEvent(event);
    this.setSelectedRange({
      start: 0,
      length: 0
    });
  }

  /**
   * Moves the cursor up to the beginning of the current paragraph, which because
   * this is a single-line text field, means moving to the beginning of the
   * value.
   *
   * @example
   *     // Hey guys|
   *     moveToBeginningOfParagraph(event)
   *     // |Hey guys
   *
   *     // Hey |guys|
   *     moveToBeginningOfParagraph(event)
   *     // |Hey guys
   *
   * @param {Event} event
   */
  moveToBeginningOfParagraph(event) {
    this.moveUp(event);
  }

  /**
   * Moves the cursor up, keeping the current anchor point and extending the
   * selection to the beginning as moveUp would.
   *
   * @example
   *     // rightward selections are shrunk
   *     // Hey guys, |where> are you?
   *     moveUpAndModifySelection(event);
   *     // <Hey guys, |where are you?
   *
   *     // leftward selections are extended
   *     // Hey guys, <where| are you?
   *     moveUpAndModifySelection(event);
   *     // <Hey guys, where| are you?
   *
   *     // neutral selections are extended
   *     // Hey guys, |where| are you?
   *     moveUpAndModifySelection(event);
   *     // <Hey guys, where| are you?
   *
   * @param {Event} event
   */
  moveUpAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    switch (this.selectionAffinity) {
      case Affinity.UPSTREAM:
      case Affinity.NONE:
        // 12<34 56|78  =>  <1234 56|78
        range.length += range.start;
        range.start = 0;
        break;
      case Affinity.DOWNSTREAM:
        // 12|34 56>78   =>   <12|34 5678
        range.length = range.start;
        range.start = 0;
        break;
    }
    this.setSelectedRangeWithAffinity(range, Affinity.UPSTREAM);
  }

  /**
   * Moves the free end of the selection to the beginning of the paragraph, or
   * since this is a single-line text field to the beginning of the line.
   *
   * @param {Event} event
   */
  moveParagraphBackwardAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    switch (this.selectionAffinity) {
      case Affinity.UPSTREAM:
      case Affinity.NONE:
        // 12<34 56|78  =>  <1234 56|78
        range.length += range.start;
        range.start = 0;
        break;
      case Affinity.DOWNSTREAM:
        // 12|34 56>78  =>  12|34 5678
        range.length = 0;
        break;
    }
    this.setSelectedRangeWithAffinity(range, Affinity.UPSTREAM);
  }

  /**
   * Moves the cursor to the beginning of the document.
   *
   * @param {Event} event
   */
  moveToBeginningOfDocument(event) {
    // Since we only support a single line this is just an alias.
    this.moveToBeginningOfLine(event);
  }

  /**
   * Moves the selection start to the beginning of the document.
   * @param {Event} event
   */
  moveToBeginningOfDocumentAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    range.length += range.start;
    range.start = 0;
    this.setSelectedRangeWithAffinity(range, Affinity.UPSTREAM);
  }

  /**
   * Moves the cursor down, which because this is a single-line text field, means
   * moving to the end of the value.
   *
   * @example
   *     // Hey |guys
   *     moveDown(event)
   *     // Hey guys|
   *
   *     // |Hey| guys
   *     moveDown(event)
   *     // Hey guys|
   *
   * @param {Event} event
   */
  moveDown(event) {
    this._handleEvent(event);
    // 12|34 56|78  =>  1234 5678|
    var range = {
      start: this.text().length,
      length: 0
    };
    this.setSelectedRangeWithAffinity(range, Affinity.NONE);
  }

  /**
   * Moves the cursor up to the end of the current paragraph, which because this
   * is a single-line text field, means moving to the end of the value.
   *
   * @example
   *     // |Hey guys
   *     moveToEndOfParagraph(event)
   *     // Hey guys|
   *
   *     // Hey |guys|
   *     moveToEndOfParagraph(event)
   *     // Hey guys|
   *
   * @param {Event} event
   */
  moveToEndOfParagraph(event) {
    this.moveDown(event);
  }

  /**
   * Moves the cursor down, keeping the current anchor point and extending the
   * selection to the end as moveDown would.
   *
   * @example
   *     // leftward selections are shrunk
   *     // Hey guys, <where| are you?
   *     moveDownAndModifySelection(event)
   *     // Hey guys, where| are you?>
   *
   *     // rightward selections are extended
   *     // Hey guys, |where> are you?
   *     moveDownAndModifySelection(event)
   *     // Hey guys, |where are you?>
   *
   *     // neutral selections are extended
   *     // Hey guys, |where| are you?
   *     moveDownAndModifySelection(event)
   *     // Hey guys, |where are you?>
   *
   * @param {Event} event
   */
  moveDownAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    var end = this.text().length;
    if (this.selectionAffinity === Affinity.UPSTREAM) {
      range.start += range.length;
    }
    range.length = end - range.start;
    this.setSelectedRangeWithAffinity(range, Affinity.DOWNSTREAM);
  }

  /**
   * Moves the free end of the selection to the end of the paragraph, or since
   * this is a single-line text field to the end of the line.
   *
   * @param {Event} event
   */
  moveParagraphForwardAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    switch (this.selectionAffinity) {
      case Affinity.DOWNSTREAM:
      case Affinity.NONE:
        // 12|34 56>78  =>  12|34 5678>
        range.length = this.text().length - range.start;
        break;
      case Affinity.UPSTREAM:
        // 12<34 56|78  =>  12|34 5678
        range.start += range.length;
        range.length = 0;
        break;
    }
    this.setSelectedRangeWithAffinity(range, Affinity.DOWNSTREAM);
  }

  /**
   * Moves the cursor to the end of the document.
   *
   * @param {Event} event
   */
  moveToEndOfDocument(event) {
    // Since we only support a single line this is just an alias.
    this.moveToEndOfLine(event);
  }

  /**
   * Moves the selection end to the end of the document.
   * @param {Event} event
   */
  moveToEndOfDocumentAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    range.length = this.text().length - range.start;
    this.setSelectedRangeWithAffinity(range, Affinity.DOWNSTREAM);
  }

  /**
   * Moves the cursor to the left, counting selections as a thing to move past.
   *
   * @example
   *     // no selection just moves the cursor left
   *     // Hey guys|
   *     moveLeft(event)
   *     // Hey guy|s
   *
   *     // selections are removed
   *     // Hey |guys|
   *     moveLeft(event)
   *     // Hey |guys
   *
   * @param {Event} event
   */
  moveLeft(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    if (range.length !== 0) {
      range.length = 0;
    } else {
      range.start--;
    }
    this.setSelectedRangeWithAffinity(range, Affinity.NONE);
  }

  /**
   * Moves the free end of the selection one to the left.
   *
   * @example
   *     // no selection just selects to the left
   *     // Hey guys|
   *     moveLeftAndModifySelection(event)
   *     // Hey guy<s|
   *
   *     // left selections are extended
   *     // Hey <guys|
   *     moveLeftAndModifySelection(event)
   *     // Hey< guys|
   *
   *     // right selections are shrunk
   *     // Hey |guys>
   *     moveLeftAndModifySelection(event)
   *     // Hey |guy>s
   *
   *     // neutral selections are extended
   *     // Hey |guys|
   *     moveLeftAndModifySelection(event)
   *     //Hey< guys|
   *
   * @param {Event} event
   */
  moveLeftAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    switch (this.selectionAffinity) {
      case Affinity.UPSTREAM:
      case Affinity.NONE:
        this.selectionAffinity = Affinity.UPSTREAM;
        range.start--;
        range.length++;
        break;
      case Affinity.DOWNSTREAM:
        range.length--;
        break;
    }
    this.setSelectedRange(range);
  }

  /**
   * Moves the cursor left until the start of a word is found.
   *
   * @example
   *     // no selection just moves the cursor left
   *     // Hey guys|
   *     moveWordLeft(event)
   *     // Hey |guys
   *
   *     // selections are removed
   *     // Hey |guys|
   *     moveWordLeft(event)
   *     // |Hey guys
   *
   * @param {Event} event
   */
  moveWordLeft(event) {
    this._handleEvent(event);
    var index = this._lastWordBreakBeforeIndex(this.selectedRange().start - 1);
    this.setSelectedRange({ start: index, length: 0 });
  }

  /**
   * Moves the free end of the current selection to the beginning of the previous
   * word.
   *
   * @example
   *     // no selection just selects to the left
   *     // Hey guys|
   *     moveWordLeftAndModifySelection(event)
   *     // Hey <guys|
   *
   *     // left selections are extended
   *     // Hey <guys|
   *     moveWordLeftAndModifySelection(event)
   *     // <Hey guys|
   *
   *     // right selections are shrunk
   *     // |Hey guys>
   *     moveWordLeftAndModifySelection(event)
   *     // |Hey >guys
   *
   *     // neutral selections are extended
   *     // Hey |guys|
   *     moveWordLeftAndModifySelection(event)
   *     // <Hey guys|
   *
   * @param {Event} event
   */
  moveWordLeftAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    switch (this.selectionAffinity) {
      case Affinity.UPSTREAM:
      case Affinity.NONE:
        this.selectionAffinity = Affinity.UPSTREAM;
        var start = this._lastWordBreakBeforeIndex(range.start - 1);
        range.length += range.start - start;
        range.start = start;
        break;
      case Affinity.DOWNSTREAM:
        var end = this._lastWordBreakBeforeIndex(range.start + range.length);
        if (end < range.start) {
          end = range.start;
        }
        range.length -= range.start + range.length - end;
        break;
    }
    this.setSelectedRange(range);
  }

  /**
   * Moves the cursor to the beginning of the current line.
   *
   * @example
   *     // Hey guys, where| are ya?
   *     moveToBeginningOfLine(event)
   *     // |Hey guys, where are ya?
   *
   * @param {Event} event
   */
  moveToBeginningOfLine(event) {
    this._handleEvent(event);
    this.setSelectedRange({ start: 0, length: 0 });
  }

  /**
   * Select from the free end of the selection to the beginning of line.
   *
   * @example
   *     // Hey guys, where| are ya?
   *     moveToBeginningOfLineAndModifySelection(event)
   *     // <Hey guys, where| are ya?
   *
   *     // Hey guys, where| are> ya?
   *     moveToBeginningOfLineAndModifySelection(event)
   *     // <Hey guys, where are| ya?
   *
   * @param {Event} event
   */
  moveToBeginningOfLineAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    range.length += range.start;
    range.start = 0;
    this.setSelectedRangeWithAffinity(range, Affinity.UPSTREAM);
  }

  /**
   * Moves the cursor to the right, counting selections as a thing to move past.
   *
   * @example
   *     // no selection just moves the cursor right
   *     // Hey guy|s
   *     moveRight(event)
   *     // Hey guys|
   *
   *     // selections are removed
   *     // Hey |guys|
   *     moveRight(event)
   *     // Hey guys|
   *
   * @param {Event} event
   */
  moveRight(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    if (range.length !== 0) {
      range.start += range.length;
      range.length = 0;
    } else {
      range.start++;
    }
    this.setSelectedRangeWithAffinity(range, Affinity.NONE);
  }

  /**
   * Moves the free end of the selection one to the right.
   *
   * @example
   *     // no selection just selects to the right
   *     // Hey |guys
   *     moveRightAndModifySelection(event)
   *     // Hey |g>uys
   *
   *     // right selections are extended
   *     // Hey |gu>ys
   *     moveRightAndModifySelection(event)
   *     // Hey |guy>s
   *
   *     // left selections are shrunk
   *     // <Hey |guys
   *     moveRightAndModifySelection(event)
   *     // H<ey |guys
   *
   *     // neutral selections are extended
   *     // |Hey| guys
   *     moveRightAndModifySelection(event)
   *     // |Hey >guys
   *
   * @param {Event} event
   */
  moveRightAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    switch (this.selectionAffinity) {
      case Affinity.UPSTREAM:
        range.start++;
        range.length--;
        break;
      case Affinity.DOWNSTREAM:
      case Affinity.NONE:
        this.selectionAffinity = Affinity.DOWNSTREAM;
        range.length++;
        break;
    }
    this.setSelectedRange(range);
  }

  /**
   * Moves the cursor right until the end of a word is found.
   *
   * @example
   *     // no selection just moves the cursor right
   *     // Hey| guys
   *     moveWordRight(event)
   *     // Hey guys|
   *
   *     // selections are removed
   *     // |Hey| guys
   *     moveWordRight(event)
   *     // Hey guys|
   *
   * @param {Event} event
   */
  moveWordRight(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    var index = this._nextWordBreakAfterIndex(range.start + range.length);
    this.setSelectedRange({ start: index, length: 0 });
  }

  /**
   * Moves the free end of the current selection to the next end of word.
   *
   * @example
   *     // no selection just selects to the right
   *     // Hey |guys
   *     moveWordRightAndModifySelection(event)
   *     // Hey |guys|
   *
   *     // right selections are extended
   *     // Hey |g>uys
   *     moveWordRightAndModifySelection(event)
   *     // Hey |guys>
   *
   *     // left selections are shrunk
   *     // He<y |guys
   *     moveWordRightAndModifySelection(event)
   *     // Hey< |guys
   *
   *     // neutral selections are extended
   *     // He|y |guys
   *     moveWordRightAndModifySelection(event)
   *     // He|y guys>
   *
   * @param {Event} event
   */
  moveWordRightAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    var start = range.start;
    var end = range.start + range.length;
    switch (this.selectionAffinity) {
      case Affinity.UPSTREAM:
        start = Math.min(this._nextWordBreakAfterIndex(start), end);
        break;
      case Affinity.DOWNSTREAM:
      case Affinity.NONE:
        this.selectionAffinity = Affinity.DOWNSTREAM;
        end = this._nextWordBreakAfterIndex(range.start + range.length);
        break;
    }
    this.setSelectedRange({ start: start, length: end - start });
  }

  /**
   * Moves the cursor to the end of the current line.
   *
   * @example
   *     // Hey guys, where| are ya?
   *     moveToEndOfLine(event)
   *     // |Hey guys, where are ya?
   *
   * @param {Event} event
   */
  moveToEndOfLine(event) {
    this._handleEvent(event);
    this.setSelectedRange({ start: this.text().length, length: 0 });
  }

  /**
   * Moves the free end of the selection to the end of the current line.
   *
   * @example
   *     // Hey guys, where| are ya?
   *     moveToEndOfLineAndModifySelection(event)
   *     // Hey guys, where| are ya?>
   *
   *     // Hey guys, <where| are ya?
   *     moveToEndOfLineAndModifySelection(event)
   *     // Hey guys, |where are ya?>
   *
   * @param {Event} event
   */
  moveToEndOfLineAndModifySelection(event) {
    this._handleEvent(event);
    var range = this.selectedRange();
    range.length = this.text().length - range.start;
    this.setSelectedRangeWithAffinity(range, Affinity.DOWNSTREAM);
  }

  /**
   * Replaces the characters within the selection with given text.
   *
   * @example
   *     // 12|34567|8
   *     replaceSelection('00')
   *     // 12|00|8
   *
   * @param {string} replacement
   */
  replaceSelection(replacement) {
    var range = this.selectedRange();
    var end = range.start + range.length;
    var text = this.text();
    text = text.substring(0, range.start) + replacement + text.substring(end);
    range.length = replacement.length;
    this.setText(text);
    this.setSelectedRangeWithAffinity(range, Affinity.NONE);
  }

  /**
   * Find ends of 'words' for navigational purposes.
   *
   * @example
   *     // given value of '123456789' and text of '123-45-6789'
   *     rightWordBreakIndexes()
   *     //=> [3, 5, 9]
   *
   * @returns {number[]}
   */
  rightWordBreakIndexes() {
    var result = [];
    var text = this.text();
    for (var i = 0, l = text.length; i < l; i++) {
      if (hasRightWordBreakAtIndex(text, i)) {
        result.push(i + 1);
      }
    }
    return result;
  }

  /**
   * Expands the selection to contain all the characters in the content.
   *
   * @example
   *     // 123|45678
   *     selectAll(event)
   *     // |12345678|
   *
   * @param {Event} event
   */
  selectAll(event) {
    this._handleEvent(event);
    this.setSelectedRangeWithAffinity({
      start: 0,
      length: this.text().length
    }, Affinity.NONE);
  }

  /**
   * Gets the object value. This is the value that should be considered the
   * 'real' value of the field.
   *
   * @returns {String}
   */
  text() {
    return this._value;
  }

  /**
   * Sets the object value of the field.
   *
   * @param {string} value
   */
  setText(value) {
    this._value = '' + value;
    this.setSelectedRange({
      start: this._value.length,
      length: 0
    });
  }

  /**
   * Gets the range of the current selection.
   *
   * @returns {Object} {start: number, length: number}
   */
  selectedRange() {
    return this._selectedRange;
  }

  /**
   * Sets the range of the current selection without changing the affinity.
   * @param {Object} range ({start: 0, length: 0})
   */
  setSelectedRange(range) {
    this.setSelectedRangeWithAffinity(range, this.selectionAffinity);
  }

  /**
   * Sets the range of the current selection and the selection affinity.
   *
   * @param {Object} range {start: number, length: number}
   * @param {Affinity} affinity
   * @returns {Object} {start: 0, length: 0}
   */
  setSelectedRangeWithAffinity(range, affinity) {
    var min = 0;
    var max = this.text().length;
    var caret = {
      start: Math.max(min, Math.min(max, range.start)),
      end: Math.max(min, Math.min(max, range.start + range.length))
    };
    this._selectedRange = {
      start: caret.start,
      length: caret.end - caret.start
    }
    this.selectionAffinity = range.length === 0 ? Affinity.NONE : affinity;
    return this._selectedRange;
  }

  /**
   * Gets the position of the current selection's anchor point, i.e. the point
   * that the selection extends from, if any.
   *
   * @returns {number}
   */
  selectionAnchor() {
    var range = this.selectedRange();
    switch (this.selectionAffinity) {
      case Affinity.UPSTREAM:
        return range.start + range.length;
      case Affinity.DOWNSTREAM:
        return range.start;
      default:
        return Affinity.NONE;
    }
  }




  /**
   * Builds the key bindings for platform
   *
   * @TODO: Make this better
   * @private
   */
  _buildKeybindings() {
    var osx;

    if(typeof navigator !== 'undefined') {
      osx = /^Mozilla\/[\d\.]+ \(Macintosh/.test(navigator.userAgent);
    } else if(typeof process !== 'undefined') {
      osx = /darwin/.test(process.platform);
    }
    this._bindings = keyBindingsForPlatform(osx ? 'OSX' : 'Default');
  }

  /**
   * Handles the event based on the `shouldCancelEvents` prop.
   *
   * @param {Event} event
   * @private
   */
  _handleEvent(event) {
    if(event && this.shouldCancelEvents) {
      event.preventDefault();
    }
  }

  /**
   * Finds the start of the 'word' before index.
   *
   * @param {number} index position at which to start looking
   * @returns {number} index in value less than or equal to the given index
   * @private
   */
  _lastWordBreakBeforeIndex(index) {
    var indexes = this._leftWordBreakIndexes();
    var result = indexes[0];
    for (var i = 0, l = indexes.length; i < l; i++) {
      var wordBreakIndex = indexes[i];
      if (index > wordBreakIndex) {
        result = wordBreakIndex;
      } else {
        break;
      }
    }
    return result;
  }

  /**
   * Find starts of 'words' for navigational purposes.
   *
   * @example
   *     // given value of '123456789' and text of '123-45-6789'
   *     leftWordBreakIndexes()
   *     // => [0, 3, 5]
   *
   * @returns {number[]} indexes in value of word starts.
   * @private
   */
  _leftWordBreakIndexes() {
    var result = [];
    var text = this.text();
    for (var i = 0, l = text.length; i < l; i++) {
      if (hasLeftWordBreakAtIndex(text, i)) {
        result.push(i);
      }
    }
    return result;
  }

  /**
   * Finds the end of the 'word' after index.
   *
   * @param {number} index position in value at which to start looking.
   * @returns {number}
   * @private
   */
  _nextWordBreakAfterIndex(index) {
    var indexes = this.rightWordBreakIndexes().reverse();
    var result = indexes[0];
    for (var i = 0, l = indexes.length; i < l; i++) {
      var wordBreakIndex = indexes[i];
      if (index < wordBreakIndex) {
        result = wordBreakIndex;
      } else {
        break;
      }
    }
    return result;
  }
}

export { Input, KEYS, keyBindingsForPlatform };