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.
6 * @fileoverview Defines the EditableTextAreaShadow class.
9 goog.provide('cvox.EditableTextAreaShadow');
12 * Creates a shadow element for an editable text area used to compute line
16 cvox.EditableTextAreaShadow = function() {
21 this.shadowElement_ = document.createElement('div');
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}>}
32 * Map from 0-based character index to 0-based line index.
33 * @type {Array<number>}
36 this.characterToLineMap_ = [];
40 * Update the shadow element.
41 * @param {Element} element The textarea element.
43 cvox.EditableTextAreaShadow.prototype.update = function(element) {
44 document.body.appendChild(this.shadowElement_);
46 while (this.shadowElement_.childNodes.length) {
47 this.shadowElement_.removeChild(this.shadowElement_.childNodes[0]);
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');
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);
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.
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}>}
77 var lines = {0: {startIndex: 0, endIndex: 0}};
79 var range = document.createRange();
81 var lastGoodOffset = 0;
83 var lastBottom = null;
84 var nearNewline = false;
86 while (offset <= text.length) {
87 range.setStart(textNode, offset);
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) {
97 // Move by one character.
99 range.setEnd(textNode, offset);
100 rect = range.getBoundingClientRect();
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.
109 // Nope, there might be a newline, better go one at a time to be safe.
110 if (rect && lastBottom !== null) {
114 range.setEnd(textNode, offset);
115 rect = range.getBoundingClientRect();
119 if (offset > 0 && text[offset - 1] == '\n') {
120 // Handle an explicit newline character - that always results in
122 lines[lineIndex].endIndex = offset - 1;
124 lines[lineIndex] = {startIndex: offset, endIndex: offset};
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
137 lines[lineIndex].endIndex = lastGoodOffset;
139 lines[lineIndex] = {startIndex: lastGoodOffset, endIndex: lastGoodOffset};
140 lastBottom = rect ? rect.bottom : null;
145 lastGoodOffset = offset;
148 // Finish up the last line.
149 lines[lineIndex].endIndex = text.length;
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;
159 // Finish updating fields and remove the shadow element.
160 this.characterToLineMap_ = characterToLineMap;
162 document.body.removeChild(this.shadowElement_);
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.
170 cvox.EditableTextAreaShadow.prototype.getLineIndex = function(index) {
171 return this.characterToLineMap_[index];
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.
179 cvox.EditableTextAreaShadow.prototype.getLineStart = function(index) {
180 return this.lines_[index].startIndex;
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.
188 cvox.EditableTextAreaShadow.prototype.getLineEnd = function(index) {
189 return this.lines_[index].endIndex;