Fix search results being clipped in app list.
[chromium-blink-merge.git] / ui / accessibility / extensions / caretbrowsing / caretbrowsing.js
blobb8a187cffc165b52e7a6e557bcd8e431e35e6910
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. */
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  */
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   }
78   if (targetNode.tabIndex >= 0) {
79     return true;
80   }
82   if (targetNode.hasAttribute &&
83       targetNode.hasAttribute('tabindex') &&
84       targetNode.getAttribute('tabindex') == '-1') {
85     return true;
86   }
88   return false;
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;
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() {};
123  * Is caret browsing enabled?
124  * @type {boolean}
125  */
126 CaretBrowsing.isEnabled = false;
129  * Keep it enabled even when flipped off (for the options page)?
130  * @type {boolean}
131  */
132 CaretBrowsing.forceEnabled = false;
135  * What to do when the caret appears?
136  * @type {string}
137  */
138 CaretBrowsing.onEnable;
141  * What to do when the caret jumps?
142  * @type {string}
143  */
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
149  * tab.
150  * @type {boolean}
151  */
152 CaretBrowsing.isWindowFocused = false;
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;
162  * The actual caret element, an absolute-positioned flashing line.
163  * @type {Element}
164  */
165 CaretBrowsing.caretElement;
168  * The x-position of the caret, in absolute pixels.
169  * @type {number}
170  */
171 CaretBrowsing.caretX = 0;
174  * The y-position of the caret, in absolute pixels.
175  * @type {number}
176  */
177 CaretBrowsing.caretY = 0;
180  * The width of the caret in pixels.
181  * @type {number}
182  */
183 CaretBrowsing.caretWidth = 0;
186  * The height of the caret in pixels.
187  * @type {number}
188  */
189 CaretBrowsing.caretHeight = 0;
192  * The foregroundc color.
193  * @type {string}
194  */
195 CaretBrowsing.caretForeground = '#000';
198  * The backgroundc color.
199  * @type {string}
200  */
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.
207  * @type {boolean}
208  */
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.
214  * @type {number?}
215  */
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.
226  * @type {number?}
227  */
228 CaretBrowsing.targetX = null;
231  * A flag that flips on or off as the caret blinks.
232  * @type {boolean}
233  */
234 CaretBrowsing.blinkFlag = true;
237  * Whether or not we're on a Mac - affects modifier keys.
238  * @type {boolean}
239  */
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
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   }
256   if (node == document.body || node != document.activeElement) {
257     return false;
258   }
260   if (node.constructor == HTMLSelectElement) {
261     return true;
262   }
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   }
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   }
314   return false;
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   }
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);
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   }
356   return false;
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;
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;
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';
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);
419  * Quick flash and then show the normal caret style.
420  */
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();
438     }
439   }, 250);
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;
453   if (CaretBrowsing.onEnable == 'anim') {
454     CaretBrowsing.animateCaretElement();
455   } else if (CaretBrowsing.onEnable == 'flash') {
456     CaretBrowsing.flashCaretElement();
457   } else {
458     CaretBrowsing.setCaretElementNormalStyle();
459   }
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   }
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;
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;
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   }
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   }
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   }
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   }
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));
639     }
640   }
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   }
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);
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));
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));
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   }
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   }
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);
733   if (sel.rangeCount != 1) {
734     return false;
735   }
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.
747  */
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.
758  */
759 CaretBrowsing.isMoveByWordEvent = function(evt) {
760   if (CaretBrowsing.isMac) {
761     return evt.altKey;
762   } else {
763     return evt.ctrlKey;
764   }
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);
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   }
787   return result;
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);
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   }
812   return result;
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;
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   }
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     }
849     if (result === null) {
850       return CaretBrowsing.moveLeft(evt);
851     }
853     if (CaretBrowsing.setAndValidateSelection(
854             evt.shiftKey ? start : end, end)) {
855       break;
856     }
857   }
859   if (!evt.shiftKey) {
860     nodesCrossed.push(end.node);
861     CaretBrowsing.setFocusToFirstFocusable(nodesCrossed);
862   }
864   return false;
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;
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   }
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     }
902     if (result === null) {
903       break;
904     }
906     if (CaretBrowsing.setAndValidateSelection(
907             evt.shiftKey ? end : start, start)) {
908       break;
909     }
910   }
912   if (!evt.shiftKey) {
913     nodesCrossed.push(start.node);
914     CaretBrowsing.setFocusToFirstFocusable(nodesCrossed);
915   }
917   return false;
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   }
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;
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       }
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       }
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         }
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   }
1029   if (!evt.shiftKey) {
1030     CaretBrowsing.setFocusToNode(leftPos.node);
1031   }
1033   return false;
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   }
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;
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       }
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       }
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         }
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   }
1140   if (!evt.shiftKey) {
1141     CaretBrowsing.setFocusToNode(rightPos.node);
1142   }
1144   return false;
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();
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 = [];
1162   while (true) {
1163     if (null === CaretBrowsing.backwards(start, nodesCrossed)) {
1164       break;
1165     }
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     }
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   }
1192   if (!isDescendantOfNode(previousStart.node, control)) {
1193     start = previousStart.clone();
1194   }
1196   if (!isDescendantOfNode(previousEnd.node, control)) {
1197     end = previousEnd.clone();
1198   }
1200   CaretBrowsing.setAndValidateSelection(start, end);
1202   window.setTimeout(function() {
1203     CaretBrowsing.updateCaretOrSelection(true);
1204   }, 0);
1208  * Toggle whether caret browsing is enabled or not.
1209  */
1210 CaretBrowsing.toggle = function() {
1211   if (CaretBrowsing.forceEnabled) {
1212     CaretBrowsing.recreateCaretElement();
1213     return;
1214   }
1216   CaretBrowsing.isEnabled = !CaretBrowsing.isEnabled;
1217   var obj = {};
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.
1227  */
1228 CaretBrowsing.onKeyDown = function(evt) {
1229   if (evt.defaultPrevented) {
1230     return;
1231   }
1233   if (evt.keyCode == 118) {  // F7
1234     CaretBrowsing.toggle();
1235   }
1237   if (!CaretBrowsing.isEnabled) {
1238     return true;
1239   }
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   }
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     }
1263     if (sel.rangeCount == 0) {
1264       return true;
1265     }
1266   }
1268   if (CaretBrowsing.caretElement) {
1269     CaretBrowsing.caretElement.style.visibility = 'visible';
1270     CaretBrowsing.blinkFlag = true;
1271   }
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   }
1289   if (result == false) {
1290     evt.preventDefault();
1291     evt.stopPropagation();
1292   }
1294   window.setTimeout(function() {
1295     CaretBrowsing.updateCaretOrSelection(result == false);
1296   }, 0);
1298   return result;
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;
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   }
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   }
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   });
1377  * Called when this window / iframe gains focus.
1378  */
1379 CaretBrowsing.onWindowFocus = function() {
1380   CaretBrowsing.isWindowFocused = true;
1381   CaretBrowsing.updateIsCaretVisible();
1385  * Called when this window / iframe loses focus.
1386  */
1387 CaretBrowsing.onWindowBlur = function() {
1388   CaretBrowsing.isWindowFocused = false;
1389   CaretBrowsing.updateIsCaretVisible();
1393  * Initializes caret browsing by adding event listeners and extension
1394  * message listeners.
1395  */
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();
1416     }
1418     chrome.storage.onChanged.addListener(function() {
1419       CaretBrowsing.onPrefsUpdated();
1420     });
1421     CaretBrowsing.onPrefsUpdated();
1422   }
1424 }, 0);