| 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 |