creamidea / keyboard

"use strict"
var Keyboard = (function () {
    function __Keyboard() {
        this.keys = {} // to record the pressed key
        this.register_list = {} // to record the registers(key combos)
        this.state = {} // to record every register matching condition. do you want to get this value?
        this.specialKeyString = {
            "altKey": "Alt",
            "ctrlKey": "Control",
            "metaKey": "Meta",
            "shiftKey": "Shift"
        }
    }
    __Keyboard.prototype.listen = function () {
        var option = this.option, element = document
        if (option.element && typeof option.element.addEventListener === 'function') {
            element = option.element
        }
        element.addEventListener('keydown', this.keydown.bind(this), false)
        element.addEventListener('keyup', this.keyup.bind(this), false)
    }

    __Keyboard.prototype.unlisten = function () {
        // maybe you need callback?
        var option = this.option, element = document
        if (option.element && typeof option.element.removeEventListener === 'function') {
            element = option.element
        }
        element.removeEventListener('keydown', function () { })
        element.removeEventListener('keyup', function () { })
    }

    __Keyboard.prototype.test = function (event) {
        return this.testRegisters(event)
    }

    __Keyboard.prototype.testRegisters = function (event) {
        var register_list = this.register_list
        var register_names = Object.keys(register_list)
        var testKeys = this.testKeys.bind(this)
        var state = {}
        for (var i = 0, len = register_names.length; i < len; i++) {
            var regName = register_names[i]
            var reg = register_list[regName]
            var keylist = reg[0]
            var callback = reg[1]

            // hit the target
            if (testKeys(keylist)) {
                if (callback && typeof callback === 'function') {
                    // TODO:
                    // Need event object? or context?
                    var __wrapper_callback = (function () {
                        event.clearKeys = this.clearKeys.bind(this)
                        // inject the event(the last key) object
                        callback(event)

                        // BUG:
                        // when use `alert` or `confirm`, the event(keyup) of the pressed key will lost.
                        // so, you will don't know the key is really pressed or not when you are back.
                        // here code just detects some special keys.
                        // SO DO NOT USE ALERT OR CONFIRM!
                        Array.prototype.map.call(Object.keys(this.specialKeyString), ((function (key) {
                            if (event[key]) this.keys[this.specialKeyString[key]] = true
                        }).bind(this)))
                    }).bind(this)

                    if (window.requestAnimationFrame)
                        window.requestAnimationFrame(__wrapper_callback)
                    else
                        setTimeout(__wrapper_callback, 16)
                }
                state[regName] = true
                // if match successfully, return directly.
                return state
            }
        }
        return state
    }

    // @param keylist Array(Array) [combo1, combo2, ...]
    __Keyboard.prototype.testKeys = function (keylist) {
        var result = [], state = false
        for (var i = 0, len = keylist.length; i < len; i++) {
            var combo = keylist[i]
            var allPressedkeys = Object.keys(this.keys)
            var nowPressedkeys = []
            var __state = 0 // no state. not true or false

            // collect all pressed key now
            allPressedkeys.forEach((function (value, index) {
                if (this.keys[value]) nowPressedkeys.push(value)
            }).bind(this))

            // DEBUG: print the pressing key message
            // console.log(allPressedkeys, this.keys)
            if (this.option.DEBUG === true) {
                var __printKey = nowPressedkeys.map(function (k, i) {
                    if (k === " ") return "Space"
                    else return k
                }).join(" ")
                console.log('[' + Date.now() + '] You hit key: %c' + __printKey, 'color: #ea4335; font-size: 16px')
            }

            // compare nowPressedkeys and combo
            // console.log('compare: ', nowPressedkeys, combo)
            if (nowPressedkeys.length !== combo.length) {
                __state = false
            } else {
                for (var j = 0, len2 = combo.length; j < len2; j++) {
                    if (nowPressedkeys.indexOf(combo[j]) < 0) {
                        // not in the array
                        __state = false
                        break
                    }
                }
                // if j is equal to combo.length, this means that user hit the combo.
                // otherwise, user does't.
                if (j === combo.length && __state !== false) __state = true
            }
            result.push(__state)
        }
        // console.log('> result', result, this.keys)
        result.forEach(function (v, i) {
            if (v === true) state = true
        })
        return state
    }

    __Keyboard.prototype.keydown = function (event) {
        var key = event.key, state = {}, rlt = true, map = Array.prototype.map
        this.keys[key] = event.type === 'keydown'
        // this.keys[key] = true
        // the result of test
        // true: hit the target, then prevent the default action, so return true
        // otherwise, don't prevent it, so return false
        state = this.test(event)
        Object.keys(state).forEach(function (regName, i) {
            if (state[regName] === true) rlt = false
        })
        this.state = state
        if (!rlt) {
            event.preventDefault()
            event.stopPropagation()
            // event.stopImmediatePropagation()
        }
        // console.log(rlt)
        return rlt
    }

    __Keyboard.prototype.keyup = function (event) {
        var key = event.key
        this.keys[key] = false
        return true
    }

    __Keyboard.prototype.register = function (name, callback, keylist) {
        if (typeof name !== 'string') throw new Error('Please input the register name.')
        if (this.register_list[name]) throw new Error('The ' + name + ' has existed!')
        var keylist = Array.prototype.slice.call(arguments, 2)
        if (!(keylist[0] instanceof Array)) keylist = [keylist] // init [combo1:Array, combo2:Array, ....]
        this.register_list[name] = [keylist, callback]
    }

    __Keyboard.prototype.clearRegister = function (name) {
        delete this.register_list[name]
    }
    __Keyboard.prototype.clearRegisterAll = function () {
        this.register_list = {}
    }
    __Keyboard.prototype.clearKeys = function () {
        this.keys = {}
    }
    var k = new __Keyboard()

    var __instance = {
        start: function () { k.listen() },
        end: function () { k.unlisten(); k.clearRegisterAll(); k.clearKeys(); },
        register: function () { k.register.apply(k, arguments) },
        unregister: function () { k.clearRegister.apply(k, arguments) },
        // for test
        __keydown: function () { k.keydown.apply(k, arguments) },
        __keyup: function () { k.keyup.apply(k, arguments) },
    }

    return function (o) {
        k.option = o || {}
        window.addEventListener('focus', function () {
            k.keys = {}
        }, false)
        // window.addEventListener('blur', function () {
        //     k.keys = {}
        // }, false)
        return __instance
    }
})()

if (typeof exports !== "undefined") {
    exports.Keyboard = Keyboard
} else if (typeof define !== 'undefined' && typeof define === 'function') {
    define("Keyboard", [], function () {
        return Keyboard
    })
} else {
    if (window.Keyboard === undefined) window.Keyboard = Keyboard
    else {
        throw new Error('Library Keyboard has existed! Loaded failed.')
    }
}