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

Side by Side Diff: chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js

Issue 2945703002: add support for rich text selections (Closed)
Patch Set: Rebase. Created 3 years, 6 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
OLDNEW
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
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
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
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
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
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
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 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698