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