Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / editable_text_base.js
blob573123915bab077d1a00c5fa4c499eb822060fda
1 // Copyright 2015 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.
5 goog.provide('cvox.ChromeVoxEditableTextBase');
6 goog.provide('cvox.TextChangeEvent');
7 goog.provide('cvox.TypingEcho');
9 goog.require('cvox.AbstractTts');
10 goog.require('cvox.ChromeVox');
11 goog.require('cvox.TtsInterface');
12 goog.require('goog.i18n.MessageFormat');
15 /**
16  * @fileoverview Generalized logic for providing spoken feedback when editing
17  * text fields, both single and multiline fields.
18  *
19  * {@code ChromeVoxEditableTextBase} is a generalized class that takes the
20  * current state in the form of a text string, a cursor start location and a
21  * cursor end location, and calls a speak method with the resulting text to
22  * be spoken.  This class can be used directly for single line fields or
23  * extended to override methods that extract lines for multiline fields
24  * or to provide other customizations.
25  */
28 /**
29  * A class containing the information needed to speak
30  * a text change event to the user.
31  *
32  * @constructor
33  * @param {string} newValue The new string value of the editable text control.
34  * @param {number} newStart The new 0-based start cursor/selection index.
35  * @param {number} newEnd The new 0-based end cursor/selection index.
36  * @param {boolean} triggeredByUser .
37  */
38 cvox.TextChangeEvent = function(newValue, newStart, newEnd, triggeredByUser) {
39   this.value = newValue;
40   this.start = newStart;
41   this.end = newEnd;
42   this.triggeredByUser = triggeredByUser;
44   // Adjust offsets to be in left to right order.
45   if (this.start > this.end) {
46     var tempOffset = this.end;
47     this.end = this.start;
48     this.start = tempOffset;
49   }
53 /**
54  * A list of typing echo options.
55  * This defines the way typed characters get spoken.
56  * CHARACTER: echoes typed characters.
57  * WORD: echoes a word once a breaking character is typed (i.e. spacebar).
58  * CHARACTER_AND_WORD: combines CHARACTER and WORD behavior.
59  * NONE: speaks nothing when typing.
60  * COUNT: The number of possible echo levels.
61  * @enum
62  */
63 cvox.TypingEcho = {
64   CHARACTER: 0,
65   WORD: 1,
66   CHARACTER_AND_WORD: 2,
67   NONE: 3,
68   COUNT: 4
72 /**
73  * @param {number} cur Current typing echo.
74  * @return {number} Next typing echo.
75  */
76 cvox.TypingEcho.cycle = function(cur) {
77   return (cur + 1) % cvox.TypingEcho.COUNT;
81 /**
82  * Return if characters should be spoken given the typing echo option.
83  * @param {number} typingEcho Typing echo option.
84  * @return {boolean} Whether the character should be spoken.
85  */
86 cvox.TypingEcho.shouldSpeakChar = function(typingEcho) {
87   return typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD ||
88       typingEcho == cvox.TypingEcho.CHARACTER;
92 /**
93  * A class representing an abstracted editable text control.
94  * @param {string} value The string value of the editable text control.
95  * @param {number} start The 0-based start cursor/selection index.
96  * @param {number} end The 0-based end cursor/selection index.
97  * @param {boolean} isPassword Whether the text control if a password field.
98  * @param {cvox.TtsInterface} tts A TTS object.
99  * @constructor
100  */
101 cvox.ChromeVoxEditableTextBase = function(value, start, end, isPassword, tts) {
102   /**
103    * Current value of the text field.
104    * @type {string}
105    * @protected
106    */
107   this.value = value;
109   /**
110    * 0-based selection start index.
111    * @type {number}
112    * @protected
113    */
114   this.start = start;
116   /**
117    * 0-based selection end index.
118    * @type {number}
119    * @protected
120    */
121   this.end = end;
123   /**
124    * True if this is a password field.
125    * @type {boolean}
126    * @protected
127    */
128   this.isPassword = isPassword;
130   /**
131    * Text-to-speech object implementing speak() and stop() methods.
132    * @type {cvox.TtsInterface}
133    * @protected
134    */
135   this.tts = tts;
137   /**
138    * Whether or not the text field is multiline.
139    * @type {boolean}
140    * @protected
141    */
142   this.multiline = false;
144   /**
145    * Whether or not the last update to the text and selection was described.
146    *
147    * Some consumers of this flag like |ChromeVoxEventWatcher| depend on and
148    * react to when this flag is false by generating alternative feedback.
149    * @type {boolean}
150    */
151   this.lastChangeDescribed = false;
157  * Performs setup for this element.
158  */
159 cvox.ChromeVoxEditableTextBase.prototype.setup = function() {};
163  * Performs teardown for this element.
164  */
165 cvox.ChromeVoxEditableTextBase.prototype.teardown = function() {};
169  * Whether or not moving the cursor from one character to another considers
170  * the cursor to be a block (false) or an i-beam (true).
172  * If the cursor is a block, then the value of the character to the right
173  * of the cursor index is always read when the cursor moves, no matter what
174  * the previous cursor location was - this is how PC screenreaders work.
176  * If the cursor is an i-beam, moving the cursor by one character reads the
177  * character that was crossed over, which may be the character to the left or
178  * right of the new cursor index depending on the direction.
180  * If the current platform is a Mac, we will use an i-beam cursor. If not,
181  * then we will use the block cursor.
183  * @type {boolean}
184  */
185 cvox.ChromeVoxEditableTextBase.useIBeamCursor = cvox.ChromeVox.isMac;
189  * Switches on or off typing echo based on events. When set, editable text
190  * updates for single-character insertions are handled in event watcher's key
191  * press handler.
192  * @type {boolean}
193  */
194 cvox.ChromeVoxEditableTextBase.eventTypingEcho = false;
198  * The maximum number of characters that are short enough to speak in response
199  * to an event. For example, if the user selects "Hello", we will speak
200  * "Hello, selected", but if the user selects 1000 characters, we will speak
201  * "text selected" instead.
203  * @type {number}
204  */
205 cvox.ChromeVoxEditableTextBase.prototype.maxShortPhraseLen = 60;
209  * Get the line number corresponding to a particular index.
210  * Default implementation that can be overridden by subclasses.
211  * @param {number} index The 0-based character index.
212  * @return {number} The 0-based line number corresponding to that character.
213  */
214 cvox.ChromeVoxEditableTextBase.prototype.getLineIndex = function(index) {
215   return 0;
220  * Get the start character index of a line.
221  * Default implementation that can be overridden by subclasses.
222  * @param {number} index The 0-based line index.
223  * @return {number} The 0-based index of the first character in this line.
224  */
225 cvox.ChromeVoxEditableTextBase.prototype.getLineStart = function(index) {
226   return 0;
231  * Get the end character index of a line.
232  * Default implementation that can be overridden by subclasses.
233  * @param {number} index The 0-based line index.
234  * @return {number} The 0-based index of the end of this line.
235  */
236 cvox.ChromeVoxEditableTextBase.prototype.getLineEnd = function(index) {
237   return this.value.length;
242  * Get the full text of the current line.
243  * @param {number} index The 0-based line index.
244  * @return {string} The text of the line.
245  */
246 cvox.ChromeVoxEditableTextBase.prototype.getLine = function(index) {
247   var lineStart = this.getLineStart(index);
248   var lineEnd = this.getLineEnd(index);
249   return this.value.substr(lineStart, lineEnd - lineStart);
254  * @param {string} ch The character to test.
255  * @return {boolean} True if a character is whitespace.
256  */
257 cvox.ChromeVoxEditableTextBase.prototype.isWhitespaceChar = function(ch) {
258   return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
263  * @param {string} ch The character to test.
264  * @return {boolean} True if a character breaks a word, used to determine
265  *     if the previous word should be spoken.
266  */
267 cvox.ChromeVoxEditableTextBase.prototype.isWordBreakChar = function(ch) {
268   return !!ch.match(/^\W$/);
273  * @param {cvox.TextChangeEvent} evt The new text changed event to test.
274  * @return {boolean} True if the event, when compared to the previous text,
275  * should trigger description.
276  */
277 cvox.ChromeVoxEditableTextBase.prototype.shouldDescribeChange = function(evt) {
278   if (evt.value == this.value &&
279       evt.start == this.start &&
280       evt.end == this.end) {
281     return false;
282   }
283   return true;
288  * Speak text, but if it's a single character, describe the character.
289  * @param {string} str The string to speak.
290  * @param {boolean=} opt_triggeredByUser True if the speech was triggered by a
291  * user action.
292  * @param {Object=} opt_personality Personality used to speak text.
293  */
294 cvox.ChromeVoxEditableTextBase.prototype.speak =
295     function(str, opt_triggeredByUser, opt_personality) {
296   var queueMode = cvox.QueueMode.QUEUE;
297   if (opt_triggeredByUser === true) {
298     queueMode = cvox.QueueMode.FLUSH;
299   }
300   this.tts.speak(str, queueMode, opt_personality || {});
305  * Update the state of the text and selection and describe any changes as
306  * appropriate.
308  * @param {cvox.TextChangeEvent} evt The text change event.
309  */
310 cvox.ChromeVoxEditableTextBase.prototype.changed = function(evt) {
311   if (!this.shouldDescribeChange(evt)) {
312     this.lastChangeDescribed = false;
313     return;
314   }
316   if (evt.value == this.value) {
317     this.describeSelectionChanged(evt);
318   } else {
319     this.describeTextChanged(evt);
320   }
321   this.lastChangeDescribed = true;
323   this.value = evt.value;
324   this.start = evt.start;
325   this.end = evt.end;
330  * Describe a change in the selection or cursor position when the text
331  * stays the same.
332  * @param {cvox.TextChangeEvent} evt The text change event.
333  */
334 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged =
335     function(evt) {
336   // TODO(deboer): Factor this into two function:
337   //   - one to determine the selection event
338   //   - one to speak
340   if (this.isPassword) {
341     this.speak((new goog.i18n.MessageFormat(cvox.ChromeVox.msgs.getMsg('dot'))
342         .format({'COUNT': 1})), evt.triggeredByUser);
343     return;
344   }
345   if (evt.start == evt.end) {
346     // It's currently a cursor.
347     if (this.start != this.end) {
348       // It was previously a selection, so just announce 'unselected'.
349       this.speak(cvox.ChromeVox.msgs.getMsg('Unselected'), evt.triggeredByUser);
350     } else if (this.getLineIndex(this.start) !=
351         this.getLineIndex(evt.start)) {
352       // Moved to a different line; read it.
353       var lineValue = this.getLine(this.getLineIndex(evt.start));
354       if (lineValue == '') {
355         lineValue = cvox.ChromeVox.msgs.getMsg('text_box_blank');
356       } else if (/^\s+$/.test(lineValue)) {
357         lineValue = cvox.ChromeVox.msgs.getMsg('text_box_whitespace');
358       }
359       this.speak(lineValue, evt.triggeredByUser);
360     } else if (this.start == evt.start + 1 ||
361         this.start == evt.start - 1) {
362       // Moved by one character; read it.
363       if (!cvox.ChromeVoxEditableTextBase.useIBeamCursor) {
364         if (evt.start == this.value.length) {
365           if (cvox.ChromeVox.verbosity == cvox.VERBOSITY_VERBOSE) {
366             this.speak(cvox.ChromeVox.msgs.getMsg('end_of_text_verbose'),
367                        evt.triggeredByUser);
368           } else {
369             this.speak(cvox.ChromeVox.msgs.getMsg('end_of_text_brief'),
370                        evt.triggeredByUser);
371           }
372         } else {
373           this.speak(this.value.substr(evt.start, 1),
374                      evt.triggeredByUser,
375                      {'phoneticCharacters': evt.triggeredByUser});
376         }
377       } else {
378         this.speak(this.value.substr(Math.min(this.start, evt.start), 1),
379             evt.triggeredByUser,
380             {'phoneticCharacters': evt.triggeredByUser});
381       }
382     } else {
383       // Moved by more than one character. Read all characters crossed.
384       this.speak(this.value.substr(Math.min(this.start, evt.start),
385           Math.abs(this.start - evt.start)), evt.triggeredByUser);
386     }
387   } else {
388     // It's currently a selection.
389     if (this.start + 1 == evt.start &&
390         this.end == this.value.length &&
391         evt.end == this.value.length) {
392       // Autocomplete: the user typed one character of autocompleted text.
393       this.speak(this.value.substr(this.start, 1), evt.triggeredByUser);
394       this.speak(this.value.substr(evt.start));
395     } else if (this.start == this.end) {
396       // It was previously a cursor.
397       this.speak(this.value.substr(evt.start, evt.end - evt.start),
398                  evt.triggeredByUser);
399       this.speak(cvox.ChromeVox.msgs.getMsg('selected'));
400     } else if (this.start == evt.start && this.end < evt.end) {
401       this.speak(this.value.substr(this.end, evt.end - this.end),
402                  evt.triggeredByUser);
403       this.speak(cvox.ChromeVox.msgs.getMsg('added_to_selection'));
404     } else if (this.start == evt.start && this.end > evt.end) {
405       this.speak(this.value.substr(evt.end, this.end - evt.end),
406                  evt.triggeredByUser);
407       this.speak(cvox.ChromeVox.msgs.getMsg('removed_from_selection'));
408     } else if (this.end == evt.end && this.start > evt.start) {
409       this.speak(this.value.substr(evt.start, this.start - evt.start),
410                  evt.triggeredByUser);
411       this.speak(cvox.ChromeVox.msgs.getMsg('added_to_selection'));
412     } else if (this.end == evt.end && this.start < evt.start) {
413       this.speak(this.value.substr(this.start, evt.start - this.start),
414                  evt.triggeredByUser);
415       this.speak(cvox.ChromeVox.msgs.getMsg('removed_from_selection'));
416     } else {
417       // The selection changed but it wasn't an obvious extension of
418       // a previous selection. Just read the new selection.
419       this.speak(this.value.substr(evt.start, evt.end - evt.start),
420                  evt.triggeredByUser);
421       this.speak(cvox.ChromeVox.msgs.getMsg('selected'));
422     }
423   }
428  * Describe a change where the text changes.
429  * @param {cvox.TextChangeEvent} evt The text change event.
430  */
431 cvox.ChromeVoxEditableTextBase.prototype.describeTextChanged = function(evt) {
432   var personality = {};
433   if (evt.value.length < this.value.length) {
434     personality = cvox.AbstractTts.PERSONALITY_DELETED;
435   }
436   if (this.isPassword) {
437     this.speak((new goog.i18n.MessageFormat(cvox.ChromeVox.msgs.getMsg('dot'))
438         .format({'COUNT': 1})), evt.triggeredByUser, personality);
439     return;
440   }
442   var value = this.value;
443   var len = value.length;
444   var newLen = evt.value.length;
445   var autocompleteSuffix = '';
446   // Make a copy of evtValue and evtEnd to avoid changing anything in
447   // the event itself.
448   var evtValue = evt.value;
449   var evtEnd = evt.end;
451   // First, see if there's a selection at the end that might have been
452   // added by autocomplete. If so, strip it off into a separate variable.
453   if (evt.start < evtEnd && evtEnd == newLen) {
454     autocompleteSuffix = evtValue.substr(evt.start);
455     evtValue = evtValue.substr(0, evt.start);
456     evtEnd = evt.start;
457   }
459   // Now see if the previous selection (if any) was deleted
460   // and any new text was inserted at that character position.
461   // This would handle pasting and entering text by typing, both from
462   // a cursor and from a selection.
463   var prefixLen = this.start;
464   var suffixLen = len - this.end;
465   if (newLen >= prefixLen + suffixLen + (evtEnd - evt.start) &&
466       evtValue.substr(0, prefixLen) == value.substr(0, prefixLen) &&
467       evtValue.substr(newLen - suffixLen) == value.substr(this.end)) {
468     // However, in a dynamic content editable, defer to authoritative events
469     // (clipboard, key press) to reduce guess work when observing insertions.
470     // Only use this logic when observing deletions (and insertion of word
471     // breakers).
472     // TODO(dtseng): Think about a more reliable way to do this.
473     if (!(this instanceof cvox.ChromeVoxEditableContentEditable) ||
474         newLen < len ||
475         this.isWordBreakChar(evt.value[newLen - 1] || '')) {
476       this.describeTextChangedHelper(
477           evt, prefixLen, suffixLen, autocompleteSuffix, personality);
478     }
479     return;
480   }
482   // Next, see if one or more characters were deleted from the previous
483   // cursor position and the new cursor is in the expected place. This
484   // handles backspace, forward-delete, and similar shortcuts that delete
485   // a word or line.
486   prefixLen = evt.start;
487   suffixLen = newLen - evtEnd;
488   if (this.start == this.end &&
489       evt.start == evtEnd &&
490       evtValue.substr(0, prefixLen) == value.substr(0, prefixLen) &&
491       evtValue.substr(newLen - suffixLen) ==
492       value.substr(len - suffixLen)) {
493     this.describeTextChangedHelper(
494         evt, prefixLen, suffixLen, autocompleteSuffix, personality);
495     return;
496   }
498   // If all else fails, we assume the change was not the result of a normal
499   // user editing operation, so we'll have to speak feedback based only
500   // on the changes to the text, not the cursor position / selection.
501   // First, restore the autocomplete text if any.
502   evtValue += autocompleteSuffix;
504   // Try to do a diff between the new and the old text. If it is a one character
505   // insertion/deletion at the start or at the end, just speak that character.
506   if ((evtValue.length == (value.length + 1)) ||
507       ((evtValue.length + 1) == value.length)) {
508     // The user added text either to the beginning or the end.
509     if (evtValue.length > value.length) {
510       if (evtValue.indexOf(value) == 0) {
511         this.speak(evtValue[evtValue.length - 1], evt.triggeredByUser,
512                    personality);
513         return;
514       } else if (evtValue.indexOf(value) == 1) {
515         this.speak(evtValue[0], evt.triggeredByUser, personality);
516         return;
517       }
518     }
519     // The user deleted text either from the beginning or the end.
520     if (evtValue.length < value.length) {
521       if (value.indexOf(evtValue) == 0) {
522         this.speak(value[value.length - 1], evt.triggeredByUser, personality);
523         return;
524       } else if (value.indexOf(evtValue) == 1) {
525         this.speak(value[0], evt.triggeredByUser, personality);
526         return;
527       }
528     }
529   }
531   if (this.multiline) {
532     // Fall back to announce deleted but omit the text that was deleted.
533     if (evt.value.length < this.value.length) {
534       this.speak(cvox.ChromeVox.msgs.getMsg('text_deleted'),
535                  evt.triggeredByUser, personality);
536     }
537     // The below is a somewhat loose way to deal with non-standard
538     // insertions/deletions. Intentionally skip for multiline since deletion
539     // announcements are covered above and insertions are non-standard (possibly
540     // due to auto complete). Since content editable's often refresh content by
541     // removing and inserting entire chunks of text, this type of logic often
542     // results in unintended consequences such as reading all text when only one
543     // character has been entered.
544     return;
545   }
547   // If the text is short, just speak the whole thing.
548   if (newLen <= this.maxShortPhraseLen) {
549     this.describeTextChangedHelper(evt, 0, 0, '', personality);
550     return;
551   }
553   // Otherwise, look for the common prefix and suffix, but back up so
554   // that we can speak complete words, to be minimally confusing.
555   prefixLen = 0;
556   while (prefixLen < len &&
557          prefixLen < newLen &&
558          value[prefixLen] == evtValue[prefixLen]) {
559     prefixLen++;
560   }
561   while (prefixLen > 0 && !this.isWordBreakChar(value[prefixLen - 1])) {
562     prefixLen--;
563   }
565   suffixLen = 0;
566   while (suffixLen < (len - prefixLen) &&
567          suffixLen < (newLen - prefixLen) &&
568          value[len - suffixLen - 1] == evtValue[newLen - suffixLen - 1]) {
569     suffixLen++;
570   }
571   while (suffixLen > 0 && !this.isWordBreakChar(value[len - suffixLen])) {
572     suffixLen--;
573   }
575   this.describeTextChangedHelper(evt, prefixLen, suffixLen, '', personality);
580  * The function called by describeTextChanged after it's figured out
581  * what text was deleted, what text was inserted, and what additional
582  * autocomplete text was added.
583  * @param {cvox.TextChangeEvent} evt The text change event.
584  * @param {number} prefixLen The number of characters in the common prefix
585  *     of this.value and newValue.
586  * @param {number} suffixLen The number of characters in the common suffix
587  *     of this.value and newValue.
588  * @param {string} autocompleteSuffix The autocomplete string that was added
589  *     to the end, if any. It should be spoken at the end of the utterance
590  *     describing the change.
591  * @param {Object=} opt_personality Personality to speak the text.
592  */
593 cvox.ChromeVoxEditableTextBase.prototype.describeTextChangedHelper = function(
594     evt, prefixLen, suffixLen, autocompleteSuffix, opt_personality) {
595   var len = this.value.length;
596   var newLen = evt.value.length;
597   var deletedLen = len - prefixLen - suffixLen;
598   var deleted = this.value.substr(prefixLen, deletedLen);
599   var insertedLen = newLen - prefixLen - suffixLen;
600   var inserted = evt.value.substr(prefixLen, insertedLen);
601   var utterance = '';
602   var triggeredByUser = evt.triggeredByUser;
604   if (insertedLen > 1) {
605     utterance = inserted;
606   } else if (insertedLen == 1) {
607     if ((cvox.ChromeVox.typingEcho == cvox.TypingEcho.WORD ||
608             cvox.ChromeVox.typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD) &&
609         this.isWordBreakChar(inserted) &&
610         prefixLen > 0 &&
611         !this.isWordBreakChar(evt.value.substr(prefixLen - 1, 1))) {
612       // Speak previous word.
613       var index = prefixLen;
614       while (index > 0 && !this.isWordBreakChar(evt.value[index - 1])) {
615         index--;
616       }
617       if (index < prefixLen) {
618         utterance = evt.value.substr(index, prefixLen + 1 - index);
619       } else {
620         utterance = inserted;
621         triggeredByUser = false; // Implies QUEUE_MODE_QUEUE.
622       }
623     } else if (cvox.ChromeVox.typingEcho == cvox.TypingEcho.CHARACTER ||
624         cvox.ChromeVox.typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD) {
625       // This particular case is handled in event watcher. See the key press
626       // handler for more details.
627       utterance = cvox.ChromeVoxEditableTextBase.eventTypingEcho ? '' :
628           inserted;
629     }
630   } else if (deletedLen > 1 && !autocompleteSuffix) {
631     utterance = deleted + ', deleted';
632   } else if (deletedLen == 1) {
633     utterance = deleted;
634   }
636   if (autocompleteSuffix && utterance) {
637     utterance += ', ' + autocompleteSuffix;
638   } else if (autocompleteSuffix) {
639     utterance = autocompleteSuffix;
640   }
642   if (utterance) {
643     this.speak(utterance, triggeredByUser, opt_personality);
644   }
649  * Moves the cursor forward by one character.
650  * @return {boolean} True if the action was handled.
651  */
652 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextCharacter =
653     function() { return false; };
657  * Moves the cursor backward by one character.
658  * @return {boolean} True if the action was handled.
659  */
660 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousCharacter =
661     function() { return false; };
665  * Moves the cursor forward by one word.
666  * @return {boolean} True if the action was handled.
667  */
668 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextWord =
669     function() { return false; };
673  * Moves the cursor backward by one word.
674  * @return {boolean} True if the action was handled.
675  */
676 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousWord =
677     function() { return false; };
681  * Moves the cursor forward by one line.
682  * @return {boolean} True if the action was handled.
683  */
684 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextLine =
685     function() { return false; };
689  * Moves the cursor backward by one line.
690  * @return {boolean} True if the action was handled.
691  */
692 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousLine =
693     function() { return false; };
697  * Moves the cursor forward by one paragraph.
698  * @return {boolean} True if the action was handled.
699  */
700 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextParagraph =
701     function() { return false; };
705  * Moves the cursor backward by one paragraph.
706  * @return {boolean} True if the action was handled.
707  */
708 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousParagraph =
709     function() { return false; };
712 /******************************************/