table_walker.js revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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