Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(145)

Side by Side Diff: chrome/browser/resources/access_chromevox/common/editable_text.js

Issue 6254007: Adding ChromeVox as a component extensions (enabled only for ChromeOS, for no... (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: '' Created 9 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
Property Changes:
Added: svn:executable
+ *
Added: svn:eol-style
+ LF
OLDNEW
(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 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698