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
;