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

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

Issue 2945703002: add support for rich text selections (Closed)
Patch Set: Address comments. 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
« no previous file with comments | « no previous file | chrome/browser/resources/chromeos/chromevox/cvox2/background/editing_test.extjs » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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.isSameLineAndSelection(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.isSameLine(prevAnchorLine) &&
263 focusLine.isSameLine(prevFocusLine)) {
264 // Intra-line changes.
242 this.changed(new cvox.TextChangeEvent( 265 this.changed(new cvox.TextChangeEvent(
243 cur.text || '', cur.startOffset || 0, cur.endOffset || 0, true)); 266 cur.text || '', cur.startOffset, cur.endOffset, true));
244 this.brailleCurrentRichLine_(); 267 this.brailleCurrentRichLine_();
245 268
246 // Finally, queue up any text markers/styles at bounds. 269 // Finally, queue up any text markers/styles at bounds.
247 var container = cur.startContainer_; 270 var container = cur.startContainer_;
248 if (!container) 271 if (!container)
249 return; 272 return;
250 273
251 if (container.markerTypes) { 274 if (container.markerTypes) {
252 // Only consider markers that start or end at the selection bounds. 275 // Only consider markers that start or end at the selection bounds.
253 var markerStartIndex = -1, markerEndIndex = -1; 276 var markerStartIndex = -1, markerEndIndex = -1;
(...skipping 22 matching lines...) Expand all
276 299
277 // Start of the container. 300 // Start of the container.
278 if (cur.containerStartOffset == cur.startOffset) 301 if (cur.containerStartOffset == cur.startOffset)
279 this.speakTextStyle_(container); 302 this.speakTextStyle_(container);
280 else if (cur.containerEndOffset == cur.endOffset) 303 else if (cur.containerEndOffset == cur.endOffset)
281 this.speakTextStyle_(container, true); 304 this.speakTextStyle_(container, true);
282 305
283 return; 306 return;
284 } 307 }
285 308
286 // Just output the current line. 309 if (cur.text == '') {
287 cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH); 310 // This line has no text content. Describe the DOM selection.
288 this.brailleCurrentRichLine_(); 311 new Output()
312 .withRichSpeechAndBraille(
313 new Range(cur.start_, cur.end_),
314 new Range(prev.start_, prev.end_), Output.EventType.NAVIGATE)
315 .go();
316 } else {
317 // Describe the current line. This accounts for previous/current
318 // selections and picking the line edge boundary that changed (as computed
319 // above). This is also the code path for describing paste.
320 cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH);
321 this.brailleCurrentRichLine_();
322 }
289 323
290 // The state in EditableTextBase needs to get updated with the new line 324 // The state in EditableTextBase needs to get updated with the new line
291 // contents, so that subsequent intra-line changes get the right state 325 // contents, so that subsequent intra-line changes get the right state
292 // transitions. 326 // transitions.
293 this.value = cur.text; 327 this.value = cur.text;
294 this.start = cur.startOffset; 328 this.start = cur.startOffset;
295 this.end = cur.endOffset; 329 this.end = cur.endOffset;
296 }, 330 },
297 331
298 /** 332 /**
(...skipping 135 matching lines...) Expand 10 before | Expand all | Expand 10 after
434 }; 468 };
435 469
436 /** 470 /**
437 * @private {ChromeVoxStateObserver} 471 * @private {ChromeVoxStateObserver}
438 */ 472 */
439 editing.observer_ = new editing.EditingChromeVoxStateObserver(); 473 editing.observer_ = new editing.EditingChromeVoxStateObserver();
440 474
441 /** 475 /**
442 * An EditableLine encapsulates all data concerning a line in the automation 476 * An EditableLine encapsulates all data concerning a line in the automation
443 * tree necessary to provide output. 477 * tree necessary to provide output.
478 * Editable: an editable selection (e.g. start/end offsets) get saved.
479 * Line: nodes/offsets at the beginning/end of a line get saved.
480 * @param {!AutomationNode} startNode
481 * @param {number} startIndex
482 * @param {!AutomationNode} endNode
483 * @param {number} endIndex
484 * @param {boolean=} opt_baseLineOnStart Controls whether to use anchor or
485 * focus for Line computations as described above. Selections are automatically
486 * truncated up to either the line start or end.
444 * @constructor 487 * @constructor
445 */ 488 */
446 editing.EditableLine = function(startNode, startIndex, endNode, endIndex) { 489 editing.EditableLine = function(
490 startNode, startIndex, endNode, endIndex, opt_baseLineOnStart) {
447 /** @private {!Cursor} */ 491 /** @private {!Cursor} */
448 this.start_ = new Cursor(startNode, startIndex); 492 this.start_ = new Cursor(startNode, startIndex);
449 this.start_ = this.start_.deepEquivalent || this.start_; 493 this.start_ = this.start_.deepEquivalent || this.start_;
450 494
451 /** @private {!Cursor} */ 495 /** @private {!Cursor} */
452 this.end_ = new Cursor(endNode, endIndex); 496 this.end_ = new Cursor(endNode, endIndex);
453 this.end_ = this.end_.deepEquivalent || this.end_; 497 this.end_ = this.end_.deepEquivalent || this.end_;
454 /** @private {number} */ 498 /** @private {number} */
455 this.localContainerStartOffset_ = startIndex; 499 this.localContainerStartOffset_ = startIndex;
456 500
457 // Computed members. 501 // Computed members.
458 /** @private {Spannable} */ 502 /** @private {Spannable} */
459 this.value_; 503 this.value_;
460 /** @private {AutomationNode} */ 504 /** @private {AutomationNode} */
461 this.lineStart_; 505 this.lineStart_;
462 /** @private {AutomationNode} */ 506 /** @private {AutomationNode} */
463 this.lineEnd_; 507 this.lineEnd_;
464 /** @private {AutomationNode|undefined} */ 508 /** @private {AutomationNode|undefined} */
465 this.startContainer_; 509 this.startContainer_;
466 /** @private {AutomationNode|undefined} */ 510 /** @private {AutomationNode|undefined} */
467 this.lineStartContainer_; 511 this.lineStartContainer_;
468 /** @private {number} */ 512 /** @private {number} */
469 this.localLineStartContainerOffset_ = 0; 513 this.localLineStartContainerOffset_ = 0;
514 /** @private {AutomationNode|undefined} */
515 this.lineEndContainer_;
516 /** @private {number} */
517 this.localLineEndContainerOffset_ = 0;
470 518
471 this.computeLineData_(); 519 this.computeLineData_(opt_baseLineOnStart);
472 }; 520 };
473 521
474 editing.EditableLine.prototype = { 522 editing.EditableLine.prototype = {
475 /** @private */ 523 /** @private */
476 computeLineData_: function() { 524 computeLineData_: function(opt_baseLineOnStart) {
525 // Note that we calculate the line based only upon anchor or focus even if
526 // they do not fall on the same line. It is up to the caller to specify
527 // which end to base this line upon since it requires reasoning about two
528 // lines.
477 var nameLen = 0; 529 var nameLen = 0;
478 if (this.start_.node.name) 530 var lineBase = opt_baseLineOnStart ? this.start_ : this.end_;
479 nameLen = this.start_.node.name.length; 531 var lineExtend = opt_baseLineOnStart ? this.end_ : this.start_;
480 532
481 this.value_ = new Spannable(this.start_.node.name || '', this.start_); 533 if (lineBase.node.name)
482 if (this.start_.node == this.end_.node) 534 nameLen = lineBase.node.name.length;
483 this.value_.setSpan(this.end_, 0, nameLen); 535
536 this.value_ = new Spannable(lineBase.node.name || '', lineBase);
537 if (lineBase.node == lineExtend.node)
538 this.value_.setSpan(lineExtend, 0, nameLen);
484 539
485 // Initialize defaults. 540 // Initialize defaults.
486 this.lineStart_ = this.start_.node; 541 this.lineStart_ = lineBase.node;
487 this.lineEnd_ = this.lineStart_; 542 this.lineEnd_ = this.lineStart_;
488 this.startContainer_ = this.lineStart_.parent; 543 this.startContainer_ = this.lineStart_.parent;
489 this.lineStartContainer_ = this.lineStart_.parent; 544 this.lineStartContainer_ = this.lineStart_.parent;
545 this.lineEndContainer_ = this.lineStart_.parent;
490 546
491 // Annotate each chunk with its associated inline text box node. 547 // Annotate each chunk with its associated inline text box node.
492 this.value_.setSpan(this.lineStart_, 0, this.lineStart_.name.length); 548 this.value_.setSpan(this.lineStart_, 0, nameLen);
493 549
494 // If the current selection is not on an inline text box (e.g. an image), 550 // 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 551 // 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. 552 // pending the ability to show non-text leaf inline objects.
497 if (this.lineStart_.role != RoleType.INLINE_TEXT_BOX) 553 if (this.lineStart_.role != RoleType.INLINE_TEXT_BOX)
498 return; 554 return;
499 555
500 // Also, track their static text parents. 556 // Also, track their static text parents.
501 var parents = [this.startContainer_]; 557 var parents = [this.startContainer_];
502 558
(...skipping 23 matching lines...) Expand all
526 this.lineEnd_ = lineEnd; 582 this.lineEnd_ = lineEnd;
527 if (parents[parents.length - 1] != lineEnd.parent) 583 if (parents[parents.length - 1] != lineEnd.parent)
528 parents.push(this.lineEnd_.parent); 584 parents.push(this.lineEnd_.parent);
529 585
530 var annotation = lineEnd; 586 var annotation = lineEnd;
531 if (lineEnd == this.end_.node) 587 if (lineEnd == this.end_.node)
532 annotation = this.end_; 588 annotation = this.end_;
533 589
534 this.value_.append(new Spannable(lineEnd.name, annotation)); 590 this.value_.append(new Spannable(lineEnd.name, annotation));
535 } 591 }
592 this.lineEndContainer_ = this.lineEnd_.parent;
536 593
537 // Finally, annotate with all parent static texts as NodeSpan's so that 594 // Finally, annotate with all parent static texts as NodeSpan's so that
538 // braille routing can key properly into the node with an offset. 595 // braille routing can key properly into the node with an offset.
539 // Note that both line start and end needs to account for 596 // Note that both line start and end needs to account for
540 // potential offsets into the static texts as follows. 597 // potential offsets into the static texts as follows.
541 var textCountBeforeLineStart = 0, textCountAfterLineEnd = 0; 598 var textCountBeforeLineStart = 0, textCountAfterLineEnd = 0;
542 var finder = this.lineStart_; 599 var finder = this.lineStart_;
543 while (finder.previousSibling) { 600 while (finder.previousSibling) {
544 finder = finder.previousSibling; 601 finder = finder.previousSibling;
545 textCountBeforeLineStart += finder.name.length; 602 textCountBeforeLineStart += finder.name.length;
546 } 603 }
547 this.localLineStartContainerOffset_ = textCountBeforeLineStart; 604 this.localLineStartContainerOffset_ = textCountBeforeLineStart;
548 605
549 finder = this.lineEnd_; 606 finder = this.lineEnd_;
550 while (finder.nextSibling) { 607 while (finder.nextSibling) {
551 finder = finder.nextSibling; 608 finder = finder.nextSibling;
552 textCountAfterLineEnd += finder.name.length; 609 textCountAfterLineEnd += finder.name.length;
553 } 610 }
554 611
612 if (this.lineEndContainer_.name) {
613 this.localLineEndContainerOffset_ =
614 this.lineEndContainer_.name.length - textCountAfterLineEnd;
615 }
616
555 var len = 0; 617 var len = 0;
556 for (var i = 0; i < parents.length; i++) { 618 for (var i = 0; i < parents.length; i++) {
557 var parent = parents[i]; 619 var parent = parents[i];
558 620
559 if (!parent.name) 621 if (!parent.name)
560 continue; 622 continue;
561 623
562 var prevLen = len; 624 var prevLen = len;
563 625
564 var currentLen = parent.name.length; 626 var currentLen = parent.name.length;
(...skipping 20 matching lines...) Expand all
585 } catch (e) { 647 } catch (e) {
586 } 648 }
587 } 649 }
588 }, 650 },
589 651
590 /** 652 /**
591 * Gets the selection offset based on the text content of this line. 653 * Gets the selection offset based on the text content of this line.
592 * @return {number} 654 * @return {number}
593 */ 655 */
594 get startOffset() { 656 get startOffset() {
595 return this.value_.getSpanStart(this.start_) + this.start_.index; 657 // It is possible that the start cursor points to content before this line
658 // (e.g. in a multi-line selection).
659 try {
660 return this.value_.getSpanStart(this.start_) + this.start_.index;
661 } catch (e) {
662 // When that happens, fall back to the start of this line.
663 return 0;
664 }
596 }, 665 },
597 666
598 /** 667 /**
599 * Gets the selection offset based on the text content of this line. 668 * Gets the selection offset based on the text content of this line.
600 * @return {number} 669 * @return {number}
601 */ 670 */
602 get endOffset() { 671 get endOffset() {
603 return this.value_.getSpanStart(this.end_) + this.end_.index; 672 try {
673 return this.value_.getSpanStart(this.end_) + this.end_.index;
674 } catch (e) {
675 return this.value_.length;
676 }
604 }, 677 },
605 678
606 /** 679 /**
607 * Gets the selection offset based on the parent's text. 680 * Gets the selection offset based on the parent's text.
608 * The parent is expected to be static text. 681 * The parent is expected to be static text.
609 * @return {number} 682 * @return {number}
610 */ 683 */
611 get localStartOffset() { 684 get localStartOffset() {
612 return this.startOffset - this.containerStartOffset; 685 return this.startOffset - this.containerStartOffset;
613 }, 686 },
(...skipping 29 matching lines...) Expand all
643 * The text content of this line. 716 * The text content of this line.
644 * @return {string} The text of this line. 717 * @return {string} The text of this line.
645 */ 718 */
646 get text() { 719 get text() {
647 return this.value_.toString(); 720 return this.value_.toString();
648 }, 721 },
649 722
650 /** 723 /**
651 * Returns true if |otherLine| surrounds the same line as |this|. Note that 724 * Returns true if |otherLine| surrounds the same line as |this|. Note that
652 * the contents of the line might be different. 725 * the contents of the line might be different.
726 * @param {editing.EditableLine} otherLine
653 * @return {boolean} 727 * @return {boolean}
654 */ 728 */
655 equals: function(otherLine) { 729 isSameLine: function(otherLine) {
656 // Equality is intentionally loose here as any of the state nodes can be 730 // 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 731 // invalidated at any time. We rely upon the start/anchor of the line
658 // staying the same. 732 // staying the same.
659 return otherLine.lineStartContainer_ == this.lineStartContainer_ && 733 return (otherLine.lineStartContainer_ == this.lineStartContainer_ &&
660 otherLine.localLineStartContainerOffset_ == 734 otherLine.localLineStartContainerOffset_ ==
661 this.localLineStartContainerOffset_; 735 this.localLineStartContainerOffset_) ||
736 (otherLine.lineEndContainer_ == this.lineEndContainer_ &&
737 otherLine.localLineEndContainerOffset_ ==
738 this.localLineEndContainerOffset_);
739 },
740
741 /**
742 * Returns true if |otherLine| surrounds the same line as |this| and has the
743 * same selection.
744 * @param {editing.EditableLine} otherLine
745 * @return {boolean}
746 */
747 isSameLineAndSelection: function(otherLine) {
748 return this.isSameLine(otherLine) &&
749 this.startOffset == otherLine.startOffset &&
750 this.endOffset == otherLine.endOffset;
662 } 751 }
663 }; 752 };
664 753
665 }); 754 });
OLDNEW
« no previous file with comments | « no previous file | chrome/browser/resources/chromeos/chromevox/cvox2/background/editing_test.extjs » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698