/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview This file contains a class for working with keyboard events
 * that repeat consistently across browsers and platforms. It also unifies the
 * key code so that it is the same in all browsers and platforms.
 *
 * Different web browsers have very different keyboard event handling. Most
 * importantly is that only certain browsers repeat keydown events:
 * IE, Opera, FF/Win32, and Safari 3 repeat keydown events.
 * FF/Mac and Safari 2 do not.
 *
 * For the purposes of this code, "Safari 3" means WebKit 525+, when WebKit
 * decided that they should try to match IE's key handling behavior.
 * Safari 3.0.4, which shipped with Leopard (WebKit 523), has the
 * Safari 2 behavior.
 *
 * Firefox, Safari, Opera prevent on keypress
 *
 * IE prevents on keydown
 *
 * Firefox does not fire keypress for shift, ctrl, alt
 * Firefox does fire keydown for shift, ctrl, alt, meta
 * Firefox does not repeat keydown for shift, ctrl, alt, meta
 *
 * Firefox does not fire keypress for up and down in an input
 *
 * Opera fires keypress for shift, ctrl, alt, meta
 * Opera does not repeat keypress for shift, ctrl, alt, meta
 *
 * Safari 2 and 3 do not fire keypress for shift, ctrl, alt
 * Safari 2 does not fire keydown for shift, ctrl, alt
 * Safari 3 *does* fire keydown for shift, ctrl, alt
 *
 * IE provides the keycode for keyup/down events and the charcode (in the
 * keycode field) for keypress.
 *
 * Mozilla provides the keycode for keyup/down and the charcode for keypress
 * unless it's a non text modifying key in which case the keycode is provided.
 *
 * Safari 3 provides the keycode and charcode for all events.
 *
 * Opera provides the keycode for keyup/down event and either the charcode or
 * the keycode (in the keycode field) for keypress events.
 *
 * Firefox x11 doesn't fire keydown events if a another key is already held down
 * until the first key is released. This can cause a key event to be fired with
 * a keyCode for the first key and a charCode for the second key.
 *
 * Safari in keypress
 *
 *        charCode keyCode which
 * ENTER:       13      13    13
 * F1:       63236   63236 63236
 * F8:       63243   63243 63243
 * ...
 * p:          112     112   112
 * P:           80      80    80
 *
 * Firefox, keypress:
 *
 *        charCode keyCode which
 * ENTER:        0      13    13
 * F1:           0     112     0
 * F8:           0     119     0
 * ...
 * p:          112       0   112
 * P:           80       0    80
 *
 * Opera, Mac+Win32, keypress:
 *
 *         charCode keyCode which
 * ENTER: undefined      13    13
 * F1:    undefined     112     0
 * F8:    undefined     119     0
 * ...
 * p:     undefined     112   112
 * P:     undefined      80    80
 *
 * IE7, keydown
 *
 *         charCode keyCode     which
 * ENTER: undefined      13 undefined
 * F1:    undefined     112 undefined
 * F8:    undefined     119 undefined
 * ...
 * p:     undefined      80 undefined
 * P:     undefined      80 undefined
 *
 * @see ../demos/keyhandler.html
 */


goog.provide('goog.events.KeyHandler');
goog.provide('goog.events.KeyHandler.EventType');

goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.KeyEvent');
goog.require('goog.userAgent');



/**
 * A wrapper around an element that you want to listen to keyboard events on.
 * @param {Element|Document=} opt_element The element or document to listen on.
 * @param {boolean=} opt_capture Whether to listen for browser events in
 *     capture phase (defaults to false).
 * @constructor
 * @extends {goog.events.EventTarget}
 * @final
 */
goog.events.KeyHandler = function(opt_element, opt_capture) {
  'use strict';
  goog.events.EventTarget.call(this);

  if (opt_element) {
    this.attach(opt_element, opt_capture);
  }
};
goog.inherits(goog.events.KeyHandler, goog.events.EventTarget);


/**
 * This is the element that we will listen to the real keyboard events on.
 * @type {?Element|?Document|null}
 * @private
 */
goog.events.KeyHandler.prototype.element_ = null;


/**
 * The key for the key press listener.
 * @type {?goog.events.Key}
 * @private
 */
goog.events.KeyHandler.prototype.keyPressKey_ = null;


/**
 * The key for the key down listener.
 * @type {?goog.events.Key}
 * @private
 */
goog.events.KeyHandler.prototype.keyDownKey_ = null;


/**
 * The key for the key up listener.
 * @type {?goog.events.Key}
 * @private
 */
goog.events.KeyHandler.prototype.keyUpKey_ = null;


/**
 * Used to detect keyboard repeat events.
 * @private
 * @type {number}
 */
goog.events.KeyHandler.prototype.lastKey_ = -1;


