Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(118)

Side by Side Diff: chrome/browser/resources/chromeos/chromevox/common/editable_text.js

Issue 889593002: Refactorings to reduce dependencies in ChromeVox 2. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Readd missing dep and add a new one that should've been there already. Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 goog.provide('cvox.ChromeVoxEditableContentEditable'); 5 goog.provide('cvox.ChromeVoxEditableContentEditable');
6 goog.provide('cvox.ChromeVoxEditableElement');
6 goog.provide('cvox.ChromeVoxEditableHTMLInput'); 7 goog.provide('cvox.ChromeVoxEditableHTMLInput');
7 goog.provide('cvox.ChromeVoxEditableTextArea'); 8 goog.provide('cvox.ChromeVoxEditableTextArea');
8 goog.provide('cvox.ChromeVoxEditableTextBase');
9 goog.provide('cvox.TextChangeEvent');
10 goog.provide('cvox.TextHandlerInterface'); 9 goog.provide('cvox.TextHandlerInterface');
11 goog.provide('cvox.TypingEcho');
12 10
13 11
14 goog.require('cvox.BrailleTextHandler'); 12 goog.require('cvox.BrailleTextHandler');
13 goog.require('cvox.ChromeVoxEditableTextBase');
15 goog.require('cvox.ContentEditableExtractor'); 14 goog.require('cvox.ContentEditableExtractor');
16 goog.require('cvox.DomUtil'); 15 goog.require('cvox.DomUtil');
17 goog.require('cvox.EditableTextAreaShadow'); 16 goog.require('cvox.EditableTextAreaShadow');
17 goog.require('cvox.TextChangeEvent');
18 goog.require('cvox.TtsInterface'); 18 goog.require('cvox.TtsInterface');
19 goog.require('goog.i18n.MessageFormat');
20 19
21 /** 20 /**
22 * @fileoverview Gives the user spoken feedback as they type, select text, 21 * @fileoverview Gives the user spoken and braille feedback as they type,
23 * and move the cursor in editable text controls, including multiline 22 * select text, and move the cursor in editable HTML text controls, including
24 * controls. 23 * multiline controls and contenteditable regions.
25 * 24 *
26 * The majority of the code is in ChromeVoxEditableTextBase, a generalized 25 * The two subclasses, ChromeVoxEditableHTMLInput and
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 26 * ChromeVoxEditableTextArea, take a HTML input (type=text) or HTML
34 * textarea node (respectively) in the constructor, and automatically 27 * textarea node (respectively) in the constructor, and automatically
35 * handle retrieving the current state of the control, including 28 * handle retrieving the current state of the control, including
36 * computing line break information for a textarea using an offscreen 29 * computing line break information for a textarea using an offscreen
37 * shadow object. It is still the responsibility of the user of this 30 * shadow object. It is the responsibility of the user of these classes to
38 * class to trap key and focus events and call this class's update 31 * trap key and focus events and call the update method as needed.
39 * method.
40 * 32 *
41 */ 33 */
42 34
43 35
44 /** 36 /**
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 */
54 cvox.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 */
79 cvox.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 */
92 cvox.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 */
102 cvox.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. 37 * An interface for being notified when the text changes.
110 * @interface 38 * @interface
111 */ 39 */
112 cvox.TextHandlerInterface = function() {}; 40 cvox.TextHandlerInterface = function() {};
113 41
114 42
115 /** 43 /**
116 * Called when text changes. 44 * Called when text changes.
117 * @param {cvox.TextChangeEvent} evt The text change event. 45 * @param {cvox.TextChangeEvent} evt The text change event.
118 */ 46 */
119 cvox.TextHandlerInterface.prototype.changed = function(evt) {}; 47 cvox.TextHandlerInterface.prototype.changed = function(evt) {};
120 48
121 49
122 /** 50 /**
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 */
131 cvox.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 */
187 cvox.ChromeVoxEditableTextBase.prototype.setup = function() {};
188
189
190 /**
191 * Performs teardown for this element.
192 */
193 cvox.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 */
213 cvox.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 */
222 cvox.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 */
233 cvox.ChromeVoxEditableTextBase.prototype.maxShortPhraseLen = 60;
234
235
236 /**
237 * Whether or not the text control is a password.
238 *
239 * @type {boolean}
240 */
241 cvox.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 */
251 cvox.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 */
260 cvox.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 */
271 cvox.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 */
282 cvox.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 */
292 cvox.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 */
303 cvox.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 */
313 cvox.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 */
323 cvox.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 */
340 cvox.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.QueueMode.QUEUE;
348 if (opt_triggeredByUser === true) {
349 queueMode = cvox.QueueMode.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 */
361 cvox.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 this.brailleCurrentLine_();
379 };
380
381
382 /**
383 * Shows the current line on the braille display.
384 * @private
385 */
386 cvox.ChromeVoxEditableTextBase.prototype.brailleCurrentLine_ = function() {
387 if (this.brailleHandler_) {
388 var lineIndex = this.getLineIndex(this.start);
389 var line = this.getLine(lineIndex);
390 // Collapsable whitespace inside the contenteditable is represented
391 // as non-breaking spaces. This confuses braille input (which relies on
392 // the text being added to be the same as the text in the input field).
393 // Since the non-breaking spaces are just an artifact of how
394 // contenteditable is implemented, normalize to normal spaces instead.
395 if (this instanceof cvox.ChromeVoxEditableContentEditable) {
396 line = line.replace(/\u00A0/g, ' ');
397 }
398 var lineStart = this.getLineStart(lineIndex);
399 var start = this.start - lineStart;
400 var end = Math.min(this.end - lineStart, line.length);
401 this.brailleHandler_.changed(line, start, end, this.multiline, this.node,
402 lineStart);
403 }
404 };
405
406 /**
407 * Describe a change in the selection or cursor position when the text
408 * stays the same.
409 * @param {cvox.TextChangeEvent} evt The text change event.
410 */
411 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged =
412 function(evt) {
413 // TODO(deboer): Factor this into two function:
414 // - one to determine the selection event
415 // - one to speak
416
417 if (this.isPassword) {
418 this.speak((new goog.i18n.MessageFormat(cvox.ChromeVox.msgs.getMsg('dot'))
419 .format({'COUNT': 1})), evt.triggeredByUser);
420 return;
421 }
422 if (evt.start == evt.end) {
423 // It's currently a cursor.
424 if (this.start != this.end) {
425 // It was previously a selection, so just announce 'unselected'.
426 this.speak(cvox.ChromeVox.msgs.getMsg('Unselected'), evt.triggeredByUser);
427 } else if (this.getLineIndex(this.start) !=
428 this.getLineIndex(evt.start)) {
429 // Moved to a different line; read it.
430 var lineValue = this.getLine(this.getLineIndex(evt.start));
431 if (lineValue == '') {
432 lineValue = cvox.ChromeVox.msgs.getMsg('text_box_blank');
433 } else if (/^\s+$/.test(lineValue)) {
434 lineValue = cvox.ChromeVox.msgs.getMsg('text_box_whitespace');
435 }
436 this.speak(lineValue, evt.triggeredByUser);
437 } else if (this.start == evt.start + 1 ||
438 this.start == evt.start - 1) {
439 // Moved by one character; read it.
440 if (!cvox.ChromeVoxEditableTextBase.useIBeamCursor) {
441 if (evt.start == this.value.length) {
442 if (cvox.ChromeVox.verbosity == cvox.VERBOSITY_VERBOSE) {
443 this.speak(cvox.ChromeVox.msgs.getMsg('end_of_text_verbose'),
444 evt.triggeredByUser);
445 } else {
446 this.speak(cvox.ChromeVox.msgs.getMsg('end_of_text_brief'),
447 evt.triggeredByUser);
448 }
449 } else {
450 this.speak(this.value.substr(evt.start, 1),
451 evt.triggeredByUser,
452 {'phoneticCharacters': evt.triggeredByUser});
453 }
454 } else {
455 this.speak(this.value.substr(Math.min(this.start, evt.start), 1),
456 evt.triggeredByUser,
457 {'phoneticCharacters': evt.triggeredByUser});
458 }
459 } else {
460 // Moved by more than one character. Read all characters crossed.
461 this.speak(this.value.substr(Math.min(this.start, evt.start),
462 Math.abs(this.start - evt.start)), evt.triggeredByUser);
463 }
464 } else {
465 // It's currently a selection.
466 if (this.start + 1 == evt.start &&
467 this.end == this.value.length &&
468 evt.end == this.value.length) {
469 // Autocomplete: the user typed one character of autocompleted text.
470 this.speak(this.value.substr(this.start, 1), evt.triggeredByUser);
471 this.speak(this.value.substr(evt.start));
472 } else if (this.start == this.end) {
473 // It was previously a cursor.
474 this.speak(this.value.substr(evt.start, evt.end - evt.start),
475 evt.triggeredByUser);
476 this.speak(cvox.ChromeVox.msgs.getMsg('selected'));
477 } else if (this.start == evt.start && this.end < evt.end) {
478 this.speak(this.value.substr(this.end, evt.end - this.end),
479 evt.triggeredByUser);
480 this.speak(cvox.ChromeVox.msgs.getMsg('added_to_selection'));
481 } else if (this.start == evt.start && this.end > evt.end) {
482 this.speak(this.value.substr(evt.end, this.end - evt.end),
483 evt.triggeredByUser);
484 this.speak(cvox.ChromeVox.msgs.getMsg('removed_from_selection'));
485 } else if (this.end == evt.end && this.start > evt.start) {
486 this.speak(this.value.substr(evt.start, this.start - evt.start),
487 evt.triggeredByUser);
488 this.speak(cvox.ChromeVox.msgs.getMsg('added_to_selection'));
489 } else if (this.end == evt.end && this.start < evt.start) {
490 this.speak(this.value.substr(this.start, evt.start - this.start),
491 evt.triggeredByUser);
492 this.speak(cvox.ChromeVox.msgs.getMsg('removed_from_selection'));
493 } else {
494 // The selection changed but it wasn't an obvious extension of
495 // a previous selection. Just read the new selection.
496 this.speak(this.value.substr(evt.start, evt.end - evt.start),
497 evt.triggeredByUser);
498 this.speak(cvox.ChromeVox.msgs.getMsg('selected'));
499 }
500 }
501 };
502
503
504 /**
505 * Describe a change where the text changes.
506 * @param {cvox.TextChangeEvent} evt The text change event.
507 */
508 cvox.ChromeVoxEditableTextBase.prototype.describeTextChanged = function(evt) {
509 var personality = {};
510 if (evt.value.length < this.value.length) {
511 personality = cvox.AbstractTts.PERSONALITY_DELETED;
512 }
513 if (this.isPassword) {
514 this.speak((new goog.i18n.MessageFormat(cvox.ChromeVox.msgs.getMsg('dot'))
515 .format({'COUNT': 1})), evt.triggeredByUser, personality);
516 return;
517 }
518
519 var value = this.value;
520 var len = value.length;
521 var newLen = evt.value.length;
522 var autocompleteSuffix = '';
523 // Make a copy of evtValue and evtEnd to avoid changing anything in
524 // the event itself.
525 var evtValue = evt.value;
526 var evtEnd = evt.end;
527
528 // First, see if there's a selection at the end that might have been
529 // added by autocomplete. If so, strip it off into a separate variable.
530 if (evt.start < evtEnd && evtEnd == newLen) {
531 autocompleteSuffix = evtValue.substr(evt.start);
532 evtValue = evtValue.substr(0, evt.start);
533 evtEnd = evt.start;
534 }
535
536 // Now see if the previous selection (if any) was deleted
537 // and any new text was inserted at that character position.
538 // This would handle pasting and entering text by typing, both from
539 // a cursor and from a selection.
540 var prefixLen = this.start;
541 var suffixLen = len - this.end;
542 if (newLen >= prefixLen + suffixLen + (evtEnd - evt.start) &&
543 evtValue.substr(0, prefixLen) == value.substr(0, prefixLen) &&
544 evtValue.substr(newLen - suffixLen) == value.substr(this.end)) {
545 // However, in a dynamic content editable, defer to authoritative events
546 // (clipboard, key press) to reduce guess work when observing insertions.
547 // Only use this logic when observing deletions (and insertion of word
548 // breakers).
549 // TODO(dtseng): Think about a more reliable way to do this.
550 if (!(this instanceof cvox.ChromeVoxEditableContentEditable) ||
551 newLen < len ||
552 this.isWordBreakChar(evt.value[newLen - 1] || '')) {
553 this.describeTextChangedHelper(
554 evt, prefixLen, suffixLen, autocompleteSuffix, personality);
555 }
556 return;
557 }
558
559 // Next, see if one or more characters were deleted from the previous
560 // cursor position and the new cursor is in the expected place. This
561 // handles backspace, forward-delete, and similar shortcuts that delete
562 // a word or line.
563 prefixLen = evt.start;
564 suffixLen = newLen - evtEnd;
565 if (this.start == this.end &&
566 evt.start == evtEnd &&
567 evtValue.substr(0, prefixLen) == value.substr(0, prefixLen) &&
568 evtValue.substr(newLen - suffixLen) ==
569 value.substr(len - suffixLen)) {
570 this.describeTextChangedHelper(
571 evt, prefixLen, suffixLen, autocompleteSuffix, personality);
572 return;
573 }
574
575 // If all else fails, we assume the change was not the result of a normal
576 // user editing operation, so we'll have to speak feedback based only
577 // on the changes to the text, not the cursor position / selection.
578 // First, restore the autocomplete text if any.
579 evtValue += autocompleteSuffix;
580
581 // Try to do a diff between the new and the old text. If it is a one character
582 // insertion/deletion at the start or at the end, just speak that character.
583 if ((evtValue.length == (value.length + 1)) ||
584 ((evtValue.length + 1) == value.length)) {
585 // The user added text either to the beginning or the end.
586 if (evtValue.length > value.length) {
587 if (evtValue.indexOf(value) == 0) {
588 this.speak(evtValue[evtValue.length - 1], evt.triggeredByUser,
589 personality);
590 return;
591 } else if (evtValue.indexOf(value) == 1) {
592 this.speak(evtValue[0], evt.triggeredByUser, personality);
593 return;
594 }
595 }
596 // The user deleted text either from the beginning or the end.
597 if (evtValue.length < value.length) {
598 if (value.indexOf(evtValue) == 0) {
599 this.speak(value[value.length - 1], evt.triggeredByUser, personality);
600 return;
601 } else if (value.indexOf(evtValue) == 1) {
602 this.speak(value[0], evt.triggeredByUser, personality);
603 return;
604 }
605 }
606 }
607
608 if (this.multiline) {
609 // Fall back to announce deleted but omit the text that was deleted.
610 if (evt.value.length < this.value.length) {
611 this.speak(cvox.ChromeVox.msgs.getMsg('text_deleted'),
612 evt.triggeredByUser, personality);
613 }
614 // The below is a somewhat loose way to deal with non-standard
615 // insertions/deletions. Intentionally skip for multiline since deletion
616 // announcements are covered above and insertions are non-standard (possibly
617 // due to auto complete). Since content editable's often refresh content by
618 // removing and inserting entire chunks of text, this type of logic often
619 // results in unintended consequences such as reading all text when only one
620 // character has been entered.
621 return;
622 }
623
624 // If the text is short, just speak the whole thing.
625 if (newLen <= this.maxShortPhraseLen) {
626 this.describeTextChangedHelper(evt, 0, 0, '', personality);
627 return;
628 }
629
630 // Otherwise, look for the common prefix and suffix, but back up so
631 // that we can speak complete words, to be minimally confusing.
632 prefixLen = 0;
633 while (prefixLen < len &&
634 prefixLen < newLen &&
635 value[prefixLen] == evtValue[prefixLen]) {
636 prefixLen++;
637 }
638 while (prefixLen > 0 && !this.isWordBreakChar(value[prefixLen - 1])) {
639 prefixLen--;
640 }
641
642 suffixLen = 0;
643 while (suffixLen < (len - prefixLen) &&
644 suffixLen < (newLen - prefixLen) &&
645 value[len - suffixLen - 1] == evtValue[newLen - suffixLen - 1]) {
646 suffixLen++;
647 }
648 while (suffixLen > 0 && !this.isWordBreakChar(value[len - suffixLen])) {
649 suffixLen--;
650 }
651
652 this.describeTextChangedHelper(evt, prefixLen, suffixLen, '', personality);
653 };
654
655
656 /**
657 * The function called by describeTextChanged after it's figured out
658 * what text was deleted, what text was inserted, and what additional
659 * autocomplete text was added.
660 * @param {cvox.TextChangeEvent} evt The text change event.
661 * @param {number} prefixLen The number of characters in the common prefix
662 * of this.value and newValue.
663 * @param {number} suffixLen The number of characters in the common suffix
664 * of this.value and newValue.
665 * @param {string} autocompleteSuffix The autocomplete string that was added
666 * to the end, if any. It should be spoken at the end of the utterance
667 * describing the change.
668 * @param {Object=} opt_personality Personality to speak the text.
669 */
670 cvox.ChromeVoxEditableTextBase.prototype.describeTextChangedHelper = function(
671 evt, prefixLen, suffixLen, autocompleteSuffix, opt_personality) {
672 var len = this.value.length;
673 var newLen = evt.value.length;
674 var deletedLen = len - prefixLen - suffixLen;
675 var deleted = this.value.substr(prefixLen, deletedLen);
676 var insertedLen = newLen - prefixLen - suffixLen;
677 var inserted = evt.value.substr(prefixLen, insertedLen);
678 var utterance = '';
679 var triggeredByUser = evt.triggeredByUser;
680
681 if (insertedLen > 1) {
682 utterance = inserted;
683 } else if (insertedLen == 1) {
684 if ((cvox.ChromeVox.typingEcho == cvox.TypingEcho.WORD ||
685 cvox.ChromeVox.typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD) &&
686 this.isWordBreakChar(inserted) &&
687 prefixLen > 0 &&
688 !this.isWordBreakChar(evt.value.substr(prefixLen - 1, 1))) {
689 // Speak previous word.
690 var index = prefixLen;
691 while (index > 0 && !this.isWordBreakChar(evt.value[index - 1])) {
692 index--;
693 }
694 if (index < prefixLen) {
695 utterance = evt.value.substr(index, prefixLen + 1 - index);
696 } else {
697 utterance = inserted;
698 triggeredByUser = false; // Implies QUEUE_MODE_QUEUE.
699 }
700 } else if (cvox.ChromeVox.typingEcho == cvox.TypingEcho.CHARACTER ||
701 cvox.ChromeVox.typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD) {
702 // This particular case is handled in event watcher. See the key press
703 // handler for more details.
704 utterance = cvox.ChromeVoxEditableTextBase.eventTypingEcho ? '' :
705 inserted;
706 }
707 } else if (deletedLen > 1 && !autocompleteSuffix) {
708 utterance = deleted + ', deleted';
709 } else if (deletedLen == 1) {
710 utterance = deleted;
711 }
712
713 if (autocompleteSuffix && utterance) {
714 utterance += ', ' + autocompleteSuffix;
715 } else if (autocompleteSuffix) {
716 utterance = autocompleteSuffix;
717 }
718
719 if (utterance) {
720 this.speak(utterance, triggeredByUser, opt_personality);
721 }
722 };
723
724
725 /**
726 * Moves the cursor forward by one character.
727 * @return {boolean} True if the action was handled.
728 */
729 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextCharacter =
730 function() { return false; };
731
732
733 /**
734 * Moves the cursor backward by one character.
735 * @return {boolean} True if the action was handled.
736 */
737 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousCharacter =
738 function() { return false; };
739
740
741 /**
742 * Moves the cursor forward by one word.
743 * @return {boolean} True if the action was handled.
744 */
745 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextWord =
746 function() { return false; };
747
748
749 /**
750 * Moves the cursor backward by one word.
751 * @return {boolean} True if the action was handled.
752 */
753 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousWord =
754 function() { return false; };
755
756
757 /**
758 * Moves the cursor forward by one line.
759 * @return {boolean} True if the action was handled.
760 */
761 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextLine =
762 function() { return false; };
763
764
765 /**
766 * Moves the cursor backward by one line.
767 * @return {boolean} True if the action was handled.
768 */
769 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousLine =
770 function() { return false; };
771
772
773 /**
774 * Moves the cursor forward by one paragraph.
775 * @return {boolean} True if the action was handled.
776 */
777 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToNextParagraph =
778 function() { return false; };
779
780
781 /**
782 * Moves the cursor backward by one paragraph.
783 * @return {boolean} True if the action was handled.
784 */
785 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousParagraph =
786 function() { return false; };
787
788
789 /******************************************/
790
791
792 /**
793 * A subclass of ChromeVoxEditableTextBase a text element that's part of 51 * A subclass of ChromeVoxEditableTextBase a text element that's part of
794 * the webpage DOM. Contains common code shared by both EditableHTMLInput 52 * the webpage DOM. Contains common code shared by both EditableHTMLInput
795 * and EditableTextArea, but that might not apply to a non-DOM text box. 53 * and EditableTextArea, but that might not apply to a non-DOM text box.
796 * @param {Element} node A DOM node which allows text input. 54 * @param {Element} node A DOM node which allows text input.
797 * @param {string} value The string value of the editable text control. 55 * @param {string} value The string value of the editable text control.
798 * @param {number} start The 0-based start cursor/selection index. 56 * @param {number} start The 0-based start cursor/selection index.
799 * @param {number} end The 0-based end cursor/selection index. 57 * @param {number} end The 0-based end cursor/selection index.
800 * @param {boolean} isPassword Whether the text control if a password field. 58 * @param {boolean} isPassword Whether the text control if a password field.
801 * @param {cvox.TtsInterface} tts A TTS object. 59 * @param {cvox.TtsInterface} tts A TTS object.
802 * @extends {cvox.ChromeVoxEditableTextBase} 60 * @extends {cvox.ChromeVoxEditableTextBase}
803 * @constructor 61 * @constructor
804 */ 62 */
805 cvox.ChromeVoxEditableElement = function(node, value, start, end, isPassword, 63 cvox.ChromeVoxEditableElement = function(node, value, start, end, isPassword,
806 tts) { 64 tts) {
807 goog.base(this, value, start, end, isPassword, tts); 65 goog.base(this, value, start, end, isPassword, tts);
808 66
809 /** 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;
74
75 /**
810 * The DOM node which allows text input. 76 * The DOM node which allows text input.
811 * @type {Element} 77 * @type {Element}
812 * @protected 78 * @protected
813 */ 79 */
814 this.node = node; 80 this.node = node;
815 81
816 /** 82 /**
817 * True if the description was just spoken. 83 * True if the description was just spoken.
818 * @type {boolean} 84 * @type {boolean}
819 * @private 85 * @private
820 */ 86 */
821 this.justSpokeDescription_ = false; 87 this.justSpokeDescription_ = false;
822 }; 88 };
823 goog.inherits(cvox.ChromeVoxEditableElement, 89 goog.inherits(cvox.ChromeVoxEditableElement,
824 cvox.ChromeVoxEditableTextBase); 90 cvox.ChromeVoxEditableTextBase);
825 91
826 92
827 /** 93 /** @override */
828 * Update the state of the text and selection and describe any changes as
829 * appropriate.
830 *
831 * @param {cvox.TextChangeEvent} evt The text change event.
832 */
833 cvox.ChromeVoxEditableElement.prototype.changed = function(evt) { 94 cvox.ChromeVoxEditableElement.prototype.changed = function(evt) {
834 // Ignore changes to the cursor and selection if they happen immediately 95 // Ignore changes to the cursor and selection if they happen immediately
835 // after the description was just spoken. This avoid double-speaking when, 96 // after the description was just spoken. This avoid double-speaking when,
836 // for example, a text field is focused and then a moment later the 97 // for example, a text field is focused and then a moment later the
837 // contents are selected. If the value changes, though, this change will 98 // contents are selected. If the value changes, though, this change will
838 // not be ignored. 99 // not be ignored.
839 if (this.justSpokeDescription_ && this.value == evt.value) { 100 if (this.justSpokeDescription_ && this.value == evt.value) {
840 this.value = evt.value; 101 this.value = evt.value;
841 this.start = evt.start; 102 this.start = evt.start;
842 this.end = evt.end; 103 this.end = evt.end;
843 this.justSpokeDescription_ = false; 104 this.justSpokeDescription_ = false;
844 } 105 }
845 goog.base(this, 'changed', evt); 106 goog.base(this, 'changed', evt);
107 if (this.lastChangeDescribed) {
108 this.brailleCurrentLine_();
109 }
846 }; 110 };
847 111
848 112
849 /** @override */ 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);
122 };
123
124 /** @override */
850 cvox.ChromeVoxEditableElement.prototype.moveCursorToNextCharacter = function() { 125 cvox.ChromeVoxEditableElement.prototype.moveCursorToNextCharacter = function() {
851 var node = this.node; 126 var node = this.node;
852 node.selectionEnd++; 127 node.selectionEnd++;
853 node.selectionStart = node.selectionEnd; 128 node.selectionStart = node.selectionEnd;
854 cvox.ChromeVoxEventWatcher.handleTextChanged(true); 129 cvox.ChromeVoxEventWatcher.handleTextChanged(true);
855 return true; 130 return true;
856 }; 131 };
857 132
858 133
859 /** @override */ 134 /** @override */
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
926 var index = node.selectionStart <= 0 ? 0 : 201 var index = node.selectionStart <= 0 ? 0 :
927 node.value.lastIndexOf('\n', node.selectionStart - 2) + 1; 202 node.value.lastIndexOf('\n', node.selectionStart - 2) + 1;
928 if (index < 0) { 203 if (index < 0) {
929 index = 0; 204 index = 0;
930 } 205 }
931 node.selectionStart = node.selectionEnd = index; 206 node.selectionStart = node.selectionEnd = index;
932 cvox.ChromeVoxEventWatcher.handleTextChanged(true); 207 cvox.ChromeVoxEventWatcher.handleTextChanged(true);
933 return true; 208 return true;
934 }; 209 };
935 210
211 /**
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 }
233 };
936 234
937 /******************************************/ 235 /******************************************/
938 236
939 237
940 /** 238 /**
941 * A subclass of ChromeVoxEditableElement for an HTMLInputElement. 239 * A subclass of ChromeVoxEditableElement for an HTMLInputElement.
942 * @param {HTMLInputElement} node The HTMLInputElement node. 240 * @param {HTMLInputElement} node The HTMLInputElement node.
943 * @param {cvox.TtsInterface} tts A TTS object. 241 * @param {cvox.TtsInterface} tts A TTS object.
944 * @extends {cvox.ChromeVoxEditableElement} 242 * @extends {cvox.ChromeVoxEditableElement}
945 * @implements {cvox.TextHandlerInterface} 243 * @implements {cvox.TextHandlerInterface}
(...skipping 301 matching lines...) Expand 10 before | Expand all | Expand 10 after
1247 new cvox.ContentEditableExtractor(); 545 new cvox.ContentEditableExtractor();
1248 } 546 }
1249 if (!this.extractorIsCurrent_) { 547 if (!this.extractorIsCurrent_) {
1250 extractor.update(this.node); 548 extractor.update(this.node);
1251 this.extractorIsCurrent_ = true; 549 this.extractorIsCurrent_ = true;
1252 } 550 }
1253 return extractor; 551 return extractor;
1254 }; 552 };
1255 553
1256 554
1257 /** 555 /** @override */
1258 * @override
1259 */
1260 cvox.ChromeVoxEditableContentEditable.prototype.changed = 556 cvox.ChromeVoxEditableContentEditable.prototype.changed =
1261 function(evt) { 557 function(evt) {
1262 if (!evt.triggeredByUser) { 558 if (!evt.triggeredByUser) {
1263 return; 559 return;
1264 } 560 }
1265 // Take over here if we can't describe a change; assume it's a blank line. 561 // Take over here if we can't describe a change; assume it's a blank line.
1266 if (!this.shouldDescribeChange(evt)) { 562 if (!this.shouldDescribeChange(evt)) {
1267 this.speak(cvox.ChromeVox.msgs.getMsg('text_box_blank'), true); 563 this.speak(cvox.ChromeVox.msgs.getMsg('text_box_blank'), true);
1268 if (this.brailleHandler_) { 564 if (this.brailleHandler_) {
1269 this.brailleHandler_.changed('' /*line*/, 0 /*start*/, 0 /*end*/, 565 this.brailleHandler_.changed('' /*line*/, 0 /*start*/, 0 /*end*/,
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after
1344 // but there is still content after the new line (like the example 640 // but there is still content after the new line (like the example
1345 // above after "Title"). In these cases, we "pretend" we're the 641 // above after "Title"). In these cases, we "pretend" we're the
1346 // last character so we speak "blank". 642 // last character so we speak "blank".
1347 return false; 643 return false;
1348 } 644 }
1349 645
1350 // Otherwise, we should never speak "blank" no matter what (even if 646 // Otherwise, we should never speak "blank" no matter what (even if
1351 // we're at the end of a content editable). 647 // we're at the end of a content editable).
1352 return true; 648 return true;
1353 }; 649 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698