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 11 goog.provide('cvox.LayoutLineWalker'); 12 13 goog.require('cvox.AbstractWalker'); 14 goog.require('cvox.StructuralLineWalker'); 15 16 17 /** 18 * @constructor 19 * @extends {cvox.AbstractWalker} 20 */ 21 cvox.LayoutLineWalker = function() { 22 this.subWalker_ = new cvox.StructuralLineWalker(); 23 }; 24 goog.inherits(cvox.LayoutLineWalker, cvox.AbstractWalker); 25 26 27 /** 28 * @override 29 */ 30 cvox.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 */ 53 cvox.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 */ 70 cvox.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 */ 103 cvox.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 */ 132 cvox.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 */ 145 cvox.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 */ 179 cvox.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 */ 191 cvox.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 */ 214 cvox.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