layout_line_walker.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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 JavaScript class for walking lines consisting of one or more
7 * clickable nodes.
8 */
9
10
11goog.provide('cvox.LayoutLineWalker');
12
13goog.require('cvox.AbstractWalker');
14goog.require('cvox.StructuralLineWalker');
15
16
17/**
18 * @constructor
19 * @extends {cvox.AbstractWalker}
20 */
21cvox.LayoutLineWalker = function() {
22  this.subWalker_ = new cvox.StructuralLineWalker();
23};
24goog.inherits(cvox.LayoutLineWalker, cvox.AbstractWalker);
25
26
27/**
28 * @override
29 */
30cvox.LayoutLineWalker.prototype.next = function(sel) {
31  // Collapse selection to the directed end.
32  var endSel = new cvox.CursorSelection(sel.end, sel.end, sel.isReversed());
33
34  // Sync to the line.
35  var sync = this.subWalker_.sync(endSel);
36  if (!sync) {
37    return null;
38  }
39
40  // Compute the next selection.
41  var start = this.subWalker_.next(endSel);
42  if (!start) {
43    return null;
44  }
45  start.setReversed(sel.isReversed());
46  return this.extend_(start).setReversed(false);
47};
48
49
50/**
51 * @override
52 */
53cvox.LayoutLineWalker.prototype.sync = function(sel) {
54  var line = this.subWalker_.sync(sel);
55  if (!line) {
56    return null;
57  }
58
59  // Extend to both line breaks (in each direction).
60  var end = this.extend_(line);
61  var start = this.extend_(line.setReversed(!line.isReversed()));
62
63  return new cvox.CursorSelection(start.end, end.end, sel.isReversed());
64};
65
66
67/**
68 * @override
69 */
70cvox.LayoutLineWalker.prototype.getDescription = function(prevSel, sel) {
71  var descriptions = [];
72  var prev = prevSel;
73  var absSel = sel.clone().setReversed(false);
74  var cur = new cvox.CursorSelection(absSel.start, absSel.start);
75  cur = this.subWalker_.sync(cur);
76  if (!cur) {
77    return [];
78  }
79
80  // No need to accumulate descriptions.
81  if (absSel.start.node == absSel.end.node) {
82    return this.subWalker_.getDescription(prevSel, sel);
83  }
84
85  // Walk through and collect descriptions for each line.
86  while (cur && !cur.end.equals(absSel.end)) {
87    descriptions.push.apply(
88        descriptions, this.subWalker_.getDescription(prev, cur));
89    prev = cur;
90    cur = this.subWalker_.next(cur);
91  }
92  if (cur) {
93    descriptions.push.apply(
94        descriptions, this.subWalker_.getDescription(prev, cur));
95  }
96  return descriptions;
97};
98
99
100/**
101 * @override
102 */
103cvox.LayoutLineWalker.prototype.getBraille = function(prevSel, sel) {
104  var braille = new cvox.NavBraille({});
105  var absSel = this.subWalker_.sync(sel.clone().setReversed(false));
106  var layoutSel = this.sync(sel).setReversed(false);
107  if (!layoutSel || !absSel) {
108    return braille;
109  }
110  var cur = new cvox.CursorSelection(layoutSel.start, layoutSel.start);
111  cur = this.subWalker_.sync(cur);
112  if (!cur) {
113    return braille;
114  }
115
116  // Walk through and collect braille for each line.
117  while (cur && !cur.end.equals(layoutSel.end)) {
118    this.appendBraille_(prevSel, absSel, cur, braille);
119    prevSel = cur;
120    cur = this.subWalker_.next(cur);
121  }
122  if (cur) {
123    this.appendBraille_(prevSel, absSel, cur, braille);
124  }
125  return braille;
126};
127
128
129/**
130 * @override
131 */
132cvox.LayoutLineWalker.prototype.getGranularityMsg = function() {
133  return cvox.ChromeVox.msgs.getMsg('layout_line');
134};
135
136
137/**
138 * Compares two selections and determines if the lie on the same horizontal
139 * line as determined by their bounding rectangles.
140 * @param {!cvox.CursorSelection} lSel Left selection.
141 * @param {!cvox.CursorSelection} rSel Right selection.
142 * @return {boolean} Whether lSel and rSel are on different visual lines.
143 * @private
144 */
145cvox.LayoutLineWalker.prototype.isVisualLineBreak_ = function(lSel, rSel) {
146  if (this.wantsOwnLine_(lSel.end.node) ||
147      this.wantsOwnLine_(rSel.start.node)) {
148    return true;
149  }
150  var lRect = lSel.getRange().getBoundingClientRect();
151  var rRect = rSel.getRange().getBoundingClientRect();
152
153  // Some ranges from the browser give us 0-sized rects (such as in the case of
154  // select's). Detect these cases and use a more reliable method (take the
155  // bounding rect of the actual element rather than the range).
156  if (lRect.width == 0 &&
157      lRect.height == 0 &&
158      lSel.end.node.nodeType == Node.ELEMENT_NODE) {
159    lRect = lSel.end.node.getBoundingClientRect();
160  }
161
162  if (rRect.width == 0 &&
163      rRect.height == 0 &&
164      rSel.start.node.nodeType == Node.ELEMENT_NODE) {
165    rRect = rSel.start.node.getBoundingClientRect();
166  }
167  return lRect.bottom != rRect.bottom;
168};
169
170
171/**
172 * Determines if node should force a line break.
173 * This is used for elements with unusual semantics, such as multi-line
174 * text fields, where the behaviour would otherwise be confusing.
175 * @param {!Node} node Node.
176 * @return {boolean} True if the node should appear next to a line break.
177 * @private
178 */
179cvox.LayoutLineWalker.prototype.wantsOwnLine_ = function(node) {
180  return node instanceof HTMLTextAreaElement ||
181      node.parentNode instanceof HTMLTextAreaElement;
182};
183
184
185/**
186 * Extends a given cursor selection up to the next visual line break.
187 * @param {!cvox.CursorSelection} start The selection.
188 * @return {!cvox.CursorSelection} The resulting selection.
189 * @private
190 */
191cvox.LayoutLineWalker.prototype.extend_ = function(start) {
192  // Extend the selection up to just before a new visual line break.
193  var end = start;
194  var next = start;
195
196  do {
197    end = next;
198    next = this.subWalker_.next(end);
199  } while (next && !this.isVisualLineBreak_(end, next));
200  return new cvox.CursorSelection(start.start, end.end, start.isReversed());
201};
202
203
204/**
205 * Private routine to append braille given three selections.
206 * @param {!cvox.CursorSelection} prevSel A previous selection in walker
207 * ordering.
208 * @param {!cvox.CursorSelection} sel A selection that represents the location
209 * of the braille cursor.
210 * @param {!cvox.CursorSelection} cur The specific selection to append.
211 * @param {!cvox.NavBraille} braille Braille on which to append.
212 * @private
213 */
214cvox.LayoutLineWalker.prototype.appendBraille_ = function(
215    prevSel, sel, cur, braille) {
216  var item = this.subWalker_.getBraille(prevSel, cur).text;
217  var valueSelectionSpan = item.getSpanInstanceOf(
218      cvox.BrailleUtil.ValueSelectionSpan);
219
220  if (braille.text.getLength() > 0) {
221    braille.text.append(cvox.BrailleUtil.ITEM_SEPARATOR);
222  }
223
224  // Find the surrounding logical "leaf node".
225  // This prevents us from labelling the braille output with the wrong node,
226  // such as a text node child of a <textarea>.
227  var node = cur.start.node;
228  while (node.parentNode && cvox.DomUtil.isLeafNode(node.parentNode)) {
229    node = node.parentNode;
230  }
231
232  var nodeStart = braille.text.getLength();
233  var nodeEnd = nodeStart + item.getLength();
234  braille.text.append(item);
235  braille.text.setSpan(node, nodeStart, nodeEnd);
236
237  if (sel && cur.absEquals(sel)) {
238    if (valueSelectionSpan) {
239      braille.startIndex = nodeStart + item.getSpanStart(valueSelectionSpan);
240      braille.endIndex = nodeStart + item.getSpanEnd(valueSelectionSpan);
241    } else {
242      braille.startIndex = nodeStart;
243      braille.endIndex = nodeStart + 1;
244    }
245  }
246};
247