blob: 8e2e0d81d3b320aff9160b9448b3f72a14a6aab4 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Defines the EditableTextAreaShadow class.
*/
goog.provide('cvox.EditableTextAreaShadow');
/**
* Creates a shadow element for an editable text area used to compute line
* numbers.
* @constructor
*/
cvox.EditableTextAreaShadow = function() {
/**
* @type {Element}
* @private
*/
this.shadowElement_ = document.createElement('div');
/**
* Map from line index to a data structure containing the start
* and end index within the line.
* @type {Object<number, {startIndex: number, endIndex: number}>}
* @private
*/
this.lines_ = {};
/**
* Map from 0-based character index to 0-based line index.
* @type {Array<number>}
* @private
*/
this.characterToLineMap_ = [];
};
/**
* Update the shadow element.
* @param {Element} element The textarea element.
*/
cvox.EditableTextAreaShadow.prototype.update = function(element) {
document.body.appendChild(this.shadowElement_);
while (this.shadowElement_.childNodes.length) {
this.shadowElement_.removeChild(this.shadowElement_.childNodes[0]);
}
this.shadowElement_.style.cssText =
window.getComputedStyle(element, null).cssText;
this.shadowElement_.style.position = 'absolute';
this.shadowElement_.style.top = '-9999';
this.shadowElement_.style.left = '-9999';
this.shadowElement_.setAttribute('aria-hidden', 'true');
// Add the text to the shadow element, but with an extra character to the
// end so that we can get the bounding box of the last line - we can't
// measure blank lines otherwise.
var text = element.value;
var textNode = document.createTextNode(text + '.');
this.shadowElement_.appendChild(textNode);
/**
* For extra speed, try to skip this many characters at a time - if
* none of the characters are newlines and they're all at the same
* vertical position, we don't have to examine each one. If not,
* fall back to moving by one character at a time.
* @const
*/
var SKIP = 8;
/**
* Map from line index to a data structure containing the start
* and end index within the line.
* @type {Object<number, {startIndex: number, endIndex: number}>}
*/
var lines = {0: {startIndex: 0, endIndex: 0}};
var range = document.createRange();
var offset = 0;
var lastGoodOffset = 0;
var lineIndex = 0;
var lastBottom = null;
var nearNewline = false;
var rect;
while (offset <= text.length) {
range.setStart(textNode, offset);
// If we're near the end or if there's an explicit newline character,
// don't even try to skip.
if (offset + SKIP > text.length ||
text.substr(offset, SKIP).indexOf('\n') >= 0) {
nearNewline = true;
}
if (nearNewline) {
// Move by one character.
offset++;
range.setEnd(textNode, offset);
rect = range.getBoundingClientRect();
} else {
// Try to move by |SKIP| characters.
range.setEnd(textNode, offset + SKIP);
rect = range.getBoundingClientRect();
if (rect.bottom == lastBottom) {
// Great, they all seem to be on the same line.
offset += SKIP;
} else {
// Nope, there might be a newline, better go one at a time to be safe.
if (rect && lastBottom !== null) {
nearNewline = true;
}
offset++;
range.setEnd(textNode, offset);
rect = range.getBoundingClientRect();
}
}
if (offset > 0 && text[offset - 1] == '\n') {
// Handle an explicit newline character - that always results in
// a new line.
lines[lineIndex].endIndex = offset - 1;
lineIndex++;
lines[lineIndex] = {startIndex: offset, endIndex: offset};
lastBottom = null;
nearNewline = false;
lastGoodOffset = offset;
} else if (rect && (lastBottom === null)) {
// This is the first character we've successfully measured on this
// line. Save the vertical position but don't do anything else.
lastBottom = rect.bottom;
} else if (rect && rect.bottom != lastBottom) {
// This character is at a different vertical position, so place an
// implicit newline immediately after the *previous* good character
// we found (which we now know was the last character of the previous
// line).
lines[lineIndex].endIndex = lastGoodOffset;
lineIndex++;
lines[lineIndex] = {startIndex: lastGoodOffset, endIndex: lastGoodOffset};
lastBottom = rect ? rect.bottom : null;
nearNewline = false;
}
if (rect) {
lastGoodOffset = offset;
}
}
// Finish up the last line.
lines[lineIndex].endIndex = text.length;
// Create a map from character index to line number.
var characterToLineMap = [];
for (var i = 0; i <= lineIndex; i++) {
for (var j = lines[i].startIndex; j <= lines[i].endIndex; j++) {
characterToLineMap[j] = i;
}
}
// Finish updating fields and remove the shadow element.
this.characterToLineMap_ = characterToLineMap;
this.lines_ = lines;
document.body.removeChild(this.shadowElement_);
};
/**
* Get the line number corresponding to a particular index.
* @param {number} index The 0-based character index.
* @return {number} The 0-based line number corresponding to that character.
*/
cvox.EditableTextAreaShadow.prototype.getLineIndex = function(index) {
return this.characterToLineMap_[index];
};
/**
* Get the start character index of a line.
* @param {number} index The 0-based line index.
* @return {number} The 0-based index of the first character in this line.
*/
cvox.EditableTextAreaShadow.prototype.getLineStart = function(index) {
return this.lines_[index].startIndex;
};
/**
* Get the end character index of a line.
* @param {number} index The 0-based line index.
* @return {number} The 0-based index of the end of this line.
*/
cvox.EditableTextAreaShadow.prototype.getLineEnd = function(index) {
return this.lines_[index].endIndex;
};