API Docs for: 1.8.0
Show:

File: src/nonvisual/keyboardmanager.js

/*
 * Copyright (c) 2014-2015, Wanadev <http://www.wanadev.fr/>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice, this
 *     list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *   * Neither the name of Wanadev nor the names of its contributors may be used
 *     to endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authored by: A. Breust <https://github.com/Breush>
 */

/**
 * PhotonUI - Javascript Web User Interface.
 *
 * @module PhotonUI
 * @submodule NonVisual
 * @namespace photonui
 */

var Helpers = require("../helpers.js");
var Base = require("../base.js");
var Widget = require("../widget.js");

/**
 * Manage document-wide keyboard events.
 *
 * wEvents:
 *
 *   * key-down:
 *      - description: When a key is pushed (event launched just once until key is released).
 *      - callback:    function(manager, kstate)
 *
 *   * key-up:
 *      - description: When a key is released.
 *      - callback:    function(manager, kstate)
 *
 *   * key-hold:
 *      - description: When the last key pressed is held down.
 *      - callback:    function(manager, kstate)
 *
 *
 * kstate:
 *
 *   A snapshot of the keyboard state when the event occured.
 *
 *     {
 *         event: <Object>,       // The original js event
 *         action: <String>,      // The event name (key-down/up, key-hold)
 *         keys: <String>,        // The object of active keys
 *         key: <String>,         // User-friendly key name
 *     }
 *
 * @class KeyboardManager
 * @constructor
 * @extends photonui.Base
 * @param {photonui.Widget} element Any PhotonUI Widget (optional).
 * @param {HTMLElement} element Any HTML element (optional).
 * @param {Object} params An object that can contain any property of the widget (optional).
 */
