Source: input/keyboard.js

Object.assign(pc, function () {
    /**
     * @constructor
     * @name pc.KeyboardEvent
     * @classdesc The KeyboardEvent is passed into all event callbacks from the {@link pc.Keyboard}. It corresponds to a key press or release.
     * @description Create a new KeyboardEvent
     * @param {pc.Keyboard} keyboard The keyboard object which is firing the event.
     * @param {KeyboardEvent} event The original browser event that was fired.
     * @property {Number} key The keyCode of the key that has changed. See the pc.KEY_* constants.
     * @property {Element} element The element that fired the keyboard event.
     * @property {KeyboardEvent} event The original browser event which was fired.
     * @example
     * var onKeyDown = function (e) {
     *     if (e.key === pc.KEY_SPACE) {
     *         // space key pressed
     *     }
     *     e.event.preventDefault(); // Use original browser event to prevent browser action.
     * };
     * app.keyboard.on("keydown", onKeyDown, this);
     */
    var KeyboardEvent = function (keyboard, event) {
        if (event) {
            this.key = event.keyCode;
            this.element = event.target;
            this.event = event;
        } else {
            this.key = null;
            this.element = null;
            this.event = null;
        }
    };

    // internal global keyboard events
    var _keyboardEvent = new KeyboardEvent();

    function makeKeyboardEvent(event) {
        _keyboardEvent.key = event.keyCode;
        _keyboardEvent.element = event.target;
        _keyboardEvent.event = event;
        return _keyboardEvent;
    }

    /**
     * @private
     * @function
     * @name pc.toKeyCode
     * @description Convert a string or keycode to a keycode
     * @param {String | Number} s Either a character code or the key character.
     * @returns {Number} The character code.
     */
    function toKeyCode(s){
        if (typeof s === "string") {
            return s.toUpperCase().charCodeAt(0);
        }
        return s;
    }

    var _keyCodeToKeyIdentifier = {
        '9': 'Tab',
        '13': 'Enter',
        '16': 'Shift',
        '17': 'Control',
        '18': 'Alt',
        '27': 'Escape',

        '37': 'Left',
        '38': 'Up',
        '39': 'Right',
        '40': 'Down',

        '46': 'Delete',

        '91': 'Win'
    };

    /**
     * @event
     * @name pc.Keyboard#keydown
     * @description Event fired when a key is pressed.
     * @param {pc.KeyboardEvent} event The Keyboard event object. Note, this event is only valid for the current callback.
     * @example
     * var onKeyDown = function (e) {
     *     if (e.key === pc.KEY_SPACE) {
     *         // space key pressed
     *     }
     *     e.event.preventDefault(); // Use original browser event to prevent browser action.
     * };
     * app.keyboard.on("keydown", onKeyDown, this);
     */

    /**
     * @event
     * @name pc.Keyboard#keyup
     * @description Event fired when a key is released.
     * @param {pc.KeyboardEvent} event The Keyboard event object. Note, this event is only valid for the current callback.
     * @example
     * var onKeyUp = function (e) {
     *     if (e.key === pc.KEY_SPACE) {
     *         // space key released
     *     }
     *     e.event.preventDefault(); // Use original browser event to prevent browser action.
     * };
     * app.keyboard.on("keyup", onKeyUp, this);
     */

    /**
     * @constructor
     * @name pc.Keyboard
     * @classdesc A Keyboard device bound to an Element. Allows you to detect the state of the key presses.
     * Note, Keyboard object must be attached to an Element before it can detect any key presses.
     * @description Create a new Keyboard object
     * @param {Element} [element] Element to attach Keyboard to. Note that elements like <div> can't
     * accept focus by default. To use keyboard events on an element like this it must have a value of 'tabindex' e.g. tabindex="0". For more details: <a href="http://www.w3.org/WAI/GL/WCAG20/WD-WCAG20-TECHS/SCR29.html">http://www.w3.org/WAI/GL/WCAG20/WD-WCAG20-TECHS/SCR29.html</a>
     * @param {Object} [options] Optional options object.
     * @param {Boolean} [options.preventDefault] Call preventDefault() in key event handlers. This stops the default action of the event occurring. e.g. Ctrl+T will not open a new browser tab
     * @param {Boolean} [options.stopPropagation] Call stopPropagation() in key event handlers. This stops the event bubbling up the DOM so no parent handlers will be notified of the event
     * @example
     * var keyboard = new pc.Keyboard(window); // attach keyboard listeners to the window
     */
    var Keyboard = function (element, options) {
        options = options || {};
        this._element = null;

        this._keyDownHandler = this._handleKeyDown.bind(this);
        this._keyUpHandler = this._handleKeyUp.bind(this);
        this._keyPressHandler = this._handleKeyPress.bind(this);

        pc.events.attach(this);

        this._keymap = {};
        this._lastmap = {};

        if (element) {
            this.attach(element);
        }

        this.preventDefault = options.preventDefault || false;
        this.stopPropagation = options.stopPropagation || false;
    };

    /**
     * @function
     * @name pc.Keyboard#attach
     * @description Attach the keyboard event handlers to an Element
     * @param {Element} element The element to listen for keyboard events on.
     */
    Keyboard.prototype.attach = function (element) {
        if (this._element) {
            // remove previous attached element
            this.detach();
        }
        this._element = element;
        this._element.addEventListener("keydown", this._keyDownHandler, false);
        this._element.addEventListener("keypress", this._keyPressHandler, false);
        this._element.addEventListener("keyup", this._keyUpHandler, false);
    };

    /**
     * @function
     * @name pc.Keyboard#detach
     * @description Detach the keyboard event handlers from the element it is attached to.
     */
    Keyboard.prototype.detach = function () {
        this._element.removeEventListener("keydown", this._keyDownHandler);
        this._element.removeEventListener("keypress", this._keyPressHandler);
        this._element.removeEventListener("keyup", this._keyUpHandler);
        this._element = null;
    };

    /**
     * @private
     * @function
     * @name pc.Keyboard#toKeyIdentifier
     * @description Convert a key code into a key identifier
     * @param {Number} keyCode The key code.
     * @returns {String} The key identifier.
     */
    Keyboard.prototype.toKeyIdentifier = function (keyCode){
        keyCode = toKeyCode(keyCode);
        var count;
        var hex;
        var length;
        var id = _keyCodeToKeyIdentifier[keyCode.toString()];

        if (id) {
            return id;
        }

        // Convert to hex and add leading 0's
        hex = keyCode.toString(16).toUpperCase();
        length = hex.length;
        for (count = 0; count < (4 - length); count++) {
            hex = '0' + hex;
        }

        return 'U+' + hex;
    };

    Keyboard.prototype._handleKeyDown = function (event) {
        var code = event.keyCode || event.charCode;

        // Google Chrome auto-filling of login forms could raise a malformed event
        if (code === undefined) return;

        var id = this.toKeyIdentifier(code);

        this._keymap[id] = true;

        // Patch on the keyIdentifier property in non-webkit browsers
        // event.keyIdentifier = event.keyIdentifier || id;

        this.fire("keydown", makeKeyboardEvent(event));

        if (this.preventDefault) {
            event.preventDefault();
        }
        if (this.stopPropagation) {
            event.stopPropagation();
        }
    };

    Keyboard.prototype._handleKeyUp = function (event){
        var code = event.keyCode || event.charCode;

        // Google Chrome auto-filling of login forms could raise a malformed event
        if (code === undefined) return;

        var id = this.toKeyIdentifier(code);

        delete this._keymap[id];

        // Patch on the keyIdentifier property in non-webkit browsers
        // event.keyIdentifier = event.keyIdentifier || id;

        this.fire("keyup", makeKeyboardEvent(event));

        if (this.preventDefault) {
            event.preventDefault();
        }
        if (this.stopPropagation) {
            event.stopPropagation();
        }
    };

    Keyboard.prototype._handleKeyPress = function (event){
        this.fire("keypress", makeKeyboardEvent(event));

        if (this.preventDefault) {
            event.preventDefault();
        }
        if (this.stopPropagation) {
            event.stopPropagation();
        }
    };

    /**
     * @private
     * @function
     * @name pc.Keyboard#update
     * @description Called once per frame to update internal state.
     */
    Keyboard.prototype.update = function () {
        var prop;

        // clear all keys
        for (prop in this._lastmap) {
            delete this._lastmap[prop];
        }

        for (prop in this._keymap) {
            if (this._keymap.hasOwnProperty(prop)) {
                this._lastmap[prop] = this._keymap[prop];
            }
        }
    };

    /**
     * @function
     * @name pc.Keyboard#isPressed
     * @description Return true if the key is currently down.
     * @param {Number} key The keyCode of the key to test. See the pc.KEY_* constants.
     * @returns {Boolean} True if the key was pressed, false if not.
     */
    Keyboard.prototype.isPressed = function (key) {
        var keyCode = toKeyCode(key);
        var id = this.toKeyIdentifier(keyCode);

        return !!(this._keymap[id]);
    };

    /**
     * @function
     * @name pc.Keyboard#wasPressed
     * @description Returns true if the key was pressed since the last update.
     * @param {Number} key The keyCode of the key to test. See the pc.KEY_* constants.
     * @returns {Boolean} true if the key was pressed.
     */
    Keyboard.prototype.wasPressed = function (key) {
        var keyCode = toKeyCode(key);
        var id = this.toKeyIdentifier(keyCode);

        return (!!(this._keymap[id]) && !!!(this._lastmap[id]));
    };

    /**
     * @function
     * @name pc.Keyboard#wasReleased
     * @description Returns true if the key was released since the last update.
     * @param {Number} key The keyCode of the key to test. See the pc.KEY_* constants.
     * @returns {Boolean} true if the key was pressed.
     */
    Keyboard.prototype.wasReleased = function (key) {
        var keyCode = toKeyCode(key);
        var id = this.toKeyIdentifier(keyCode);

        return (!!!(this._keymap[id]) && !!(this._lastmap[id]));
    };

    return {
        Keyboard: Keyboard,
        KeyboardEvent: KeyboardEvent
    };
}());