Initial commit of new ChromeVox earcon engine.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / cvox2 / background / background.js
blob98cc4578abf5dd36e763d6b6e21916a269e6b04f
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.onEventDefault,
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    * Provides all feedback once a focus event fires.
458    * @param {Object} evt
459    */
460   onFocus: function(evt) {
461     // Invalidate any previous editable text handler state.
462     this.editableTextHandler = null;
464     var node = evt.target;
466     // It almost never makes sense to place focus directly on a rootWebArea.
467     if (node.role == RoleType.rootWebArea) {
468       // Try to find a focusable descendant.
469       node = AutomationUtil.findNodePost(node,
470                                          Dir.FORWARD,
471                                          AutomationPredicate.focused) || node;
473       // Fall back to the first leaf node in the document.
474       if (node.role == RoleType.rootWebArea) {
475         node = AutomationUtil.findNodePost(node,
476                                            Dir.FORWARD,
477                                            AutomationPredicate.leaf);
478       }
479     }
481     if (evt.target.role == RoleType.textField)
482       this.createEditableTextHandlerIfNeeded_(evt.target);
484     this.onEventDefault({target: node, type: 'focus'});
485   },
487   /**
488    * Provides all feedback once a load complete event fires.
489    * @param {Object} evt
490    */
491   onLoadComplete: function(evt) {
492     this.setupChromeVoxVariants_(evt.target.docUrl);
494     // Don't process nodes inside of web content if ChromeVox Next is inactive.
495     if (evt.target.root.role != RoleType.desktop &&
496         this.mode_ === ChromeVoxMode.CLASSIC)
497       return;
499     // If initial focus was already placed on this page (e.g. if a user starts
500     // tabbing before load complete), then don't move ChromeVox's position on
501     // the page.
502     if (this.currentRange_ &&
503         this.currentRange_.start.node.role != RoleType.rootWebArea &&
504         this.currentRange_.start.node.root.docUrl == evt.target.docUrl)
505       return;
507     var root = evt.target;
508     var webView = root;
509     while (webView && webView.role != RoleType.webView)
510       webView = webView.parent;
512     if (!webView || !webView.state.focused)
513       return;
515     var node = AutomationUtil.findNodePost(root,
516         Dir.FORWARD,
517         AutomationPredicate.leaf);
519     if (node)
520       this.currentRange_ = cursors.Range.fromNode(node);
522     if (this.currentRange_)
523       new Output().withSpeechAndBraille(
524               this.currentRange_, null, evt.type)
525           .go();
526   },
528   /**
529    * Provides all feedback once a text selection change event fires.
530    * @param {Object} evt
531    */
532   onTextOrTextSelectionChanged: function(evt) {
533     // Don't process nodes inside of web content if ChromeVox Next is inactive.
534     if (evt.target.root.role != RoleType.desktop &&
535         this.mode_ === ChromeVoxMode.CLASSIC)
536       return;
538     if (!evt.target.state.focused)
539       return;
541     if (!this.currentRange_) {
542       this.onEventDefault(evt);
543       this.currentRange_ = cursors.Range.fromNode(evt.target);
544     }
546     this.createEditableTextHandlerIfNeeded_(evt.target);
547     var textChangeEvent = new cvox.TextChangeEvent(
548         evt.target.value,
549         evt.target.textSelStart,
550         evt.target.textSelEnd,
551         true);  // triggered by user
553     this.editableTextHandler_.changed(textChangeEvent);
555     new Output().withBraille(
556             this.currentRange_, null, evt.type)
557         .go();
558   },
560   /**
561    * Provides all feedback once a value changed event fires.
562    * @param {Object} evt
563    */
564   onValueChanged: function(evt) {
565     // Don't process nodes inside of web content if ChromeVox Next is inactive.
566     if (evt.target.root.role != RoleType.desktop &&
567         this.mode_ === ChromeVoxMode.CLASSIC)
568       return;
570     if (!evt.target.state.focused)
571       return;
573     // Value change events fire on web text fields and text areas when pressing
574     // enter; suppress them.
575     if (!this.currentRange_ ||
576         evt.target.role != RoleType.textField) {
577       this.onEventDefault(evt);
578       this.currentRange_ = cursors.Range.fromNode(evt.target);
579     }
580   },
582   /**
583    * Called when the automation tree is changed.
584    * @param {chrome.automation.TreeChange} treeChange
585    */
586   onTreeChange: function(treeChange) {
587     if (this.mode_ === ChromeVoxMode.CLASSIC)
588       return;
590     var node = treeChange.target;
591     if (!node.containerLiveStatus)
592       return;
594     if (node.containerLiveRelevant.indexOf('additions') >= 0 &&
595         treeChange.type == 'nodeCreated')
596       this.outputLiveRegionChange_(node, null);
597     if (node.containerLiveRelevant.indexOf('text') >= 0 &&
598         treeChange.type == 'nodeChanged')
599       this.outputLiveRegionChange_(node, null);
600     if (node.containerLiveRelevant.indexOf('removals') >= 0 &&
601         treeChange.type == 'nodeRemoved')
602       this.outputLiveRegionChange_(node, '@live_regions_removed');
603   },
605   /**
606    * Given a node that needs to be spoken as part of a live region
607    * change and an additional optional format string, output the
608    * live region description.
609    * @param {!chrome.automation.AutomationNode} node The changed node.
610    * @param {?string} opt_prependFormatStr If set, a format string for
611    *     cvox2.Output to prepend to the output.
612    * @private
613    */
614   outputLiveRegionChange_: function(node, opt_prependFormatStr) {
615     var range = cursors.Range.fromNode(node);
616     var output = new Output();
617     if (opt_prependFormatStr) {
618       output.format(opt_prependFormatStr);
619     }
620     output.withSpeech(range, null, Output.EventType.NAVIGATE);
621     output.go();
622   },
624   /**
625    * Returns true if the url should have Classic running.
626    * @return {boolean}
627    * @private
628    */
629   shouldEnableClassicForUrl_: function(url) {
630     return this.mode_ != ChromeVoxMode.FORCE_NEXT &&
631         !this.isWhitelistedForCompat_(url) &&
632         !this.isWhitelistedForNext_(url);
633   },
635   /**
636    * @return {boolean}
637    * @private
638    */
639   isWhitelistedForCompat_: function(url) {
640     return url.indexOf('chrome://md-settings') != -1 ||
641           url.indexOf('chrome://oobe/login') != -1 ||
642           url.indexOf(
643               'https://accounts.google.com/embedded/setup/chromeos') === 0 ||
644           url === '';
645   },
647   /**
648    * @private
649    * @param {string} url
650    * @return {boolean} Whether the given |url| is whitelisted.
651    */
652   isWhitelistedForNext_: function(url) {
653     return this.whitelist_.some(function(item) {
654       return url.indexOf(item) != -1;
655     }.bind(this));
656   },
658   /**
659    * Setup ChromeVox variants.
660    * @param {string} url
661    * @private
662    */
663   setupChromeVoxVariants_: function(url) {
664     var mode = this.mode_;
665     if (mode != ChromeVoxMode.FORCE_NEXT) {
666       if (this.isWhitelistedForCompat_(url))
667         mode = ChromeVoxMode.COMPAT;
668       else if (this.isWhitelistedForNext_(url))
669         mode = ChromeVoxMode.NEXT;
670       else
671         mode = ChromeVoxMode.CLASSIC;
672     }
674     this.setChromeVoxMode(mode);
675   },
677   /**
678    * Disables classic ChromeVox in current web content.
679    */
680   disableClassicChromeVox_: function() {
681     cvox.ExtensionBridge.send({
682         message: 'SYSTEM_COMMAND',
683         command: 'killChromeVox'
684     });
685   },
687   /**
688    * Sets the current ChromeVox mode.
689    * @param {ChromeVoxMode} mode
690    */
691   setChromeVoxMode: function(mode) {
692     if (mode === ChromeVoxMode.NEXT ||
693         mode === ChromeVoxMode.COMPAT ||
694         mode === ChromeVoxMode.FORCE_NEXT) {
695       if (!chrome.commands.onCommand.hasListener(this.onGotCommand))
696         chrome.commands.onCommand.addListener(this.onGotCommand);
697     } else {
698       if (chrome.commands.onCommand.hasListener(this.onGotCommand))
699         chrome.commands.onCommand.removeListener(this.onGotCommand);
700     }
702     chrome.tabs.query({active: true}, function(tabs) {
703       if (mode === ChromeVoxMode.CLASSIC) {
704         // This case should do nothing because Classic gets injected by the
705         // extension system via our manifest. Once ChromeVox Next is enabled
706         // for tabs, re-enable.
707         // cvox.ChromeVox.injectChromeVoxIntoTabs(tabs);
708       } else {
709         // When in compat mode, if the focus is within the desktop tree proper,
710         // then do not disable content scripts.
711         if (this.currentRange_.start.node.root.role == 'desktop')
712           return;
714         this.disableClassicChromeVox_();
715       }
716     }.bind(this));
718     this.mode_ = mode;
719   },
721   /**
722    * @param {!cvox.Spannable} text
723    * @param {number} position
724    * @private
725    */
726   brailleRoutingCommand_: function(text, position) {
727     var actionNode = null;
728     var selectionSpan = null;
729     text.getSpans(position).forEach(function(span) {
730       if (span instanceof Output.SelectionSpan) {
731         selectionSpan = span;
732       } else if (span instanceof Output.NodeSpan) {
733         if (!actionNode ||
734             (text.getSpanEnd(actionNode) - text.getSpanStart(actionNode) >
735             text.getSpanEnd(span) - text.getSpanStart(span))) {
736           actionNode = span.node;
737         }
738       }
739     });
740     if (!actionNode)
741       return;
742     if (actionNode.role === RoleType.inlineTextBox)
743       actionNode = actionNode.parent;
744     actionNode.doDefault();
745     if (selectionSpan) {
746       var start = text.getSpanStart(selectionSpan);
747       actionNode.setSelection(position - start, position - start);
748     }
749   },
751   /**
752    * Create an editable text handler for the given node if needed.
753    * @param {Object} node
754    */
755   createEditableTextHandlerIfNeeded_: function(node) {
756     if (!this.editableTextHandler_ || node != this.currentRange_.start.node) {
757       var start = node.textSelStart;
758       var end = node.textSelEnd;
759       if (start > end) {
760         var tempOffset = end;
761         end = start;
762         start = tempOffset;
763       }
765       this.editableTextHandler_ =
766           new cvox.ChromeVoxEditableTextBase(
767               node.value,
768               start,
769               end,
770               node.state.protected,
771               cvox.ChromeVox.tts);
772     }
773   }
776 /** @type {Background} */
777 global.backgroundObj = new Background();
779 });  // goog.scope