Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

YUI.add('node-focusmanager', function (Y, NAME) {

/**
* <p>The Focus Manager Node Plugin makes it easy to manage focus among
* a Node's descendants.  Primarily intended to help with widget development,
* the Focus Manager Node Plugin can be used to improve the keyboard
* accessibility of widgets.</p>
*
* <p>
* When designing widgets that manage a set of descendant controls (i.e. buttons
* in a toolbar, tabs in a tablist, menuitems in a menu, etc.) it is important to
* limit the number of descendants in the browser's default tab flow.  The fewer
* number of descendants in the default tab flow, the easier it is for keyboard
* users to navigate between widgets by pressing the tab key.  When a widget has
* focus it should provide a set of shortcut keys (typically the arrow keys)
* to move focus among its descendants.
* </p>
*
* <p>
* To this end, the Focus Manager Node Plugin makes it easy to define a Node's
* focusable descendants, define which descendant should be in the default tab
* flow, and define the keys that move focus among each descendant.
* Additionally, as the CSS
* <a href="http://www.w3.org/TR/CSS21/selector.html#x38"><code>:focus</code></a>
* pseudo class is not supported on all elements in all
* <a href="http://developer.yahoo.com/yui/articles/gbs/">A-Grade browsers</a>,
* the Focus Manager Node Plugin provides an easy, cross-browser means of
* styling focus.
* </p>
*

DEPRECATED: The FocusManager Node Plugin has been deprecated as of YUI 3.9.0. This module will be removed from the library in a future version. If you require functionality similar to the one provided by this  module, consider taking a look at the various modules in the YUI Gallery <http://yuilibrary.com/gallery/>.

* @module node-focusmanager
* @deprecated 3.9.0
*/

        //      Frequently used strings

var ACTIVE_DESCENDANT = "activeDescendant",
        ID = "id",
        DISABLED = "disabled",
        TAB_INDEX = "tabIndex",
        FOCUSED = "focused",
        FOCUS_CLASS = "focusClass",
        CIRCULAR = "circular",
        UI = "UI",
        KEY = "key",
        ACTIVE_DESCENDANT_CHANGE = ACTIVE_DESCENDANT + "Change",
        HOST = "host",

        //      Collection of keys that, when pressed, cause the browser viewport
        //      to scroll.
        scrollKeys = {
                37: true,
                38: true,
                39: true,
                40: true
        },

        clickableElements = {
                "a": true,
                "button": true,
                "input": true,
                "object": true
        },

        //      Library shortcuts

        Lang = Y.Lang,
        UA = Y.UA,

        /**
        * The NodeFocusManager class is a plugin for a Node instance.  The class is used
        * via the <a href="Node.html#method_plug"><code>plug</code></a> method of Node
        * and should not be instantiated directly.
        * @namespace plugin
        * @class NodeFocusManager
        */
        NodeFocusManager = function () {

                NodeFocusManager.superclass.constructor.apply(this, arguments);

        };


NodeFocusManager.ATTRS = {

        /**
        * Boolean indicating that one of the descendants is focused.
        *
        * @attribute focused
        * @readOnly
        * @default false
        * @type boolean
        */
        focused: {

                value: false,
                readOnly: true

        },


        /**
        * String representing the CSS selector used to define the descendant Nodes
        * whose focus should be managed.
        *
        * @attribute descendants
        * @type Y.NodeList
        */
        descendants: {

                getter: function (value) {

                        return this.get(HOST).all(value);

                }

        },


        /**
        * <p>Node, or index of the Node, representing the descendant that is either
        * focused or is focusable (<code>tabIndex</code> attribute is set to 0).
        * The value cannot represent a disabled descendant Node.  Use a value of -1
        * to remove all descendant Nodes from the default tab flow.
        * If no value is specified, the active descendant will be inferred using
        * the following criteria:</p>
        * <ol>
        * <li>Examining the <code>tabIndex</code> attribute of each descendant and
        * using the first descendant whose <code>tabIndex</code> attribute is set
        * to 0</li>
        * <li>If no default can be inferred then the value is set to either 0 or
        * the index of the first enabled descendant.</li>
        * </ol>
        *
        * @attribute activeDescendant
        * @type Number
        */
        activeDescendant: {

                setter: function (value) {

                        var isNumber = Lang.isNumber,
                                INVALID_VALUE = Y.Attribute.INVALID_VALUE,
                                descendantsMap = this._descendantsMap,
                                descendants = this._descendants,
                                nodeIndex,
                                returnValue,
                                oNode;


                        if (isNumber(value)) {
                                nodeIndex = value;
                                returnValue = nodeIndex;
                        }
                        else if ((value instanceof Y.Node) && descendantsMap) {

                                nodeIndex = descendantsMap[value.get(ID)];

                                if (isNumber(nodeIndex)) {
                                        returnValue = nodeIndex;
                                }
                                else {

                                        //      The user passed a reference to a Node that wasn't one
                                        //      of the descendants.
                                        returnValue = INVALID_VALUE;

                                }

                        }
                        else {
                                returnValue = INVALID_VALUE;
                        }


                        if (descendants) {

                                oNode = descendants.item(nodeIndex);

                                if (oNode && oNode.get("disabled")) {

                                        //      Setting the "activeDescendant" attribute to the index
                                        //      of a disabled descendant is invalid.
                                        returnValue = INVALID_VALUE;

                                }

                        }


                        return returnValue;

                }

        },


        /**
        * Object literal representing the keys to be used to navigate between the
        * next/previous descendant.  The format for the attribute's value is
        * <code>{ next: "down:40", previous: "down:38" }</code>.  The value for the
        * "next" and "previous" properties are used to attach
        * <a href="event/#keylistener"><code>key</code></a> event listeners. See
        * the <a href="event/#keylistener">Using the key Event</a> section of
        * the Event documentation for more information on "key" event listeners.
        *
        * @attribute keys
        * @type Object
        */
        keys: {

                value: {

                        next: null,
                        previous: null

                }


        },


        /**
        * String representing the name of class applied to the focused active
        * descendant Node.  Can also be an object literal used to define both the
        * class name, and the Node to which the class should be applied.  If using
        * an object literal, the format is:
        * <code>{ className: "focus", fn: myFunction }</code>.  The function
        * referenced by the <code>fn</code> property in the object literal will be
        * passed a reference to the currently focused active descendant Node.
        *
        * @attribute focusClass
        * @type String|Object
        */
        focusClass: { },


        /**
        * Boolean indicating if focus should be set to the first/last descendant
        * when the end or beginning of the descendants has been reached.
        *
        * @attribute circular
        * @type Boolean
        * @default true
        */
        circular: {
                value: true
        }

};

Y.extend(NodeFocusManager, Y.Plugin.Base, {

        //      Protected properties

        //      Boolean indicating if the NodeFocusManager is active.
        _stopped: true,

        //      NodeList representing the descendants selected via the
        //      "descendants" attribute.
        _descendants: null,

        //      Object literal mapping the IDs of each descendant to its index in the
        //      "_descendants" NodeList.
        _descendantsMap: null,

        //      Reference to the Node instance to which the focused class (defined
        //      by the "focusClass" attribute) is currently applied.
        _focusedNode: null,

        //      Number representing the index of the last descendant Node.
        _lastNodeIndex: 0,

        //      Array of handles for event handlers used for a NodeFocusManager instance.
        _eventHandlers: null,



        //      Protected methods

        /**
        * @method _initDescendants
        * @description Sets the <code>tabIndex</code> attribute of all of the
        * descendants to -1, except the active descendant, whose
        * <code>tabIndex</code> attribute is set to 0.
        * @protected
        */
        _initDescendants: function () {

                var descendants = this.get("descendants"),
                        descendantsMap = {},
                        nFirstEnabled = -1,
                        nDescendants,
                        nActiveDescendant = this.get(ACTIVE_DESCENDANT),
                        oNode,
                        sID,
                        i = 0;



                if (Lang.isUndefined(nActiveDescendant)) {
                        nActiveDescendant = -1;
                }


                if (descendants) {

                        nDescendants = descendants.size();


            for (i = 0; i < nDescendants; i++) {

                oNode = descendants.item(i);

                if (nFirstEnabled === -1 && !oNode.get(DISABLED)) {
                    nFirstEnabled = i;
                }


                //      If the user didn't specify a value for the
                //      "activeDescendant" attribute try to infer it from
                //      the markup.

                //      Need to pass "2" when using "getAttribute" for IE to get
                //      the attribute value as it is set in the markup.
                //      Need to use "parseInt" because IE always returns the
                //      value as a number, whereas all other browsers return
                //      the attribute as a string when accessed
                //      via "getAttribute".

                if (nActiveDescendant < 0 &&
                        parseInt(oNode.getAttribute(TAB_INDEX, 2), 10) === 0) {

                    nActiveDescendant = i;

                }

                if (oNode) {
                    oNode.set(TAB_INDEX, -1);
                }

                sID = oNode.get(ID);

                if (!sID) {
                    sID = Y.guid();
                    oNode.set(ID, sID);
                }

                descendantsMap[sID] = i;

            }


            //  If the user didn't specify a value for the
            //  "activeDescendant" attribute and no default value could be
            //  determined from the markup, then default to 0.

            if (nActiveDescendant < 0) {
                nActiveDescendant = 0;
            }


            oNode = descendants.item(nActiveDescendant);

            //  Check to make sure the active descendant isn't disabled,
            //  and fall back to the first enabled descendant if it is.

            if (!oNode || oNode.get(DISABLED)) {
                oNode = descendants.item(nFirstEnabled);
                nActiveDescendant = nFirstEnabled;
            }

            this._lastNodeIndex = nDescendants - 1;
            this._descendants = descendants;
            this._descendantsMap = descendantsMap;

            this.set(ACTIVE_DESCENDANT, nActiveDescendant);

            //  Need to set the "tabIndex" attribute here, since the
            //  "activeDescendantChange" event handler used to manage
            //  the setting of the "tabIndex" attribute isn't wired up yet.

            if (oNode) {
                oNode.set(TAB_INDEX, 0);
            }

                }

        },


        /**
        * @method _isDescendant
        * @description Determines if the specified Node instance is a descendant
        * managed by the Focus Manager.
        * @param node {Node} Node instance to be checked.
        * @return {Boolean} Boolean indicating if the specified Node instance is a
        * descendant managed by the Focus Manager.
        * @protected
        */
        _isDescendant: function (node) {

                return (node.get(ID) in this._descendantsMap);

        },


        /**
        * @method _removeFocusClass
        * @description Removes the class name representing focus (as specified by
        * the "focusClass" attribute) from the Node instance to which it is
        * currently applied.
        * @protected
        */
        _removeFocusClass: function () {

                var oFocusedNode = this._focusedNode,
                        focusClass = this.get(FOCUS_CLASS),
                        sClassName;

                if (focusClass) {
                        sClassName = Lang.isString(focusClass) ?
                                focusClass : focusClass.className;
                }

                if (oFocusedNode && sClassName) {
                        oFocusedNode.removeClass(sClassName);
                }

        },


        /**
        * @method _detachKeyHandler
        * @description Detaches the "key" event handlers used to support the "keys"
        * attribute.
        * @protected
        */
        _detachKeyHandler: function () {

                var prevKeyHandler = this._prevKeyHandler,
                        nextKeyHandler = this._nextKeyHandler;

                if (prevKeyHandler) {
                        prevKeyHandler.detach();
                }

                if (nextKeyHandler) {
                        nextKeyHandler.detach();
                }

        },


        /**
        * @method _preventScroll
        * @description Prevents the viewport from scolling when the user presses
        * the up, down, left, or right key.
        * @protected
        */
        _preventScroll: function (event) {

                if (scrollKeys[event.keyCode] && this._isDescendant(event.target)) {
                        event.preventDefault();
                }

        },


        /**
        * @method _fireClick
        * @description Fires the click event if the enter key is pressed while
        * focused on an HTML element that is not natively clickable.
        * @protected
        */
        _fireClick: function (event) {

                var oTarget = event.target,
                        sNodeName = oTarget.get("nodeName").toLowerCase();

                if (event.keyCode === 13 && (!clickableElements[sNodeName] ||
                                (sNodeName === "a" && !oTarget.getAttribute("href")))) {


                        oTarget.simulate("click");

                }

        },


        /**
        * @method _attachKeyHandler
        * @description Attaches the "key" event handlers used to support the "keys"
        * attribute.
        * @protected
        */
        _attachKeyHandler: function () {

                this._detachKeyHandler();

                var sNextKey = this.get("keys.next"),
                        sPrevKey = this.get("keys.previous"),
                        oNode = this.get(HOST),
                        aHandlers = this._eventHandlers;

                if (sPrevKey) {
                        this._prevKeyHandler =
                                Y.on(KEY, Y.bind(this._focusPrevious, this), oNode, sPrevKey);
                }

                if (sNextKey) {
                        this._nextKeyHandler =
                                Y.on(KEY, Y.bind(this._focusNext, this), oNode, sNextKey);
                }


                //      In Opera it is necessary to call the "preventDefault" method in
                //      response to the user pressing the arrow keys in order to prevent
                //      the viewport from scrolling when the user is moving focus among
                //      the focusable descendants.

                if (UA.opera) {
                        aHandlers.push(oNode.on("keypress", this._preventScroll, this));
                }


                //      For all browsers except Opera: HTML elements that are not natively
                //      focusable but made focusable via the tabIndex attribute don't
                //      fire a click event when the user presses the enter key.  It is
                //      possible to work around this problem by simplying dispatching a
                //      click event in response to the user pressing the enter key.

                if (!UA.opera) {
                        aHandlers.push(oNode.on("keypress", this._fireClick, this));
                }

        },


        /**
        * @method _detachEventHandlers
        * @description Detaches all event handlers used by the Focus Manager.
        * @protected
        */
        _detachEventHandlers: function () {

                this._detachKeyHandler();

                var aHandlers = this._eventHandlers;

                if (aHandlers) {

                        Y.Array.each(aHandlers, function (handle) {
                                handle.detach();
                        });

                        this._eventHandlers = null;

                }

        },


        /**
        * @method _detachEventHandlers
        * @description Attaches all event handlers used by the Focus Manager.
        * @protected
        */
        _attachEventHandlers: function () {

                var descendants = this._descendants,
                        aHandlers,
                        oDocument,
                        handle;

                if (descendants && descendants.size()) {

                        aHandlers = this._eventHandlers || [];
                        oDocument = this.get(HOST).get("ownerDocument");


                        if (aHandlers.length === 0) {


                                aHandlers.push(oDocument.on("focus", this._onDocFocus, this));

                                aHandlers.push(oDocument.on("mousedown",
                                        this._onDocMouseDown, this));

                                aHandlers.push(
                                                this.after("keysChange", this._attachKeyHandler));

                                aHandlers.push(
                                                this.after("descendantsChange", this._initDescendants));

                                aHandlers.push(
                                                this.after(ACTIVE_DESCENDANT_CHANGE,
                                                                this._afterActiveDescendantChange));


                                //      For performance: defer attaching all key-related event
                                //      handlers until the first time one of the specified
                                //      descendants receives focus.

                                handle = this.after("focusedChange", Y.bind(function (event) {

                                        if (event.newVal) {


                                                this._attachKeyHandler();

                                                //      Detach this "focusedChange" handler so that the
                                                //      key-related handlers only get attached once.

                                                handle.detach();

                                        }

                                }, this));

                                aHandlers.push(handle);

                        }


                        this._eventHandlers = aHandlers;

                }

        },


        //      Protected event handlers

        /**
        * @method _onDocMouseDown
        * @description "mousedown" event handler for the owner document of the
        * Focus Manager's Node.
        * @protected
        * @param event {Object} Object representing the DOM event.
        */
        _onDocMouseDown: function (event) {

                var oHost = this.get(HOST),
                        oTarget = event.target,
                        bChildNode = oHost.contains(oTarget),
                        node,

                        getFocusable = function (node) {

                                var returnVal = false;

                                if (!node.compareTo(oHost)) {

                                        returnVal = this._isDescendant(node) ? node :
                                                                        getFocusable.call(this, node.get("parentNode"));

                                }

                                return returnVal;

                        };


                if (bChildNode) {

                        //      Check to make sure that the target isn't a child node of one
                        //      of the focusable descendants.

                        node = getFocusable.call(this, oTarget);

                        if (node) {
                                oTarget = node;
                        }
                        else if (!node && this.get(FOCUSED)) {

                                //      The target was a non-focusable descendant of the root
                                //      node, so the "focused" attribute should be set to false.

                                this._set(FOCUSED, false);
                                this._onDocFocus(event);

                        }

                }


                if (bChildNode && this._isDescendant(oTarget)) {

                        //      Fix general problem in Webkit: mousing down on a button or an
                        //      anchor element doesn't focus it.

                        //      For all browsers: makes sure that the descendant that
                        //      was the target of the mousedown event is now considered the
                        //      active descendant.

                        this.focus(oTarget);
                }
                else if (UA.webkit && this.get(FOCUSED) &&
                        (!bChildNode || (bChildNode && !this._isDescendant(oTarget)))) {

                        //      Fix for Webkit:

                        //      Document doesn't receive focus in Webkit when the user mouses
                        //      down on it, so the "focused" attribute won't get set to the
                        //      correct value.

                        //      The goal is to force a blur if the user moused down on
                        //      either: 1) A descendant node, but not one that managed by
                        //      the FocusManager, or 2) an element outside of the
                        //      FocusManager

                        this._set(FOCUSED, false);
                        this._onDocFocus(event);

                }

        },


        /**
        * @method _onDocFocus
        * @description "focus" event handler for the owner document of the
        * Focus Manager's Node.
        * @protected
        * @param event {Object} Object representing the DOM event.
        */
        _onDocFocus: function (event) {

                var oTarget = this._focusTarget || event.target,
                        bFocused = this.get(FOCUSED),
                        focusClass = this.get(FOCUS_CLASS),
                        oFocusedNode = this._focusedNode,
                        bInCollection;

                if (this._focusTarget) {
                        this._focusTarget = null;
                }


                if (this.get(HOST).contains(oTarget)) {

                        //      The target is a descendant of the root Node.

                        bInCollection = this._isDescendant(oTarget);

                        if (!bFocused && bInCollection) {

                                //      The user has focused a focusable descendant.

                                bFocused = true;

                        }
                        else if (bFocused && !bInCollection) {

                                //      The user has focused a child of the root Node that is
                                //      not one of the descendants managed by this Focus Manager
                                //      so clear the currently focused descendant.

                                bFocused = false;

                        }

                }
                else {

                        // The target is some other node in the document.

                        bFocused = false;

                }


                if (focusClass) {

                        if (oFocusedNode && (!oFocusedNode.compareTo(oTarget) || !bFocused)) {
                                this._removeFocusClass();
                        }

                        if (bInCollection && bFocused) {

                                if (focusClass.fn) {
                                        oTarget = focusClass.fn(oTarget);
                                        oTarget.addClass(focusClass.className);
                                }
                                else {
                                        oTarget.addClass(focusClass);
                                }

                                this._focusedNode = oTarget;

                        }

                }


                this._set(FOCUSED, bFocused);

        },


        /**
        * @method _focusNext
        * @description Keydown event handler that moves focus to the next
        * enabled descendant.
        * @protected
        * @param event {Object} Object representing the DOM event.
        * @param activeDescendant {Number} Number representing the index of the
        * next descendant to be focused
        */
        _focusNext: function (event, activeDescendant) {

                var nActiveDescendant = activeDescendant || this.get(ACTIVE_DESCENDANT),
                        oNode;


                if (this._isDescendant(event.target) &&
                        (nActiveDescendant <= this._lastNodeIndex)) {

                        nActiveDescendant = nActiveDescendant + 1;

                        if (nActiveDescendant === (this._lastNodeIndex + 1) &&
                                this.get(CIRCULAR)) {

                                nActiveDescendant = 0;

                        }

                        oNode = this._descendants.item(nActiveDescendant);

            if (oNode) {

                if (oNode.get("disabled")) {
                    this._focusNext(event, nActiveDescendant);
                }
                else {
                    this.focus(nActiveDescendant);
                }

            }

                }

                this._preventScroll(event);

        },


        /**
        * @method _focusPrevious
        * @description Keydown event handler that moves focus to the previous
        * enabled descendant.
        * @protected
        * @param event {Object} Object representing the DOM event.
        * @param activeDescendant {Number} Number representing the index of the
        * next descendant to be focused.
        */
        _focusPrevious: function (event, activeDescendant) {

                var nActiveDescendant = activeDescendant || this.get(ACTIVE_DESCENDANT),
                        oNode;

                if (this._isDescendant(event.target) && nActiveDescendant >= 0) {

                        nActiveDescendant = nActiveDescendant - 1;

                        if (nActiveDescendant === -1 && this.get(CIRCULAR)) {
                                nActiveDescendant = this._lastNodeIndex;
                        }

            oNode = this._descendants.item(nActiveDescendant);

            if (oNode) {

                if (oNode.get("disabled")) {
                    this._focusPrevious(event, nActiveDescendant);
                }
                else {
                    this.focus(nActiveDescendant);
                }

            }

                }

                this._preventScroll(event);

        },


        /**
        * @method _afterActiveDescendantChange
        * @description afterChange event handler for the
        * "activeDescendant" attribute.
        * @protected
        * @param event {Object} Object representing the change event.
        */
        _afterActiveDescendantChange: function (event) {

                var oNode = this._descendants.item(event.prevVal);

                if (oNode) {
                        oNode.set(TAB_INDEX, -1);
                }

                oNode = this._descendants.item(event.newVal);

                if (oNode) {
                        oNode.set(TAB_INDEX, 0);
                }

        },



        //      Public methods

    initializer: function (config) {
                this.start();

    },

        destructor: function () {

                this.stop();
                this.get(HOST).focusManager = null;

    },


        /**
        * @method focus
        * @description Focuses the active descendant and sets the
        * <code>focused</code> attribute to true.
        * @param index {Number|Node} Optional. Number representing the index of the
        * descendant to be set as the active descendant or Node instance
        * representing the descendant to be set as the active descendant.
        */
        focus: function (index) {

                if (Lang.isUndefined(index)) {
                        index = this.get(ACTIVE_DESCENDANT);
                }

                this.set(ACTIVE_DESCENDANT, index, { src: UI });

                var oNode = this._descendants.item(this.get(ACTIVE_DESCENDANT));

                if (oNode) {

                        oNode.focus();

                        //      In Opera focusing a <BUTTON> element programmatically
                        //      will result in the document-level focus event handler
                        //      "_onDocFocus" being called, resulting in the handler
                        //      incorrectly setting the "focused" Attribute to false.  To fix
                        //      this, set a flag ("_focusTarget") that the "_onDocFocus" method
                        //      can look for to properly handle this edge case.

                        if (UA.opera && oNode.get("nodeName").toLowerCase() === "button") {
                                this._focusTarget = oNode;
                        }

                }

        },


        /**
        * @method blur
        * @description Blurs the current active descendant and sets the
        * <code>focused</code> attribute to false.
        */
        blur: function () {

                var oNode;

                if (this.get(FOCUSED)) {

                        oNode = this._descendants.item(this.get(ACTIVE_DESCENDANT));

                        if (oNode) {

                                oNode.blur();

                                //      For Opera and Webkit:  Blurring an element in either browser
                                //      doesn't result in another element (such as the document)
                                //      being focused.  Therefore, the "_onDocFocus" method
                                //      responsible for managing the application and removal of the
                                //      focus indicator class name is never called.

                                this._removeFocusClass();

                        }

                        this._set(FOCUSED, false, { src: UI });
                }

        },


        /**
        * @method start
        * @description Enables the Focus Manager.
        */
        start: function () {

                if (this._stopped) {

                        this._initDescendants();
                        this._attachEventHandlers();

                        this._stopped = false;

                }

        },


        /**
        * @method stop
        * @description Disables the Focus Manager by detaching all event handlers.
        */
        stop: function () {

                if (!this._stopped) {

                        this._detachEventHandlers();

                        this._descendants = null;
                        this._focusedNode = null;
                        this._lastNodeIndex = 0;
                        this._stopped = true;

                }

        },


        /**
        * @method refresh
        * @description Refreshes the Focus Manager's descendants by re-executing the
        * CSS selector query specified by the <code>descendants</code> attribute.
        */
        refresh: function () {

                this._initDescendants();

                if (!this._eventHandlers) {
                        this._attachEventHandlers();
                }

        }

});


NodeFocusManager.NAME = "nodeFocusManager";
NodeFocusManager.NS = "focusManager";

Y.namespace("Plugin");
Y.Plugin.NodeFocusManager = NodeFocusManager;


}, '3.18.1', {"requires": ["attribute", "node", "plugin", "node-event-simulate", "event-key", "event-focus"]});