Home Reference Source

src/keasy.js

import _isString from 'lodash/isString';
import _isInteger from 'lodash/isInteger';
import _isFunction from 'lodash/isFunction';
import _debounce from 'lodash/debounce';
import _keys from 'lodash/keys'
import KeyDown from './events/keydown';
import KeyPress from './events/keypress';
import KeyUp from './events/keyup';
import EventListener from './eventlistener';
import KeyMatcher from './keymatcher';

const keasyEventSpace = "keasy:";
/**
 * @type {string}
 */
export const down = keasyEventSpace + "keydown";
/**
 * @type {string}
 */
export const up = keasyEventSpace + "keyup";
/**
 * @type {string}
 */
export const press = keasyEventSpace + "keypress";
const eventMap = {
    'keydown': KeyDown,
    'keypress': KeyPress,
    'keyup': KeyUp
};

/**
 * @type {string}
 */
export const milliseconds = 'milliseconds';
/**
 * @type {string}
 */
export const seconds = 'seconds';
/**
 * @type {string}
 */
export const minutes = 'minutes';

export default class Keasy {

    /**
     * @external {KeyBoardEvent} https://developer.mozilla.org/en-US/docs/Web/API/KeyBoardEvent
     */
    /**
     * @external {EventTarget} https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
     */

    /**
     * @desc Sets the eventType {@link String} this {@link Keasy} instance will capture.
     * @param {String} [eventType] - This parameter is optional. If Keasy.when(eventType) is not passed an eventType it defaults to 'keydown'. Passing an eventType is advisable for readability purposes.
     * @returns {Keasy}
     * @example <caption>Example 1 (Using Keasy shorthand)</caption>
     * Keasy.when(Keasy.down).on(document.getElementById('fancyId')).then(function(){console.warn("Keasy-peasy!")});
     * @example <caption>Example 2 (Using string)</caption>
     * Keasy.when('keydown').on(document.getElementById('fancyId')).then(function(){console.warn("Keasy-peasy!")});
     */
    when(eventType) {
        if (_isString(eventType)) {
            this._event = this._generateKeyEventObject(Keasy._removeKeasyEventSpace(eventType));
        }
        else {
            this._event = new KeyDown();
        }
        return this;
    }

    /**
     * @desc Binds an {@link EventTarget} to this {@link Keasy} instance.
     * @param {EventTarget} eventTarget - The targeted {@link EventTarget} Keasy should target for {@link KeyBoardEvent}.
     * @throws {TypeError} If {@link Keasy.on} is not passed a valid {@link EventTarget} it will throw a {@link TypeError}.
     * @returns {Keasy}
     * @example <caption>Example 1 (Using DOM)</caption>
     * Keasy.when(Keasy.down).on(document.getElementById('fancyId')).then(function(){console.warn("Keasy-peasy!")});
     * @example <caption>Example 2 (Using jQuery)</caption>
     * Keasy.when(Keasy.down).on($('#fancyId')[0]).then(function(){console.warn("Keasy-peasy!")});
     */
    on(eventTarget) {
        const DOMELEMENT_TYPE_ERROR = "Keasy.on(eventTarget) expects 'eventTarget' to be a valid EventTarget. It isn't.";
        if (eventTarget instanceof EventTarget) {
            this._eventTarget = eventTarget;
        }
        else {
            throw new TypeError(DOMELEMENT_TYPE_ERROR);
        }
        return this;
    }

    /**
     * @desc Binds a callback {@link Function} to this Keasy intsance.
     * Not calling {@link Keasy.then} means Keasy produces no output. Keasy still catches events of the type passed to {@link Keasy.when}.
     * Meaning you can call {@link Keasy.then} at another point in your code. See Example 3 for details.
     * @param {Function} callback - The Function Keasy will call after it has caught a {@link KeyBoardEvent}.
     * @throws {TypeError} If Keasy.then(callback) is not passed a {@link Function} a {@link TypeError} is thrown.
     * @returns {Keasy}
     * @example <caption>Example 1</caption>
     * Keasy.when(Keasy.down).on(document.getElementById('fancyId')).then(function(){console.warn('Thank you!!')});
     * @example <caption>Example 2</caption>
     * Keasy.when(Keasy.down).on(document.getElementById('fancyId')).then(function(){document.getElementById('pretty').innterText = "Thank you!!"});
     * @example <caption>Example 3</caption>
     * var keasy = Keasy.when(Keasy.down).on(document.getElementById('fancyId'));
     * document.getElementById('pretty').innerText = "Waiting is so boring..";
     * function delayedThen(){
     *      document.getElementById('pretty').innerText = "Ok I'm fed up with waiting.. For the love of God please type something!";
     *      keasy.then(function(){
     *          document.getElementById('pretty').innerText = "Thank you!!";
     *      });
     * };
     * window.setTimeout(delayedThen, 5000);
     */
    then(callback) {
        const CALLBACK_TYPE_ERROR = "Keasy.then(callback) expects 'callback' to be a Function. It isn't.";
        if (_isFunction(callback)) {
            this._callback = callback;
            this.off();
            this._subscribeToEvent();
        }
        else {
            throw new TypeError(CALLBACK_TYPE_ERROR);
        }
        return this;
    }

