Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / cvox2 / background / background.js
blob832456dd48f54ae4229dfbfff2960da67f46c364
1 // Copyright 2014 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.
5 /**
6  * @fileoverview The entry point for all ChromeVox2 related code for the
7  * background page.
8  */
10 goog.provide('Background');
11 goog.provide('global');
13 goog.require('AutomationPredicate');
14 goog.require('AutomationUtil');
15 goog.require('ClassicCompatibility');
16 goog.require('Output');
17 goog.require('Output.EventType');
18 goog.require('cursors.Cursor');
19 goog.require('cvox.BrailleKeyCommand');
20 goog.require('cvox.ChromeVoxEditableTextBase');
21 goog.require('cvox.ExtensionBridge');
22 goog.require('cvox.NavBraille');
24 goog.scope(function() {
25 var AutomationNode = chrome.automation.AutomationNode;
26 var Dir = AutomationUtil.Dir;
27 var EventType = chrome.automation.EventType;
28 var RoleType = chrome.automation.RoleType;
30 /**
31  * All possible modes ChromeVox can run.
32  * @enum {string}
33  */
34 var ChromeVoxMode = {
35   CLASSIC: 'classic',
36   COMPAT: 'compat',
37   NEXT: 'next',
38   FORCE_NEXT: 'force_next'
41 /**
42  * ChromeVox2 background page.
43  * @constructor
44  */
45 Background = function() {
46   /**
47    * A list of site substring patterns to use with ChromeVox next. Keep these
48    * strings relatively specific.
49    * @type {!Array<string>}
50    * @private
51    */
52   this.whitelist_ = ['chromevox_next_test'];
54   /**
55    * @type {cursors.Range}
56    * @private
57    */
58   this.currentRange_ = null;
60   /**
61    * Which variant of ChromeVox is active.
62    * @type {ChromeVoxMode}
63    * @private
64    */
65   this.mode_ = ChromeVoxMode.CLASSIC;
67   /** @type {!ClassicCompatibility} @private */
68   this.compat_ = new ClassicCompatibility();
70   // Manually bind all functions to |this|.
71   for (var func in this) {
72     if (typeof(this[func]) == 'function')
73       this[func] = this[func].bind(this);
74   }
76   /**
77    * Maps an automation event to its listener.
78    * @type {!Object<EventType, function(Object) : void>}
79    */
80   this.listeners_ = {
81     alert: this.onAlert,
82     focus: this.onFocus,
83     hover: this.onEventDefault,
84     loadComplete: this.onLoadComplete,
85     menuStart: this.onEventDefault,
86     menuEnd: this.onEventDefault,
87     textChanged: this.onTextOrTextSelectionChanged,
88     textSelectionChanged: this.onTextOrTextSelectionChanged,
89     valueChanged: this.onValueChanged
90   };
92   /**
93    * The object that speaks changes to an editable text field.
94    * @type {?cvox.ChromeVoxEditableTextBase}
95    */
96   this.editableTextHandler_ = null;
98   chrome.automation.getDesktop(this.onGotDesktop);
100   // Handle messages directed to the Next background page.
101   cvox.ExtensionBridge.addMessageListener(function(msg, port) {
102     var target = msg['target'];
103     var action = msg['action'];
105     switch (target) {
106       case 'next':
107         if (action == 'getIsClassicEnabled') {
108           var url = msg['url'];
109           var isClassicEnabled = this.shouldEnableClassicForUrl_(url);
110           port.postMessage({
111             target: 'next',
112             isClassicEnabled: isClassicEnabled
113           });
114         }
115         break;
116     }
117   }.bind(this));
120 Background.prototype = {
121   /** Forces ChromeVox Next to be active for all tabs. */
122   forceChromeVoxNextActive: function() {
123     this.setChromeVoxMode(ChromeVoxMode.FORCE_NEXT);
124   },
126   /**
127    * Handles all setup once a new automation tree appears.
128    * @param {chrome.automation.AutomationNode} desktop
129    */
130   onGotDesktop: function(desktop) {
131     // Register all automation event listeners.
132     for (var eventType in this.listeners_)
133       desktop.addEventListener(eventType, this.listeners_[eventType], true);
135     // Register a tree change observer.
136     chrome.automation.addTreeChangeObserver(this.onTreeChange);
138     // The focused state gets set on the containing webView node.
139     var webView = desktop.find({role: RoleType.webView,
140                                 state: {focused: true}});
141     if (webView) {
142       var root = webView.find({role: RoleType.rootWebArea});
143       if (root) {
144         this.onLoadComplete(
145             {target: root,
146              type: chrome.automation.EventType.loadComplete});
147       }
148     }
149   },
151   /**
152    * Handles chrome.commands.onCommand.
153    * @param {string} command
154    * @param {boolean=} opt_skipCompat Whether to skip compatibility checks.
155    */
156   onGotCommand: function(command, opt_skipCompat) {
157     if (!this.currentRange_)
158       return;
160     if (!opt_skipCompat && this.mode_ === ChromeVoxMode.COMPAT) {
161       if (this.compat_.onGotCommand(command))
162         return;
163     }
165     var current = this.currentRange_;
166     var dir = Dir.FORWARD;
167     var pred = null;
168     var predErrorMsg = undefined;
169     switch (command) {
170       case 'nextCharacter':
171         current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD);
172         break;
173       case 'previousCharacter':
174         current = current.move(cursors.Unit.CHARACTER, Dir.BACKWARD);
175         break;
176       case 'nextWord':
177         current = current.move(cursors.Unit.WORD, Dir.FORWARD);
178         break;
179       case 'previousWord':
180         current = current.move(cursors.Unit.WORD, Dir.BACKWARD);
181         break;
182       case 'nextLine':
183         current = current.move(cursors.Unit.LINE, Dir.FORWARD);
184         break;
185       case 'previousLine':
186         current = current.move(cursors.Unit.LINE, Dir.BACKWARD);
187         break;
188       case 'nextButton':
189         dir = Dir.FORWARD;
190         pred = AutomationPredicate.button;
191         predErrorMsg = 'no_next_button';
192         break;
193       case 'previousButton':
194         dir = Dir.BACKWARD;
195         pred = AutomationPredicate.button;
196         predErrorMsg = 'no_previous_button';
197         break;
198       case 'nextCheckBox':
199         dir = Dir.FORWARD;
200         pred = AutomationPredicate.checkBox;
201         predErrorMsg = 'no_next_checkbox';
202         break;
203       case 'previousCheckBox':
204         dir = Dir.BACKWARD;
205         pred = AutomationPredicate.checkBox;
206         predErrorMsg = 'no_previous_checkbox';
207         break;
208       case 'nextComboBox':
209         dir = Dir.FORWARD;
210         pred = AutomationPredicate.comboBox;
211         predErrorMsg = 'no_next_combo_box';
212         break;
213       case 'previousComboBox':
214         dir = Dir.BACKWARD;
215         pred = AutomationPredicate.comboBox;
216         predErrorMsg = 'no_previous_combo_box';
217         break;
218       case 'nextEditText':
219         dir = Dir.FORWARD;
220         pred = AutomationPredicate.editText;
221         predErrorMsg = 'no_next_edit_text';
222         break;
223       case 'previousEditText':
224         dir = Dir.BACKWARD;
225         pred = AutomationPredicate.editText;
226         predErrorMsg = 'no_previous_edit_text';
227         break;
228       case 'nextFormField':
229         dir = Dir.FORWARD;
230         pred = AutomationPredicate.formField;
231         predErrorMsg = 'no_next_form_field';
232         break;
233       case 'previousFormField':
234         dir = Dir.BACKWARD;
235         pred = AutomationPredicate.formField;
236         predErrorMsg = 'no_previous_form_field';
237         break;
238       case 'nextHeading':
239         dir = Dir.FORWARD;
240         pred = AutomationPredicate.heading;
241         predErrorMsg = 'no_next_heading';
242         break;
243       case 'previousHeading':
244         dir = Dir.BACKWARD;
245         pred = AutomationPredicate.heading;
246         predErrorMsg = 'no_previous_heading';
247         break;
248       case 'nextLink':
249         dir = Dir.FORWARD;
250         pred = AutomationPredicate.link;
251         predErrorMsg = 'no_next_link';
252         break;
253       case 'previousLink':
254         dir = Dir.BACKWARD;
255         pred = AutomationPredicate.link;
256         predErrorMsg = 'no_previous_link';
257         break;
258       case 'nextTable':
259         dir = Dir.FORWARD;
260         pred = AutomationPredicate.table;
261         predErrorMsg = 'no_next_table';
262         break;
263       case 'previousTable':
264         dir = Dir.BACKWARD;
265         pred = AutomationPredicate.table;
266         predErrorMsg = 'no_previous_table';
267         break;
268       case 'nextVisitedLink':
269         dir = Dir.FORWARD;
270         pred = AutomationPredicate.visitedLink;
271         predErrorMsg = 'no_next_visited_link';
272         break;
273       case 'previousVisitedLink':
274         dir = Dir.BACKWARD;
275         pred = AutomationPredicate.visitedLink;
276         predErrorMsg = 'no_previous_visited_link';
277         break;
278       case 'nextElement':
279         current = current.move(cursors.Unit.DOM_NODE, Dir.FORWARD);
280         break;
281       case 'previousElement':
282         current = current.move(cursors.Unit.DOM_NODE, Dir.BACKWARD);
283         break;
284       case 'goToBeginning':
285         var node =
286             AutomationUtil.findNodePost(current.start.node.root,
287                                         Dir.FORWARD,
288                                         AutomationPredicate.leaf);
289         if (node)
290           current = cursors.Range.fromNode(node);
291         break;
292       case 'goToEnd':
293         var node =
294             AutomationUtil.findNodePost(current.start.node.root,
295                                         Dir.BACKWARD,
296                                         AutomationPredicate.leaf);
297         if (node)
298           current = cursors.Range.fromNode(node);
299         break;
300       case 'doDefault':
301         if (this.currentRange_) {
302           var actionNode = this.currentRange_.start.node;
303           if (actionNode.role == RoleType.inlineTextBox)
304             actionNode = actionNode.parent;
305           actionNode.doDefault();
306         }
307         // Skip all other processing; if focus changes, we should get an event
308         // for that.
309         return;
310       case 'continuousRead':
311         global.isReadingContinuously = true;
312         var continueReading = function(prevRange) {
313           if (!global.isReadingContinuously || !this.currentRange_)
314             return;
316           new Output().withSpeechAndBraille(
317                   this.currentRange_, prevRange, Output.EventType.NAVIGATE)
318               .onSpeechEnd(function() { continueReading(prevRange); })
319               .go();
320           prevRange = this.currentRange_;
321           this.currentRange_ =
322               this.currentRange_.move(cursors.Unit.NODE, Dir.FORWARD);
324           if (!this.currentRange_ || this.currentRange_.equals(prevRange))
325             global.isReadingContinuously = false;
326         }.bind(this);
328         continueReading(null);
329         return;
330       case 'showContextMenu':
331         if (this.currentRange_) {
332           var actionNode = this.currentRange_.start.node;
333           if (actionNode.role == RoleType.inlineTextBox)
334             actionNode = actionNode.parent;
335           actionNode.showContextMenu();
336           return;
337         }
338         break;
339       case 'showOptionsPage':
340         var optionsPage = {url: 'chromevox/background/options.html'};
341         chrome.tabs.create(optionsPage);
342         break;
343     }
345     if (pred) {
346       var node = AutomationUtil.findNextNode(
347           current.getBound(dir).node, dir, pred);
349       if (node) {
350         current = cursors.Range.fromNode(node);
351       } else {
352         if (predErrorMsg) {
353           cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg(predErrorMsg),
354                                    cvox.QueueMode.FLUSH);
355         }
356         return;
357       }
358     }
360     if (current) {
361       // TODO(dtseng): Figure out what it means to focus a range.
362       var actionNode = current.start.node;
363       if (actionNode.role == RoleType.inlineTextBox)
364         actionNode = actionNode.parent;
365       actionNode.focus();
367       var prevRange = this.currentRange_;
368       this.currentRange_ = current;
370       new Output().withSpeechAndBraille(
371               this.currentRange_, prevRange, Output.EventType.NAVIGATE)
372           .go();
373     }
374   },
376   /**
377    * Handles a braille command.
378    * @param {!cvox.BrailleKeyEvent} evt
379    * @param {!cvox.NavBraille} content
380    * @return {boolean} True if evt was processed.
381    */
382   onBrailleKeyEvent: function(evt, content) {
383     if (this.mode_ === ChromeVoxMode.CLASSIC)
384       return false;
386     switch (evt.command) {
387       case cvox.BrailleKeyCommand.PAN_LEFT:
388         this.onGotCommand('previousElement', true);
389         break;
390       case cvox.BrailleKeyCommand.PAN_RIGHT:
391         this.onGotCommand('nextElement', true);
392         break;
393       case cvox.BrailleKeyCommand.LINE_UP:
394         this.onGotCommand('previousLine', true);
395         break;
396       case cvox.BrailleKeyCommand.LINE_DOWN:
397         this.onGotCommand('nextLine', true);
398         break;
399       case cvox.BrailleKeyCommand.TOP:
400         this.onGotCommand('goToBeginning', true);
401         break;
402       case cvox.BrailleKeyCommand.BOTTOM:
403         this.onGotCommand('goToEnd', true);
404         break;
405       case cvox.BrailleKeyCommand.ROUTING:
406         this.brailleRoutingCommand_(
407             content.text,
408             // Cast ok since displayPosition is always defined in this case.
409             /** @type {number} */ (evt.displayPosition));
410         break;
411       default:
412         return false;
413     }
414     return true;
415   },
417   /**
418    * Provides all feedback once ChromeVox's focus changes.
419    * @param {Object} evt
420    */
421   onEventDefault: function(evt) {
422     var node = evt.target;
424     if (!node)
425       return;
427     var prevRange = this.currentRange_;
429     this.currentRange_ = cursors.Range.fromNode(node);
431     // Check to see if we've crossed roots. Continue if we've crossed roots or
432     // are not within web content.
433     if (node.root.role == 'desktop' ||
434         !prevRange ||
435         prevRange.start.node.root != node.root)
436       this.setupChromeVoxVariants_(node.root.docUrl || '');
438     // Don't process nodes inside of web content if ChromeVox Next is inactive.
439     if (node.root.role != RoleType.desktop &&
440         this.mode_ === ChromeVoxMode.CLASSIC) {
441       chrome.accessibilityPrivate.setFocusRing([]);
442       return;
443     }
445     // Don't output if focused node hasn't changed.
446     if (prevRange &&
447         evt.type == 'focus' &&
448         this.currentRange_.equals(prevRange))
449       return;
451     new Output().withSpeechAndBraille(
452             this.currentRange_, prevRange, evt.type)
453         .go();
454   },
456   /**
457    * Makes an announcement without changing focus.
458    * @param {Object} evt
459    */
460   onAlert: function(evt) {
461     var node = evt.target;
462     if (!node)
463       return;
465     // Don't process nodes inside of web content if ChromeVox Next is inactive.
466     if (node.root.role != RoleType.desktop &&
467         this.mode_ === ChromeVoxMode.CLASSIC) {
468       return;
469     }
471     var range = cursors.Range.fromNode(node);
473     new Output().withSpeechAndBraille(range, null, evt.type).go();
474   },
476   /**
477    * Provides all feedback once a focus event fires.
478    * @param {Object} evt
479    */
480   onFocus: function(evt) {
481     // Invalidate any previous editable text handler state.
482     this.editableTextHandler = null;
484     var node = evt.target;
486     // It almost never makes sense to place focus directly on a rootWebArea.
487     if (node.role == RoleType.rootWebArea) {
488       // Try to find a focusable descendant.
489       node = AutomationUtil.findNodePost(node,
490                                          Dir.FORWARD,
491                                          AutomationPredicate.focused) || node;
493       // Fall back to the first leaf node in the document.
494       if (node.role == RoleType.rootWebArea) {
495         node = AutomationUtil.findNodePost(node,
496                                            Dir.FORWARD,
497                                            AutomationPredicate.leaf);
498       }
499     }
501     if (evt.target.role == RoleType.textField)
502       this.createEditableTextHandlerIfNeeded_(evt.target);
504     this.onEventDefault({target: node, type: 'focus'});
505   },
507   /**
508    * Provides all feedback once a load complete event fires.
509    * @param {Object} evt
510    */
511   onLoadComplete: function(evt) {
512     this.setupChromeVoxVariants_(evt.target.docUrl);
514     // Don't process nodes inside of web content if ChromeVox Next is inactive.
515     if (evt.target.root.role != RoleType.desktop &&
516         this.mode_ === ChromeVoxMode.CLASSIC)
517       return;
519     // If initial focus was already placed on this page (e.g. if a user starts
520     // tabbing before load complete), then don't move ChromeVox's position on
521     // the page.
522     if (this.currentRange_ &&
523         this.currentRange_.start.node.role != RoleType.rootWebArea &&
524         this.currentRange_.start.node.root.docUrl == evt.target.docUrl)
525       return;
527     var root = evt.target;
528     var webView = root;
529     while (webView && webView.role != RoleType.webView)
530       webView = webView.parent;
532     if (!webView || !webView.state.focused)
533       return;
535     var node = AutomationUtil.findNodePost(root,
536         Dir.FORWARD,
537         AutomationPredicate.leaf);
539     if (node)
540       this.currentRange_ = cursors.Range.fromNode(node);
542     if (this.currentRange_)
543       new Output().withSpeechAndBraille(
544               this.currentRange_, null, evt.type)
545           .go();
546   },
548   /**
549    * Provides all feedback once a text selection change event fires.
550    * @param {Object} evt
551    */
552   onTextOrTextSelectionChanged: function(evt) {
553     // Don't process nodes inside of web content if ChromeVox Next is inactive.
554     if (evt.target.root.role != RoleType.desktop &&
555         this.mode_ === ChromeVoxMode.CLASSIC)
556       return;
558     if (!evt.target.state.focused)
559       return;
561     if (!this.currentRange_) {
562       this.onEventDefault(evt);
563       this.currentRange_ = cursors.Range.fromNode(evt.target);
564     }
566     this.createEditableTextHandlerIfNeeded_(evt.target);
567     var textChangeEvent = new cvox.TextChangeEvent(
568         evt.target.value,
569         evt.target.textSelStart,
570         evt.target.textSelEnd,
571         true);  // triggered by user
573     this.editableTextHandler_.changed(textChangeEvent);
575     new Output().withBraille(
576             this.currentRange_, null, evt.type)
577         .go();
578   },
580   /**
581    * Provides all feedback once a value changed event fires.
582    * @param {Object} evt
583    */
584   onValueChanged: function(evt) {
585     // Don't process nodes inside of web content if ChromeVox Next is inactive.
586     if (evt.target.root.role != RoleType.desktop &&
587         this.mode_ === ChromeVoxMode.CLASSIC)
588       return;
590     if (!evt.target.state.focused)
591       return;
593     // Value change events fire on web text fields and text areas when pressing
594     // enter; suppress them.
595     if (!this.currentRange_ ||
596         evt.target.role != RoleType.textField) {
597       this.onEventDefault(evt);
598       this.currentRange_ = cursors.Range.fromNode(evt.target);
599     }
600   },
602   /**
603    * Called when the automation tree is changed.
604    * @param {chrome.automation.TreeChange} treeChange
605    */
606   onTreeChange: function(treeChange) {
607     if (this.mode_ === ChromeVoxMode.CLASSIC)
608       return;
610     var node = treeChange.target;
611     if (!node.containerLiveStatus)
612       return;
614     if (node.containerLiveRelevant.indexOf('additions') >= 0 &&
615         treeChange.type == 'nodeCreated')
616       this.outputLiveRegionChange_(node, null);
617     if (node.containerLiveRelevant.indexOf('text') >= 0 &&
618         treeChange.type == 'nodeChanged')
619       this.outputLiveRegionChange_(node, null);
620     if (node.containerLiveRelevant.indexOf('removals') >= 0 &&
621         treeChange.type == 'nodeRemoved')
622       this.outputLiveRegionChange_(node, '@live_regions_removed');
623   },
625   /**
626    * Given a node that needs to be spoken as part of a live region
627    * change and an additional optional format string, output the
628    * live region description.
629    * @param {!chrome.automation.AutomationNode} node The changed node.
630    * @param {?string} opt_prependFormatStr If set, a format string for
631    *     cvox2.Output to prepend to the output.
632    * @private
633    */
634   outputLiveRegionChange_: function(node, opt_prependFormatStr) {
635     var range = cursors.Range.fromNode(node);
636     var output = new Output();
637     if (opt_prependFormatStr) {
638       output.format(opt_prependFormatStr);
639     }
640     output.withSpeech(range, null, Output.EventType.NAVIGATE);
641     output.go();
642   },
644   /**
645    * Returns true if the url should have Classic running.
646    * @return {boolean}
647    * @private
648    */
649   shouldEnableClassicForUrl_: function(url) {
650     return this.mode_ != ChromeVoxMode.FORCE_NEXT &&
651         !this.isWhitelistedForCompat_(url) &&
652         !this.isWhitelistedForNext_(url);
653   },
655   /**
656    * @return {boolean}
657    * @private
658    */
659   isWhitelistedForCompat_: function(url) {
660     return url.indexOf('chrome://md-settings') != -1 ||
661           url.indexOf('chrome://oobe/login') != -1 ||
662           url.indexOf(
663               'https://accounts.google.com/embedded/setup/chromeos') === 0 ||
664           url === '';
665   },
667   /**
668    * @private
669    * @param {string} url
670    * @return {boolean} Whether the given |url| is whitelisted.
671    */
672   isWhitelistedForNext_: function(url) {
673     return this.whitelist_.some(function(item) {
674       return url.indexOf(item) != -1;
675     }.bind(this));
676   },
678   /**
679    * Setup ChromeVox variants.
680    * @param {string} url
681    * @private
682    */
683   setupChromeVoxVariants_: function(url) {
684     var mode = this.mode_;
685     if (mode != ChromeVoxMode.FORCE_NEXT) {
686       if (this.isWhitelistedForCompat_(url))
687         mode = ChromeVoxMode.COMPAT;
688       else if (this.isWhitelistedForNext_(url))
689         mode = ChromeVoxMode.NEXT;
690       else
691         mode = ChromeVoxMode.CLASSIC;
692     }
694     this.setChromeVoxMode(mode);
695   },
697   /**
698    * Disables classic ChromeVox in current web content.
699    */
700   disableClassicChromeVox_: function() {
701     cvox.ExtensionBridge.send({
702         message: 'SYSTEM_COMMAND',
703         command: 'killChromeVox'
704     });
705   },
707   /**
708    * Sets the current ChromeVox mode.
709    * @param {ChromeVoxMode} mode
710    */
711   setChromeVoxMode: function(mode) {
712     if (mode === ChromeVoxMode.NEXT ||
713         mode === ChromeVoxMode.COMPAT ||
714         mode === ChromeVoxMode.FORCE_NEXT) {
715       if (!chrome.commands.onCommand.hasListener(this.onGotCommand))
716         chrome.commands.onCommand.addListener(this.onGotCommand);
717     } else {
718       if (chrome.commands.onCommand.hasListener(this.onGotCommand))
719         chrome.commands.onCommand.removeListener(this.onGotCommand);
720     }
722     chrome.tabs.query({active: true}, function(tabs) {
723       if (mode === ChromeVoxMode.CLASSIC) {
724         // This case should do nothing because Classic gets injected by the
725         // extension system via our manifest. Once ChromeVox Next is enabled
726         // for tabs, re-enable.
727         // cvox.ChromeVox.injectChromeVoxIntoTabs(tabs);
728       } else {
729         // When in compat mode, if the focus is within the desktop tree proper,
730         // then do not disable content scripts.
731         if (this.currentRange_.start.node.root.role == 'desktop')
732           return;
734         this.disableClassicChromeVox_();
735       }
736     }.bind(this));
738     this.mode_ = mode;
739   },
741   /**
742    * @param {!cvox.Spannable} text
743    * @param {number} position
744    * @private
745    */
746   brailleRoutingCommand_: function(text, position) {
747     var actionNode = null;
748     var selectionSpan = null;
749     text.getSpans(position).forEach(function(span) {
750       if (span instanceof Output.SelectionSpan) {
751         selectionSpan = span;
752       } else if (span instanceof Output.NodeSpan) {
753         if (!actionNode ||
754             (text.getSpanEnd(actionNode) - text.getSpanStart(actionNode) >
755             text.getSpanEnd(span) - text.getSpanStart(span))) {
756           actionNode = span.node;
757         }
758       }
759     });
760     if (!actionNode)
761       return;
762     if (actionNode.role === RoleType.inlineTextBox)
763       actionNode = actionNode.parent;
764     actionNode.doDefault();
765     if (selectionSpan) {
766       var start = text.getSpanStart(selectionSpan);
767       actionNode.setSelection(position - start, position - start);
768     }
769   },
771   /**
772    * Create an editable text handler for the given node if needed.
773    * @param {Object} node
774    */
775   createEditableTextHandlerIfNeeded_: function(node) {
776     if (!this.editableTextHandler_ || node != this.currentRange_.start.node) {
777       var start = node.textSelStart;
778       var end = node.textSelEnd;
779       if (start > end) {
780         var tempOffset = end;
781         end = start;
782         start = tempOffset;
783       }
785       this.editableTextHandler_ =
786           new cvox.ChromeVoxEditableTextBase(
787               node.value,
788               start,
789               end,
790               node.state.protected,
791               cvox.ChromeVox.tts);
792     }
793   }
796 /** @type {Background} */
797 global.backgroundObj = new Background();
799 });  // goog.scope