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 ContentEditableExtractor class.
      7  */
      8 
      9 goog.provide('cvox.ContentEditableExtractor');
     10 
     11 goog.require('cvox.Cursor');
     12 goog.require('cvox.TraverseUtil');
     13 
     14 /**
     15  * Extracts the text and line break information from a contenteditable region.
     16  * @constructor
     17  */
     18 cvox.ContentEditableExtractor = function() {
     19   /**
     20    * The extracted, flattened, text.
     21    * @type {string}
     22    * @private
     23    */
     24   this.text_ = '';
     25 
     26   /**
     27    * The start cursor/selection index.
     28    * @type {number}
     29    * @private
     30    */
     31   this.start_ = 0;
     32 
     33   /**
     34    * The end cursor/selection index.
     35    * @type {number}
     36    * @private
     37    */
     38   this.end_ = 0;
     39 
     40   /**
     41    * Map from line index to a data structure containing the start
     42    * and end index within the line.
     43    * @type {Object.<number, {startIndex: number, endIndex: number}>}
     44    * @private
     45    */
     46   this.lines_ = {};
     47 
     48   /**
     49    * Map from 0-based character index to 0-based line index.
     50    * @type {Array.<number>}
     51    * @private
     52    */
     53   this.characterToLineMap_ = [];
     54 };
     55 
     56 /**
     57  * Update the metadata.
     58  * @param {Element} element The DOM element that's contentEditable.
     59  */
     60 cvox.ContentEditableExtractor.prototype.update = function(element) {
     61   /**
     62    * Map from line index to a data structure containing the start
     63    * and end index within the line.
     64    * @type {Object.<number, {startIndex: number, endIndex: number}>}
     65    */
     66   var lines = {0: {startIndex: 0, endIndex: 0}};
     67   var startCursor = new cvox.Cursor(element, 0, '');
     68   var endCursor = startCursor.clone();
     69   var range = document.createRange();
     70   var rect;
     71   var lineIndex = 0;
     72   var lastBottom = null;
     73   var text = '';
     74   var textSize = 0;
     75   var selectionStartIndex = -1;
     76   var selectionEndIndex = -1;
     77   var sel = window.getSelection();
     78   var selectionStart = new cvox.Cursor(sel.baseNode, sel.baseOffset, '');
     79   var selectionEnd = new cvox.Cursor(sel.extentNode, sel.extentOffset, '');
     80   var setStart = false;
     81   var setEnd = false;
     82   while (true) {
     83     var entered = [];
     84     var left = [];
     85     var c = cvox.TraverseUtil.forwardsChar(endCursor, entered, left);
     86     var done = false;
     87     if (!c) {
     88       done = true;
     89     }
     90     for (var i = 0; i < left.length && !done; i++) {
     91       if (left[i] == element) {
     92         done = true;
     93       }
     94     }
     95     if (done) {
     96       break;
     97     }
     98 
     99     range.setStart(startCursor.node, startCursor.index);
    100     range.setEnd(endCursor.node, endCursor.index);
    101     rect = range.getBoundingClientRect();
    102     if (!rect || rect.width == 0 || rect.height == 0) {
    103       continue;
    104     }
    105 
    106     if (lastBottom !== null &&
    107         rect.bottom != lastBottom &&
    108         textSize > 0 &&
    109         text.substr(-1).match(/\S/) &&
    110         c.match(/\S/)) {
    111       text += '\n';
    112       textSize++;
    113     }
    114 
    115     if (startCursor.node != endCursor.node && endCursor.index > 0) {
    116       range.setStart(endCursor.node, endCursor.index - 1);
    117       rect = range.getBoundingClientRect();
    118       if (!rect || rect.width == 0 || rect.height == 0) {
    119         continue;
    120       }
    121     }
    122 
    123     if (!setStart &&
    124         selectionStartIndex == -1 &&
    125         endCursor.node == selectionStart.node &&
    126         endCursor.index >= selectionStart.index) {
    127       if (endCursor.index > selectionStart.index) {
    128         selectionStartIndex = textSize;
    129       } else {
    130         setStart = true;
    131       }
    132     }
    133     if (!setEnd &&
    134         selectionEndIndex == -1 &&
    135         endCursor.node == selectionEnd.node &&
    136         endCursor.index >= selectionEnd.index) {
    137       if (endCursor.index > selectionEnd.index) {
    138         selectionEndIndex = textSize;
    139       } else {
    140         setEnd = true;
    141       }
    142     }
    143 
    144     if (lastBottom === null) {
    145       // This is the first character we've successfully measured on this
    146       // line. Save the vertical position but don't do anything else.
    147       lastBottom = rect.bottom;
    148     } else if (rect.bottom != lastBottom) {
    149       lines[lineIndex].endIndex = textSize;
    150       lineIndex++;
    151       lines[lineIndex] = {startIndex: textSize, endIndex: textSize};
    152       lastBottom = rect.bottom;
    153     }
    154     text += c;
    155     textSize++;
    156     startCursor = endCursor.clone();
    157 
    158     if (setStart) {
    159       selectionStartIndex = textSize;
    160       setStart = false;
    161     }
    162     if (setEnd) {
    163       selectionEndIndex = textSize;
    164       setEnd = false;
    165     }
    166   }
    167 
    168   // Finish up the last line.
    169   lines[lineIndex].endIndex = textSize;
    170 
    171   // Create a map from character index to line number.
    172   var characterToLineMap = [];
    173   for (var i = 0; i <= lineIndex; i++) {
    174     for (var j = lines[i].startIndex; j <= lines[i].endIndex; j++) {
    175       characterToLineMap[j] = i;
    176     }
    177   }
    178 
    179   // Finish updating fields.
    180   this.text_ = text;
    181   this.characterToLineMap_ = characterToLineMap;
    182   this.lines_ = lines;
    183 
    184   this.start_ = selectionStartIndex >= 0 ? selectionStartIndex : text.length;
    185   this.end_ = selectionEndIndex >= 0 ? selectionEndIndex : text.length;
    186 };
    187 
    188 /**
    189  * Get the text.
    190  * @return {string} The extracted, flattened, text.
    191  */
    192 cvox.ContentEditableExtractor.prototype.getText = function() {
    193   return this.text_;
    194 };
    195 
    196 /**
    197  * @return {number} The start cursor/selection index.
    198  */
    199 cvox.ContentEditableExtractor.prototype.getStartIndex = function() {
    200   return this.start_;
    201 };
    202 
    203 /**
    204  * @return {number} The end cursor/selection index.
    205  */
    206 cvox.ContentEditableExtractor.prototype.getEndIndex = function() {
    207   return this.end_;
    208 };
    209 
    210 /**
    211  * Get the line number corresponding to a particular index.
    212  * @param {number} index The 0-based character index.
    213  * @return {number} The 0-based line number corresponding to that character.
    214  */
    215 cvox.ContentEditableExtractor.prototype.getLineIndex = function(index) {
    216   return this.characterToLineMap_[index];
    217 };
    218 
    219 /**
    220  * Get the start character index of a line.
    221  * @param {number} index The 0-based line index.
    222  * @return {number} The 0-based index of the first character in this line.
    223  */
    224 cvox.ContentEditableExtractor.prototype.getLineStart = function(index) {
    225   return this.lines_[index].startIndex;
    226 };
    227 
    228 /**
    229  * Get the end character index of a line.
    230  * @param {number} index The 0-based line index.
    231  * @return {number} The 0-based index of the end of this line.
    232  */
    233 cvox.ContentEditableExtractor.prototype.getLineEnd = function(index) {
    234   return this.lines_[index].endIndex;
    235 };
    236