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 |