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 Simple class to represent a cursor selection. 7 * A cursor selection is just two cursors; one for the start and one for 8 * the end of some interval in the document. 9 */ 10 11 goog.provide('cvox.CursorSelection'); 12 13 goog.require('cvox.Cursor'); 14 goog.require('cvox.SelectionUtil'); 15 goog.require('cvox.TraverseUtil'); 16 17 18 /** 19 * If the start node and end node are the same, and the indexes are the same, 20 * the selection is interpreted to be a node. Otherwise, it is interpreted 21 * to be a range. 22 * @param {!cvox.Cursor} start The starting cursor. 23 * @param {!cvox.Cursor} end The ending cursor. 24 * @param {boolean=} opt_reverse Whether to make it a reversed selection or 25 * not. Default is selection is not reversed. If start and end are in the 26 * wrong order, they will be swapped automatically. 27 * NOTE: Can't infer automatically whether the selection is reversed because 28 * for a selection on a single node, the start and end are equal. 29 * @constructor 30 */ 31 cvox.CursorSelection = function(start, end, opt_reverse) { 32 this.start = start.clone(); 33 this.end = end.clone(); 34 35 if (opt_reverse == undefined) { 36 opt_reverse = false; 37 } 38 /** @private */ 39 this.isReversed_ = opt_reverse; 40 41 if ((this.isReversed_ && 42 this.start.node.compareDocumentPosition(this.end.node) == 43 cvox.CursorSelection.BEFORE) || 44 (!this.isReversed_ && 45 this.end.node.compareDocumentPosition(this.start.node) == 46 cvox.CursorSelection.BEFORE)) { 47 var oldStart = this.start; 48 this.start = this.end; 49 this.end = oldStart; 50 } 51 }; 52 53 54 /** 55 * From http://www.w3schools.com/jsref/met_node_comparedocumentposition.asp 56 */ 57 cvox.CursorSelection.BEFORE = 4; 58 59 60 /** 61 * If true, ensures that this selection is reversed. Otherwise, ensures that 62 * it is not reversed. 63 * @param {boolean} reversed True to reverse. False to nonreverse. 64 * @return {!cvox.CursorSelection} For chaining. 65 */ 66 cvox.CursorSelection.prototype.setReversed = function(reversed) { 67 if (reversed == this.isReversed_) { 68 return this; 69 } 70 var oldStart = this.start; 71 this.start = this.end; 72 this.end = oldStart; 73 this.isReversed_ = reversed; 74 return this; 75 }; 76 77 78 /** 79 * Returns true if this selection is a reverse selection. 80 * @return {boolean} true if reversed. 81 */ 82 cvox.CursorSelection.prototype.isReversed = function() { 83 return this.isReversed_; 84 }; 85 86 87 /** 88 * Returns start if not reversed, end if reversed. 89 * @return {!cvox.Cursor} start if not reversed, end if reversed. 90 */ 91 cvox.CursorSelection.prototype.absStart = function() { 92 return this.isReversed_ ? this.end : this.start; 93 }; 94 95 /** 96 * Returns end if not reversed, start if reversed. 97 * @return {!cvox.Cursor} end if not reversed, start if reversed. 98 */ 99 cvox.CursorSelection.prototype.absEnd = function() { 100 return this.isReversed_ ? this.start : this.end; 101 }; 102 103 104 /** 105 * Clones the selection. 106 * @return {!cvox.CursorSelection} The cloned selection. 107 */ 108 cvox.CursorSelection.prototype.clone = function() { 109 return new cvox.CursorSelection(this.start, this.end, this.isReversed_); 110 }; 111 112 113 /** 114 * Places a DOM selection around this CursorSelection. 115 */ 116 cvox.CursorSelection.prototype.select = function() { 117 var sel = window.getSelection(); 118 sel.removeAllRanges(); 119 this.normalize(); 120 sel.addRange(this.getRange()); 121 }; 122 123 124 /** 125 * Creates a new cursor selection that starts and ends at the node. 126 * Returns null if node is null. 127 * @param {Node} node The node. 128 * @return {cvox.CursorSelection} The selection. 129 */ 130 cvox.CursorSelection.fromNode = function(node) { 131 if (!node) { 132 return null; 133 } 134 var text = cvox.TraverseUtil.getNodeText(node); 135 136 return new cvox.CursorSelection( 137 new cvox.Cursor(node, 0, text), 138 new cvox.Cursor(node, 0, text)); 139 }; 140 141 142 /** 143 * Creates a new cursor selection that starts and ends at document.body. 144 * @return {!cvox.CursorSelection} The selection. 145 */ 146 cvox.CursorSelection.fromBody = function() { 147 return /** @type {!cvox.CursorSelection} */ ( 148 cvox.CursorSelection.fromNode(document.body)); 149 }; 150 151 /** 152 * Returns the text that the selection spans. 153 * @return {string} Text within the selection. '' if it is a node selection. 154 */ 155 cvox.CursorSelection.prototype.getText = function() { 156 if (this.start.equals(this.end)) { 157 return cvox.TraverseUtil.getNodeText(this.start.node); 158 } 159 return cvox.SelectionUtil.getRangeText(this.getRange()); 160 }; 161 162 /** 163 * Returns a range from the given selection. 164 * @return {Range} The range. 165 */ 166 cvox.CursorSelection.prototype.getRange = function() { 167 var range = document.createRange(); 168 if (this.isReversed_) { 169 range.setStart(this.end.node, this.end.index); 170 range.setEnd(this.start.node, this.start.index); 171 } else { 172 range.setStart(this.start.node, this.start.index); 173 range.setEnd(this.end.node, this.end.index); 174 } 175 return range; 176 }; 177 178 /** 179 * Check for equality. 180 * @param {!cvox.CursorSelection} rhs The CursorSelection to compare against. 181 * @return {boolean} True if equal. 182 */ 183 cvox.CursorSelection.prototype.equals = function(rhs) { 184 return this.start.equals(rhs.start) && this.end.equals(rhs.end); 185 }; 186 187 /** 188 * Check for equality regardless of direction. 189 * @param {!cvox.CursorSelection} rhs The CursorSelection to compare against. 190 * @return {boolean} True if equal. 191 */ 192 cvox.CursorSelection.prototype.absEquals = function(rhs) { 193 return ((this.start.equals(rhs.start) && this.end.equals(rhs.end)) || 194 (this.end.equals(rhs.start) && this.start.equals(rhs.end))); 195 }; 196 197 /** 198 * Determines if this starts before another CursorSelection in document order. 199 * If this is reversed, then a reversed document order is checked. 200 * In the case that this and rhs start at the same position, we return true. 201 * @param {!cvox.CursorSelection} rhs The selection to compare. 202 * @return {boolean} True if this is before rhs. 203 */ 204 cvox.CursorSelection.prototype.directedBefore = function(rhs) { 205 var leftToRight = this.start.node.compareDocumentPosition(rhs.start.node) == 206 cvox.CursorSelection.BEFORE; 207 return this.start.node == rhs.start.node || 208 (this.isReversed() ? !leftToRight : leftToRight); 209 }; 210 /** 211 * Normalizes this selection. 212 * Use this routine to adjust CursorSelection's that have been collapsed due to 213 * convention such as when a CursorSelection references a node without attention 214 * to its endpoints. 215 * The result is to surround the node with this cursor. 216 * @return {!cvox.CursorSelection} The normalized selection. 217 */ 218 cvox.CursorSelection.prototype.normalize = function() { 219 if (this.absEnd().index == 0 && this.absEnd().node) { 220 var node = this.absEnd().node; 221 222 // DOM ranges use different conventions when surrounding a node. For 223 // instance, input nodes endOffset is always 0 while h1's endOffset is 1 224 //with both having no children. Use a range to compute the endOffset. 225 var testRange = document.createRange(); 226 testRange.selectNodeContents(node); 227 this.absEnd().index = testRange.endOffset; 228 } 229 return this; 230 }; 231 232 /** 233 * Collapses to the directed start of the selection. 234 * @return {!cvox.CursorSelection} For chaining. 235 */ 236 cvox.CursorSelection.prototype.collapse = function() { 237 // Not a selection. 238 if (this.start.equals(this.end)) { 239 return this; 240 } 241 this.end.copyFrom(this.start); 242 if (this.start.text.length == 0) { 243 return this; 244 } 245 if (this.isReversed()) { 246 if (this.end.index > 0) { 247 this.end.index--; 248 } 249 } else { 250 if (this.end.index < this.end.text.length) { 251 this.end.index++; 252 } 253 } 254 return this; 255 }; 256