/**
 * Keycode recorded for key down events. As most browsers don't report the
 * keycode in the key press event we need to record it in the key down phase.
 * @private
 * @type {number}
 */
goog.events.KeyHandler.prototype.keyCode_ = -1;


/**
 * Alt key recorded for key down events. FF on Mac does not report the alt key
 * flag in the key press event, we need to record it in the key down phase.
 * @type {boolean}
 * @private
 */
goog.events.KeyHandler.prototype.altKey_ = false;


/**
 * Enum type for the events fired by the key handler
 * @const
 * @deprecated use `goog.events.KeyEvent.EventType` instead.
 */
goog.events.KeyHandler.EventType = goog.events.KeyEvent.EventType;


/**
 * An enumeration of key codes that Safari 2 does incorrectly
 * @type {Object}
 * @private
 */
goog.events.KeyHandler.safariKey_ = {
  '3': goog.events.KeyCodes.ENTER,             // 13
  '12': goog.events.KeyCodes.NUMLOCK,          // 144
  '63232': goog.events.KeyCodes.UP,            // 38
  '63233': goog.events.KeyCodes.DOWN,          // 40
  '63234': goog.events.KeyCodes.LEFT,          // 37
  '63235': goog.events.KeyCodes.RIGHT,         // 39
  '63236': goog.events.KeyCodes.F1,            // 112
  '63237': goog.events.KeyCodes.F2,            // 113
  '63238': goog.events.KeyCodes.F3,            // 114
  '63239': goog.events.KeyCodes.F4,            // 115
  '63240': goog.events.KeyCodes.F5,            // 116
  '63241': goog.events.KeyCodes.F6,            // 117
  '63242': goog.events.KeyCodes.F7,            // 118
  '63243': goog.events.KeyCodes.F8,            // 119
  '63244': goog.events.KeyCodes.F9,            // 120
  '63245': goog.events.KeyCodes.F10,           // 121
  '63246': goog.events.KeyCodes.F11,           // 122
  '63247': goog.events.KeyCodes.F12,           // 123
  '63248': goog.events.KeyCodes.PRINT_SCREEN,  // 44
  '63272': goog.events.KeyCodes.DELETE,        // 46
  '63273': goog.events.KeyCodes.HOME,          // 36
  '63275': goog.events.KeyCodes.END,           // 35
  '63276': goog.events.KeyCodes.PAGE_UP,       // 33
  '63277': goog.events.KeyCodes.PAGE_DOWN,     // 34
  '63289': goog.events.KeyCodes.NUMLOCK,       // 144
  '63302': goog.events.KeyCodes.INSERT         // 45
};


/**
 * An enumeration of key identifiers currently part of the W3C draft for DOM3
 * and their mappings to keyCodes.
 * http://www.w3.org/TR/DOM-Level-3-Events/keyset.html#KeySet-Set
 * This is currently supported in Safari and should be platform independent.
 * @type {Object}
 * @private
 */
goog.events.KeyHandler.keyIdentifier_ = {
  'Up': goog.events.KeyCodes.UP,               // 38
  'Down': goog.events.KeyCodes.DOWN,           // 40
  'Left': goog.events.KeyCodes.LEFT,           // 37
  'Right': goog.events.KeyCodes.RIGHT,         // 39
  'Enter': goog.events.KeyCodes.ENTER,         // 13
  'F1': goog.events.KeyCodes.F1,               // 112
  'F2': goog.events.KeyCodes.F2,               // 113
  'F3': goog.events.KeyCodes.F3,               // 114
  'F4': goog.events.KeyCodes.F4,               // 115
  'F5': goog.events.KeyCodes.F5,               // 116
  'F6': goog.events.KeyCodes.F6,               // 117
  'F7': goog.events.KeyCodes.F7,               // 118
  'F8': goog.events.KeyCodes.F8,               // 119
  'F9': goog.events.KeyCodes.F9,               // 120
  'F10': goog.events.KeyCodes.F10,             // 121
  'F11': goog.events.KeyCodes.F11,             // 122
  'F12': goog.events.KeyCodes.F12,             // 123
  'U+007F': goog.events.KeyCodes.DELETE,       // 46
  'Home': goog.events.KeyCodes.HOME,           // 36
  'End': goog.events.KeyCodes.END,             // 35
  'PageUp': goog.events.KeyCodes.PAGE_UP,      // 33
  'PageDown': goog.events.KeyCodes.PAGE_DOWN,  // 34
  'Insert': goog.events.KeyCodes.INSERT        // 45
};




/**
 * If true, the alt key flag is saved during the key down and reused when
 * handling the key press. FF on Mac does not set the alt flag in the key press
 * event.
 * @type {boolean}
 * @private
 */
goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_ =
    goog.userAgent.MAC && goog.userAgent.GECKO;


