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. */
6 * @fileoverview Caret browsing content script, runs in each frame.
8 * The behavior is based on Mozilla's spec whenever possible:
9 * http://www.mozilla.org/access/keyboard/proposal
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).
15 * Some details about how Chrome selection works, which will help in
16 * understanding the code:
18 * The Selection object (window.getSelection()) has four components that
19 * completely describe the state of the caret or selection:
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.
25 * When the selection is a cursor, the base, anchor, extent, and focus are
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.
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.
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.
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.
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
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.
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.
70 * @param {Object} targetNode The node to check if it's focusable.
71 * @return {boolean} True if the node is focusable.
73 function isFocusable(targetNode
) {
74 if (!targetNode
|| typeof(targetNode
.tabIndex
) != 'number') {
78 if (targetNode
.tabIndex
>= 0) {
82 if (targetNode
.hasAttribute
&&
83 targetNode
.hasAttribute('tabindex') &&
84 targetNode
.getAttribute('tabindex') == '-1') {
92 * Determines whether or not a node is or is the descendant of another node.
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.
98 function isDescendantOfNode(node
, ancestor
) {
99 while (node
&& ancestor
) {
100 if (node
.isSameNode(ancestor
)) {
103 node
= node
.parentNode
;
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.
120 var CaretBrowsing = function() {};
123 * Is caret browsing enabled?
126 CaretBrowsing
.isEnabled
= false;
129 * Keep it enabled even when flipped off (for the options page)?
132 CaretBrowsing
.forceEnabled
= false;
135 * What to do when the caret appears?
138 CaretBrowsing
.onEnable
;
141 * What to do when the caret jumps?
144 CaretBrowsing
.onJump
;
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
152 CaretBrowsing
.isWindowFocused
= false;
155 * Is the caret actually visible? This is true only if isEnabled and
156 * isWindowFocused are both true.
159 CaretBrowsing
.isCaretVisible
= false;
162 * The actual caret element, an absolute-positioned flashing line.
165 CaretBrowsing
.caretElement
;
168 * The x-position of the caret, in absolute pixels.
171 CaretBrowsing
.caretX
= 0;
174 * The y-position of the caret, in absolute pixels.
177 CaretBrowsing
.caretY
= 0;
180 * The width of the caret in pixels.
183 CaretBrowsing
.caretWidth
= 0;
186 * The height of the caret in pixels.
189 CaretBrowsing
.caretHeight
= 0;
192 * The foregroundc color.
195 CaretBrowsing
.caretForeground
= '#000';
198 * The backgroundc color.
201 CaretBrowsing
.caretBackground
= '#fff';
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.
209 CaretBrowsing
.isSelectionCollapsed
= false;
212 * The id returned by window.setInterval for our blink function, so
213 * we can cancel it when caret browsing is disabled.
216 CaretBrowsing
.blinkFunctionId
= null;
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.
228 CaretBrowsing
.targetX
= null;
231 * A flag that flips on or off as the caret blinks.
234 CaretBrowsing
.blinkFlag
= true;
237 * Whether or not we're on a Mac - affects modifier keys.
240 CaretBrowsing
.isMac
= (navigator
.appVersion
.indexOf("Mac") != -1);
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
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.
251 CaretBrowsing
.isControlThatNeedsArrowKeys = function(node
) {
256 if (node
== document
.body
|| node
!= document
.activeElement
) {
260 if (node
.constructor == HTMLSelectElement
) {
264 if (node
.constructor == HTMLInputElement
) {
274 return true; // All of these are text boxes.
276 case 'datetime-local':
282 return true; // These are other input elements that use arrows.
286 // Handle focusable ARIA controls.
287 if (node
.getAttribute
&& isFocusable(node
)) {
288 var role
= node
.getAttribute('role');
297 case 'menuitemcheckbox':
298 case 'menuitemradio':
318 * If there's no initial selection, set the cursor just before the
319 * first text character in the document.
321 CaretBrowsing
.setInitialCursor = function() {
322 var sel
= window
.getSelection();
323 if (sel
.rangeCount
> 0) {
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) {
334 CaretBrowsing
.setAndValidateSelection(start
, start
);
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.
344 CaretBrowsing
.setFocusToNode = function(node
) {
345 while (node
&& node
!= document
.body
) {
346 if (isFocusable(node
) && node
.constructor != HTMLIFrameElement
) {
348 if (node
.constructor == HTMLInputElement
&& node
.select
) {
353 node
= node
.parentNode
;
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.
366 CaretBrowsing
.setFocusToFirstFocusable = function(nodeList
) {
367 for (var i
= 0; i
< nodeList
.length
; i
++) {
368 if (CaretBrowsing
.setFocusToNode(nodeList
[i
])) {
376 * Set the caret element's normal style, i.e. not when animating.
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
;
390 * Animate the caret element into the normal style.
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';
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
) {
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';
413 element
.addEventListener(
414 'webkitTransitionEnd', listener
, false);
419 * Quick flash and then show the normal caret style.
421 CaretBrowsing
.flashCaretElement = function() {
422 var x
= CaretBrowsing
.caretX
;
423 var y
= CaretBrowsing
.caretY
;
424 var height
= CaretBrowsing
.caretHeight
;
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
);
434 window
.setTimeout(function() {
435 document
.body
.removeChild(vert
);
436 if (CaretBrowsing
.caretElement
) {
437 CaretBrowsing
.setCaretElementNormalStyle();
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.
447 CaretBrowsing
.createCaretElement = function() {
448 var element
= document
.createElement('div');
449 element
.className
= 'CaretBrowsing_Caret';
450 document
.body
.appendChild(element
);
451 CaretBrowsing
.caretElement
= element
;
453 if (CaretBrowsing
.onEnable
== 'anim') {
454 CaretBrowsing
.animateCaretElement();
455 } else if (CaretBrowsing
.onEnable
== 'flash') {
456 CaretBrowsing
.flashCaretElement();
458 CaretBrowsing
.setCaretElementNormalStyle();
463 * Recreate the caret element, triggering any intro animation.
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();
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.
484 CaretBrowsing
.getCursorRect = function(cursor
) {
485 var node
= cursor
.node
;
486 var index
= cursor
.index
;
493 if (node
.constructor == Text
) {
496 var max
= node
.data
.length
;
497 var newRange
= document
.createRange();
498 while (left
> 0 || right
< max
) {
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
;
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
;
525 rect
.height
= node
.offsetHeight
;
526 while (node
!== null) {
527 rect
.left
+= node
.offsetLeft
;
528 rect
.top
+= node
.offsetTop
;
529 node
= node
.offsetParent
;
532 rect
.left
+= window
.pageXOffset
;
533 rect
.top
+= window
.pageYOffset
;
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.
543 CaretBrowsing
.updateCaretOrSelection = function(scrollToSelection
) {
544 var previousX
= CaretBrowsing
.caretX
;
545 var previousY
= CaretBrowsing
.caretY
;
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';
556 var range
= sel
.getRangeAt(0);
558 if (CaretBrowsing
.caretElement
) {
559 CaretBrowsing
.isSelectionCollapsed
= false;
560 CaretBrowsing
.caretElement
.style
.opacity
= '0.0';
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
;
576 CaretBrowsing
.isSelectionCollapsed
= false;
577 } else if (range
.startOffset
!= range
.endOffset
||
578 range
.startContainer
!= range
.endContainer
) {
579 var rect
= range
.getBoundingClientRect();
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;
589 var rect
= CaretBrowsing
.getCursorRect(
590 new Cursor(range
.startContainer
,
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;
600 if (!CaretBrowsing
.caretElement
) {
601 CaretBrowsing
.createCaretElement();
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';
611 element
.style
.opacity
= '0.0';
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
);
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
)));
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));
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();
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.
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
);
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.
673 CaretBrowsing
.makeAnchorCursor = function(sel
) {
674 return new Cursor(sel
.anchorNode
, sel
.anchorOffset
,
675 TraverseUtil
.getNodeText(sel
.anchorNode
));
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.
683 CaretBrowsing
.makeFocusCursor = function(sel
) {
684 return new Cursor(sel
.focusNode
, sel
.focusOffset
,
685 TraverseUtil
.getNodeText(sel
.focusNode
));
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.
694 CaretBrowsing
.makeLeftCursor = function(sel
) {
695 var range
= sel
.rangeCount
== 1 ? sel
.getRangeAt(0) : null;
697 range
.endContainer
== sel
.anchorNode
&&
698 range
.endOffset
== sel
.anchorOffset
) {
699 return CaretBrowsing
.makeFocusCursor(sel
);
701 return CaretBrowsing
.makeAnchorCursor(sel
);
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.
711 CaretBrowsing
.makeRightCursor = function(sel
) {
712 var range
= sel
.rangeCount
== 1 ? sel
.getRangeAt(0) : null;
714 range
.endContainer
== sel
.anchorNode
&&
715 range
.endOffset
== sel
.anchorOffset
) {
716 return CaretBrowsing
.makeAnchorCursor(sel
);
718 return CaretBrowsing
.makeFocusCursor(sel
);
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.
729 CaretBrowsing
.setAndValidateSelection = function(start
, end
) {
730 var sel
= window
.getSelection();
731 sel
.setBaseAndExtent(start
.node
, start
.index
, end
.node
, end
.index
);
733 if (sel
.rangeCount
!= 1) {
737 return (sel
.anchorNode
== start
.node
&&
738 sel
.anchorOffset
== start
.index
&&
739 sel
.focusNode
== end
.node
&&
740 sel
.focusOffset
== end
.index
);
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.
748 CaretBrowsing
.isCollapsed = function(sel
) {
749 return (sel
.anchorOffset
== sel
.focusOffset
&&
750 sel
.anchorNode
== sel
.focusNode
);
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.
759 CaretBrowsing
.isMoveByWordEvent = function(evt
) {
760 if (CaretBrowsing
.isMac
) {
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.
776 CaretBrowsing
.forwards = function(cursor
, nodesCrossed
) {
777 var previousCursor
= cursor
.clone();
778 var result
= TraverseUtil
.forwardsChar(cursor
, nodesCrossed
);
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) {
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.
799 CaretBrowsing
.backwards = function(cursor
, nodesCrossed
) {
800 var previousCursor
= cursor
.clone();
801 var result
= TraverseUtil
.backwardsChar(cursor
, nodesCrossed
);
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.
807 cursor
.node
!= previousCursor
.node
&&
808 cursor
.index
< cursor
.text
.length
) {
809 cursor
.index
= cursor
.text
.length
;
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.
822 CaretBrowsing
.moveRight = function(evt
) {
823 CaretBrowsing
.targetX
= null;
825 var sel
= window
.getSelection();
826 if (!evt
.shiftKey
&& !CaretBrowsing
.isCollapsed(sel
)) {
827 var right
= CaretBrowsing
.makeRightCursor(sel
);
828 CaretBrowsing
.setAndValidateSelection(right
, right
);
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
= [];
842 if (CaretBrowsing
.isMoveByWordEvent(evt
)) {
843 result
= TraverseUtil
.getNextWord(previousEnd
, end
, nodesCrossed
);
845 previousEnd
= end
.clone();
846 result
= CaretBrowsing
.forwards(end
, nodesCrossed
);
849 if (result
=== null) {
850 return CaretBrowsing
.moveLeft(evt
);
853 if (CaretBrowsing
.setAndValidateSelection(
854 evt
.shiftKey
? start
: end
, end
)) {
860 nodesCrossed
.push(end
.node
);
861 CaretBrowsing
.setFocusToFirstFocusable(nodesCrossed
);
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.
874 CaretBrowsing
.moveLeft = function(evt
) {
875 CaretBrowsing
.targetX
= null;
877 var sel
= window
.getSelection();
878 if (!evt
.shiftKey
&& !CaretBrowsing
.isCollapsed(sel
)) {
879 var left
= CaretBrowsing
.makeLeftCursor(sel
);
880 CaretBrowsing
.setAndValidateSelection(left
, left
);
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
= [];
894 if (CaretBrowsing
.isMoveByWordEvent(evt
)) {
895 result
= TraverseUtil
.getPreviousWord(
896 start
, previousStart
, nodesCrossed
);
898 previousStart
= start
.clone();
899 result
= CaretBrowsing
.backwards(start
, nodesCrossed
);
902 if (result
=== null) {
906 if (CaretBrowsing
.setAndValidateSelection(
907 evt
.shiftKey
? end
: start
, start
)) {
913 nodesCrossed
.push(start
.node
);
914 CaretBrowsing
.setFocusToFirstFocusable(nodesCrossed
);
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
927 * @param {Event} evt The DOM event.
928 * @return {boolean} True if the default action should be performed.
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
);
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
;
948 var previousEnd
= end
.clone();
949 var leftPos
= end
.clone();
950 var rightPos
= end
.clone();
953 var bestDelta
= null;
954 var bestHeight
= null;
955 var nodesCrossed
= [];
958 if (null === CaretBrowsing
.forwards(rightPos
, nodesCrossed
)) {
959 if (CaretBrowsing
.setAndValidateSelection(
960 evt
.shiftKey
? start
: leftPos
, leftPos
)) {
963 return CaretBrowsing
.moveLeft(evt
);
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
;
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
)) {
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
)) {
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();
1001 bestDelta
= deltaLeft
;
1002 bestHeight
= rect
.height
;
1004 var deltaRight
= Math
.abs(CaretBrowsing
.targetX
- rect
.right
);
1005 if (bestDelta
== null || deltaRight
< bestDelta
) {
1006 bestPos
= rightPos
.clone();
1008 bestDelta
= deltaRight
;
1009 bestHeight
= rect
.height
;
1012 // Return the best match so far if the deltas are getting worse,
1014 if (bestDelta
!= null &&
1015 deltaLeft
> bestDelta
&&
1016 deltaRight
> bestDelta
) {
1017 if (CaretBrowsing
.setAndValidateSelection(
1018 evt
.shiftKey
? start
: bestPos
, bestPos
)) {
1026 leftPos
= rightPos
.clone();
1029 if (!evt
.shiftKey
) {
1030 CaretBrowsing
.setFocusToNode(leftPos
.node
);
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.
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
);
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
;
1063 var previousStart
= start
.clone();
1064 var leftPos
= start
.clone();
1065 var rightPos
= start
.clone();
1068 var bestDelta
= null;
1069 var bestHeight
= null;
1070 var nodesCrossed
= [];
1073 if (null === CaretBrowsing
.backwards(leftPos
, nodesCrossed
)) {
1074 CaretBrowsing
.setAndValidateSelection(
1075 evt
.shiftKey
? end
: rightPos
, rightPos
);
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
;
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
)) {
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
)) {
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();
1111 bestDelta
= deltaLeft
;
1112 bestHeight
= rect
.height
;
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();
1119 bestDelta
= deltaRight
;
1120 bestHeight
= rect
.height
;
1123 // Return the best match so far if the deltas are getting worse,
1125 if (bestDelta
!= null &&
1126 deltaLeft
> bestDelta
&&
1127 deltaRight
> bestDelta
) {
1128 if (CaretBrowsing
.setAndValidateSelection(
1129 evt
.shiftKey
? end
: bestPos
, bestPos
)) {
1137 rightPos
= leftPos
.clone();
1140 if (!evt
.shiftKey
) {
1141 CaretBrowsing
.setFocusToNode(rightPos
.node
);
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.
1153 CaretBrowsing
.escapeFromControl = function(control
) {
1156 var start
= new Cursor(control
, 0, '');
1157 var previousStart
= start
.clone();
1158 var end
= new Cursor(control
, 0, '');
1159 var previousEnd
= end
.clone();
1161 var nodesCrossed
= [];
1163 if (null === CaretBrowsing
.backwards(start
, nodesCrossed
)) {
1167 var r
= document
.createRange();
1168 r
.setStart(start
.node
, start
.index
);
1169 r
.setEnd(previousStart
.node
, previousStart
.index
);
1170 if (r
.getBoundingClientRect()) {
1173 previousStart
= start
.clone();
1176 if (null === CaretBrowsing
.forwards(end
, nodesCrossed
)) {
1179 if (isDescendantOfNode(end
.node
, control
)) {
1180 previousEnd
= end
.clone();
1184 var r
= document
.createRange();
1185 r
.setStart(previousEnd
.node
, previousEnd
.index
);
1186 r
.setEnd(end
.node
, end
.index
);
1187 if (r
.getBoundingClientRect()) {
1192 if (!isDescendantOfNode(previousStart
.node
, control
)) {
1193 start
= previousStart
.clone();
1196 if (!isDescendantOfNode(previousEnd
.node
, control
)) {
1197 end
= previousEnd
.clone();
1200 CaretBrowsing
.setAndValidateSelection(start
, end
);
1202 window
.setTimeout(function() {
1203 CaretBrowsing
.updateCaretOrSelection(true);
1208 * Toggle whether caret browsing is enabled or not.
1210 CaretBrowsing
.toggle = function() {
1211 if (CaretBrowsing
.forceEnabled
) {
1212 CaretBrowsing
.recreateCaretElement();
1216 CaretBrowsing
.isEnabled
= !CaretBrowsing
.isEnabled
;
1218 obj
['enabled'] = CaretBrowsing
.isEnabled
;
1219 chrome
.storage
.sync
.set(obj
);
1220 CaretBrowsing
.updateIsCaretVisible();
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.
1228 CaretBrowsing
.onKeyDown = function(evt
) {
1229 if (evt
.defaultPrevented
) {
1233 if (evt
.keyCode
== 118) { // F7
1234 CaretBrowsing
.toggle();
1237 if (!CaretBrowsing
.isEnabled
) {
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();
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();
1263 if (sel
.rangeCount
== 0) {
1268 if (CaretBrowsing
.caretElement
) {
1269 CaretBrowsing
.caretElement
.style
.visibility
= 'visible';
1270 CaretBrowsing
.blinkFlag
= true;
1274 switch (evt
.keyCode
) {
1276 result
= CaretBrowsing
.moveLeft(evt
);
1279 result
= CaretBrowsing
.moveUp(evt
);
1282 result
= CaretBrowsing
.moveRight(evt
);
1285 result
= CaretBrowsing
.moveDown(evt
);
1289 if (result
== false) {
1290 evt
.preventDefault();
1291 evt
.stopPropagation();
1294 window
.setTimeout(function() {
1295 CaretBrowsing
.updateCaretOrSelection(result
== false);
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.
1308 CaretBrowsing
.onClick = function(evt
) {
1309 if (!CaretBrowsing
.isEnabled
) {
1312 window
.setTimeout(function() {
1313 CaretBrowsing
.targetX
= null;
1314 CaretBrowsing
.updateCaretOrSelection(false);
1320 * Called at a regular interval. Blink the cursor by changing its visibility.
1322 CaretBrowsing
.caretBlinkFunction = function() {
1323 if (CaretBrowsing
.caretElement
) {
1324 if (CaretBrowsing
.blinkFlag
) {
1325 CaretBrowsing
.caretElement
.style
.backgroundColor
=
1326 CaretBrowsing
.caretForeground
;
1327 CaretBrowsing
.blinkFlag
= false;
1329 CaretBrowsing
.caretElement
.style
.backgroundColor
=
1330 CaretBrowsing
.caretBackground
;
1331 CaretBrowsing
.blinkFlag
= true;
1337 * Update whether or not the caret is visible, based on whether caret browsing
1338 * is enabled and whether this window / iframe has focus.
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);
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;
1363 * Called when the prefs get updated.
1365 CaretBrowsing
.onPrefsUpdated = function() {
1366 chrome
.storage
.sync
.get(null, function(result
) {
1367 if (!CaretBrowsing
.forceEnabled
) {
1368 CaretBrowsing
.isEnabled
= result
['enabled'];
1370 CaretBrowsing
.onEnable
= result
['onenable'];
1371 CaretBrowsing
.onJump
= result
['onjump'];
1372 CaretBrowsing
.recreateCaretElement();
1377 * Called when this window / iframe gains focus.
1379 CaretBrowsing
.onWindowFocus = function() {
1380 CaretBrowsing
.isWindowFocused
= true;
1381 CaretBrowsing
.updateIsCaretVisible();
1385 * Called when this window / iframe loses focus.
1387 CaretBrowsing
.onWindowBlur = function() {
1388 CaretBrowsing
.isWindowFocused
= false;
1389 CaretBrowsing
.updateIsCaretVisible();
1393 * Initializes caret browsing by adding event listeners and extension
1394 * message listeners.
1396 CaretBrowsing
.init = function() {
1397 CaretBrowsing
.isWindowFocused
= document
.hasFocus();
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);
1405 window
.setTimeout(function() {
1407 // Make sure the script only loads once.
1408 if (!window
['caretBrowsingLoaded']) {
1409 window
['caretBrowsingLoaded'] = true;
1410 CaretBrowsing
.init();
1412 if (document
.body
.getAttribute('caretbrowsing') == 'on') {
1413 CaretBrowsing
.forceEnabled
= true;
1414 CaretBrowsing
.isEnabled
= true;
1415 CaretBrowsing
.updateIsCaretVisible();
1418 chrome
.storage
.onChanged
.addListener(function() {
1419 CaretBrowsing
.onPrefsUpdated();
1421 CaretBrowsing
.onPrefsUpdated();