API Docs for: 3.10.3
Show:

File: node-focusmanager/js/node-focusmanager.js

  1. /**
  2. * <p>The Focus Manager Node Plugin makes it easy to manage focus among
  3. * a Node's descendants. Primarily intended to help with widget development,
  4. * the Focus Manager Node Plugin can be used to improve the keyboard
  5. * accessibility of widgets.</p>
  6. *
  7. * <p>
  8. * When designing widgets that manage a set of descendant controls (i.e. buttons
  9. * in a toolbar, tabs in a tablist, menuitems in a menu, etc.) it is important to
  10. * limit the number of descendants in the browser's default tab flow. The fewer
  11. * number of descendants in the default tab flow, the easier it is for keyboard
  12. * users to navigate between widgets by pressing the tab key. When a widget has
  13. * focus it should provide a set of shortcut keys (typically the arrow keys)
  14. * to move focus among its descendants.
  15. * </p>
  16. *
  17. * <p>
  18. * To this end, the Focus Manager Node Plugin makes it easy to define a Node's
  19. * focusable descendants, define which descendant should be in the default tab
  20. * flow, and define the keys that move focus among each descendant.
  21. * Additionally, as the CSS
  22. * <a href="http://www.w3.org/TR/CSS21/selector.html#x38"><code>:focus</code></a>
  23. * pseudo class is not supported on all elements in all
  24. * <a href="http://developer.yahoo.com/yui/articles/gbs/">A-Grade browsers</a>,
  25. * the Focus Manager Node Plugin provides an easy, cross-browser means of
  26. * styling focus.
  27. * </p>
  28. *
  29.  
  30. 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/>.
  31.  
  32. * @module node-focusmanager
  33. * @deprecated 3.9.0
  34. */
  35.  
  36. // Frequently used strings
  37.  
  38. var ACTIVE_DESCENDANT = "activeDescendant",
  39. ID = "id",
  40. DISABLED = "disabled",
  41. TAB_INDEX = "tabIndex",
  42. FOCUSED = "focused",
  43. FOCUS_CLASS = "focusClass",
  44. CIRCULAR = "circular",
  45. UI = "UI",
  46. KEY = "key",
  47. ACTIVE_DESCENDANT_CHANGE = ACTIVE_DESCENDANT + "Change",
  48. HOST = "host",
  49.  
  50. // Collection of keys that, when pressed, cause the browser viewport
  51. // to scroll.
  52. scrollKeys = {
  53. 37: true,
  54. 38: true,
  55. 39: true,
  56. 40: true
  57. },
  58.  
  59. clickableElements = {
  60. "a": true,
  61. "button": true,
  62. "input": true,
  63. "object": true
  64. },
  65.  
  66. // Library shortcuts
  67.  
  68. Lang = Y.Lang,
  69. UA = Y.UA,
  70.  
  71. /**
  72. * The NodeFocusManager class is a plugin for a Node instance. The class is used
  73. * via the <a href="Node.html#method_plug"><code>plug</code></a> method of Node
  74. * and should not be instantiated directly.
  75. * @namespace plugin
  76. * @class NodeFocusManager
  77. */
  78. NodeFocusManager = function () {
  79.  
  80. NodeFocusManager.superclass.constructor.apply(this, arguments);
  81.  
  82. };
  83.  
  84.  
  85. NodeFocusManager.ATTRS = {
  86.  
  87. /**
  88. * Boolean indicating that one of the descendants is focused.
  89. *
  90. * @attribute focused
  91. * @readOnly
  92. * @default false
  93. * @type boolean
  94. */
  95. focused: {
  96.  
  97. value: false,
  98. readOnly: true
  99.  
  100. },
  101.  
  102.  
  103. /**
  104. * String representing the CSS selector used to define the descendant Nodes
  105. * whose focus should be managed.
  106. *
  107. * @attribute descendants
  108. * @type Y.NodeList
  109. */
  110. descendants: {
  111.  
  112. getter: function (value) {
  113. return this.get(HOST).all(value);
  114.  
  115. }
  116.  
  117. },
  118.  
  119.  
  120. /**
  121. * <p>Node, or index of the Node, representing the descendant that is either
  122. * focused or is focusable (<code>tabIndex</code> attribute is set to 0).
  123. * The value cannot represent a disabled descendant Node. Use a value of -1
  124. * to remove all descendant Nodes from the default tab flow.
  125. * If no value is specified, the active descendant will be inferred using
  126. * the following criteria:</p>
  127. * <ol>
  128. * <li>Examining the <code>tabIndex</code> attribute of each descendant and
  129. * using the first descendant whose <code>tabIndex</code> attribute is set
  130. * to 0</li>
  131. * <li>If no default can be inferred then the value is set to either 0 or
  132. * the index of the first enabled descendant.</li>
  133. * </ol>
  134. *
  135. * @attribute activeDescendant
  136. * @type Number
  137. */
  138. activeDescendant: {
  139.  
  140. setter: function (value) {
  141.  
  142. var isNumber = Lang.isNumber,
  143. INVALID_VALUE = Y.Attribute.INVALID_VALUE,
  144. descendantsMap = this._descendantsMap,
  145. descendants = this._descendants,
  146. nodeIndex,
  147. returnValue,
  148. oNode;
  149.  
  150.  
  151. if (isNumber(value)) {
  152. nodeIndex = value;
  153. returnValue = nodeIndex;
  154. }
  155. else if ((value instanceof Y.Node) && descendantsMap) {
  156.  
  157. nodeIndex = descendantsMap[value.get(ID)];
  158.  
  159. if (isNumber(nodeIndex)) {
  160. returnValue = nodeIndex;
  161. }
  162. else {
  163.  
  164. // The user passed a reference to a Node that wasn't one
  165. // of the descendants.
  166. returnValue = INVALID_VALUE;
  167.  
  168. }
  169.  
  170. }
  171. else {
  172. returnValue = INVALID_VALUE;
  173. }
  174.  
  175.  
  176. if (descendants) {
  177.  
  178. oNode = descendants.item(nodeIndex);
  179.  
  180. if (oNode && oNode.get("disabled")) {
  181.  
  182. // Setting the "activeDescendant" attribute to the index
  183. // of a disabled descendant is invalid.
  184. returnValue = INVALID_VALUE;
  185.  
  186. }
  187.  
  188. }
  189.  
  190. return returnValue;
  191.  
  192. }
  193.  
  194. },
  195.  
  196.  
  197. /**
  198. * Object literal representing the keys to be used to navigate between the
  199. * next/previous descendant. The format for the attribute's value is
  200. * <code>{ next: "down:40", previous: "down:38" }</code>. The value for the
  201. * "next" and "previous" properties are used to attach
  202. * <a href="event/#keylistener"><code>key</code></a> event listeners. See
  203. * the <a href="event/#keylistener">Using the key Event</a> section of
  204. * the Event documentation for more information on "key" event listeners.
  205. *
  206. * @attribute keys
  207. * @type Object
  208. */
  209. keys: {
  210.  
  211. value: {
  212.  
  213. next: null,
  214. previous: null
  215.  
  216. }
  217.  
  218.  
  219. },
  220.  
  221.  
  222. /**
  223. * String representing the name of class applied to the focused active
  224. * descendant Node. Can also be an object literal used to define both the
  225. * class name, and the Node to which the class should be applied. If using
  226. * an object literal, the format is:
  227. * <code>{ className: "focus", fn: myFunction }</code>. The function
  228. * referenced by the <code>fn</code> property in the object literal will be
  229. * passed a reference to the currently focused active descendant Node.
  230. *
  231. * @attribute focusClass
  232. * @type String|Object
  233. */
  234. focusClass: { },
  235.  
  236.  
  237. /**
  238. * Boolean indicating if focus should be set to the first/last descendant
  239. * when the end or beginning of the descendants has been reached.
  240. *
  241. * @attribute circular
  242. * @type Boolean
  243. * @default true
  244. */
  245. circular: {
  246. value: true
  247. }
  248.  
  249. };
  250.  
  251. Y.extend(NodeFocusManager, Y.Plugin.Base, {
  252.  
  253. // Protected properties
  254.  
  255. // Boolean indicating if the NodeFocusManager is active.
  256. _stopped: true,
  257.  
  258. // NodeList representing the descendants selected via the
  259. // "descendants" attribute.
  260. _descendants: null,
  261.  
  262. // Object literal mapping the IDs of each descendant to its index in the
  263. // "_descendants" NodeList.
  264. _descendantsMap: null,
  265.  
  266. // Reference to the Node instance to which the focused class (defined
  267. // by the "focusClass" attribute) is currently applied.
  268. _focusedNode: null,
  269.  
  270. // Number representing the index of the last descendant Node.
  271. _lastNodeIndex: 0,
  272.  
  273. // Array of handles for event handlers used for a NodeFocusManager instance.
  274. _eventHandlers: null,
  275.  
  276.  
  277.  
  278. // Protected methods
  279.  
  280. /**
  281. * @method _initDescendants
  282. * @description Sets the <code>tabIndex</code> attribute of all of the
  283. * descendants to -1, except the active descendant, whose
  284. * <code>tabIndex</code> attribute is set to 0.
  285. * @protected
  286. */
  287. _initDescendants: function () {
  288.  
  289. var descendants = this.get("descendants"),
  290. descendantsMap = {},
  291. nFirstEnabled = -1,
  292. nDescendants,
  293. nActiveDescendant = this.get(ACTIVE_DESCENDANT),
  294. oNode,
  295. sID,
  296. i = 0;
  297.  
  298.  
  299.  
  300. if (Lang.isUndefined(nActiveDescendant)) {
  301. nActiveDescendant = -1;
  302. }
  303.  
  304.  
  305. if (descendants) {
  306.  
  307. nDescendants = descendants.size();
  308.  
  309.  
  310. for (i = 0; i < nDescendants; i++) {
  311.  
  312. oNode = descendants.item(i);
  313.  
  314. if (nFirstEnabled === -1 && !oNode.get(DISABLED)) {
  315. nFirstEnabled = i;
  316. }
  317.  
  318.  
  319. // If the user didn't specify a value for the
  320. // "activeDescendant" attribute try to infer it from
  321. // the markup.
  322.  
  323. // Need to pass "2" when using "getAttribute" for IE to get
  324. // the attribute value as it is set in the markup.
  325. // Need to use "parseInt" because IE always returns the
  326. // value as a number, whereas all other browsers return
  327. // the attribute as a string when accessed
  328. // via "getAttribute".
  329.  
  330. if (nActiveDescendant < 0 &&
  331. parseInt(oNode.getAttribute(TAB_INDEX, 2), 10) === 0) {
  332.  
  333. nActiveDescendant = i;
  334.  
  335. }
  336.  
  337. if (oNode) {
  338. oNode.set(TAB_INDEX, -1);
  339. }
  340.  
  341. sID = oNode.get(ID);
  342.  
  343. if (!sID) {
  344. sID = Y.guid();
  345. oNode.set(ID, sID);
  346. }
  347.  
  348. descendantsMap[sID] = i;
  349.  
  350. }
  351.  
  352.  
  353. // If the user didn't specify a value for the
  354. // "activeDescendant" attribute and no default value could be
  355. // determined from the markup, then default to 0.
  356.  
  357. if (nActiveDescendant < 0) {
  358. nActiveDescendant = 0;
  359. }
  360.  
  361.  
  362. oNode = descendants.item(nActiveDescendant);
  363.  
  364. // Check to make sure the active descendant isn't disabled,
  365. // and fall back to the first enabled descendant if it is.
  366.  
  367. if (!oNode || oNode.get(DISABLED)) {
  368. oNode = descendants.item(nFirstEnabled);
  369. nActiveDescendant = nFirstEnabled;
  370. }
  371.  
  372. this._lastNodeIndex = nDescendants - 1;
  373. this._descendants = descendants;
  374. this._descendantsMap = descendantsMap;
  375.  
  376. this.set(ACTIVE_DESCENDANT, nActiveDescendant);
  377.  
  378. // Need to set the "tabIndex" attribute here, since the
  379. // "activeDescendantChange" event handler used to manage
  380. // the setting of the "tabIndex" attribute isn't wired up yet.
  381.  
  382. if (oNode) {
  383. oNode.set(TAB_INDEX, 0);
  384. }
  385.  
  386. }
  387.  
  388. },
  389.  
  390.  
  391. /**
  392. * @method _isDescendant
  393. * @description Determines if the specified Node instance is a descendant
  394. * managed by the Focus Manager.
  395. * @param node {Node} Node instance to be checked.
  396. * @return {Boolean} Boolean indicating if the specified Node instance is a
  397. * descendant managed by the Focus Manager.
  398. * @protected
  399. */
  400. _isDescendant: function (node) {
  401.  
  402. return (node.get(ID) in this._descendantsMap);
  403.  
  404. },
  405.  
  406.  
  407. /**
  408. * @method _removeFocusClass
  409. * @description Removes the class name representing focus (as specified by
  410. * the "focusClass" attribute) from the Node instance to which it is
  411. * currently applied.
  412. * @protected
  413. */
  414. _removeFocusClass: function () {
  415.  
  416. var oFocusedNode = this._focusedNode,
  417. focusClass = this.get(FOCUS_CLASS),
  418. sClassName;
  419.  
  420. if (focusClass) {
  421. sClassName = Lang.isString(focusClass) ?
  422. focusClass : focusClass.className;
  423. }
  424.  
  425. if (oFocusedNode && sClassName) {
  426. oFocusedNode.removeClass(sClassName);
  427. }
  428.  
  429. },
  430.  
  431.  
  432. /**
  433. * @method _detachKeyHandler
  434. * @description Detaches the "key" event handlers used to support the "keys"
  435. * attribute.
  436. * @protected
  437. */
  438. _detachKeyHandler: function () {
  439.  
  440. var prevKeyHandler = this._prevKeyHandler,
  441. nextKeyHandler = this._nextKeyHandler;
  442.  
  443. if (prevKeyHandler) {
  444. prevKeyHandler.detach();
  445. }
  446.  
  447. if (nextKeyHandler) {
  448. nextKeyHandler.detach();
  449. }
  450.  
  451. },
  452.  
  453.  
  454. /**
  455. * @method _preventScroll
  456. * @description Prevents the viewport from scolling when the user presses
  457. * the up, down, left, or right key.
  458. * @protected
  459. */
  460. _preventScroll: function (event) {
  461.  
  462. if (scrollKeys[event.keyCode] && this._isDescendant(event.target)) {
  463. event.preventDefault();
  464. }
  465.  
  466. },
  467.  
  468.  
  469. /**
  470. * @method _fireClick
  471. * @description Fires the click event if the enter key is pressed while
  472. * focused on an HTML element that is not natively clickable.
  473. * @protected
  474. */
  475. _fireClick: function (event) {
  476.  
  477. var oTarget = event.target,
  478. sNodeName = oTarget.get("nodeName").toLowerCase();
  479.  
  480. if (event.keyCode === 13 && (!clickableElements[sNodeName] ||
  481. (sNodeName === "a" && !oTarget.getAttribute("href")))) {
  482.  
  483. Y.log(("Firing click event for node:" + oTarget.get("id")), "info", "nodeFocusManager");
  484.  
  485. oTarget.simulate("click");
  486.  
  487. }
  488.  
  489. },
  490.  
  491.  
  492. /**
  493. * @method _attachKeyHandler
  494. * @description Attaches the "key" event handlers used to support the "keys"
  495. * attribute.
  496. * @protected
  497. */
  498. _attachKeyHandler: function () {
  499.  
  500. this._detachKeyHandler();
  501.  
  502. var sNextKey = this.get("keys.next"),
  503. sPrevKey = this.get("keys.previous"),
  504. oNode = this.get(HOST),
  505. aHandlers = this._eventHandlers;
  506.  
  507. if (sPrevKey) {
  508. this._prevKeyHandler =
  509. Y.on(KEY, Y.bind(this._focusPrevious, this), oNode, sPrevKey);
  510. }
  511.  
  512. if (sNextKey) {
  513. this._nextKeyHandler =
  514. Y.on(KEY, Y.bind(this._focusNext, this), oNode, sNextKey);
  515. }
  516.  
  517.  
  518. // In Opera it is necessary to call the "preventDefault" method in
  519. // response to the user pressing the arrow keys in order to prevent
  520. // the viewport from scrolling when the user is moving focus among
  521. // the focusable descendants.
  522.  
  523. if (UA.opera) {
  524. aHandlers.push(oNode.on("keypress", this._preventScroll, this));
  525. }
  526.  
  527.  
  528. // For all browsers except Opera: HTML elements that are not natively
  529. // focusable but made focusable via the tabIndex attribute don't
  530. // fire a click event when the user presses the enter key. It is
  531. // possible to work around this problem by simplying dispatching a
  532. // click event in response to the user pressing the enter key.
  533.  
  534. if (!UA.opera) {
  535. aHandlers.push(oNode.on("keypress", this._fireClick, this));
  536. }
  537.  
  538. },
  539.  
  540.  
  541. /**
  542. * @method _detachEventHandlers
  543. * @description Detaches all event handlers used by the Focus Manager.
  544. * @protected
  545. */
  546. _detachEventHandlers: function () {
  547.  
  548. this._detachKeyHandler();
  549.  
  550. var aHandlers = this._eventHandlers;
  551.  
  552. if (aHandlers) {
  553.  
  554. Y.Array.each(aHandlers, function (handle) {
  555. handle.detach();
  556. });
  557.  
  558. this._eventHandlers = null;
  559.  
  560. }
  561.  
  562. },
  563.  
  564.  
  565. /**
  566. * @method _detachEventHandlers
  567. * @description Attaches all event handlers used by the Focus Manager.
  568. * @protected
  569. */
  570. _attachEventHandlers: function () {
  571.  
  572. var descendants = this._descendants,
  573. aHandlers,
  574. oDocument,
  575. handle;
  576.  
  577. if (descendants && descendants.size()) {
  578.  
  579. aHandlers = this._eventHandlers || [];
  580. oDocument = this.get(HOST).get("ownerDocument");
  581.  
  582.  
  583. if (aHandlers.length === 0) {
  584.  
  585. Y.log("Attaching base set of event handlers.", "info", "nodeFocusManager");
  586.  
  587. aHandlers.push(oDocument.on("focus", this._onDocFocus, this));
  588.  
  589. aHandlers.push(oDocument.on("mousedown",
  590. this._onDocMouseDown, this));
  591.  
  592. aHandlers.push(
  593. this.after("keysChange", this._attachKeyHandler));
  594.  
  595. aHandlers.push(
  596. this.after("descendantsChange", this._initDescendants));
  597.  
  598. aHandlers.push(
  599. this.after(ACTIVE_DESCENDANT_CHANGE,
  600. this._afterActiveDescendantChange));
  601.  
  602.  
  603. // For performance: defer attaching all key-related event
  604. // handlers until the first time one of the specified
  605. // descendants receives focus.
  606.  
  607. handle = this.after("focusedChange", Y.bind(function (event) {
  608.  
  609. if (event.newVal) {
  610.  
  611. Y.log("Attaching key event handlers.", "info", "nodeFocusManager");
  612.  
  613. this._attachKeyHandler();
  614.  
  615. // Detach this "focusedChange" handler so that the
  616. // key-related handlers only get attached once.
  617.  
  618. handle.detach();
  619.  
  620. }
  621.  
  622. }, this));
  623.  
  624. aHandlers.push(handle);
  625.  
  626. }
  627.  
  628.  
  629. this._eventHandlers = aHandlers;
  630.  
  631. }
  632.  
  633. },
  634.  
  635.  
  636. // Protected event handlers
  637.  
  638. /**
  639. * @method _onDocMouseDown
  640. * @description "mousedown" event handler for the owner document of the
  641. * Focus Manager's Node.
  642. * @protected
  643. * @param event {Object} Object representing the DOM event.
  644. */
  645. _onDocMouseDown: function (event) {
  646.  
  647. var oHost = this.get(HOST),
  648. oTarget = event.target,
  649. bChildNode = oHost.contains(oTarget),
  650. node,
  651.  
  652. getFocusable = function (node) {
  653.  
  654. var returnVal = false;
  655.  
  656. if (!node.compareTo(oHost)) {
  657.  
  658. returnVal = this._isDescendant(node) ? node :
  659. getFocusable.call(this, node.get("parentNode"));
  660.  
  661. }
  662.  
  663. return returnVal;
  664.  
  665. };
  666.  
  667.  
  668. if (bChildNode) {
  669.  
  670. // Check to make sure that the target isn't a child node of one
  671. // of the focusable descendants.
  672.  
  673. node = getFocusable.call(this, oTarget);
  674.  
  675. if (node) {
  676. oTarget = node;
  677. }
  678. else if (!node && this.get(FOCUSED)) {
  679.  
  680. // The target was a non-focusable descendant of the root
  681. // node, so the "focused" attribute should be set to false.
  682.  
  683. this._set(FOCUSED, false);
  684. this._onDocFocus(event);
  685.  
  686. }
  687.  
  688. }
  689.  
  690.  
  691. if (bChildNode && this._isDescendant(oTarget)) {
  692.  
  693. // Fix general problem in Webkit: mousing down on a button or an
  694. // anchor element doesn't focus it.
  695.  
  696. // For all browsers: makes sure that the descendant that
  697. // was the target of the mousedown event is now considered the
  698. // active descendant.
  699.  
  700. this.focus(oTarget);
  701. }
  702. else if (UA.webkit && this.get(FOCUSED) &&
  703. (!bChildNode || (bChildNode && !this._isDescendant(oTarget)))) {
  704.  
  705. // Fix for Webkit:
  706.  
  707. // Document doesn't receive focus in Webkit when the user mouses
  708. // down on it, so the "focused" attribute won't get set to the
  709. // correct value.
  710.  
  711. // The goal is to force a blur if the user moused down on
  712. // either: 1) A descendant node, but not one that managed by
  713. // the FocusManager, or 2) an element outside of the
  714. // FocusManager
  715.  
  716. this._set(FOCUSED, false);
  717. this._onDocFocus(event);
  718.  
  719. }
  720.  
  721. },
  722.  
  723.  
  724. /**
  725. * @method _onDocFocus
  726. * @description "focus" event handler for the owner document of the
  727. * Focus Manager's Node.
  728. * @protected
  729. * @param event {Object} Object representing the DOM event.
  730. */
  731. _onDocFocus: function (event) {
  732.  
  733. var oTarget = this._focusTarget || event.target,
  734. bFocused = this.get(FOCUSED),
  735. focusClass = this.get(FOCUS_CLASS),
  736. oFocusedNode = this._focusedNode,
  737. bInCollection;
  738.  
  739. if (this._focusTarget) {
  740. this._focusTarget = null;
  741. }
  742.  
  743.  
  744. if (this.get(HOST).contains(oTarget)) {
  745.  
  746. // The target is a descendant of the root Node.
  747.  
  748. bInCollection = this._isDescendant(oTarget);
  749.  
  750. if (!bFocused && bInCollection) {
  751.  
  752. // The user has focused a focusable descendant.
  753.  
  754. bFocused = true;
  755.  
  756. }
  757. else if (bFocused && !bInCollection) {
  758.  
  759. // The user has focused a child of the root Node that is
  760. // not one of the descendants managed by this Focus Manager
  761. // so clear the currently focused descendant.
  762.  
  763. bFocused = false;
  764.  
  765. }
  766.  
  767. }
  768. else {
  769.  
  770. // The target is some other node in the document.
  771.  
  772. bFocused = false;
  773.  
  774. }
  775.  
  776.  
  777. if (focusClass) {
  778.  
  779. if (oFocusedNode && (!oFocusedNode.compareTo(oTarget) || !bFocused)) {
  780. this._removeFocusClass();
  781. }
  782.  
  783. if (bInCollection && bFocused) {
  784.  
  785. if (focusClass.fn) {
  786. oTarget = focusClass.fn(oTarget);
  787. oTarget.addClass(focusClass.className);
  788. }
  789. else {
  790. oTarget.addClass(focusClass);
  791. }
  792.  
  793. this._focusedNode = oTarget;
  794.  
  795. }
  796.  
  797. }
  798.  
  799.  
  800. this._set(FOCUSED, bFocused);
  801.  
  802. },
  803.  
  804.  
  805. /**
  806. * @method _focusNext
  807. * @description Keydown event handler that moves focus to the next
  808. * enabled descendant.
  809. * @protected
  810. * @param event {Object} Object representing the DOM event.
  811. * @param activeDescendant {Number} Number representing the index of the
  812. * next descendant to be focused
  813. */
  814. _focusNext: function (event, activeDescendant) {
  815.  
  816. var nActiveDescendant = activeDescendant || this.get(ACTIVE_DESCENDANT),
  817. oNode;
  818.  
  819.  
  820. if (this._isDescendant(event.target) &&
  821. (nActiveDescendant <= this._lastNodeIndex)) {
  822.  
  823. nActiveDescendant = nActiveDescendant + 1;
  824.  
  825. if (nActiveDescendant === (this._lastNodeIndex + 1) &&
  826. this.get(CIRCULAR)) {
  827.  
  828. nActiveDescendant = 0;
  829.  
  830. }
  831.  
  832. oNode = this._descendants.item(nActiveDescendant);
  833.  
  834. if (oNode) {
  835.  
  836. if (oNode.get("disabled")) {
  837. this._focusNext(event, nActiveDescendant);
  838. }
  839. else {
  840. this.focus(nActiveDescendant);
  841. }
  842.  
  843. }
  844.  
  845. }
  846.  
  847. this._preventScroll(event);
  848.  
  849. },
  850.  
  851.  
  852. /**
  853. * @method _focusPrevious
  854. * @description Keydown event handler that moves focus to the previous
  855. * enabled descendant.
  856. * @protected
  857. * @param event {Object} Object representing the DOM event.
  858. * @param activeDescendant {Number} Number representing the index of the
  859. * next descendant to be focused.
  860. */
  861. _focusPrevious: function (event, activeDescendant) {
  862.  
  863. var nActiveDescendant = activeDescendant || this.get(ACTIVE_DESCENDANT),
  864. oNode;
  865.  
  866. if (this._isDescendant(event.target) && nActiveDescendant >= 0) {
  867.  
  868. nActiveDescendant = nActiveDescendant - 1;
  869.  
  870. if (nActiveDescendant === -1 && this.get(CIRCULAR)) {
  871. nActiveDescendant = this._lastNodeIndex;
  872. }
  873.  
  874. oNode = this._descendants.item(nActiveDescendant);
  875.  
  876. if (oNode) {
  877.  
  878. if (oNode.get("disabled")) {
  879. this._focusPrevious(event, nActiveDescendant);
  880. }
  881. else {
  882. this.focus(nActiveDescendant);
  883. }
  884.  
  885. }
  886.  
  887. }
  888.  
  889. this._preventScroll(event);
  890.  
  891. },
  892.  
  893.  
  894. /**
  895. * @method _afterActiveDescendantChange
  896. * @description afterChange event handler for the
  897. * "activeDescendant" attribute.
  898. * @protected
  899. * @param event {Object} Object representing the change event.
  900. */
  901. _afterActiveDescendantChange: function (event) {
  902.  
  903. var oNode = this._descendants.item(event.prevVal);
  904.  
  905. if (oNode) {
  906. oNode.set(TAB_INDEX, -1);
  907. }
  908.  
  909. oNode = this._descendants.item(event.newVal);
  910.  
  911. if (oNode) {
  912. oNode.set(TAB_INDEX, 0);
  913. }
  914.  
  915. },
  916.  
  917.  
  918.  
  919. // Public methods
  920.  
  921. initializer: function (config) {
  922. 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");
  923. this.start();
  924.  
  925. },
  926.  
  927. destructor: function () {
  928. this.stop();
  929. this.get(HOST).focusManager = null;
  930.  
  931. },
  932.  
  933.  
  934. /**
  935. * @method focus
  936. * @description Focuses the active descendant and sets the
  937. * <code>focused</code> attribute to true.
  938. * @param index {Number} Optional. Number representing the index of the
  939. * descendant to be set as the active descendant.
  940. * @param index {Node} Optional. Node instance representing the
  941. * descendant to be set as the active descendant.
  942. */
  943. focus: function (index) {
  944. if (Lang.isUndefined(index)) {
  945. index = this.get(ACTIVE_DESCENDANT);
  946. }
  947.  
  948. this.set(ACTIVE_DESCENDANT, index, { src: UI });
  949.  
  950. var oNode = this._descendants.item(this.get(ACTIVE_DESCENDANT));
  951.  
  952. if (oNode) {
  953.  
  954. oNode.focus();
  955.  
  956. // In Opera focusing a <BUTTON> element programmatically
  957. // will result in the document-level focus event handler
  958. // "_onDocFocus" being called, resulting in the handler
  959. // incorrectly setting the "focused" Attribute to false. To fix
  960. // this, set a flag ("_focusTarget") that the "_onDocFocus" method
  961. // can look for to properly handle this edge case.
  962.  
  963. if (UA.opera && oNode.get("nodeName").toLowerCase() === "button") {
  964. this._focusTarget = oNode;
  965. }
  966.  
  967. }
  968.  
  969. },
  970.  
  971.  
  972. /**
  973. * @method blur
  974. * @description Blurs the current active descendant and sets the
  975. * <code>focused</code> attribute to false.
  976. */
  977. blur: function () {
  978. var oNode;
  979. if (this.get(FOCUSED)) {
  980.  
  981. oNode = this._descendants.item(this.get(ACTIVE_DESCENDANT));
  982.  
  983. if (oNode) {
  984.  
  985. oNode.blur();
  986.  
  987. // For Opera and Webkit: Blurring an element in either browser
  988. // doesn't result in another element (such as the document)
  989. // being focused. Therefore, the "_onDocFocus" method
  990. // responsible for managing the application and removal of the
  991. // focus indicator class name is never called.
  992.  
  993. this._removeFocusClass();
  994.  
  995. }
  996.  
  997. this._set(FOCUSED, false, { src: UI });
  998. }
  999.  
  1000. },
  1001.  
  1002.  
  1003. /**
  1004. * @method start
  1005. * @description Enables the Focus Manager.
  1006. */
  1007. start: function () {
  1008. if (this._stopped) {
  1009.  
  1010. this._initDescendants();
  1011. this._attachEventHandlers();
  1012.  
  1013. this._stopped = false;
  1014.  
  1015. }
  1016.  
  1017. },
  1018.  
  1019.  
  1020. /**
  1021. * @method stop
  1022. * @description Disables the Focus Manager by detaching all event handlers.
  1023. */
  1024. stop: function () {
  1025. if (!this._stopped) {
  1026.  
  1027. this._detachEventHandlers();
  1028.  
  1029. this._descendants = null;
  1030. this._focusedNode = null;
  1031. this._lastNodeIndex = 0;
  1032. this._stopped = true;
  1033.  
  1034. }
  1035.  
  1036. },
  1037.  
  1038.  
  1039. /**
  1040. * @method refresh
  1041. * @description Refreshes the Focus Manager's descendants by re-executing the
  1042. * CSS selector query specified by the <code>descendants</code> attribute.
  1043. */
  1044. refresh: function () {
  1045. this._initDescendants();
  1046.  
  1047. if (!this._eventHandlers) {
  1048. this._attachEventHandlers();
  1049. }
  1050.  
  1051. }
  1052.  
  1053. });
  1054.  
  1055.  
  1056. NodeFocusManager.NAME = "nodeFocusManager";
  1057. NodeFocusManager.NS = "focusManager";
  1058.  
  1059. Y.namespace("Plugin");
  1060. Y.Plugin.NodeFocusManager = NodeFocusManager;
  1061.