    /**
     * @desc Sets the amount of time and the unit of time this {@link Keasy} instance should wait until triggering the callback set in {@link Keasy.then}
     * @param {Number} amount - The amount of time Keasy waits until the {@link EventTarget} passed to {@link Keasy.on} stops firing {@link KeyBoardEvent}s.
     * @param {String} [unit] - This parameter is optional. If  Keasy.after(amount,unit) is not passed a unit it defaults to milliseconds.
     * @throws {TypeError}
     * @returns {Keasy}
     * @example <caption>Example 1</caption>
     * Keasy.when(Keasy.down).on(document.getElementById('fancyId')).then(function(){console.warn('Thank you!!')}).after(600);
     * @example <caption>Example 2</caption>
     * Keasy.when(Keasy.down).on(document.getElementById('fancyId')).then(function(){console.warn('Thank you!!')}).after(60000, Keasy.milliseconds);
     * @example <caption>Example 3</caption>
     * Keasy.when(Keasy.down).on(document.getElementById('fancyId')).then(function(){console.warn('Thank you!!')}).after(60, Keasy.seconds);
     * @example <caption>Example 4</caption>
     * Keasy.when(Keasy.down).on(document.getElementById('fancyId')).then(function(){console.warn('Thank you!!')}).after(1, Keasy.minutes);
     */
    after(amount, unit) {
        const AMOUNT_TYPE_ERROR = "Keasy.after(amount,unit) expects 'amount' to be an Integer. It isn't.";
        if (_isInteger(amount)) {
            this._delay = Keasy._toMilliseconds(amount, unit || milliseconds);
            this.off();
            this._subscribeToEvent();
        }
        else {
            throw new TypeError(AMOUNT_TYPE_ERROR);
        }
        return this;
    }

    /**
     * @desc Removes the current listener from the {@link EventTarget} passed to {@link Keasy.on}.
     */
    off() {
        if (this._eventListener) {
            this._eventListener.removeFrom(this._eventTarget);
        }
    }

    /**
     * @desc Binds a {@link String} representing a set of keys to this {@link Keasy} instance limiting when the callback {@link Function} passed to {@link Keasy.then} gets called.
     * The keys in the {String} that is passed should be divided by a "+" character.
     * @param {String} keyString
     * @throws {TypeError} If {@link Keasy.is} is not passed a {String} a {@link TypeError} is thrown.
     * @returns {Keasy}
     * @example <caption>Example 1</caption>
     * Keasy.when(Keasy.eventType).on(target).is(Keasy.ctrl+"+k").then(function(){console.warn("Event was CTRL+K")})
     */
    is(keyString) {
        const KEYSTRING_TYPE_ERROR = "Keasy.is(keyString) expects 'keyString' to be a String. It isn't.";
        if (_isString(keyString)) {
            this._keyStringToMatch = keyString;
            this.off();
            this._subscribeToEvent();
        }
        else {
            throw new TypeError(KEYSTRING_TYPE_ERROR);
        }
        return this;
    }

    _generateKeyEventObject(eventName) {
        let event = null;
        _keys(eventMap).forEach((key) => {
            const keyEvent = new eventMap[key]();
            if (keyEvent.equals(eventName)) {
                event = keyEvent;
            }
        });
        return event;
    }

    _subscribeToEvent() {
        this._eventListener = new EventListener(this._event, this._generateExecutor());
        this._eventListener.addTo(this._eventTarget);
    }

    _generateExecutor() {

        return (this._hasDelay()) ? this._generateDebouncedListener() : this._genereateRegularListener();
    }

    _generateDebouncedListener() {
        const self = this;
        const debouncedListener = _debounce(function () {
            self._callback();
        }, self._delay);


        const keyMatchingDebouncedListener = _debounce(function (event) {
            if (self._keyStringToMatch && new KeyMatcher().match(self._keyStringToMatch).withEvent(event)) {
                self._callback();
            }
        }, self._delay);

        return (this._keyStringToMatch) ? keyMatchingDebouncedListener : debouncedListener;
    }

    _genereateRegularListener() {
        const self = this;

        const listener = function () {
            self._callback();
        };

        const keyMatchingListener = function (event) {
            if (self._keyStringToMatch && new KeyMatcher().match(self._keyStringToMatch).withEvent(event)) {
                self._callback();
            }
        };

        return (this._keyStringToMatch) ? keyMatchingListener : listener;
    }

    static _removeKeasyEventSpace(eventString) {
        return eventString.replace(keasyEventSpace, "");
    }

    _hasDelay() {
        return _isInteger(this._delay);
    }

    static _toMilliseconds(amount, unit) {
        if (amount > 0) {
            switch (unit) {
                case seconds:
                    return amount * 1000;
                case minutes:
                    return amount * 60 * 1000;
                default:
                    return amount;
            }
        }
        else {
            return 0;
        }
    }
}