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