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 |