Home | History | Annotate | Download | only in common
      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.
      4 
      5 /**
      6  * @fileoverview Defines the EditableTextAreaShadow class.
      7  */
      8 
      9 goog.provide('cvox.EditableTextAreaShadow');
     10 
     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');
     22 
     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_ = {};
     30 
     31   /**
     32    * Map from 0-based character index to 0-based line index.
     33    * @type {Array.<number>}
     34    * @private
     35    */
     36   this.characterToLineMap_ = [];
     37 };
     38 
     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_);
     45 
     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');
     55 
     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);
     62 
     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;
     71 
     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}};
     78 
     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);
     88 
     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     }
     95 
     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     }
    118 
    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     }
    143 
    144     if (rect) {
    145       lastGoodOffset = offset;
    146     }
    147   }
    148   // Finish up the last line.
    149   lines[lineIndex].endIndex = text.length;
    150 
    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   }
    158 
    159   // Finish updating fields and remove the shadow element.
    160   this.characterToLineMap_ = characterToLineMap;
    161   this.lines_ = lines;
    162   document.body.removeChild(this.shadowElement_);
    163 };
    164 
    165 /**
    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];
    172 };
    173 
    174 /**
    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;
    181 };
    182 
    183 /**
    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;
    190 };
    191