OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 goog.provide('cvox.ChromeVoxEditableHTMLInput'); |
| 6 goog.provide('cvox.ChromeVoxEditableTextArea'); |
| 7 goog.provide('cvox.ChromeVoxEditableTextBase'); |
| 8 |
| 9 /** |
| 10 * @fileoverview Gives the user spoken feedback as they type, select text, |
| 11 * and move the cursor in editable text controls, including multiline |
| 12 * controls. |
| 13 * |
| 14 * The majority of the code is in ChromeVoxEditableTextBase, a generalized |
| 15 * class that takes the current state in the form of a text string, a |
| 16 * cursor start location and a cursor end location, and calls a speak |
| 17 * method with the resulting text to be spoken. If the control is multiline, |
| 18 * information about line breaks (including automatic ones) is also needed. |
| 19 * |
| 20 * Two subclasses, ChromeVoxEditableHTMLInput and |
| 21 * ChromeVoxEditableTextArea, take a HTML input (type=text) or HTML |
| 22 * textarea node (respectively) in the constructor, and automatically |
| 23 * handle retrieving the current state of the control, including |
| 24 * computing line break information for a textarea using an offscreen |
| 25 * shadow object. It is still the responsibility of the user of this |
| 26 * class to trap key and focus events and call this class's update |
| 27 * method. |
| 28 */ |
| 29 |
| 30 /** |
| 31 * A class representing an abstracted editable text control. |
| 32 * @param {string} value The string value of the editable text control. |
| 33 * @param {number} start The 0-based start cursor/selection index. |
| 34 * @param {number} end The 0-based end cursor/selection index. |
| 35 * @param {Object} tts A TTS object implementing speak() and stop() methods. |
| 36 * @constructor |
| 37 */ |
| 38 cvox.ChromeVoxEditableTextBase = function(value, start, end, tts) { |
| 39 this.value = value; |
| 40 this.start = start; |
| 41 this.end = end; |
| 42 this.tts = tts; |
| 43 }; |
| 44 |
| 45 /** |
| 46 * Whether or not the text field is multiline. |
| 47 * @type {boolean} |
| 48 */ |
| 49 cvox.ChromeVoxEditableTextBase.prototype.multiline = false; |
| 50 |
| 51 /** |
| 52 * Whether or not moving the cursor from one character to another considers |
| 53 * the cursor to be a block (true) or an i-beam (false). |
| 54 * |
| 55 * If the cursor is a block, then the value of the character to the right |
| 56 * of the cursor index is always read when the cursor moves, no matter what |
| 57 * the previous cursor location was - this is how PC screenreaders work. |
| 58 * |
| 59 * If the cursor is an i-beam, moving the cursor by one character reads the |
| 60 * character that was crossed over, which may be the character to the left or |
| 61 * right of the new cursor index depending on the direction. |
| 62 * |
| 63 * @type {boolean} |
| 64 */ |
| 65 cvox.ChromeVoxEditableTextBase.prototype.cursorIsBlock = false; |
| 66 |
| 67 /** |
| 68 * The maximum number of characters that are short enough to speak in response |
| 69 * to an event. For example, if the user selects "Hello", we will speak |
| 70 * "Hello, selected", but if the user selects 1000 characters, we will speak |
| 71 * "text selected" instead. |
| 72 * @type {number} |
| 73 */ |
| 74 cvox.ChromeVoxEditableTextBase.prototype.maxShortPhraseLen = 60; |
| 75 |
| 76 /** |
| 77 * Describe the current state of the text control. |
| 78 */ |
| 79 cvox.ChromeVoxEditableTextBase.prototype.describe = function() { |
| 80 this.speak(this.getDescription()); |
| 81 }; |
| 82 |
| 83 /** |
| 84 * Get a speakable text string describing the current state of the |
| 85 * text control and its title and/or label. |
| 86 * @return {string} The speakable description. |
| 87 */ |
| 88 cvox.ChromeVoxEditableTextBase.prototype.getDescription = function() { |
| 89 var speech = ''; |
| 90 if (this.multiline) { |
| 91 speech += 'multiline editable text. '; |
| 92 if (this.start == this.end) { |
| 93 // It's a cursor: read the current line. |
| 94 var line = this.getLine(this.getLineIndex(this.start)); |
| 95 if (line) { |
| 96 speech += line; |
| 97 } else { |
| 98 speech += 'blank.'; |
| 99 } |
| 100 } |
| 101 } else { |
| 102 if (this.value.length <= this.maxShortPhraseLen) { |
| 103 speech += this.value + ', editable text.'; |
| 104 } else { |
| 105 speech += 'editable text.'; |
| 106 } |
| 107 } |
| 108 return speech; |
| 109 }; |
| 110 |
| 111 /** |
| 112 * Get the line number corresponding to a particular index. |
| 113 * Default implementation that can be overridden by subclasses. |
| 114 * @param {number} index The 0-based character index. |
| 115 * @return {number} The 0-based line number corresponding to that character. |
| 116 */ |
| 117 cvox.ChromeVoxEditableTextBase.prototype.getLineIndex = function(index) { |
| 118 return 0; |
| 119 }; |
| 120 |
| 121 /** |
| 122 * Get the start character index of a line. |
| 123 * Default implementation that can be overridden by subclasses. |
| 124 * @param {number} index The 0-based line index. |
| 125 * @return {number} The 0-based index of the first character in this line. |
| 126 */ |
| 127 cvox.ChromeVoxEditableTextBase.prototype.getLineStart = function(index) { |
| 128 return 0; |
| 129 }; |
| 130 |
| 131 /** |
| 132 * Get the end character index of a line. |
| 133 * Default implementation that can be overridden by subclasses. |
| 134 * @param {number} index The 0-based line index. |
| 135 * @return {number} The 0-based index of the end of this line. |
| 136 */ |
| 137 cvox.ChromeVoxEditableTextBase.prototype.getLineEnd = function(index) { |
| 138 return this.value.length; |
| 139 }; |
| 140 |
| 141 /** |
| 142 * Get the full text of the current line. |
| 143 * @param {number} index The 0-based line index. |
| 144 * @return {string} The text of the line. |
| 145 */ |
| 146 cvox.ChromeVoxEditableTextBase.prototype.getLine = function(index) { |
| 147 var lineStart = this.getLineStart(index); |
| 148 var lineEnd = this.getLineEnd(index); |
| 149 var line = this.value.substr(lineStart, lineEnd - lineStart); |
| 150 return line.replace(/^\s+|\s+$/g, ''); |
| 151 }; |
| 152 |
| 153 /** |
| 154 * @param {string} ch The character to test. |
| 155 * @return {boolean} True if a character is whitespace. |
| 156 */ |
| 157 cvox.ChromeVoxEditableTextBase.prototype.isWhitespaceChar = function(ch) { |
| 158 return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; |
| 159 }; |
| 160 |
| 161 /** |
| 162 * @param {string} ch The character to test. |
| 163 * @return {boolean} True if a character breaks a word, used to determine |
| 164 * if the previous word should be spoken. |
| 165 */ |
| 166 cvox.ChromeVoxEditableTextBase.prototype.isWordBreakChar = function(ch) { |
| 167 return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t' || |
| 168 ch == ',' || ch == '.' || ch == '/'; |
| 169 }; |
| 170 |
| 171 |
| 172 |
| 173 /** |
| 174 * Speak text, but if it's a single character, describe the character. |
| 175 * TODO(dmazzoni) make this more general, for use outside editable text. |
| 176 * @param {string} ch The character to speak. |
| 177 * @return {string} ch The character to speak. |
| 178 */ |
| 179 cvox.ChromeVoxEditableTextBase.prototype.describeChar = function(ch) { |
| 180 if (ch.length != 1) { |
| 181 return ch; |
| 182 } |
| 183 |
| 184 switch (ch) { |
| 185 case ' ': return 'space.'; |
| 186 case '`': return 'backtick.'; |
| 187 case '~': return 'tilde.'; |
| 188 case '!': return 'bang.'; |
| 189 case '@': return 'at.'; |
| 190 case '#': return 'pound.'; |
| 191 case '$': return 'dollar.'; |
| 192 case '%': return 'percent.'; |
| 193 case '^': return 'caret.'; |
| 194 case '&': return 'ampersand.'; |
| 195 case '*': return 'asterisk.'; |
| 196 case '(': return 'open paren.'; |
| 197 case ')': return 'close paren.'; |
| 198 case '-': return 'hyphen.'; |
| 199 case '_': return 'underscore.'; |
| 200 case '=': return 'equals.'; |
| 201 case '+': return 'plus.'; |
| 202 case '[': return 'left bracket.'; |
| 203 case ']': return 'right bracket.'; |
| 204 case '{': return 'left brace.'; |
| 205 case '}': return 'right brace.'; |
| 206 case '|': return 'pipe.'; |
| 207 case ';': return 'semicolon.'; |
| 208 case ':': return 'colon.'; |
| 209 case ',': return 'comma.'; |
| 210 case '.': return 'period.'; |
| 211 case '<': return 'less than.'; |
| 212 case '>': return 'greater than.'; |
| 213 case '/': return 'slash.'; |
| 214 case '?': return 'question mark.'; |
| 215 case '\t': return 'tab.'; |
| 216 case '\r': return 'return.'; |
| 217 case '\n': return 'return.'; |
| 218 case '\\': return 'backslash.'; |
| 219 default: |
| 220 return ch.toUpperCase() + '.'; |
| 221 break; |
| 222 } |
| 223 }; |
| 224 |
| 225 /** |
| 226 * Speak text, but if it's a single character, describe the character. |
| 227 * @param {string} str The string to speak. |
| 228 */ |
| 229 cvox.ChromeVoxEditableTextBase.prototype.speak = function(str) { |
| 230 if (str.length == 1) { |
| 231 this.tts.speak(this.describeChar(str), 0, {}); |
| 232 } else if (str.length > 1) { |
| 233 this.tts.speak(str, 0, {}); |
| 234 } |
| 235 }; |
| 236 |
| 237 /** |
| 238 * Return the state as an opaque object so that a client can restore it |
| 239 * to this state later without needing to know about its internal fields. |
| 240 * |
| 241 * @return {Object} The state as an opaque object. |
| 242 */ |
| 243 cvox.ChromeVoxEditableTextBase.prototype.saveState = function() { |
| 244 return { 'value': this.value, 'start': this.start, 'end': this.end }; |
| 245 }; |
| 246 |
| 247 /** |
| 248 * Restore the state that was previously saved using saveState, without |
| 249 * speaking any feedback. |
| 250 * |
| 251 * @param {Object} state A state returned by saveState. |
| 252 */ |
| 253 cvox.ChromeVoxEditableTextBase.prototype.restoreState = function(state) { |
| 254 this.value = state.value; |
| 255 this.start = state.start; |
| 256 this.end = state.end; |
| 257 }; |
| 258 |
| 259 /** |
| 260 * Check if the underlying text control has changed and an update is needed. |
| 261 * The default implementation always returns false, but subclasses that |
| 262 * track an INPUT or TEXTAREA element will return true if the underlying |
| 263 * element has changed. |
| 264 * |
| 265 * @return {boolean} True if the object needs to be updated. |
| 266 */ |
| 267 cvox.ChromeVoxEditableTextBase.prototype.needsUpdate = function() { |
| 268 return false; |
| 269 }; |
| 270 |
| 271 /** |
| 272 * Update the state of the text and selection and describe any changes as |
| 273 * appropriate. |
| 274 * @param {string} newValue The new string value of the editable text control. |
| 275 * @param {number} newStart The new 0-based start cursor/selection index. |
| 276 * @param {number} newEnd The new 0-based end cursor/selection index. |
| 277 */ |
| 278 cvox.ChromeVoxEditableTextBase.prototype.changed = function( |
| 279 newValue, newStart, newEnd) { |
| 280 if (newValue == this.value && newStart == this.start && newEnd == this.end) { |
| 281 return; |
| 282 } |
| 283 |
| 284 if (newValue == this.value) { |
| 285 this.describeSelectionChanged(newStart, newEnd); |
| 286 } else { |
| 287 this.describeTextChanged(newValue, newStart, newEnd); |
| 288 } |
| 289 |
| 290 this.value = newValue; |
| 291 this.start = newStart; |
| 292 this.end = newEnd; |
| 293 }; |
| 294 |
| 295 /** |
| 296 * Describe a change in the selection or cursor position when the text |
| 297 * stays the same. |
| 298 * @param {number} newStart The new 0-based start cursor/selection index. |
| 299 * @param {number} newEnd The new 0-based end cursor/selection index. |
| 300 */ |
| 301 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged = |
| 302 function(newStart, newEnd) { |
| 303 if (newStart == newEnd) { |
| 304 // It's currently a cursor. |
| 305 if (this.start != this.end) { |
| 306 // It was previously a selection, so just announce 'unselected'. |
| 307 this.speak('Unselected.'); |
| 308 } else if (this.getLineIndex(this.start) != this.getLineIndex(newStart)) { |
| 309 // Moved to a different line; read it. |
| 310 this.speak(this.getLine(this.getLineIndex(newStart))); |
| 311 } else if (this.start == newStart + 1 || this.start == newStart - 1) { |
| 312 // Moved by one character; read it. |
| 313 if (this.cursorIsBlock) { |
| 314 if (newStart == this.value.length) { |
| 315 this.speak('end'); |
| 316 } else { |
| 317 this.speak(this.value.substr(newStart, 1)); |
| 318 } |
| 319 } else { |
| 320 this.speak(this.value.substr(Math.min(this.start, newStart), 1)); |
| 321 } |
| 322 } else { |
| 323 // Moved by more than one character. Read all characters crossed. |
| 324 this.speak(this.value.substr(Math.min(this.start, newStart), |
| 325 Math.abs(this.start - newStart))); |
| 326 } |
| 327 } else { |
| 328 // It's currently a selection. |
| 329 if (this.start + 1 == newStart && |
| 330 this.end == this.value.length && |
| 331 newEnd == this.value.length) { |
| 332 // Autocomplete: the user typed one character of autocompleted text. |
| 333 this.speak(this.describeChar(this.value.substr(this.start, 1)) + |
| 334 ', ' + |
| 335 this.describeChar(this.value.substr(newStart))); |
| 336 } else if (this.start == this.end) { |
| 337 // It was previously a cursor. |
| 338 this.speak(this.describeChar( |
| 339 this.value.substr(newStart, newEnd - newStart)) + |
| 340 ', selected.'); |
| 341 } else if (this.start == newStart && this.end < newEnd) { |
| 342 this.speak(this.describeChar( |
| 343 this.value.substr(this.end, newEnd - this.end)) + |
| 344 ', added to selection.'); |
| 345 } else if (this.start == newStart && this.end > newEnd) { |
| 346 this.speak(this.describeChar( |
| 347 this.value.substr(newEnd, this.end - newEnd)) + |
| 348 ', removed from selection.'); |
| 349 } else if (this.end == newEnd && this.start > newStart) { |
| 350 this.speak(this.describeChar( |
| 351 this.value.substr(newStart, this.start - newStart)) + |
| 352 ', added to selection.'); |
| 353 } else if (this.end == newEnd && this.start < newStart) { |
| 354 this.speak(this.describeChar( |
| 355 this.value.substr(this.start, newStart - this.start)) + |
| 356 ', removed from selection.'); |
| 357 } else { |
| 358 // The selection changed but it wasn't an obvious extension of |
| 359 // a previous selection. Just read the new selection. |
| 360 this.speak(this.describeChar( |
| 361 this.value.substr(newStart, newEnd - newStart)) + |
| 362 ', selected.'); |
| 363 } |
| 364 } |
| 365 }; |
| 366 |
| 367 /** |
| 368 * Describe a change where the text changes. |
| 369 * @param {string} newValue The new string value of the editable text control. |
| 370 * @param {number} newStart The new 0-based start cursor/selection index. |
| 371 * @param {number} newEnd The new 0-based end cursor/selection index. |
| 372 */ |
| 373 cvox.ChromeVoxEditableTextBase.prototype.describeTextChanged = function( |
| 374 newValue, newStart, newEnd) { |
| 375 var value = this.value; |
| 376 var len = value.length; |
| 377 var newLen = newValue.length; |
| 378 var autocompleteSuffix = ''; |
| 379 var savedValue = newValue; |
| 380 |
| 381 // First, see if there's a selection at the end that might have been |
| 382 // added by autocomplete. If so, strip it off into a separate variable. |
| 383 if (newStart < newEnd && newEnd == newLen) { |
| 384 autocompleteSuffix = newValue.substr(newStart); |
| 385 newValue = newValue.substr(0, newStart); |
| 386 newEnd = newStart; |
| 387 } |
| 388 |
| 389 // Now see if the previous selection (if any) was deleted |
| 390 // and any new text was inserted at that character position. |
| 391 // This handles pasting and entering text by typing, both from |
| 392 // a cursor and from a selection. |
| 393 var prefixLen = this.start; |
| 394 var suffixLen = len - this.end; |
| 395 if (newLen >= prefixLen + suffixLen + (newEnd - newStart) && |
| 396 newValue.substr(0, prefixLen) == value.substr(0, prefixLen) && |
| 397 newValue.substr(newLen - suffixLen) == value.substr(this.end)) { |
| 398 this.describeTextChangedHelper( |
| 399 newValue, newStart, newEnd, prefixLen, suffixLen, autocompleteSuffix); |
| 400 return; |
| 401 } |
| 402 |
| 403 // Next, see if one or more characters were deleted from the previous |
| 404 // cursor position and the new cursor is in the expected place. This |
| 405 // handles backspace, forward-delete, and similar shortcuts that delete |
| 406 // a word or line. |
| 407 prefixLen = newStart; |
| 408 suffixLen = newLen - newEnd; |
| 409 if (this.start == this.end && |
| 410 newStart == newEnd && |
| 411 newValue.substr(0, prefixLen) == value.substr(0, prefixLen) && |
| 412 newValue.substr(newLen - suffixLen) == value.substr(len - suffixLen)) { |
| 413 this.describeTextChangedHelper( |
| 414 newValue, newStart, newEnd, prefixLen, suffixLen, autocompleteSuffix); |
| 415 return; |
| 416 } |
| 417 |
| 418 // If all else fails, we assume the change was not the result of a normal |
| 419 // user editing operation, so we'll have to speak feedback based only |
| 420 // on the changes to the text, not the cursor position / selection. |
| 421 // First, restore the autocomplete text if any. |
| 422 newValue += autocompleteSuffix; |
| 423 |
| 424 // If the text is short, just speak the whole thing. |
| 425 if (newLen <= this.maxShortPhraseLen) { |
| 426 this.describeTextChangedHelper(newValue, newStart, newEnd, 0, 0, ''); |
| 427 return; |
| 428 } |
| 429 |
| 430 // Otherwise, look for the common prefix and suffix, but back up so |
| 431 // that we can speak complete words, to be minimally confusing. |
| 432 prefixLen = 0; |
| 433 while (prefixLen < len && |
| 434 prefixLen < newLen && |
| 435 value[prefixLen] == newValue[prefixLen]) { |
| 436 prefixLen++; |
| 437 } |
| 438 while (prefixLen > 0 && !this.isWordBreakChar(value[prefixLen - 1])) { |
| 439 prefixLen--; |
| 440 } |
| 441 |
| 442 suffixLen = 0; |
| 443 while (suffixLen < (len - prefixLen) && |
| 444 suffixLen < (newLen - prefixLen) && |
| 445 value[len - suffixLen - 1] == newValue[newLen - suffixLen - 1]) { |
| 446 suffixLen++; |
| 447 } |
| 448 while (suffixLen > 0 && !this.isWordBreakChar(value[len - suffixLen])) { |
| 449 suffixLen--; |
| 450 } |
| 451 |
| 452 this.describeTextChangedHelper( |
| 453 newValue, newStart, newEnd, prefixLen, suffixLen, ''); |
| 454 }; |
| 455 |
| 456 /** |
| 457 * The function called by describeTextChanged after it's figured out |
| 458 * what text was deleted, what text was inserted, and what additional |
| 459 * autocomplete text was added. |
| 460 * @param {string} newValue The new string value of the editable text control. |
| 461 * @param {number} newStart The new 0-based start cursor/selection index. |
| 462 * @param {number} newEnd The new 0-based end cursor/selection index. |
| 463 * @param {number} prefixLen The number of characters in the common prefix |
| 464 * of this.value and newValue. |
| 465 * @param {number} suffixLen The number of characters in the common suffix |
| 466 * of this.value and newValue. |
| 467 * @param {string} autocompleteSuffix The autocomplete string that was added |
| 468 * to the end, if any. It should be spoken at the end of the utterance |
| 469 * describing the change. |
| 470 */ |
| 471 cvox.ChromeVoxEditableTextBase.prototype.describeTextChangedHelper = function( |
| 472 newValue, newStart, newEnd, prefixLen, suffixLen, autocompleteSuffix) { |
| 473 var len = this.value.length; |
| 474 var newLen = newValue.length; |
| 475 var deletedLen = len - prefixLen - suffixLen; |
| 476 var deleted = this.value.substr(prefixLen, deletedLen); |
| 477 var insertedLen = newLen - prefixLen - suffixLen; |
| 478 var inserted = newValue.substr(prefixLen, insertedLen); |
| 479 var utterance = ''; |
| 480 |
| 481 if (insertedLen > 1) { |
| 482 utterance = inserted; |
| 483 } else if (insertedLen == 1) { |
| 484 if (this.isWordBreakChar(inserted) && |
| 485 prefixLen > 0 && |
| 486 !this.isWordBreakChar(newValue.substr(prefixLen - 1, 1))) { |
| 487 // Speak previous word. |
| 488 var index = prefixLen; |
| 489 while (index > 0 && !this.isWordBreakChar(newValue[index - 1])) { |
| 490 index--; |
| 491 } |
| 492 if (index < prefixLen) { |
| 493 utterance = newValue.substr(index, prefixLen + 1 - index); |
| 494 } else { |
| 495 utterance = this.describeChar(inserted); |
| 496 } |
| 497 } else { |
| 498 utterance = this.describeChar(inserted); |
| 499 } |
| 500 } else if (deletedLen > 1 && !autocompleteSuffix) { |
| 501 utterance = deleted + ', deleted.'; |
| 502 } else if (deletedLen == 1) { |
| 503 utterance = this.describeChar(deleted); |
| 504 } |
| 505 |
| 506 if (autocompleteSuffix && utterance) { |
| 507 utterance += ', ' + autocompleteSuffix; |
| 508 } else if (autocompleteSuffix) { |
| 509 utterance = autocompleteSuffix; |
| 510 } |
| 511 |
| 512 this.speak(utterance); |
| 513 }; |
| 514 |
| 515 /******************************************/ |
| 516 |
| 517 /** |
| 518 * A subclass of ChromeVoxEditableTextBase a text element that's part of |
| 519 * the webpage DOM. Contains common code shared by both EditableHTMLInput |
| 520 * and EditableTextArea, but that might not apply to a non-DOM text box. |
| 521 * @extends {cvox.ChromeVoxEditableTextBase} |
| 522 * @constructor |
| 523 */ |
| 524 cvox.ChromeVoxEditableElement = function() { |
| 525 this.justSpokeDescription = false; |
| 526 }; |
| 527 goog.inherits(cvox.ChromeVoxEditableElement, |
| 528 cvox.ChromeVoxEditableTextBase); |
| 529 |
| 530 /** |
| 531 * @type boolean |
| 532 */ |
| 533 cvox.ChromeVoxEditableElement.prototype.justSpokeDescription = false; |
| 534 |
| 535 /** |
| 536 * Update the state of the text and selection and describe any changes as |
| 537 * appropriate. |
| 538 * @param {string} newValue The new string value of the editable text control. |
| 539 * @param {number} newStart The new 0-based start cursor/selection index. |
| 540 * @param {number} newEnd The new 0-based end cursor/selection index. |
| 541 */ |
| 542 cvox.ChromeVoxEditableElement.prototype.changed = function( |
| 543 newValue, newStart, newEnd) { |
| 544 // Ignore changes to the cursor and selection if they happen immediately |
| 545 // after the description was just spoken. This avoid double-speaking when, |
| 546 // for example, a text field is focused and then a moment later the |
| 547 // contents are selected. If the value changes, though, this change will |
| 548 // not be ignored. |
| 549 if (this.justSpokeDescription && this.value == newValue) { |
| 550 this.value = newValue; |
| 551 this.start = newStart; |
| 552 this.end = newEnd; |
| 553 this.justSpokeDescription = false; |
| 554 } |
| 555 |
| 556 cvox.ChromeVoxEditableTextBase.prototype.changed.apply( |
| 557 this, [newValue, newStart, newEnd]); |
| 558 }; |
| 559 |
| 560 /** |
| 561 * Get a speakable text string describing the current state of the |
| 562 * text control and its title and/or label. |
| 563 * @return {string} The speakable description. |
| 564 */ |
| 565 cvox.ChromeVoxEditableElement.prototype.getDescription = function() { |
| 566 var speech = ''; |
| 567 |
| 568 if (this.node.title) { |
| 569 speech = cvox.DomUtil.getTitle(this.node) + '. '; |
| 570 } |
| 571 |
| 572 // Find the label and use heuristics if there was no title. |
| 573 speech = cvox.DomUtil.getLabel(this.node, (speech.length < 1)); |
| 574 |
| 575 this.justSpokeDescription = true; |
| 576 |
| 577 return speech + ' ' + |
| 578 cvox.ChromeVoxEditableTextBase.prototype.getDescription.apply(this); |
| 579 }; |
| 580 |
| 581 /******************************************/ |
| 582 |
| 583 /** |
| 584 * A subclass of ChromeVoxEditableElement for an HTMLInputElement. |
| 585 * @param {HTMLInputElement} node The HTMLInputElement node. |
| 586 * @param {Object} tts A TTS object implementing speak() and stop() methods. |
| 587 * @extends {cvox.ChromeVoxEditableElement} |
| 588 * @constructor |
| 589 */ |
| 590 cvox.ChromeVoxEditableHTMLInput = function(node, tts) { |
| 591 this.node = node; |
| 592 this.value = node.value; |
| 593 this.start = node.selectionStart; |
| 594 this.end = node.selectionEnd; |
| 595 this.tts = tts; |
| 596 |
| 597 if (this.node.type == 'password') { |
| 598 this.value = this.value.replace(/./g, '*'); |
| 599 } |
| 600 }; |
| 601 goog.inherits(cvox.ChromeVoxEditableHTMLInput, |
| 602 cvox.ChromeVoxEditableElement); |
| 603 |
| 604 /** |
| 605 * Update the state of the text and selection and describe any changes as |
| 606 * appropriate. |
| 607 */ |
| 608 cvox.ChromeVoxEditableHTMLInput.prototype.update = function() { |
| 609 var newValue = this.node.value; |
| 610 if (this.node.type == 'password') { |
| 611 newValue = newValue.replace(/./g, '*'); |
| 612 } |
| 613 |
| 614 this.changed(newValue, this.node.selectionStart, this.node.selectionEnd); |
| 615 }; |
| 616 |
| 617 /** |
| 618 * @return {boolean} True if the object needs to be updated. |
| 619 */ |
| 620 cvox.ChromeVoxEditableHTMLInput.prototype.needsUpdate = function() { |
| 621 var newValue = this.node.value; |
| 622 if (this.node.type == 'password') { |
| 623 newValue = newValue.replace(/./g, '*'); |
| 624 } |
| 625 |
| 626 return (this.value != newValue || |
| 627 this.start != this.node.selectionStart || |
| 628 this.end != this.node.selectionEnd); |
| 629 }; |
| 630 |
| 631 /******************************************/ |
| 632 |
| 633 /** |
| 634 * A subclass of ChromeVoxEditableElement for an HTMLTextAreaElement. |
| 635 * @param {HTMLTextAreaElement} node The HTMLTextAreaElement node. |
| 636 * @param {Object} tts A TTS object implementing speak() and stop() methods. |
| 637 * @extends {cvox.ChromeVoxEditableElement} |
| 638 * @constructor |
| 639 */ |
| 640 cvox.ChromeVoxEditableTextArea = function(node, tts) { |
| 641 this.node = node; |
| 642 this.value = node.value; |
| 643 this.start = node.selectionStart; |
| 644 this.end = node.selectionEnd; |
| 645 this.tts = tts; |
| 646 this.multiline = true; |
| 647 this.shadowIsCurrent = false; |
| 648 this.characterToLineMap = {}; |
| 649 this.lines = {}; |
| 650 }; |
| 651 goog.inherits(cvox.ChromeVoxEditableTextArea, |
| 652 cvox.ChromeVoxEditableElement); |
| 653 |
| 654 /** |
| 655 * An offscreen div used to compute the line numbers. A single div is |
| 656 * shared by all instances of the class. |
| 657 */ |
| 658 cvox.ChromeVoxEditableTextArea.shadow; |
| 659 |
| 660 /** |
| 661 * Update the state of the text and selection and describe any changes as |
| 662 * appropriate. |
| 663 */ |
| 664 cvox.ChromeVoxEditableTextArea.prototype.update = function() { |
| 665 if (this.node.value != this.value) { |
| 666 this.shadowIsCurrent = false; |
| 667 } |
| 668 |
| 669 this.changed( |
| 670 this.node.value, this.node.selectionStart, this.node.selectionEnd); |
| 671 }; |
| 672 |
| 673 /** |
| 674 * @return {boolean} True if the object needs to be updated. |
| 675 */ |
| 676 cvox.ChromeVoxEditableTextArea.prototype.needsUpdate = function() { |
| 677 return (this.value != this.node.value || |
| 678 this.start != this.node.selectionStart || |
| 679 this.end != this.node.selectionEnd); |
| 680 }; |
| 681 |
| 682 /** |
| 683 * Get the line number corresponding to a particular index. |
| 684 * @param {number} index The 0-based character index. |
| 685 * @return {number} The 0-based line number corresponding to that character. |
| 686 */ |
| 687 cvox.ChromeVoxEditableTextArea.prototype.getLineIndex = function(index) { |
| 688 if (!this.shadowIsCurrent) { |
| 689 this.updateShadow(); |
| 690 } |
| 691 |
| 692 return this.characterToLineMap[index]; |
| 693 }; |
| 694 |
| 695 /** |
| 696 * Get the start character index of a line. |
| 697 * @param {number} index The 0-based line index. |
| 698 * @return {number} The 0-based index of the first character in this line. |
| 699 */ |
| 700 cvox.ChromeVoxEditableTextArea.prototype.getLineStart = function(index) { |
| 701 if (!this.shadowIsCurrent) { |
| 702 this.updateShadow(); |
| 703 } |
| 704 |
| 705 return this.lines[index].startIndex; |
| 706 }; |
| 707 |
| 708 /** |
| 709 * Get the end character index of a line. |
| 710 * @param {number} index The 0-based line index. |
| 711 * @return {number} The 0-based index of the end of this line. |
| 712 */ |
| 713 cvox.ChromeVoxEditableTextArea.prototype.getLineEnd = function(index) { |
| 714 if (!this.shadowIsCurrent) { |
| 715 this.updateShadow(); |
| 716 } |
| 717 |
| 718 return this.lines[index].endIndex; |
| 719 }; |
| 720 |
| 721 /** |
| 722 * Update the shadow object, an offscreen div used to compute line numbers. |
| 723 */ |
| 724 cvox.ChromeVoxEditableTextArea.prototype.updateShadow = function() { |
| 725 var shadow = cvox.ChromeVoxEditableTextArea.shadow; |
| 726 if (!shadow) { |
| 727 shadow = document.createElement('div'); |
| 728 document.body.appendChild(shadow); |
| 729 cvox.ChromeVoxEditableTextArea.shadow = shadow; |
| 730 } |
| 731 |
| 732 while (shadow.childNodes.length) { |
| 733 shadow.removeChild(shadow.childNodes[0]); |
| 734 } |
| 735 |
| 736 shadow.style.cssText = window.getComputedStyle(this.node, null).cssText; |
| 737 shadow.style.visibility = 'hidden'; |
| 738 shadow.style.position = 'absolute'; |
| 739 shadow.style.top = -9999; |
| 740 shadow.style.left = -9999; |
| 741 |
| 742 var shadowWrap = document.createElement('div'); |
| 743 shadow.appendChild(shadowWrap); |
| 744 |
| 745 var text = this.node.value; |
| 746 var outputHtml = ''; |
| 747 var lastWasWhitespace = false; |
| 748 var currentSpan = null; |
| 749 for (var i = 0; i < text.length; i++) { |
| 750 var ch = text[i]; |
| 751 var isWhitespace = this.isWhitespaceChar(ch); |
| 752 if ((isWhitespace != lastWasWhitespace) || i == 0) { |
| 753 currentSpan = document.createElement('span'); |
| 754 currentSpan.startIndex = i; |
| 755 shadowWrap.appendChild(currentSpan); |
| 756 } |
| 757 currentSpan.innerText += ch; |
| 758 currentSpan.endIndex = i; |
| 759 lastWasWhitespace = isWhitespace; |
| 760 } |
| 761 if (currentSpan) { |
| 762 currentSpan.endIndex = text.length; |
| 763 } else { |
| 764 currentSpan = document.createElement('span'); |
| 765 currentSpan.startIndex = 0; |
| 766 currentSpan.endIndex = 0; |
| 767 shadowWrap.appendChild(currentSpan); |
| 768 } |
| 769 |
| 770 this.characterToLineMap = {}; |
| 771 this.lines = {}; |
| 772 var firstSpan = shadowWrap.childNodes[0]; |
| 773 var lineIndex = -1; |
| 774 var lineOffset = -1; |
| 775 for (var n = firstSpan; n; n = n.nextSibling) { |
| 776 if (n.offsetTop > lineOffset) { |
| 777 lineIndex++; |
| 778 this.lines[lineIndex] = {}; |
| 779 this.lines[lineIndex].startIndex = n.startIndex; |
| 780 lineOffset = n.offsetTop; |
| 781 } |
| 782 this.lines[lineIndex].endIndex = n.endIndex; |
| 783 for (var j = n.startIndex; j <= n.endIndex; j++) { |
| 784 this.characterToLineMap[j] = lineIndex; |
| 785 } |
| 786 } |
| 787 |
| 788 this.shadowIsCurrent = true; |
| 789 }; |
OLD | NEW |