OLD | NEW |
---|---|
1 // Copyright 2015 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 /** | 5 /** |
6 * @fileoverview Processes events related to editing text and emits the | 6 * @fileoverview Processes events related to editing text and emits the |
7 * appropriate spoken and braille feedback. | 7 * appropriate spoken and braille feedback. |
8 */ | 8 */ |
9 | 9 |
10 goog.provide('editing.TextEditHandler'); | 10 goog.provide('editing.TextEditHandler'); |
(...skipping 197 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
208 * A |ChromeVoxEditableTextBase| that implements text editing feedback | 208 * A |ChromeVoxEditableTextBase| that implements text editing feedback |
209 * for automation tree text fields using anchor and focus selection. | 209 * for automation tree text fields using anchor and focus selection. |
210 * @constructor | 210 * @constructor |
211 * @param {!AutomationNode} node | 211 * @param {!AutomationNode} node |
212 * @extends {AutomationEditableText} | 212 * @extends {AutomationEditableText} |
213 */ | 213 */ |
214 function AutomationRichEditableText(node) { | 214 function AutomationRichEditableText(node) { |
215 AutomationEditableText.call(this, node); | 215 AutomationEditableText.call(this, node); |
216 | 216 |
217 var root = this.node_.root; | 217 var root = this.node_.root; |
218 if (!root || !root.anchorObject || !root.focusObject) | 218 if (!root || !root.anchorObject || !root.focusObject || |
219 root.anchorOffset === undefined || root.focusOffset === undefined) | |
219 return; | 220 return; |
220 | 221 |
221 this.line_ = new editing.EditableLine( | 222 this.anchorLine_ = new editing.EditableLine( |
222 root.anchorObject, root.anchorOffset, root.focusObject, root.focusOffset); | 223 root.anchorObject, root.anchorOffset, root.anchorObject, |
224 root.anchorOffset); | |
225 this.focusLine_ = new editing.EditableLine( | |
226 root.focusObject, root.focusOffset, root.focusObject, root.focusOffset); | |
223 } | 227 } |
224 | 228 |
225 AutomationRichEditableText.prototype = { | 229 AutomationRichEditableText.prototype = { |
226 __proto__: AutomationEditableText.prototype, | 230 __proto__: AutomationEditableText.prototype, |
227 | 231 |
228 /** @override */ | 232 /** @override */ |
229 onUpdate: function() { | 233 onUpdate: function() { |
230 var root = this.node_.root; | 234 var root = this.node_.root; |
231 if (!root.anchorObject || !root.focusObject) | 235 if (!root.anchorObject || !root.focusObject || |
236 root.anchorOffset === undefined || root.focusOffset === undefined) | |
232 return; | 237 return; |
233 | 238 |
239 var anchorLine = new editing.EditableLine( | |
240 root.anchorObject, root.anchorOffset, root.anchorObject, | |
241 root.anchorOffset); | |
242 var focusLine = new editing.EditableLine( | |
243 root.focusObject, root.focusOffset, root.focusObject, root.focusOffset); | |
244 | |
245 var prevAnchorLine = this.anchorLine_; | |
246 var prevFocusLine = this.focusLine_; | |
247 this.anchorLine_ = anchorLine; | |
248 this.focusLine_ = focusLine; | |
249 | |
250 // Compute the current line based upon whether the current selection was | |
251 // extended from anchor or focus. The default behavior is to compute lines | |
252 // via focus. | |
253 var baseLineOnStart = prevFocusLine.equalsStrict(focusLine); | |
254 | |
234 var cur = new editing.EditableLine( | 255 var cur = new editing.EditableLine( |
235 root.anchorObject, root.anchorOffset || 0, root.focusObject, | 256 root.anchorObject, root.anchorOffset, root.focusObject, |
236 root.focusOffset || 0); | 257 root.focusOffset, baseLineOnStart); |
237 var prev = this.line_; | 258 var prev = this.line_; |
238 this.line_ = cur; | 259 this.line_ = cur; |
239 | 260 |
240 if (prev.equals(cur)) { | 261 // Selection stayed within the same line(s) and didn't cross into new lines. |
241 // Collapsed cursor. | 262 if (anchorLine.equals(prevAnchorLine) && focusLine.equals(prevFocusLine)) { |
263 // Intra-line changes. | |
242 this.changed(new cvox.TextChangeEvent( | 264 this.changed(new cvox.TextChangeEvent( |
243 cur.text || '', cur.startOffset || 0, cur.endOffset || 0, true)); | 265 cur.text || '', cur.startOffset, cur.endOffset, true)); |
244 this.brailleCurrentRichLine_(); | 266 this.brailleCurrentRichLine_(); |
245 | 267 |
246 // Finally, queue up any text markers/styles at bounds. | 268 // Finally, queue up any text markers/styles at bounds. |
247 var container = cur.startContainer_; | 269 var container = cur.startContainer_; |
248 if (!container) | 270 if (!container) |
249 return; | 271 return; |
250 | 272 |
251 if (container.markerTypes) { | 273 if (container.markerTypes) { |
252 // Only consider markers that start or end at the selection bounds. | 274 // Only consider markers that start or end at the selection bounds. |
253 var markerStartIndex = -1, markerEndIndex = -1; | 275 var markerStartIndex = -1, markerEndIndex = -1; |
(...skipping 22 matching lines...) Expand all Loading... | |
276 | 298 |
277 // Start of the container. | 299 // Start of the container. |
278 if (cur.containerStartOffset == cur.startOffset) | 300 if (cur.containerStartOffset == cur.startOffset) |
279 this.speakTextStyle_(container); | 301 this.speakTextStyle_(container); |
280 else if (cur.containerEndOffset == cur.endOffset) | 302 else if (cur.containerEndOffset == cur.endOffset) |
281 this.speakTextStyle_(container, true); | 303 this.speakTextStyle_(container, true); |
282 | 304 |
283 return; | 305 return; |
284 } | 306 } |
285 | 307 |
286 // Just output the current line. | 308 if (cur.text == '') { |
287 cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH); | 309 // This line has no text content. Describe the DOM selection. |
288 this.brailleCurrentRichLine_(); | 310 new Output() |
311 .withRichSpeechAndBraille( | |
312 new Range(cur.start_, cur.end_), | |
313 new Range(prev.start_, prev.end_), Output.EventType.NAVIGATE) | |
314 .go(); | |
315 } else { | |
316 // Describe the current line. This accounts for previous/current | |
317 // selections and picking the line edge boundary that changed (as computed | |
318 // above). This is also the code path for describing paste. | |
319 cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH); | |
320 this.brailleCurrentRichLine_(); | |
321 } | |
289 | 322 |
290 // The state in EditableTextBase needs to get updated with the new line | 323 // The state in EditableTextBase needs to get updated with the new line |
291 // contents, so that subsequent intra-line changes get the right state | 324 // contents, so that subsequent intra-line changes get the right state |
292 // transitions. | 325 // transitions. |
293 this.value = cur.text; | 326 this.value = cur.text; |
294 this.start = cur.startOffset; | 327 this.start = cur.startOffset; |
295 this.end = cur.endOffset; | 328 this.end = cur.endOffset; |
296 }, | 329 }, |
297 | 330 |
298 /** | 331 /** |
(...skipping 135 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
434 }; | 467 }; |
435 | 468 |
436 /** | 469 /** |
437 * @private {ChromeVoxStateObserver} | 470 * @private {ChromeVoxStateObserver} |
438 */ | 471 */ |
439 editing.observer_ = new editing.EditingChromeVoxStateObserver(); | 472 editing.observer_ = new editing.EditingChromeVoxStateObserver(); |
440 | 473 |
441 /** | 474 /** |
442 * An EditableLine encapsulates all data concerning a line in the automation | 475 * An EditableLine encapsulates all data concerning a line in the automation |
443 * tree necessary to provide output. | 476 * tree necessary to provide output. |
477 * Editable: an editable selection (e.g. start/end offsets) get saved. | |
478 * Line: nodes/offsets at the beginning/end of a line get saved. | |
479 * @param {!AutomationNode} startNode | |
480 * @param {number} startIndex | |
481 * @param {!AutomationNode} endNode | |
482 * @param {number} endIndex | |
483 * @param {boolean=} opt_baseLineOnStart Controls whether to use anchor or | |
484 * focus for Line computations as described above. Selections are automatically | |
485 * truncated up to either the line start or end. | |
444 * @constructor | 486 * @constructor |
445 */ | 487 */ |
446 editing.EditableLine = function(startNode, startIndex, endNode, endIndex) { | 488 editing.EditableLine = function( |
489 startNode, startIndex, endNode, endIndex, opt_baseLineOnStart) { | |
447 /** @private {!Cursor} */ | 490 /** @private {!Cursor} */ |
448 this.start_ = new Cursor(startNode, startIndex); | 491 this.start_ = new Cursor(startNode, startIndex); |
449 this.start_ = this.start_.deepEquivalent || this.start_; | 492 this.start_ = this.start_.deepEquivalent || this.start_; |
450 | 493 |
451 /** @private {!Cursor} */ | 494 /** @private {!Cursor} */ |
452 this.end_ = new Cursor(endNode, endIndex); | 495 this.end_ = new Cursor(endNode, endIndex); |
453 this.end_ = this.end_.deepEquivalent || this.end_; | 496 this.end_ = this.end_.deepEquivalent || this.end_; |
454 /** @private {number} */ | 497 /** @private {number} */ |
455 this.localContainerStartOffset_ = startIndex; | 498 this.localContainerStartOffset_ = startIndex; |
456 | 499 |
457 // Computed members. | 500 // Computed members. |
458 /** @private {Spannable} */ | 501 /** @private {Spannable} */ |
459 this.value_; | 502 this.value_; |
460 /** @private {AutomationNode} */ | 503 /** @private {AutomationNode} */ |
461 this.lineStart_; | 504 this.lineStart_; |
462 /** @private {AutomationNode} */ | 505 /** @private {AutomationNode} */ |
463 this.lineEnd_; | 506 this.lineEnd_; |
464 /** @private {AutomationNode|undefined} */ | 507 /** @private {AutomationNode|undefined} */ |
465 this.startContainer_; | 508 this.startContainer_; |
466 /** @private {AutomationNode|undefined} */ | 509 /** @private {AutomationNode|undefined} */ |
467 this.lineStartContainer_; | 510 this.lineStartContainer_; |
468 /** @private {number} */ | 511 /** @private {number} */ |
469 this.localLineStartContainerOffset_ = 0; | 512 this.localLineStartContainerOffset_ = 0; |
513 /** @private {AutomationNode|undefined} */ | |
514 this.lineEndContainer_; | |
515 /** @private {number} */ | |
516 this.localLineEndContainerOffset_ = 0; | |
470 | 517 |
471 this.computeLineData_(); | 518 this.computeLineData_(opt_baseLineOnStart); |
472 }; | 519 }; |
473 | 520 |
474 editing.EditableLine.prototype = { | 521 editing.EditableLine.prototype = { |
475 /** @private */ | 522 /** @private */ |
476 computeLineData_: function() { | 523 computeLineData_: function(opt_baseLineOnStart) { |
524 // Note that we calculate the line based only upon anchor or focus even if | |
525 // they do not fall on the same line. It is up to the caller to specify | |
526 // which end to base this line upon since it requires reasoning about two | |
527 // lines. | |
477 var nameLen = 0; | 528 var nameLen = 0; |
478 if (this.start_.node.name) | 529 var lineBase = opt_baseLineOnStart ? this.start_ : this.end_; |
479 nameLen = this.start_.node.name.length; | 530 var lineExtend = opt_baseLineOnStart ? this.end_ : this.start_; |
480 | 531 |
481 this.value_ = new Spannable(this.start_.node.name || '', this.start_); | 532 if (lineBase.node.name) |
482 if (this.start_.node == this.end_.node) | 533 nameLen = lineBase.node.name.length; |
483 this.value_.setSpan(this.end_, 0, nameLen); | 534 |
535 this.value_ = new Spannable(lineBase.node.name || '', lineBase); | |
536 if (lineBase.node == lineExtend.node) | |
537 this.value_.setSpan(lineExtend, 0, nameLen); | |
484 | 538 |
485 // Initialize defaults. | 539 // Initialize defaults. |
486 this.lineStart_ = this.start_.node; | 540 this.lineStart_ = lineBase.node; |
487 this.lineEnd_ = this.lineStart_; | 541 this.lineEnd_ = this.lineStart_; |
488 this.startContainer_ = this.lineStart_.parent; | 542 this.startContainer_ = this.lineStart_.parent; |
489 this.lineStartContainer_ = this.lineStart_.parent; | 543 this.lineStartContainer_ = this.lineStart_.parent; |
544 this.lineEndContainer_ = this.lineStart_.parent; | |
490 | 545 |
491 // Annotate each chunk with its associated inline text box node. | 546 // Annotate each chunk with its associated inline text box node. |
492 this.value_.setSpan(this.lineStart_, 0, this.lineStart_.name.length); | 547 this.value_.setSpan(this.lineStart_, 0, nameLen); |
493 | 548 |
494 // If the current selection is not on an inline text box (e.g. an image), | 549 // If the current selection is not on an inline text box (e.g. an image), |
495 // return early here so that the line contents are just the node. This is | 550 // return early here so that the line contents are just the node. This is |
496 // pending the ability to show non-text leaf inline objects. | 551 // pending the ability to show non-text leaf inline objects. |
497 if (this.lineStart_.role != RoleType.INLINE_TEXT_BOX) | 552 if (this.lineStart_.role != RoleType.INLINE_TEXT_BOX) |
498 return; | 553 return; |
499 | 554 |
500 // Also, track their static text parents. | 555 // Also, track their static text parents. |
501 var parents = [this.startContainer_]; | 556 var parents = [this.startContainer_]; |
502 | 557 |
(...skipping 23 matching lines...) Expand all Loading... | |
526 this.lineEnd_ = lineEnd; | 581 this.lineEnd_ = lineEnd; |
527 if (parents[parents.length - 1] != lineEnd.parent) | 582 if (parents[parents.length - 1] != lineEnd.parent) |
528 parents.push(this.lineEnd_.parent); | 583 parents.push(this.lineEnd_.parent); |
529 | 584 |
530 var annotation = lineEnd; | 585 var annotation = lineEnd; |
531 if (lineEnd == this.end_.node) | 586 if (lineEnd == this.end_.node) |
532 annotation = this.end_; | 587 annotation = this.end_; |
533 | 588 |
534 this.value_.append(new Spannable(lineEnd.name, annotation)); | 589 this.value_.append(new Spannable(lineEnd.name, annotation)); |
535 } | 590 } |
591 this.lineEndContainer_ = this.lineEnd_.parent; | |
536 | 592 |
537 // Finally, annotate with all parent static texts as NodeSpan's so that | 593 // Finally, annotate with all parent static texts as NodeSpan's so that |
538 // braille routing can key properly into the node with an offset. | 594 // braille routing can key properly into the node with an offset. |
539 // Note that both line start and end needs to account for | 595 // Note that both line start and end needs to account for |
540 // potential offsets into the static texts as follows. | 596 // potential offsets into the static texts as follows. |
541 var textCountBeforeLineStart = 0, textCountAfterLineEnd = 0; | 597 var textCountBeforeLineStart = 0, textCountAfterLineEnd = 0; |
542 var finder = this.lineStart_; | 598 var finder = this.lineStart_; |
543 while (finder.previousSibling) { | 599 while (finder.previousSibling) { |
544 finder = finder.previousSibling; | 600 finder = finder.previousSibling; |
545 textCountBeforeLineStart += finder.name.length; | 601 textCountBeforeLineStart += finder.name.length; |
546 } | 602 } |
547 this.localLineStartContainerOffset_ = textCountBeforeLineStart; | 603 this.localLineStartContainerOffset_ = textCountBeforeLineStart; |
548 | 604 |
549 finder = this.lineEnd_; | 605 finder = this.lineEnd_; |
550 while (finder.nextSibling) { | 606 while (finder.nextSibling) { |
551 finder = finder.nextSibling; | 607 finder = finder.nextSibling; |
552 textCountAfterLineEnd += finder.name.length; | 608 textCountAfterLineEnd += finder.name.length; |
553 } | 609 } |
554 | 610 |
611 if (this.lineEndContainer_.name) { | |
612 this.localLineEndContainerOffset_ = | |
613 this.lineEndContainer_.name.length - textCountAfterLineEnd; | |
614 } | |
615 | |
555 var len = 0; | 616 var len = 0; |
556 for (var i = 0; i < parents.length; i++) { | 617 for (var i = 0; i < parents.length; i++) { |
557 var parent = parents[i]; | 618 var parent = parents[i]; |
558 | 619 |
559 if (!parent.name) | 620 if (!parent.name) |
560 continue; | 621 continue; |
561 | 622 |
562 var prevLen = len; | 623 var prevLen = len; |
563 | 624 |
564 var currentLen = parent.name.length; | 625 var currentLen = parent.name.length; |
(...skipping 20 matching lines...) Expand all Loading... | |
585 } catch (e) { | 646 } catch (e) { |
586 } | 647 } |
587 } | 648 } |
588 }, | 649 }, |
589 | 650 |
590 /** | 651 /** |
591 * Gets the selection offset based on the text content of this line. | 652 * Gets the selection offset based on the text content of this line. |
592 * @return {number} | 653 * @return {number} |
593 */ | 654 */ |
594 get startOffset() { | 655 get startOffset() { |
595 return this.value_.getSpanStart(this.start_) + this.start_.index; | 656 // It is possible that the start cursor points to content before this line |
657 // (e.g. in a multi-line selection). | |
658 try { | |
659 return this.value_.getSpanStart(this.start_) + this.start_.index; | |
660 } catch (e) { | |
661 // When that happens, fall back to the start of this line. | |
662 return 0; | |
663 } | |
596 }, | 664 }, |
597 | 665 |
598 /** | 666 /** |
599 * Gets the selection offset based on the text content of this line. | 667 * Gets the selection offset based on the text content of this line. |
600 * @return {number} | 668 * @return {number} |
601 */ | 669 */ |
602 get endOffset() { | 670 get endOffset() { |
603 return this.value_.getSpanStart(this.end_) + this.end_.index; | 671 try { |
672 return this.value_.getSpanStart(this.end_) + this.end_.index; | |
673 } catch (e) { | |
674 return this.value_.length; | |
675 } | |
604 }, | 676 }, |
605 | 677 |
606 /** | 678 /** |
607 * Gets the selection offset based on the parent's text. | 679 * Gets the selection offset based on the parent's text. |
608 * The parent is expected to be static text. | 680 * The parent is expected to be static text. |
609 * @return {number} | 681 * @return {number} |
610 */ | 682 */ |
611 get localStartOffset() { | 683 get localStartOffset() { |
612 return this.startOffset - this.containerStartOffset; | 684 return this.startOffset - this.containerStartOffset; |
613 }, | 685 }, |
(...skipping 27 matching lines...) Expand all Loading... | |
641 | 713 |
642 /** | 714 /** |
643 * The text content of this line. | 715 * The text content of this line. |
644 * @return {string} The text of this line. | 716 * @return {string} The text of this line. |
645 */ | 717 */ |
646 get text() { | 718 get text() { |
647 return this.value_.toString(); | 719 return this.value_.toString(); |
648 }, | 720 }, |
649 | 721 |
650 /** | 722 /** |
723 * Returns true if |otherLine| shares its start position with this line. | |
724 * @return {boolean} | |
dmazzoni
2017/06/21 06:22:07
Add @param {editing.EditableLine}, I'm assuming a
David Tseng
2017/06/21 16:58:28
Acknowledged and done elsewhere for isSameLine and
| |
725 */ | |
726 equalsStart: function(otherLine) { | |
dmazzoni
2017/06/21 06:22:08
how about hasSameStart?
David Tseng
2017/06/21 16:58:28
Start is really overloaded in all of the editing c
| |
727 return otherLine.lineStartContainer_ == this.lineStartContainer_ && | |
728 otherLine.localLineStartContainerOffset_ == | |
729 this.localLineStartContainerOffset_; | |
730 }, | |
731 | |
732 /** | |
733 * Returns true if |otherLine| shares its end position with this line. | |
734 * @return {boolean} | |
735 */ | |
736 equalsEnd: function(otherLine) { | |
737 return otherLine.lineEndContainer_ == this.lineEndContainer_ && | |
738 otherLine.localLineEndContainerOffset_ == | |
739 this.localLineEndContainerOffset_; | |
740 }, | |
741 | |
742 /** | |
651 * Returns true if |otherLine| surrounds the same line as |this|. Note that | 743 * Returns true if |otherLine| surrounds the same line as |this|. Note that |
652 * the contents of the line might be different. | 744 * the contents of the line might be different. |
653 * @return {boolean} | 745 * @return {boolean} |
654 */ | 746 */ |
655 equals: function(otherLine) { | 747 equals: function(otherLine) { |
dmazzoni
2017/06/21 06:22:07
I think it'd be more clear to rename equalsStrict
| |
656 // Equality is intentionally loose here as any of the state nodes can be | 748 // Equality is intentionally loose here as any of the state nodes can be |
657 // invalidated at any time. We rely upon the start/anchor of the line | 749 // invalidated at any time. We rely upon the start/anchor of the line |
658 // staying the same. | 750 // staying the same. |
659 return otherLine.lineStartContainer_ == this.lineStartContainer_ && | 751 return this.equalsStart(otherLine) || this.equalsEnd(otherLine); |
660 otherLine.localLineStartContainerOffset_ == | 752 }, |
661 this.localLineStartContainerOffset_; | 753 |
754 /** | |
755 * Returns true if |otherLine| surrounds the same line as |this| and has the | |
756 * same selection. | |
757 * @return {boolean} | |
758 */ | |
759 equalsStrict: function(otherLine) { | |
760 return this.equals(otherLine) && | |
761 this.startOffset == otherLine.startOffset && | |
762 this.endOffset == otherLine.endOffset; | |
662 } | 763 } |
663 }; | 764 }; |
664 | 765 |
665 }); | 766 }); |
OLD | NEW |