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