editable_text_area_shadow.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 Defines the EditableTextAreaShadow class. 7 */ 8 9goog.provide('cvox.EditableTextAreaShadow'); 10 11/** 12 * Creates a shadow element for an editable text area used to compute line 13 * numbers. 14 * @constructor 15 */ 16cvox.EditableTextAreaShadow = function() { 17 /** 18 * @type {Element} 19 * @private 20 */ 21 this.shadowElement_ = document.createElement('div'); 22 23 /** 24 * Map from line index to a data structure containing the start 25 * and end index within the line. 26 * @type {Object.<number, {startIndex: number, endIndex: number}>} 27 * @private 28 */ 29 this.lines_ = {}; 30 31 /** 32 * Map from 0-based character index to 0-based line index. 33 * @type {Array.<number>} 34 * @private 35 */ 36 this.characterToLineMap_ = []; 37}; 38 39/** 40 * Update the shadow element. 41 * @param {Element} element The textarea element. 42 */ 43cvox.EditableTextAreaShadow.prototype.update = function(element) { 44 document.body.appendChild(this.shadowElement_); 45 46 while (this.shadowElement_.childNodes.length) { 47 this.shadowElement_.removeChild(this.shadowElement_.childNodes[0]); 48 } 49 this.shadowElement_.style.cssText = 50 window.getComputedStyle(element, null).cssText; 51 this.shadowElement_.style.position = 'absolute'; 52 this.shadowElement_.style.top = -9999; 53 this.shadowElement_.style.left = -9999; 54 this.shadowElement_.setAttribute('aria-hidden', 'true'); 55 56 // Add the text to the shadow element, but with an extra character to the 57 // end so that we can get the bounding box of the last line - we can't 58 // measure blank lines otherwise. 59 var text = element.value; 60 var textNode = document.createTextNode(text + '.'); 61 this.shadowElement_.appendChild(textNode); 62 63 /** 64 * For extra speed, try to skip this many characters at a time - if 65 * none of the characters are newlines and they're all at the same 66 * vertical position, we don't have to examine each one. If not, 67 * fall back to moving by one character at a time. 68 * @const 69 */ 70 var SKIP = 8; 71 72 /** 73 * Map from line index to a data structure containing the start 74 * and end index within the line. 75 * @type {Object.<number, {startIndex: number, endIndex: number}>} 76 */ 77 var lines = {0: {startIndex: 0, endIndex: 0}}; 78 79 var range = document.createRange(); 80 var offset = 0; 81 var lastGoodOffset = 0; 82 var lineIndex = 0; 83 var lastBottom = null; 84 var nearNewline = false; 85 var rect; 86 while (offset <= text.length) { 87 range.setStart(textNode, offset); 88 89 // If we're near the end or if there's an explicit newline character, 90 // don't even try to skip. 91 if (offset + SKIP > text.length || 92 text.substr(offset, SKIP).indexOf('\n') >= 0) { 93 nearNewline = true; 94 } 95 96 if (nearNewline) { 97 // Move by one character. 98 offset++; 99 range.setEnd(textNode, offset); 100 rect = range.getBoundingClientRect(); 101 } else { 102 // Try to move by |SKIP| characters. 103 range.setEnd(textNode, offset + SKIP); 104 rect = range.getBoundingClientRect(); 105 if (rect.bottom == lastBottom) { 106 // Great, they all seem to be on the same line. 107 offset += SKIP; 108 } else { 109 // Nope, there might be a newline, better go one at a time to be safe. 110 if (rect && lastBottom !== null) { 111 nearNewline = true; 112 } 113 offset++; 114 range.setEnd(textNode, offset); 115 rect = range.getBoundingClientRect(); 116 } 117 } 118 119 if (offset > 0 && text[offset - 1] == '\n') { 120 // Handle an explicit newline character - that always results in 121 // a new line. 122 lines[lineIndex].endIndex = offset - 1; 123 lineIndex++; 124 lines[lineIndex] = {startIndex: offset, endIndex: offset}; 125 lastBottom = null; 126 nearNewline = false; 127 lastGoodOffset = offset; 128 } else if (rect && (lastBottom === null)) { 129 // This is the first character we've successfully measured on this 130 // line. Save the vertical position but don't do anything else. 131 lastBottom = rect.bottom; 132 } else if (rect && rect.bottom != lastBottom) { 133 // This character is at a different vertical position, so place an 134 // implicit newline immediately after the *previous* good character 135 // we found (which we now know was the last character of the previous 136 // line). 137 lines[lineIndex].endIndex = lastGoodOffset; 138 lineIndex++; 139 lines[lineIndex] = {startIndex: lastGoodOffset, endIndex: lastGoodOffset}; 140 lastBottom = rect ? rect.bottom : null; 141 nearNewline = false; 142 } 143 144 if (rect) { 145 lastGoodOffset = offset; 146 } 147 } 148 // Finish up the last line. 149 lines[lineIndex].endIndex = text.length; 150 151 // Create a map from character index to line number. 152 var characterToLineMap = []; 153 for (var i = 0; i <= lineIndex; i++) { 154 for (var j = lines[i].startIndex; j <= lines[i].endIndex; j++) { 155 characterToLineMap[j] = i; 156 } 157 } 158 159 // Finish updating fields and remove the shadow element. 160 this.characterToLineMap_ = characterToLineMap; 161 this.lines_ = lines; 162 document.body.removeChild(this.shadowElement_); 163}; 164 165/** 166 * Get the line number corresponding to a particular index. 167 * @param {number} index The 0-based character index. 168 * @return {number} The 0-based line number corresponding to that character. 169 */ 170cvox.EditableTextAreaShadow.prototype.getLineIndex = function(index) { 171 return this.characterToLineMap_[index]; 172}; 173 174/** 175 * Get the start character index of a line. 176 * @param {number} index The 0-based line index. 177 * @return {number} The 0-based index of the first character in this line. 178 */ 179cvox.EditableTextAreaShadow.prototype.getLineStart = function(index) { 180 return this.lines_[index].startIndex; 181}; 182 183/** 184 * Get the end character index of a line. 185 * @param {number} index The 0-based line index. 186 * @return {number} The 0-based index of the end of this line. 187 */ 188cvox.EditableTextAreaShadow.prototype.getLineEnd = function(index) { 189 return this.lines_[index].endIndex; 190}; 191