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

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

Issue 2903973002: Rich editable text implementation using spannables (Closed)
Patch Set: Text style. 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 209 matching lines...) Expand 10 before | Expand all | Expand 10 after
220 * @param {!AutomationNode} node 220 * @param {!AutomationNode} node
221 * @extends {AutomationEditableText} 221 * @extends {AutomationEditableText}
222 */ 222 */
223 function AutomationRichEditableText(node) { 223 function AutomationRichEditableText(node) {
224 AutomationEditableText.call(this, node); 224 AutomationEditableText.call(this, node);
225 225
226 var root = this.node_.root; 226 var root = this.node_.root;
227 if (!root || !root.anchorObject || !root.focusObject) 227 if (!root || !root.anchorObject || !root.focusObject)
228 return; 228 return;
229 229
230 this.range = new cursors.Range( 230 this.line_ = new editing.EditableLine(
231 new cursors.Cursor(root.anchorObject, root.anchorOffset || 0), 231 root.anchorObject, root.anchorOffset, root.focusObject, root.focusOffset);
232 new cursors.Cursor(root.focusObject, root.focusOffset || 0));
233 } 232 }
234 233
235 AutomationRichEditableText.prototype = { 234 AutomationRichEditableText.prototype = {
236 __proto__: AutomationEditableText.prototype, 235 __proto__: AutomationEditableText.prototype,
237 236
238 /** @override */ 237 /** @override */
239 onUpdate: function() { 238 onUpdate: function() {
240 var root = this.node_.root; 239 var root = this.node_.root;
241 if (!root.anchorObject || !root.focusObject) 240 if (!root.anchorObject || !root.focusObject)
242 return; 241 return;
243 242
244 var cur = new cursors.Range( 243 var cur = new editing.EditableLine(
245 new cursors.Cursor(root.anchorObject, root.anchorOffset || 0), 244 root.anchorObject, root.anchorOffset || 0,
246 new cursors.Cursor(root.focusObject, root.focusOffset || 0)); 245 root.focusObject, root.focusOffset || 0);
247 var prev = this.range; 246 var prev = this.line_;
247 this.line_ = cur;
248 248
249 this.range = cur; 249 if (prev.equals(cur)) {
250 250 // Collapsed cursor.
251 if (prev.start.node == cur.start.node &&
252 prev.end.node == cur.end.node &&
253 cur.start.node == cur.end.node) {
254 // Plain text: diff the two positions.
255 this.changed(new cvox.TextChangeEvent( 251 this.changed(new cvox.TextChangeEvent(
256 root.anchorObject.name || '', 252 cur.text || '',
257 root.anchorOffset || 0, 253 cur.startOffset || 0,
258 root.focusOffset || 0, 254 cur.endOffset || 0,
259 true)); 255 true));
260 256
261 var lineIndex = this.getLineIndex(this.start); 257 var value = cur.value_;
262 var brailleLineStart = this.getLineStart(lineIndex); 258 value.setSpan(new cvox.ValueSpan(0), 0, cur.value_.length);
263 var brailleLineEnd = this.getLineEnd(lineIndex); 259 value.setSpan(
264 var buff = new Spannable(this.value); 260 new cvox.ValueSelectionSpan(), cur.startOffset, cur.endOffset);
265 buff.setSpan(new cvox.ValueSpan(0), brailleLineStart, brailleLineEnd); 261 cvox.ChromeVox.braille.write(new cvox.NavBraille({text: value,
262 startIndex: cur.startOffset,
263 endIndex: cur.endOffset}));
266 264
267 var selStart = this.start - brailleLineStart; 265 // Finally, queue up any text markers/styles at bounds.
268 var selEnd = this.end - brailleLineStart; 266 var container = cur.lineContainer_;
269 buff.setSpan(new cvox.ValueSelectionSpan(), selStart, selEnd); 267 if (!container)
270 cvox.ChromeVox.braille.write(new cvox.NavBraille({text: buff, 268 return;
271 startIndex: selStart, 269
272 endIndex: selEnd})); 270 if (container.markerTypes) {
271 // Only consider markers that start or end at the selection bounds.
272 var markerStartIndex = -1, markerEndIndex = -1;
273 var localStartOffset = cur.localStartOffset;
274 for (var i = 0; i < container.markerStarts.length; i++) {
275 if (container.markerStarts[i] == localStartOffset) {
276 markerStartIndex = i;
277 break;
278 }
279 }
280
281 var localEndOffset = cur.localEndOffset;
282 for (var i = 0; i < container.markerEnds.length; i++) {
283 if (container.markerEnds[i] == localEndOffset) {
284 markerEndIndex = i;
285 break;
286 }
287 }
288
289 if (markerStartIndex > -1)
290 this.speakTextMarker_(container.markerTypes[markerStartIndex]);
291
292 if (markerEndIndex > -1)
293 this.speakTextMarker_(container.markerTypes[markerEndIndex], true);
294 }
295
296 if (!container.textStyle)
297 return;
298
299 // Start of the container.
300 if (cur.containerStartOffset == cur.startOffset)
301 this.speakTextStyle_(container.textStyle);
302 else if (cur.containerEndOffset == cur.endOffset)
303 this.speakTextStyle_(container.textStyle, true);
304
273 return; 305 return;
274 } else {
275 // Rich text:
276 // If the position is collapsed, expand to the current line.
277 var start = cur.start;
278 var end = cur.end;
279 if (start.equals(end)) {
280 start = start.move(Unit.LINE, Movement.BOUND, Dir.BACKWARD);
281 end = end.move(Unit.LINE, Movement.BOUND, Dir.FORWARD);
282 }
283 var range = new cursors.Range(start, end);
284 var output = new Output().withRichSpeechAndBraille(
285 range, prev, Output.EventType.NAVIGATE);
286
287 // This position is not describable.
288 if (!output.hasSpeech) {
289 cvox.ChromeVox.tts.speak('blank', cvox.QueueMode.CATEGORY_FLUSH);
290 cvox.ChromeVox.braille.write(
291 new cvox.NavBraille({text: '', startIndex: 0, endIndex: 0}));
292 } else {
293 output.go();
294 }
295 } 306 }
296 307
297 // Keep the other members in sync for any future editable text base state 308 // Just output the current line.
298 // machine changes. 309 if (!cur.lineStart_ || !cur.lineEnd_)
299 this.value = cur.start.node.name || ''; 310 return;
300 this.start = cur.start.index; 311 var prevRange = null;
301 this.end = cur.start.index; 312 if (prev.lineStart_ && prev.lineEnd_) {
313 prevRange = new Range(
314 Cursor.fromNode(prev.lineStart_), Cursor.fromNode(prev.lineEnd_));
315 }
316
317 new Output().withRichSpeechAndBraille(new Range(
318 Cursor.fromNode(cur.lineStart_), Cursor.fromNode(cur.lineEnd_)),
319 prevRange,
320 Output.EventType.NAVIGATE).go();
321 },
322
323 /**
324 * @param {number} markerType
325 * @param {boolean=} opt_end
326 * @private
327 */
328 speakTextMarker_: function(markerType, opt_end) {
329 var msgs = [];
330 if (markerType & 1)
dmazzoni 2017/06/07 17:30:13 Maybe add constants for these at the top of the fi
331 msgs.push(opt_end ? 'misspelling_end' : 'misspelling_start');
332 if (markerType & 2)
333 msgs.push(opt_end ? 'grammar_end' : 'grammar_start');
334 if (markerType & 4)
335 msgs.push(opt_end ? 'text_match_end' : 'text_match_start');
336
337 if (msgs.length) {
338 msgs.forEach(function(msg) {
339 cvox.ChromeVox.tts.speak(Msgs.getMsg(msg),
340 cvox.QueueMode.QUEUE,
341 cvox.AbstractTts.PERSONALITY_ANNOTATION);
342 });
343 }
344 },
345
346 /**
347 * @param {number} style
348 * @param {boolean=} opt_end
349 * @private
350 */
351 speakTextStyle_: function(style, opt_end) {
352 var msgs = [];
353 if (style & 1)
354 msgs.push(opt_end ? 'bold_end' : 'bold_start');
355 if (style & 2)
356 msgs.push(opt_end ? 'italic_end' : 'italic_start');
357 if (style & 4)
358 msgs.push(opt_end ? 'underline_end' : 'underline_start');
359 if (style & 8)
360 msgs.push(opt_end ? 'line_through_end' : 'line_through_start');
361
362 if (msgs.length) {
363 msgs.forEach(function(msg) {
364 cvox.ChromeVox.tts.speak(Msgs.getMsg(msg),
365 cvox.QueueMode.QUEUE,
366 cvox.AbstractTts.PERSONALITY_ANNOTATION);
367 });
368 }
302 }, 369 },
303 370
304 /** @override */ 371 /** @override */
305 describeSelectionChanged: function(evt) { 372 describeSelectionChanged: function(evt) {
306 // Ignore end of text announcements. 373 // Ignore end of text announcements.
307 if ((this.start + 1) == evt.start && evt.start == this.value.length) 374 if ((this.start + 1) == evt.start && evt.start == this.value.length)
308 return; 375 return;
309 376
310 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged.call( 377 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged.call(
311 this, evt); 378 this, evt);
312 }, 379 },
313 380
314 /** @override */ 381 /** @override */
315 getLineIndex: function(charIndex) { 382 getLineIndex: function(charIndex) {
316 var breaks = this.getLineBreaks_(); 383 return 0;
317 var index = 0;
318 while (index < breaks.length && breaks[index] <= charIndex)
319 ++index;
320 return index;
321 }, 384 },
322 385
323 /** @override */ 386 /** @override */
324 getLineStart: function(lineIndex) { 387 getLineStart: function(lineIndex) {
325 if (lineIndex == 0) 388 return 0;
326 return 0;
327 var breaks = this.getLineBreaks_();
328 return breaks[lineIndex - 1] ||
329 this.node_.root.focusObject.value.length;
330 }, 389 },
331 390
332 /** @override */ 391 /** @override */
333 getLineEnd: function(lineIndex) { 392 getLineEnd: function(lineIndex) {
334 var breaks = this.getLineBreaks_(); 393 return this.value.length;
335 var value = this.node_.root.focusObject.name;
336 if (lineIndex >= breaks.length)
337 return value.length;
338 return breaks[lineIndex];
339 }, 394 },
340 395
341 /** @override */ 396 /** @override */
342 getLineBreaks_: function() { 397 getLineBreaks_: function() {
343 return this.node_.root.focusObject.lineStartOffsets || []; 398 return [];
344 } 399 }
345 }; 400 };
346 401
347 /** 402 /**
348 * @param {!AutomationNode} node The root editable node, i.e. the root of a 403 * @param {!AutomationNode} node The root editable node, i.e. the root of a
349 * contenteditable subtree or a text field. 404 * contenteditable subtree or a text field.
350 * @return {editing.TextEditHandler} 405 * @return {editing.TextEditHandler}
351 */ 406 */
352 editing.TextEditHandler.createForNode = function(node) { 407 editing.TextEditHandler.createForNode = function(node) {
353 var rootFocusedEditable = null; 408 var rootFocusedEditable = null;
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
389 cvox.BrailleBackground.getInstance().getTranslatorManager().refresh( 444 cvox.BrailleBackground.getInstance().getTranslatorManager().refresh(
390 localStorage['brailleTable']); 445 localStorage['brailleTable']);
391 } 446 }
392 }; 447 };
393 448
394 /** 449 /**
395 * @private {ChromeVoxStateObserver} 450 * @private {ChromeVoxStateObserver}
396 */ 451 */
397 editing.observer_ = new editing.EditingChromeVoxStateObserver(); 452 editing.observer_ = new editing.EditingChromeVoxStateObserver();
398 453
454 /**
455 * An EditableLine encapsulates all data concerning a line in the automation
456 * tree necessary to provide output.
457 * @constructor
458 */
459 editing.EditableLine = function(startNode, startIndex, endNode, endIndex) {
460 /** @private {!Cursor} */
461 this.start_ = new Cursor(startNode, startIndex);
462 this.start_ = this.start_.deepEquivalent || this.start_;
463
464 /** @private {!Cursor} */
465 this.end_ = new Cursor(endNode, endIndex);
466 this.end_ = this.end_.deepEquivalent || this.end_;
467 /** @private {number} */
468 this.localContainerStartOffset_ = startIndex;
469
470 // Computed members.
471 /** @private {Spannable} */
472 this.value_;
473 /** @private {AutomationNode} */
474 this.lineStart_;
475 /** @private {AutomationNode} */
476 this.lineEnd_;
477 /** @private {AutomationNode|undefined} */
478 this.lineContainer_;
479
480 this.computeLineData_();
481 };
482
483 editing.EditableLine.prototype = {
484 /** @private */
485 computeLineData_: function() {
486 this.value_ = new Spannable(this.start_.node.name, this.start_);
487 if (this.start_.node == this.end_.node)
488 this.value_.setSpan(this.end_, 0, this.start_.node.name.length);
489
490 this.lineStart_ = this.start_.node;
491 this.lineEnd_ = this.lineStart_;
492 this.lineContainer_ = this.lineStart_.parent;
493
494 // Annotate each chunk with its associated node.
495 this.value_.setSpan(this.lineStart_, 0, this.lineStart_.name.length);
496
497 while (this.lineStart_.previousOnLine) {
498 this.lineStart_ = this.lineStart_.previousOnLine;
499 var prepend = new Spannable(this.lineStart_.name, this.lineStart_);
500 prepend.append(this.value_);
501 this.value_ = prepend;
502 }
503
504 while (this.lineEnd_.nextOnLine) {
505 this.lineEnd_ = this.lineEnd_.nextOnLine;
506
507 var annotation = this.lineEnd_;
508 if (this.lineEnd_ == this.end_.node)
509 annotation = this.end_;
510
511 this.value_.append(new Spannable(this.lineEnd_.name, annotation));
512 }
513 },
514
515 /**
516 * Gets the selection offset based on the text content of this line.
517 * @return {number}
518 */
519 get startOffset() {
520 return this.value_.getSpanStart(this.start_) + this.start_.index;
521 },
522
523 /**
524 * Gets the selection offset based on the text content of this line.
525 * @return {number}
526 */
527 get endOffset() {
528 return this.value_.getSpanStart(this.end_) + this.end_.index;
529 },
530
531 /**
532 * Gets the selection offset based on the parent's text.
533 * @return {number}
534 */
535 get localStartOffset() {
536 return this.startOffset - this.containerStartOffset;
537 },
538
539 /**
540 * Gets the selection offset based on the parent's text.
541 * @return {number}
542 */
543 get localEndOffset() {
544 return this.endOffset - this.containerStartOffset;
545 },
546
547 /**
548 * Gets the start offset of the line, relative to the container's text.
549 * @return {number}
550 */
551 get containerLineStartOffset() {
552 // When the container start offset is larger, the start offset is usually
553 // part of a line wrap, so the two offsets differ.
554 // When the container start offset is smaller, there is usually more line
555 // content before the container accounted for in start offset.
556 // Taking the difference either way will give us the offset at which the
557 // line begins.
558 return Math.abs(this.localContainerStartOffset_ - this.startOffset);
559 },
560
561 /**
562 * Gets the start offset of the container, relative to the line text content.
563 * @return {number}
564 */
565 get containerStartOffset() {
566 return this.value_.getSpanStart(this.lineContainer_.firstChild);
567 },
568
569 /**
570 * Gets the end offset of the container, relative to the line text content.
571 * @return {number}
572 */
573 get containerEndOffset() {
574 return this.value_.getSpanEnd(this.lineContainer_.lastChild) - 1;
575 },
576
577 /**
578 * @return {string} The text of this line.
579 */
580 get text() {
581 return this.value_.toString();
582 },
583
584 /**
585 * Returns true if |otherLine| surrounds the same line as |this|. Note that
586 * the contents of the line might be different.
587 * @return {boolean}
588 */
589 equals: function(otherLine) {
590 // Equality is intentionally loose here as any of the state nodes can be
591 // invalidated at any time.
592 return (otherLine.lineStart_ == this.lineStart_ &&
593 otherLine.lineEnd_ == this.lineEnd_) ||
594 (otherLine.lineContainer_ == this.lineContainer_ &&
595 otherLine.containerLineStartOffset == this.containerLineStartOffset);
596 }
597 };
598
399 }); 599 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698