handler.js

import Navigation from './navigation';
import Menu from './menu';

// element level attribute that will be used for link to another pages
const hrefAttribute = 'data-href-page';
const hrefOptionsAttribute = 'data-href-page-options';
const hrefPageReplaceAttribute = 'data-href-page-replace';
const modalCloseBtnAttribute = 'data-alert-dissmiss';

/**
 * Page level default handlers.
 *
 * @private
 * @type {Object<Object<Function>>}
 */
let handlers = {
    /**
     * All the handlers associated to the select event.
     * @type {Object}
     */
    select: {
        /**
         * A handler to allow declaring anchors to other pages in the TVML templates.
         *
         * @example <caption>A typical usage would be something like:</caption>
         *     <TVML TEMPLATE>
         *         ...
         *
         *         <lockup data-href-page="details" data-href-page-options="{id: 'MOVIE_ID'}">
         *             ...
         *         </lockup>
         *
         *         ...
         *     </END TEMPLATE>
         *     which will navigate to the details page with the provided options object
         *
         * @private
         * @param  {Event} e    The event passed while this handler is invoked.
         */
        onLinkClick(e) {
            let element = e.target;
            let page = element.getAttribute(hrefAttribute);
            let replace = element.getAttribute(hrefPageReplaceAttribute);

            if (!page) return;

            let options = element.getAttribute(hrefOptionsAttribute);
            options = options || "{}";

            // try to make the options object
            try {
                options = JSON.parse(options);
            } catch (ex) {
                console.warn(`Invalid value for the page options (${hrefOptionsAttribute}=${options}) in the template.`);
                options = {};
            }

            Navigation.navigate(page, options, replace); // perform navigation
        },
        /**
         * A handler that will allow declaring modal dismiss button in the TVML alert templates.
         *
         * @example <caption>A typical usage would be something like:</caption>
         *          
         *          <TVML ALERT TEMPLATE>
         *              
         *              ...
         *
         *              <button data-alert-dissmiss="close">
         *                  <text>Cancel</text>
         *              </button>
         *              
         *              ...
         *              
         *          </END TEMPLATE>
         *
         * @private
         * @param  {Event} e    The event passed while this handler was invoked
         */
        onModalCloseBtnClick(e) {
            let element = e.target;
            let closeBtn = element.getAttribute(modalCloseBtnAttribute);

            if (closeBtn) {
                console.log('close button clicked within the modal, dismissing modal...');
                Navigation.dismissModal();
            }
        },
        /**
         * Handler for menu navigation
         *
         * @private
         * @param  {Event} e    The event passed while this handler was invoked
         */
        onMenuItemSelect(e) {
            let element = e.target;
            let menuId = element.getAttribute('id');
            let elementType = element.nodeName.toLowerCase();
            let page = element.page;

            if (elementType === 'menuitem') {
                // no need to proceed if the page is already loaded or there is no page definition present
                if (!element.pageDoc && page) {
                    // set a loading message intially to the menuitem
                    Menu.setDocument(Navigation.getLoaderDoc(Menu.getLoadingMessage()), menuId)
                    // load the page
                    page().then((doc) => {
                        // if there is a document loaded, assign it to the menuitem
                        if (doc) {
                            // assign the pageDoc to disable reload everytime
                            element.pageDoc = doc;
                            Menu.setDocument(doc, menuId);    
                        }
                        // dissmiss any open modals
                        Navigation.dismissModal();
                    }, (error) => {
                        // if there was an error loading the page, set an error page to the menu item
                        Menu.setDocument(Navigation.getErrorDoc(error), menuId);
                        // dissmiss any open modals
                        Navigation.dismissModal();
                    });
                }
            }
        }
    }
};

/**
 * Sets the default handlers options
 *
 * @inner
 * @alias module:handler.setOptions
 *
 * @param {Object} cfg The configuration object {defaults}
 */
function setOptions(cfg = {}) {
    console.log('setting handler options...', cfg);
    // override the default options
    _.defaultsDeep(handlers, cfg.handlers);
}

/**
 * Iterates over the events configuration and add event listeners to the document.
 *
 * @example
 * {
 *     events: {
 *         'scroll': function(e) { // do the magic here },
 *         'select listItemLockup title': 'onTitleSelect',
 *         'someOtherEvent': ['onTitleSelect', function(e) { // some other magic }, ...]
 *     },
 *     onTitleSelect: function(e) {
 *         // do the magic here
 *     }
 * }
 *
 * @todo Implement querySelectorAll polyfill (it doesn't seem to exist on the xml document)
 *
 * @private
 *
 * @param {Document} doc            The document to add the listeners on.
 * @param {Object} cfg              The page object configuration.
 * @param {Boolean} [add=true]      Whether to add or remove listeners. Defaults to true (add)
 */
