1export function dataGrid(aria) { 2 /* 3 * This content is licensed according to the W3C Software License at 4 * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document 5 * Original source: 6 * https://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/js/dataGrid.js 7 */ 8 9 /** 10 * @namespace aria 11 */ 12 13 /** 14 * @desc 15 * Values for aria-sort 16 */ 17 aria.SortType = { 18 ASCENDING: 'ascending', 19 DESCENDING: 'descending', 20 NONE: 'none' 21 }; 22 23 /** 24 * @desc 25 * DOM Selectors to find the grid components 26 */ 27 aria.GridSelector = { 28 ROW: 'tr, [role="row"]', 29 CELL: 'th, td, [role="gridcell"]', 30 SCROLL_ROW: 'tr:not([data-fixed]), [role="row"]', 31 SORT_HEADER: 'th[aria-sort]', 32 TABBABLE: '[tabindex="0"]' 33 }; 34 35 /** 36 * @desc 37 * CSS Class names 38 */ 39 aria.CSSClass = { 40 HIDDEN: 'hidden' 41 }; 42 43 /** 44 * @constructor 45 * 46 * @desc 47 * Grid object representing the state and interactions for a grid widget 48 * 49 * Assumptions: 50 * All focusable cells initially have tabindex="-1" 51 * Produces a fully filled in mxn grid (with no holes) 52 * 53 * @param gridNode 54 * The DOM node pointing to the grid 55 */ 56 aria.Grid = function (gridNode) { 57 this.navigationDisabled = false; 58 this.gridNode = gridNode; 59 this.paginationEnabled = this.gridNode.hasAttribute('data-per-page'); 60 this.shouldWrapCols = this.gridNode.hasAttribute('data-wrap-cols'); 61 this.shouldWrapRows = this.gridNode.hasAttribute('data-wrap-rows'); 62 this.shouldRestructure = this.gridNode.hasAttribute('data-restructure'); 63 this.topIndex = 0; 64 65 this.keysIndicator = document.getElementById('arrow-keys-indicator'); 66 67 aria.Utils.bindMethods(this, 68 'checkFocusChange', 'checkPageChange', 'checkRestructureGrid', 69 'delegateButtonHandler', 'focusClickedCell', 'restructureGrid', 70 'showKeysIndicator', 'hideKeysIndicator'); 71 this.setupFocusGrid(); 72 this.setFocusPointer(0, 0); 73 74 if (this.paginationEnabled) { 75 this.setupPagination(); 76 } 77 else { 78 this.perPage = this.grid.length; 79 } 80 81 this.registerEvents(); 82 }; 83 84 /** 85 * @desc 86 * Creates a 2D array of the focusable cells in the grid. 87 */ 88 aria.Grid.prototype.setupFocusGrid = function () { 89 this.grid = []; 90 91 Array.prototype.forEach.call( 92 this.gridNode.querySelectorAll(aria.GridSelector.ROW), 93 (function (row) { 94 var rowCells = []; 95 96 Array.prototype.forEach.call( 97 row.querySelectorAll(aria.GridSelector.CELL), 98 (function (cell) { 99 var focusableSelector = '[tabindex]'; 100 101 if (aria.Utils.matches(cell, focusableSelector)) { 102 rowCells.push(cell); 103 } 104 else { 105 var focusableCell = cell.querySelector(focusableSelector); 106 107 if (focusableCell) { 108 rowCells.push(focusableCell); 109 } 110 } 111 }).bind(this) 112 ); 113 114 if (rowCells.length) { 115 this.grid.push(rowCells); 116 } 117 }).bind(this) 118 ); 119 120 if (this.paginationEnabled) { 121 this.setupIndices(); 122 } 123 }; 124 125 /** 126 * @desc 127 * If possible, set focus pointer to the cell with the specified coordinates 128 * 129 * @param row 130 * The index of the cell's row 131 * 132 * @param col 133 * The index of the cell's column 134 * 135 * @returns 136 * Returns whether or not the focus could be set on the cell. 137 */ 138 aria.Grid.prototype.setFocusPointer = function (row, col) { 139 if (!this.isValidCell(row, col)) { 140 return false; 141 } 142 143 if (this.isHidden(row, col)) { 144 return false; 145 } 146 147 if (!isNaN(this.focusedRow) && !isNaN(this.focusedCol)) { 148 this.grid[this.focusedRow][this.focusedCol].setAttribute('tabindex', -1); 149 } 150 151 this.grid[row][col] 152 .removeEventListener('focus', this.showKeysIndicator); 153 this.grid[row][col] 154 .removeEventListener('blur', this.hideKeysIndicator); 155 156 // Disable navigation if focused on an input 157 this.navigationDisabled = aria.Utils.matches(this.grid[row][col], 'input'); 158 159 this.grid[row][col].setAttribute('tabindex', 0); 160 this.focusedRow = row; 161 this.focusedCol = col; 162 163 this.grid[row][col] 164 .addEventListener('focus', this.showKeysIndicator); 165 this.grid[row][col] 166 .addEventListener('blur', this.hideKeysIndicator); 167 168 return true; 169 }; 170 171 /** 172 * @param row 173 * The index of the cell's row 174 * 175 * @param col 176 * The index of the cell's column 177 * 178 * @returns 179 * Returns whether or not the coordinates are within the grid's boundaries. 180 */ 181 aria.Grid.prototype.isValidCell = function (row, col) { 182 return ( 183 !isNaN(row) && 184 !isNaN(col) && 185 row >= 0 && 186 col >= 0 && 187 this.grid && 188 this.grid.length && 189 row < this.grid.length &&
190 col < this.grid[row].length 191 ); 192 }; 193 194 /** 195 * @param row 196 * The index of the cell's row 197 * 198 * @param col 199 * The index of the cell's column 200 * 201 * @returns 202 * Returns whether or not the cell has been hidden. 203 */ 204 aria.Grid.prototype.isHidden = function (row, col) { 205 var cell = this.gridNode.querySelectorAll(aria.GridSelector.ROW)[row] 206 .querySelectorAll(aria.GridSelector.CELL)[col]; 207 return aria.Utils.hasClass(cell, aria.CSSClass.HIDDEN); 208 }; 209 210 /** 211 * @desc 212 * Clean up grid events 213 */ 214 aria.Grid.prototype.clearEvents = function () { 215 this.gridNode.removeEventListener('keydown', this.checkFocusChange); 216 this.gridNode.removeEventListener('keydown', this.delegateButtonHandler); 217 this.gridNode.removeEventListener('click', this.focusClickedCell); 218 this.gridNode.removeEventListener('click', this.delegateButtonHandler); 219 220 if (this.paginationEnabled) { 221 this.gridNode.removeEventListener('keydown', this.checkPageChange); 222 } 223 224 if (this.shouldRestructure) { 225 window.removeEventListener('resize', this.checkRestructureGrid); 226 } 227 228 this.grid[this.focusedRow][this.focusedCol] 229 .removeEventListener('focus', this.showKeysIndicator); 230 this.grid[this.focusedRow][this.focusedCol] 231 .removeEventListener('blur', this.hideKeysIndicator); 232 }; 233 234 /** 235 * @desc 236 * Register grid events 237 */ 238 aria.Grid.prototype.registerEvents = function () { 239 this.clearEvents(); 240 241 this.gridNode.addEventListener('keydown', this.checkFocusChange); 242 this.gridNode.addEventListener('keydown', this.delegateButtonHandler); 243 this.gridNode.addEventListener('click', this.focusClickedCell); 244 this.gridNode.addEventListener('click', this.delegateButtonHandler); 245 246 if (this.paginationEnabled) { 247 this.gridNode.addEventListener('keydown', this.checkPageChange); 248 } 249 250 if (this.shouldRestructure) { 251 window.addEventListener('resize', this.checkRestructureGrid); 252 } 253 }; 254 255 /** 256 * @desc 257 * Focus on the cell in the specified row and column 258 * 259 * @param row 260 * The index of the cell's row 261 * 262 * @param col 263 * The index of the cell's column 264 */ 265 aria.Grid.prototype.focusCell = function (row, col) { 266 if (this.setFocusPointer(row, col)) { 267 this.grid[row][col].focus(); 268 } 269 }; 270 271 aria.Grid.prototype.showKeysIndicator = function () { 272 if (this.keysIndicator) { 273 aria.Utils.removeClass(this.keysIndicator, 'hidden'); 274 } 275 }; 276 277 aria.Grid.prototype.hideKeysIndicator = function () { 278 if (this.keysIndicator && 279 this.grid[this.focusedRow][this.focusedCol].tabIndex === 0) { 280 aria.Utils.addClass(this.keysIndicator, 'hidden'); 281 } 282 }; 283 284 /** 285 * @desc 286 * Triggered on keydown. Checks if an arrow key was pressed, and (if possible) 287 * moves focus to the next valid cell in the direction of the arrow key. 288 * 289 * @param event 290 * Keydown event 291 */ 292 aria.Grid.prototype.checkFocusChange = function (event) { 293 if (!event || this.navigationDisabled) { 294 return; 295 } 296 297 this.findFocusedItem(event.target); 298 299 var key = event.which || event.keyCode; 300 var rowCaret = this.focusedRow; 301 var colCaret = this.focusedCol; 302 var nextCell; 303 304 switch (key) { 305 case aria.KeyCode.UP: 306 nextCell = this.getNextVisibleCell(0, -1); 307 rowCaret = nextCell.row; 308 colCaret = nextCell.col; 309 break; 310 case aria.KeyCode.DOWN: 311 nextCell = this.getNextVisibleCell(0, 1); 312 rowCaret = nextCell.row; 313 colCaret = nextCell.col; 314 break; 315 case aria.KeyCode.LEFT: 316 nextCell = this.getNextVisibleCell(-1, 0); 317 rowCaret = nextCell.row; 318 colCaret = nextCell.col; 319 break; 320 case aria.KeyCode.RIGHT: 321 nextCell = this.getNextVisibleCell(1, 0); 322 rowCaret = nextCell.row; 323 colCaret = nextCell.col; 324 break; 325 case aria.KeyCode.HOME: 326 if (event.ctrlKey) { 327 rowCaret = 0; 328 } 329 colCaret = 0; 330 break; 331 case aria.KeyCode.END: 332 if (event.ctrlKey) { 333 rowCaret = this.grid.length - 1; 334 } 335 colCaret = this.grid[this.focusedRow].length - 1; 336 break; 337 default: 338 return; 339 } 340 341 if (this.paginationEnabled) { 342 if (rowCaret < this.topIndex) {
343 this.showFromRow(rowCaret, true); 344 } 345 346 if (rowCaret >= this.topIndex + this.perPage) { 347 this.showFromRow(rowCaret, false); 348 } 349 } 350 351 this.focusCell(rowCaret, colCaret); 352 event.preventDefault(); 353 }; 354 355 /** 356 * @desc 357 * Reset focused row and col if it doesn't match focusedRow and focusedCol 358 * 359 * @param focusedTarget 360 * Element that is currently focused by browser 361 */ 362 aria.Grid.prototype.findFocusedItem = function (focusedTarget) { 363 var focusedCell = this.grid[this.focusedRow][this.focusedCol]; 364 365 if (focusedCell === focusedTarget || 366 focusedCell.contains(focusedTarget)) { 367 return; 368 } 369 370 for (var i = 0; i < this.grid.length; i++) { 371 for (var j = 0; j < this.grid[i].length; j++) { 372 if (this.grid[i][j] === focusedTarget || 373 this.grid[i][j].contains(focusedTarget)) { 374 this.setFocusPointer(i, j); 375 return; 376 } 377 } 378 } 379 }; 380 381 /** 382 * @desc 383 * Triggered on click. Finds the cell that was clicked on and focuses on it. 384 * 385 * @param event 386 * Keydown event 387 */ 388 aria.Grid.prototype.focusClickedCell = function (event) { 389 var clickedGridCell = this.findClosest(event.target, '[tabindex]'); 390 391 for (var row = 0; row < this.grid.length; row++) { 392 for (var col = 0; col < this.grid[row].length; col++) { 393 if (this.grid[row][col] === clickedGridCell) { 394 this.setFocusPointer(row, col); 395 396 if (!aria.Utils.matches(clickedGridCell, 'button[aria-haspopup]')) { 397 // Don't focus if it's a menu button (focus should be set to menu) 398 this.focusCell(row, col); 399 } 400 401 return; 402 } 403 } 404 } 405 }; 406 407 /** 408 * @desc 409 * Triggered on click. Checks if user clicked on a header with aria-sort. 410 * If so, it sorts the column based on the aria-sort attribute. 411 * 412 * @param event 413 * Keydown event 414 */ 415 aria.Grid.prototype.delegateButtonHandler = function (event) { 416 var key = event.which || event.keyCode; 417 var target = event.target; 418 var isClickEvent = (event.type === 'click'); 419 420 if (!target) { 421 return; 422 } 423 424 if ( 425 target.parentNode && 426 target.parentNode.matches('th[aria-sort]') && 427 ( 428 isClickEvent || 429 key === aria.KeyCode.SPACE || 430 key === aria.KeyCode.RETURN 431 ) 432 ) { 433 event.preventDefault(); 434 this.handleSort(target.parentNode); 435 } 436 437 if ( 438 aria.Utils.matches(target, '.editable-text, .edit-text-button') && 439 ( 440 isClickEvent || 441 key === aria.KeyCode.RETURN 442 ) 443 ) { 444 event.preventDefault(); 445 this.toggleEditMode( 446 this.findClosest(target, '.editable-text'), 447 true, 448 true 449 ); 450 } 451 452 if ( 453 aria.Utils.matches(target, '.edit-text-input') && 454 ( 455 key === aria.KeyCode.RETURN || 456 key === aria.KeyCode.ESC 457 ) 458 ) { 459 event.preventDefault(); 460 this.toggleEditMode( 461 this.findClosest(target, '.editable-text'), 462 false, 463 key === aria.KeyCode.RETURN 464 ); 465 } 466 }; 467 468 /** 469 * @desc 470 * Toggles the mode of an editable cell between displaying the edit button 471 * and displaying the editable input. 472 * 473 * @param editCell 474 * Cell to toggle 475 * 476 * @param toggleOn 477 * Whether to show or hide edit input 478 * 479 * @param updateText 480 * Whether or not to update the button text with the input text 481 */ 482 aria.Grid.prototype.toggleEditMode = function (editCell, toggleOn, updateText) { 483 var onClassName = toggleOn ? 'edit-text-input' : 'edit-text-button'; 484 var offClassName = toggleOn ? 'edit-text-button' : 'edit-text-input'; 485 var onNode = editCell.querySelector('.' + onClassName); 486 var offNode = editCell.querySelector('.' + offClassName); 487 488 if (toggleOn) { 489 onNode.value = offNode.innerText; 490 } 491 else if (updateText) { 492 onNode.innerText = offNode.value; 493 } 494 495 aria.Utils.addClass(offNode, aria.CSSClass.HIDDEN); 496 aria.Utils.removeClass(onNode, aria.CSSClass.HIDDEN); 497 offNode.setAttribute('tabindex', -1); 498 onNode.setAttribute('tabindex', 0); 499 onNode.focus(); 500 this.grid[this.focusedRow][this.focusedCol] = onNode; 501 this.navigationDisabled = toggleOn; 502 }; 503 504 /** 505 * @desc 506 * Sorts the column below the header node, based on the aria-sort attribute. 507 * aria-sort="none" => aria-sort="ascending" 508 * aria-sort="ascending" => aria-sort="descending" 509 * All other headers with aria-sort are reset to "none" 510 * 511 * Note: This implementation assumes that there is no pagination on the grid. 512 * 513 * @param headerNode 514 * Header DOM node 515 */ 516 aria.Grid.prototype.handleSort = function (headerNode) { 517 var columnIndex = headerNode.cellIndex; 518 var sortType = headerNode.getAttribute('aria-sort'); 519 520 if (sortType === aria.SortType.ASCENDING) { 521 sortType = aria.SortType.DESCENDING; 522 } 523 else { 524 sortType = aria.SortType.ASCENDING; 525 } 526 527 var comparator = function (row1, row2) { 528 if(row1.children[columnIndex].classList.contains('align-top') || row2.children[columnIndex].classList.contains('align-top')) { 529 return 530 } 531 var row1Text = row1.children[columnIndex].innerText; 532 var row2Text = row2.children[columnIndex].innerText; 533 var row1Value = parseInt(row1Text) ? parseInt(row1Text.replace(/[^0-9\.]+/g, '')) : row1Text
534 var row2Value = parseInt(row2Text) ? parseInt(row2Text.replace(/[^0-9\.]+/g, '')) : row2Text 535 536 var orderValue = parseInt(row1Text) ? 'numbers' : 'strings' 537 538 if (sortType === aria.SortType.ASCENDING && orderValue === 'numbers') { 539 return row1Value - row2Value; 540 } 541 542 if (sortType === aria.SortType.DESCENDING && orderValue === 'numbers') { 543 return row2Value - row1Value; 544 } 545 546 if (sortType === aria.SortType.ASCENDING && orderValue === 'strings') { 547 return row1Value.toString().localeCompare(row2Value); 548 } 549 550 if (sortType === aria.SortType.DESCENDING && orderValue === 'strings') { 551 return row2Value.toString().localeCompare(row1Value); 552 } 553 554 }; 555 556 this.sortRows(comparator); 557 this.setupFocusGrid(); 558 559 Array.prototype.forEach.call( 560 this.gridNode.querySelectorAll(aria.GridSelector.SORT_HEADER), 561 function (headerCell) { 562 headerCell.setAttribute('aria-sort', aria.SortType.NONE); 563 } 564 ); 565 566 headerNode.setAttribute('aria-sort', sortType); 567 }; 568 569 /** 570 * @desc 571 * Sorts the grid's rows according to the specified compareFn 572 * 573 * @param compareFn 574 * Comparison function to sort the rows 575 */ 576 aria.Grid.prototype.sortRows = function (compareFn) { 577 var rows = this.gridNode.querySelectorAll("tbody tr"); 578 var rowWrapper = this.gridNode.querySelector('tbody'); 579 var dataRows = Array.prototype.slice.call(rows, 1); 580 581 dataRows.sort(compareFn); 582 583 dataRows.forEach((function (row) { 584 rowWrapper.appendChild(row); 585 }).bind(this)); 586 }; 587 588 /** 589 * @desc 590 * Adds aria-rowindex and aria-colindex to the cells in the grid 591 */ 592 aria.Grid.prototype.setupIndices = function () { 593 var rows = this.gridNode.querySelectorAll(aria.GridSelector.ROW); 594 595 for (var row = 0; row < rows.length; row++) { 596 var cols = rows[row].querySelectorAll(aria.GridSelector.CELL); 597 rows[row].setAttribute('aria-rowindex', row + 1); 598 599 for (var col = 0; col < cols.length; col++) { 600 cols[col].setAttribute('aria-colindex', col + 1); 601 } 602 603 } 604 }; 605 606 /** 607 * @desc 608 * Determines the per page attribute of the grid, and shows/hides rows 609 * accordingly. 610 */ 611 aria.Grid.prototype.setupPagination = function () { 612 this.onPaginationChange = this.onPaginationChange || function () {}; 613 this.perPage = parseInt(this.gridNode.getAttribute('data-per-page')); 614 this.showFromRow(0, true); 615 }; 616 617 aria.Grid.prototype.setPaginationChangeHandler = function (onPaginationChange) { 618 this.onPaginationChange = onPaginationChange; 619 }; 620 621 /** 622 * @desc 623 * Check if page up or page down was pressed, and show the next page if so. 624 * 625 * @param event 626 * Keydown event 627 */ 628 aria.Grid.prototype.checkPageChange = function (event) { 629 if (!event) { 630 return; 631 } 632 633 var key = event.which || event.keyCode; 634 635 if (key === aria.KeyCode.PAGE_UP) { 636 event.preventDefault(); 637 this.movePageUp(); 638 } 639 else if (key === aria.KeyCode.PAGE_DOWN) { 640 event.preventDefault(); 641 this.movePageDown(); 642 } 643 }; 644 645 aria.Grid.prototype.movePageUp = function () { 646 var startIndex = Math.max(this.perPage - 1, this.topIndex - 1); 647 this.showFromRow(startIndex, false); 648 this.focusCell(startIndex, this.focusedCol); 649 }; 650 651 aria.Grid.prototype.movePageDown = function () { 652 var startIndex = this.topIndex + this.perPage; 653 this.showFromRow(startIndex, true); 654 this.focusCell(startIndex, this.focusedCol); 655 }; 656 657 /** 658 * @desc 659 * Scroll the specified row into view in the specified direction 660 * 661 * @param startIndex 662 * Row index to use as the start index 663 * 664 * @param scrollDown 665 * Whether to scroll the new page above or below the row index 666 */ 667 aria.Grid.prototype.showFromRow = function (startIndex, scrollDown) { 668 var dataRows = 669 this.gridNode.querySelectorAll(aria.GridSelector.SCROLL_ROW); 670 var reachedTop = false; 671 var firstIndex = -1; 672 var endIndex = -1; 673 674 if (startIndex < 0 || startIndex >= dataRows.length) { 675 return; 676 } 677 678 for (var i = 0; i < dataRows.length; i++) { 679 680 if ( 681 ( 682 scrollDown && 683 i >= startIndex && 684 i < startIndex + this.perPage) || 685 ( 686 !scrollDown && 687 i <= startIndex && 688 i > startIndex - this.perPage 689 ) 690 ) { 691 aria.Utils.removeClass(dataRows[i], aria.CSSClass.HIDDEN); 692 693 if (!reachedTop) { 694 this.topIndex = i; 695 reachedTop = true; 696 } 697 698 if (firstIndex < 0) { 699 firstIndex = i; 700 } 701 endIndex = i; 702 } 703 else { 704 aria.Utils.addClass(dataRows[i], aria.CSSClass.HIDDEN); 705 } 706 } 707 this.onPaginationChange(firstIndex, endIndex); 708 }; 709 710 /** 711 * @desc 712 * Throttle restructuring to only happen every 300ms 713 */ 714 aria.Grid.prototype.checkRestructureGrid = function () { 715 if (this.waitingToRestructure) { 716 return; 717 } 718 719 this.waitingToRestructure = true; 720 721 setTimeout(this.restructureGrid, 300); 722 }; 723 724 /** 725 * @desc 726 * Restructure grid based on the size. 727 */ 728 aria.Grid.prototype.restructureGrid = function () { 729 this.waitingToRestructure = false; 730 731 var gridWidth = this.gridNode.offsetWidth; 732 var cells = this.gridNode.querySelectorAll(aria.GridSelector.CELL); 733 var currentWidth = 0; 734 735 var focusedElement = this.gridNode.querySelector(aria.GridSelector.TABBABLE); 736 var shouldRefocus = (document.activeElement === focusedElement); 737 var focusedIndex = (this.focusedRow * this.grid[0].length + this.focusedCol); 738 739 var newRow = document.createElement('div'); 740 newRow.setAttribute('role', 'row'); 741 this.gridNode.innerHTML = ''; 742 this.gridNode.append(newRow); 743
744 cells.forEach(function (cell, index) { 745 var cellWidth = cell.offsetWidth; 746 747 if (currentWidth > 0 && currentWidth >= (gridWidth - cellWidth)) { 748 newRow = document.createElement('div'); 749 newRow.setAttribute('role', 'row'); 750 this.gridNode.append(newRow); 751 currentWidth = 0; 752 } 753 754 newRow.append(cell); 755 currentWidth += cellWidth; 756 }); 757 758 this.setupFocusGrid(); 759 760 this.focusedRow = Math.floor(focusedIndex / this.grid[0].length); 761 this.focusedCol = focusedIndex % this.grid[0].length; 762 763 if (shouldRefocus) { 764 this.focusCell(this.focusedRow, this.focusedCol); 765 } 766 }; 767 768 /** 769 * @desc 770 * Get next cell to the right or left (direction) of the focused 771 * cell. 772 * 773 * @param currRow 774 * Row index to start searching from 775 * 776 * @param currCol 777 * Column index to start searching from 778 * 779 * @param directionX 780 * X direction for where to check for cells. +1 to check to the right, -1 to 781 * check to the left 782 * 783 * @return 784 * Indices of the next cell in the specified direction. Returns the focused 785 * cell if none are found. 786 */ 787 aria.Grid.prototype.getNextCell = function ( 788 currRow, 789 currCol, 790 directionX, 791 directionY 792 ) { 793 var row = currRow + directionY; 794 var col = currCol + directionX; 795 var rowCount = this.grid.length; 796 var isLeftRight = directionX !== 0; 797 798 if (!rowCount) { 799 return false; 800 } 801 802 var colCount = this.grid[0].length; 803 804 if (this.shouldWrapCols && isLeftRight) { 805 if (col < 0) { 806 col = colCount - 1; 807 row--; 808 } 809 810 if (col >= colCount) { 811 col = 0; 812 row++; 813 } 814 } 815 816 if (this.shouldWrapRows && !isLeftRight) { 817 if (row < 0) { 818 col--; 819 row = rowCount - 1; 820 if (this.grid[row] && col >= 0 && !this.grid[row][col]) { 821 // Sometimes the bottom row is not completely filled in. In this case, 822 // jump to the next filled in cell. 823 row--; 824 } 825 } 826 else if (row >= rowCount || !this.grid[row][col]) { 827 row = 0; 828 col++; 829 } 830 } 831 832 if (this.isValidCell(row, col)) { 833 return { 834 row: row, 835 col: col 836 }; 837 } 838 else if (this.isValidCell(currRow, currCol)) { 839 return { 840 row: currRow, 841 col: currCol 842 }; 843 } 844 else { 845 return false; 846 } 847 }; 848 849 /** 850 * @desc 851 * Get next visible column to the right or left (direction) of the focused 852 * cell. 853 * 854 * @param direction 855 * Direction for where to check for cells. +1 to check to the right, -1 to 856 * check to the left 857 * 858 * @return 859 * Indices of the next visible cell in the specified direction. If no visible 860 * cells are found, returns false if the current cell is hidden and returns 861 * the current cell if it is not hidden. 862 */ 863 aria.Grid.prototype.getNextVisibleCell = function (directionX, directionY) { 864 var nextCell = this.getNextCell( 865 this.focusedRow, 866 this.focusedCol, 867 directionX, 868 directionY 869 ); 870 871 if (!nextCell) { 872 return false; 873 } 874 875 var rowCount = this.grid.length; 876 var colCount = this.grid[nextCell.row].length; 877 878 while (this.isHidden(nextCell.row, nextCell.col)) { 879 var currRow = nextCell.row; 880 var currCol = nextCell.col; 881 882 nextCell = this.getNextCell(currRow, currCol, directionX, directionY); 883 884 if (currRow === nextCell.row && currCol === nextCell.col) { 885 // There are no more cells to try if getNextCell returns the current cell 886 return false; 887 } 888 } 889 890 return nextCell; 891 }; 892 893 /** 894 * @desc 895 * Show or hide the cells in the specified column 896 * 897 * @param columnIndex 898 * Index of the column to toggle 899 * 900 * @param isShown 901 * Whether or not to show the column 902 */ 903 aria.Grid.prototype.toggleColumn = function (columnIndex, isShown) { 904 var cellSelector = '[aria-colindex="' + columnIndex + '"]'; 905 var columnCells = this.gridNode.querySelectorAll(cellSelector); 906 907 Array.prototype.forEach.call( 908 columnCells, 909 function (cell) { 910 if (isShown) { 911 aria.Utils.removeClass(cell, aria.CSSClass.HIDDEN); 912 } 913 else { 914 aria.Utils.addClass(cell, aria.CSSClass.HIDDEN); 915 } 916 } 917 ); 918 919 if (!isShown && this.focusedCol === (columnIndex - 1)) { 920 // If focus was set on the hidden column, shift focus to the right 921 var nextCell = this.getNextVisibleCell(1, 0); 922 if (nextCell) { 923 this.setFocusPointer(nextCell.row, nextCell.col); 924 } 925 } 926 }; 927 928 /** 929 * @desc 930 * Find the closest element matching the selector. Only checks parent and 931 * direct children. 932 * 933 * @param element 934 * Element to start searching from 935 * 936 * @param selector 937 * Index of the column to toggle 938 */ 939 aria.Grid.prototype.findClosest = function (element, selector) { 940 if (aria.Utils.matches(element, selector)) { 941 return element; 942 } 943 944 if (aria.Utils.matches(element.parentNode, selector)) { 945 return element.parentNode; 946 } 947 948 return element.querySelector(selector); 949 }; 950}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.