| 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 |