[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/yuilib/3.17.2/datatable-keynav/ -> datatable-keynav.js (source)

   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"]});


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1