Home | History | Annotate | Download | only in caretbrowsing
      1 /* Copyright (c) 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. */
      4 
      5 /**
      6  * @fileoverview Caret browsing content script, runs in each frame.
      7  *
      8  * The behavior is based on Mozilla's spec whenever possible:
      9  *   http://www.mozilla.org/access/keyboard/proposal
     10  *
     11  * The one exception is that Esc is used to escape out of a form control,
     12  * rather than their proposed key (which doesn't seem to work in the
     13  * latest Firefox anyway).
     14  *
     15  * Some details about how Chrome selection works, which will help in
     16  * understanding the code:
     17  *
     18  * The Selection object (window.getSelection()) has four components that
     19  * completely describe the state of the caret or selection:
     20  *
     21  * base and anchor: this is the start of the selection, the fixed point.
     22  * extent and focus: this is the end of the selection, the part that
     23  *     moves when you hold down shift and press the left or right arrows.
     24  *
     25  * When the selection is a cursor, the base, anchor, extent, and focus are
     26  * all the same.
     27  *
     28  * There's only one time when the base and anchor are not the same, or the
     29  * extent and focus are not the same, and that's when the selection is in
     30  * an ambiguous state - i.e. it's not clear which edge is the focus and which
     31  * is the anchor. As an example, if you double-click to select a word, then
     32  * the behavior is dependent on your next action. If you press Shift+Right,
     33  * the right edge becomes the focus. But if you press Shift+Left, the left
     34  * edge becomes the focus.
     35  *
     36  * When the selection is in an ambiguous state, the base and extent are set
     37  * to the position where the mouse clicked, and the anchor and focus are set
     38  * to the boundaries of the selection.
     39  *
     40  * The only way to set the selection and give it direction is to use
     41  * the non-standard Selection.setBaseAndExtent method. If you try to use
     42  * Selection.addRange(), the anchor will always be on the left and the focus
     43  * will always be on the right, making it impossible to manipulate
     44  * selections that move from right to left.
     45  *
     46  * Finally, Chrome will throw an exception if you try to set an invalid
     47  * selection - a selection where the left and right edges are not the same,
     48  * but it doesn't span any visible characters. A common example is that
     49  * there are often many whitespace characters in the DOM that are not
     50  * visible on the page; trying to select them will fail. Another example is
     51  * any node that's invisible or not displayed.
     52  *
     53  * While there are probably many possible methods to determine what is
     54  * selectable, this code uses the method of determining if there's a valid
     55  * bounding box for the range or not - keep moving the cursor forwards until
     56  * the range from the previous position and candidate next position has a
     57  * valid bounding box.
     58  */
     59 
     60 /**
     61  * Return whether a node is focusable. This includes nodes whose tabindex
     62  * attribute is set to "-1" explicitly - these nodes are not in the tab
     63  * order, but they should still be focused if the user navigates to them
     64  * using linear or smart DOM navigation.
     65  *
     66  * Note that when the tabIndex property of an Element is -1, that doesn't
     67  * tell us whether the tabIndex attribute is missing or set to "-1" explicitly,
     68  * so we have to check the attribute.
     69  *
     70  * @param {Object} targetNode The node to check if it's focusable.
     71  * @return {boolean} True if the node is focusable.
     72  */
     73 function isFocusable(targetNode) {
     74   if (!targetNode || typeof(targetNode.tabIndex) != 'number') {
     75     return false;
     76   }
     77 
     78   if (targetNode.tabIndex >= 0) {
     79     return true;
     80   }
     81 
     82   if (targetNode.hasAttribute &&
     83       targetNode.hasAttribute('tabindex') &&
     84       targetNode.getAttribute('tabindex') == '-1') {
     85     return true;
     86   }
     87 
     88   return false;
     89 }
     90 
     91 /**
     92  * Determines whether or not a node is or is the descendant of another node.
     93  *
     94  * @param {Object} node The node to be checked.
     95  * @param {Object} ancestor The node to see if it's a descendant of.
     96  * @return {boolean} True if the node is ancestor or is a descendant of it.
     97  */
     98 function isDescendantOfNode(node, ancestor) {
     99   while (node && ancestor) {
    100     if (node.isSameNode(ancestor)) {
    101       return true;
    102     }
    103     node = node.parentNode;
    104   }
    105   return false;
    106 }
    107 
    108 
    109 
    110 /**
    111  * The class handling the Caret Browsing implementation in the page.
    112  * Installs a keydown listener that always responds to the F7 key,
    113  * sets up communication with the background page, and then when caret
    114  * browsing is enabled, response to various key events to move the caret
    115  * or selection within the text content of the document. Uses the native
    116  * Chrome selection wherever possible, but displays its own flashing
    117  * caret using a DIV because there's no native caret available.
    118  * @constructor
    119  */
    120 var CaretBrowsing = function() {};
    121 
    122 /**
    123  * Is caret browsing enabled?
    124  * @type {boolean}
    125  */
    126 CaretBrowsing.isEnabled = false;
    127 
    128 /**
    129  * Keep it enabled even when flipped off (for the options page)?
    130  * @type {boolean}
    131  */
    132 CaretBrowsing.forceEnabled = false;
    133 
    134 /**
    135  * What to do when the caret appears?
    136  * @type {string}
    137  */
    138 CaretBrowsing.onEnable;
    139 
    140 /**
    141  * What to do when the caret jumps?
    142  * @type {string}
    143  */
    144 CaretBrowsing.onJump;
    145 
    146 /**
    147  * Is this window / iframe focused? We won't show the caret if not,
    148  * especially so that carets aren't shown in two iframes of the same
    149  * tab.
    150  * @type {boolean}
    151  */
    152 CaretBrowsing.isWindowFocused = false;
    153 
    154 /**
    155  * Is the caret actually visible? This is true only if isEnabled and
    156  * isWindowFocused are both true.
    157  * @type {boolean}
    158  */
    159 CaretBrowsing.isCaretVisible = false;
    160 
    161 /**
    162  * The actual caret element, an absolute-positioned flashing line.
    163  * @type {Element}
    164  */
    165 CaretBrowsing.caretElement;
    166 
    167 /**
    168  * The x-position of the caret, in absolute pixels.
    169  * @type {number}
    170  */
    171 CaretBrowsing.caretX = 0;
    172 
    173 /**
    174  * The y-position of the caret, in absolute pixels.
    175  * @type {number}
    176  */
    177 CaretBrowsing.caretY = 0;
    178 
    179 /**
    180  * The width of the caret in pixels.
    181  * @type {number}
    182  */
    183 CaretBrowsing.caretWidth = 0;
    184 
    185 /**
    186  * The height of the caret in pixels.
    187  * @type {number}
    188  */
    189 CaretBrowsing.caretHeight = 0;
    190 
    191 /**
    192  * The foregroundc color.
    193  * @type {string}
    194  */
    195 CaretBrowsing.caretForeground = '#000';
    196 
    197 /**
    198  * The backgroundc color.
    199  * @type {string}
    200  */
    201 CaretBrowsing.caretBackground = '#fff';
    202 
    203 /**
    204  * Is the selection collapsed, i.e. are the start and end locations
    205  * the same? If so, our blinking caret image is shown; otherwise
    206  * the Chrome selection is shown.
    207  * @type {boolean}
    208  */
    209 CaretBrowsing.isSelectionCollapsed = false;
    210 
    211 /**
    212  * The id returned by window.setInterval for our blink function, so
    213  * we can cancel it when caret browsing is disabled.
    214  * @type {number?}
    215  */
    216 CaretBrowsing.blinkFunctionId = null;
    217 
    218 /**
    219  * The desired x-coordinate to match when moving the caret up and down.
    220  * To match the behavior as documented in Mozilla's caret browsing spec
    221  * (http://www.mozilla.org/access/keyboard/proposal), we keep track of the
    222  * initial x position when the user starts moving the caret up and down,
    223  * so that the x position doesn't drift as you move throughout lines, but
    224  * stays as close as possible to the initial position. This is reset when
    225  * moving left or right or clicking.
    226  * @type {number?}
    227  */
    228 CaretBrowsing.targetX = null;
    229 
    230 /**
    231  * A flag that flips on or off as the caret blinks.
    232  * @type {boolean}
    233  */
    234 CaretBrowsing.blinkFlag = true;
    235 
    236 /**
    237  * Whether or not we're on a Mac - affects modifier keys.
    238  * @type {boolean}
    239  */
    240 CaretBrowsing.isMac = (navigator.appVersion.indexOf("Mac") != -1);
    241 
    242 /**
    243  * Check if a node is a control that normally allows the user to interact
    244  * with it using arrow keys. We won't override the arrow keys when such a
    245  * control has focus, the user must press Escape to do caret browsing outside
    246  * that control.
    247  * @param {Node} node A node to check.
    248  * @return {boolean} True if this node is a control that the user can
    249  *     interact with using arrow keys.
    250  */
    251 CaretBrowsing.isControlThatNeedsArrowKeys = function(node) {
    252   if (!node) {
    253     return false;
    254   }
    255 
    256   if (node == document.body || node != document.activeElement) {
    257     return false;
    258   }
    259 
    260   if (node.constructor == HTMLSelectElement) {
    261     return true;
    262   }
    263 
    264   if (node.constructor == HTMLInputElement) {
    265     switch (node.type) {
    266       case 'email':
    267       case 'number':
    268       case 'password':
    269       case 'search':
    270       case 'text':
    271       case 'tel':
    272       case 'url':
    273       case '':
    274         return true;  // All of these are text boxes.
    275       case 'datetime':
    276       case 'datetime-local':
    277       case 'date':
    278       case 'month':
    279       case 'radio':
    280       case 'range':
    281       case 'week':
    282         return true;  // These are other input elements that use arrows.
    283     }
    284   }
    285 
    286   // Handle focusable ARIA controls.
    287   if (node.getAttribute && isFocusable(node)) {
    288     var role = node.getAttribute('role');
    289     switch (role) {
    290       case 'combobox':
    291       case 'grid':
    292       case 'gridcell':
    293       case 'listbox':
    294       case 'menu':
    295       case 'menubar':
    296       case 'menuitem':
    297       case 'menuitemcheckbox':
    298       case 'menuitemradio':
    299       case 'option':
    300       case 'radiogroup':
    301       case 'scrollbar':
    302       case 'slider':
    303       case 'spinbutton':
    304       case 'tab':
    305       case 'tablist':
    306       case 'textbox':
    307       case 'tree':
    308       case 'treegrid':
    309       case 'treeitem':
    310         return true;
    311     }
    312   }
    313 
    314   return false;
    315 };
    316 
    317 /**
    318  * If there's no initial selection, set the cursor just before the
    319  * first text character in the document.
    320  */
    321 CaretBrowsing.setInitialCursor = function() {
    322   var sel = window.getSelection();
    323   if (sel.rangeCount > 0) {
    324     return;
    325   }
    326 
    327   var start = new Cursor(document.body, 0, '');
    328   var end = new Cursor(document.body, 0, '');
    329   var nodesCrossed = [];
    330   var result = TraverseUtil.getNextChar(start, end, nodesCrossed, true);
    331   if (result == null) {
    332     return;
    333   }
    334   CaretBrowsing.setAndValidateSelection(start, start);
    335 };
    336 
    337 /**
    338  * Set focus to a node if it's focusable. If it's an input element,
    339  * select the text, otherwise it doesn't appear focused to the user.
    340  * Every other control behaves normally if you just call focus() on it.
    341  * @param {Node} node The node to focus.
    342  * @return {boolean} True if the node was focused.
    343  */
    344 CaretBrowsing.setFocusToNode = function(node) {
    345   while (node && node != document.body) {
    346     if (isFocusable(node) && node.constructor != HTMLIFrameElement) {
    347       node.focus();
    348       if (node.constructor == HTMLInputElement && node.select) {
    349         node.select();
    350       }
    351       return true;
    352     }
    353     node = node.parentNode;
    354   }
    355 
    356   return false;
    357 };
    358 
    359 /**
    360  * Set focus to the first focusable node in the given list.
    361  * select the text, otherwise it doesn't appear focused to the user.
    362  * Every other control behaves normally if you just call focus() on it.
    363  * @param {Array.<Node>} nodeList An array of nodes to focus.
    364  * @return {boolean} True if the node was focused.
    365  */
    366 CaretBrowsing.setFocusToFirstFocusable = function(nodeList) {
    367   for (var i = 0; i < nodeList.length; i++) {
    368     if (CaretBrowsing.setFocusToNode(nodeList[i])) {
    369       return true;
    370     }
    371   }
    372   return false;
    373 };
    374 
    375 /**
    376  * Set the caret element's normal style, i.e. not when animating.
    377  */
    378 CaretBrowsing.setCaretElementNormalStyle = function() {
    379   var element = CaretBrowsing.caretElement;
    380   element.className = 'CaretBrowsing_Caret';
    381   element.style.opacity = CaretBrowsing.isSelectionCollapsed ? '1.0' : '0.0';
    382   element.style.left = CaretBrowsing.caretX + 'px';
    383   element.style.top = CaretBrowsing.caretY + 'px';
    384   element.style.width = CaretBrowsing.caretWidth + 'px';
    385   element.style.height = CaretBrowsing.caretHeight + 'px';
    386   element.style.color = CaretBrowsing.caretForeground;
    387 };
    388 
    389 /**
    390  * Animate the caret element into the normal style.
    391  */
    392 CaretBrowsing.animateCaretElement = function() {
    393   var element = CaretBrowsing.caretElement;
    394   element.style.left = (CaretBrowsing.caretX - 50) + 'px';
    395   element.style.top = (CaretBrowsing.caretY - 100) + 'px';
    396   element.style.width = (CaretBrowsing.caretWidth + 100) + 'px';
    397   element.style.height = (CaretBrowsing.caretHeight + 200) + 'px';
    398   element.className = 'CaretBrowsing_AnimateCaret';
    399 
    400   // Start the animation. The setTimeout is so that the old values will get
    401   // applied first, so we can animate to the new values.
    402   window.setTimeout(function() {
    403     if (!CaretBrowsing.caretElement) {
    404       return;
    405     }
    406     CaretBrowsing.setCaretElementNormalStyle();
    407     element.style['-webkit-transition'] = 'all 0.8s ease-in';
    408     function listener() {
    409       element.removeEventListener(
    410           'webkitTransitionEnd', listener, false);
    411       element.style['-webkit-transition'] = 'none';
    412     }
    413     element.addEventListener(
    414         'webkitTransitionEnd', listener, false);
    415   }, 0);
    416 };
    417 
    418 /**
    419  * Quick flash and then show the normal caret style.
    420  */
    421 CaretBrowsing.flashCaretElement = function() {
    422   var x = CaretBrowsing.caretX - window.pageXOffset;
    423   var y = CaretBrowsing.caretY - window.pageYOffset;
    424   var height = CaretBrowsing.caretHeight;
    425 
    426   var vert = document.createElement('div');
    427   vert.className = 'CaretBrowsing_FlashVert';
    428   vert.style.left = (x - 6) + 'px';
    429   vert.style.top = (y - 100) + 'px';
    430   vert.style.width = '11px';
    431   vert.style.height = (200) + 'px';
    432   document.body.appendChild(vert);
    433 
    434   window.setTimeout(function() {
    435     document.body.removeChild(vert);
    436     if (CaretBrowsing.caretElement) {
    437       CaretBrowsing.setCaretElementNormalStyle();
    438     }
    439   }, 250);
    440 };
    441 
    442 /**
    443  * Create the caret element. This assumes that caretX, caretY,
    444  * caretWidth, and caretHeight have all been set. The caret is
    445  * animated in so the user can find it when it first appears.
    446  */
    447 CaretBrowsing.createCaretElement = function() {
    448   var element = document.createElement('div');
    449   element.className = 'CaretBrowsing_Caret';
    450   document.body.appendChild(element);
    451   CaretBrowsing.caretElement = element;
    452 
    453   if (CaretBrowsing.onEnable == 'anim') {
    454     CaretBrowsing.animateCaretElement();
    455   } else if (CaretBrowsing.onEnable == 'flash') {
    456     CaretBrowsing.flashCaretElement();
    457   } else {
    458     CaretBrowsing.setCaretElementNormalStyle();
    459   }
    460 };
    461 
    462 /**
    463  * Recreate the caret element, triggering any intro animation.
    464  */
    465 CaretBrowsing.recreateCaretElement = function() {
    466   if (CaretBrowsing.caretElement) {
    467     window.clearInterval(CaretBrowsing.blinkFunctionId);
    468     CaretBrowsing.caretElement.parentElement.removeChild(
    469         CaretBrowsing.caretElement);
    470     CaretBrowsing.caretElement = null;
    471     CaretBrowsing.updateIsCaretVisible();
    472   }
    473 };
    474 
    475 /**
    476  * Get the rectangle for a cursor position. This is tricky because
    477  * you can't get the bounding rectangle of an empty range, so this function
    478  * computes the rect by trying a range including one character earlier or
    479  * later than the cursor position.
    480  * @param {Cursor} cursor A single cursor position.
    481  * @return {{left: number, top: number, width: number, height: number}}
    482  *     The bounding rectangle of the cursor.
    483  */
    484 CaretBrowsing.getCursorRect = function(cursor) {
    485   var node = cursor.node;
    486   var index = cursor.index;
    487   var rect = {
    488     left: 0,
    489     top: 0,
    490     width: 1,
    491     height: 0
    492   };
    493   if (node.constructor == Text) {
    494     var left = index;
    495     var right = index;
    496     var max = node.data.length;
    497     var newRange = document.createRange();
    498     while (left > 0 || right < max) {
    499       if (left > 0) {
    500         left--;
    501         newRange.setStart(node, left);
    502         newRange.setEnd(node, index);
    503         var rangeRect = newRange.getBoundingClientRect();
    504         if (rangeRect && rangeRect.width && rangeRect.height) {
    505           rect.left = rangeRect.right;
    506           rect.top = rangeRect.top;
    507           rect.height = rangeRect.height;
    508           break;
    509         }
    510       }
    511       if (right < max) {
    512         right++;
    513         newRange.setStart(node, index);
    514         newRange.setEnd(node, right);
    515         var rangeRect = newRange.getBoundingClientRect();
    516         if (rangeRect && rangeRect.width && rangeRect.height) {
    517           rect.left = rangeRect.left;
    518           rect.top = rangeRect.top;
    519           rect.height = rangeRect.height;
    520           break;
    521         }
    522       }
    523     }
    524   } else {
    525     rect.height = node.offsetHeight;
    526     while (node !== null) {
    527       rect.left += node.offsetLeft;
    528       rect.top += node.offsetTop;
    529       node = node.offsetParent;
    530     }
    531   }
    532   rect.left += window.pageXOffset;
    533   rect.top += window.pageYOffset;
    534   return rect;
    535 };
    536 
    537 /**
    538  * Compute the new location of the caret or selection and update
    539  * the element as needed.
    540  * @param {boolean} scrollToSelection If true, will also scroll the page
    541  *     to the caret / selection location.
    542  */
    543 CaretBrowsing.updateCaretOrSelection = function(scrollToSelection) {
    544   var previousX = CaretBrowsing.caretX;
    545   var previousY = CaretBrowsing.caretY;
    546 
    547   var sel = window.getSelection();
    548   if (sel.rangeCount == 0) {
    549     if (CaretBrowsing.caretElement) {
    550       CaretBrowsing.isSelectionCollapsed = false;
    551       CaretBrowsing.caretElement.style.opacity = '0.0';
    552     }
    553     return;
    554   }
    555 
    556   var range = sel.getRangeAt(0);
    557   if (!range) {
    558     if (CaretBrowsing.caretElement) {
    559       CaretBrowsing.isSelectionCollapsed = false;
    560       CaretBrowsing.caretElement.style.opacity = '0.0';
    561     }
    562     return;
    563   }
    564 
    565   if (CaretBrowsing.isControlThatNeedsArrowKeys(document.activeElement)) {
    566     var node = document.activeElement;
    567     CaretBrowsing.caretWidth = node.offsetWidth;
    568     CaretBrowsing.caretHeight = node.offsetHeight;
    569     CaretBrowsing.caretX = 0;
    570     CaretBrowsing.caretY = 0;
    571     while (node.offsetParent) {
    572       CaretBrowsing.caretX += node.offsetLeft;
    573       CaretBrowsing.caretY += node.offsetTop;
    574       node = node.offsetParent;
    575     }
    576     CaretBrowsing.isSelectionCollapsed = false;
    577   } else if (range.startOffset != range.endOffset ||
    578              range.startContainer != range.endContainer) {
    579     var rect = range.getBoundingClientRect();
    580     if (!rect) {
    581       return;
    582     }
    583     CaretBrowsing.caretX = rect.left + window.pageXOffset;
    584     CaretBrowsing.caretY = rect.top + window.pageYOffset;
    585     CaretBrowsing.caretWidth = rect.width;
    586     CaretBrowsing.caretHeight = rect.height;
    587     CaretBrowsing.isSelectionCollapsed = false;
    588   } else {
    589     var rect = CaretBrowsing.getCursorRect(
    590         new Cursor(range.startContainer,
    591                    range.startOffset,
    592                    TraverseUtil.getNodeText(range.startContainer)));
    593     CaretBrowsing.caretX = rect.left;
    594     CaretBrowsing.caretY = rect.top;
    595     CaretBrowsing.caretWidth = rect.width;
    596     CaretBrowsing.caretHeight = rect.height;
    597     CaretBrowsing.isSelectionCollapsed = true;
    598   }
    599 
    600   if (!CaretBrowsing.caretElement) {
    601     CaretBrowsing.createCaretElement();
    602   } else {
    603     var element = CaretBrowsing.caretElement;
    604     if (CaretBrowsing.isSelectionCollapsed) {
    605       element.style.opacity = '1.0';
    606       element.style.left = CaretBrowsing.caretX + 'px';
    607       element.style.top = CaretBrowsing.caretY + 'px';
    608       element.style.width = CaretBrowsing.caretWidth + 'px';
    609       element.style.height = CaretBrowsing.caretHeight + 'px';
    610     } else {
    611       element.style.opacity = '0.0';
    612     }
    613   }
    614 
    615   var elem = range.startContainer;
    616   if (elem.constructor == Text)
    617     elem = elem.parentElement;
    618   var style = window.getComputedStyle(elem);
    619   var bg = axs.utils.getBgColor(style, elem);
    620   var fg = axs.utils.getFgColor(style, elem, bg);
    621   CaretBrowsing.caretBackground = axs.utils.colorToString(bg);
    622   CaretBrowsing.caretForeground = axs.utils.colorToString(fg);
    623 
    624   if (scrollToSelection) {
    625     // Scroll just to the "focus" position of the selection,
    626     // the part the user is manipulating.
    627     var rect = CaretBrowsing.getCursorRect(
    628         new Cursor(sel.focusNode, sel.focusOffset,
    629                    TraverseUtil.getNodeText(sel.focusNode)));
    630 
    631     var yscroll = window.pageYOffset;
    632     var pageHeight = window.innerHeight;
    633     var caretY = rect.top;
    634     var caretHeight = Math.min(rect.height, 30);
    635     if (yscroll + pageHeight < caretY + caretHeight) {
    636       window.scroll(0, (caretY + caretHeight - pageHeight + 100));
    637     } else if (caretY < yscroll) {
    638       window.scroll(0, (caretY - 100));
    639     }
    640   }
    641 
    642   if (Math.abs(previousX - CaretBrowsing.caretX) > 500 ||
    643       Math.abs(previousY - CaretBrowsing.caretY) > 100) {
    644     if (CaretBrowsing.onJump == 'anim') {
    645       CaretBrowsing.animateCaretElement();
    646     } else if (CaretBrowsing.onJump == 'flash') {
    647       CaretBrowsing.flashCaretElement();
    648     }
    649   }
    650 };
    651 
    652 /**
    653  * Return true if the selection directionality is ambiguous, which happens
    654  * if, for example, the user double-clicks in the middle of a word to select
    655  * it. In that case, the selection should extend by the right edge if the
    656  * user presses right, and by the left edge if the user presses left.
    657  * @param {Selection} sel The selection.
    658  * @return {boolean} True if the selection directionality is ambiguous.
    659  */
    660 CaretBrowsing.isAmbiguous = function(sel) {
    661   return (sel.anchorNode != sel.baseNode ||
    662           sel.anchorOffset != sel.baseOffset ||
    663           sel.focusNode != sel.extentNode ||
    664           sel.focusOffset != sel.extentOffset);
    665 };
    666 
    667 /**
    668  * Create a Cursor from the anchor position of the selection, the
    669  * part that doesn't normally move.
    670  * @param {Selection} sel The selection.
    671  * @return {Cursor} A cursor pointing to the selection's anchor location.
    672  */
    673 CaretBrowsing.makeAnchorCursor = function(sel) {
    674   return new Cursor(sel.anchorNode, sel.anchorOffset,
    675                     TraverseUtil.getNodeText(sel.anchorNode));
    676 };
    677 
    678 /**
    679  * Create a Cursor from the focus position of the selection.
    680  * @param {Selection} sel The selection.
    681  * @return {Cursor} A cursor pointing to the selection's focus location.
    682  */
    683 CaretBrowsing.makeFocusCursor = function(sel) {
    684   return new Cursor(sel.focusNode, sel.focusOffset,
    685                     TraverseUtil.getNodeText(sel.focusNode));
    686 };
    687 
    688 /**
    689  * Create a Cursor from the left boundary of the selection - the boundary
    690  * closer to the start of the document.
    691  * @param {Selection} sel The selection.
    692  * @return {Cursor} A cursor pointing to the selection's left boundary.
    693  */
    694 CaretBrowsing.makeLeftCursor = function(sel) {
    695   var range = sel.rangeCount == 1 ? sel.getRangeAt(0) : null;
    696   if (range &&
    697       range.endContainer == sel.anchorNode &&
    698       range.endOffset == sel.anchorOffset) {
    699     return CaretBrowsing.makeFocusCursor(sel);
    700   } else {
    701     return CaretBrowsing.makeAnchorCursor(sel);
    702   }
    703 };
    704 
    705 /**
    706  * Create a Cursor from the right boundary of the selection - the boundary
    707  * closer to the end of the document.
    708  * @param {Selection} sel The selection.
    709  * @return {Cursor} A cursor pointing to the selection's right boundary.
    710  */
    711 CaretBrowsing.makeRightCursor = function(sel) {
    712   var range = sel.rangeCount == 1 ? sel.getRangeAt(0) : null;
    713   if (range &&
    714       range.endContainer == sel.anchorNode &&
    715       range.endOffset == sel.anchorOffset) {
    716     return CaretBrowsing.makeAnchorCursor(sel);
    717   } else {
    718     return CaretBrowsing.makeFocusCursor(sel);
    719   }
    720 };
    721 
    722 /**
    723  * Try to set the window's selection to be between the given start and end
    724  * cursors, and return whether or not it was successful.
    725  * @param {Cursor} start The start position.
    726  * @param {Cursor} end The end position.
    727  * @return {boolean} True if the selection was successfully set.
    728  */
    729 CaretBrowsing.setAndValidateSelection = function(start, end) {
    730   var sel = window.getSelection();
    731   sel.setBaseAndExtent(start.node, start.index, end.node, end.index);
    732 
    733   if (sel.rangeCount != 1) {
    734     return false;
    735   }
    736 
    737   return (sel.anchorNode == start.node &&
    738           sel.anchorOffset == start.index &&
    739           sel.focusNode == end.node &&
    740           sel.focusOffset == end.index);
    741 };
    742 
    743 /**
    744  * Note: the built-in function by the same name is unreliable.
    745  * @param {Selection} sel The selection.
    746  * @return {boolean} True if the start and end positions are the same.
    747  */
    748 CaretBrowsing.isCollapsed = function(sel) {
    749   return (sel.anchorOffset == sel.focusOffset &&
    750           sel.anchorNode == sel.focusNode);
    751 };
    752 
    753 /**
    754  * Determines if the modifier key is held down that should cause
    755  * the cursor to move by word rather than by character.
    756  * @param {Event} evt A keyboard event.
    757  * @return {boolean} True if the cursor should move by word.
    758  */
    759 CaretBrowsing.isMoveByWordEvent = function(evt) {
    760   if (CaretBrowsing.isMac) {
    761     return evt.altKey;
    762   } else {
    763     return evt.ctrlKey;
    764   }
    765 };
    766 
    767 /**
    768  * Moves the cursor forwards to the next valid position.
    769  * @param {Cursor} cursor The current cursor location.
    770  *     On exit, the cursor will be at the next position.
    771  * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
    772  *     initial and final cursor position will be pushed onto this array.
    773  * @return {?string} The character reached, or null if the bottom of the
    774  *     document has been reached.
    775  */
    776 CaretBrowsing.forwards = function(cursor, nodesCrossed) {
    777   var previousCursor = cursor.clone();
    778   var result = TraverseUtil.forwardsChar(cursor, nodesCrossed);
    779 
    780   // Work around the fact that TraverseUtil.forwardsChar returns once per
    781   // char in a block of text, rather than once per possible selection
    782   // position in a block of text.
    783   if (result && cursor.node != previousCursor.node && cursor.index > 0) {
    784     cursor.index = 0;
    785   }
    786 
    787   return result;
    788 };
    789 
    790 /**
    791  * Moves the cursor backwards to the previous valid position.
    792  * @param {Cursor} cursor The current cursor location.
    793  *     On exit, the cursor will be at the previous position.
    794  * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
    795  *     initial and final cursor position will be pushed onto this array.
    796  * @return {?string} The character reached, or null if the top of the
    797  *     document has been reached.
    798  */
    799 CaretBrowsing.backwards = function(cursor, nodesCrossed) {
    800   var previousCursor = cursor.clone();
    801   var result = TraverseUtil.backwardsChar(cursor, nodesCrossed);
    802 
    803   // Work around the fact that TraverseUtil.backwardsChar returns once per
    804   // char in a block of text, rather than once per possible selection
    805   // position in a block of text.
    806   if (result &&
    807       cursor.node != previousCursor.node &&
    808       cursor.index < cursor.text.length) {
    809     cursor.index = cursor.text.length;
    810   }
    811 
    812   return result;
    813 };
    814 
    815 /**
    816  * Called when the user presses the right arrow. If there's a selection,
    817  * moves the cursor to the end of the selection range. If it's a cursor,
    818  * moves past one character.
    819  * @param {Event} evt The DOM event.
    820  * @return {boolean} True if the default action should be performed.
    821  */
    822 CaretBrowsing.moveRight = function(evt) {
    823   CaretBrowsing.targetX = null;
    824 
    825   var sel = window.getSelection();
    826   if (!evt.shiftKey && !CaretBrowsing.isCollapsed(sel)) {
    827     var right = CaretBrowsing.makeRightCursor(sel);
    828     CaretBrowsing.setAndValidateSelection(right, right);
    829     return false;
    830   }
    831 
    832   var start = CaretBrowsing.isAmbiguous(sel) ?
    833               CaretBrowsing.makeLeftCursor(sel) :
    834               CaretBrowsing.makeAnchorCursor(sel);
    835   var end = CaretBrowsing.isAmbiguous(sel) ?
    836             CaretBrowsing.makeRightCursor(sel) :
    837             CaretBrowsing.makeFocusCursor(sel);
    838   var previousEnd = end.clone();
    839   var nodesCrossed = [];
    840   while (true) {
    841     var result;
    842     if (CaretBrowsing.isMoveByWordEvent(evt)) {
    843       result = TraverseUtil.getNextWord(previousEnd, end, nodesCrossed);
    844     } else {
    845       previousEnd = end.clone();
    846       result = CaretBrowsing.forwards(end, nodesCrossed);
    847     }
    848 
    849     if (result === null) {
    850       return CaretBrowsing.moveLeft(evt);
    851     }
    852 
    853     if (CaretBrowsing.setAndValidateSelection(
    854             evt.shiftKey ? start : end, end)) {
    855       break;
    856     }
    857   }
    858 
    859   if (!evt.shiftKey) {
    860     nodesCrossed.push(end.node);
    861     CaretBrowsing.setFocusToFirstFocusable(nodesCrossed);
    862   }
    863 
    864   return false;
    865 };
    866 
    867 /**
    868  * Called when the user presses the left arrow. If there's a selection,
    869  * moves the cursor to the start of the selection range. If it's a cursor,
    870  * moves backwards past one character.
    871  * @param {Event} evt The DOM event.
    872  * @return {boolean} True if the default action should be performed.
    873  */
    874 CaretBrowsing.moveLeft = function(evt) {
    875   CaretBrowsing.targetX = null;
    876 
    877   var sel = window.getSelection();
    878   if (!evt.shiftKey && !CaretBrowsing.isCollapsed(sel)) {
    879     var left = CaretBrowsing.makeLeftCursor(sel);
    880     CaretBrowsing.setAndValidateSelection(left, left);
    881     return false;
    882   }
    883 
    884   var start = CaretBrowsing.isAmbiguous(sel) ?
    885               CaretBrowsing.makeLeftCursor(sel) :
    886               CaretBrowsing.makeFocusCursor(sel);
    887   var end = CaretBrowsing.isAmbiguous(sel) ?
    888             CaretBrowsing.makeRightCursor(sel) :
    889             CaretBrowsing.makeAnchorCursor(sel);
    890   var previousStart = start.clone();
    891   var nodesCrossed = [];
    892   while (true) {
    893     var result;
    894     if (CaretBrowsing.isMoveByWordEvent(evt)) {
    895       result = TraverseUtil.getPreviousWord(
    896           start, previousStart, nodesCrossed);
    897     } else {
    898       previousStart = start.clone();
    899       result = CaretBrowsing.backwards(start, nodesCrossed);
    900     }
    901 
    902     if (result === null) {
    903       break;
    904     }
    905 
    906     if (CaretBrowsing.setAndValidateSelection(
    907             evt.shiftKey ? end : start, start)) {
    908       break;
    909     }
    910   }
    911 
    912   if (!evt.shiftKey) {
    913     nodesCrossed.push(start.node);
    914     CaretBrowsing.setFocusToFirstFocusable(nodesCrossed);
    915   }
    916 
    917   return false;
    918 };
    919 
    920 
    921 /**
    922  * Called when the user presses the down arrow. If there's a selection,
    923  * moves the cursor to the end of the selection range. If it's a cursor,
    924  * attempts to move to the equivalent horizontal pixel position in the
    925  * subsequent line of text. If this is impossible, go to the first character
    926  * of the next line.
    927  * @param {Event} evt The DOM event.
    928  * @return {boolean} True if the default action should be performed.
    929  */
    930 CaretBrowsing.moveDown = function(evt) {
    931   var sel = window.getSelection();
    932   if (!evt.shiftKey && !CaretBrowsing.isCollapsed(sel)) {
    933     var right = CaretBrowsing.makeRightCursor(sel);
    934     CaretBrowsing.setAndValidateSelection(right, right);
    935     return false;
    936   }
    937 
    938   var start = CaretBrowsing.isAmbiguous(sel) ?
    939               CaretBrowsing.makeLeftCursor(sel) :
    940               CaretBrowsing.makeAnchorCursor(sel);
    941   var end = CaretBrowsing.isAmbiguous(sel) ?
    942             CaretBrowsing.makeRightCursor(sel) :
    943             CaretBrowsing.makeFocusCursor(sel);
    944   var endRect = CaretBrowsing.getCursorRect(end);
    945   if (CaretBrowsing.targetX === null) {
    946     CaretBrowsing.targetX = endRect.left;
    947   }
    948   var previousEnd = end.clone();
    949   var leftPos = end.clone();
    950   var rightPos = end.clone();
    951   var bestPos = null;
    952   var bestY = null;
    953   var bestDelta = null;
    954   var bestHeight = null;
    955   var nodesCrossed = [];
    956   var y = -1;
    957   while (true) {
    958     if (null === CaretBrowsing.forwards(rightPos, nodesCrossed)) {
    959       if (CaretBrowsing.setAndValidateSelection(
    960             evt.shiftKey ? start : leftPos, leftPos)) {
    961         break;
    962       } else {
    963         return CaretBrowsing.moveLeft(evt);
    964       }
    965       break;
    966     }
    967     var range = document.createRange();
    968     range.setStart(leftPos.node, leftPos.index);
    969     range.setEnd(rightPos.node, rightPos.index);
    970     var rect = range.getBoundingClientRect();
    971     if (rect && rect.width < rect.height) {
    972       y = rect.top + window.pageYOffset;
    973 
    974       // Return the best match so far if we get half a line past the best.
    975       if (bestY != null && y > bestY + bestHeight / 2) {
    976         if (CaretBrowsing.setAndValidateSelection(
    977                 evt.shiftKey ? start : bestPos, bestPos)) {
    978           break;
    979         } else {
    980           bestY = null;
    981         }
    982       }
    983 
    984       // Stop here if we're an entire line the wrong direction
    985       // (for example, we reached the top of the next column).
    986       if (y < endRect.top - endRect.height) {
    987         if (CaretBrowsing.setAndValidateSelection(
    988                 evt.shiftKey ? start : leftPos, leftPos)) {
    989           break;
    990         }
    991       }
    992 
    993       // Otherwise look to see if this current position is on the
    994       // next line and better than the previous best match, if any.
    995       if (y >= endRect.top + endRect.height) {
    996         var deltaLeft = Math.abs(CaretBrowsing.targetX - rect.left);
    997         if ((bestDelta == null || deltaLeft < bestDelta) &&
    998             (leftPos.node != end.node || leftPos.index != end.index)) {
    999           bestPos = leftPos.clone();
   1000           bestY = y;
   1001           bestDelta = deltaLeft;
   1002           bestHeight = rect.height;
   1003         }
   1004         var deltaRight = Math.abs(CaretBrowsing.targetX - rect.right);
   1005         if (bestDelta == null || deltaRight < bestDelta) {
   1006           bestPos = rightPos.clone();
   1007           bestY = y;
   1008           bestDelta = deltaRight;
   1009           bestHeight = rect.height;
   1010         }
   1011 
   1012         // Return the best match so far if the deltas are getting worse,
   1013         // not better.
   1014         if (bestDelta != null &&
   1015             deltaLeft > bestDelta &&
   1016             deltaRight > bestDelta) {
   1017           if (CaretBrowsing.setAndValidateSelection(
   1018                   evt.shiftKey ? start : bestPos, bestPos)) {
   1019             break;
   1020           } else {
   1021             bestY = null;
   1022           }
   1023         }
   1024       }
   1025     }
   1026     leftPos = rightPos.clone();
   1027   }
   1028 
   1029   if (!evt.shiftKey) {
   1030     CaretBrowsing.setFocusToNode(leftPos.node);
   1031   }
   1032 
   1033   return false;
   1034 };
   1035 
   1036 /**
   1037  * Called when the user presses the up arrow. If there's a selection,
   1038  * moves the cursor to the start of the selection range. If it's a cursor,
   1039  * attempts to move to the equivalent horizontal pixel position in the
   1040  * previous line of text. If this is impossible, go to the last character
   1041  * of the previous line.
   1042  * @param {Event} evt The DOM event.
   1043  * @return {boolean} True if the default action should be performed.
   1044  */
   1045 CaretBrowsing.moveUp = function(evt) {
   1046   var sel = window.getSelection();
   1047   if (!evt.shiftKey && !CaretBrowsing.isCollapsed(sel)) {
   1048     var left = CaretBrowsing.makeLeftCursor(sel);
   1049     CaretBrowsing.setAndValidateSelection(left, left);
   1050     return false;
   1051   }
   1052 
   1053   var start = CaretBrowsing.isAmbiguous(sel) ?
   1054               CaretBrowsing.makeLeftCursor(sel) :
   1055               CaretBrowsing.makeFocusCursor(sel);
   1056   var end = CaretBrowsing.isAmbiguous(sel) ?
   1057             CaretBrowsing.makeRightCursor(sel) :
   1058             CaretBrowsing.makeAnchorCursor(sel);
   1059   var startRect = CaretBrowsing.getCursorRect(start);
   1060   if (CaretBrowsing.targetX === null) {
   1061     CaretBrowsing.targetX = startRect.left;
   1062   }
   1063   var previousStart = start.clone();
   1064   var leftPos = start.clone();
   1065   var rightPos = start.clone();
   1066   var bestPos = null;
   1067   var bestY = null;
   1068   var bestDelta = null;
   1069   var bestHeight = null;
   1070   var nodesCrossed = [];
   1071   var y = 999999;
   1072   while (true) {
   1073     if (null === CaretBrowsing.backwards(leftPos, nodesCrossed)) {
   1074       CaretBrowsing.setAndValidateSelection(
   1075           evt.shiftKey ? end : rightPos, rightPos);
   1076       break;
   1077     }
   1078     var range = document.createRange();
   1079     range.setStart(leftPos.node, leftPos.index);
   1080     range.setEnd(rightPos.node, rightPos.index);
   1081     var rect = range.getBoundingClientRect();
   1082     if (rect && rect.width < rect.height) {
   1083       y = rect.top + window.pageYOffset;
   1084 
   1085       // Return the best match so far if we get half a line past the best.
   1086       if (bestY != null && y < bestY - bestHeight / 2) {
   1087         if (CaretBrowsing.setAndValidateSelection(
   1088                 evt.shiftKey ? end : bestPos, bestPos)) {
   1089           break;
   1090         } else {
   1091           bestY = null;
   1092         }
   1093       }
   1094 
   1095       // Exit if we're an entire line the wrong direction
   1096       // (for example, we reached the bottom of the previous column.)
   1097       if (y > startRect.top + startRect.height) {
   1098         if (CaretBrowsing.setAndValidateSelection(
   1099                 evt.shiftKey ? end : rightPos, rightPos)) {
   1100           break;
   1101         }
   1102       }
   1103 
   1104       // Otherwise look to see if this current position is on the
   1105       // next line and better than the previous best match, if any.
   1106       if (y <= startRect.top - startRect.height) {
   1107         var deltaLeft = Math.abs(CaretBrowsing.targetX - rect.left);
   1108         if (bestDelta == null || deltaLeft < bestDelta) {
   1109           bestPos = leftPos.clone();
   1110           bestY = y;
   1111           bestDelta = deltaLeft;
   1112           bestHeight = rect.height;
   1113         }
   1114         var deltaRight = Math.abs(CaretBrowsing.targetX - rect.right);
   1115         if ((bestDelta == null || deltaRight < bestDelta) &&
   1116             (rightPos.node != start.node || rightPos.index != start.index)) {
   1117           bestPos = rightPos.clone();
   1118           bestY = y;
   1119           bestDelta = deltaRight;
   1120           bestHeight = rect.height;
   1121         }
   1122 
   1123         // Return the best match so far if the deltas are getting worse,
   1124         // not better.
   1125         if (bestDelta != null &&
   1126             deltaLeft > bestDelta &&
   1127             deltaRight > bestDelta) {
   1128           if (CaretBrowsing.setAndValidateSelection(
   1129                   evt.shiftKey ? end : bestPos, bestPos)) {
   1130             break;
   1131           } else {
   1132             bestY = null;
   1133           }
   1134         }
   1135       }
   1136     }
   1137     rightPos = leftPos.clone();
   1138   }
   1139 
   1140   if (!evt.shiftKey) {
   1141     CaretBrowsing.setFocusToNode(rightPos.node);
   1142   }
   1143 
   1144   return false;
   1145 };
   1146 
   1147 /**
   1148  * Set the document's selection to surround a control, so that the next
   1149  * arrow key they press will allow them to explore the content before
   1150  * or after a given control.
   1151  * @param {Node} control The control to escape from.
   1152  */
   1153 CaretBrowsing.escapeFromControl = function(control) {
   1154   control.blur();
   1155 
   1156   var start = new Cursor(control, 0, '');
   1157   var previousStart = start.clone();
   1158   var end = new Cursor(control, 0, '');
   1159   var previousEnd = end.clone();
   1160 
   1161   var nodesCrossed = [];
   1162   while (true) {
   1163     if (null === CaretBrowsing.backwards(start, nodesCrossed)) {
   1164       break;
   1165     }
   1166 
   1167     var r = document.createRange();
   1168     r.setStart(start.node, start.index);
   1169     r.setEnd(previousStart.node, previousStart.index);
   1170     if (r.getBoundingClientRect()) {
   1171       break;
   1172     }
   1173     previousStart = start.clone();
   1174   }
   1175   while (true) {
   1176     if (null === CaretBrowsing.forwards(end, nodesCrossed)) {
   1177       break;
   1178     }
   1179     if (isDescendantOfNode(end.node, control)) {
   1180       previousEnd = end.clone();
   1181       continue;
   1182     }
   1183 
   1184     var r = document.createRange();
   1185     r.setStart(previousEnd.node, previousEnd.index);
   1186     r.setEnd(end.node, end.index);
   1187     if (r.getBoundingClientRect()) {
   1188       break;
   1189     }
   1190   }
   1191 
   1192   if (!isDescendantOfNode(previousStart.node, control)) {
   1193     start = previousStart.clone();
   1194   }
   1195 
   1196   if (!isDescendantOfNode(previousEnd.node, control)) {
   1197     end = previousEnd.clone();
   1198   }
   1199 
   1200   CaretBrowsing.setAndValidateSelection(start, end);
   1201 
   1202   window.setTimeout(function() {
   1203     CaretBrowsing.updateCaretOrSelection(true);
   1204   }, 0);
   1205 };
   1206 
   1207 /**
   1208  * Toggle whether caret browsing is enabled or not.
   1209  */
   1210 CaretBrowsing.toggle = function() {
   1211   if (CaretBrowsing.forceEnabled) {
   1212     CaretBrowsing.recreateCaretElement();
   1213     return;
   1214   }
   1215 
   1216   CaretBrowsing.isEnabled = !CaretBrowsing.isEnabled;
   1217   var obj = {};
   1218   obj['enabled'] = CaretBrowsing.isEnabled;
   1219   chrome.storage.sync.set(obj);
   1220   CaretBrowsing.updateIsCaretVisible();
   1221 };
   1222 
   1223 /**
   1224  * Event handler, called when a key is pressed.
   1225  * @param {Event} evt The DOM event.
   1226  * @return {boolean} True if the default action should be performed.
   1227  */
   1228 CaretBrowsing.onKeyDown = function(evt) {
   1229   if (evt.defaultPrevented) {
   1230     return;
   1231   }
   1232 
   1233   if (evt.keyCode == 118) {  // F7
   1234     CaretBrowsing.toggle();
   1235   }
   1236 
   1237   if (!CaretBrowsing.isEnabled) {
   1238     return true;
   1239   }
   1240 
   1241   if (evt.target && CaretBrowsing.isControlThatNeedsArrowKeys(
   1242       /** @type (Node) */(evt.target))) {
   1243     if (evt.keyCode == 27) {
   1244       CaretBrowsing.escapeFromControl(/** @type {Node} */(evt.target));
   1245       evt.preventDefault();
   1246       evt.stopPropagation();
   1247       return false;
   1248     } else {
   1249       return true;
   1250     }
   1251   }
   1252 
   1253   // If the current selection doesn't have a range, try to escape out of
   1254   // the current control. If that fails, return so we don't fail whe
   1255   // trying to move the cursor or selection.
   1256   var sel = window.getSelection();
   1257   if (sel.rangeCount == 0) {
   1258     if (document.activeElement) {
   1259       CaretBrowsing.escapeFromControl(document.activeElement);
   1260       sel = window.getSelection();
   1261     }
   1262 
   1263     if (sel.rangeCount == 0) {
   1264       return true;
   1265     }
   1266   }
   1267 
   1268   if (CaretBrowsing.caretElement) {
   1269     CaretBrowsing.caretElement.style.visibility = 'visible';
   1270     CaretBrowsing.blinkFlag = true;
   1271   }
   1272 
   1273   var result = true;
   1274   switch (evt.keyCode) {
   1275     case 37:
   1276       result = CaretBrowsing.moveLeft(evt);
   1277       break;
   1278     case 38:
   1279       result = CaretBrowsing.moveUp(evt);
   1280       break;
   1281     case 39:
   1282       result = CaretBrowsing.moveRight(evt);
   1283       break;
   1284     case 40:
   1285       result = CaretBrowsing.moveDown(evt);
   1286       break;
   1287   }
   1288 
   1289   if (result == false) {
   1290     evt.preventDefault();
   1291     evt.stopPropagation();
   1292   }
   1293 
   1294   window.setTimeout(function() {
   1295     CaretBrowsing.updateCaretOrSelection(result == false);
   1296   }, 0);
   1297 
   1298   return result;
   1299 };
   1300 
   1301 /**
   1302  * Event handler, called when the mouse is clicked. Chrome already
   1303  * sets the selection when the mouse is clicked, all we need to do is
   1304  * update our cursor.
   1305  * @param {Event} evt The DOM event.
   1306  * @return {boolean} True if the default action should be performed.
   1307  */
   1308 CaretBrowsing.onClick = function(evt) {
   1309   if (!CaretBrowsing.isEnabled) {
   1310     return true;
   1311   }
   1312   window.setTimeout(function() {
   1313     CaretBrowsing.targetX = null;
   1314     CaretBrowsing.updateCaretOrSelection(false);
   1315   }, 0);
   1316   return true;
   1317 };
   1318 
   1319 /**
   1320  * Called at a regular interval. Blink the cursor by changing its visibility.
   1321  */
   1322 CaretBrowsing.caretBlinkFunction = function() {
   1323   if (CaretBrowsing.caretElement) {
   1324     if (CaretBrowsing.blinkFlag) {
   1325       CaretBrowsing.caretElement.style.backgroundColor =
   1326           CaretBrowsing.caretForeground;
   1327       CaretBrowsing.blinkFlag = false;
   1328     } else {
   1329       CaretBrowsing.caretElement.style.backgroundColor =
   1330           CaretBrowsing.caretBackground;
   1331       CaretBrowsing.blinkFlag = true;
   1332     }
   1333   }
   1334 };
   1335 
   1336 /**
   1337  * Update whether or not the caret is visible, based on whether caret browsing
   1338  * is enabled and whether this window / iframe has focus.
   1339  */
   1340 CaretBrowsing.updateIsCaretVisible = function() {
   1341   CaretBrowsing.isCaretVisible =
   1342       (CaretBrowsing.isEnabled && CaretBrowsing.isWindowFocused);
   1343   if (CaretBrowsing.isCaretVisible && !CaretBrowsing.caretElement) {
   1344     CaretBrowsing.setInitialCursor();
   1345     CaretBrowsing.updateCaretOrSelection(true);
   1346     if (CaretBrowsing.caretElement) {
   1347       CaretBrowsing.blinkFunctionId = window.setInterval(
   1348           CaretBrowsing.caretBlinkFunction, 500);
   1349     }
   1350   } else if (!CaretBrowsing.isCaretVisible &&
   1351              CaretBrowsing.caretElement) {
   1352     window.clearInterval(CaretBrowsing.blinkFunctionId);
   1353     if (CaretBrowsing.caretElement) {
   1354       CaretBrowsing.isSelectionCollapsed = false;
   1355       CaretBrowsing.caretElement.parentElement.removeChild(
   1356           CaretBrowsing.caretElement);
   1357       CaretBrowsing.caretElement = null;
   1358     }
   1359   }
   1360 };
   1361 
   1362 /**
   1363  * Called when the prefs get updated.
   1364  */
   1365 CaretBrowsing.onPrefsUpdated = function() {
   1366   chrome.storage.sync.get(null, function(result) {
   1367     if (!CaretBrowsing.forceEnabled) {
   1368       CaretBrowsing.isEnabled = result['enabled'];
   1369     }
   1370     CaretBrowsing.onEnable = result['onenable'];
   1371     CaretBrowsing.onJump = result['onjump'];
   1372     CaretBrowsing.recreateCaretElement();
   1373   });
   1374 };
   1375 
   1376 /**
   1377  * Called when this window / iframe gains focus.
   1378  */
   1379 CaretBrowsing.onWindowFocus = function() {
   1380   CaretBrowsing.isWindowFocused = true;
   1381   CaretBrowsing.updateIsCaretVisible();
   1382 };
   1383 
   1384 /**
   1385  * Called when this window / iframe loses focus.
   1386  */
   1387 CaretBrowsing.onWindowBlur = function() {
   1388   CaretBrowsing.isWindowFocused = false;
   1389   CaretBrowsing.updateIsCaretVisible();
   1390 };
   1391 
   1392 /**
   1393  * Initializes caret browsing by adding event listeners and extension
   1394  * message listeners.
   1395  */
   1396 CaretBrowsing.init = function() {
   1397   CaretBrowsing.isWindowFocused = document.hasFocus();
   1398 
   1399   document.addEventListener('keydown', CaretBrowsing.onKeyDown, false);
   1400   document.addEventListener('click', CaretBrowsing.onClick, false);
   1401   window.addEventListener('focus', CaretBrowsing.onWindowFocus, false);
   1402   window.addEventListener('blur', CaretBrowsing.onWindowBlur, false);
   1403 };
   1404 
   1405 window.setTimeout(function() {
   1406 
   1407   // Make sure the script only loads once.
   1408   if (!window['caretBrowsingLoaded']) {
   1409     window['caretBrowsingLoaded'] = true;
   1410     CaretBrowsing.init();
   1411 
   1412     if (document.body.getAttribute('caretbrowsing') == 'on') {
   1413       CaretBrowsing.forceEnabled = true;
   1414       CaretBrowsing.isEnabled = true;
   1415       CaretBrowsing.updateIsCaretVisible();
   1416     }
   1417 
   1418     chrome.storage.onChanged.addListener(function() {
   1419       CaretBrowsing.onPrefsUpdated();
   1420     });
   1421     CaretBrowsing.onPrefsUpdated();
   1422   }
   1423 
   1424 }, 0);
   1425