var KeyboardManager = Base.$extend({

    // Constructor
    __init__: function (element, params) {
        this._registerWEvents(["key-down", "key-up", "key-hold"]);

        this.__keys = {};

        if (element && (element instanceof Widget || element instanceof HTMLElement || element === document)) {
            this.$super(params);
            this.element = element;
        } else {
            this.$super(element);
        }

        this._initKeyCache();

        // NOTE Holding event might be broken on old versions of Ubuntu + GTK
        // for an event "keyup" is fired before the "keydown" repeat.
        // One work-around would be to send by ourself the events inside an animation loop,
        // according to the last and the current states of active keys.
    },

    //////////////////////////////////////////
    // Properties and Accessors             //
    //////////////////////////////////////////

    // ====== Public properties ======

    /**
     * Disable the keyboard manager callbacks when focusing a input field-like element.
     *
     * @property safe
     * @type Boolean
     * @default true
     */
    _safe: true,

    getSafe: function () {
        "@photonui-update";
        return this._safe;
    },

    setSafe: function (safe) {
        this._safe = safe;
    },

    /**
     * Disable concerned events default actions.
     *
     * @property noPreventDefault
     * @type Boolean
     * @default false
     */
    _noPreventDefault: false,

    getNoPreventDefault: function () {
        "@photonui-update";
        return this._noPreventDefault;
    },

    setNoPreventDefault: function (noPreventDefault) {
        this._noPreventDefault = noPreventDefault;
    },

    /**
     * The HTML Element on which the events are binded.
     *
     * NOTE: If a photonui.Widget object is assigned to this property,
     *       its HTML Element will be automatically assigned to the property instead.
     *
     * @property element
     * @type HTMLElement
     * @default null
     */
    _element: null,

    getElement: function () {
        return this._element || document;
    },

    setElement: function (element) {
        if (element instanceof Widget) {
            this._element = element.interactiveNode || element.html;
        } else if (element instanceof HTMLElement || element === document) {
            this._element = element;
        } else {
            this._element = null;
        }
        this._updateEvents();
    },

    /**
     * The action:
     *
     *   * "key-down"
     *   * "key-up"
     *   * "key-hold"
     *
     * @property action
     * @readOnly
     * @type String
     */
    _action: "",

    getAction: function () {
        return this._action;
    },

    // ====== Private properties ======

    /**
     * Last event object.
     *
     * @property __event
     * @private
     * @type Object
     * @default null
     */
    __event: null,

    /**
     * The currently active keys.
     *
     * @property keys
     * @private
     * @type Object
     * @default {}
     */
    __keys: null,

    /**
     * Last key concerned.
     *
     * @property __key
     * @private
     * @type String
     * @default null
     */
    __key: null,

    /**
     * KeyCode correspondance to key name.
     *
     * @property __keyCache
     * @private
     * @type Array
     */
    __keyCache: [],

    /**
     * Key name correspondance to key code.
     *
     * @property __keyCodeCache
     * @private
     * @type Array
     */
    __keyCodeCache: [],

    //////////////////////////////////////////
    // Methods                              //
    //////////////////////////////////////////

    // ====== Public methods ======

    /**
     * Bind events on the HTML Element.
     *
     * @method _updateEvents
     * @private
     */
    _updateEvents: function () {
        // Unbind all existing events
        for (var id in this.__events) {
            this._unbindEvent(id);
        }
        // Check if we have an html element
        if (!this.element) {
            return;
        }
        // Bind new events
        // We will simulate the standard "keypress" event because some browsers
        // do not emit one for special keys (alt, ctrl, shift, escape).
        this._bindEvent("key-down", this.getElement(), "keydown", this.__onDocumentKeyDown.bind(this));
        this._bindEvent("key-up", this.getElement(), "keyup", this.__onDocumentKeyUp.bind(this));

        this._bindEvent("lost-focus", window, "blur", this.__onWindowBlur.bind(this));
    },

    /**
     * Check if a specific key is currently pressed.
     *
     * @method isKeyPressed
     * @param {String} key The key name
     * @return {Boolean}
     */
    isKeyPressed: function (key) {
        var keyState = this.__keys[this.__keyCodeCache[key]];
        return (keyState) ? true : false;
    },

    /**
     * Check if a specific key is currently released.
     *
     * @method isKeyReleased
     * @param {String} key The key name
     * @return {Boolean}
     */
    isKeyReleased: function (key) {
        var keyState = this.__keys[this.__keyCodeCache[key]];
        return (keyState) ? false : true;
    },

    // ====== Private methods ======

    /**
     * Take a snapshot of the KeyboardManager
     *
     * @method _dump
     * @private
     * @return {Object}
     */
    _dump: function () {
        return {
            event: this.__event,
            action: this._action,
            key: this.__key
        };
    },

    /**
     * Check if the user has not focused an input element
     *
     * @method _checkFocus
     * @private
     * @return {Boolean}
     */
    _checkFocus: function () {
        return !(this._safe && (document.activeElement instanceof HTMLInputElement ||
               document.activeElement instanceof HTMLSelectElement ||
               document.activeElement instanceof HTMLTextAreaElement));
    },

    /**
     * Check the validity of the keyboard event
     *
     * @method _checkEvent
     * @private
     * @param {KeyboardEvent} event The keyboard event
     * @return {Boolean}
     */
    _checkEvent: function (event) {
        this.__event = event;
        this.__key = this._keyFromEvent(event);

        if (!this.__key || this.__key === "Dead") {
            return false;
        }

        this.__keyCodeCache[this.__key] = event.keyCode;

        return this._checkFocus();
    },

    /**
     * Create the key correspondance cache for basic touches
     *
     * @method _initKeyCache
     * @private
     */
    _initKeyCache: function () {
        // General
        this.__keyCache[3] = "cancel";
        this.__keyCache[8] = "backspace";
        this.__keyCache[9] = "tab";
        this.__keyCache[12] = "clear";
        this.__keyCache[13] = "enter";
        this.__keyCache[16] = "shift";
        this.__keyCache[17] = "ctrl";
        this.__keyCache[18] = "alt";
        this.__keyCache[19] = "pause";
        this.__keyCache[20] = "capslock";
        this.__keyCache[27] = "escape";
        this.__keyCache[32] = "space";
        this.__keyCache[33] = "pageup";
        this.__keyCache[34] = "pagedown";
        this.__keyCache[35] = "end";
        this.__keyCache[36] = "home";
        this.__keyCache[37] = "left";
        this.__keyCache[38] = "up";
        this.__keyCache[39] = "right";
        this.__keyCache[40] = "down";
        this.__keyCache[41] = "select";
        this.__keyCache[42] = "printscreen";
        this.__keyCache[43] = "execute";
        this.__keyCache[44] = "snapshot";
        this.__keyCache[45] = "insert";
        this.__keyCache[46] = "delete";
        this.__keyCache[47] = "help";

        // 0-9
        this.__keyCache[48] = "0";
        this.__keyCache[49] = "1";
        this.__keyCache[50] = "2";
        this.__keyCache[51] = "3";
        this.__keyCache[52] = "4";
        this.__keyCache[53] = "5";
        this.__keyCache[54] = "6";
        this.__keyCache[55] = "7";
        this.__keyCache[56] = "8";
        this.__keyCache[57] = "9";

        // A-Z
        for (var keyCode = 65; keyCode <= 90; ++keyCode) {
            this.__keyCache[keyCode] = String.fromCharCode(keyCode);
        }

        // numpad
        this.__keyCache[96] = "num0";
        this.__keyCache[97] = "num1";
        this.__keyCache[98] = "num2";
        this.__keyCache[99] = "num3";
        this.__keyCache[100] = "num4";
        this.__keyCache[101] = "num5";
        this.__keyCache[102] = "num6";
        this.__keyCache[103] = "num7";
        this.__keyCache[104] = "num8";
        this.__keyCache[105] = "num9";
        this.__keyCache[106] = "num*";
        this.__keyCache[107] = "num+";
        this.__keyCache[108] = "numenter";
        this.__keyCache[109] = "num-";
        this.__keyCache[110] = "num.";
        this.__keyCache[111] = "num/";
        this.__keyCache[144] = "numlock";
        this.__keyCache[145] = "scrolllock";

        // function keys
        this.__keyCache[112] = "f1";
        this.__keyCache[113] = "f2";
        this.__keyCache[114] = "f3";
        this.__keyCache[115] = "f4";
        this.__keyCache[116] = "f5";
        this.__keyCache[117] = "f6";
        this.__keyCache[118] = "f7";
        this.__keyCache[119] = "f8";
        this.__keyCache[120] = "f9";
        this.__keyCache[121] = "f10";
        this.__keyCache[122] = "f11";
        this.__keyCache[123] = "f12";
    },

    /**
     * Get the key name from a native keyboard event
     *
     * @method _keyFromEvent
     * @private
     * @param {KeyboardEvent} event The keyboard event
     * @return {String}
     */
    _keyFromEvent: function (keyboardEvent) {
        var key = this.__keyCache[keyboardEvent.keyCode];
        if (!key) {
            key = keyboardEvent.key;
            this.__keyCache[keyboardEvent.keyCode] = key;
        }
        return key;
    },

    //////////////////////////////////////////
    // Internal Events Callbacks            //
    //////////////////////////////////////////

    /**
     * Used to grab all keyboard events
     *
     * @method __onDocumentKeyDown
     * @private
     * @param event
     */
    __onDocumentKeyDown: function (event) {
        if (!this._checkEvent(event)) {
            return;
        }

        this._action = (this.__keys[event.keyCode]) ? "key-hold" : "key-down";
        this.__keys[event.keyCode] = true;
        this._callCallbacks(this._action, [this._dump()]);

        if (!this._noPreventDefault) {
            event.stopPropagation();
            event.preventDefault();
        }
    },

    /**
     * Used to grab all keyboard events
     *
     * @method __onDocumentKeyUp
     * @private
     * @param event
     */
    __onDocumentKeyUp: function (event) {
        if (!this._checkEvent(event) || !this.__keys[event.keyCode]) {
            return;
        }

        this._action = "key-up";
        delete this.__keys[event.keyCode];
        this._callCallbacks(this._action, [this._dump()]);

        if (!this._noPreventDefault) {
            event.stopPropagation();
            event.preventDefault();
        }
    },

    /**
     * Called when the window loose focus
     *
     * @method __onWindowBlur
     * @private
     */
    __onWindowBlur: function () {
        this._action = "key-up";
        this._event = undefined;

        for (var keyCode in this.__keys) {
            delete this.__keys[keyCode];
            this.__key = this.__keyCache[keyCode];
            this._callCallbacks(this._action, [this._dump()]);
        }
    }
});

module.exports = KeyboardManager;