| OLD | NEW |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 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'); | |
| 6 goog.provide('cvox.ChromeVoxEditableHTMLInput'); | |
| 7 goog.provide('cvox.ChromeVoxEditableTextArea'); | |
| 8 goog.provide('cvox.ChromeVoxEditableTextBase'); | 5 goog.provide('cvox.ChromeVoxEditableTextBase'); |
| 9 goog.provide('cvox.TextChangeEvent'); | 6 goog.provide('cvox.TextChangeEvent'); |
| 10 goog.provide('cvox.TextHandlerInterface'); | |
| 11 goog.provide('cvox.TypingEcho'); | 7 goog.provide('cvox.TypingEcho'); |
| 12 | 8 |
| 13 | 9 goog.require('cvox.ChromeVox'); |
| 14 goog.require('cvox.BrailleTextHandler'); | |
| 15 goog.require('cvox.ContentEditableExtractor'); | |
| 16 goog.require('cvox.DomUtil'); | |
| 17 goog.require('cvox.EditableTextAreaShadow'); | |
| 18 goog.require('cvox.TtsInterface'); | 10 goog.require('cvox.TtsInterface'); |
| 19 goog.require('goog.i18n.MessageFormat'); | 11 goog.require('goog.i18n.MessageFormat'); |
| 20 | 12 |
| 13 |
| 21 /** | 14 /** |
| 22 * @fileoverview Gives the user spoken feedback as they type, select text, | 15 * @fileoverview Generalized logic for providing spoken feedback when editing |
| 23 * and move the cursor in editable text controls, including multiline | 16 * text fields, both single and multiline fields. |
| 24 * controls. | |
| 25 * | 17 * |
| 26 * The majority of the code is in ChromeVoxEditableTextBase, a generalized | 18 * {@code ChromeVoxEditableTextBase} is a generalized class that takes the |
| 27 * class that takes the current state in the form of a text string, a | 19 * current state in the form of a text string, a cursor start location and a |
| 28 * cursor start location and a cursor end location, and calls a speak | 20 * cursor end location, and calls a speak method with the resulting text to |
| 29 * method with the resulting text to be spoken. If the control is multiline, | 21 * be spoken. This class can be used directly for single line fields or |
| 30 * information about line breaks (including automatic ones) is also needed. | 22 * extended to override methods that extract lines for multiline fields |
| 31 * | 23 * or to provide other customizations. |
| 32 * Two subclasses, ChromeVoxEditableHTMLInput and | |
| 33 * ChromeVoxEditableTextArea, take a HTML input (type=text) or HTML | |
| 34 * textarea node (respectively) in the constructor, and automatically | |
| 35 * handle retrieving the current state of the control, including | |
| 36 * computing line break information for a textarea using an offscreen | |
| 37 * shadow object. It is still the responsibility of the user of this | |
| 38 * class to trap key and focus events and call this class's update | |
| 39 * method. | |
| 40 * | |
| 41 */ | 24 */ |
| 42 | 25 |
| 43 | 26 |
| 44 /** | 27 /** |
| 45 * A class containing the information needed to speak | 28 * A class containing the information needed to speak |
| 46 * a text change event to the user. | 29 * a text change event to the user. |
| 47 * | 30 * |
| 48 * @constructor | 31 * @constructor |
| 49 * @param {string} newValue The new string value of the editable text control. | 32 * @param {string} newValue The new string value of the editable text control. |
| 50 * @param {number} newStart The new 0-based start cursor/selection index. | 33 * @param {number} newStart The new 0-based start cursor/selection index. |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 99 * @param {number} typingEcho Typing echo option. | 82 * @param {number} typingEcho Typing echo option. |
| 100 * @return {boolean} Whether the character should be spoken. | 83 * @return {boolean} Whether the character should be spoken. |
| 101 */ | 84 */ |
| 102 cvox.TypingEcho.shouldSpeakChar = function(typingEcho) { | 85 cvox.TypingEcho.shouldSpeakChar = function(typingEcho) { |
| 103 return typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD || | 86 return typingEcho == cvox.TypingEcho.CHARACTER_AND_WORD || |
| 104 typingEcho == cvox.TypingEcho.CHARACTER; | 87 typingEcho == cvox.TypingEcho.CHARACTER; |
| 105 }; | 88 }; |
| 106 | 89 |
| 107 | 90 |
| 108 /** | 91 /** |
| 109 * An interface for being notified when the text changes. | |
| 110 * @interface | |
| 111 */ | |
| 112 cvox.TextHandlerInterface = function() {}; | |
| 113 | |
| 114 | |
| 115 /** | |
| 116 * Called when text changes. | |
| 117 * @param {cvox.TextChangeEvent} evt The text change event. | |
| 118 */ | |
| 119 cvox.TextHandlerInterface.prototype.changed = function(evt) {}; | |
| 120 | |
| 121 | |
| 122 /** | |
| 123 * A class representing an abstracted editable text control. | 92 * A class representing an abstracted editable text control. |
| 124 * @param {string} value The string value of the editable text control. | 93 * @param {string} value The string value of the editable text control. |
| 125 * @param {number} start The 0-based start cursor/selection index. | 94 * @param {number} start The 0-based start cursor/selection index. |
| 126 * @param {number} end The 0-based end cursor/selection index. | 95 * @param {number} end The 0-based end cursor/selection index. |
| 127 * @param {boolean} isPassword Whether the text control if a password field. | 96 * @param {boolean} isPassword Whether the text control if a password field. |
| 128 * @param {cvox.TtsInterface} tts A TTS object. | 97 * @param {cvox.TtsInterface} tts A TTS object. |
| 129 * @constructor | 98 * @constructor |
| 130 */ | 99 */ |
| 131 cvox.ChromeVoxEditableTextBase = function(value, start, end, isPassword, tts) { | 100 cvox.ChromeVoxEditableTextBase = function(value, start, end, isPassword, tts) { |
| 132 /** | 101 /** |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 165 this.tts = tts; | 134 this.tts = tts; |
| 166 | 135 |
| 167 /** | 136 /** |
| 168 * Whether or not the text field is multiline. | 137 * Whether or not the text field is multiline. |
| 169 * @type {boolean} | 138 * @type {boolean} |
| 170 * @protected | 139 * @protected |
| 171 */ | 140 */ |
| 172 this.multiline = false; | 141 this.multiline = false; |
| 173 | 142 |
| 174 /** | 143 /** |
| 175 * An optional handler for braille output. | 144 * Whether or not the last update to the text and selection was described. |
| 176 * @type {cvox.BrailleTextHandler|undefined} | 145 * |
| 177 * @private | 146 * Some consumers of this flag like |ChromeVoxEventWatcher| depend on and |
| 147 * react to when this flag is false by generating alternative feedback. |
| 148 * @type {boolean} |
| 178 */ | 149 */ |
| 179 this.brailleHandler_ = cvox.ChromeVox.braille ? | 150 this.lastChangeDescribed = false; |
| 180 new cvox.BrailleTextHandler(cvox.ChromeVox.braille) : undefined; | 151 |
| 181 }; | 152 }; |
| 182 | 153 |
| 183 | 154 |
| 184 /** | 155 /** |
| 185 * Performs setup for this element. | 156 * Performs setup for this element. |
| 186 */ | 157 */ |
| 187 cvox.ChromeVoxEditableTextBase.prototype.setup = function() {}; | 158 cvox.ChromeVoxEditableTextBase.prototype.setup = function() {}; |
| 188 | 159 |
| 189 | 160 |
| 190 /** | 161 /** |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 227 * to an event. For example, if the user selects "Hello", we will speak | 198 * 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 | 199 * "Hello, selected", but if the user selects 1000 characters, we will speak |
| 229 * "text selected" instead. | 200 * "text selected" instead. |
| 230 * | 201 * |
| 231 * @type {number} | 202 * @type {number} |
| 232 */ | 203 */ |
| 233 cvox.ChromeVoxEditableTextBase.prototype.maxShortPhraseLen = 60; | 204 cvox.ChromeVoxEditableTextBase.prototype.maxShortPhraseLen = 60; |
| 234 | 205 |
| 235 | 206 |
| 236 /** | 207 /** |
| 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. | 208 * Get the line number corresponding to a particular index. |
| 256 * Default implementation that can be overridden by subclasses. | 209 * Default implementation that can be overridden by subclasses. |
| 257 * @param {number} index The 0-based character index. | 210 * @param {number} index The 0-based character index. |
| 258 * @return {number} The 0-based line number corresponding to that character. | 211 * @return {number} The 0-based line number corresponding to that character. |
| 259 */ | 212 */ |
| 260 cvox.ChromeVoxEditableTextBase.prototype.getLineIndex = function(index) { | 213 cvox.ChromeVoxEditableTextBase.prototype.getLineIndex = function(index) { |
| 261 return 0; | 214 return 0; |
| 262 }; | 215 }; |
| 263 | 216 |
| 264 | 217 |
| (...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 332 | 285 |
| 333 /** | 286 /** |
| 334 * Speak text, but if it's a single character, describe the character. | 287 * Speak text, but if it's a single character, describe the character. |
| 335 * @param {string} str The string to speak. | 288 * @param {string} str The string to speak. |
| 336 * @param {boolean=} opt_triggeredByUser True if the speech was triggered by a | 289 * @param {boolean=} opt_triggeredByUser True if the speech was triggered by a |
| 337 * user action. | 290 * user action. |
| 338 * @param {Object=} opt_personality Personality used to speak text. | 291 * @param {Object=} opt_personality Personality used to speak text. |
| 339 */ | 292 */ |
| 340 cvox.ChromeVoxEditableTextBase.prototype.speak = | 293 cvox.ChromeVoxEditableTextBase.prototype.speak = |
| 341 function(str, opt_triggeredByUser, opt_personality) { | 294 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; | 295 var queueMode = cvox.QueueMode.QUEUE; |
| 348 if (opt_triggeredByUser === true) { | 296 if (opt_triggeredByUser === true) { |
| 349 queueMode = cvox.QueueMode.FLUSH; | 297 queueMode = cvox.QueueMode.FLUSH; |
| 350 } | 298 } |
| 351 this.tts.speak(str, queueMode, opt_personality || {}); | 299 this.tts.speak(str, queueMode, opt_personality || {}); |
| 352 }; | 300 }; |
| 353 | 301 |
| 354 | 302 |
| 355 /** | 303 /** |
| 356 * Update the state of the text and selection and describe any changes as | 304 * Update the state of the text and selection and describe any changes as |
| (...skipping 10 matching lines...) Expand all Loading... |
| 367 if (evt.value == this.value) { | 315 if (evt.value == this.value) { |
| 368 this.describeSelectionChanged(evt); | 316 this.describeSelectionChanged(evt); |
| 369 } else { | 317 } else { |
| 370 this.describeTextChanged(evt); | 318 this.describeTextChanged(evt); |
| 371 } | 319 } |
| 372 this.lastChangeDescribed = true; | 320 this.lastChangeDescribed = true; |
| 373 | 321 |
| 374 this.value = evt.value; | 322 this.value = evt.value; |
| 375 this.start = evt.start; | 323 this.start = evt.start; |
| 376 this.end = evt.end; | 324 this.end = evt.end; |
| 377 | |
| 378 this.brailleCurrentLine_(); | |
| 379 }; | 325 }; |
| 380 | 326 |
| 381 | 327 |
| 382 /** | 328 /** |
| 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 | 329 * Describe a change in the selection or cursor position when the text |
| 408 * stays the same. | 330 * stays the same. |
| 409 * @param {cvox.TextChangeEvent} evt The text change event. | 331 * @param {cvox.TextChangeEvent} evt The text change event. |
| 410 */ | 332 */ |
| 411 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged = | 333 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged = |
| 412 function(evt) { | 334 function(evt) { |
| 413 // TODO(deboer): Factor this into two function: | 335 // TODO(deboer): Factor this into two function: |
| 414 // - one to determine the selection event | 336 // - one to determine the selection event |
| 415 // - one to speak | 337 // - one to speak |
| 416 | 338 |
| (...skipping 363 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 780 | 702 |
| 781 /** | 703 /** |
| 782 * Moves the cursor backward by one paragraph. | 704 * Moves the cursor backward by one paragraph. |
| 783 * @return {boolean} True if the action was handled. | 705 * @return {boolean} True if the action was handled. |
| 784 */ | 706 */ |
| 785 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousParagraph = | 707 cvox.ChromeVoxEditableTextBase.prototype.moveCursorToPreviousParagraph = |
| 786 function() { return false; }; | 708 function() { return false; }; |
| 787 | 709 |
| 788 | 710 |
| 789 /******************************************/ | 711 /******************************************/ |
| 790 | |
| 791 | |
| 792 /** | |
| 793 * A subclass of ChromeVoxEditableTextBase a text element that's part of | |
| 794 * the webpage DOM. Contains common code shared by both EditableHTMLInput | |
| 795 * and EditableTextArea, but that might not apply to a non-DOM text box. | |
| 796 * @param {Element} node A DOM node which allows text input. | |
| 797 * @param {string} value The string value of the editable text control. | |
| 798 * @param {number} start The 0-based start cursor/selection index. | |
| 799 * @param {number} end The 0-based end cursor/selection index. | |
| 800 * @param {boolean} isPassword Whether the text control if a password field. | |
| 801 * @param {cvox.TtsInterface} tts A TTS object. | |
| 802 * @extends {cvox.ChromeVoxEditableTextBase} | |
| 803 * @constructor | |
| 804 */ | |
| 805 cvox.ChromeVoxEditableElement = function(node, value, start, end, isPassword, | |
| 806 tts) { | |
| 807 goog.base(this, value, start, end, isPassword, tts); | |
| 808 | |
| 809 /** | |
| 810 * The DOM node which allows text input. | |
| 811 * @type {Element} | |
| 812 * @protected | |
| 813 */ | |
| 814 this.node = node; | |
| 815 | |
| 816 /** | |
| 817 * True if the description was just spoken. | |
| 818 * @type {boolean} | |
| 819 * @private | |
| 820 */ | |
| 821 this.justSpokeDescription_ = false; | |
| 822 }; | |
| 823 goog.inherits(cvox.ChromeVoxEditableElement, | |
| 824 cvox.ChromeVoxEditableTextBase); | |
| 825 | |
| 826 | |
| 827 /** | |
| 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) { | |
| 834 // Ignore changes to the cursor and selection if they happen immediately | |
| 835 // 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 | |
| 837 // contents are selected. If the value changes, though, this change will | |
| 838 // not be ignored. | |
| 839 if (this.justSpokeDescription_ && this.value == evt.value) { | |
| 840 this.value = evt.value; | |
| 841 this.start = evt.start; | |
| 842 this.end = evt.end; | |
| 843 this.justSpokeDescription_ = false; | |
| 844 } | |
| 845 goog.base(this, 'changed', evt); | |
| 846 }; | |
| 847 | |
| 848 | |
| 849 /** @override */ | |
| 850 cvox.ChromeVoxEditableElement.prototype.moveCursorToNextCharacter = function() { | |
| 851 var node = this.node; | |
| 852 node.selectionEnd++; | |
| 853 node.selectionStart = node.selectionEnd; | |
| 854 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 855 return true; | |
| 856 }; | |
| 857 | |
| 858 | |
| 859 /** @override */ | |
| 860 cvox.ChromeVoxEditableElement.prototype.moveCursorToPreviousCharacter = | |
| 861 function() { | |
| 862 var node = this.node; | |
| 863 node.selectionStart--; | |
| 864 node.selectionEnd = node.selectionStart; | |
| 865 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 866 return true; | |
| 867 }; | |
| 868 | |
| 869 | |
| 870 /** @override */ | |
| 871 cvox.ChromeVoxEditableElement.prototype.moveCursorToNextWord = function() { | |
| 872 var node = this.node; | |
| 873 var length = node.value.length; | |
| 874 var re = /\W+/gm; | |
| 875 var substring = node.value.substring(node.selectionEnd); | |
| 876 var match = re.exec(substring); | |
| 877 if (match !== null && match.index == 0) { | |
| 878 // Ignore word-breaking sequences right next to the cursor. | |
| 879 match = re.exec(substring); | |
| 880 } | |
| 881 var index = (match === null) ? length : match.index + node.selectionEnd; | |
| 882 node.selectionStart = node.selectionEnd = index; | |
| 883 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 884 return true; | |
| 885 }; | |
| 886 | |
| 887 | |
| 888 /** @override */ | |
| 889 cvox.ChromeVoxEditableElement.prototype.moveCursorToPreviousWord = function() { | |
| 890 var node = this.node; | |
| 891 var length = node.value.length; | |
| 892 var re = /\W+/gm; | |
| 893 var substring = node.value.substring(0, node.selectionStart); | |
| 894 var index = 0; | |
| 895 while (re.exec(substring) !== null) { | |
| 896 if (re.lastIndex < node.selectionStart) { | |
| 897 index = re.lastIndex; | |
| 898 } | |
| 899 } | |
| 900 node.selectionStart = node.selectionEnd = index; | |
| 901 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 902 return true; | |
| 903 }; | |
| 904 | |
| 905 | |
| 906 /** @override */ | |
| 907 cvox.ChromeVoxEditableElement.prototype.moveCursorToNextParagraph = | |
| 908 function() { | |
| 909 var node = this.node; | |
| 910 var length = node.value.length; | |
| 911 var index = node.selectionEnd >= length ? length : | |
| 912 node.value.indexOf('\n', node.selectionEnd); | |
| 913 if (index < 0) { | |
| 914 index = length; | |
| 915 } | |
| 916 node.selectionStart = node.selectionEnd = index + 1; | |
| 917 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 918 return true; | |
| 919 }; | |
| 920 | |
| 921 | |
| 922 /** @override */ | |
| 923 cvox.ChromeVoxEditableElement.prototype.moveCursorToPreviousParagraph = | |
| 924 function() { | |
| 925 var node = this.node; | |
| 926 var index = node.selectionStart <= 0 ? 0 : | |
| 927 node.value.lastIndexOf('\n', node.selectionStart - 2) + 1; | |
| 928 if (index < 0) { | |
| 929 index = 0; | |
| 930 } | |
| 931 node.selectionStart = node.selectionEnd = index; | |
| 932 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 933 return true; | |
| 934 }; | |
| 935 | |
| 936 | |
| 937 /******************************************/ | |
| 938 | |
| 939 | |
| 940 /** | |
| 941 * A subclass of ChromeVoxEditableElement for an HTMLInputElement. | |
| 942 * @param {HTMLInputElement} node The HTMLInputElement node. | |
| 943 * @param {cvox.TtsInterface} tts A TTS object. | |
| 944 * @extends {cvox.ChromeVoxEditableElement} | |
| 945 * @implements {cvox.TextHandlerInterface} | |
| 946 * @constructor | |
| 947 */ | |
| 948 cvox.ChromeVoxEditableHTMLInput = function(node, tts) { | |
| 949 this.node = node; | |
| 950 this.setup(); | |
| 951 goog.base(this, | |
| 952 node, | |
| 953 node.value, | |
| 954 node.selectionStart, | |
| 955 node.selectionEnd, | |
| 956 node.type === 'password', | |
| 957 tts); | |
| 958 }; | |
| 959 goog.inherits(cvox.ChromeVoxEditableHTMLInput, | |
| 960 cvox.ChromeVoxEditableElement); | |
| 961 | |
| 962 | |
| 963 /** | |
| 964 * Performs setup for this input node. | |
| 965 * This accounts for exception-throwing behavior introduced by crbug.com/324360. | |
| 966 * @override | |
| 967 */ | |
| 968 cvox.ChromeVoxEditableHTMLInput.prototype.setup = function() { | |
| 969 if (!this.node) { | |
| 970 return; | |
| 971 } | |
| 972 if (!cvox.DomUtil.doesInputSupportSelection(this.node)) { | |
| 973 this.originalType = this.node.type; | |
| 974 this.node.type = 'text'; | |
| 975 } | |
| 976 }; | |
| 977 | |
| 978 | |
| 979 /** | |
| 980 * Performs teardown for this input node. | |
| 981 * This accounts for exception-throwing behavior introduced by crbug.com/324360. | |
| 982 * @override | |
| 983 */ | |
| 984 cvox.ChromeVoxEditableHTMLInput.prototype.teardown = function() { | |
| 985 if (this.node && this.originalType) { | |
| 986 this.node.type = this.originalType; | |
| 987 } | |
| 988 }; | |
| 989 | |
| 990 | |
| 991 /** | |
| 992 * Update the state of the text and selection and describe any changes as | |
| 993 * appropriate. | |
| 994 * | |
| 995 * @param {boolean} triggeredByUser True if this was triggered by a user action. | |
| 996 */ | |
| 997 cvox.ChromeVoxEditableHTMLInput.prototype.update = function(triggeredByUser) { | |
| 998 var newValue = this.node.value; | |
| 999 var textChangeEvent = new cvox.TextChangeEvent(newValue, | |
| 1000 this.node.selectionStart, | |
| 1001 this.node.selectionEnd, | |
| 1002 triggeredByUser); | |
| 1003 this.changed(textChangeEvent); | |
| 1004 }; | |
| 1005 | |
| 1006 | |
| 1007 /******************************************/ | |
| 1008 | |
| 1009 | |
| 1010 /** | |
| 1011 * A subclass of ChromeVoxEditableElement for an HTMLTextAreaElement. | |
| 1012 * @param {HTMLTextAreaElement} node The HTMLTextAreaElement node. | |
| 1013 * @param {cvox.TtsInterface} tts A TTS object. | |
| 1014 * @extends {cvox.ChromeVoxEditableElement} | |
| 1015 * @implements {cvox.TextHandlerInterface} | |
| 1016 * @constructor | |
| 1017 */ | |
| 1018 cvox.ChromeVoxEditableTextArea = function(node, tts) { | |
| 1019 goog.base(this, node, node.value, node.selectionStart, node.selectionEnd, | |
| 1020 false /* isPassword */, tts); | |
| 1021 this.multiline = true; | |
| 1022 | |
| 1023 /** | |
| 1024 * True if the shadow is up-to-date with the current value of this text area. | |
| 1025 * @type {boolean} | |
| 1026 * @private | |
| 1027 */ | |
| 1028 this.shadowIsCurrent_ = false; | |
| 1029 }; | |
| 1030 goog.inherits(cvox.ChromeVoxEditableTextArea, | |
| 1031 cvox.ChromeVoxEditableElement); | |
| 1032 | |
| 1033 | |
| 1034 /** | |
| 1035 * An offscreen div used to compute the line numbers. A single div is | |
| 1036 * shared by all instances of the class. | |
| 1037 * @type {!cvox.EditableTextAreaShadow|undefined} | |
| 1038 * @private | |
| 1039 */ | |
| 1040 cvox.ChromeVoxEditableTextArea.shadow_; | |
| 1041 | |
| 1042 | |
| 1043 /** | |
| 1044 * Update the state of the text and selection and describe any changes as | |
| 1045 * appropriate. | |
| 1046 * | |
| 1047 * @param {boolean} triggeredByUser True if this was triggered by a user action. | |
| 1048 */ | |
| 1049 cvox.ChromeVoxEditableTextArea.prototype.update = function(triggeredByUser) { | |
| 1050 if (this.node.value != this.value) { | |
| 1051 this.shadowIsCurrent_ = false; | |
| 1052 } | |
| 1053 var textChangeEvent = new cvox.TextChangeEvent(this.node.value, | |
| 1054 this.node.selectionStart, this.node.selectionEnd, triggeredByUser); | |
| 1055 this.changed(textChangeEvent); | |
| 1056 }; | |
| 1057 | |
| 1058 | |
| 1059 /** | |
| 1060 * Get the line number corresponding to a particular index. | |
| 1061 * @param {number} index The 0-based character index. | |
| 1062 * @return {number} The 0-based line number corresponding to that character. | |
| 1063 */ | |
| 1064 cvox.ChromeVoxEditableTextArea.prototype.getLineIndex = function(index) { | |
| 1065 return this.getShadow().getLineIndex(index); | |
| 1066 }; | |
| 1067 | |
| 1068 | |
| 1069 /** | |
| 1070 * Get the start character index of a line. | |
| 1071 * @param {number} index The 0-based line index. | |
| 1072 * @return {number} The 0-based index of the first character in this line. | |
| 1073 */ | |
| 1074 cvox.ChromeVoxEditableTextArea.prototype.getLineStart = function(index) { | |
| 1075 return this.getShadow().getLineStart(index); | |
| 1076 }; | |
| 1077 | |
| 1078 | |
| 1079 /** | |
| 1080 * Get the end character index of a line. | |
| 1081 * @param {number} index The 0-based line index. | |
| 1082 * @return {number} The 0-based index of the end of this line. | |
| 1083 */ | |
| 1084 cvox.ChromeVoxEditableTextArea.prototype.getLineEnd = function(index) { | |
| 1085 return this.getShadow().getLineEnd(index); | |
| 1086 }; | |
| 1087 | |
| 1088 | |
| 1089 /** | |
| 1090 * Update the shadow object, an offscreen div used to compute line numbers. | |
| 1091 * @return {!cvox.EditableTextAreaShadow} The shadow object. | |
| 1092 */ | |
| 1093 cvox.ChromeVoxEditableTextArea.prototype.getShadow = function() { | |
| 1094 var shadow = cvox.ChromeVoxEditableTextArea.shadow_; | |
| 1095 if (!shadow) { | |
| 1096 shadow = cvox.ChromeVoxEditableTextArea.shadow_ = | |
| 1097 new cvox.EditableTextAreaShadow(); | |
| 1098 } | |
| 1099 if (!this.shadowIsCurrent_) { | |
| 1100 shadow.update(this.node); | |
| 1101 this.shadowIsCurrent_ = true; | |
| 1102 } | |
| 1103 return shadow; | |
| 1104 }; | |
| 1105 | |
| 1106 | |
| 1107 /** @override */ | |
| 1108 cvox.ChromeVoxEditableTextArea.prototype.moveCursorToNextLine = function() { | |
| 1109 var node = this.node; | |
| 1110 var length = node.value.length; | |
| 1111 if (node.selectionEnd >= length) { | |
| 1112 return false; | |
| 1113 } | |
| 1114 var shadow = this.getShadow(); | |
| 1115 var lineIndex = shadow.getLineIndex(node.selectionEnd); | |
| 1116 var lineStart = shadow.getLineStart(lineIndex); | |
| 1117 var offset = node.selectionEnd - lineStart; | |
| 1118 var lastLine = (length == 0) ? 0 : shadow.getLineIndex(length - 1); | |
| 1119 var newCursorPosition = (lineIndex >= lastLine) ? length : | |
| 1120 Math.min(shadow.getLineStart(lineIndex + 1) + offset, | |
| 1121 shadow.getLineEnd(lineIndex + 1)); | |
| 1122 node.selectionStart = node.selectionEnd = newCursorPosition; | |
| 1123 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 1124 return true; | |
| 1125 }; | |
| 1126 | |
| 1127 | |
| 1128 /** @override */ | |
| 1129 cvox.ChromeVoxEditableTextArea.prototype.moveCursorToPreviousLine = function() { | |
| 1130 var node = this.node; | |
| 1131 if (node.selectionStart <= 0) { | |
| 1132 return false; | |
| 1133 } | |
| 1134 var shadow = this.getShadow(); | |
| 1135 var lineIndex = shadow.getLineIndex(node.selectionStart); | |
| 1136 var lineStart = shadow.getLineStart(lineIndex); | |
| 1137 var offset = node.selectionStart - lineStart; | |
| 1138 var newCursorPosition = (lineIndex <= 0) ? 0 : | |
| 1139 Math.min(shadow.getLineStart(lineIndex - 1) + offset, | |
| 1140 shadow.getLineEnd(lineIndex - 1)); | |
| 1141 node.selectionStart = node.selectionEnd = newCursorPosition; | |
| 1142 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 1143 return true; | |
| 1144 }; | |
| 1145 | |
| 1146 | |
| 1147 /******************************************/ | |
| 1148 | |
| 1149 | |
| 1150 /** | |
| 1151 * A subclass of ChromeVoxEditableElement for elements that are contentEditable. | |
| 1152 * This is also used for a region of HTML with the ARIA role of "textbox", | |
| 1153 * so that an author can create a pure-JavaScript editable text object - this | |
| 1154 * will work the same as contentEditable as long as the DOM selection is | |
| 1155 * updated properly within the textbox when it has focus. | |
| 1156 * @param {Element} node The root contentEditable node. | |
| 1157 * @param {cvox.TtsInterface} tts A TTS object. | |
| 1158 * @extends {cvox.ChromeVoxEditableElement} | |
| 1159 * @implements {cvox.TextHandlerInterface} | |
| 1160 * @constructor | |
| 1161 */ | |
| 1162 cvox.ChromeVoxEditableContentEditable = function(node, tts) { | |
| 1163 goog.base(this, node, '', 0, 0, false /* isPassword */, tts); | |
| 1164 | |
| 1165 | |
| 1166 /** | |
| 1167 * True if the ContentEditableExtractor is current with this field's data. | |
| 1168 * @type {boolean} | |
| 1169 * @private | |
| 1170 */ | |
| 1171 this.extractorIsCurrent_ = false; | |
| 1172 | |
| 1173 var extractor = this.getExtractor(); | |
| 1174 this.value = extractor.getText(); | |
| 1175 this.start = extractor.getStartIndex(); | |
| 1176 this.end = extractor.getEndIndex(); | |
| 1177 this.multiline = true; | |
| 1178 }; | |
| 1179 goog.inherits(cvox.ChromeVoxEditableContentEditable, | |
| 1180 cvox.ChromeVoxEditableElement); | |
| 1181 | |
| 1182 /** | |
| 1183 * A helper used to compute the line numbers. A single object is | |
| 1184 * shared by all instances of the class. | |
| 1185 * @type {!cvox.ContentEditableExtractor|undefined} | |
| 1186 * @private | |
| 1187 */ | |
| 1188 cvox.ChromeVoxEditableContentEditable.extractor_; | |
| 1189 | |
| 1190 | |
| 1191 /** | |
| 1192 * Update the state of the text and selection and describe any changes as | |
| 1193 * appropriate. | |
| 1194 * | |
| 1195 * @param {boolean} triggeredByUser True if this was triggered by a user action. | |
| 1196 */ | |
| 1197 cvox.ChromeVoxEditableContentEditable.prototype.update = | |
| 1198 function(triggeredByUser) { | |
| 1199 this.extractorIsCurrent_ = false; | |
| 1200 var textChangeEvent = new cvox.TextChangeEvent( | |
| 1201 this.getExtractor().getText(), | |
| 1202 this.getExtractor().getStartIndex(), | |
| 1203 this.getExtractor().getEndIndex(), | |
| 1204 triggeredByUser); | |
| 1205 this.changed(textChangeEvent); | |
| 1206 }; | |
| 1207 | |
| 1208 | |
| 1209 /** | |
| 1210 * Get the line number corresponding to a particular index. | |
| 1211 * @param {number} index The 0-based character index. | |
| 1212 * @return {number} The 0-based line number corresponding to that character. | |
| 1213 */ | |
| 1214 cvox.ChromeVoxEditableContentEditable.prototype.getLineIndex = function(index) { | |
| 1215 return this.getExtractor().getLineIndex(index); | |
| 1216 }; | |
| 1217 | |
| 1218 | |
| 1219 /** | |
| 1220 * Get the start character index of a line. | |
| 1221 * @param {number} index The 0-based line index. | |
| 1222 * @return {number} The 0-based index of the first character in this line. | |
| 1223 */ | |
| 1224 cvox.ChromeVoxEditableContentEditable.prototype.getLineStart = function(index) { | |
| 1225 return this.getExtractor().getLineStart(index); | |
| 1226 }; | |
| 1227 | |
| 1228 | |
| 1229 /** | |
| 1230 * Get the end character index of a line. | |
| 1231 * @param {number} index The 0-based line index. | |
| 1232 * @return {number} The 0-based index of the end of this line. | |
| 1233 */ | |
| 1234 cvox.ChromeVoxEditableContentEditable.prototype.getLineEnd = function(index) { | |
| 1235 return this.getExtractor().getLineEnd(index); | |
| 1236 }; | |
| 1237 | |
| 1238 | |
| 1239 /** | |
| 1240 * Update the extractor object, an offscreen div used to compute line numbers. | |
| 1241 * @return {!cvox.ContentEditableExtractor} The extractor object. | |
| 1242 */ | |
| 1243 cvox.ChromeVoxEditableContentEditable.prototype.getExtractor = function() { | |
| 1244 var extractor = cvox.ChromeVoxEditableContentEditable.extractor_; | |
| 1245 if (!extractor) { | |
| 1246 extractor = cvox.ChromeVoxEditableContentEditable.extractor_ = | |
| 1247 new cvox.ContentEditableExtractor(); | |
| 1248 } | |
| 1249 if (!this.extractorIsCurrent_) { | |
| 1250 extractor.update(this.node); | |
| 1251 this.extractorIsCurrent_ = true; | |
| 1252 } | |
| 1253 return extractor; | |
| 1254 }; | |
| 1255 | |
| 1256 | |
| 1257 /** | |
| 1258 * @override | |
| 1259 */ | |
| 1260 cvox.ChromeVoxEditableContentEditable.prototype.changed = | |
| 1261 function(evt) { | |
| 1262 if (!evt.triggeredByUser) { | |
| 1263 return; | |
| 1264 } | |
| 1265 // Take over here if we can't describe a change; assume it's a blank line. | |
| 1266 if (!this.shouldDescribeChange(evt)) { | |
| 1267 this.speak(cvox.ChromeVox.msgs.getMsg('text_box_blank'), true); | |
| 1268 if (this.brailleHandler_) { | |
| 1269 this.brailleHandler_.changed('' /*line*/, 0 /*start*/, 0 /*end*/, | |
| 1270 true /*multiline*/, null /*element*/, | |
| 1271 evt.start /*lineStart*/); | |
| 1272 } | |
| 1273 } else { | |
| 1274 goog.base(this, 'changed', evt); | |
| 1275 } | |
| 1276 }; | |
| 1277 | |
| 1278 | |
| 1279 /** @override */ | |
| 1280 cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToNextCharacter = | |
| 1281 function() { | |
| 1282 window.getSelection().modify('move', 'forward', 'character'); | |
| 1283 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 1284 return true; | |
| 1285 }; | |
| 1286 | |
| 1287 | |
| 1288 /** @override */ | |
| 1289 cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToPreviousCharacter = | |
| 1290 function() { | |
| 1291 window.getSelection().modify('move', 'backward', 'character'); | |
| 1292 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 1293 return true; | |
| 1294 }; | |
| 1295 | |
| 1296 | |
| 1297 /** @override */ | |
| 1298 cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToNextParagraph = | |
| 1299 function() { | |
| 1300 window.getSelection().modify('move', 'forward', 'paragraph'); | |
| 1301 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 1302 return true; | |
| 1303 }; | |
| 1304 | |
| 1305 /** @override */ | |
| 1306 cvox.ChromeVoxEditableContentEditable.prototype.moveCursorToPreviousParagraph = | |
| 1307 function() { | |
| 1308 window.getSelection().modify('move', 'backward', 'paragraph'); | |
| 1309 cvox.ChromeVoxEventWatcher.handleTextChanged(true); | |
| 1310 return true; | |
| 1311 }; | |
| 1312 | |
| 1313 | |
| 1314 /** | |
| 1315 * @override | |
| 1316 */ | |
| 1317 cvox.ChromeVoxEditableContentEditable.prototype.shouldDescribeChange = | |
| 1318 function(evt) { | |
| 1319 var sel = window.getSelection(); | |
| 1320 var cursor = new cvox.Cursor(sel.baseNode, sel.baseOffset, ''); | |
| 1321 | |
| 1322 // This is a very specific work around because of our buggy content editable | |
| 1323 // support. Blank new lines are not captured in the line indexing data | |
| 1324 // structures. | |
| 1325 // Scenario: given a piece of text like: | |
| 1326 // | |
| 1327 // Some Title | |
| 1328 // | |
| 1329 // Description | |
| 1330 // Footer | |
| 1331 // | |
| 1332 // The new lines after Title are not traversed to by TraverseUtil. A root fix | |
| 1333 // would make changes there. However, considering the fickle nature of that | |
| 1334 // code, we specifically detect for new lines here. | |
| 1335 if (Math.abs(this.start - evt.start) != 1 && | |
| 1336 this.start == this.end && | |
| 1337 evt.start == evt.end && | |
| 1338 sel.baseNode == sel.extentNode && | |
| 1339 sel.baseOffset == sel.extentOffset && | |
| 1340 sel.baseNode.nodeType == Node.ELEMENT_NODE && | |
| 1341 sel.baseNode.querySelector('BR') && | |
| 1342 cvox.TraverseUtil.forwardsChar(cursor, [], [])) { | |
| 1343 // This case detects if the range selection surrounds a new line, | |
| 1344 // but there is still content after the new line (like the example | |
| 1345 // above after "Title"). In these cases, we "pretend" we're the | |
| 1346 // last character so we speak "blank". | |
| 1347 return false; | |
| 1348 } | |
| 1349 | |
| 1350 // Otherwise, we should never speak "blank" no matter what (even if | |
| 1351 // we're at the end of a content editable). | |
| 1352 return true; | |
| 1353 }; | |
| OLD | NEW |