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 Defines the ContentEditableExtractor class. 7 */ 8 9 goog.provide('cvox.ContentEditableExtractor'); 10 11 goog.require('cvox.Cursor'); 12 goog.require('cvox.TraverseUtil'); 13 14 /** 15 * Extracts the text and line break information from a contenteditable region. 16 * @constructor 17 */ 18 cvox.ContentEditableExtractor = function() { 19 /** 20 * The extracted, flattened, text. 21 * @type {string} 22 * @private 23 */ 24 this.text_ = ''; 25 26 /** 27 * The start cursor/selection index. 28 * @type {number} 29 * @private 30 */ 31 this.start_ = 0; 32 33 /** 34 * The end cursor/selection index. 35 * @type {number} 36 * @private 37 */ 38 this.end_ = 0; 39 40 /** 41 * Map from line index to a data structure containing the start 42 * and end index within the line. 43 * @type {Object.<number, {startIndex: number, endIndex: number}>} 44 * @private 45 */ 46 this.lines_ = {}; 47 48 /** 49 * Map from 0-based character index to 0-based line index. 50 * @type {Array.<number>} 51 * @private 52 */ 53 this.characterToLineMap_ = []; 54 }; 55 56 /** 57 * Update the metadata. 58 * @param {Element} element The DOM element that's contentEditable. 59 */ 60 cvox.ContentEditableExtractor.prototype.update = function(element) { 61 /** 62 * Map from line index to a data structure containing the start 63 * and end index within the line. 64 * @type {Object.<number, {startIndex: number, endIndex: number}>} 65 */ 66 var lines = {0: {startIndex: 0, endIndex: 0}}; 67 var startCursor = new cvox.Cursor(element, 0, ''); 68 var endCursor = startCursor.clone(); 69 var range = document.createRange(); 70 var rect; 71 var lineIndex = 0; 72 var lastBottom = null; 73 var text = ''; 74 var textSize = 0; 75 var selectionStartIndex = -1; 76 var selectionEndIndex = -1; 77 var sel = window.getSelection(); 78 var selectionStart = new cvox.Cursor(sel.baseNode, sel.baseOffset, ''); 79 var selectionEnd = new cvox.Cursor(sel.extentNode, sel.extentOffset, ''); 80 var setStart = false; 81 var setEnd = false; 82 while (true) { 83 var entered = []; 84 var left = []; 85 var c = cvox.TraverseUtil.forwardsChar(endCursor, entered, left); 86 var done = false; 87 if (!c) { 88 done = true; 89 } 90 for (var i = 0; i < left.length && !done; i++) { 91 if (left[i] == element) { 92 done = true; 93 } 94 } 95 if (done) { 96 break; 97 } 98 99 range.setStart(startCursor.node, startCursor.index); 100 range.setEnd(endCursor.node, endCursor.index); 101 rect = range.getBoundingClientRect(); 102 if (!rect || rect.width == 0 || rect.height == 0) { 103 continue; 104 } 105 106 if (lastBottom !== null && 107 rect.bottom != lastBottom && 108 textSize > 0 && 109 text.substr(-1).match(/\S/) && 110 c.match(/\S/)) { 111 text += '\n'; 112 textSize++; 113 } 114 115 if (startCursor.node != endCursor.node && endCursor.index > 0) { 116 range.setStart(endCursor.node, endCursor.index - 1); 117 rect = range.getBoundingClientRect(); 118 if (!rect || rect.width == 0 || rect.height == 0) { 119 continue; 120 } 121 } 122 123 if (!setStart && 124 selectionStartIndex == -1 && 125 endCursor.node == selectionStart.node && 126 endCursor.index >= selectionStart.index) { 127 if (endCursor.index > selectionStart.index) { 128 selectionStartIndex = textSize; 129 } else { 130 setStart = true; 131 } 132 } 133 if (!setEnd && 134 selectionEndIndex == -1 && 135 endCursor.node == selectionEnd.node && 136 endCursor.index >= selectionEnd.index) { 137 if (endCursor.index > selectionEnd.index) { 138 selectionEndIndex = textSize; 139 } else { 140 setEnd = true; 141 } 142 } 143 144 if (lastBottom === null) { 145 // This is the first character we've successfully measured on this 146 // line. Save the vertical position but don't do anything else. 147 lastBottom = rect.bottom; 148 } else if (rect.bottom != lastBottom) { 149 lines[lineIndex].endIndex = textSize; 150 lineIndex++; 151 lines[lineIndex] = {startIndex: textSize, endIndex: textSize}; 152 lastBottom = rect.bottom; 153 } 154 text += c; 155 textSize++; 156 startCursor = endCursor.clone(); 157 158 if (setStart) { 159 selectionStartIndex = textSize; 160 setStart = false; 161 } 162 if (setEnd) { 163 selectionEndIndex = textSize; 164 setEnd = false; 165 } 166 } 167 168 // Finish up the last line. 169 lines[lineIndex].endIndex = textSize; 170 171 // Create a map from character index to line number. 172 var characterToLineMap = []; 173 for (var i = 0; i <= lineIndex; i++) { 174 for (var j = lines[i].startIndex; j <= lines[i].endIndex; j++) { 175 characterToLineMap[j] = i; 176 } 177 } 178 179 // Finish updating fields. 180 this.text_ = text; 181 this.characterToLineMap_ = characterToLineMap; 182 this.lines_ = lines; 183 184 this.start_ = selectionStartIndex >= 0 ? selectionStartIndex : text.length; 185 this.end_ = selectionEndIndex >= 0 ? selectionEndIndex : text.length; 186 }; 187 188 /** 189 * Get the text. 190 * @return {string} The extracted, flattened, text. 191 */ 192 cvox.ContentEditableExtractor.prototype.getText = function() { 193 return this.text_; 194 }; 195 196 /** 197 * @return {number} The start cursor/selection index. 198 */ 199 cvox.ContentEditableExtractor.prototype.getStartIndex = function() { 200 return this.start_; 201 }; 202 203 /** 204 * @return {number} The end cursor/selection index. 205 */ 206 cvox.ContentEditableExtractor.prototype.getEndIndex = function() { 207 return this.end_; 208 }; 209 210 /** 211 * Get the line number corresponding to a particular index. 212 * @param {number} index The 0-based character index. 213 * @return {number} The 0-based line number corresponding to that character. 214 */ 215 cvox.ContentEditableExtractor.prototype.getLineIndex = function(index) { 216 return this.characterToLineMap_[index]; 217 }; 218 219 /** 220 * Get the start character index of a line. 221 * @param {number} index The 0-based line index. 222 * @return {number} The 0-based index of the first character in this line. 223 */ 224 cvox.ContentEditableExtractor.prototype.getLineStart = function(index) { 225 return this.lines_[index].startIndex; 226 }; 227 228 /** 229 * Get the end character index of a line. 230 * @param {number} index The 0-based line index. 231 * @return {number} The 0-based index of the end of this line. 232 */ 233 cvox.ContentEditableExtractor.prototype.getLineEnd = function(index) { 234 return this.lines_[index].endIndex; 235 }; 236