API Docs for: 0.0.1
Show:

File: src/focusmanager/focusmanager.js

"use strict";

require('js-ext/lib/object.js');
require('polyfill');
require('./css/focusmanager.css');

/**
 *
 *
 *
 * <i>Copyright (c) 2014 ITSA - https://github.com/itsa</i>
 * New BSD License - http://choosealicense.com/licenses/bsd-3-clause/
 *
 * @module focusmanager
 * @class FocusManager
 * @since 0.0.1
*/

var NAME = '[focusmanager]: ',
    async = require('utils').async,
    createHashMap = require('js-ext/extra/hashmap.js').createMap,
    DEFAULT_SELECTOR = 'input, button, select, textarea, [contenteditable="true"], .focusable, [plugin-fm="true"], [itag-formelement="true"]',
    // SPECIAL_KEYS needs to be a native Object --> we need .some()
    SPECIAL_KEYS = {
        shift: 'shiftKey',
        ctrl: 'ctrlKey',
        cmd: 'metaKey',
        alt: 'altKey'
    },
    DEFAULT_KEYUP = 'shift+9',
    DEFAULT_KEYDOWN = '9',
    DEFAULT_NOLOOP = false,
    FM_SELECTION = 'fm-selection',
    FM_SELECTION_START = FM_SELECTION+'start',
    FM_SELECTION_END = FM_SELECTION+'end',
    FOCUSSED = 'focussed';