/**
 * Records the keycode for browsers that only returns the keycode for key up/
 * down events. For browser/key combinations that doesn't trigger a key pressed
 * event it also fires the patched key event.
 * @param {goog.events.BrowserEvent} e The key down event.
 * @private
 */
goog.events.KeyHandler.prototype.handleKeyDown_ = function(e) {
  'use strict';
  // Ctrl-Tab and Alt-Tab can cause the focus to be moved to another window
  // before we've caught a key-up event.  If the last-key was one of these we
  // reset the state.
  if (goog.userAgent.WEBKIT || goog.userAgent.EDGE) {
    if (this.lastKey_ == goog.events.KeyCodes.CTRL && !e.ctrlKey ||
        this.lastKey_ == goog.events.KeyCodes.ALT && !e.altKey ||
        goog.userAgent.MAC && this.lastKey_ == goog.events.KeyCodes.META &&
            !e.metaKey) {
      this.resetState();
    }
  }

  if (this.lastKey_ == -1) {
    if (e.ctrlKey && e.keyCode != goog.events.KeyCodes.CTRL) {
      this.lastKey_ = goog.events.KeyCodes.CTRL;
    } else if (e.altKey && e.keyCode != goog.events.KeyCodes.ALT) {
      this.lastKey_ = goog.events.KeyCodes.ALT;
    } else if (e.metaKey && e.keyCode != goog.events.KeyCodes.META) {
      this.lastKey_ = goog.events.KeyCodes.META;
    }
  }

  if (!goog.events.KeyCodes.firesKeyPressEvent(
          e.keyCode, this.lastKey_, e.shiftKey, e.ctrlKey, e.altKey,
          e.metaKey)) {
    this.handleEvent(e);
  } else {
    this.keyCode_ = goog.events.KeyCodes.normalizeKeyCode(e.keyCode);
    if (goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_) {
      this.altKey_ = e.altKey;
    }
  }
};


/**
 * Resets the stored previous values. Needed to be called for webkit which will
 * not generate a key up for meta key operations. This should only be called
 * when having finished with repeat key possibilities.
 */
goog.events.KeyHandler.prototype.resetState = function() {
  'use strict';
  this.lastKey_ = -1;
  this.keyCode_ = -1;
};


/**
 * Clears the stored previous key value, resetting the key repeat status. Uses
 * -1 because the Safari 3 Windows beta reports 0 for certain keys (like Home
 * and End.)
 * @param {goog.events.BrowserEvent} e The keyup event.
 * @private
 */
goog.events.KeyHandler.prototype.handleKeyup_ = function(e) {
  'use strict';
  this.resetState();
  this.altKey_ = e.altKey;
};


/**
 * Handles the events on the element.
 * @param {goog.events.BrowserEvent} e  The keyboard event sent from the
 *     browser.
 * @suppress {strictMissingProperties} Added to tighten compiler checks
 */
