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 */ 73function 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 */ 98function 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 */ 120var CaretBrowsing = function() {}; 121 122/** 123 * Is caret browsing enabled? 124 * @type {boolean} 125 */ 126CaretBrowsing.isEnabled = false; 127 128/** 129 * Keep it enabled even when flipped off (for the options page)? 130 * @type {boolean} 131 */ 132CaretBrowsing.forceEnabled = false; 133 134/** 135 * What to do when the caret appears? 136 * @type {string} 137 */ 138CaretBrowsing.onEnable; 139 140/** 141 * What to do when the caret jumps? 142 * @type {string} 143 */ 144CaretBrowsing.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 */ 152CaretBrowsing.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 */ 159CaretBrowsing.isCaretVisible = false; 160 161/** 162 * The actual caret element, an absolute-positioned flashing line. 163 * @type {Element} 164 */ 165CaretBrowsing.caretElement; 166 167/** 168 * The x-position of the caret, in absolute pixels. 169 * @type {number} 170 */ 171CaretBrowsing.caretX = 0; 172 173/** 174 * The y-position of the caret, in absolute pixels. 175 * @type {number} 176 */ 177CaretBrowsing.caretY = 0; 178 179/** 180 * The width of the caret in pixels. 181 * @type {number} 182 */ 183CaretBrowsing.caretWidth = 0; 184 185/** 186 * The height of the caret in pixels. 187 * @type {number} 188 */ 189CaretBrowsing.caretHeight = 0; 190 191/** 192 * The foregroundc color. 193 * @type {string} 194 */ 195CaretBrowsing.caretForeground = '#000'; 196 197/** 198 * The backgroundc color. 199 * @type {string} 200 */ 201CaretBrowsing.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 */ 209CaretBrowsing.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 */ 216CaretBrowsing.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 */ 228CaretBrowsing.targetX = null; 229 230/** 231 * A flag that flips on or off as the caret blinks. 232 * @type {boolean} 233 */ 234CaretBrowsing.blinkFlag = true; 235 236/** 237 * Whether or not we're on a Mac - affects modifier keys. 238 * @type {boolean} 239 */ 240CaretBrowsing.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 */ 251CaretBrowsing.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 */ 321CaretBrowsing.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 */ 344CaretBrowsing.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 */ 366CaretBrowsing.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 */ 378CaretBrowsing.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 */ 392CaretBrowsing.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 */ 421CaretBrowsing.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 */ 447CaretBrowsing.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 */ 465CaretBrowsing.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 */ 484CaretBrowsing.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 */ 543CaretBrowsing.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 */ 660CaretBrowsing.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 */ 673CaretBrowsing.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 */ 683CaretBrowsing.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 */ 694CaretBrowsing.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 */ 711CaretBrowsing.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 */ 729CaretBrowsing.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 */ 748CaretBrowsing.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 */ 759CaretBrowsing.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 */ 776CaretBrowsing.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 */ 799CaretBrowsing.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 */ 822CaretBrowsing.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 */ 874CaretBrowsing.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 */ 930CaretBrowsing.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 */ 1045CaretBrowsing.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 */ 1153CaretBrowsing.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 */ 1210CaretBrowsing.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 */ 1228CaretBrowsing.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 */ 1308CaretBrowsing.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 */ 1322CaretBrowsing.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 */ 1340CaretBrowsing.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 */ 1365CaretBrowsing.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 */ 1379CaretBrowsing.onWindowFocus = function() { 1380 CaretBrowsing.isWindowFocused = true; 1381 CaretBrowsing.updateIsCaretVisible(); 1382}; 1383 1384/** 1385 * Called when this window / iframe loses focus. 1386 */ 1387CaretBrowsing.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 */ 1396CaretBrowsing.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 1405window.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