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
5goog.provide('cvox.ChromeVoxEditableContentEditable');
6goog.provide('cvox.ChromeVoxEditableHTMLInput');
7goog.provide('cvox.ChromeVoxEditableTextArea');
8goog.provide('cvox.ChromeVoxEditableTextBase');
9goog.provide('cvox.TextChangeEvent');
10goog.provide('cvox.TextHandlerInterface');
11goog.provide('cvox.TypingEcho');
12
13
14goog.require('cvox.BrailleTextHandler');
15goog.require('cvox.ContentEditableExtractor');
16goog.require('cvox.DomUtil');
17goog.require('cvox.EditableTextAreaShadow');
18goog.require('cvox.TtsInterface');
19goog.require('goog.i18n.MessageFormat');
20
21/**
22 * @fileoverview Gives the user spoken feedback as they type, select text,
23 * and move the cursor in editable text controls, including multiline
24 * controls.
25 *
26 * The majority of the code is in ChromeVoxEditableTextBase, a generalized
27 * class that takes the current state in the form of a text string, a
28 * cursor start location and a cursor end location, and calls a speak
29 * method with the resulting text to be spoken. If the control is multiline,
30 * information about line breaks (including automatic ones) is also needed.
31 *
32 * Two subclasses, ChromeVoxEditableHTMLInput and
33 * ChromeVoxEditableTextArea, take a HTML input (type=text) or HTML
34 * textarea node (respectively) in the constructor, and automatically
35 * handle retrieving the current state of the control, including
36 * computing line break information for a textarea using an offscreen
37 * shadow object. It is still the responsibility of the user of this
38 * class to trap key and focus events and call this class's update
39 * method.
40 *
41 */
42
43
44/**
45 * A class containing the information needed to speak
46 * a text change event to the user.
47 *
48 * @constructor
49 * @param {string} newValue The new string value of the editable text control.
50 * @param {number} newStart The new 0-based start cursor/selection index.
51 * @param {number} newEnd The new 0-based end cursor/selection index.
52 * @param {boolean} triggeredByUser .
53 */
54cvox.TextChangeEvent = function(newValue, newStart, newEnd, triggeredByUser) {
55  this.value = newValue;
56  this.start = newStart;
57  this.end = newEnd;
58  this.triggeredByUser = triggeredByUser;
59
60  // Adjust offsets to be in left to right order.
61  if (this.start > this.end) {
62    var tempOffset = this.end;
63    this.end = this.start;
64    this.start = tempOffset;
65  }
66};
67
68
69/**
70 * A list of typing echo options.
71 * This defines the way typed characters get spoken.
72 * CHARACTER: echoes typed characters.
73 * WORD: echoes a word once a breaking character is typed (i.e. spacebar).
74 * CHARACTER_AND_WORD: combines CHARACTER and WORD behavior.
75 * NONE: speaks nothing when typing.
76 * COUNT: The number of possible echo levels.
77 * @enum
78 */
79cvox.TypingEcho = {
80  CHARACTER: 0,
81  WORD: 1,
82  CHARACTER_AND_WORD: 2,
83  NONE: 3,
84  COUNT: 4
85};
86
87
88/**
89 * @param {number} cur Current typing echo.
90 * @return {number} Next typing echo.
91 */
92cvox.TypingEcho.cycle = function(cur) {
93  return (cur + 1) % cvox.TypingEcho.COUNT;
94};
95
96
97/**
98 * Return if characters should be spoken given the typing echo option.
99 * @param {number} typingEcho Typing echo option.
100 * @return {boolean} Whether the character should be spoken.
101 */
102cvox.TypingEcho.shouldSpeakChar = function(typingEcho) {
103  return typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD ||
104      typingEcho == cvox.TypingEcho.CHARACTER;
105};
106
107
108/**
109 * An interface for being notified when the text changes.
110 * @interface
111 */
112cvox.TextHandlerInterface = function() {};
113
114
115/**
116 * Called when text changes.
117 * @param {cvox.TextChangeEvent} evt The text change event.
118 */
119cvox.TextHandlerInterface.prototype.changed = function(evt) {};
120
121
122/**
123 * A class representing an abstracted editable text control.
124 * @param {string} value The string value of the editable text control.
125 * @param {number} start The 0-based start cursor/selection index.
126 * @param {number} end The 0-based end cursor/selection index.
127 * @param {boolean} isPassword Whether the text control if a password field.
128 * @param {cvox.TtsInterface} tts A TTS object.
129 * @constructor
130 */
131cvox.ChromeVoxEditableTextBase = function(value, start, end, isPassword, tts) {
132  /**
133   * Current value of the text field.
134   * @type {string}
135   * @protected
136   */
137  this.value = value;
138
139  /**
140   * 0-based selection start index.
141   * @type {number}
142   * @protected
143   */
144  this.start = start;
145
146  /**
147   * 0-based selection end index.
148   * @type {number}
149   * @protected
150   */
151  this.end = end;
152
153  /**
154   * True if this is a password field.
155   * @type {boolean}
156   * @protected
157   */
158  this.isPassword = isPassword;
159
160  /**
161   * Text-to-speech object implementing speak() and stop() methods.
162   * @type {cvox.TtsInterface}
163   * @protected
164   */
165  this.tts = tts;
166
167  /**
168   * Whether or not the text field is multiline.
169   * @type {boolean}
170   * @protected
171   */
172  this.multiline = false;
173
174  /**
175   * An optional handler for braille output.
176   * @type {cvox.BrailleTextHandler|undefined}
177   * @private
178   */
179  this.brailleHandler_ = cvox.ChromeVox.braille ?
180      new cvox.BrailleTextHandler(cvox.ChromeVox.braille) : undefined;
181};
182
183
184/**
185 * Performs setup for this element.
186 */
187cvox.ChromeVoxEditableTextBase.prototype.setup = function() {};
188
189
190/**
191 * Performs teardown for this element.
192 */
193cvox.ChromeVoxEditableTextBase.prototype.teardown = function() {};
194
195
196/**
197 * Whether or not moving the cursor from one character to another considers
198 * the cursor to be a block (false) or an i-beam (true).
199 *
200 * If the cursor is a block, then the value of the character to the right
201 * of the cursor index is always read when the cursor moves, no matter what
202 * the previous cursor location was - this is how PC screenreaders work.
203 *
204 * If the cursor is an i-beam, moving the cursor by one character reads the
205 * character that was crossed over, which may be the character to the left or
206 * right of the new cursor index depending on the direction.
207 *
208 * If the current platform is a Mac, we will use an i-beam cursor. If not,
209 * then we will use the block cursor.
210 *
211 * @type {boolean}
212 */
213cvox.ChromeVoxEditableTextBase.useIBeamCursor = cvox.ChromeVox.isMac;
214
215
216/**
217 * Switches on or off typing echo based on events. When set, editable text
218 * updates for single-character insertions are handled in event watcher's key
219 * press handler.
220 * @type {boolean}
221 */
222cvox.ChromeVoxEditableTextBase.eventTypingEcho = false;
223
224
225/**
226 * The maximum number of characters that are short enough to speak in response
227 * to an event. For example, if the user selects "Hello", we will speak
228 * "Hello, selected", but if the user selects 1000 characters, we will speak
229 * "text selected" instead.
230 *
231 * @type {number}
232 */
233cvox.ChromeVoxEditableTextBase.prototype.maxShortPhraseLen = 60;
234
235
236/**
237 * Whether or not the text control is a password.
238 *
239 * @type {boolean}
240 */
241cvox.ChromeVoxEditableTextBase.prototype.isPassword = false;
242
243
244/**
245 * Whether or not the last update to the text and selection was described.
246 *
247 * Some consumers of this flag like |ChromeVoxEventWatcher| depend on and
248 * react to when this flag is false by generating alternative feedback.
249 * @type {boolean}
250 */
251cvox.ChromeVoxEditableTextBase.prototype.lastChangeDescribed = false;
252
253
254/**
255 * Get the line number corresponding to a particular index.
256 * Default implementation that can be overridden by subclasses.
257 * @param {number} index The 0-based character index.
258 * @return {number} The 0-based line number corresponding to that character.
259 */
260cvox.ChromeVoxEditableTextBase.prototype.getLineIndex = function(index) {
261  return 0;
262};
263
264
265/**
266 * Get the start character index of a line.
267 * Default implementation that can be overridden by subclasses.
268 * @param {number} index The 0-based line index.
269 * @return {number} The 0-based index of the first character in this line.
270 */
271cvox.ChromeVoxEditableTextBase.prototype.getLineStart = function(index) {
272  return 0;
273};
274
275
276/**
277 * Get the end character index of a line.
278 * Default implementation that can be overridden by subclasses.
279 * @param {number} index The 0-based line index.
280 * @return {number} The 0-based index of the end of this line.
281 */
282cvox.ChromeVoxEditableTextBase.prototype.getLineEnd = function(index) {
283  return this.value.length;
284};
285
286
287/**
288 * Get the full text of the current line.
289 * @param {number} index The 0-based line index.
290 * @return {string} The text of the line.
291 */
292cvox.ChromeVoxEditableTextBase.prototype.getLine = function(index) {
293  var lineStart = this.getLineStart(index);
294  var lineEnd = this.getLineEnd(index);
295  return this.value.substr(lineStart, lineEnd - lineStart);
296};
297
298
299/**
300 * @param {string} ch The character to test.
301 * @return {boolean} True if a character is whitespace.
302 */
303cvox.ChromeVoxEditableTextBase.prototype.isWhitespaceChar = function(ch) {
304  return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
305};
306
307
308/**
309 * @param {string} ch The character to test.
310 * @return {boolean} True if a character breaks a word, used to determine
311 *     if the previous word should be spoken.
312 */
313cvox.ChromeVoxEditableTextBase.prototype.isWordBreakChar = function(ch) {
314  return !!ch.match(/^\W$/);
315};
316
317
318/**
319 * @param {cvox.TextChangeEvent} evt The new text changed event to test.
320 * @return {boolean} True if the event, when compared to the previous text,
321 * should trigger description.
322 */
323cvox.ChromeVoxEditableTextBase.prototype.shouldDescribeChange = function(evt) {
324  if (evt.value == this.value &&
325      evt.start == this.start &&
326      evt.end == this.end) {
327    return false;
328  }
329  return true;
330};
331
332
333/**
334 * Speak text, but if it's a single character, describe the character.
335 * @param {string} str The string to speak.
336 * @param {boolean=} opt_triggeredByUser True if the speech was triggered by a
337 * user action.
338 * @param {Object=} opt_personality Personality used to speak text.
339 */
340cvox.ChromeVoxEditableTextBase.prototype.speak =
341    function(str, opt_triggeredByUser, opt_personality) {
342  // If there is a node associated with the editable text object,
343  // make sure that node has focus before speaking it.
344  if (this.node && (document.activeElement != this.node)) {
345    return;
346  }
347  var queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
348  if (opt_triggeredByUser === true) {
349    queueMode = cvox.AbstractTts.QUEUE_MODE_FLUSH;
350  }
351  this.tts.speak(str, queueMode, opt_personality || {});
352};
353
354
355/**
356 * Update the state of the text and selection and describe any changes as
357 * appropriate.
358 *
359 * @param {cvox.TextChangeEvent} evt The text change event.
360 */
361cvox.ChromeVoxEditableTextBase.prototype.changed = function(evt) {
362  if (!this.shouldDescribeChange(evt)) {
363    this.lastChangeDescribed = false;
364    return;
365  }
366
367  if (evt.value == this.value) {
368    this.describeSelectionChanged(evt);
369  } else {
370    this.describeTextChanged(evt);
371  }
372  this.lastChangeDescribed = true;
373
374  this.value = evt.value;
375  this.start = evt.start;
376  this.end = evt.end;
377
378  if (this.brailleHandler_) {
379    var line = this.getLine(this.getLineIndex(evt.start));
380    var lineStart = this.getLineStart(this.getLineIndex(evt.start));
381    var start = evt.start - lineStart;
382    var end = Math.min(evt.end - lineStart, line.length);
383    this.brailleHandler_.changed(line, start, end, this.multiline, this.node,
384                                lineStart);
385  }
386};
387
388
389/**
390 * Describe a change in the selection or cursor position when the text
391 * stays the same.
392 * @param {cvox.TextChangeEvent} evt The text change event.
393 */
394cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged =
395    function(evt) {
396  // TODO(deboer): Factor this into two function:
397  //   - one to determine the selection event
398  //   - one to speak
399
400  if (this.isPassword) {
401    this.speak((new goog.i18n.MessageFormat(cvox.ChromeVox.msgs.getMsg('dot'))
402        .format({'COUNT': 1})), evt.triggeredByUser);
403    return;
404  }
405  if (evt.start == evt.end) {
406    // It's currently a cursor.
407    if (this.start != this.end) {
408      // It was previously a selection, so just announce 'unselected'.
409      this.speak(cvox.ChromeVox.msgs.getMsg('Unselected'), evt.triggeredByUser);
410    } else if (this.getLineIndex(this.start) !=
411        this.getLineIndex(evt.start)) {
412      // Moved to a different line; read it.
413      var lineValue = this.getLine(this.getLineIndex(evt.start));
414      if (lineValue == '') {
415        lineValue = cvox.ChromeVox.msgs.getMsg('text_box_blank');
416      } else if (/^\s+$/.test(lineValue)) {
417        lineValue = cvox.ChromeVox.msgs.getMsg('text_box_whitespace');
418      }
419      this.speak(lineValue, evt.triggeredByUser);
420    } else if (this.start == evt.start + 1 ||
421        this.start == evt.start - 1) {
422      // Moved by one character; read it.
423      if (!cvox.ChromeVoxEditableTextBase.useIBeamCursor) {
424        if (evt.start == this.value.length) {
425          if (cvox.ChromeVox.verbosity == cvox.VERBOSITY_VERBOSE) {
426            this.speak(cvox.ChromeVox.msgs.getMsg('end_of_text_verbose'),
427                       evt.triggeredByUser);
428          } else {
429            this.speak(cvox.ChromeVox.msgs.getMsg('end_of_text_brief'),
430                       evt.triggeredByUser);
431          }
432        } else {
433          this.speak(this.value.substr(evt.start, 1),
434                     evt.triggeredByUser,
435                     {'phoneticCharacters': evt.triggeredByUser});
436        }
437      } else {
438        this.speak(this.value.substr(Math.min(this.start, evt.start), 1),
439            evt.triggeredByUser,
440            {'phoneticCharacters': evt.triggeredByUser});
441      }
442    } else {
443      // Moved by more than one character. Read all characters crossed.
444      this.speak(this.value.substr(Math.min(this.start, evt.start),
445          Math.abs(this.start - evt.start)), evt.triggeredByUser);
446    }
447  } else {
448    // It's currently a selection.
449    if (this.start + 1 == evt.start &&
450        this.end == this.value.length &&
451        evt.end == this.value.length) {
452      // Autocomplete: the user typed one character of autocompleted text.
453      this.speak(this.value.substr(this.start, 1), evt.triggeredByUser);
454      this.speak(this.value.substr(evt.start));
455    } else if (this.start == this.end) {
456      // It was previously a cursor.
457      this.speak(this.value.substr(evt.start, evt.end - evt.start),
458                 evt.triggeredByUser);
459      this.speak(cvox.ChromeVox.msgs.getMsg('selected'));
460    } else if (this.start == evt.start && this.end < evt.end) {
461      this.speak(this.value.substr(this.end, evt.end - this.end),
462                 evt.triggeredByUser);
463      this.speak(cvox.ChromeVox.msgs.getMsg('added_to_selection'));
464    } else if (this.start == evt.start && this.end > evt.end) {
465      this.speak(this.value.substr(evt.end, this.end - evt.end),
466                 evt.triggeredByUser);
467      this.speak(cvox.ChromeVox.msgs.getMsg('removed_from_selection'));
468    } else if (this.end == evt.end && this.start > evt.start) {
469      this.speak(this.value.substr(evt.start, this.start - evt.start),
470                 evt.triggeredByUser);
471      this.speak(cvox.ChromeVox.msgs.getMsg('added_to_selection'));
472    } else if (this.end == evt.end && this.start < evt.start) {
473      this.speak(this.value.substr(this.start, evt.start - this.start),
474                 evt.triggeredByUser);
475      this.speak(cvox.ChromeVox.msgs.getMsg('removed_from_selection'));
476    } else {
477      // The selection changed but it wasn't an obvious extension of
478      // a previous selection. Just read the new selection.
479      this.speak(this.value.substr(evt.start, evt.end - evt.start),
480                 evt.triggeredByUser);
481      this.speak(cvox.ChromeVox.msgs.getMsg('selected'));
482    }
483  }
484};
485
486
487/**
488 * Describe a change where the text changes.
489 * @param {cvox.TextChangeEvent} evt The text change event.
490 */
491cvox.ChromeVoxEditableTextBase.prototype.describeTextChanged = function(evt) {
492  var personality = {};
493  if (evt.value.length < this.value.length) {
494    personality = cvox.AbstractTts.PERSONALITY_DELETED;
495  }
496  if (this.isPassword) {
497    this.speak((new goog.i18n.MessageFormat(cvox.ChromeVox.msgs.getMsg('dot'))
498        .format({'COUNT': 1})), evt.triggeredByUser, personality);
499    return;
500  }
501
502  var value = this.value;
503  var len = value.length;
504  var newLen = evt.value.length;
505  var autocompleteSuffix = '';
506  // Make a copy of evtValue and evtEnd to avoid changing anything in
507  // the event itself.
508  var evtValue = evt.value;
509  var evtEnd = evt.end;
510
511  // First, see if there's a selection at the end that might have been
512  // added by autocomplete. If so, strip it off into a separate variable.
513  if (evt.start < evtEnd && evtEnd == newLen) {
514    autocompleteSuffix = evtValue.substr(evt.start);
515    evtValue = evtValue.substr(0, evt.start);
516    evtEnd = evt.start;
517  }
518
519  // Now see if the previous selection (if any) was deleted
520  // and any new text was inserted at that character position.
521  // This would handle pasting and entering text by typing, both from
522  // a cursor and from a selection.
523  var prefixLen = this.start;
524  var suffixLen = len - this.end;
525  if (newLen >= prefixLen + suffixLen + (evtEnd - evt.start) &&
526      evtValue.substr(0, prefixLen) == value.substr(0, prefixLen) &&
527      evtValue.substr(newLen - suffixLen) == value.substr(this.end)) {
528    // However, in a dynamic content editable, defer to authoritative events
529    // (clipboard, key press) to reduce guess work when observing insertions.
530    // Only use this logic when observing deletions (and insertion of word
531    // breakers).
532    // TODO(dtseng): Think about a more reliable way to do this.
533    if (!(this instanceof cvox.ChromeVoxEditableContentEditable) ||
534        newLen < len ||
535        this.isWordBreakChar(evt.value[newLen - 1] || '')) {
536      this.describeTextChangedHelper(
537          evt, prefixLen, suffixLen, autocompleteSuffix, personality);
538    }
539    return;
540  }
541
542  // Next, see if one or more characters were deleted from the previous
543  // cursor position and the new cursor is in the expected place. This
544  // handles backspace, forward-delete, and similar shortcuts that delete
545  // a word or line.
546  prefixLen = evt.start;
547  suffixLen = newLen - evtEnd;
548  if (this.start == this.end &&
549      evt.start == evtEnd &&
550      evtValue.substr(0, prefixLen) == value.substr(0, prefixLen) &&
551      evtValue.substr(newLen - suffixLen) ==
552      value.substr(len - suffixLen)) {
553    this.describeTextChangedHelper(
554        evt, prefixLen, suffixLen, autocompleteSuffix, personality);
555    return;
556  }
557
558  // If all else fails, we assume the change was not the result of a normal
559  // user editing operation, so we'll have to speak feedback based only
560  // on the changes to the text, not the cursor position / selection.
561  // First, restore the autocomplete text if any.
562  evtValue += autocompleteSuffix;
563
564  // Try to do a diff between the new and the old text. If it is a one character
565  // insertion/deletion at the start or at the end, just speak that character.
566  if ((evtValue.length == (value.length + 1)) ||
567      ((evtValue.length + 1) == value.length)) {
568    // The user added text either to the beginning or the end.
569    if (evtValue.length > value.length) {
570      if (evtValue.indexOf(value) == 0) {
571        this.speak(evtValue[evtValue.length - 1], evt.triggeredByUser,
572                   personality);
573        return;
574      } else if (evtValue.indexOf(value) == 1) {
575        this.speak(evtValue[0], evt.triggeredByUser, personality);
576        return;
577      }
578    }
579    // The user deleted text either from the beginning or the end.
580    if (evtValue.length < value.length) {
581      if (value.indexOf(evtValue) == 0) {
582        this.speak(value[value.length - 1], evt.triggeredByUser, personality);
583        return;
584      } else if (value.indexOf(evtValue) == 1) {
585        this.speak(value[0], evt.triggeredByUser, personality);
586        return;
587      }
588    }
589  }
590
591  if (this.multiline) {
592    // Fall back to announce deleted but omit the text that was deleted.
593    if (evt.value.length < this.value.length) {
594      this.speak(cvox.ChromeVox.msgs.getMsg('text_deleted'),
595                 evt.triggeredByUser, personality);
596    }
597    // The below is a somewhat loose way to deal with non-standard
598    // insertions/deletions. Intentionally skip for multiline since deletion
599    // announcements are covered above and insertions are non-standard (possibly
600    // due to auto complete). Since content editable's often refresh content by
601    // removing and inserting entire chunks of text, this type of logic often
602    // results in unintended consequences such as reading all text when only one
603    // character has been entered.
604    return;
605  }
606
607  // If the text is short, just speak the whole thing.
608  if (newLen <= this.maxShortPhraseLen) {
609    this.describeTextChangedHelper(evt, 0, 0, '', personality);
610    return;
611  }
612
613  // Otherwise, look for the common prefix and suffix, but back up so
614  // that we can speak complete words, to be minimally confusing.
615  prefixLen = 0;
616  while (prefixLen < len &&
617         prefixLen < newLen &&
618         value[prefixLen] == evtValue[prefixLen]) {
619    prefixLen++;
620  }
621  while (prefixLen > 0 && !this.isWordBreakChar(value[prefixLen - 1])) {
622    prefixLen--;
623  }
624
625  suffixLen = 0;
626  while (suffixLen < (len - prefixLen) &&
627         suffixLen < (newLen - prefixLen) &&
628         value[len - suffixLen - 1] == evtValue[newLen - suffixLen - 1]) {
629    suffixLen++;
630  }
631  while (suffixLen > 0 && !this.isWordBreakChar(value[len - suffixLen])) {
632    suffixLen--;
633  }
634
635  this.describeTextChangedHelper(evt, prefixLen, suffixLen, '', personality);
636};
637
638
639/**
640 * The function called by describeTextChanged after it's figured out
641 * what text was deleted, what text was inserted, and what additional
642 * autocomplete text was added.
643 * @param {cvox.TextChangeEvent} evt The text change event.
644 * @param {number} prefixLen The number of characters in the common prefix
645 *     of this.value and newValue.
646 * @param {number} suffixLen The number of characters in the common suffix
647 *     of this.value and newValue.
648 * @param {string} autocompleteSuffix The autocomplete string that was added
649 *     to the end, if any. It should be spoken at the end of the utterance
650 *     describing the change.
651 * @param {Object=} opt_personality Personality to speak the text.
652 */
653cvox.ChromeVoxEditableTextBase.prototype.describeTextChangedHelper = function(
654    evt, prefixLen, suffixLen, autocompleteSuffix, opt_personality) {
655  var len = this.value.length;
656  var newLen = evt.value.length;
657  var deletedLen = len - prefixLen - suffixLen;
658  var deleted = this.value.substr(prefixLen, deletedLen);
659  var insertedLen = newLen - prefixLen - suffixLen;
660  var inserted = evt.value.substr(prefixLen, insertedLen);
661  var utterance = '';
662  var triggeredByUser = evt.triggeredByUser;
663
664  if (insertedLen > 1) {
665    utterance = inserted;
666  } else if (insertedLen == 1) {
667    if ((cvox.ChromeVox.typingEcho == cvox.TypingEcho.WORD ||
668            cvox.ChromeVox.typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD) &&
669        this.isWordBreakChar(inserted) &&
670        prefixLen > 0 &&
671        !this.isWordBreakChar(evt.value.substr(prefixLen - 1, 1))) {
672      // Speak previous word.
673      var index = prefixLen;
674      while (index > 0 && !this.isWordBreakChar(evt.value[index - 1])) {
675        index--;
676      }
677      if (index < prefixLen) {
678        utterance = evt.value.substr(index, prefixLen + 1 - index);
679      } else {
680        utterance = inserted;
681        triggeredByUser = false; // Implies QUEUE_MODE_QUEUE.
682      }
683    } else if (cvox.ChromeVox.typingEcho == cvox.TypingEcho.CHARACTER ||
684        cvox.ChromeVox.typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD) {
685      // This particular case is handled in event watcher. See the key press
686      // handler for more details.
687      utterance = cvox.ChromeVoxEditableTextBase.eventTypingEcho ? '' :
688          inserted;
689    }
690  } else if (deletedLen > 1 && !autocompleteSuffix) {
691    utterance = deleted + ', deleted';
692  } else if (deletedLen == 1) {
693    utterance = deleted;
694  }
695
696  if (autocompleteSuffix && utterance) {
697    utterance += ', ' + autocompleteSuffix;
698  } else if (autocompleteSuffix) {
699    utterance = autocompleteSuffix;
700  }
701
702  if (utterance) {
703    this.speak(utterance, triggeredByUser, opt_personality);
704  }
705};
706
707
708/**
709 * Moves the cursor forward by one character.
710 * @return {boolean} True if the action was handled.
711 */
712cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextCharacter =
713    function() { return false; };
714
715
716/**
717 * Moves the cursor backward by one character.
718 * @return {boolean} True if the action was handled.
719 */
720cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousCharacter =
721    function() { return false; };
722
723
724/**
725 * Moves the cursor forward by one word.
726 * @return {boolean} True if the action was handled.
727 */
728cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextWord =
729    function() { return false; };
730
731
732/**
733 * Moves the cursor backward by one word.
734 * @return {boolean} True if the action was handled.
735 */
736cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousWord =
737    function() { return false; };
738
739
740/**
741 * Moves the cursor forward by one line.
742 * @return {boolean} True if the action was handled.
743 */
744cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextLine =
745    function() { return false; };
746
747
748/**
749 * Moves the cursor backward by one line.
750 * @return {boolean} True if the action was handled.
751 */
752cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousLine =
753    function() { return false; };
754
755
756/**
757 * Moves the cursor forward by one paragraph.
758 * @return {boolean} True if the action was handled.
759 */
760cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextParagraph =
761    function() { return false; };
762
763
764/**
765 * Moves the cursor backward by one paragraph.
766 * @return {boolean} True if the action was handled.
767 */
768cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousParagraph =
769    function() { return false; };
770
771
772/******************************************/
773
774
775/**
776 * A subclass of ChromeVoxEditableTextBase a text element that's part of
777 * the webpage DOM. Contains common code shared by both EditableHTMLInput
778 * and EditableTextArea, but that might not apply to a non-DOM text box.
779 * @param {Element} node A DOM node which allows text input.
780 * @param {string} value The string value of the editable text control.
781 * @param {number} start The 0-based start cursor/selection index.
782 * @param {number} end The 0-based end cursor/selection index.
783 * @param {boolean} isPassword Whether the text control if a password field.
784 * @param {cvox.TtsInterface} tts A TTS object.
785 * @extends {cvox.ChromeVoxEditableTextBase}
786 * @constructor
787 */
788cvox.ChromeVoxEditableElement = function(node, value, start, end, isPassword,
789    tts) {
790  goog.base(this, value, start, end, isPassword, tts);
791
792  /**
793   * The DOM node which allows text input.
794   * @type {Element}
795   * @protected
796   */
797  this.node = node;
798
799  /**
800   * True if the description was just spoken.
801   * @type {boolean}
802   * @private
803   */
804  this.justSpokeDescription_ = false;
805};
806goog.inherits(cvox.ChromeVoxEditableElement,
807    cvox.ChromeVoxEditableTextBase);
808
809
810/**
811 * Update the state of the text and selection and describe any changes as
812 * appropriate.
813 *
814 * @param {cvox.TextChangeEvent} evt The text change event.
815 */
816cvox.ChromeVoxEditableElement.prototype.changed = function(evt) {
817  // Ignore changes to the cursor and selection if they happen immediately
818  // after the description was just spoken. This avoid double-speaking when,
819  // for example, a text field is focused and then a moment later the
820  // contents are selected. If the value changes, though, this change will
821  // not be ignored.
822  if (this.justSpokeDescription_ && this.value == evt.value) {
823    this.value = evt.value;
824    this.start = evt.start;
825    this.end = evt.end;
826    this.justSpokeDescription_ = false;
827  }
828  goog.base(this, 'changed', evt);
829};
830
831
832/** @override */
833cvox.ChromeVoxEditableElement.prototype.moveCursorToNextCharacter = function() {
834  var node = this.node;
835  node.selectionEnd++;
836  node.selectionStart = node.selectionEnd;
837  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
838  return true;
839};
840
841
842/** @override */
843cvox.ChromeVoxEditableElement.prototype.moveCursorToPreviousCharacter =
844    function() {
845  var node = this.node;
846  node.selectionStart--;
847  node.selectionEnd = node.selectionStart;
848  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
849  return true;
850};
851
852
853/** @override */
854cvox.ChromeVoxEditableElement.prototype.moveCursorToNextWord = function() {
855  var node = this.node;
856  var length = node.value.length;
857  var re = /\W+/gm;
858  var substring = node.value.substring(node.selectionEnd);
859  var match = re.exec(substring);
860  if (match !== null && match.index == 0) {
861    // Ignore word-breaking sequences right next to the cursor.
862    match = re.exec(substring);
863  }
864  var index = (match === null) ? length : match.index + node.selectionEnd;
865  node.selectionStart = node.selectionEnd = index;
866  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
867  return true;
868};
869
870
871/** @override */
872cvox.ChromeVoxEditableElement.prototype.moveCursorToPreviousWord = function() {
873  var node = this.node;
874  var length = node.value.length;
875  var re = /\W+/gm;
876  var substring = node.value.substring(0, node.selectionStart);
877  var index = 0;
878  while (re.exec(substring) !== null) {
879    if (re.lastIndex < node.selectionStart) {
880      index = re.lastIndex;
881    }
882  }
883  node.selectionStart = node.selectionEnd = index;
884  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
885  return true;
886};
887
888
889/** @override */
890cvox.ChromeVoxEditableElement.prototype.moveCursorToNextParagraph =
891    function() {
892  var node = this.node;
893  var length = node.value.length;
894  var index = node.selectionEnd >= length ? length :
895      node.value.indexOf('\n', node.selectionEnd);
896  if (index < 0) {
897    index = length;
898  }
899  node.selectionStart = node.selectionEnd = index + 1;
900  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
901  return true;
902};
903
904
905/** @override */
906cvox.ChromeVoxEditableElement.prototype.moveCursorToPreviousParagraph =
907    function() {
908  var node = this.node;
909  var index = node.selectionStart <= 0 ? 0 :
910      node.value.lastIndexOf('\n', node.selectionStart - 2) + 1;
911  if (index < 0) {
912    index = 0;
913  }
914  node.selectionStart = node.selectionEnd = index;
915  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
916  return true;
917};
918
919
920/******************************************/
921
922
923/**
924 * A subclass of ChromeVoxEditableElement for an HTMLInputElement.
925 * @param {HTMLInputElement} node The HTMLInputElement node.
926 * @param {cvox.TtsInterface} tts A TTS object.
927 * @extends {cvox.ChromeVoxEditableElement}
928 * @implements {cvox.TextHandlerInterface}
929 * @constructor
930 */
931cvox.ChromeVoxEditableHTMLInput = function(node, tts) {
932  this.node = node;
933  this.setup();
934  goog.base(this,
935            node,
936            node.value,
937            node.selectionStart,
938            node.selectionEnd,
939            node.type === 'password',
940            tts);
941};
942goog.inherits(cvox.ChromeVoxEditableHTMLInput,
943    cvox.ChromeVoxEditableElement);
944
945
946/**
947 * Performs setup for this input node.
948 * This accounts for exception-throwing behavior introduced by crbug.com/324360.
949 * @override
950 */
951cvox.ChromeVoxEditableHTMLInput.prototype.setup = function() {
952  if (!this.node) {
953    return;
954  }
955  if (!cvox.DomUtil.doesInputSupportSelection(this.node)) {
956    this.originalType = this.node.type;
957    this.node.type = 'text';
958  }
959};
960
961
962/**
963 * Performs teardown for this input node.
964 * This accounts for exception-throwing behavior introduced by crbug.com/324360.
965 * @override
966 */
967cvox.ChromeVoxEditableHTMLInput.prototype.teardown = function() {
968  if (this.node && this.originalType) {
969    this.node.type = this.originalType;
970  }
971};
972
973
974/**
975 * Update the state of the text and selection and describe any changes as
976 * appropriate.
977 *
978 * @param {boolean} triggeredByUser True if this was triggered by a user action.
979 */
980cvox.ChromeVoxEditableHTMLInput.prototype.update = function(triggeredByUser) {
981  var newValue = this.node.value;
982  var textChangeEvent = new cvox.TextChangeEvent(newValue,
983                                                 this.node.selectionStart,
984                                                 this.node.selectionEnd,
985                                                 triggeredByUser);
986  this.changed(textChangeEvent);
987};
988
989
990/******************************************/
991
992
993/**
994 * A subclass of ChromeVoxEditableElement for an HTMLTextAreaElement.
995 * @param {HTMLTextAreaElement} node The HTMLTextAreaElement node.
996 * @param {cvox.TtsInterface} tts A TTS object.
997 * @extends {cvox.ChromeVoxEditableElement}
998 * @implements {cvox.TextHandlerInterface}
999 * @constructor
1000 */
1001cvox.ChromeVoxEditableTextArea = function(node, tts) {
1002  goog.base(this, node, node.value, node.selectionStart, node.selectionEnd,
1003      false /* isPassword */, tts);
1004  this.multiline = true;
1005
1006  /**
1007   * True if the shadow is up-to-date with the current value of this text area.
1008   * @type {boolean}
1009   * @private
1010   */
1011  this.shadowIsCurrent_ = false;
1012};
1013goog.inherits(cvox.ChromeVoxEditableTextArea,
1014    cvox.ChromeVoxEditableElement);
1015
1016
1017/**
1018 * An offscreen div used to compute the line numbers. A single div is
1019 * shared by all instances of the class.
1020 * @type {!cvox.EditableTextAreaShadow|undefined}
1021 * @private
1022 */
1023cvox.ChromeVoxEditableTextArea.shadow_;
1024
1025
1026/**
1027 * Update the state of the text and selection and describe any changes as
1028 * appropriate.
1029 *
1030 * @param {boolean} triggeredByUser True if this was triggered by a user action.
1031 */
1032cvox.ChromeVoxEditableTextArea.prototype.update = function(triggeredByUser) {
1033  if (this.node.value != this.value) {
1034    this.shadowIsCurrent_ = false;
1035  }
1036  var textChangeEvent = new cvox.TextChangeEvent(this.node.value,
1037      this.node.selectionStart, this.node.selectionEnd, triggeredByUser);
1038  this.changed(textChangeEvent);
1039};
1040
1041
1042/**
1043 * Get the line number corresponding to a particular index.
1044 * @param {number} index The 0-based character index.
1045 * @return {number} The 0-based line number corresponding to that character.
1046 */
1047cvox.ChromeVoxEditableTextArea.prototype.getLineIndex = function(index) {
1048  return this.getShadow().getLineIndex(index);
1049};
1050
1051
1052/**
1053 * Get the start character index of a line.
1054 * @param {number} index The 0-based line index.
1055 * @return {number} The 0-based index of the first character in this line.
1056 */
1057cvox.ChromeVoxEditableTextArea.prototype.getLineStart = function(index) {
1058  return this.getShadow().getLineStart(index);
1059};
1060
1061
1062/**
1063 * Get the end character index of a line.
1064 * @param {number} index The 0-based line index.
1065 * @return {number} The 0-based index of the end of this line.
1066 */
1067cvox.ChromeVoxEditableTextArea.prototype.getLineEnd = function(index) {
1068  return this.getShadow().getLineEnd(index);
1069};
1070
1071
1072/**
1073 * Update the shadow object, an offscreen div used to compute line numbers.
1074 * @return {!cvox.EditableTextAreaShadow} The shadow object.
1075 */
1076cvox.ChromeVoxEditableTextArea.prototype.getShadow = function() {
1077  var shadow = cvox.ChromeVoxEditableTextArea.shadow_;
1078  if (!shadow) {
1079    shadow = cvox.ChromeVoxEditableTextArea.shadow_ =
1080        new cvox.EditableTextAreaShadow();
1081  }
1082  if (!this.shadowIsCurrent_) {
1083    shadow.update(this.node);
1084    this.shadowIsCurrent_ = true;
1085  }
1086  return shadow;
1087};
1088
1089
1090/** @override */
1091cvox.ChromeVoxEditableTextArea.prototype.moveCursorToNextLine = function() {
1092  var node = this.node;
1093  var length = node.value.length;
1094  if (node.selectionEnd >= length) {
1095    return false;
1096  }
1097  var shadow = this.getShadow();
1098  var lineIndex = shadow.getLineIndex(node.selectionEnd);
1099  var lineStart = shadow.getLineStart(lineIndex);
1100  var offset = node.selectionEnd - lineStart;
1101  var lastLine = (length == 0) ? 0 : shadow.getLineIndex(length - 1);
1102  var newCursorPosition = (lineIndex >= lastLine) ? length :
1103      Math.min(shadow.getLineStart(lineIndex + 1) + offset,
1104          shadow.getLineEnd(lineIndex + 1));
1105  node.selectionStart = node.selectionEnd = newCursorPosition;
1106  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
1107  return true;
1108};
1109
1110
1111/** @override */
1112cvox.ChromeVoxEditableTextArea.prototype.moveCursorToPreviousLine = function() {
1113  var node = this.node;
1114  if (node.selectionStart <= 0) {
1115    return false;
1116  }
1117  var shadow = this.getShadow();
1118  var lineIndex = shadow.getLineIndex(node.selectionStart);
1119  var lineStart = shadow.getLineStart(lineIndex);
1120  var offset = node.selectionStart - lineStart;
1121  var newCursorPosition = (lineIndex <= 0) ? 0 :
1122      Math.min(shadow.getLineStart(lineIndex - 1) + offset,
1123          shadow.getLineEnd(lineIndex - 1));
1124  node.selectionStart = node.selectionEnd = newCursorPosition;
1125  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
1126  return true;
1127};
1128
1129
1130/******************************************/
1131
1132
1133/**
1134 * A subclass of ChromeVoxEditableElement for elements that are contentEditable.
1135 * This is also used for a region of HTML with the ARIA role of "textbox",
1136 * so that an author can create a pure-JavaScript editable text object - this
1137 * will work the same as contentEditable as long as the DOM selection is
1138 * updated properly within the textbox when it has focus.
1139 * @param {Element} node The root contentEditable node.
1140 * @param {cvox.TtsInterface} tts A TTS object.
1141 * @extends {cvox.ChromeVoxEditableElement}
1142 * @implements {cvox.TextHandlerInterface}
1143 * @constructor
1144 */
1145cvox.ChromeVoxEditableContentEditable = function(node, tts) {
1146  goog.base(this, node, '', 0, 0, false /* isPassword */, tts);
1147
1148
1149  /**
1150   * True if the ContentEditableExtractor is current with this field's data.
1151   * @type {boolean}
1152   * @private
1153   */
1154  this.extractorIsCurrent_ = false;
1155
1156  var extractor = this.getExtractor();
1157  this.value = extractor.getText();
1158  this.start = extractor.getStartIndex();
1159  this.end = extractor.getEndIndex();
1160  this.multiline = true;
1161};
1162goog.inherits(cvox.ChromeVoxEditableContentEditable,
1163    cvox.ChromeVoxEditableElement);
1164
1165/**
1166 * A helper used to compute the line numbers. A single object is
1167 * shared by all instances of the class.
1168 * @type {!cvox.ContentEditableExtractor|undefined}
1169 * @private
1170 */
1171cvox.ChromeVoxEditableContentEditable.extractor_;
1172
1173
1174/**
1175 * Update the state of the text and selection and describe any changes as
1176 * appropriate.
1177 *
1178 * @param {boolean} triggeredByUser True if this was triggered by a user action.
1179 */
1180cvox.ChromeVoxEditableContentEditable.prototype.update =
1181    function(triggeredByUser) {
1182  this.extractorIsCurrent_ = false;
1183  var textChangeEvent = new cvox.TextChangeEvent(
1184      this.getExtractor().getText(),
1185      this.getExtractor().getStartIndex(),
1186      this.getExtractor().getEndIndex(),
1187      triggeredByUser);
1188  this.changed(textChangeEvent);
1189};
1190
1191
1192/**
1193 * Get the line number corresponding to a particular index.
1194 * @param {number} index The 0-based character index.
1195 * @return {number} The 0-based line number corresponding to that character.
1196 */
1197cvox.ChromeVoxEditableContentEditable.prototype.getLineIndex = function(index) {
1198  return this.getExtractor().getLineIndex(index);
1199};
1200
1201
1202/**
1203 * Get the start character index of a line.
1204 * @param {number} index The 0-based line index.
1205 * @return {number} The 0-based index of the first character in this line.
1206 */
1207cvox.ChromeVoxEditableContentEditable.prototype.getLineStart = function(index) {
1208  return this.getExtractor().getLineStart(index);
1209};
1210
1211
1212/**
1213 * Get the end character index of a line.
1214 * @param {number} index The 0-based line index.
1215 * @return {number} The 0-based index of the end of this line.
1216 */
1217cvox.ChromeVoxEditableContentEditable.prototype.getLineEnd = function(index) {
1218  return this.getExtractor().getLineEnd(index);
1219};
1220
1221
1222/**
1223 * Update the extractor object, an offscreen div used to compute line numbers.
1224 * @return {!cvox.ContentEditableExtractor} The extractor object.
1225 */
1226cvox.ChromeVoxEditableContentEditable.prototype.getExtractor = function() {
1227  var extractor = cvox.ChromeVoxEditableContentEditable.extractor_;
1228  if (!extractor) {
1229    extractor = cvox.ChromeVoxEditableContentEditable.extractor_ =
1230        new cvox.ContentEditableExtractor();
1231  }
1232  if (!this.extractorIsCurrent_) {
1233    extractor.update(this.node);
1234    this.extractorIsCurrent_ = true;
1235  }
1236  return extractor;
1237};
1238
1239
1240/**
1241 * @override
1242 */
1243cvox.ChromeVoxEditableContentEditable.prototype.changed =
1244    function(evt) {
1245  if (!evt.triggeredByUser) {
1246    return;
1247  }
1248  // Take over here if we can't describe a change; assume it's a blank line.
1249  if (!this.shouldDescribeChange(evt)) {
1250    this.speak(cvox.ChromeVox.msgs.getMsg('text_box_blank'), true);
1251  } else {
1252    goog.base(this, 'changed', evt);
1253  }
1254};
1255
1256
1257/** @override */
1258cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToNextCharacter =
1259    function() {
1260  window.getSelection().modify('move', 'forward', 'character');
1261  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
1262  return true;
1263};
1264
1265
1266/** @override */
1267cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToPreviousCharacter =
1268    function() {
1269  window.getSelection().modify('move', 'backward', 'character');
1270  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
1271  return true;
1272};
1273
1274
1275/** @override */
1276cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToNextParagraph =
1277    function() {
1278  window.getSelection().modify('move', 'forward', 'paragraph');
1279  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
1280  return true;
1281};
1282
1283/** @override */
1284cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToPreviousParagraph =
1285    function() {
1286  window.getSelection().modify('move', 'backward', 'paragraph');
1287  cvox.ChromeVoxEventWatcher.handleTextChanged(true);
1288  return true;
1289};
1290
1291
1292/**
1293 * @override
1294 */
1295cvox.ChromeVoxEditableContentEditable.prototype.shouldDescribeChange =
1296    function(evt) {
1297  var sel = window.getSelection();
1298  var cursor = new cvox.Cursor(sel.baseNode, sel.baseOffset, '');
1299
1300  // This is a very specific work around because of our buggy content editable
1301  // support. Blank new lines are not captured in the line indexing data
1302  // structures.
1303  // Scenario: given a piece of text like:
1304  //
1305  // Some Title
1306  //
1307  // Description
1308  // Footer
1309  //
1310  // The new lines after Title are not traversed to by TraverseUtil. A root fix
1311  // would make changes there. However, considering the fickle nature of that
1312  // code, we specifically detect for new lines here.
1313  if (Math.abs(this.start - evt.start) != 1 &&
1314      this.start == this.end &&
1315      evt.start == evt.end &&
1316      sel.baseNode == sel.extentNode &&
1317      sel.baseOffset == sel.extentOffset &&
1318      sel.baseNode.nodeType == Node.ELEMENT_NODE &&
1319      sel.baseNode.querySelector('BR') &&
1320      cvox.TraverseUtil.forwardsChar(cursor, [], [])) {
1321    // This case detects if the range selection surrounds a new line,
1322    // but there is still content after the new line (like the example
1323    // above after "Title"). In these cases, we "pretend" we're the
1324    // last character so we speak "blank".
1325    return false;
1326  }
1327
1328  // Otherwise, we should never speak "blank" no matter what (even if
1329  // we're at the end of a content editable).
1330  return true;
1331};
1332