- /**
- Provides keyboard navigation of DataTable cells and support for adding other
- keyboard actions.
-
- @module datatable
- @submodule datatable-keynav
- */
- var arrEach = Y.Array.each,
-
- /**
- A DataTable class extension that provides navigation via keyboard, based on
- WAI-ARIA recommendation for the [Grid widget](http://www.w3.org/WAI/PF/aria-practices/#grid)
- and extensible to support other actions.
-
-
- @class DataTable.KeyNav
- @for DataTable
- */
- DtKeyNav = function (){};
-
- /**
- Mapping of key codes to friendly key names that can be used in the
- [keyActions](#property_keyActions) property and [ARIA_ACTIONS](#property_ARIA_ACTIONS)
- property.
-
- It contains aliases for the following keys:
- <ul>
- <li>backspace</li>
- <li>tab</li>
- <li>enter</li>
- <li>esc</li>
- <li>space</li>
- <li>pgup</li>
- <li>pgdown</li>
- <li>end</li>
- <li>home</li>
- <li>left</li>
- <li>up</li>
- <li>right</li>
- <li>down</li>
- <li>f1 .. f12</li>
- </ul>
-
-
- @property KEY_NAMES
- @type {Object}
- @static
- **/
- DtKeyNav.KEY_NAMES = {
- 8: 'backspace',
- 9: 'tab',
- 13: 'enter',
- 27: 'esc',
- 32: 'space',
- 33: 'pgup',
- 34: 'pgdown',
- 35: 'end',
- 36: 'home',
- 37: 'left',
- 38: 'up',
- 39: 'right',
- 40: 'down',
- 112:'f1',
- 113:'f2',
- 114:'f3',
- 115:'f4',
- 116:'f5',
- 117:'f6',
- 118:'f7',
- 119:'f8',
- 120:'f9',
- 121:'f10',
- 122:'f11',
- 123:'f12'
- };
-
- /**
- Mapping of key codes to actions according to the WAI-ARIA suggestion for the
- [Grid Widget](http://www.w3.org/WAI/PF/aria-practices/#grid).
-
- The key for each entry is a key-code or [keyName](#property_KEY_NAMES) while the
- value can be a function that performs the action or a string. If a string,
- it can either correspond to the name of a method in this module (or any
- method in a DataTable instance) or the name of an event to fire.
- @property ARIA_ACTIONS
- @type Object
- @static
- */
- DtKeyNav.ARIA_ACTIONS = {
- left: '_keyMoveLeft',
- right: '_keyMoveRight',
- up: '_keyMoveUp',
- down: '_keyMoveDown',
- home: '_keyMoveRowStart',
- end: '_keyMoveRowEnd',
- pgup: '_keyMoveColTop',
- pgdown: '_keyMoveColBottom'
- };
-
- DtKeyNav.ATTRS = {
- /**
- Cell that's currently either focused or
- focusable when the DataTable gets the focus.
-
- @attribute focusedCell
- @type Node
- @default first cell in the table.
- **/
- focusedCell: {
- setter: '_focusedCellSetter'
- },
-
- /**
- Determines whether it is possible to navigate into the header area.
- The examples referenced in the document show both behaviors so it seems
- it is optional.
-
- @attribute keyIntoHeaders
- @type Boolean
- @default true
- */
- keyIntoHeaders: {
- value: true
- }
-
- };
-
- Y.mix( DtKeyNav.prototype, {
-
- /**
- Table of actions to be performed for each key. It is loaded with a clone
- of [ARIA_ACTIONS](#property_ARIA_ACTIONS) by default.
-
- The key for each entry is either a key-code or an alias from the
- [KEY_NAMES](#property_KEY_NAMES) table. They can be prefixed with any combination
- of the modifier keys `alt`, `ctrl`, `meta` or `shift` each followed by a hyphen,
- such as `"ctrl-shift-up"` (modifiers, if more than one, should appear in alphabetical order).
-
- The value for each entry should be a function or the name of a method in
- the DataTable instance. The method will receive the original keyboard
- EventFacade as its only argument.
-
- If the value is a string and it cannot be resolved into a method,
- it will be assumed to be the name of an event to fire. The listener for that
- event will receive an EventFacade containing references to the cell that has the focus,
- the row, column and, unless it is a header row, the record it corresponds to.
- The second argument will be the original EventFacade for the keyboard event.
-
- @property keyActions
- @type {Object}
- @default Y.DataTable.keyNav.ARIA_ACTIONS
- */
-
- keyActions: null,
-
- /**
- Array containing the event handles to any event that might need to be detached
- on destruction.
- @property _keyNavSubscr
- @type Array
- @default null,
- @private
- */
- _keyNavSubscr: null,
-
- /**
- Reference to the THead section that holds the headers for the datatable.
- For a Scrolling DataTable, it is the one visible to the user.
- @property _keyNavTHead
- @type Node
- @default: null
- @private
- */
- _keyNavTHead: null,
-
- /**
- Indicates if the headers of the table are nested or not.
- Nested headers makes navigation in the headers much harder.
- @property _keyNavNestedHeaders
- @default false
- @private
- */
- _keyNavNestedHeaders: false,
-
- /**
- CSS class name prefix for columns, used to search for a cell by key.
- @property _keyNavColPrefix
- @type String
- @default null (initialized via getClassname() )
- @private
- */
- _keyNavColPrefix:null,
-
- /**
- Regular expression to extract the column key from a cell via its CSS class name.
- @property _keyNavColRegExp
- @type RegExp
- @default null (initialized based on _keyNavColPrefix)
- @private
- */
- _keyNavColRegExp:null,
-
- initializer: function () {
- this.onceAfter('render', this._afterKeyNavRender);
- this._keyNavSubscr = [
- this.after('focusedCellChange', this._afterKeyNavFocusedCellChange),
- this.after('focusedChange', this._afterKeyNavFocusedChange)
- ];
- this._keyNavColPrefix = this.getClassName('col', '');
- this._keyNavColRegExp = new RegExp(this._keyNavColPrefix + '(.+?)(\\s|$)');
- this.keyActions = Y.clone(DtKeyNav.ARIA_ACTIONS);
-
- },
-
- destructor: function () {
- arrEach(this._keyNavSubscr, function (evHandle) {
- if (evHandle && evHandle.detach) {
- evHandle.detach();
- }
- });
- },
-
- /**
- Sets the tabIndex on the focused cell and, if the DataTable has the focus,
- sets the focus on it.
-
- @method _afterFocusedCellChange
- @param e {EventFacade}
- @private
- */
- _afterKeyNavFocusedCellChange: function (e) {
- var newVal = e.newVal,
- prevVal = e.prevVal;
-
- if (prevVal) {
- prevVal.set('tabIndex', -1);
- }
-
- if (newVal) {
- newVal.set('tabIndex', 0);
-
- if (this.get('focused')) {
- newVal.scrollIntoView();
- newVal.focus();
- }
- } else {
- this.set('focused', null);
- }
- },
-
- /**
- When the DataTable gets the focus, it ensures the correct cell regains
- the focus.
-
- @method _afterKeyNavFocusedChange
- @param e {EventFacade}
- @private
- */
- _afterKeyNavFocusedChange: function (e) {
- var cell = this.get('focusedCell');
- if (e.newVal) {
- if (cell) {
- cell.scrollIntoView();
- cell.focus();
- } else {
- this._keyMoveFirst();
- }
- } else {
- if (cell) {
- cell.blur();
- }
- }
- },
-
- /**
- Subscribes to the events on the DataTable elements once they have been rendered,
- finds out the header section and makes the top-left element focusable.
-
- @method _afterKeyNavRender
- @private
- */
- _afterKeyNavRender: function () {
- var cbx = this.get('contentBox');
- this._keyNavSubscr.push(
- cbx.on('keydown', this._onKeyNavKeyDown, this),
- cbx.on('click', this._onKeyNavClick, this)
- );
- this._keyNavTHead = (this._yScrollHeader || this._tableNode).one('thead');
- this._keyMoveFirst();
-
- // determine if we have nested headers
- this._keyNavNestedHeaders = (this.get('columns').length !== this.head.theadNode.all('th').size());
- },
-
- /**
- In response to a click event, it sets the focus on the clicked cell
-
- @method _onKeyNavClick
- @param e {EventFacade}
- @private
- */
- _onKeyNavClick: function (e) {
- var cell = e.target.ancestor((this.get('keyIntoHeaders') ? 'td, th': 'td'), true);
- if (cell) {
- this.focus();
- this.set('focusedCell', cell);
- }
- },
-
- /**
- Responds to a key down event by executing the action set in the
- [keyActions](#property_keyActions) table.
-
- @method _onKeyNavKeyDown
- @param e {EventFacade}
- @private
- */
- _onKeyNavKeyDown: function (e) {
- var keyCode = e.keyCode,
- keyName = DtKeyNav.KEY_NAMES[keyCode] || keyCode,
- action;
-
- arrEach(['alt', 'ctrl', 'meta', 'shift'], function (modifier) {
- if (e[modifier + 'Key']) {
- keyCode = modifier + '-' + keyCode;
- keyName = modifier + '-' + keyName;
- }
- });
- action = this.keyActions[keyCode] || this.keyActions[keyName];
-
- if (typeof action === 'string') {
- if (this[action]) {
- this[action].call(this, e);
- } else {
- this._keyNavFireEvent(action, e);
- }
- } else {
- action.call(this, e);
- }
- },
-
- /**
- If the action associated to a key combination is a string and no method
- by that name was found in this instance, this method will
- fire an event using that string and provides extra information
- to the listener.
-
- @method _keyNavFireEvent
- @param action {String} Name of the event to fire
- @param e {EventFacade} Original facade from the keydown event.
- @private
- */
- _keyNavFireEvent: function (action, e) {
- var cell = e.target.ancestor('td, th', true);
- if (cell) {
- this.fire(action, {
- cell: cell,
- row: cell.ancestor('tr'),
- record: this.getRecord(cell),
- column: this.getColumn(cell.get('cellIndex'))
- }, e);
- }
- },
-
- /**
- Sets the focus on the very first cell in the header of the table.
-
- @method _keyMoveFirst
- @private
- */
- _keyMoveFirst: function () {
- this.set('focusedCell' , (this.get('keyIntoHeaders') ? this._keyNavTHead.one('th') : this._tbodyNode.one('td')), {src:'keyNav'});
- },
-
- /**
- Sets the focus on the cell to the left of the currently focused one.
- Does not wrap, following the WAI-ARIA recommendation.
-
- @method _keyMoveLeft
- @param e {EventFacade} Event Facade for the keydown event
- @private
- */
- _keyMoveLeft: function (e) {
- var cell = this.get('focusedCell'),
- index = cell.get('cellIndex'),
- row = cell.ancestor();
-
- e.preventDefault();
-
- if (index === 0) {
- return;
- }
- cell = row.get('cells').item(index - 1);
- this.set('focusedCell', cell , {src:'keyNav'});
- },
-
- /**
- Sets the focus on the cell to the right of the currently focused one.
- Does not wrap, following the WAI-ARIA recommendation.
-
- @method _keyMoveRight
- @param e {EventFacade} Event Facade for the keydown event
- @private
- */
- _keyMoveRight: function (e) {
- var cell = this.get('focusedCell'),
- row = cell.ancestor('tr'),
- section = row.ancestor(),
- inHead = section === this._keyNavTHead,
- nextCell,
- parent;
-
- e.preventDefault();
-
- // a little special with nested headers
- /*
- +-------------+-------+
- | ABC | DE |
- +-------+-----+---+---+
- | AB | | | |
- +---+---+ | | |
- | A | B | C | D | E |
- +---+---+-----+---+---+
- */
-
- nextCell = cell.next();
-
- if (row.get('rowIndex') !== 0 && inHead && this._keyNavNestedHeaders) {
- if (nextCell) {
- cell = nextCell;
- } else { //-- B -> C
- parent = this._getTHParent(cell);
-
- if (parent && parent.next()) {
- cell = parent.next();
- } else { //-- E -> ...
- return;
- }
- }
-
- } else {
- if (!nextCell) {
- return;
- } else {
- cell = nextCell;
- }
- }
-
- this.set('focusedCell', cell, { src:'keyNav' });
-
- },
-
- /**
- Sets the focus on the cell above the currently focused one.
- It will move into the headers when the top of the data rows is reached.
- Does not wrap, following the WAI-ARIA recommendation.
-
- @method _keyMoveUp
- @param e {EventFacade} Event Facade for the keydown event
- @private
- */
- _keyMoveUp: function (e) {
- var cell = this.get('focusedCell'),
- cellIndex = cell.get('cellIndex'),
- row = cell.ancestor('tr'),
- rowIndex = row.get('rowIndex'),
- section = row.ancestor(),
- sectionRows = section.get('rows'),
- inHead = section === this._keyNavTHead,
- parent;
-
- e.preventDefault();
-
- if (!inHead) {
- rowIndex -= section.get('firstChild').get('rowIndex');
- }
-
- if (rowIndex === 0) {
- if (inHead || !this.get('keyIntoHeaders')) {
- return;
- }
-
- section = this._keyNavTHead;
- sectionRows = section.get('rows');
-
- if (this._keyNavNestedHeaders) {
- key = this._getCellColumnName(cell);
- cell = section.one('.' + this._keyNavColPrefix + key);
- cellIndex = cell.get('cellIndex');
- row = cell.ancestor('tr');
- } else {
- row = section.get('firstChild');
- cell = row.get('cells').item(cellIndex);
- }
- } else {
- if (inHead && this._keyNavNestedHeaders) {
- key = this._getCellColumnName(cell);
- parent = this._columnMap[key]._parent;
- if (parent) {
- cell = section.one('#' + parent.id);
- }
- } else {
- row = sectionRows.item(rowIndex -1);
- cell = row.get('cells').item(cellIndex);
- }
- }
- this.set('focusedCell', cell);
- },
-
- /**
- Sets the focus on the cell below the currently focused one.
- It will move into the data rows when the bottom of the header rows is reached.
- Does not wrap, following the WAI-ARIA recommendation.
-
- @method _keyMoveDown
- @param e {EventFacade} Event Facade for the keydown event
- @private
- */
- _keyMoveDown: function (e) {
- var cell = this.get('focusedCell'),
- cellIndex = cell.get('cellIndex'),
- row = cell.ancestor('tr'),
- rowIndex = row.get('rowIndex') + 1,
- section = row.ancestor(),
- inHead = section === this._keyNavTHead,
- tbody = (this.body && this.body.tbodyNode),
- sectionRows = section.get('rows'),
- key,
- children;
-
- e.preventDefault();
-
- if (inHead) { // focused cell is in the header
- if (this._keyNavNestedHeaders) { // the header is nested
- key = this._getCellColumnName(cell);
- children = this._columnMap[key].children;
-
- rowIndex += (cell.getAttribute('rowspan') || 1) - 1;
-
- if (children) {
- // stay in thead
- cell = section.one('#' + children[0].id);
- } else {
- // moving into tbody
- cell = tbody.one('.' + this._keyNavColPrefix + key);
- section = tbody;
- sectionRows = section.get('rows');
- }
- cellIndex = cell.get('cellIndex');
-
- } else { // the header is not nested
- row = tbody.one('tr');
- cell = row.get('cells').item(cellIndex);
- }
- }
-
- // offset row index to tbody
- rowIndex -= sectionRows.item(0).get('rowIndex');
-
-
- if (rowIndex >= sectionRows.size()) {
- if (!inHead) { // last row in tbody
- return;
- }
- section = tbody;
- row = section.one('tr');
-
- } else {
- row = sectionRows.item(rowIndex);
- }
-
- this.set('focusedCell', row.get('cells').item(cellIndex));
- },
-
- /**
- Sets the focus on the left-most cell of the row containing the currently focused cell.
-
- @method _keyMoveRowStart
- @param e {EventFacade} Event Facade for the keydown event
- @private
- */
- _keyMoveRowStart: function (e) {
- var row = this.get('focusedCell').ancestor();
- this.set('focusedCell', row.get('firstChild'), {src:'keyNav'});
- e.preventDefault();
- },
-
- /**
- Sets the focus on the right-most cell of the row containing the currently focused cell.
-
- @method _keyMoveRowEnd
- @param e {EventFacade} Event Facade for the keydown event
- @private
- */
- _keyMoveRowEnd: function (e) {
- var row = this.get('focusedCell').ancestor();
- this.set('focusedCell', row.get('lastChild'), {src:'keyNav'});
- e.preventDefault();
- },
-
- /**
- Sets the focus on the top-most cell of the column containing the currently focused cell.
- It would normally be a header cell.
-
- @method _keyMoveColTop
- @param e {EventFacade} Event Facade for the keydown event
- @private
- */
- _keyMoveColTop: function (e) {
- var cell = this.get('focusedCell'),
- cellIndex = cell.get('cellIndex'),
- key, header;
-
- e.preventDefault();
-
- if (this._keyNavNestedHeaders && this.get('keyIntoHeaders')) {
- key = this._getCellColumnName(cell);
- header = this._columnMap[key];
- while (header._parent) {
- header = header._parent;
- }
- cell = this._keyNavTHead.one('#' + header.id);
-
- } else {
- cell = (this.get('keyIntoHeaders') ? this._keyNavTHead: this._tbodyNode).get('firstChild').get('cells').item(cellIndex);
- }
- this.set('focusedCell', cell , {src:'keyNav'});
- },
-
- /**
- Sets the focus on the last cell of the column containing the currently focused cell.
-
- @method _keyMoveColBottom
- @param e {EventFacade} Event Facade for the keydown event
- @private
- */
- _keyMoveColBottom: function (e) {
- var cell = this.get('focusedCell'),
- cellIndex = cell.get('cellIndex');
-
- this.set('focusedCell', this._tbodyNode.get('lastChild').get('cells').item(cellIndex), {src:'keyNav'});
- e.preventDefault();
-
- },
-
- /**
- Setter method for the [focusedCell](#attr_focusedCell) attribute.
- Checks that the passed value is a Node, either a TD or TH and is
- contained within the DataTable contentBox.
-
- @method _focusedCellSetter
- @param cell {Node} DataTable cell to receive the focus
- @return cell or Y.Attribute.INVALID_VALUE
- @private
- */
- _focusedCellSetter: function (cell) {
- if (cell instanceof Y.Node) {
- var tag = cell.get('tagName').toUpperCase();
- if ((tag === 'TD' || tag === 'TH') && this.get('contentBox').contains(cell) ) {
- return cell;
- }
- } else if (cell === null) {
- return cell;
- }
- return Y.Attribute.INVALID_VALUE;
- },
-
- /**
- Retrieves the parent cell of the given TH cell. If there is no parent for
- the provided cell, null is returned.
- @protected
- @method _getTHParent
- @param {Node} thCell Cell to find parent of
- @return {Node} Parent of the cell provided or null
- */
- _getTHParent: function (thCell) {
- var key = this._getCellColumnName(thCell),
- parent = this._columnMap[key] && this._columnMap[key]._parent;
-
- if (parent) {
- return thCell.ancestor().ancestor().one('.' + this._keyNavColPrefix + parent.key);
- }
-
- return null;
- },
-
- /**
- Retrieves the column name based from the data attribute on the cell if
- available. Other wise, extracts the column name from the classname
- @protected
- @method _getCellColumnName
- @param {Node} cell Cell to get column name from
- @return String Column name of the provided cell
- */
- _getCellColumnName: function (cell) {
- return cell.getData('yui3-col-id') || this._keyNavColRegExp.exec(cell.get('className'))[1];
- }
- });
-
- Y.DataTable.KeyNav = DtKeyNav;
- Y.Base.mix(Y.DataTable, [DtKeyNav]);
-
-