Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / editable_text.js
blobd336d37f1d56381818753e6fe95f1989c6b92b7f
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.
5 goog.provide('cvox.ChromeVoxEditableContentEditable');
6 goog.provide('cvox.ChromeVoxEditableElement');
7 goog.provide('cvox.ChromeVoxEditableHTMLInput');
8 goog.provide('cvox.ChromeVoxEditableTextArea');
9 goog.provide('cvox.TextHandlerInterface');
12 goog.require('cvox.BrailleTextHandler');
13 goog.require('cvox.ChromeVoxEditableTextBase');
14 goog.require('cvox.ContentEditableExtractor');
15 goog.require('cvox.DomUtil');
16 goog.require('cvox.EditableTextAreaShadow');
17 goog.require('cvox.TextChangeEvent');
18 goog.require('cvox.TtsInterface');
20 /**
21  * @fileoverview Gives the user spoken and braille feedback as they type,
22  * select text, and move the cursor in editable HTML text controls, including
23  * multiline controls and contenteditable regions.
24  *
25  * The two subclasses, ChromeVoxEditableHTMLInput and
26  * ChromeVoxEditableTextArea, take a HTML input (type=text) or HTML
27  * textarea node (respectively) in the constructor, and automatically
28  * handle retrieving the current state of the control, including
29  * computing line break information for a textarea using an offscreen
30  * shadow object. It is the responsibility of the user of these classes to
31  * trap key and focus events and call the update method as needed.
32  *
33  */
36 /**
37  * An interface for being notified when the text changes.
38  * @interface
39  */
40 cvox.TextHandlerInterface = function() {};
43 /**
44  * Called when text changes.
45  * @param {cvox.TextChangeEvent} evt The text change event.
46  */
47 cvox.TextHandlerInterface.prototype.changed = function(evt) {};
50 /**
51  * A subclass of ChromeVoxEditableTextBase a text element that's part of
52  * the webpage DOM. Contains common code shared by both EditableHTMLInput
53  * and EditableTextArea, but that might not apply to a non-DOM text box.
54  * @param {Element} node A DOM node which allows text input.
55  * @param {string} value The string value of the editable text control.
56  * @param {number} start The 0-based start cursor/selection index.
57  * @param {number} end The 0-based end cursor/selection index.
58  * @param {boolean} isPassword Whether the text control if a password field.
59  * @param {cvox.TtsInterface} tts A TTS object.
60  * @extends {cvox.ChromeVoxEditableTextBase}
61  * @constructor
62  */
63 cvox.ChromeVoxEditableElement = function(node, value, start, end, isPassword,
64     tts) {
65   goog.base(this, value, start, end, isPassword, tts);
67   /**
68    * An optional handler for braille output.
69    * @type {cvox.BrailleTextHandler|undefined}
70    * @private
71    */
72   this.brailleHandler_ = cvox.ChromeVox.braille ?
73       new cvox.BrailleTextHandler(cvox.ChromeVox.braille) : undefined;
75   /**
76    * The DOM node which allows text input.
77    * @type {Element}
78    * @protected
79    */
80   this.node = node;
82   /**
83    * True if the description was just spoken.
84    * @type {boolean}
85    * @private
86    */
87   this.justSpokeDescription_ = false;
89 goog.inherits(cvox.ChromeVoxEditableElement,
90     cvox.ChromeVoxEditableTextBase);
93 /** @override */
94 cvox.ChromeVoxEditableElement.prototype.changed = function(evt) {
95   // Ignore changes to the cursor and selection if they happen immediately
96   // after the description was just spoken. This avoid double-speaking when,
97   // for example, a text field is focused and then a moment later the
98   // contents are selected. If the value changes, though, this change will
99   // not be ignored.
100   if (this.justSpokeDescription_ && this.value == evt.value) {
101     this.value = evt.value;
102     this.start = evt.start;
103     this.end = evt.end;
104     this.justSpokeDescription_ = false;
105   }
106   goog.base(this, 'changed', evt);
107   if (this.lastChangeDescribed) {
108     this.brailleCurrentLine_();
109   }
113 /** @override */
114 cvox.ChromeVoxEditableElement.prototype.speak = function(
115     str, opt_triggeredByUser, opt_personality) {
116   // If there is a node associated with the editable text object,
117   // make sure that node has focus before speaking it.
118   if (this.node && (document.activeElement != this.node)) {
119     return;
120   }
121   goog.base(this, 'speak', str, opt_triggeredByUser, opt_personality);
124 /** @override */
125 cvox.ChromeVoxEditableElement.prototype.moveCursorToNextCharacter = function() {
126   var node = this.node;
127   node.selectionEnd++;
128   node.selectionStart = node.selectionEnd;
129   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
130   return true;
134 /** @override */
135 cvox.ChromeVoxEditableElement.prototype.moveCursorToPreviousCharacter =
136     function() {
137   var node = this.node;
138   node.selectionStart--;
139   node.selectionEnd = node.selectionStart;
140   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
141   return true;
145 /** @override */
146 cvox.ChromeVoxEditableElement.prototype.moveCursorToNextWord = function() {
147   var node = this.node;
148   var length = node.value.length;
149   var re = /\W+/gm;
150   var substring = node.value.substring(node.selectionEnd);
151   var match = re.exec(substring);
152   if (match !== null && match.index == 0) {
153     // Ignore word-breaking sequences right next to the cursor.
154     match = re.exec(substring);
155   }
156   var index = (match === null) ? length : match.index + node.selectionEnd;
157   node.selectionStart = node.selectionEnd = index;
158   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
159   return true;
163 /** @override */
164 cvox.ChromeVoxEditableElement.prototype.moveCursorToPreviousWord = function() {
165   var node = this.node;
166   var length = node.value.length;
167   var re = /\W+/gm;
168   var substring = node.value.substring(0, node.selectionStart);
169   var index = 0;
170   while (re.exec(substring) !== null) {
171     if (re.lastIndex < node.selectionStart) {
172       index = re.lastIndex;
173     }
174   }
175   node.selectionStart = node.selectionEnd = index;
176   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
177   return true;
181 /** @override */
182 cvox.ChromeVoxEditableElement.prototype.moveCursorToNextParagraph =
183     function() {
184   var node = this.node;
185   var length = node.value.length;
186   var index = node.selectionEnd >= length ? length :
187       node.value.indexOf('\n', node.selectionEnd);
188   if (index < 0) {
189     index = length;
190   }
191   node.selectionStart = node.selectionEnd = index + 1;
192   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
193   return true;
197 /** @override */
198 cvox.ChromeVoxEditableElement.prototype.moveCursorToPreviousParagraph =
199     function() {
200   var node = this.node;
201   var index = node.selectionStart <= 0 ? 0 :
202       node.value.lastIndexOf('\n', node.selectionStart - 2) + 1;
203   if (index < 0) {
204     index = 0;
205   }
206   node.selectionStart = node.selectionEnd = index;
207   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
208   return true;
212  * Shows the current line on the braille display.
213  * @private
214  */
215 cvox.ChromeVoxEditableElement.prototype.brailleCurrentLine_ = function() {
216   if (this.brailleHandler_) {
217     var lineIndex = this.getLineIndex(this.start);
218     var line = this.getLine(lineIndex);
219     // Collapsable whitespace inside the contenteditable is represented
220     // as non-breaking spaces.  This confuses braille input (which relies on
221     // the text being added to be the same as the text in the input field).
222     // Since the non-breaking spaces are just an artifact of how
223     // contenteditable is implemented, normalize to normal spaces instead.
224     if (this instanceof cvox.ChromeVoxEditableContentEditable) {
225       line = line.replace(/\u00A0/g, ' ');
226     }
227     var lineStart = this.getLineStart(lineIndex);
228     var start = this.start - lineStart;
229     var end = Math.min(this.end - lineStart, line.length);
230     this.brailleHandler_.changed(line, start, end, this.multiline, this.node,
231                                  lineStart);
232   }
235 /******************************************/
239  * A subclass of ChromeVoxEditableElement for an HTMLInputElement.
240  * @param {HTMLInputElement} node The HTMLInputElement node.
241  * @param {cvox.TtsInterface} tts A TTS object.
242  * @extends {cvox.ChromeVoxEditableElement}
243  * @implements {cvox.TextHandlerInterface}
244  * @constructor
245  */
246 cvox.ChromeVoxEditableHTMLInput = function(node, tts) {
247   this.node = node;
248   this.setup();
249   goog.base(this,
250             node,
251             node.value,
252             node.selectionStart,
253             node.selectionEnd,
254             node.type === 'password',
255             tts);
257 goog.inherits(cvox.ChromeVoxEditableHTMLInput,
258     cvox.ChromeVoxEditableElement);
262  * Performs setup for this input node.
263  * This accounts for exception-throwing behavior introduced by crbug.com/324360.
264  * @override
265  */
266 cvox.ChromeVoxEditableHTMLInput.prototype.setup = function() {
267   if (!this.node) {
268     return;
269   }
270   if (!cvox.DomUtil.doesInputSupportSelection(this.node)) {
271     this.originalType = this.node.type;
272     this.node.type = 'text';
273   }
278  * Performs teardown for this input node.
279  * This accounts for exception-throwing behavior introduced by crbug.com/324360.
280  * @override
281  */
282 cvox.ChromeVoxEditableHTMLInput.prototype.teardown = function() {
283   if (this.node && this.originalType) {
284     this.node.type = this.originalType;
285   }
290  * Update the state of the text and selection and describe any changes as
291  * appropriate.
293  * @param {boolean} triggeredByUser True if this was triggered by a user action.
294  */
295 cvox.ChromeVoxEditableHTMLInput.prototype.update = function(triggeredByUser) {
296   var newValue = this.node.value;
297   var textChangeEvent = new cvox.TextChangeEvent(newValue,
298                                                  this.node.selectionStart,
299                                                  this.node.selectionEnd,
300                                                  triggeredByUser);
301   this.changed(textChangeEvent);
305 /******************************************/
309  * A subclass of ChromeVoxEditableElement for an HTMLTextAreaElement.
310  * @param {HTMLTextAreaElement} node The HTMLTextAreaElement node.
311  * @param {cvox.TtsInterface} tts A TTS object.
312  * @extends {cvox.ChromeVoxEditableElement}
313  * @implements {cvox.TextHandlerInterface}
314  * @constructor
315  */
316 cvox.ChromeVoxEditableTextArea = function(node, tts) {
317   goog.base(this, node, node.value, node.selectionStart, node.selectionEnd,
318       false /* isPassword */, tts);
319   this.multiline = true;
321   /**
322    * True if the shadow is up-to-date with the current value of this text area.
323    * @type {boolean}
324    * @private
325    */
326   this.shadowIsCurrent_ = false;
328 goog.inherits(cvox.ChromeVoxEditableTextArea,
329     cvox.ChromeVoxEditableElement);
333  * An offscreen div used to compute the line numbers. A single div is
334  * shared by all instances of the class.
335  * @type {!cvox.EditableTextAreaShadow|undefined}
336  * @private
337  */
338 cvox.ChromeVoxEditableTextArea.shadow_;
342  * Update the state of the text and selection and describe any changes as
343  * appropriate.
345  * @param {boolean} triggeredByUser True if this was triggered by a user action.
346  */
347 cvox.ChromeVoxEditableTextArea.prototype.update = function(triggeredByUser) {
348   if (this.node.value != this.value) {
349     this.shadowIsCurrent_ = false;
350   }
351   var textChangeEvent = new cvox.TextChangeEvent(this.node.value,
352       this.node.selectionStart, this.node.selectionEnd, triggeredByUser);
353   this.changed(textChangeEvent);
358  * Get the line number corresponding to a particular index.
359  * @param {number} index The 0-based character index.
360  * @return {number} The 0-based line number corresponding to that character.
361  */
362 cvox.ChromeVoxEditableTextArea.prototype.getLineIndex = function(index) {
363   return this.getShadow().getLineIndex(index);
368  * Get the start character index of a line.
369  * @param {number} index The 0-based line index.
370  * @return {number} The 0-based index of the first character in this line.
371  */
372 cvox.ChromeVoxEditableTextArea.prototype.getLineStart = function(index) {
373   return this.getShadow().getLineStart(index);
378  * Get the end character index of a line.
379  * @param {number} index The 0-based line index.
380  * @return {number} The 0-based index of the end of this line.
381  */
382 cvox.ChromeVoxEditableTextArea.prototype.getLineEnd = function(index) {
383   return this.getShadow().getLineEnd(index);
388  * Update the shadow object, an offscreen div used to compute line numbers.
389  * @return {!cvox.EditableTextAreaShadow} The shadow object.
390  */
391 cvox.ChromeVoxEditableTextArea.prototype.getShadow = function() {
392   var shadow = cvox.ChromeVoxEditableTextArea.shadow_;
393   if (!shadow) {
394     shadow = cvox.ChromeVoxEditableTextArea.shadow_ =
395         new cvox.EditableTextAreaShadow();
396   }
397   if (!this.shadowIsCurrent_) {
398     shadow.update(this.node);
399     this.shadowIsCurrent_ = true;
400   }
401   return shadow;
405 /** @override */
406 cvox.ChromeVoxEditableTextArea.prototype.moveCursorToNextLine = function() {
407   var node = this.node;
408   var length = node.value.length;
409   if (node.selectionEnd >= length) {
410     return false;
411   }
412   var shadow = this.getShadow();
413   var lineIndex = shadow.getLineIndex(node.selectionEnd);
414   var lineStart = shadow.getLineStart(lineIndex);
415   var offset = node.selectionEnd - lineStart;
416   var lastLine = (length == 0) ? 0 : shadow.getLineIndex(length - 1);
417   var newCursorPosition = (lineIndex >= lastLine) ? length :
418       Math.min(shadow.getLineStart(lineIndex + 1) + offset,
419           shadow.getLineEnd(lineIndex + 1));
420   node.selectionStart = node.selectionEnd = newCursorPosition;
421   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
422   return true;
426 /** @override */
427 cvox.ChromeVoxEditableTextArea.prototype.moveCursorToPreviousLine = function() {
428   var node = this.node;
429   if (node.selectionStart <= 0) {
430     return false;
431   }
432   var shadow = this.getShadow();
433   var lineIndex = shadow.getLineIndex(node.selectionStart);
434   var lineStart = shadow.getLineStart(lineIndex);
435   var offset = node.selectionStart - lineStart;
436   var newCursorPosition = (lineIndex <= 0) ? 0 :
437       Math.min(shadow.getLineStart(lineIndex - 1) + offset,
438           shadow.getLineEnd(lineIndex - 1));
439   node.selectionStart = node.selectionEnd = newCursorPosition;
440   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
441   return true;
445 /******************************************/
449  * A subclass of ChromeVoxEditableElement for elements that are contentEditable.
450  * This is also used for a region of HTML with the ARIA role of "textbox",
451  * so that an author can create a pure-JavaScript editable text object - this
452  * will work the same as contentEditable as long as the DOM selection is
453  * updated properly within the textbox when it has focus.
454  * @param {Element} node The root contentEditable node.
455  * @param {cvox.TtsInterface} tts A TTS object.
456  * @extends {cvox.ChromeVoxEditableElement}
457  * @implements {cvox.TextHandlerInterface}
458  * @constructor
459  */
460 cvox.ChromeVoxEditableContentEditable = function(node, tts) {
461   goog.base(this, node, '', 0, 0, false /* isPassword */, tts);
464   /**
465    * True if the ContentEditableExtractor is current with this field's data.
466    * @type {boolean}
467    * @private
468    */
469   this.extractorIsCurrent_ = false;
471   var extractor = this.getExtractor();
472   this.value = extractor.getText();
473   this.start = extractor.getStartIndex();
474   this.end = extractor.getEndIndex();
475   this.multiline = true;
477 goog.inherits(cvox.ChromeVoxEditableContentEditable,
478     cvox.ChromeVoxEditableElement);
481  * A helper used to compute the line numbers. A single object is
482  * shared by all instances of the class.
483  * @type {!cvox.ContentEditableExtractor|undefined}
484  * @private
485  */
486 cvox.ChromeVoxEditableContentEditable.extractor_;
490  * Update the state of the text and selection and describe any changes as
491  * appropriate.
493  * @param {boolean} triggeredByUser True if this was triggered by a user action.
494  */
495 cvox.ChromeVoxEditableContentEditable.prototype.update =
496     function(triggeredByUser) {
497   this.extractorIsCurrent_ = false;
498   var textChangeEvent = new cvox.TextChangeEvent(
499       this.getExtractor().getText(),
500       this.getExtractor().getStartIndex(),
501       this.getExtractor().getEndIndex(),
502       triggeredByUser);
503   this.changed(textChangeEvent);
508  * Get the line number corresponding to a particular index.
509  * @param {number} index The 0-based character index.
510  * @return {number} The 0-based line number corresponding to that character.
511  */
512 cvox.ChromeVoxEditableContentEditable.prototype.getLineIndex = function(index) {
513   return this.getExtractor().getLineIndex(index);
518  * Get the start character index of a line.
519  * @param {number} index The 0-based line index.
520  * @return {number} The 0-based index of the first character in this line.
521  */
522 cvox.ChromeVoxEditableContentEditable.prototype.getLineStart = function(index) {
523   return this.getExtractor().getLineStart(index);
528  * Get the end character index of a line.
529  * @param {number} index The 0-based line index.
530  * @return {number} The 0-based index of the end of this line.
531  */
532 cvox.ChromeVoxEditableContentEditable.prototype.getLineEnd = function(index) {
533   return this.getExtractor().getLineEnd(index);
538  * Update the extractor object, an offscreen div used to compute line numbers.
539  * @return {!cvox.ContentEditableExtractor} The extractor object.
540  */
541 cvox.ChromeVoxEditableContentEditable.prototype.getExtractor = function() {
542   var extractor = cvox.ChromeVoxEditableContentEditable.extractor_;
543   if (!extractor) {
544     extractor = cvox.ChromeVoxEditableContentEditable.extractor_ =
545         new cvox.ContentEditableExtractor();
546   }
547   if (!this.extractorIsCurrent_) {
548     extractor.update(this.node);
549     this.extractorIsCurrent_ = true;
550   }
551   return extractor;
555 /** @override */
556 cvox.ChromeVoxEditableContentEditable.prototype.changed =
557     function(evt) {
558   if (!evt.triggeredByUser) {
559     return;
560   }
561   // Take over here if we can't describe a change; assume it's a blank line.
562   if (!this.shouldDescribeChange(evt)) {
563     this.speak(cvox.ChromeVox.msgs.getMsg('text_box_blank'), true);
564     if (this.brailleHandler_) {
565       this.brailleHandler_.changed('' /*line*/, 0 /*start*/, 0 /*end*/,
566                                    true /*multiline*/, null /*element*/,
567                                    evt.start /*lineStart*/);
568     }
569   } else {
570     goog.base(this, 'changed', evt);
571   }
575 /** @override */
576 cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToNextCharacter =
577     function() {
578   window.getSelection().modify('move', 'forward', 'character');
579   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
580   return true;
584 /** @override */
585 cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToPreviousCharacter =
586     function() {
587   window.getSelection().modify('move', 'backward', 'character');
588   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
589   return true;
593 /** @override */
594 cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToNextParagraph =
595     function() {
596   window.getSelection().modify('move', 'forward', 'paragraph');
597   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
598   return true;
601 /** @override */
602 cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToPreviousParagraph =
603     function() {
604   window.getSelection().modify('move', 'backward', 'paragraph');
605   cvox.ChromeVoxEventWatcher.handleTextChanged(true);
606   return true;
611  * @override
612  */
613 cvox.ChromeVoxEditableContentEditable.prototype.shouldDescribeChange =
614     function(evt) {
615   var sel = window.getSelection();
616   var cursor = new cvox.Cursor(sel.baseNode, sel.baseOffset, '');
618   // This is a very specific work around because of our buggy content editable
619   // support. Blank new lines are not captured in the line indexing data
620   // structures.
621   // Scenario: given a piece of text like:
622   //
623   // Some Title
624   //
625   // Description
626   // Footer
627   //
628   // The new lines after Title are not traversed to by TraverseUtil. A root fix
629   // would make changes there. However, considering the fickle nature of that
630   // code, we specifically detect for new lines here.
631   if (Math.abs(this.start - evt.start) != 1 &&
632       this.start == this.end &&
633       evt.start == evt.end &&
634       sel.baseNode == sel.extentNode &&
635       sel.baseOffset == sel.extentOffset &&
636       sel.baseNode.nodeType == Node.ELEMENT_NODE &&
637       sel.baseNode.querySelector('BR') &&
638       cvox.TraverseUtil.forwardsChar(cursor, [], [])) {
639     // This case detects if the range selection surrounds a new line,
640     // but there is still content after the new line (like the example
641     // above after "Title"). In these cases, we "pretend" we're the
642     // last character so we speak "blank".
643     return false;
644   }
646   // Otherwise, we should never speak "blank" no matter what (even if
647   // we're at the end of a content editable).
648   return true;