API Docs for: 3.17.2
Show:

File: datatable/js/keynav.js

  1. /**
  2. Provides keyboard navigation of DataTable cells and support for adding other
  3. keyboard actions.
  4.  
  5. @module datatable
  6. @submodule datatable-keynav
  7. */
  8. var arrEach = Y.Array.each,
  9.  
  10. /**
  11. A DataTable class extension that provides navigation via keyboard, based on
  12. WAI-ARIA recommendation for the [Grid widget](http://www.w3.org/WAI/PF/aria-practices/#grid)
  13. and extensible to support other actions.
  14.  
  15.  
  16. @class DataTable.KeyNav
  17. @for DataTable
  18. */
  19. DtKeyNav = function (){};
  20.  
  21. /**
  22. Mapping of key codes to friendly key names that can be used in the
  23. [keyActions](#property_keyActions) property and [ARIA_ACTIONS](#property_ARIA_ACTIONS)
  24. property.
  25.  
  26. It contains aliases for the following keys:
  27. <ul>
  28. <li>backspace</li>
  29. <li>tab</li>
  30. <li>enter</li>
  31. <li>esc</li>
  32. <li>space</li>
  33. <li>pgup</li>
  34. <li>pgdown</li>
  35. <li>end</li>
  36. <li>home</li>
  37. <li>left</li>
  38. <li>up</li>
  39. <li>right</li>
  40. <li>down</li>
  41. <li>f1 .. f12</li>
  42. </ul>
  43.  
  44.  
  45. @property KEY_NAMES
  46. @type {Object}
  47. @static
  48. **/
  49. DtKeyNav.KEY_NAMES = {
  50. 8: 'backspace',
  51. 9: 'tab',
  52. 13: 'enter',
  53. 27: 'esc',
  54. 32: 'space',
  55. 33: 'pgup',
  56. 34: 'pgdown',
  57. 35: 'end',
  58. 36: 'home',
  59. 37: 'left',
  60. 38: 'up',
  61. 39: 'right',
  62. 40: 'down',
  63. 112:'f1',
  64. 113:'f2',
  65. 114:'f3',
  66. 115:'f4',
  67. 116:'f5',
  68. 117:'f6',
  69. 118:'f7',
  70. 119:'f8',
  71. 120:'f9',
  72. 121:'f10',
  73. 122:'f11',
  74. 123:'f12'
  75. };
  76.  
  77. /**
  78. Mapping of key codes to actions according to the WAI-ARIA suggestion for the
  79. [Grid Widget](http://www.w3.org/WAI/PF/aria-practices/#grid).
  80.  
  81. The key for each entry is a key-code or [keyName](#property_KEY_NAMES) while the
  82. value can be a function that performs the action or a string. If a string,
  83. it can either correspond to the name of a method in this module (or any
  84. method in a DataTable instance) or the name of an event to fire.
  85. @property ARIA_ACTIONS
  86. @type Object
  87. @static
  88. */
  89. DtKeyNav.ARIA_ACTIONS = {
  90. left: '_keyMoveLeft',
  91. right: '_keyMoveRight',
  92. up: '_keyMoveUp',
  93. down: '_keyMoveDown',
  94. home: '_keyMoveRowStart',
  95. end: '_keyMoveRowEnd',
  96. pgup: '_keyMoveColTop',
  97. pgdown: '_keyMoveColBottom'
  98. };
  99.  
  100. DtKeyNav.ATTRS = {
  101. /**
  102. Cell that's currently either focused or
  103. focusable when the DataTable gets the focus.
  104.  
  105. @attribute focusedCell
  106. @type Node
  107. @default first cell in the table.
  108. **/
  109. focusedCell: {
  110. setter: '_focusedCellSetter'
  111. },
  112.  
  113. /**
  114. Determines whether it is possible to navigate into the header area.
  115. The examples referenced in the document show both behaviors so it seems
  116. it is optional.
  117.  
  118. @attribute keyIntoHeaders
  119. @type Boolean
  120. @default true
  121. */
  122. keyIntoHeaders: {
  123. value: true
  124. }
  125.  
  126. };
  127.  
  128. Y.mix( DtKeyNav.prototype, {
  129.  
  130. /**
  131. Table of actions to be performed for each key. It is loaded with a clone
  132. of [ARIA_ACTIONS](#property_ARIA_ACTIONS) by default.
  133.  
  134. The key for each entry is either a key-code or an alias from the
  135. [KEY_NAMES](#property_KEY_NAMES) table. They can be prefixed with any combination
  136. of the modifier keys `alt`, `ctrl`, `meta` or `shift` each followed by a hyphen,
  137. such as `"ctrl-shift-up"` (modifiers, if more than one, should appear in alphabetical order).
  138.  
  139. The value for each entry should be a function or the name of a method in
  140. the DataTable instance. The method will receive the original keyboard
  141. EventFacade as its only argument.
  142.  
  143. If the value is a string and it cannot be resolved into a method,
  144. it will be assumed to be the name of an event to fire. The listener for that
  145. event will receive an EventFacade containing references to the cell that has the focus,
  146. the row, column and, unless it is a header row, the record it corresponds to.
  147. The second argument will be the original EventFacade for the keyboard event.
  148.  
  149. @property keyActions
  150. @type {Object}
  151. @default Y.DataTable.keyNav.ARIA_ACTIONS
  152. */
  153.  
  154. keyActions: null,
  155.  
  156. /**
  157. Array containing the event handles to any event that might need to be detached
  158. on destruction.
  159. @property _keyNavSubscr
  160. @type Array
  161. @default null,
  162. @private
  163. */
  164. _keyNavSubscr: null,
  165.  
  166. /**
  167. Reference to the THead section that holds the headers for the datatable.
  168. For a Scrolling DataTable, it is the one visible to the user.
  169. @property _keyNavTHead
  170. @type Node
  171. @default: null
  172. @private
  173. */
  174. _keyNavTHead: null,
  175.  
  176. /**
  177. Indicates if the headers of the table are nested or not.
  178. Nested headers makes navigation in the headers much harder.
  179. @property _keyNavNestedHeaders
  180. @default false
  181. @private
  182. */
  183. _keyNavNestedHeaders: false,
  184.  
  185. /**
  186. CSS class name prefix for columns, used to search for a cell by key.
  187. @property _keyNavColPrefix
  188. @type String
  189. @default null (initialized via getClassname() )
  190. @private
  191. */
  192. _keyNavColPrefix:null,
  193.  
  194. /**
  195. Regular expression to extract the column key from a cell via its CSS class name.
  196. @property _keyNavColRegExp
  197. @type RegExp
  198. @default null (initialized based on _keyNavColPrefix)
  199. @private
  200. */
  201. _keyNavColRegExp:null,
  202.  
  203. initializer: function () {
  204. this.onceAfter('render', this._afterKeyNavRender);
  205. this._keyNavSubscr = [
  206. this.after('focusedCellChange', this._afterKeyNavFocusedCellChange),
  207. this.after('focusedChange', this._afterKeyNavFocusedChange)
  208. ];
  209. this._keyNavColPrefix = this.getClassName('col', '');
  210. this._keyNavColRegExp = new RegExp(this._keyNavColPrefix + '(.+?)(\\s|$)');
  211. this.keyActions = Y.clone(DtKeyNav.ARIA_ACTIONS);
  212.  
  213. },
  214.  
  215. destructor: function () {
  216. arrEach(this._keyNavSubscr, function (evHandle) {
  217. if (evHandle && evHandle.detach) {
  218. evHandle.detach();
  219. }
  220. });
  221. },
  222.  
  223. /**
  224. Sets the tabIndex on the focused cell and, if the DataTable has the focus,
  225. sets the focus on it.
  226.  
  227. @method _afterFocusedCellChange
  228. @param e {EventFacade}
  229. @private
  230. */
  231. _afterKeyNavFocusedCellChange: function (e) {
  232. var newVal = e.newVal,
  233. prevVal = e.prevVal;
  234.  
  235. if (prevVal) {
  236. prevVal.set('tabIndex', -1);
  237. }
  238.  
  239. if (newVal) {
  240. newVal.set('tabIndex', 0);
  241.  
  242. if (this.get('focused')) {
  243. newVal.scrollIntoView();
  244. newVal.focus();
  245. }
  246. } else {
  247. this.set('focused', null);
  248. }
  249. },
  250.  
  251. /**
  252. When the DataTable gets the focus, it ensures the correct cell regains
  253. the focus.
  254.  
  255. @method _afterKeyNavFocusedChange
  256. @param e {EventFacade}
  257. @private
  258. */
  259. _afterKeyNavFocusedChange: function (e) {
  260. var cell = this.get('focusedCell');
  261. if (e.newVal) {
  262. if (cell) {
  263. cell.scrollIntoView();
  264. cell.focus();
  265. } else {
  266. this._keyMoveFirst();
  267. }
  268. } else {
  269. if (cell) {
  270. cell.blur();
  271. }
  272. }
  273. },
  274.  
  275. /**
  276. Subscribes to the events on the DataTable elements once they have been rendered,
  277. finds out the header section and makes the top-left element focusable.
  278.  
  279. @method _afterKeyNavRender
  280. @private
  281. */
  282. _afterKeyNavRender: function () {
  283. var cbx = this.get('contentBox');
  284. this._keyNavSubscr.push(
  285. cbx.on('keydown', this._onKeyNavKeyDown, this),
  286. cbx.on('click', this._onKeyNavClick, this)
  287. );
  288. this._keyNavTHead = (this._yScrollHeader || this._tableNode).one('thead');
  289. this._keyMoveFirst();
  290.  
  291. // determine if we have nested headers
  292. this._keyNavNestedHeaders = (this.get('columns').length !== this.head.theadNode.all('th').size());
  293. },
  294.  
  295. /**
  296. In response to a click event, it sets the focus on the clicked cell
  297.  
  298. @method _onKeyNavClick
  299. @param e {EventFacade}
  300. @private
  301. */
  302. _onKeyNavClick: function (e) {
  303. var cell = e.target.ancestor((this.get('keyIntoHeaders') ? 'td, th': 'td'), true);
  304. if (cell) {
  305. this.focus();
  306. this.set('focusedCell', cell);
  307. }
  308. },
  309.  
  310. /**
  311. Responds to a key down event by executing the action set in the
  312. [keyActions](#property_keyActions) table.
  313.  
  314. @method _onKeyNavKeyDown
  315. @param e {EventFacade}
  316. @private
  317. */
  318. _onKeyNavKeyDown: function (e) {
  319. var keyCode = e.keyCode,
  320. keyName = DtKeyNav.KEY_NAMES[keyCode] || keyCode,
  321. action;
  322.  
  323. arrEach(['alt', 'ctrl', 'meta', 'shift'], function (modifier) {
  324. if (e[modifier + 'Key']) {
  325. keyCode = modifier + '-' + keyCode;
  326. keyName = modifier + '-' + keyName;
  327. }
  328. });
  329. action = this.keyActions[keyCode] || this.keyActions[keyName];
  330.  
  331. if (typeof action === 'string') {
  332. if (this[action]) {
  333. this[action].call(this, e);
  334. } else {
  335. this._keyNavFireEvent(action, e);
  336. }
  337. } else {
  338. action.call(this, e);
  339. }
  340. },
  341.  
  342. /**
  343. If the action associated to a key combination is a string and no method
  344. by that name was found in this instance, this method will
  345. fire an event using that string and provides extra information
  346. to the listener.
  347.  
  348. @method _keyNavFireEvent
  349. @param action {String} Name of the event to fire
  350. @param e {EventFacade} Original facade from the keydown event.
  351. @private
  352. */
  353. _keyNavFireEvent: function (action, e) {
  354. var cell = e.target.ancestor('td, th', true);
  355. if (cell) {
  356. this.fire(action, {
  357. cell: cell,
  358. row: cell.ancestor('tr'),
  359. record: this.getRecord(cell),
  360. column: this.getColumn(cell.get('cellIndex'))
  361. }, e);
  362. }
  363. },
  364.  
  365. /**
  366. Sets the focus on the very first cell in the header of the table.
  367.  
  368. @method _keyMoveFirst
  369. @private
  370. */
  371. _keyMoveFirst: function () {
  372. this.set('focusedCell' , (this.get('keyIntoHeaders') ? this._keyNavTHead.one('th') : this._tbodyNode.one('td')), {src:'keyNav'});
  373. },
  374.  
  375. /**
  376. Sets the focus on the cell to the left of the currently focused one.
  377. Does not wrap, following the WAI-ARIA recommendation.
  378.  
  379. @method _keyMoveLeft
  380. @param e {EventFacade} Event Facade for the keydown event
  381. @private
  382. */
  383. _keyMoveLeft: function (e) {
  384. var cell = this.get('focusedCell'),
  385. index = cell.get('cellIndex'),
  386. row = cell.ancestor();
  387.  
  388. e.preventDefault();
  389.  
  390. if (index === 0) {
  391. return;
  392. }
  393. cell = row.get('cells').item(index - 1);
  394. this.set('focusedCell', cell , {src:'keyNav'});
  395. },
  396.  
  397. /**
  398. Sets the focus on the cell to the right of the currently focused one.
  399. Does not wrap, following the WAI-ARIA recommendation.
  400.  
  401. @method _keyMoveRight
  402. @param e {EventFacade} Event Facade for the keydown event
  403. @private
  404. */
  405. _keyMoveRight: function (e) {
  406. var cell = this.get('focusedCell'),
  407. row = cell.ancestor('tr'),
  408. section = row.ancestor(),
  409. inHead = section === this._keyNavTHead,
  410. nextCell,
  411. parent;
  412.  
  413. e.preventDefault();
  414.  
  415. // a little special with nested headers
  416. /*
  417. +-------------+-------+
  418. | ABC | DE |
  419. +-------+-----+---+---+
  420. | AB | | | |
  421. +---+---+ | | |
  422. | A | B | C | D | E |
  423. +---+---+-----+---+---+
  424. */
  425.  
  426. nextCell = cell.next();
  427.  
  428. if (row.get('rowIndex') !== 0 && inHead && this._keyNavNestedHeaders) {
  429. if (nextCell) {
  430. cell = nextCell;
  431. } else { //-- B -> C
  432. parent = this._getTHParent(cell);
  433.  
  434. if (parent && parent.next()) {
  435. cell = parent.next();
  436. } else { //-- E -> ...
  437. return;
  438. }
  439. }
  440.  
  441. } else {
  442. if (!nextCell) {
  443. return;
  444. } else {
  445. cell = nextCell;
  446. }
  447. }
  448.  
  449. this.set('focusedCell', cell, { src:'keyNav' });
  450.  
  451. },
  452.  
  453. /**
  454. Sets the focus on the cell above the currently focused one.
  455. It will move into the headers when the top of the data rows is reached.
  456. Does not wrap, following the WAI-ARIA recommendation.
  457.  
  458. @method _keyMoveUp
  459. @param e {EventFacade} Event Facade for the keydown event
  460. @private
  461. */
  462. _keyMoveUp: function (e) {
  463. var cell = this.get('focusedCell'),
  464. cellIndex = cell.get('cellIndex'),
  465. row = cell.ancestor('tr'),
  466. rowIndex = row.get('rowIndex'),
  467. section = row.ancestor(),
  468. sectionRows = section.get('rows'),
  469. inHead = section === this._keyNavTHead,
  470. parent;
  471.  
  472. e.preventDefault();
  473.  
  474. if (!inHead) {
  475. rowIndex -= section.get('firstChild').get('rowIndex');
  476. }
  477.  
  478. if (rowIndex === 0) {
  479. if (inHead || !this.get('keyIntoHeaders')) {
  480. return;
  481. }
  482.  
  483. section = this._keyNavTHead;
  484. sectionRows = section.get('rows');
  485.  
  486. if (this._keyNavNestedHeaders) {
  487. key = this._getCellColumnName(cell);
  488. cell = section.one('.' + this._keyNavColPrefix + key);
  489. cellIndex = cell.get('cellIndex');
  490. row = cell.ancestor('tr');
  491. } else {
  492. row = section.get('firstChild');
  493. cell = row.get('cells').item(cellIndex);
  494. }
  495. } else {
  496. if (inHead && this._keyNavNestedHeaders) {
  497. key = this._getCellColumnName(cell);
  498. parent = this._columnMap[key]._parent;
  499. if (parent) {
  500. cell = section.one('#' + parent.id);
  501. }
  502. } else {
  503. row = sectionRows.item(rowIndex -1);
  504. cell = row.get('cells').item(cellIndex);
  505. }
  506. }
  507. this.set('focusedCell', cell);
  508. },
  509.  
  510. /**
  511. Sets the focus on the cell below the currently focused one.
  512. It will move into the data rows when the bottom of the header rows is reached.
  513. Does not wrap, following the WAI-ARIA recommendation.
  514.  
  515. @method _keyMoveDown
  516. @param e {EventFacade} Event Facade for the keydown event
  517. @private
  518. */
  519. _keyMoveDown: function (e) {
  520. var cell = this.get('focusedCell'),
  521. cellIndex = cell.get('cellIndex'),
  522. row = cell.ancestor('tr'),
  523. rowIndex = row.get('rowIndex') + 1,
  524. section = row.ancestor(),
  525. inHead = section === this._keyNavTHead,
  526. tbody = (this.body && this.body.tbodyNode),
  527. sectionRows = section.get('rows'),
  528. key,
  529. children;
  530.  
  531. e.preventDefault();
  532.  
  533. if (inHead) { // focused cell is in the header
  534. if (this._keyNavNestedHeaders) { // the header is nested
  535. key = this._getCellColumnName(cell);
  536. children = this._columnMap[key].children;
  537.  
  538. rowIndex += (cell.getAttribute('rowspan') || 1) - 1;
  539.  
  540. if (children) {
  541. // stay in thead
  542. cell = section.one('#' + children[0].id);
  543. } else {
  544. // moving into tbody
  545. cell = tbody.one('.' + this._keyNavColPrefix + key);
  546. section = tbody;
  547. sectionRows = section.get('rows');
  548. }
  549. cellIndex = cell.get('cellIndex');
  550.  
  551. } else { // the header is not nested
  552. row = tbody.one('tr');
  553. cell = row.get('cells').item(cellIndex);
  554. }
  555. }
  556.  
  557. // offset row index to tbody
  558. rowIndex -= sectionRows.item(0).get('rowIndex');
  559.  
  560.  
  561. if (rowIndex >= sectionRows.size()) {
  562. if (!inHead) { // last row in tbody
  563. return;
  564. }
  565. section = tbody;
  566. row = section.one('tr');
  567.  
  568. } else {
  569. row = sectionRows.item(rowIndex);
  570. }
  571.  
  572. this.set('focusedCell', row.get('cells').item(cellIndex));
  573. },
  574.  
  575. /**
  576. Sets the focus on the left-most cell of the row containing the currently focused cell.
  577.  
  578. @method _keyMoveRowStart
  579. @param e {EventFacade} Event Facade for the keydown event
  580. @private
  581. */
  582. _keyMoveRowStart: function (e) {
  583. var row = this.get('focusedCell').ancestor();
  584. this.set('focusedCell', row.get('firstChild'), {src:'keyNav'});
  585. e.preventDefault();
  586. },
  587.  
  588. /**
  589. Sets the focus on the right-most cell of the row containing the currently focused cell.
  590.  
  591. @method _keyMoveRowEnd
  592. @param e {EventFacade} Event Facade for the keydown event
  593. @private
  594. */
  595. _keyMoveRowEnd: function (e) {
  596. var row = this.get('focusedCell').ancestor();
  597. this.set('focusedCell', row.get('lastChild'), {src:'keyNav'});
  598. e.preventDefault();
  599. },
  600.  
  601. /**
  602. Sets the focus on the top-most cell of the column containing the currently focused cell.
  603. It would normally be a header cell.
  604.  
  605. @method _keyMoveColTop
  606. @param e {EventFacade} Event Facade for the keydown event
  607. @private
  608. */
  609. _keyMoveColTop: function (e) {
  610. var cell = this.get('focusedCell'),
  611. cellIndex = cell.get('cellIndex'),
  612. key, header;
  613.  
  614. e.preventDefault();
  615.  
  616. if (this._keyNavNestedHeaders && this.get('keyIntoHeaders')) {
  617. key = this._getCellColumnName(cell);
  618. header = this._columnMap[key];
  619. while (header._parent) {
  620. header = header._parent;
  621. }
  622. cell = this._keyNavTHead.one('#' + header.id);
  623.  
  624. } else {
  625. cell = (this.get('keyIntoHeaders') ? this._keyNavTHead: this._tbodyNode).get('firstChild').get('cells').item(cellIndex);
  626. }
  627. this.set('focusedCell', cell , {src:'keyNav'});
  628. },
  629.  
  630. /**
  631. Sets the focus on the last cell of the column containing the currently focused cell.
  632.  
  633. @method _keyMoveColBottom
  634. @param e {EventFacade} Event Facade for the keydown event
  635. @private
  636. */
  637. _keyMoveColBottom: function (e) {
  638. var cell = this.get('focusedCell'),
  639. cellIndex = cell.get('cellIndex');
  640.  
  641. this.set('focusedCell', this._tbodyNode.get('lastChild').get('cells').item(cellIndex), {src:'keyNav'});
  642. e.preventDefault();
  643.  
  644. },
  645.  
  646. /**
  647. Setter method for the [focusedCell](#attr_focusedCell) attribute.
  648. Checks that the passed value is a Node, either a TD or TH and is
  649. contained within the DataTable contentBox.
  650.  
  651. @method _focusedCellSetter
  652. @param cell {Node} DataTable cell to receive the focus
  653. @return cell or Y.Attribute.INVALID_VALUE
  654. @private
  655. */
  656. _focusedCellSetter: function (cell) {
  657. if (cell instanceof Y.Node) {
  658. var tag = cell.get('tagName').toUpperCase();
  659. if ((tag === 'TD' || tag === 'TH') && this.get('contentBox').contains(cell) ) {
  660. return cell;
  661. }
  662. } else if (cell === null) {
  663. return cell;
  664. }
  665. return Y.Attribute.INVALID_VALUE;
  666. },
  667.  
  668. /**
  669. Retrieves the parent cell of the given TH cell. If there is no parent for
  670. the provided cell, null is returned.
  671. @protected
  672. @method _getTHParent
  673. @param {Node} thCell Cell to find parent of
  674. @return {Node} Parent of the cell provided or null
  675. */
  676. _getTHParent: function (thCell) {
  677. var key = this._getCellColumnName(thCell),
  678. parent = this._columnMap[key] && this._columnMap[key]._parent;
  679.  
  680. if (parent) {
  681. return thCell.ancestor().ancestor().one('.' + this._keyNavColPrefix + parent.key);
  682. }
  683.  
  684. return null;
  685. },
  686.  
  687. /**
  688. Retrieves the column name based from the data attribute on the cell if
  689. available. Other wise, extracts the column name from the classname
  690. @protected
  691. @method _getCellColumnName
  692. @param {Node} cell Cell to get column name from
  693. @return String Column name of the provided cell
  694. */
  695. _getCellColumnName: function (cell) {
  696. return cell.getData('yui3-col-id') || this._keyNavColRegExp.exec(cell.get('className'))[1];
  697. }
  698. });
  699.  
  700. Y.DataTable.KeyNav = DtKeyNav;
  701. Y.Base.mix(Y.DataTable, [DtKeyNav]);
  702.