1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * TODO(stoarca): This class has become obsolete except for the shadow table. 7 * Chop most of it away. 8 * @fileoverview A DOM traversal interface for navigating data in tables. 9 */ 10 11goog.provide('cvox.TraverseTable'); 12 13goog.require('cvox.DomPredicates'); 14goog.require('cvox.DomUtil'); 15goog.require('cvox.SelectionUtil'); 16goog.require('cvox.TableUtil'); 17goog.require('cvox.TraverseUtil'); 18 19 20 21/** 22 * An object that represents an active table cell inside the shadow table. 23 * @constructor 24 */ 25function ShadowTableNode() {} 26 27 28/** 29 * Whether or not the active cell is spanned by a preceding cell. 30 * @type {boolean} 31 */ 32ShadowTableNode.prototype.spanned; 33 34 35/** 36 * Whether or not this cell is spanned by a rowSpan. 37 * @type {?boolean} 38 */ 39ShadowTableNode.prototype.rowSpan; 40 41 42/** 43 * Whether or not this cell is spanned by a colspan 44 * @type {?boolean} 45 */ 46ShadowTableNode.prototype.colSpan; 47 48 49/** 50 * The row index of the corresponding active table cell 51 * @type {?number} 52 */ 53ShadowTableNode.prototype.i; 54 55 56/** 57 * The column index of the corresponding active table cell 58 * @type {?number} 59 */ 60ShadowTableNode.prototype.j; 61 62 63/** 64 * The corresponding <TD> or <TH> node in the active table. 65 * @type {?Node} 66 */ 67ShadowTableNode.prototype.activeCell; 68 69 70/** 71 * The cells that are row headers of the corresponding active table cell 72 * @type {!Array} 73 */ 74ShadowTableNode.prototype.rowHeaderCells = []; 75 76 77/** 78 * The cells that are column headers of the corresponding active table cell 79 * @type {!Array} 80 */ 81ShadowTableNode.prototype.colHeaderCells = []; 82 83 84 85/** 86 * Initializes the traversal with the provided table node. 87 * 88 * @constructor 89 * @param {Node} tableNode The table to be traversed. 90 */ 91cvox.TraverseTable = function(tableNode) { 92 93 /** 94 * The active table <TABLE> node. In this context, "active" means that this is 95 * the table the TraverseTable object is navigating. 96 * @type {Node} 97 * @private 98 */ 99 this.activeTable_ = null; 100 101 /** 102 * A 2D array "shadow table" that contains pointers to nodes in the active 103 * table. More specifically, each cell of the shadow table contains a special 104 * object ShadowTableNode that has as one of its member variables the 105 * corresponding cell in the active table. 106 * 107 * The shadow table will allow us efficient navigation of tables with 108 * rowspans and colspans without needing to repeatedly scan the table. For 109 * example, if someone requests a cell at (1,3), predecessor cells with 110 * rowspans/colspans mean the cell you eventually return could actually be 111 * one located at (0,2) that spans out to (1,3). 112 * 113 * This shadow table will contain a ShadowTableNode with the (0, 2) index at 114 * the (1,3) position, eliminating the need to check for predecessor cells 115 * with rowspan/colspan every time we traverse the table. 116 * 117 * @type {!Array.<Array.<ShadowTableNode>>} 118 * @private 119 */ 120 this.shadowTable_ = []; 121 122 /** 123 * An array of shadow table nodes that have been determined to contain header 124 * cells or information about header cells. This array is collected at 125 * initialization and then only recalculated if the table changes. 126 * This array is used by findHeaderCells() to determine table row headers 127 * and column headers. 128 * @type {Array.<ShadowTableNode>} 129 * @private 130 */ 131 this.candidateHeaders_ = []; 132 133 /** 134 * An array that associates cell IDs with their corresponding shadow nodes. 135 * If there are two shadow nodes for the same cell (i.e. when a cell spans 136 * other cells) then the first one will be associated with the ID. This means 137 * that shadow nodes that have spanned set to true will not be included in 138 * this array. 139 * @type {Array.<ShadowTableNode>} 140 * @private 141 */ 142 this.idToShadowNode_ = []; 143 144 this.initialize(tableNode); 145}; 146 147 148/** 149 * The cell cursor, represented by an array that stores the row and 150 * column location [i, j] of the active cell. These numbers are 0-based. 151 * In this context, "active" means that this is the cell the user is 152 * currently looking at. 153 * @type {Array} 154 */ 155cvox.TraverseTable.prototype.currentCellCursor; 156 157 158/** 159 * The number of columns in the active table. This is calculated at 160 * initialization and then only recalculated if the table changes. 161 * 162 * Please Note: We have chosen to use the number of columns in the shadow 163 * table as the canonical column count. This is important for tables that 164 * have colspans - the number of columns in the active table will always be 165 * less than the true number of columns. 166 * @type {?number} 167 */ 168cvox.TraverseTable.prototype.colCount = null; 169 170 171/** 172 * The number of rows in the active table. This is calculated at 173 * initialization and then only recalculated if the table changes. 174 * @type {?number} 175 */ 176cvox.TraverseTable.prototype.rowCount = null; 177 178 179/** 180 * The row headers in the active table. This is calculated at 181 * initialization and then only recalculated if the table changes. 182 * 183 * Please Note: 184 * Row headers are defined here as <TH> or <TD> elements. <TD> elements when 185 * serving as header cells must have either: 186 * - The scope attribute defined 187 * - Their IDs referenced in the header content attribute of another <TD> or 188 * <TH> element. 189 * 190 * The HTML5 spec specifies that only header <TH> elements can be row headers 191 * ( http://dev.w3.org/html5/spec/tabular-data.html#row-header ) but the 192 * HTML4 spec says that <TD> elements can act as both 193 * ( http://www.w3.org/TR/html401/struct/tables.html#h-11.2.6 ). In the 194 * interest of providing meaningful header information for all tables, here 195 * we take the position that <TD> elements can act as both. 196 * 197 * @type {Array} 198 */ 199cvox.TraverseTable.prototype.tableRowHeaders = null; 200 201 202/** 203 * The column headers in the active table. This is calculated at 204 * initialization and then only recalculated if the table changes. 205 * 206 * Please Note: see comment for tableRowHeaders. 207 * 208 * @type {Array} 209 */ 210cvox.TraverseTable.prototype.tableColHeaders = null; 211 212 213// TODO (stoarca): tighten up interface to {!Node} 214/** 215 * Initializes the class member variables. 216 * @param {Node} tableNode The table to be traversed. 217 */ 218cvox.TraverseTable.prototype.initialize = function(tableNode) { 219 if (!tableNode) { 220 return; 221 } 222 if (tableNode == this.activeTable_) { 223 return; 224 } 225 this.activeTable_ = tableNode; 226 this.currentCellCursor = null; 227 228 this.tableRowHeaders = []; 229 this.tableColHeaders = []; 230 231 this.buildShadowTable_(); 232 233 this.colCount = this.shadowColCount_(); 234 this.rowCount = this.countRows_(); 235 236 this.findHeaderCells_(); 237 238 // Listen for changes to the active table. If the active table changes, 239 // rebuild the shadow table. 240 // TODO (stoarca): Is this safe? When this object goes away, doesn't the 241 // eventListener stay on the node? Someone with better knowledge of js 242 // please confirm. If so, this is a leak. 243 this.activeTable_.addEventListener('DOMSubtreeModified', 244 goog.bind(function() { 245 this.buildShadowTable_(); 246 this.colCount = this.shadowColCount_(); 247 this.rowCount = this.countRows_(); 248 249 this.tableRowHeaders = []; 250 this.tableColHeaders = []; 251 this.findHeaderCells_(); 252 253 if (this.colCount == 0 && this.rowCount == 0) { 254 return; 255 } 256 257 if (this.getCell() == null) { 258 this.attachCursorToNearestCell_(); 259 } 260 }, this), false); 261}; 262 263 264/** 265 * Finds the cell cursor containing the specified node within the table. 266 * Returns null if there is no close cell. 267 * @param {!Node} node The node for which to find the cursor. 268 * @return {Array.<number>} The table index for the node. 269 */ 270cvox.TraverseTable.prototype.findNearestCursor = function(node) { 271 // TODO (stoarca): The current structure for representing the 272 // shadow table is not optimal for this query, but it's not urgent 273 // since this only gets executed at most once per user action. 274 275 // In case node is in a table but above any individual cell, we go down as 276 // deep as we can, being careful to avoid going into nested tables. 277 var n = node; 278 279 while (n.firstElementChild && 280 !(n.firstElementChild.tagName == 'TABLE' || 281 cvox.AriaUtil.isGrid(n.firstElementChild))) { 282 n = n.firstElementChild; 283 } 284 while (!cvox.DomPredicates.cellPredicate(cvox.DomUtil.getAncestors(n))) { 285 n = cvox.DomUtil.directedNextLeafNode(n); 286 // TODO(stoarca): Ugly logic. Captions should be part of tables. 287 // There have been a bunch of bugs as a result of 288 // DomUtil.findTableNodeInList excluding captions from tables because 289 // it makes them non-contiguous. 290 if (!cvox.DomUtil.getContainingTable(n, {allowCaptions: true})) { 291 return null; 292 } 293 } 294 for (var i = 0; i < this.rowCount; ++i) { 295 for (var j = 0; j < this.colCount; ++j) { 296 if (this.shadowTable_[i][j]) { 297 if (cvox.DomUtil.isDescendantOfNode( 298 n, this.shadowTable_[i][j].activeCell)) { 299 return [i, j]; 300 } 301 } 302 } 303 } 304 return null; 305}; 306 307/** 308 * Finds the valid cell nearest to the current cell cursor and moves the cell 309 * cursor there. To be used when the table has changed and the current cell 310 * cursor is now invalid (doesn't exist anymore). 311 * @private 312 */ 313cvox.TraverseTable.prototype.attachCursorToNearestCell_ = function() { 314 if (!this.currentCellCursor) { 315 // We have no idea. Just go 'somewhere'. Other code paths in this 316 // function go to the last cell, so let's do that! 317 this.goToLastCell(); 318 return; 319 } 320 321 var currentCursor = this.currentCellCursor; 322 323 // Does the current row still exist in the table? 324 var currentRow = this.shadowTable_[currentCursor[0]]; 325 if (currentRow) { 326 // Try last cell of current row 327 this.currentCellCursor = [currentCursor[0], (currentRow.length - 1)]; 328 } else { 329 // Current row does not exist anymore. Does current column still exist? 330 // Try last cell of current column 331 var numRows = this.shadowTable_.length; 332 if (numRows == 0) { 333 // Table has been deleted! 334 this.currentCellCursor = null; 335 return; 336 } 337 var aboveCell = 338 this.shadowTable_[numRows - 1][currentCursor[1]]; 339 if (aboveCell) { 340 this.currentCellCursor = [(numRows - 1), currentCursor[1]]; 341 } else { 342 // Current column does not exist anymore either. 343 // Move cursor to last cell in table. 344 this.goToLastCell(); 345 } 346 } 347}; 348 349 350/** 351 * Builds or rebuilds the shadow table by iterating through all of the cells 352 * ( <TD> or <TH> or role='gridcell' nodes) of the active table. 353 * @return {!Array} The shadow table. 354 * @private 355 */ 356cvox.TraverseTable.prototype.buildShadowTable_ = function() { 357 // Clear shadow table 358 this.shadowTable_ = []; 359 360 // Build shadow table structure. Initialize it as a 2D array. 361 var allRows = cvox.TableUtil.getChildRows(this.activeTable_); 362 var currentRowParent = null; 363 var currentRowGroup = null; 364 365 var colGroups = cvox.TableUtil.getColGroups(this.activeTable_); 366 var colToColGroup = cvox.TableUtil.determineColGroups(colGroups); 367 368 for (var ctr = 0; ctr < allRows.length; ctr++) { 369 this.shadowTable_.push([]); 370 } 371 372 // Iterate through active table by row 373 for (var i = 0; i < allRows.length; i++) { 374 var childCells = cvox.TableUtil.getChildCells(allRows[i]); 375 376 // Keep track of position in active table 377 var activeTableCol = 0; 378 // Keep track of position in shadow table 379 var shadowTableCol = 0; 380 381 while (activeTableCol < childCells.length) { 382 383 // Check to make sure we haven't already filled this cell. 384 if (this.shadowTable_[i][shadowTableCol] == null) { 385 386 var activeTableCell = childCells[activeTableCol]; 387 388 // Default value for colspan and rowspan is 1 389 var colsSpanned = 1; 390 var rowsSpanned = 1; 391 392 if (activeTableCell.hasAttribute('colspan')) { 393 394 colsSpanned = 395 parseInt(activeTableCell.getAttribute('colspan'), 10); 396 397 if ((isNaN(colsSpanned)) || (colsSpanned <= 0)) { 398 // The HTML5 spec defines colspan MUST be greater than 0: 399 // http://dev.w3.org/html5/spec/Overview.html#attr-tdth-colspan 400 // 401 // This is a change from the HTML4 spec: 402 // http://www.w3.org/TR/html401/struct/tables.html#adef-colspan 403 // 404 // We will degrade gracefully by treating a colspan=0 as 405 // equivalent to a colspan=1. 406 // Tested in method testColSpan0 in rowColSpanTable_test.js 407 colsSpanned = 1; 408 } 409 } 410 if (activeTableCell.hasAttribute('rowspan')) { 411 rowsSpanned = 412 parseInt(activeTableCell.getAttribute('rowspan'), 10); 413 414 if ((isNaN(rowsSpanned)) || (rowsSpanned <= 0)) { 415 // The HTML5 spec defines that rowspan can be any non-negative 416 // integer, including 0: 417 // http://dev.w3.org/html5/spec/Overview.html#attr-tdth-rowspan 418 // 419 // However, Chromium treats rowspan=0 as rowspan=1. This appears 420 // to be a bug from WebKit: 421 // https://bugs.webkit.org/show_bug.cgi?id=10300 422 // Inherited from a bug (since fixed) in KDE: 423 // http://bugs.kde.org/show_bug.cgi?id=41063 424 // 425 // We will follow Chromium and treat rowspan=0 as equivalent to 426 // rowspan=1. 427 // 428 // Tested in method testRowSpan0 in rowColSpanTable_test.js 429 // 430 // Filed as a bug in Chromium: http://crbug.com/58223 431 rowsSpanned = 1; 432 } 433 } 434 for (var r = 0; r < rowsSpanned; r++) { 435 for (var c = 0; c < colsSpanned; c++) { 436 var shadowNode = new ShadowTableNode(); 437 if ((r == 0) && (c == 0)) { 438 // This position is not spanned. 439 shadowNode.spanned = false; 440 shadowNode.rowSpan = false; 441 shadowNode.colSpan = false; 442 shadowNode.i = i; 443 shadowNode.j = shadowTableCol; 444 shadowNode.activeCell = activeTableCell; 445 shadowNode.rowHeaderCells = []; 446 shadowNode.colHeaderCells = []; 447 shadowNode.isRowHeader = false; 448 shadowNode.isColHeader = false; 449 } else { 450 // This position is spanned. 451 shadowNode.spanned = true; 452 shadowNode.rowSpan = (rowsSpanned > 1); 453 shadowNode.colSpan = (colsSpanned > 1); 454 shadowNode.i = i; 455 shadowNode.j = shadowTableCol; 456 shadowNode.activeCell = activeTableCell; 457 shadowNode.rowHeaderCells = []; 458 shadowNode.colHeaderCells = []; 459 shadowNode.isRowHeader = false; 460 shadowNode.isColHeader = false; 461 } 462 // Check this shadowNode to see if it is a candidate header cell 463 if (cvox.TableUtil.checkIfHeader(shadowNode.activeCell)) { 464 this.candidateHeaders_.push(shadowNode); 465 } else if (shadowNode.activeCell.hasAttribute('headers')) { 466 // This shadowNode has information about other header cells 467 this.candidateHeaders_.push(shadowNode); 468 } 469 470 // Check and update row group status. 471 if (currentRowParent == null) { 472 // This is the first row 473 currentRowParent = allRows[i].parentNode; 474 currentRowGroup = 0; 475 } else { 476 if (allRows[i].parentNode != currentRowParent) { 477 // We're in a different row group now 478 currentRowParent = allRows[i].parentNode; 479 currentRowGroup = currentRowGroup + 1; 480 } 481 } 482 shadowNode.rowGroup = currentRowGroup; 483 484 // Check and update col group status 485 if (colToColGroup.length > 0) { 486 shadowNode.colGroup = colToColGroup[shadowTableCol]; 487 } else { 488 shadowNode.colGroup = 0; 489 } 490 491 if (! shadowNode.spanned) { 492 if (activeTableCell.id != null) { 493 this.idToShadowNode_[activeTableCell.id] = shadowNode; 494 } 495 } 496 497 this.shadowTable_[i + r][shadowTableCol + c] = shadowNode; 498 } 499 } 500 shadowTableCol += colsSpanned; 501 activeTableCol++; 502 } else { 503 // This position has already been filled (by a previous cell that has 504 // a colspan or a rowspan) 505 shadowTableCol += 1; 506 } 507 } 508 } 509 return this.shadowTable_; 510}; 511 512 513/** 514 * Finds header cells from the list of candidate headers and classifies them 515 * in two ways: 516 * -- Identifies them for the entire table by adding them to 517 * this.tableRowHeaders and this.tableColHeaders. 518 * -- Identifies them for each shadow table node by adding them to the node's 519 * rowHeaderCells or colHeaderCells arrays. 520 * 521 * @private 522 */ 523cvox.TraverseTable.prototype.findHeaderCells_ = function() { 524 // Forming relationships between data cells and header cells: 525 // http://dev.w3.org/html5/spec/tabular-data.html 526 // #header-and-data-cell-semantics 527 528 for (var i = 0; i < this.candidateHeaders_.length; i++) { 529 530 var currentShadowNode = this.candidateHeaders_[i]; 531 var currentCell = currentShadowNode.activeCell; 532 533 var assumedScope = null; 534 var specifiedScope = null; 535 536 if (currentShadowNode.spanned) { 537 continue; 538 } 539 540 if ((currentCell.tagName == 'TH') && 541 !(currentCell.hasAttribute('scope'))) { 542 // No scope specified - compute scope ourselves. 543 // Go left/right - if there's a header node, then this is a column 544 // header 545 if (currentShadowNode.j > 0) { 546 if (this.shadowTable_[currentShadowNode.i][currentShadowNode.j - 1]. 547 activeCell.tagName == 'TH') { 548 assumedScope = 'col'; 549 } 550 } else if (currentShadowNode.j < this.shadowTable_[currentShadowNode.i]. 551 length - 1) { 552 if (this.shadowTable_[currentShadowNode.i][currentShadowNode.j + 1]. 553 activeCell.tagName == 'TH') { 554 assumedScope = 'col'; 555 } 556 } else { 557 // This row has a width of 1 cell, just assume this is a colum header 558 assumedScope = 'col'; 559 } 560 561 if (assumedScope == null) { 562 // Go up/down - if there's a header node, then this is a row header 563 if (currentShadowNode.i > 0) { 564 if (this.shadowTable_[currentShadowNode.i - 1][currentShadowNode.j]. 565 activeCell.tagName == 'TH') { 566 assumedScope = 'row'; 567 } 568 } else if (currentShadowNode.i < this.shadowTable_.length - 1) { 569 if (this.shadowTable_[currentShadowNode.i + 1][currentShadowNode.j]. 570 activeCell.tagName == 'TH') { 571 assumedScope = 'row'; 572 } 573 } else { 574 // This column has a height of 1 cell, just assume that this is 575 // a row header 576 assumedScope = 'row'; 577 } 578 } 579 } else if (currentCell.hasAttribute('scope')) { 580 specifiedScope = currentCell.getAttribute('scope'); 581 } else if (currentCell.hasAttribute('role') && 582 (currentCell.getAttribute('role') == 'rowheader')) { 583 specifiedScope = 'row'; 584 } else if (currentCell.hasAttribute('role') && 585 (currentCell.getAttribute('role') == 'columnheader')) { 586 specifiedScope = 'col'; 587 } 588 589 if ((specifiedScope == 'row') || (assumedScope == 'row')) { 590 currentShadowNode.isRowHeader = true; 591 592 // Go right until you hit the edge of the table or a data 593 // cell after another header cell. 594 // Add this cell to each shadowNode.rowHeaderCells attribute as you go. 595 for (var rightCtr = currentShadowNode.j; 596 rightCtr < this.shadowTable_[currentShadowNode.i].length; 597 rightCtr++) { 598 599 var rightShadowNode = this.shadowTable_[currentShadowNode.i][rightCtr]; 600 var rightCell = rightShadowNode.activeCell; 601 602 if ((rightCell.tagName == 'TH') || 603 (rightCell.hasAttribute('scope'))) { 604 605 if (rightCtr < this.shadowTable_[currentShadowNode.i].length - 1) { 606 var checkDataCell = 607 this.shadowTable_[currentShadowNode.i][rightCtr + 1]; 608 } 609 } 610 rightShadowNode.rowHeaderCells.push(currentCell); 611 } 612 this.tableRowHeaders.push(currentCell); 613 } else if ((specifiedScope == 'col') || (assumedScope == 'col')) { 614 currentShadowNode.isColHeader = true; 615 616 // Go down until you hit the edge of the table or a data cell 617 // after another header cell. 618 // Add this cell to each shadowNode.colHeaders attribute as you go. 619 620 for (var downCtr = currentShadowNode.i; 621 downCtr < this.shadowTable_.length; 622 downCtr++) { 623 624 var downShadowNode = this.shadowTable_[downCtr][currentShadowNode.j]; 625 if (downShadowNode == null) { 626 break; 627 } 628 var downCell = downShadowNode.activeCell; 629 630 if ((downCell.tagName == 'TH') || 631 (downCell.hasAttribute('scope'))) { 632 633 if (downCtr < this.shadowTable_.length - 1) { 634 var checkDataCell = 635 this.shadowTable_[downCtr + 1][currentShadowNode.j]; 636 } 637 } 638 downShadowNode.colHeaderCells.push(currentCell); 639 } 640 this.tableColHeaders.push(currentCell); 641 } else if (specifiedScope == 'rowgroup') { 642 currentShadowNode.isRowHeader = true; 643 644 // This cell is a row header for the rest of the cells in this row group. 645 var currentRowGroup = currentShadowNode.rowGroup; 646 647 // Get the rest of the cells in this row first 648 for (var cellsInRow = currentShadowNode.j + 1; 649 cellsInRow < this.shadowTable_[currentShadowNode.i].length; 650 cellsInRow++) { 651 this.shadowTable_[currentShadowNode.i][cellsInRow]. 652 rowHeaderCells.push(currentCell); 653 } 654 655 // Now propagate to rest of row group 656 for (var downCtr = currentShadowNode.i + 1; 657 downCtr < this.shadowTable_.length; 658 downCtr++) { 659 660 if (this.shadowTable_[downCtr][0].rowGroup != currentRowGroup) { 661 break; 662 } 663 664 for (var rightCtr = 0; 665 rightCtr < this.shadowTable_[downCtr].length; 666 rightCtr++) { 667 668 this.shadowTable_[downCtr][rightCtr]. 669 rowHeaderCells.push(currentCell); 670 } 671 } 672 this.tableRowHeaders.push(currentCell); 673 674 } else if (specifiedScope == 'colgroup') { 675 currentShadowNode.isColHeader = true; 676 677 // This cell is a col header for the rest of the cells in this col group. 678 var currentColGroup = currentShadowNode.colGroup; 679 680 // Get the rest of the cells in this colgroup first 681 for (var cellsInCol = currentShadowNode.j + 1; 682 cellsInCol < this.shadowTable_[currentShadowNode.i].length; 683 cellsInCol++) { 684 if (this.shadowTable_[currentShadowNode.i][cellsInCol].colGroup == 685 currentColGroup) { 686 this.shadowTable_[currentShadowNode.i][cellsInCol]. 687 colHeaderCells.push(currentCell); 688 } 689 } 690 691 // Now propagate to rest of col group 692 for (var downCtr = currentShadowNode.i + 1; 693 downCtr < this.shadowTable_.length; 694 downCtr++) { 695 696 for (var rightCtr = 0; 697 rightCtr < this.shadowTable_[downCtr].length; 698 rightCtr++) { 699 700 if (this.shadowTable_[downCtr][rightCtr].colGroup == 701 currentColGroup) { 702 this.shadowTable_[downCtr][rightCtr]. 703 colHeaderCells.push(currentCell); 704 } 705 } 706 } 707 this.tableColHeaders.push(currentCell); 708 } 709 if (currentCell.hasAttribute('headers')) { 710 this.findAttrbHeaders_(currentShadowNode); 711 } 712 if (currentCell.hasAttribute('aria-describedby')) { 713 this.findAttrbDescribedBy_(currentShadowNode); 714 } 715 } 716}; 717 718 719/** 720 * Finds header cells from the 'headers' attribute of a given shadow node's 721 * active cell and classifies them in two ways: 722 * -- Identifies them for the entire table by adding them to 723 * this.tableRowHeaders and this.tableColHeaders. 724 * -- Identifies them for the shadow table node by adding them to the node's 725 * rowHeaderCells or colHeaderCells arrays. 726 * Please note that header cells found through the 'headers' attribute are 727 * difficult to attribute to being either row or column headers because a 728 * table cell can declare arbitrary cells as its headers. A guess is made here 729 * based on which axis the header cell is closest to. 730 * 731 * @param {ShadowTableNode} currentShadowNode A shadow node with an active cell 732 * that has a 'headers' attribute. 733 * 734 * @private 735 */ 736cvox.TraverseTable.prototype.findAttrbHeaders_ = function(currentShadowNode) { 737 var activeTableCell = currentShadowNode.activeCell; 738 739 var idList = activeTableCell.getAttribute('headers').split(' '); 740 for (var idToken = 0; idToken < idList.length; idToken++) { 741 // Find cell(s) with this ID, add to header list 742 var idCellArray = cvox.TableUtil.getCellWithID(this.activeTable_, 743 idList[idToken]); 744 745 for (var idCtr = 0; idCtr < idCellArray.length; idCtr++) { 746 if (idCellArray[idCtr].id == activeTableCell.id) { 747 // Skip if the ID is the same as the current cell's ID 748 break; 749 } 750 // Check if this list of candidate headers contains a 751 // shadowNode with an active cell with this ID already 752 var possibleHeaderNode = 753 this.idToShadowNode_[idCellArray[idCtr].id]; 754 if (! cvox.TableUtil.checkIfHeader(possibleHeaderNode.activeCell)) { 755 // This listed header cell will not be handled later. 756 // Determine whether this is a row or col header for 757 // the active table cell 758 759 var iDiff = Math.abs(possibleHeaderNode.i - currentShadowNode.i); 760 var jDiff = Math.abs(possibleHeaderNode.j - currentShadowNode.j); 761 if ((iDiff == 0) || (iDiff < jDiff)) { 762 cvox.TableUtil.pushIfNotContained(currentShadowNode.rowHeaderCells, 763 possibleHeaderNode.activeCell); 764 cvox.TableUtil.pushIfNotContained(this.tableRowHeaders, 765 possibleHeaderNode.activeCell); 766 } else { 767 // This is a column header 768 cvox.TableUtil.pushIfNotContained(currentShadowNode.colHeaderCells, 769 possibleHeaderNode.activeCell); 770 cvox.TableUtil.pushIfNotContained(this.tableColHeaders, 771 possibleHeaderNode.activeCell); 772 } 773 } 774 } 775 } 776}; 777 778 779/** 780 * Finds header cells from the 'aria-describedby' attribute of a given shadow 781 * node's active cell and classifies them in two ways: 782 * -- Identifies them for the entire table by adding them to 783 * this.tableRowHeaders and this.tableColHeaders. 784 * -- Identifies them for the shadow table node by adding them to the node's 785 * rowHeaderCells or colHeaderCells arrays. 786 * 787 * Please note that header cells found through the 'aria-describedby' attribute 788 * must have the role='rowheader' or role='columnheader' attributes in order to 789 * be considered header cells. 790 * 791 * @param {ShadowTableNode} currentShadowNode A shadow node with an active cell 792 * that has an 'aria-describedby' attribute. 793 * 794 * @private 795 */ 796cvox.TraverseTable.prototype.findAttrbDescribedBy_ = 797 function(currentShadowNode) { 798 var activeTableCell = currentShadowNode.activeCell; 799 800 var idList = activeTableCell.getAttribute('aria-describedby').split(' '); 801 for (var idToken = 0; idToken < idList.length; idToken++) { 802 // Find cell(s) with this ID, add to header list 803 var idCellArray = cvox.TableUtil.getCellWithID(this.activeTable_, 804 idList[idToken]); 805 806 for (var idCtr = 0; idCtr < idCellArray.length; idCtr++) { 807 if (idCellArray[idCtr].id == activeTableCell.id) { 808 // Skip if the ID is the same as the current cell's ID 809 break; 810 } 811 // Check if this list of candidate headers contains a 812 // shadowNode with an active cell with this ID already 813 var possibleHeaderNode = 814 this.idToShadowNode_[idCellArray[idCtr].id]; 815 if (! cvox.TableUtil.checkIfHeader(possibleHeaderNode.activeCell)) { 816 // This listed header cell will not be handled later. 817 // Determine whether this is a row or col header for 818 // the active table cell 819 820 if (possibleHeaderNode.activeCell.hasAttribute('role') && 821 (possibleHeaderNode.activeCell.getAttribute('role') == 822 'rowheader')) { 823 cvox.TableUtil.pushIfNotContained(currentShadowNode.rowHeaderCells, 824 possibleHeaderNode.activeCell); 825 cvox.TableUtil.pushIfNotContained(this.tableRowHeaders, 826 possibleHeaderNode.activeCell); 827 } else if (possibleHeaderNode.activeCell.hasAttribute('role') && 828 (possibleHeaderNode.activeCell.getAttribute('role') == 829 'columnheader')) { 830 cvox.TableUtil.pushIfNotContained(currentShadowNode.colHeaderCells, 831 possibleHeaderNode.activeCell); 832 cvox.TableUtil.pushIfNotContained(this.tableColHeaders, 833 possibleHeaderNode.activeCell); 834 } 835 } 836 } 837 } 838}; 839 840 841/** 842 * Gets the current cell or null if there is no current cell. 843 * @return {?Node} The cell <TD> or <TH> or role='gridcell' node. 844 */ 845cvox.TraverseTable.prototype.getCell = function() { 846 if (!this.currentCellCursor || !this.shadowTable_) { 847 return null; 848 } 849 850 var shadowEntry = 851 this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; 852 853 return shadowEntry && shadowEntry.activeCell; 854}; 855 856 857/** 858 * Gets the cell at the specified location. 859 * @param {Array.<number>} index The index <i, j> of the required cell. 860 * @return {?Node} The cell <TD> or <TH> or role='gridcell' node at the 861 * specified location. Null if that cell does not exist. 862 */ 863cvox.TraverseTable.prototype.getCellAt = function(index) { 864 if (((index[0] < this.rowCount) && (index[0] >= 0)) && 865 ((index[1] < this.colCount) && (index[1] >= 0))) { 866 var shadowEntry = this.shadowTable_[index[0]][index[1]]; 867 if (shadowEntry != null) { 868 return shadowEntry.activeCell; 869 } 870 } 871 return null; 872}; 873 874 875/** 876 * Gets the cells that are row headers of the current cell. 877 * @return {!Array} The cells that are row headers of the current cell. Empty if 878 * the current cell does not have row headers. 879 */ 880cvox.TraverseTable.prototype.getCellRowHeaders = function() { 881 var shadowEntry = 882 this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; 883 884 return shadowEntry.rowHeaderCells; 885}; 886 887 888/** 889 * Gets the cells that are col headers of the current cell. 890 * @return {!Array} The cells that are col headers of the current cell. Empty if 891 * the current cell does not have col headers. 892 */ 893cvox.TraverseTable.prototype.getCellColHeaders = function() { 894 var shadowEntry = 895 this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; 896 897 return shadowEntry.colHeaderCells; 898}; 899 900 901/** 902 * Whether or not the current cell is spanned by another cell. 903 * @return {boolean} Whether or not the current cell is spanned by another cell. 904 */ 905cvox.TraverseTable.prototype.isSpanned = function() { 906 var shadowEntry = 907 this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; 908 909 return shadowEntry.spanned; 910}; 911 912 913/** 914 * Whether or not the current cell is a row header cell. 915 * @return {boolean} Whether or not the current cell is a row header cell. 916 */ 917cvox.TraverseTable.prototype.isRowHeader = function() { 918 var shadowEntry = 919 this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; 920 921 return shadowEntry.isRowHeader; 922}; 923 924 925/** 926 * Whether or not the current cell is a col header cell. 927 * @return {boolean} Whether or not the current cell is a col header cell. 928 */ 929cvox.TraverseTable.prototype.isColHeader = function() { 930 var shadowEntry = 931 this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; 932 933 return shadowEntry.isColHeader; 934}; 935 936 937/** 938 * Gets the active column, represented as an array of <TH> or <TD> nodes that 939 * make up a column. In this context, "active" means that this is the column 940 * that contains the cell the user is currently looking at. 941 * @return {Array} An array of <TH> or <TD> or role='gridcell' nodes. 942 */ 943cvox.TraverseTable.prototype.getCol = function() { 944 var colArray = []; 945 for (var i = 0; i < this.shadowTable_.length; i++) { 946 947 if (this.shadowTable_[i][this.currentCellCursor[1]]) { 948 var shadowEntry = this.shadowTable_[i][this.currentCellCursor[1]]; 949 950 if (shadowEntry.colSpan && shadowEntry.rowSpan) { 951 // Look at the last element in the column cell aray. 952 var prev = colArray[colArray.length - 1]; 953 if (prev != 954 shadowEntry.activeCell) { 955 // Watch out for positions spanned by a cell with rowspan and 956 // colspan. We don't want the same cell showing up multiple times 957 // in per-column cell lists. 958 colArray.push( 959 shadowEntry.activeCell); 960 } 961 } else if ((shadowEntry.colSpan) || (!shadowEntry.rowSpan)) { 962 colArray.push( 963 shadowEntry.activeCell); 964 } 965 } 966 } 967 return colArray; 968}; 969 970 971/** 972 * Gets the active row <TR> node. In this context, "active" means that this is 973 * the row that contains the cell the user is currently looking at. 974 * @return {Node} The active row node. 975 */ 976cvox.TraverseTable.prototype.getRow = function() { 977 var childRows = cvox.TableUtil.getChildRows(this.activeTable_); 978 return childRows[this.currentCellCursor[0]]; 979}; 980 981 982/** 983 * Gets the table summary text. 984 * 985 * @return {?string} Either: 986 * 1) The table summary text 987 * 2) Null if the table does not contain a summary attribute. 988 */ 989cvox.TraverseTable.prototype.summaryText = function() { 990 // see http://code.google.com/p/chromium/issues/detail?id=46567 991 // for information why this is necessary 992 if (!this.activeTable_.hasAttribute('summary')) { 993 return null; 994 } 995 return this.activeTable_.getAttribute('summary'); 996}; 997 998 999/** 1000 * Gets the table caption text. 1001 * 1002 * @return {?string} Either: 1003 * 1) The table caption text 1004 * 2) Null if the table does not include a caption tag. 1005 */ 1006cvox.TraverseTable.prototype.captionText = function() { 1007 // If there's more than one outer <caption> element, choose the first one. 1008 var captionNodes = cvox.XpathUtil.evalXPath('caption\[1]', 1009 this.activeTable_); 1010 if (captionNodes.length > 0) { 1011 return captionNodes[0].innerHTML; 1012 } else { 1013 return null; 1014 } 1015}; 1016 1017 1018/** 1019 * Calculates the number of columns in the shadow table. 1020 * @return {number} The number of columns in the shadow table. 1021 * @private 1022 */ 1023cvox.TraverseTable.prototype.shadowColCount_ = function() { 1024 // As the shadow table is a 2D array, the number of columns is the 1025 // max number of elements in the second-level arrays. 1026 var max = 0; 1027 for (var i = 0; i < this.shadowTable_.length; i++) { 1028 if (this.shadowTable_[i].length > max) { 1029 max = this.shadowTable_[i].length; 1030 } 1031 } 1032 return max; 1033}; 1034 1035 1036/** 1037 * Calculates the number of rows in the table. 1038 * @return {number} The number of rows in the table. 1039 * @private 1040 */ 1041cvox.TraverseTable.prototype.countRows_ = function() { 1042 // Number of rows in a table is equal to the number of TR elements contained 1043 // by the (outer) TBODY elements. 1044 var rowCount = cvox.TableUtil.getChildRows(this.activeTable_); 1045 return rowCount.length; 1046}; 1047 1048 1049/** 1050 * Calculates the number of columns in the table. 1051 * This uses the W3C recommended algorithm for calculating number of 1052 * columns, but it does not take rowspans or colspans into account. This means 1053 * that the number of columns calculated here might be lower than the actual 1054 * number of columns in the table if columns are indicated by colspans. 1055 * @return {number} The number of columns in the table. 1056 * @private 1057 */ 1058cvox.TraverseTable.prototype.getW3CColCount_ = function() { 1059 // See http://www.w3.org/TR/html401/struct/tables.html#h-11.2.4.3 1060 1061 var colgroupNodes = cvox.XpathUtil.evalXPath('child::colgroup', 1062 this.activeTable_); 1063 var colNodes = cvox.XpathUtil.evalXPath('child::col', this.activeTable_); 1064 1065 if ((colgroupNodes.length == 0) && (colNodes.length == 0)) { 1066 var maxcols = 0; 1067 var outerChildren = cvox.TableUtil.getChildRows(this.activeTable_); 1068 for (var i = 0; i < outerChildren.length; i++) { 1069 var childrenCount = cvox.TableUtil.getChildCells(outerChildren[i]); 1070 if (childrenCount.length > maxcols) { 1071 maxcols = childrenCount.length; 1072 } 1073 } 1074 return maxcols; 1075 } else { 1076 var sum = 0; 1077 for (var i = 0; i < colNodes.length; i++) { 1078 if (colNodes[i].hasAttribute('span')) { 1079 sum += colNodes[i].getAttribute('span'); 1080 } else { 1081 sum += 1; 1082 } 1083 } 1084 for (i = 0; i < colgroupNodes.length; i++) { 1085 var colChildren = cvox.XpathUtil.evalXPath('child::col', 1086 colgroupNodes[i]); 1087 if (colChildren.length == 0) { 1088 if (colgroupNodes[i].hasAttribute('span')) { 1089 sum += colgroupNodes[i].getAttribute('span'); 1090 } else { 1091 sum += 1; 1092 } 1093 } 1094 } 1095 } 1096 return sum; 1097}; 1098 1099 1100/** 1101 * Moves to the next row in the table. Updates the cell cursor. 1102 * 1103 * @return {boolean} Either: 1104 * 1) True if the update has been made. 1105 * 2) False if the end of the table has been reached and the update has not 1106 * happened. 1107 */ 1108cvox.TraverseTable.prototype.nextRow = function() { 1109 if (!this.currentCellCursor) { 1110 // We have not started moving through the table yet 1111 return this.goToRow(0); 1112 } else { 1113 return this.goToRow(this.currentCellCursor[0] + 1); 1114 } 1115 1116}; 1117 1118 1119/** 1120 * Moves to the previous row in the table. Updates the cell cursor. 1121 * 1122 * @return {boolean} Either: 1123 * 1) True if the update has been made. 1124 * 2) False if the end of the table has been reached and the update has not 1125 * happened. 1126 */ 1127cvox.TraverseTable.prototype.prevRow = function() { 1128 if (!this.currentCellCursor) { 1129 // We have not started moving through the table yet 1130 return this.goToRow(this.rowCount - 1); 1131 } else { 1132 return this.goToRow(this.currentCellCursor[0] - 1); 1133 } 1134}; 1135 1136 1137/** 1138 * Moves to the next column in the table. Updates the cell cursor. 1139 * 1140 * @return {boolean} Either: 1141 * 1) True if the update has been made. 1142 * 2) False if the end of the table has been reached and the update has not 1143 * happened. 1144 */ 1145cvox.TraverseTable.prototype.nextCol = function() { 1146 if (!this.currentCellCursor) { 1147 // We have not started moving through the table yet 1148 return this.goToCol(0); 1149 } else { 1150 return this.goToCol(this.currentCellCursor[1] + 1); 1151 } 1152}; 1153 1154 1155/** 1156 * Moves to the previous column in the table. Updates the cell cursor. 1157 * 1158 * @return {boolean} Either: 1159 * 1) True if the update has been made. 1160 * 2) False if the end of the table has been reached and the update has not 1161 * happened. 1162 */ 1163cvox.TraverseTable.prototype.prevCol = function() { 1164 if (!this.currentCellCursor) { 1165 // We have not started moving through the table yet 1166 return this.goToCol(this.shadowColCount_() - 1); 1167 } else { 1168 return this.goToCol(this.currentCellCursor[1] - 1); 1169 } 1170}; 1171 1172 1173/** 1174 * Moves to the row at the specified index in the table. Updates the cell 1175 * cursor. 1176 * @param {number} index The index of the required row. 1177 * @return {boolean} Either: 1178 * 1) True if the index is valid and the update has been made. 1179 * 2) False if the index is not valid (either less than 0 or greater than 1180 * the number of rows in the table). 1181 */ 1182cvox.TraverseTable.prototype.goToRow = function(index) { 1183 if (this.shadowTable_[index] != null) { 1184 if (this.currentCellCursor == null) { 1185 // We haven't started moving through the table yet 1186 this.currentCellCursor = [index, 0]; 1187 } else { 1188 this.currentCellCursor = [index, this.currentCellCursor[1]]; 1189 } 1190 return true; 1191 } else { 1192 return false; 1193 } 1194}; 1195 1196 1197/** 1198 * Moves to the column at the specified index in the table. Updates the cell 1199 * cursor. 1200 * @param {number} index The index of the required column. 1201 * @return {boolean} Either: 1202 * 1) True if the index is valid and the update has been made. 1203 * 2) False if the index is not valid (either less than 0 or greater than 1204 * the number of rows in the table). 1205 */ 1206cvox.TraverseTable.prototype.goToCol = function(index) { 1207 if (index < 0 || index >= this.colCount) { 1208 return false; 1209 } 1210 if (this.currentCellCursor == null) { 1211 // We haven't started moving through the table yet 1212 this.currentCellCursor = [0, index]; 1213 } else { 1214 this.currentCellCursor = [this.currentCellCursor[0], index]; 1215 } 1216 return true; 1217}; 1218 1219 1220/** 1221 * Moves to the cell at the specified index <i, j> in the table. Updates the 1222 * cell cursor. 1223 * @param {Array.<number>} index The index <i, j> of the required cell. 1224 * @return {boolean} Either: 1225 * 1) True if the index is valid and the update has been made. 1226 * 2) False if the index is not valid (either less than 0, greater than 1227 * the number of rows or columns in the table, or there is no cell 1228 * at that location). 1229 */ 1230cvox.TraverseTable.prototype.goToCell = function(index) { 1231 if (((index[0] < this.rowCount) && (index[0] >= 0)) && 1232 ((index[1] < this.colCount) && (index[1] >= 0))) { 1233 var cell = this.shadowTable_[index[0]][index[1]]; 1234 if (cell != null) { 1235 this.currentCellCursor = index; 1236 return true; 1237 } 1238 } 1239 return false; 1240}; 1241 1242 1243/** 1244 * Moves to the cell at the last index in the table. Updates the cell cursor. 1245 * @return {boolean} Either: 1246 * 1) True if the index is valid and the update has been made. 1247 * 2) False if the index is not valid (there is no cell at that location). 1248 */ 1249cvox.TraverseTable.prototype.goToLastCell = function() { 1250 var numRows = this.shadowTable_.length; 1251 if (numRows == 0) { 1252 return false; 1253 } 1254 var lastRow = this.shadowTable_[numRows - 1]; 1255 var lastIndex = [(numRows - 1), (lastRow.length - 1)]; 1256 var cell = 1257 this.shadowTable_[lastIndex[0]][lastIndex[1]]; 1258 if (cell != null) { 1259 this.currentCellCursor = lastIndex; 1260 return true; 1261 } 1262 return false; 1263}; 1264 1265 1266/** 1267 * Moves to the cell at the last index in the current row of the table. Update 1268 * the cell cursor. 1269 * @return {boolean} Either: 1270 * 1) True if the index is valid and the update has been made. 1271 * 2) False if the index is not valid (there is no cell at that location). 1272 */ 1273cvox.TraverseTable.prototype.goToRowLastCell = function() { 1274 var currentRow = this.currentCellCursor[0]; 1275 var lastIndex = [currentRow, (this.shadowTable_[currentRow].length - 1)]; 1276 var cell = 1277 this.shadowTable_[lastIndex[0]][lastIndex[1]]; 1278 if (cell != null) { 1279 this.currentCellCursor = lastIndex; 1280 return true; 1281 } 1282 return false; 1283}; 1284 1285 1286/** 1287 * Moves to the cell at the last index in the current column of the table. 1288 * Update the cell cursor. 1289 * @return {boolean} Either: 1290 * 1) True if the index is valid and the update has been made. 1291 * 2) False if the index is not valid (there is no cell at that location). 1292 */ 1293cvox.TraverseTable.prototype.goToColLastCell = function() { 1294 var currentCol = this.getCol(); 1295 var lastIndex = [(currentCol.length - 1), this.currentCellCursor[1]]; 1296 var cell = 1297 this.shadowTable_[lastIndex[0]][lastIndex[1]]; 1298 if (cell != null) { 1299 this.currentCellCursor = lastIndex; 1300 return true; 1301 } 1302 return false; 1303}; 1304 1305 1306/** 1307 * Resets the table cursors. 1308 * 1309 */ 1310cvox.TraverseTable.prototype.resetCursor = function() { 1311 this.currentCellCursor = null; 1312}; 1313