goog.events.KeyHandler.prototype.handleEvent = function(e) {
  'use strict';
  var be = e.getBrowserEvent();
  var keyCode, charCode;
  var altKey = be.altKey;

  // IE reports the character code in the keyCode field for keypress events.
  // There are two exceptions however, Enter and Escape.
  if (goog.userAgent.IE && e.type == goog.events.EventType.KEYPRESS) {
    keyCode = this.keyCode_;
    charCode = keyCode != goog.events.KeyCodes.ENTER &&
            keyCode != goog.events.KeyCodes.ESC ?
        be.keyCode :
        0;

    // Safari reports the character code in the keyCode field for keypress
    // events but also has a charCode field.
  } else if (
      (goog.userAgent.WEBKIT || goog.userAgent.EDGE) &&
      e.type == goog.events.EventType.KEYPRESS) {
    keyCode = this.keyCode_;
    charCode = be.charCode >= 0 && be.charCode < 63232 &&
            goog.events.KeyCodes.isCharacterKey(keyCode) ?
        be.charCode :
        0;

    // Opera reports the keycode or the character code in the keyCode field.
  } else {
    if (e.type == goog.events.EventType.KEYPRESS) {
      if (goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_) {
        altKey = this.altKey_;
      }

      // Newer versions of Firefox will set the keyCode of non-function keys to
      // be the same as charCode. We need to account for this and update the
      // key event values accordingly. See
      // https://github.com/google/closure-library/issues/932 for more details.
      if (be.keyCode == be.charCode) {
        // Adjust any function key (ie. non-printable, such as ESC or
        // backspace) to not have a charCode. We don't want these keys to
        // accidentally be interpreted as insertable characters.
        if (be.keyCode < 0x20) {
          keyCode = be.keyCode;
          charCode = 0;
        } else {
          // For character keys, we want to use the preserved key code rather
          // than the keyCode on the browser event, which now uses the charCode.
          // These differ (eg. pressing 'a' gives keydown with keyCode = 65,
          // keypress with keyCode = charCode = 97) and so we need to account
          // for this.
          keyCode = this.keyCode_;
          charCode = be.charCode;
        }
      } else {
        keyCode = be.keyCode || this.keyCode_;
        charCode = be.charCode || 0;
      }
    } else {
      keyCode = be.keyCode || this.keyCode_;
      charCode = be.charCode || 0;
    }

    // On the Mac, shift-/ triggers a question mark char code and no key code
    // (WIN_KEY_FF_LINUX), so we synthesize the latter.
    if (goog.userAgent.MAC && charCode == goog.events.KeyCodes.QUESTION_MARK &&
        keyCode == goog.events.KeyCodes.WIN_KEY) {
      keyCode = goog.events.KeyCodes.SLASH;
    }
  }

  keyCode = goog.events.KeyCodes.normalizeKeyCode(keyCode);
  var key = keyCode;

  // Correct the key value for certain browser-specific quirks.
  if (keyCode) {
    if (keyCode >= 63232 && keyCode in goog.events.KeyHandler.safariKey_) {
      // NOTE(nicksantos): Safari 3 has fixed this problem,
      // this is only needed for Safari 2.
      key = goog.events.KeyHandler.safariKey_[keyCode];
    } else {
      // Safari returns 25 for Shift+Tab instead of 9.
      if (keyCode == 25 && e.shiftKey) {
        key = 9;
      }
    }
  } else if (
      be.keyIdentifier &&
      be.keyIdentifier in goog.events.KeyHandler.keyIdentifier_) {
    // This is needed for Safari Windows because it currently doesn't give a
    // keyCode/which for non printable keys.
    key = goog.events.KeyHandler.keyIdentifier_[be.keyIdentifier];
  }

  // If this was a redundant keypress event, we ignore it to avoid double-firing
  // an event as the event would've been handled by KEYDOWN. Gecko is currently
  // in the process of removing keypress events for non-printable characters
  // (https://bugzilla.mozilla.org/show_bug.cgi?id=968056) so we simulate this
  // logic here for older Gecko versions which still fire the events.
  if (goog.userAgent.GECKO && e.type == goog.events.EventType.KEYPRESS &&
      !goog.events.KeyCodes.firesKeyPressEvent(
          key, this.lastKey_, e.shiftKey, e.ctrlKey, altKey, e.metaKey)) {
    return;
  }

  // If we get the same keycode as a keydown/keypress without having seen a
  // keyup event, then this event was caused by key repeat.
  var repeat = key == this.lastKey_;
  this.lastKey_ = key;

  var event = new goog.events.KeyEvent(key, charCode, repeat, be);
  event.altKey = altKey;
  this.dispatchEvent(event);
};


/**
 * Returns the element listened on for the real keyboard events.
 * @return {Element|Document|null} The element listened on for the real
 *     keyboard events.
 */
goog.events.KeyHandler.prototype.getElement = function() {
  'use strict';
  return this.element_;
};


/**
 * Adds the proper key event listeners to the element.
 * @param {Element|Document} element The element to listen on.
 * @param {boolean=} opt_capture Whether to listen for browser events in
 *     capture phase (defaults to false).
 */
goog.events.KeyHandler.prototype.attach = function(element, opt_capture) {
  'use strict';
  if (this.keyUpKey_) {
    this.detach();
  }

  this.element_ = element;

  this.keyPressKey_ = goog.events.listen(
      this.element_, goog.events.EventType.KEYPRESS, this, opt_capture);

  // Most browsers (Safari 2 being the notable exception) doesn't include the
  // keyCode in keypress events (IE has the char code in the keyCode field and
  // Mozilla only included the keyCode if there's no charCode). Thus we have to
  // listen for keydown to capture the keycode.
  this.keyDownKey_ = goog.events.listen(
      this.element_, goog.events.EventType.KEYDOWN, this.handleKeyDown_,
      opt_capture, this);


  this.keyUpKey_ = goog.events.listen(
      this.element_, goog.events.EventType.KEYUP, this.handleKeyup_,
      opt_capture, this);
};


/**
 * Removes the listeners that may exist.
 */
goog.events.KeyHandler.prototype.detach = function() {
  'use strict';
  if (this.keyPressKey_) {
    goog.events.unlistenByKey(this.keyPressKey_);
    goog.events.unlistenByKey(this.keyDownKey_);
    goog.events.unlistenByKey(this.keyUpKey_);
    this.keyPressKey_ = null;
    this.keyDownKey_ = null;
    this.keyUpKey_ = null;
  }
  this.element_ = null;
  this.lastKey_ = -1;
  this.keyCode_ = -1;
};


/** @override */
goog.events.KeyHandler.prototype.disposeInternal = function() {
  'use strict';
  goog.events.KeyHandler.superClass_.disposeInternal.call(this);
  this.detach();
};
