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