diff -r d334a616c023 -r e16a97fb364a src/cm/media/js/lib/yui/yui3-3.15.0/build/datatable-keynav/datatable-keynav-debug.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cm/media/js/lib/yui/yui3-3.15.0/build/datatable-keynav/datatable-keynav-debug.js Mon Mar 10 15:19:48 2014 +0100 @@ -0,0 +1,706 @@ +YUI.add('datatable-keynav', function (Y, NAME) { + +/** + 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: + + + +@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]); + + +}, '@VERSION@', {"requires": ["datatable-base"]});