PageSourceSearch

https://www.aragon.es/o/aragon-theme/js/aria/dataGrid.js

js aragon.es collected 2026-09-24 08:28:14 UTC 26,366 bytes, 950 lines download raw bytes

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.