function setListeners(doc, cfg = {}, add = true) {
    if (!doc || !(doc instanceof Document)) {
        return;
    }

    let listenerFn = doc.addEventListener;
    if (!add) {
        listenerFn = doc.removeEventListener;
    }
    if (_.isObject(cfg.events)) {
        let events = cfg.events;
        
        _.each(events, (fns, e) => {
            let [ev, selector] = e.split(' ');
            let elements = null;
            if (!_.isArray(fns)) { // support list of event handlers
                fns = [fns];
            }
            if (selector) {
                selector = e.substring(e.indexOf(' ') + 1); // everything after space
                elements = _.attempt(() => doc.querySelectorAll(selector)); // catch any errors while document selection
            } else {
                elements = [doc];
            }
            elements = _.isError(elements) ? [] : elements;
            _.each(fns, (fn) => {
                fn = _.isString(fn) ? cfg[fn] : fn; // assume the function to be present on the page configuration obeject
                if (_.isFunction(fn)) {
                    console.log((add ? 'adding' : 'removing') + ' event on documents...', ev, elements);
                    _.each(elements, (el) => listenerFn.call(el, ev, (e) => fn.call(cfg, e))); // bind to the original configuration object
                }
            });
        })
    }
}

/**
 * Iterates over the events configuration and add event listeners to the document.
 *
 * @example
 * ATV.Handler.addListeners(tvmlDoc,
 * {
 *     events: {
 *         'scroll': function(e) { // do the magic here },
 *         'select listItemLockup title': 'onTitleSelect',
 *         'someOtherEvent': ['onTitleSelect', function(e) { // some other magic }, ...]
 *     },
 *     onTitleSelect: function(e) {
 *         // do the magic here
 *     }
 * });
 *
 * @todo Implement querySelectorAll polyfill (it doesn't seem to exist on the xml document)
 *
 * @inner
 * @alias module:handler.addListeners
 *
 * @param {Document} doc            The document to add the listeners on.
 * @param {Object} cfg              The page object configuration.
 */
function addListeners(doc, cfg) {
    setListeners(doc, cfg, true);
}

/**
 * Iterates over the events configuration and remove event listeners from document.
 *
 * ATV.Handler.removeListeners(tvmlDoc,
 * {
 *     events: {
 *         'scroll': function(e) { // do the magic here },
 *         'select listItemLockup title': 'onTitleSelect',
 *         'someOtherEvent': ['onTitleSelect', function(e) { // some other magic }, ...]
 *     },
 *     onTitleSelect: function(e) {
 *         // do the magic here
 *     }
 * });
 * 
 * @todo Implement querySelectorAll polyfill (it doesn't seem to exist on the xml document)
 *
 * @inner
 * @alias module:handler.removeListeners
 *
 * @param {Document} doc            The document to add the listeners on.
 * @param {Object} cfg              The page object configuration.
 */
function removeListeners(doc, cfg) {
    setListeners(doc, cfg, false);
}

/**
 * Iterates over the list of page level default handlers and set/unset listeners on the provided document.
 *
 * @private
 *
 * @param {Document} doc            The document to set/unset listeners on.
 * @param {Boolean} [add=true]      Whether to add or remove listeners. Defaults to true (add)
 */
function setDefaultHandlers(doc, add = true) {
    if (!doc || !(doc instanceof Document)) {
        return;
    }

    let listenerFn = doc.addEventListener;
    if (!add) {
        listenerFn = doc.removeEventListener;
    }

    // iterate over all the handlers and add it as an event listener on the doc
    for (let name in handlers) {
        for (let key in handlers[name]) {
            listenerFn.call(doc, name, handlers[name][key]);
        }
    }
}

/**
 * Syntactical sugar to {setDefaultHandlers} with add=true
 *
 * @private
 *
 * @param {Document} doc        The document to add the listeners on.
 */
function addDefaultHandlers(doc) {
    setDefaultHandlers(doc, true);
}

/**
 * Syntactical sugar to {setDefaultHandlers} with add=false
 *
 * @private
 *
 * @param {Document} doc        The document to add the listeners on.
 */
function removeDefaultHandlers(doc) {
    setDefaultHandlers(doc, false);
}

/**
 * Sets/unsets the event handlers as per the event configuration.
 * Also adds/removes the [default page level handlers]{@link handlers}.
 *
 * @private
 * 
 * @param {Document}  doc           The page document.
 * @param {Obejct}  cfg             Page configuration object
 * @param {Boolean} [add=true]      Whether to add or remove the handlers
 */
function setHandlers(doc, cfg, add = true) {
    if (add) {
        addDefaultHandlers(doc);
        addListeners(doc, cfg);    
    } else {
        removeDefaultHandlers(doc);
        removeListeners(doc, cfg);
    }
}

/**
 * Sets the event handlers as per the event configuration.
 * Also adds the [default page level handlers]{@link handlers}.
 *
 * @inner
 * @alias module:handler.addAll
 *
 * @param {Document}  doc           The page document.
 * @param {Obejct}  cfg             Page configuration object
 */
function addHandlers(doc, cfg) {
    setHandlers(doc, cfg, true);
}

/**
 * Unset the event handlers as per the event configuration.
 * Also removes the [default page level handlers]{@link handlers}.
 *
 * @inner
 * @alias module:handler.removeAll
 *
 * @param {Document}  doc           The page document.
 * @param {Obejct}  cfg             Page configuration object
 */
function removeHandlers(doc, cfg) {
    setHandlers(doc, cfg, false);
}

/**
 * A minimalistic Event handling library for Apple TV applications
 *
 * @module handler
 *
 * @author eMAD <emad.alam@yahoo.com>
 *
 */
export default {
    setOptions: setOptions,
    addListeners: addListeners,
    removeListeners: removeListeners,
    addAll: addHandlers,
    removeAll: removeHandlers
};