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 * @fileoverview A class for walking tables. 7 * NOTE: This class has a very different interface than the other walkers. 8 * This means it does not lend itself easily to e.g. decorators. 9 * TODO (stoarca): This might be able to be fixed by breaking it up into 10 * separate walkers for cell, row and column. 11 */ 12 13 14goog.provide('cvox.TableWalker'); 15 16goog.require('cvox.AbstractWalker'); 17goog.require('cvox.BrailleUtil'); 18goog.require('cvox.DescriptionUtil'); 19goog.require('cvox.DomUtil'); 20goog.require('cvox.NavDescription'); 21goog.require('cvox.TraverseTable'); 22 23/** 24 * @constructor 25 * @extends {cvox.AbstractWalker} 26 */ 27cvox.TableWalker = function() { 28 cvox.AbstractWalker.call(this); 29 30 /** 31 * Only used as a cache for faster lookup. 32 * @type {!cvox.TraverseTable} 33 */ 34 this.tt = new cvox.TraverseTable(null); 35}; 36goog.inherits(cvox.TableWalker, cvox.AbstractWalker); 37 38/** 39 * @override 40 */ 41cvox.TableWalker.prototype.next = function(sel) { 42 // TODO (stoarca): See bug 6677953 43 return this.nextRow(sel); 44}; 45 46/** 47 * @override 48 */ 49cvox.TableWalker.prototype.sync = function(sel) { 50 return this.goTo_(sel, goog.bind(function(position) { 51 return this.tt.goToCell(position); 52 }, this)); 53}; 54 55/** 56 * @override 57 * @suppress {checkTypes} actual parameter 2 of 58 * cvox.Msgs.prototype.getMsg does not match formal parameter 59 * found : Array.<number> 60 * required: (Array.<string>|null|undefined) 61 */ 62cvox.TableWalker.prototype.getDescription = function(prevSel, sel) { 63 var position = this.syncPosition_(sel); 64 if (!position) { 65 return []; 66 } 67 this.tt.goToCell(position); 68 var descs = cvox.DescriptionUtil.getCollectionDescription(prevSel, sel); 69 if (descs.length == 0) { 70 descs.push(new cvox.NavDescription({ 71 annotation: cvox.ChromeVox.msgs.getMsg('empty_cell') 72 })); 73 } 74 return descs; 75}; 76 77/** 78 * @override 79 */ 80cvox.TableWalker.prototype.getBraille = function(prevSel, sel) { 81 var ret = new cvox.NavBraille({}); 82 var position = this.syncPosition_(sel); 83 if (position) { 84 var text = 85 cvox.BrailleUtil.getTemplated(prevSel.start.node, sel.start.node); 86 text.append(' ' + ++position[0] + '/' + ++position[1]); 87 } 88 return new cvox.NavBraille({text: text}); 89}; 90 91/** 92 * @override 93 */ 94cvox.TableWalker.prototype.getGranularityMsg = goog.abstractMethod; 95 96 97/** Table Actions. */ 98 99 100/** 101 * Returns the first cell of the table that this selection is inside. 102 * @param {!cvox.CursorSelection} sel The selection. 103 * @return {cvox.CursorSelection} The selection for first cell of the table. 104 * @expose 105 */ 106cvox.TableWalker.prototype.goToFirstCell = function(sel) { 107 return this.goTo_(sel, goog.bind(function(position) { 108 return this.tt.goToCell([0, 0]); 109 }, this)); 110}; 111 112/** 113 * Returns the last cell of the table that this selection is inside. 114 * @param {!cvox.CursorSelection} sel The selection. 115 * @return {cvox.CursorSelection} The selection for the last cell of the table. 116 * @expose 117 */ 118cvox.TableWalker.prototype.goToLastCell = function(sel) { 119 return this.goTo_(sel, goog.bind(function(position) { 120 return this.tt.goToLastCell(); 121 }, this)); 122}; 123 124/** 125 * Returns the first cell of the row that the selection is in. 126 * @param {!cvox.CursorSelection} sel The selection. 127 * @return {cvox.CursorSelection} The selection for the first cell in the row. 128 * @expose 129 */ 130cvox.TableWalker.prototype.goToRowFirstCell = function(sel) { 131 return this.goTo_(sel, goog.bind(function(position) { 132 return this.tt.goToCell([position[0], 0]); 133 }, this)); 134}; 135 136/** 137 * Returns the last cell of the row that the selection is in. 138 * @param {!cvox.CursorSelection} sel The selection. 139 * @return {cvox.CursorSelection} The selection for the last cell in the row. 140 * @expose 141 */ 142cvox.TableWalker.prototype.goToRowLastCell = function(sel) { 143 return this.goTo_(sel, goog.bind(function(position) { 144 return this.tt.goToRowLastCell(); 145 }, this)); 146}; 147 148/** 149 * Returns the first cell of the column that the selection is in. 150 * @param {!cvox.CursorSelection} sel The selection. 151 * @return {cvox.CursorSelection} The selection for the first cell in the col. 152 * @expose 153 */ 154cvox.TableWalker.prototype.goToColFirstCell = function(sel) { 155 return this.goTo_(sel, goog.bind(function(position) { 156 return this.tt.goToCell([0, position[1]]); 157 }, this)); 158}; 159 160/** 161 * Returns the last cell of the column that the selection is in. 162 * @param {!cvox.CursorSelection} sel The selection. 163 * @return {cvox.CursorSelection} The selection for the last cell in the col. 164 * @expose 165 */ 166cvox.TableWalker.prototype.goToColLastCell = function(sel) { 167 return this.goTo_(sel, goog.bind(function(position) { 168 return this.tt.goToColLastCell(); 169 }, this)); 170}; 171 172/** 173 * Returns the first cell in the row after the current selection. 174 * @param {!cvox.CursorSelection} sel The selection. 175 * @return {cvox.CursorSelection} The selection for the first cell in the next 176 * row. 177 * @expose 178 */ 179cvox.TableWalker.prototype.nextRow = function(sel) { 180 return this.goTo_(sel, goog.bind(function(position) { 181 return this.tt.goToCell([position[0] + (sel.isReversed() ? -1 : 1), 182 position[1]]); 183 }, this)); 184}; 185 186/** 187 * Returns the first cell in the column after the current selection. 188 * @param {!cvox.CursorSelection} sel The selection. 189 * @return {cvox.CursorSelection} The selection for the first cell in the 190 * next col. 191 * @expose 192 */ 193cvox.TableWalker.prototype.nextCol = function(sel) { 194 return this.goTo_(sel, goog.bind(function(position) { 195 return this.tt.goToCell([position[0], 196 position[1] + (sel.isReversed() ? -1 : 1)]); 197 }, this)); 198}; 199 200/** 201 * @param {!cvox.CursorSelection} sel The current selection. 202 * @return {cvox.CursorSelection} The resulting selection. 203 * @expose 204 */ 205cvox.TableWalker.prototype.announceHeaders = function(sel) { 206 cvox.ChromeVox.tts.speak(this.getHeaderText_(sel), 207 cvox.AbstractTts.QUEUE_MODE_FLUSH, 208 cvox.AbstractTts.PERSONALITY_ANNOTATION); 209 return sel; 210}; 211 212/** 213 * @param {!cvox.CursorSelection} sel The current selection. 214 * @return {cvox.CursorSelection} The resulting selection. 215 * @expose 216 */ 217cvox.TableWalker.prototype.speakTableLocation = function(sel) { 218 cvox.ChromeVox.navigationManager.speakDescriptionArray( 219 this.getLocationDescription_(sel), 220 cvox.AbstractTts.QUEUE_MODE_FLUSH, 221 null); 222 return sel; 223}; 224 225 226/** 227 * @param {!cvox.CursorSelection} sel The current selection. 228 * @return {cvox.CursorSelection} The resulting selection. 229 * @expose 230 */ 231cvox.TableWalker.prototype.exitShifterContent = function(sel) { 232 var tableNode = this.getTableNode_(sel); 233 if (!tableNode) { 234 return null; 235 } 236 var nextNode = cvox.DomUtil.directedNextLeafNode(tableNode, false); 237 return cvox.CursorSelection.fromNode(nextNode); 238}; 239 240 241/** End of actions. */ 242 243 244/** 245 * Returns the text content of the header(s) of the cell that contains sel. 246 * @param {!cvox.CursorSelection} sel The selection. 247 * @return {!string} The header text. 248 * @private 249 */ 250cvox.TableWalker.prototype.getHeaderText_ = function(sel) { 251 this.tt.initialize(this.getTableNode_(sel)); 252 var position = this.tt.findNearestCursor(sel.start.node); 253 if (!position) { 254 return cvox.ChromeVox.msgs.getMsg('not_inside_table'); 255 } 256 if (!this.tt.goToCell(position)) { 257 return cvox.ChromeVox.msgs.getMsg('not_inside_table'); 258 } 259 return ( 260 this.getRowHeaderText_(position) + 261 ' ' + 262 this.getColHeaderText_(position)); 263}; 264 265/** 266 * Returns the location description. 267 * @param {!cvox.CursorSelection} sel A valid selection. 268 * @return {Array.<cvox.NavDescription>} The location description. 269 * @suppress {checkTypes} actual parameter 2 of 270 * cvox.Msgs.prototype.getMsg does not match 271 * formal parameter 272 * found : Array.<number> 273 * required: (Array.<string>|null|undefined) 274 * @private 275 */ 276cvox.TableWalker.prototype.getLocationDescription_ = function(sel) { 277 var locationInfo = this.getLocationInfo(sel); 278 if (locationInfo == null) { 279 return null; 280 } 281 return [new cvox.NavDescription({ 282 text: cvox.ChromeVox.msgs.getMsg('table_location', locationInfo) 283 })]; 284}; 285 286/** 287 * Returns the text content of the row header(s) of the cell that contains sel. 288 * @param {!Array.<number>} position The selection. 289 * @return {!string} The header text. 290 * @private 291 */ 292cvox.TableWalker.prototype.getRowHeaderText_ = function(position) { 293 // TODO(stoarca): OPTMZ Replace with join(); 294 var rowHeaderText = ''; 295 296 var rowHeaders = this.tt.getCellRowHeaders(); 297 if (rowHeaders.length == 0) { 298 var firstCellInRow = this.tt.getCellAt([position[0], 0]); 299 rowHeaderText += cvox.DomUtil.collapseWhitespace( 300 cvox.DomUtil.getValue(firstCellInRow) + ' ' + 301 cvox.DomUtil.getName(firstCellInRow)); 302 return cvox.ChromeVox.msgs.getMsg('row_header') + rowHeaderText; 303 } 304 305 for (var i = 0; i < rowHeaders.length; ++i) { 306 rowHeaderText += cvox.DomUtil.collapseWhitespace( 307 cvox.DomUtil.getValue(rowHeaders[i]) + ' ' + 308 cvox.DomUtil.getName(rowHeaders[i])); 309 } 310 if (rowHeaderText == '') { 311 return cvox.ChromeVox.msgs.getMsg('empty_row_header'); 312 } 313 return cvox.ChromeVox.msgs.getMsg('row_header') + rowHeaderText; 314}; 315 316/** 317 * Returns the text content of the col header(s) of the cell that contains sel. 318 * @param {!Array.<number>} position The selection. 319 * @return {!string} The header text. 320 * @private 321 */ 322cvox.TableWalker.prototype.getColHeaderText_ = function(position) { 323 // TODO(stoarca): OPTMZ Replace with join(); 324 var colHeaderText = ''; 325 326 var colHeaders = this.tt.getCellColHeaders(); 327 if (colHeaders.length == 0) { 328 var firstCellInCol = this.tt.getCellAt([0, position[1]]); 329 colHeaderText += cvox.DomUtil.collapseWhitespace( 330 cvox.DomUtil.getValue(firstCellInCol) + ' ' + 331 cvox.DomUtil.getName(firstCellInCol)); 332 return cvox.ChromeVox.msgs.getMsg('column_header') + colHeaderText; 333 } 334 335 for (var i = 0; i < colHeaders.length; ++i) { 336 colHeaderText += cvox.DomUtil.collapseWhitespace( 337 cvox.DomUtil.getValue(colHeaders[i]) + ' ' + 338 cvox.DomUtil.getName(colHeaders[i])); 339 } 340 if (colHeaderText == '') { 341 return cvox.ChromeVox.msgs.getMsg('empty_row_header'); 342 } 343 return cvox.ChromeVox.msgs.getMsg('column_header') + colHeaderText; 344}; 345 346/** 347 * Returns the location info of sel within the containing table. 348 * @param {!cvox.CursorSelection} sel The selection. 349 * @return {Array.<number>} The location info: 350 * [row index, row count, col index, col count]. 351 */ 352cvox.TableWalker.prototype.getLocationInfo = function(sel) { 353 this.tt.initialize(this.getTableNode_(sel)); 354 var position = this.tt.findNearestCursor(sel.start.node); 355 if (!position) { 356 return null; 357 } 358 // + 1 to account for 0-indexed 359 return [ 360 position[0] + 1, 361 this.tt.rowCount, 362 position[1] + 1, 363 this.tt.colCount 364 ].map(function(x) {return cvox.ChromeVox.msgs.getNumber(x);}); 365}; 366 367/** 368 * Returns true if sel is inside a table. 369 * @param {!cvox.CursorSelection} sel The selection. 370 * @return {boolean} True if inside a table node. 371 */ 372cvox.TableWalker.prototype.isInTable = function(sel) { 373 return this.getTableNode_(sel) != null; 374}; 375 376/** 377 * Wrapper for going to somewhere so that boilerplate is not repeated. 378 * @param {!cvox.CursorSelection} sel The selection from which to base the 379 * movement. 380 * @param {function(Array.<number>):boolean} f The function to use for moving. 381 * Returns true on success and false on failure. 382 * @return {cvox.CursorSelection} The resulting selection. 383 * @private 384 */ 385cvox.TableWalker.prototype.goTo_ = function(sel, f) { 386 this.tt.initialize(this.getTableNode_(sel)); 387 var position = this.tt.findNearestCursor(sel.end.node); 388 if (!position) { 389 return null; 390 } 391 this.tt.goToCell(position); 392 if (!f(position)) { 393 return null; 394 } 395 return cvox.CursorSelection.fromNode(this.tt.getCell()). 396 setReversed(sel.isReversed()); 397}; 398 399/** 400 * Returns the nearest table node containing the end of the selection 401 * @param {!cvox.CursorSelection} sel The selection. 402 * @return {Node} The table node containing sel. null if not in a table. 403 * @private 404 */ 405cvox.TableWalker.prototype.getTableNode_ = function(sel) { 406 return cvox.DomUtil.getContainingTable(sel.end.node); 407}; 408 409/** 410 * Sync the backing traversal utility to the given selection. 411 * @param {!cvox.CursorSelection} sel The selection. 412 * @return {Array.<number>} The position [x, y] of the selection. 413 * @private 414 */ 415cvox.TableWalker.prototype.syncPosition_ = function(sel) { 416 var tableNode = this.getTableNode_(sel); 417 this.tt.initialize(tableNode); 418 // we need to align the TraverseTable with our sel because our walker 419 // uses parts of it (for example isSpanned relies on being at a specific cell) 420 return this.tt.findNearestCursor(sel.end.node); 421}; 422