module.exports = function (window) {

    var DOCUMENT = window.document,
        FocusManager, Event, nextFocusNode, searchFocusNode, markAsFocussed,
        resetLastValue, getFocusManagerSelector, setupEvents, defineFocusEvent;

    window._ITSAmodules || Object.protectedProp(window, '_ITSAmodules', createHashMap());

/*jshint boss:true */
    if (FocusManager=window._ITSAmodules.FocusManager) {
/*jshint boss:false */
        return FocusManager; // FocusManager was already created
    }

    require('window-ext')(window);
    require('node-plugin')(window);

    Event = require('event-mobile')(window);

    getFocusManagerSelector = function(focusContainerNode) {
        var selector = focusContainerNode._plugin.fm.model.manage;
        (selector.toLowerCase()==='true') && (selector=DEFAULT_SELECTOR);
        return selector;
    };

    nextFocusNode = function(e, keyCode, actionkey, focusContainerNode, sourceNode, selector, downwards, initialSourceNode) {
        console.log(NAME+'nextFocusNode');
        var keys, lastIndex, i, specialKeysMatch, specialKey, len, enterPressedOnInput, primaryButtons,
            inputType, foundNode, formNode, primaryonenter, noloop, nodeHit, foundContainer, type;
        keys = actionkey.split('+');
        len = keys.length;
        lastIndex = len - 1;

        if ((keyCode===13) && (sourceNode.getTagName()==='INPUT')) {
            type = sourceNode.getAttr('type') || 'text';
            inputType = type.toLowerCase();
            enterPressedOnInput = (inputType==='text') || (inputType==='password');
        }

        if (enterPressedOnInput) {
            // check if we need to press the primary button - if available
/*jshint boss:true */
            if ((primaryonenter=sourceNode.getAttr('fm-primaryonenter')) && (primaryonenter.toLowerCase()==='true')) {
/*jshint boss:false */
                primaryButtons = focusContainerNode.getAll('button.pure-button-primary');
                primaryButtons.some(function(buttonNode) {
                    buttonNode.matches(selector) && (foundNode=buttonNode);
                    return foundNode;
                });
                if (foundNode) {
                    async(function() {
                        Event.emit(foundNode, 'UI:tap');
                        // _buttonPressed make event-dom to simulate a pressed button for 200ms
                        Event.emit(foundNode, 'UI:tap', {_buttonPressed: true});
                        // if the button is of type `submit`, then try to submit the form
                        formNode = foundNode.inside('form');
                        formNode && formNode.submit();
                    });
                    return foundNode;
                }
            }
        }
        // double == --> keyCode is number, keys is a string
        if (enterPressedOnInput || (keyCode==keys[lastIndex])) {
            // posible keyup --> check if special characters match:
            specialKeysMatch = true;
            SPECIAL_KEYS.some(function(value) {
                specialKeysMatch = !e[value];
                return !specialKeysMatch;
            });
            for (i=lastIndex-1; (i>=0) && !specialKeysMatch; i--) {
                specialKey = keys[i].toLowerCase();
                specialKeysMatch = e[SPECIAL_KEYS[specialKey]];
            }
        }
        if (specialKeysMatch) {
            noloop = focusContainerNode._plugin.fm.model.noloop;
            // in case sourceNode is an innernode of a selector, we need to start from the selector:
            sourceNode.matches(selector) || (sourceNode=sourceNode.inside(selector));
            if (downwards) {
                nodeHit = sourceNode;
/*jshint noempty:true */
                while ((nodeHit=nodeHit.next(selector, focusContainerNode)) && (nodeHit.getStyle('display')==='none')) {}
/*jshint noempty:false */
                if (!nodeHit) {
                    nodeHit = noloop ? sourceNode.last(selector, focusContainerNode) : sourceNode.first(selector, focusContainerNode);
                    if (nodeHit.getStyle('display')==='none') {
/*jshint noempty:true */
                        while ((nodeHit=nodeHit[noloop ? 'previous' : 'next'](selector, focusContainerNode)) && (nodeHit.getStyle('display')==='none')) {}
/*jshint noempty:false */
                    }
                }
            }
            else {
                nodeHit = sourceNode;
/*jshint noempty:true */
                while ((nodeHit=nodeHit.previous(selector, focusContainerNode)) && (nodeHit.getStyle('display')==='none')) {}
/*jshint noempty:false */
                if (!nodeHit) {
                    nodeHit = noloop ? sourceNode.first(selector, focusContainerNode) : sourceNode.last(selector, focusContainerNode);
                    if (nodeHit.getStyle('display')==='none') {
/*jshint noempty:true */
                        while ((nodeHit=nodeHit[noloop ? 'next' : 'previous'](selector, focusContainerNode)) && (nodeHit.getStyle('display')==='none')) {}
/*jshint noempty:false */
                    }
                }
            }
            if (nodeHit===sourceNode) {
                // cannot found another, return itself, BUT return `initialSourceNode` if it is available
                return initialSourceNode || sourceNode;
            }
            else {
                foundContainer = nodeHit.inside('[plugin-fm="true"]');
                // only if `nodeHit` is inside the runniong focusContainer, we may return it,
                // otherwise look further
                return (foundContainer===focusContainerNode) ? nodeHit : nextFocusNode(e, keyCode, actionkey, focusContainerNode, nodeHit, selector, downwards, sourceNode);
            }
        }
        return false;
    };

    markAsFocussed = function(focusContainerNode, node) {
        console.log(NAME+'markAsFocussed');
        var selector = getFocusManagerSelector(focusContainerNode),
            index = focusContainerNode.getAll(selector).indexOf(node) || 0;
        // we also need to set the appropriate nodeData, so that when the itags re-render,
        // they don't reset this particular information
        resetLastValue(focusContainerNode);

        // also store the lastitem's index --> in case the node gets removed,
        // or re-rendering itags which don't have the attribute-data.
        // otherwise, a refocus on the container will set the focus to the nearest item
        focusContainerNode.setData('fm-lastitem-bkp', index);
        node.setData('fm-tabindex', true);
        node.setAttrs([
            {name: 'tabindex', value: '0'},
            {name: 'fm-lastitem', value: true}
        ], true);
    };

    resetLastValue = function(focusContainerNode) {
        var lastItemNodes = focusContainerNode.getAll('[fm-lastitem]');
        lastItemNodes.removeAttrs(['fm-lastitem', 'tabindex'], true)
                     .removeData('fm-tabindex');
        focusContainerNode.removeData('fm-lastitem-bkp');
    };

    searchFocusNode = function(initialNode, deeper) {
        console.log(NAME+'searchFocusNode');
        var focusContainerNode = initialNode.hasAttr('fm-manage') ? initialNode : initialNode.inside('[plugin-fm="true"]'),
            focusNode, alwaysDefault, selector, allFocusableNodes, index, parentContainerNode, parentSelector;

        if (focusContainerNode) {
            selector = getFocusManagerSelector(focusContainerNode);
            focusNode = initialNode.matches(selector) ? initialNode : initialNode.inside(selector);
            // focusNode can only be equal focusContainerNode when focusContainerNode lies with a focusnode itself with that particular selector:
            if (focusNode===focusContainerNode) {
                parentContainerNode = focusNode.inside('[plugin-fm="true"]');
                if (parentContainerNode) {
                    parentSelector = getFocusManagerSelector(parentContainerNode);
                    if (!focusNode.matches(parentSelector) || deeper) {
                        focusNode = null;
                    }
                }
                else {
                    focusNode = null;
                }
            }
            if (focusNode && focusContainerNode.contains(focusNode, true)) {
                markAsFocussed(parentContainerNode || focusContainerNode, focusNode);
            }
            else {
                // find the right node that should get focus
/*jshint boss:true */
                alwaysDefault = focusContainerNode._plugin.fm.model.alwaysdefault;
/*jshint boss:false */
                alwaysDefault && (focusNode=focusContainerNode.getElement('[fm-defaultitem="true"], [defaultitem="true"]')); // itags use attribute without `fm-`
                if (!focusNode) {
                    // search for last item
                    focusNode = focusContainerNode.getElement('[fm-lastitem="true"]');
                    if (!focusNode) {
                        // look at the lastitemindex of the focuscontainer
                        index = focusContainerNode.getData('fm-lastitem-bkp');
                        if (index!==undefined) {
                            allFocusableNodes = focusContainerNode.getAll(selector);
                            focusNode = allFocusableNodes[index];
                        }
                    }
                }
                // still not found and alwaysDefault was falsy: try the defualt node:
                !focusNode && !alwaysDefault && (focusNode=focusContainerNode.getElement('[fm-defaultitem="true"], [defaultitem="true"]')); // itags use attribute without `fm-`
                // still not found: try the first focussable node (which we might find inside `allFocusableNodes`:
                !focusNode && (focusNode = allFocusableNodes ? allFocusableNodes[0] : focusContainerNode.getElement(selector));
                if (focusNode) {
                    markAsFocussed(parentContainerNode || focusContainerNode, focusNode);
                }
                else {
                    focusNode = initialNode;
                }
            }
        }
        else {
            focusNode = initialNode;
        }
        return focusNode;
    };

    setupEvents = function() {

        Event.before('keydown', function(e) {
            console.log(NAME+'before keydown-event');
            var focusContainerNode,
                sourceNode = e.target,
                selector, keyCode, actionkey, focusNode, keys, len, lastIndex, specialKeysMatch, i, specialKey;

            focusContainerNode = sourceNode.inside('[plugin-fm="true"]');
            if (focusContainerNode) {
                // key was pressed inside a focusmanagable container
                selector = getFocusManagerSelector(focusContainerNode);
                keyCode = e.keyCode;

                // first check for keydown:
                actionkey = focusContainerNode._plugin.fm.model.keydown;
                focusNode = nextFocusNode(e, keyCode, actionkey, focusContainerNode, sourceNode, selector, true);
                if (!focusNode) {
                    // check for keyup:
                    actionkey = focusContainerNode._plugin.fm.model.keyup;
                    focusNode = nextFocusNode(e, keyCode, actionkey, focusContainerNode, sourceNode, selector);
                }
                if (!focusNode) {
                    // check for keyenter, but only when e.target equals a focusmanager:
                    if (sourceNode.matches('[plugin-fm="true"]')) {
                        actionkey = sourceNode._plugin.fm.model.keyenter;
                        if (actionkey) {
                            keys = actionkey.split('+');
                            len = keys.length;
                            lastIndex = len - 1;
                            // double == --> keyCode is number, keys is a string
                            if (keyCode==keys[lastIndex]) {
                                // posible keyup --> check if special characters match:
                                specialKeysMatch = true;
                                SPECIAL_KEYS.some(function(value) {
                                    specialKeysMatch = !e[value];
                                    return !specialKeysMatch;
                                });
                                for (i=lastIndex-1; (i>=0) && !specialKeysMatch; i--) {
                                    specialKey = keys[i].toLowerCase();
                                    specialKeysMatch = e[SPECIAL_KEYS[specialKey]];
                                }
                            }
                            if (specialKeysMatch) {
                                resetLastValue(sourceNode);
                                focusNode = searchFocusNode(sourceNode, true);
                            }
                        }
                    }
                }
                if (!focusNode) {
                    // check for keyleave:
                    actionkey = focusContainerNode._plugin.fm.model.keyleave;
                    if (actionkey) {
                        keys = actionkey.split('+');
                        len = keys.length;
                        lastIndex = len - 1;
                        // double == --> keyCode is number, keys is a string
                        if (keyCode==keys[lastIndex]) {
                            // posible keyup --> check if special characters match:
                            specialKeysMatch = true;
                            SPECIAL_KEYS.some(function(value) {
                                specialKeysMatch = !e[value];
                                return !specialKeysMatch;
                            });
                            for (i=lastIndex-1; (i>=0) && !specialKeysMatch; i--) {
                                specialKey = keys[i].toLowerCase();
                                specialKeysMatch = e[SPECIAL_KEYS[specialKey]];
                            }
                        }
                        if (specialKeysMatch) {
                            resetLastValue(focusContainerNode);
                            focusNode = focusContainerNode;
                        }
                    }
                }
                if (focusNode) {
                    e.preventDefaultContinue();
                    // prevent default action --> we just want to re-focus, but we DO want afterlisteners
                    // to be handled in the after-listener: someone else might want to halt the keydown event.
                    e._focusNode = focusNode;
                }
            }
        });

        Event.after('keydown', function(e) {
            console.log(NAME+'after keydown-event');
            var focusNode = e._focusNode;
            if (focusNode && focusNode.focus) {
                focusNode.focus();
            }
        });

        Event.after('focus', function(e) {
            console.log(NAME+'after focus-event');
            var node = e.target,
                body = DOCUMENT.body,
                cleanFocussedData = function(element, loop) {
                    if (element.removeData) {
                        do {
                            // we also need to set the appropriate nodeData, so that when the itags re-render,
                            // they don't reset this particular information
                            element.removeData(FOCUSSED);
                            element.removeClass(FOCUSSED, null, null, true);
                            element = (element===body) ? null : element.getParent();
                        } while (element && loop);
                    }
                };
            // first, unfocus currently focussed items and up the tree
            DOCUMENT.getAll('.'+FOCUSSED, true).forEach(cleanFocussedData);
            if (node && node.setClass) {
                do {
                    // we also need to set the appropriate nodeData, so that when the itags re-render,
                    // they don't reset this particular information
                    node.setData(FOCUSSED, true);
                    node.setClass(FOCUSSED, null, null, true);
                    node = (node===body) ? null : node.getParent();
                } while (node);
            }
        }, true); // set in front: we need to make use of the previous DOCUMENT._activeElement, before it gets updated by event-dom

        // focus-fix for keeping focus when a mouse gets down for a longer time
        Event.after('mousedown', function(e) {
            console.log(NAME+'after focus-event');
            var node = e.target;
            if (!node.hasFocus()) {
                node.focus();
            }
        }, 'button');

        Event.after('tap', function(e) {
            console.log(NAME+'after tap-event');
            var focusNode = e.target,
                focusContainerNode;
            if (e._noFocus) {
                return;
            }
            if (focusNode && focusNode.inside) {
                focusContainerNode = focusNode.hasAttr('plugin-fm') ? focusNode : focusNode.inside('[plugin-fm="true"]');
            }
            if (focusContainerNode) {
                if ((focusNode===focusContainerNode) || !focusNode.matches(getFocusManagerSelector(focusContainerNode))) {
                    focusNode = searchFocusNode(focusNode, true);
                }
                if (focusNode.hasFocus()) {
                    markAsFocussed(focusContainerNode, focusNode);
                }
                else {
                    focusNode.focus();
                }
            }
        }, null, null, true);

        Event.after(['keypress', 'mouseup', 'panup', 'mousedown', 'pandown'], function(e) {
            console.log(NAME+'after '+e.type+'-event');
            var focusContainerNode,
                sourceNode = e.target,
                selector;

            focusContainerNode = sourceNode.inside('[plugin-fm="true"]');
            if (focusContainerNode) {
                // key was pressed inside a focusmanagable container
                selector = getFocusManagerSelector(focusContainerNode);
                if (sourceNode.matches(selector)) {
                    sourceNode.setAttr(FM_SELECTION_START, sourceNode.selectionStart || '0', true)
                              .setAttr(FM_SELECTION_END, sourceNode.selectionEnd || '0', true);
                }
            }
        }, 'input[type="text"], textarea');

        Event.after('focus', function(e) {
            console.log(NAME+'after focus-event');
            var focusContainerNode,
                sourceNode = e.target,
                selector, selectionStart, selectionEnd;

            focusContainerNode = sourceNode.inside('[plugin-fm="true"]');
            if (focusContainerNode) {
                // key was pressed inside a focusmanagable container
                selector = getFocusManagerSelector(focusContainerNode);
                if (sourceNode.matches(selector)) {
                    // cautious: fm-selectionstart can be 0 --> which would lead into a falsy value
                    selectionStart = sourceNode.getAttr(FM_SELECTION_START);
                    (selectionStart===null) && (selectionStart=sourceNode.getValue().length);
                    selectionEnd = Math.max(sourceNode.getAttr(FM_SELECTION_END) || selectionStart, selectionStart);
                    sourceNode.selectionEnd = selectionEnd;
                    sourceNode.selectionStart = selectionStart;
                    markAsFocussed(focusContainerNode, sourceNode);
                }
            }
        }, 'input[type="text"], textarea');

    };

    setupEvents();

    window._ITSAmodules.FocusManager = FocusManager = DOCUMENT.definePlugin('fm', null, {
                attrs: {
                    manage: 'string',
                    alwaysdefault: 'boolean',
                    keyup: 'string',
                    keydown: 'string',
                    keyenter: 'string',
                    keyleave: 'string',
                    noloop: 'boolean'
                },
                defaults: {
                    manage: 'true',
                    alwaysdefault: false,
                    keyup: DEFAULT_KEYUP,
                    keydown: DEFAULT_KEYDOWN,
                    noloop: DEFAULT_NOLOOP
                }
            });

    defineFocusEvent = function(customevent) {
        Event.defineEvent(customevent)
             .defaultFn(function(e) {
                 var node = e.target,
                     leftScroll = window.getScrollLeft(),
                     topScroll = window.getScrollTop();
                 node._focus();
                 // reset winscroll:
                 window.scrollTo(leftScroll, topScroll);
                 // make sure the node is inside the viewport:
                 // node.forceIntoView();
             });
    };

    (function(HTMLElementPrototype) {

        HTMLElementPrototype._focus = HTMLElementPrototype.focus;
        HTMLElementPrototype.focus = function(noRefocus) {
            console.log(NAME+'focus');
            /**
             * In case of a manual focus (node.focus()) the node will fire an `manualfocus`-event
             * which can be prevented.
             * @event manualfocus
            */
            var focusElement = this,
                doEmit, focusContainerNode;
            doEmit = function(focusNode) {
                var emitterName = focusNode._emitterName,
                    customevent = emitterName+':manualfocus';
                Event._ce[customevent] || defineFocusEvent(customevent);
                focusNode.emit('manualfocus');
            };
            if (noRefocus) {
                doEmit(focusElement);
            }
            else {
                focusContainerNode = (this.getAttr('plugin-fm')==='true') ? focusElement : focusElement.inside('[plugin-fm="true"]');
                if (focusContainerNode) {
                    focusContainerNode.pluginReady('fm').then(
                        function() {
                            doEmit(searchFocusNode(focusElement));
                        }
                    );
                }
                else {
                    doEmit(focusElement);
                }
            }
        };

    }(window.HTMLElement.prototype));


    return FocusManager;
};