Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / editable_text_area_shadow.js
blobbd381156c698bd8a5470c3aa09690bfe44265b72
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.
5 /**
6  * @fileoverview Defines the EditableTextAreaShadow class.
7  */
9 goog.provide('cvox.EditableTextAreaShadow');
11 /**
12  * Creates a shadow element for an editable text area used to compute line
13  * numbers.
14  * @constructor
15  */
16 cvox.EditableTextAreaShadow = function() {
17   /**
18    * @type {Element}
19    * @private
20    */
21   this.shadowElement_ = document.createElement('div');
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_ = {};
31   /**
32    * Map from 0-based character index to 0-based line index.
33    * @type {Array<number>}
34    * @private
35    */
36   this.characterToLineMap_ = [];
39 /**
40  * Update the shadow element.
41  * @param {Element} element The textarea element.
42  */
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]);
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');
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);
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;
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}};
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);
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     }
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     }
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     }
144     if (rect) {
145       lastGoodOffset = offset;
146     }
147   }
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;
156     }
157   }
159   // Finish updating fields and remove the shadow element.
160   this.characterToLineMap_ = characterToLineMap;
161   this.lines_ = lines;
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.
169  */
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.
178  */
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.
187  */
188 cvox.EditableTextAreaShadow.prototype.getLineEnd = function(index) {
189   return this.lines_[index].endIndex;