OLD | NEW |
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 Loading... |
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 Loading... |
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 Loading... |
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 }; |
OLD | NEW |