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 stringsvar 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 shortcutsLang = 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")))) {Y.log(("Firing click event for node:" + oTarget.get("id")), "info", "nodeFocusManager");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) {Y.log("Attaching base set of event handlers.", "info", "nodeFocusManager");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) {Y.log("Attaching key event handlers.", "info", "nodeFocusManager");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// FocusManagerthis._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 methodsinitializer: function (config) {Y.log("WARNING: node-focusmanager is a deprecated module as of YUI 3.9.0. This module will be removed from a later version of the library.", "warn");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"]});