| OLD | NEW |
| (Empty) | |
| 1 // Copyright 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 Support code for the Contextual Search feature. Given a tap |
| 7 * location, locates and highlights the word at that location, and retuns |
| 8 * some contextual data about the selection. |
| 9 * |
| 10 */ |
| 11 |
| 12 |
| 13 /** |
| 14 * Namespace for this file. Depends on __gCrWeb having already been injected. |
| 15 */ |
| 16 __gCrWeb['contextualSearch'] = {}; |
| 17 |
| 18 /* Anyonymizing block */ |
| 19 new function() { |
| 20 |
| 21 /** |
| 22 * Utility loggging function. |
| 23 * @param {string} message Text of log message to be sent to application. |
| 24 */ |
| 25 __gCrWeb['contextualSearch'].cxlog = function(message) { |
| 26 if (Context.debugMode) { |
| 27 console.log('[CS] ' + message); |
| 28 } |
| 29 }; |
| 30 |
| 31 /** |
| 32 * Enables or disabled the selection change notification forward to the |
| 33 * controller. |
| 34 * @param {boolean} enabled whether to turn on (true) or off (false). |
| 35 */ |
| 36 __gCrWeb['contextualSearch'].enableSelectionChangeListener = function(enabled) { |
| 37 if (enabled) { |
| 38 document.addEventListener('selectionchange', |
| 39 Context.selectionChanged, |
| 40 true); |
| 41 } else { |
| 42 document.removeEventListener('selectionchange', |
| 43 Context.selectionChanged, |
| 44 true); |
| 45 } |
| 46 }; |
| 47 |
| 48 __gCrWeb['contextualSearch'].getMutatedElementCount = function() { |
| 49 return Context.mutationCount; |
| 50 }; |
| 51 |
| 52 /** |
| 53 * Enables the DOM mutation event listener. |
| 54 * @param {number} delay after a mutation event before a tap event can be |
| 55 * handled. |
| 56 */ |
| 57 __gCrWeb['contextualSearch'].setMutationObserverDelay = function(delay) { |
| 58 Context.DOMMutationTimeoutMillisecond = delay; |
| 59 |
| 60 // select the target node |
| 61 var target = document.body; |
| 62 |
| 63 // create an observer instance |
| 64 Context.DOMMutationObserver = new MutationObserver(function(mutations) { |
| 65 // Clear any mutation records older than |DOMMutationTimeoutMillisecond|. |
| 66 // Only do this every |DOMMutationTimeoutMillisecond|s to avoid thrashing |
| 67 // on pages with (for example) continuous animations. |
| 68 |
| 69 var d = new Date(); |
| 70 if ((d.getTime() - Context.lastMutationPrune) > |
| 71 Context.DOMMutationTimeoutMillisecond) { |
| 72 Context.processMutations(function(mutationId, mutationTime) { |
| 73 if ((d.getTime() - mutationTime) <= |
| 74 Context.DOMMutationTimeoutMillisecond) { |
| 75 Context.clearMutation(mutationId); |
| 76 } |
| 77 return false; |
| 78 }); |
| 79 Context.lastMutationPrune = d.getTime(); |
| 80 } |
| 81 |
| 82 mutations.forEach(function(mutation) { |
| 83 // Don't count mutations with invalid targets. |
| 84 if (!mutation.target) { |
| 85 return; |
| 86 } |
| 87 // Don't count mutations to invisible elements. |
| 88 if (mutation.type != 'characterData' && !mutation.target.offsetParent) { |
| 89 return; |
| 90 } |
| 91 // Don't count attribute mutations where the attribute's current and |
| 92 // old values are the same. Also don't count attribute mutation where |
| 93 // the current and old values are both "false-ish" (so changing a null to |
| 94 // an empty string has no effect). |
| 95 if (mutation.type == 'attributes') { |
| 96 if (mutation.target.getAttribute(mutation.attributeName) == |
| 97 mutation.oldValue) { |
| 98 return; |
| 99 } |
| 100 if (!mutation.target.getAttribute(mutation.attributeName) && |
| 101 !mutation.oldValue) { |
| 102 return; |
| 103 } |
| 104 // Don't count the attribute mutation from setting an ID for tracking or |
| 105 // if it is unsetting the ID. |
| 106 if (mutation.attributeName == 'id' && |
| 107 (mutation.target.id.match(/__CTXSM___\d__\d+/) || |
| 108 mutation.target.id == '')) { |
| 109 return; |
| 110 } |
| 111 } |
| 112 |
| 113 if (mutation.type == 'characterData' && |
| 114 mutation.target.textContent == mutation.oldValue) { |
| 115 return; |
| 116 } |
| 117 |
| 118 // If this is the first mutation after tap, and the mutation target |
| 119 // intersects with the highlighted elements, forward it to the Chrome |
| 120 // application to check if CS must be dismissed. |
| 121 if (Context.highlightRange && |
| 122 !Context.mutationEventForwarded && |
| 123 Context.highlightRange.intersectsNode(mutation.target)) { |
| 124 Context.mutationEventForwarded = true; |
| 125 __gCrWeb.message.invokeOnHost( |
| 126 {'command' : 'contextualSearch.mutationEvent'}); |
| 127 } |
| 128 |
| 129 Context.lastDOMMutationMillisecond = d.getTime(); |
| 130 // If the mutated item isn't an element, find its parent. |
| 131 // If the element doesn't have an ID, assign one to it. |
| 132 var idContainer = mutation.target; |
| 133 if (mutation.type == 'characterData') { |
| 134 idContainer = idContainer.parentElement; |
| 135 } |
| 136 var id = idContainer.id; |
| 137 if (!id) { |
| 138 id = Context.newID(Context.lastDOMMutationMillisecond); |
| 139 idContainer.id = id; |
| 140 } |
| 141 Context.recordMutation(id, Context.lastDOMMutationMillisecond); |
| 142 }); |
| 143 }); |
| 144 |
| 145 // configuration of the observer: |
| 146 var config = { |
| 147 attributes: true, |
| 148 characterData: true, |
| 149 subtree: true, |
| 150 attributeOldValue: true, |
| 151 characterDataOldValue: true |
| 152 }; |
| 153 |
| 154 // pass in the target node, as well as the observer options |
| 155 Context.DOMMutationObserver.observe(target, config); |
| 156 }; |
| 157 |
| 158 /** |
| 159 * Enables the body touch end event listener. This will catch touch events that |
| 160 * don't call preventDefault. |
| 161 * @param {number} delay after a mutation event before a tap event can be |
| 162 * handled. |
| 163 */ |
| 164 __gCrWeb['contextualSearch'].setBodyTouchListenerDelay = function(delay) { |
| 165 Context.touchEventTimeoutMillisecond = delay; |
| 166 Context.bodyTouchEndEventListener = function(event) { |
| 167 if (!event.defaultPrevented) { |
| 168 var d = new Date(); |
| 169 Context.lastTouchEventMillisecond = d.getTime(); |
| 170 } else { |
| 171 __gCrWeb['contextualSearch'].cxlog('Touch default prevented'); |
| 172 } |
| 173 }; |
| 174 document.body.addEventListener('touchend', Context.bodyTouchEndEventListener, |
| 175 false); |
| 176 }; |
| 177 |
| 178 /** |
| 179 * Disables the DOM mutation listener. |
| 180 */ |
| 181 __gCrWeb['contextualSearch'].disableMutationObserver = function() { |
| 182 Context.DOMMutationObserver.disconnect(); |
| 183 Context.DOMMutationObserver = null; |
| 184 Context.lastDOMMutationMillisecond = 0; |
| 185 }; |
| 186 |
| 187 /** |
| 188 * Disables the body touchend listener. |
| 189 */ |
| 190 __gCrWeb['contextualSearch'].disableBodyTouchListener = function() { |
| 191 document.body.removeEventListener('touchend', |
| 192 Context.bodyTouchEndEventListener, false); |
| 193 Context.lastTouchEventMillisecond = 0; |
| 194 }; |
| 195 |
| 196 /** |
| 197 * Expands the highlight to [startOffset, endOffset] in the surrounding range. |
| 198 * @param {number} startOffset first character to include in the range. |
| 199 * @param {number} endOffset last character to include in the range. |
| 200 * @return {JSON} new highlighted rects. |
| 201 */ |
| 202 __gCrWeb['contextualSearch'].expandHighlight = |
| 203 function(startOffset, endOffset) { |
| 204 if ((startOffset == Context.surroundingRange.highlightStartOffset && |
| 205 endOffset == Context.surroundingRange.highlightEndOffset) || |
| 206 startOffset > endOffset) { |
| 207 return; |
| 208 } |
| 209 var range = Context.createSurroundingRange(startOffset, endOffset); |
| 210 Context.highlightRange = range; |
| 211 return __gCrWeb['contextualSearch'].highlightRects(); |
| 212 }; |
| 213 |
| 214 /** |
| 215 * Returns the rects to draw the current highlight. |
| 216 * @return {JSON} hilighted rects. |
| 217 */ |
| 218 __gCrWeb['contextualSearch'].highlightRects = function() { |
| 219 return __gCrWeb.stringify( |
| 220 {'rects' : Context.getHighlightRects(), |
| 221 'size': {'width' : document.documentElement.scrollWidth, |
| 222 'height' : document.documentElement.scrollHeight |
| 223 }}); |
| 224 }; |
| 225 |
| 226 /** |
| 227 * Clears the current highlight. |
| 228 */ |
| 229 __gCrWeb['contextualSearch'].clearHighlight = function() { |
| 230 Context.highlightRange = null; |
| 231 }; |
| 232 |
| 233 /** |
| 234 * Retrieve the currently highlighted string. |
| 235 * This is used for test purposes only. |
| 236 * @return {string} the currently highlighted string. |
| 237 */ |
| 238 __gCrWeb['contextualSearch'].retrieveHighlighted = function() { |
| 239 return Context.rangeToString(Context.highlightRange); |
| 240 }; |
| 241 |
| 242 /** |
| 243 * Prepares Contextual Search for a given point in the window. This method |
| 244 * will find which word is located at the given point, extract the context |
| 245 * data for that word, and (if a word was located), pass the context data |
| 246 * back to the calling application. |
| 247 * @param {number} x The point's x coordinate as a ratio of page width. |
| 248 * @param {number} y The point's y coordinate as a ratio of page height. |
| 249 * @return {object} Empty if no word was found, or the context, x and y if |
| 250 * a word was found. |
| 251 */ |
| 252 __gCrWeb['contextualSearch'].handleTapAtPoint = function(x, y) { |
| 253 var tapResults; |
| 254 if (window.getSelection().toString()) { |
| 255 tapResults = new ContextData(); |
| 256 tapResults.error = 'Failed: selection is not empty.'; |
| 257 return __gCrWeb.stringify({'context' : tapResults.returnContext()}); |
| 258 } |
| 259 var d = new Date(); |
| 260 var lastTouchDelta = d.getTime() - Context.lastTouchEventMillisecond; |
| 261 if (Context.touchEventTimeoutMillisecond && |
| 262 lastTouchDelta > Context.touchEventTimeoutMillisecond) { |
| 263 tapResults = new ContextData(); |
| 264 tapResults.error = 'Failed: last touch was ' + lastTouchDelta + |
| 265 'ms ago (>' + Context.touchEventTimeoutMillisecond + 'ms timeout)'; |
| 266 } else { |
| 267 var absoluteX = |
| 268 x * document.documentElement.scrollWidth - document.body.scrollLeft; |
| 269 var absoluteY = |
| 270 y * document.documentElement.scrollHeight - document.body.scrollTop; |
| 271 tapResults = Context.getContextDataFromPoint(absoluteX, absoluteY); |
| 272 |
| 273 if (!tapResults.error) { |
| 274 var range = tapResults.range; |
| 275 if (!range) { |
| 276 tapResults.error = 'Failed: context data range was empty'; |
| 277 } else if (tapResults.surroundingText.length < tapResults.offsetEnd) { |
| 278 tapResults.error = |
| 279 'Failed: surrounding text is shorter than text offset'; |
| 280 } else if (tapResults.getSelectedText() != tapResults.selectedText) { |
| 281 tapResults.error = 'Failed: offsets do not match selected text: (' + |
| 282 tapResults.getSelectedText() + ') vs. (' + |
| 283 tapResults.selectedText + ')'; |
| 284 } |
| 285 } |
| 286 } |
| 287 Context.mutationEventForwarded = false; |
| 288 return __gCrWeb.stringify({'context' : tapResults.returnContext()}); |
| 289 }; |
| 290 |
| 291 //------------------------------------------------------------------------------ |
| 292 // ContextData |
| 293 //------------------------------------------------------------------------------ |
| 294 |
| 295 /** |
| 296 * @constructor |
| 297 */ |
| 298 var ContextData = function() {}; |
| 299 |
| 300 /** |
| 301 * An error message, if any, associated with the context. |
| 302 * @type {?string} |
| 303 */ |
| 304 ContextData.prototype.error = null; |
| 305 |
| 306 /** |
| 307 * The range containing the selected text. |
| 308 * @type {?Range} |
| 309 */ |
| 310 ContextData.prototype.range = null; |
| 311 |
| 312 /** |
| 313 * The URL from where the context was extracted. |
| 314 * @type {?string} |
| 315 */ |
| 316 ContextData.prototype.url = null; |
| 317 |
| 318 /** |
| 319 * The selected text. |
| 320 * @type {?string} |
| 321 */ |
| 322 ContextData.prototype.selectedText = null; |
| 323 |
| 324 /** |
| 325 * The surrounding text. |
| 326 * @type {?string} |
| 327 */ |
| 328 ContextData.prototype.surroundingText = null; |
| 329 |
| 330 /** |
| 331 * The start position of the selected text relative to the surrounding text. |
| 332 * @type {?number} |
| 333 */ |
| 334 ContextData.prototype.offsetStart = null; |
| 335 |
| 336 /** |
| 337 * The end position of the selected text relative to the surrounding text. |
| 338 * @type {?number} |
| 339 */ |
| 340 ContextData.prototype.offsetEnd = null; |
| 341 |
| 342 /** |
| 343 * The rewritten query. |
| 344 * @type {?string} |
| 345 */ |
| 346 ContextData.prototype.rewrittenQuery = null; |
| 347 |
| 348 /** |
| 349 * Gets the search query for the context. |
| 350 * @return {?string} The search query. |
| 351 */ |
| 352 ContextData.prototype.getQuery = function() { |
| 353 return this.rewrittenQuery || this.selectedText; |
| 354 }; |
| 355 |
| 356 /** |
| 357 * @return {string} The part of the surrounding text before the selected text. |
| 358 */ |
| 359 ContextData.prototype.getTextBefore = function() { |
| 360 var selectedText = this.selectedText; |
| 361 var surroundingText = this.surroundingText; |
| 362 var result = ''; |
| 363 if (!this.rewrittenQuery && surroundingText) { |
| 364 result = surroundingText.substring(0, this.offsetStart); |
| 365 } |
| 366 return result; |
| 367 }; |
| 368 |
| 369 /** |
| 370 * @return {string} The part of the surrounding text after the selected text. |
| 371 */ |
| 372 ContextData.prototype.getTextAfter = function() { |
| 373 var selectedText = this.selectedText; |
| 374 var surroundingText = this.surroundingText; |
| 375 var result = ''; |
| 376 if (!this.rewrittenQuery && selectedText && surroundingText) { |
| 377 result = surroundingText.substring(this.offsetEnd); |
| 378 } |
| 379 return result; |
| 380 }; |
| 381 |
| 382 /** |
| 383 * @return {string} The selected text as indicated by offsetStart and |
| 384 * offsetEnd. This should be the same as selectedText |
| 385 */ |
| 386 ContextData.prototype.getSelectedText = function() { |
| 387 var surroundingText = this.surroundingText; |
| 388 var result = ''; |
| 389 if (!this.rewrittenQuery && surroundingText) { |
| 390 result = surroundingText.substring(this.offsetStart, this.offsetEnd); |
| 391 } |
| 392 return result; |
| 393 }; |
| 394 |
| 395 /** |
| 396 * @return {JSONDictionary} Context data assembeld for return to native app. |
| 397 */ |
| 398 ContextData.prototype.returnContext = function() { |
| 399 var context = {'url' : this.url, |
| 400 'selectedText' : this.selectedText, |
| 401 'surroundingText' : this.surroundingText, |
| 402 'offsetStart' : this.offsetStart, |
| 403 'offsetEnd' : this.offsetEnd, |
| 404 'rects': Context.getHighlightRects() |
| 405 }; |
| 406 if (this.error) { |
| 407 context['error'] = this.error; |
| 408 } |
| 409 return context; |
| 410 }; |
| 411 |
| 412 //------------------------------------------------------------------------------ |
| 413 // Context |
| 414 //------------------------------------------------------------------------------ |
| 415 |
| 416 var Context = {}; |
| 417 |
| 418 /** |
| 419 * Whether to send log output to the host. |
| 420 * @type {bool} |
| 421 */ |
| 422 Context.debugMode = false; |
| 423 |
| 424 /** |
| 425 * The maximium amount of time that should be spent searching for a text range, |
| 426 * in milliseconds. If the search does not finish within the specified value, |
| 427 * it should terminate without returning a result. |
| 428 * @const {number} |
| 429 */ |
| 430 Context.GET_RANGE_TIMEOUT_MS = 50; |
| 431 |
| 432 /** |
| 433 * Number of surrouding sentences when calculating the surrounding text. |
| 434 * @const {number} |
| 435 */ |
| 436 Context.NUMBER_OF_CHARS_IN_SURROUNDING_SENTENCES = 1500; |
| 437 |
| 438 /** |
| 439 * Maximum number of chars for a selection to trigger the search. |
| 440 * @const {number} |
| 441 */ |
| 442 Context.MAX_NUMBER_OF_CHARS_IN_SELECTION = 100; |
| 443 |
| 444 /** |
| 445 * Last range returned by Context.extractSurroundingDataFromRange. |
| 446 * @type {JSONObject} Contains startContainer, endContainer, startOffset, |
| 447 * endOffset of the surrounding range and relative position of the tapped word. |
| 448 */ |
| 449 Context.surroundingRange = null; |
| 450 |
| 451 /** |
| 452 * The range that is curently highlighted. |
| 453 * @type {Range} |
| 454 */ |
| 455 Context.highlightRange = null; |
| 456 |
| 457 /** |
| 458 * A boolean to check if a mutation event has been forwarded after the latest |
| 459 * tap. |
| 460 * @type {boolean} |
| 461 */ |
| 462 Context.mutationEventForwarded = false; |
| 463 |
| 464 /** |
| 465 * A Regular Expression that matches Unicode word characters. |
| 466 * @type {RegExp} |
| 467 */ |
| 468 Context.reWordCharacter_ = |
| 469 /[\u00C0-\u1FFF\u2C00-\uD7FF\w]/; |
| 470 |
| 471 /** |
| 472 * An observer of the DOM mutation. |
| 473 * @type {MutationObserver} |
| 474 */ |
| 475 Context.DOMMutationObserver = null; |
| 476 |
| 477 /** |
| 478 * The date of the last DOM mutation (in ms). |
| 479 * @type {number} |
| 480 */ |
| 481 Context.lastDOMMutationMillisecond = 0; |
| 482 |
| 483 /** |
| 484 * A hash of timestamps keyed by element-id. |
| 485 * @type {Object} |
| 486 */ |
| 487 Context.mutatedElements = {}; |
| 488 |
| 489 /** |
| 490 * A running count of tracked mutated objects. |
| 491 * @type {number} |
| 492 */ |
| 493 Context.mutationCount = 0; |
| 494 |
| 495 /** |
| 496 * Date of the last time the mutation list was pruned of old entries (in ms). |
| 497 * @type {number} |
| 498 */ |
| 499 Context.lastMutationPrune = 0; |
| 500 |
| 501 /** |
| 502 * An incrementing integer for generating temporary element ids when needed. |
| 503 * @type {number} |
| 504 */ |
| 505 Context.mutationIdCounter = 0; |
| 506 |
| 507 /** |
| 508 * A snapshot of the previous text selection (if any), used to determine if a |
| 509 * selection change is a new selection or not. previousSelection stores the |
| 510 * anchor and focus nodes and offsets of the previously-reported selection. |
| 511 * @type {Object} |
| 512 */ |
| 513 Context.previousSelection = null; |
| 514 |
| 515 /** |
| 516 * Generates a string suitable for use as a temporary element id. |
| 517 * @param {string} nonce A string that varies based on the current time. |
| 518 * @return {string} A string to be used as an element id. |
| 519 */ |
| 520 Context.newID = function(nonce) { |
| 521 return '__CTXSM___' + (Context.mutationIdCounter++) + '__' + nonce; |
| 522 }; |
| 523 |
| 524 /** |
| 525 * The timeout of DOM mutation after which a contextual search can be triggered |
| 526 * (in ms) |
| 527 * @type {number} |
| 528 */ |
| 529 Context.DOMMutationTimeoutMillisecond = 200; |
| 530 |
| 531 /** |
| 532 * An observer of body to catch unhandled touch events. |
| 533 * @type {EventListener} |
| 534 */ |
| 535 Context.bodyTouchEndEventListener = null; |
| 536 |
| 537 /** |
| 538 * The date of the last unhandled touch event (in ms). |
| 539 * @type {number} |
| 540 */ |
| 541 Context.lastTouchEventMillisecond = 0; |
| 542 |
| 543 /** |
| 544 * The timeout of DOM mutation after which a contextual search can be triggered |
| 545 * (in ms) |
| 546 * @type {number} |
| 547 */ |
| 548 Context.touchEventTimeoutMillisecond = 0; |
| 549 |
| 550 /** |
| 551 * List of node types whose contents should not be parsed by Contextual Search. |
| 552 * @type {Array.<string>} |
| 553 */ |
| 554 Context['invalidElements_'] = [ |
| 555 'A', |
| 556 'APPLET', |
| 557 'AREA', |
| 558 'AUDIO', |
| 559 'BUTTON', |
| 560 'CANVAS', |
| 561 'EMBED', |
| 562 'FRAME', |
| 563 'FRAMESET', |
| 564 'IFRAME', |
| 565 'IMG', |
| 566 'INPUT', |
| 567 'KEYGEN', |
| 568 'LABEL', |
| 569 'MAP', |
| 570 'OBJECT', |
| 571 'OPTGROUP', |
| 572 'OPTION', |
| 573 'PROGRESS', |
| 574 'SCRIPT', |
| 575 'SELECT', |
| 576 'TEXTAREA', |
| 577 'VIDEO' |
| 578 ]; |
| 579 |
| 580 /** |
| 581 * List of ARIA roles that define widgets. |
| 582 * For more info, see: http://www.w3.org/TR/wai-aria/roles#widget_roles |
| 583 * @type {Array.<string>} |
| 584 */ |
| 585 Context['widgetRoles_'] = [ |
| 586 'alert', |
| 587 'alertdialog', |
| 588 'button', |
| 589 'checkbox', |
| 590 'dialog', |
| 591 'gridcell', |
| 592 'link', |
| 593 'log', |
| 594 'marquee', |
| 595 'menuitem', |
| 596 'menuitemcheckbox', |
| 597 'menuitemradio', |
| 598 'option', |
| 599 'progressbar', |
| 600 'radio', |
| 601 'scrollbar', |
| 602 'slider', |
| 603 'spinbutton', |
| 604 'status', |
| 605 'tab', |
| 606 'tabpanel', |
| 607 'textbox', |
| 608 'timer', |
| 609 'tooltip', |
| 610 'treeitem' |
| 611 ]; |
| 612 |
| 613 /** |
| 614 * List of ARIA roles that define composite widgets. |
| 615 * For more info, see: http://www.w3.org/TR/wai-aria/roles#widget_roles |
| 616 * @type {Array.<string>} |
| 617 */ |
| 618 Context['compositeWidgetRoles_'] = [ |
| 619 'combobox', |
| 620 'grid', |
| 621 'listbox', |
| 622 'menu', |
| 623 'menubar', |
| 624 'radiogroup', |
| 625 'tablist', |
| 626 'tree', |
| 627 'treegrid' |
| 628 ]; |
| 629 |
| 630 /** |
| 631 * Mutation record handling |
| 632 */ |
| 633 |
| 634 /** |
| 635 * Records a DOM mutation. |
| 636 * @param {string} mutationId The id of the mutated DOM element. |
| 637 * @param {number} mutationTime The time of the mutation. |
| 638 */ |
| 639 Context.recordMutation = function(mutationId, mutationTime) { |
| 640 Context.mutatedElements[mutationId] = mutationTime; |
| 641 Context.mutationCount += 1; |
| 642 }; |
| 643 |
| 644 /** |
| 645 * Clears the record of a DOM mutation. |
| 646 * @param {string} mutationId The id of the mutated DOM element. |
| 647 */ |
| 648 Context.clearMutation = function(mutationId) { |
| 649 delete Context.mutatedElements[mutationId]; |
| 650 Context.mutationCount -= 1; |
| 651 }; |
| 652 |
| 653 /** |
| 654 * Performs some operation on all recorded mutations, passing the mutated node |
| 655 * id and mutation time into func. |
| 656 * @param {function} func The function to apply to the recorded mutations. |
| 657 */ |
| 658 Context.processMutations = function(func) { |
| 659 for (mutationId in Context.mutatedElements) { |
| 660 var mutationTime = Context.mutatedElements[mutationId]; |
| 661 if (func(mutationId, mutationTime)) { |
| 662 break; |
| 663 } |
| 664 } |
| 665 }; |
| 666 |
| 667 /** |
| 668 * Returns whether the selection is valid to trigger a contextual search. |
| 669 * An invalid selection is a selection that is either too long or contains a |
| 670 * single latin character (there are some site that use x's or o's as crosses or |
| 671 * circles), or is included or contains invalid elements. |
| 672 * @param {selection} selection The current selection to test. |
| 673 * @return {boolean} Whether selection should trigger contextual search. |
| 674 */ |
| 675 Context.isSelectionValid = function(selection) { |
| 676 var selectionText = selection.toString(); |
| 677 var length = selectionText.length; |
| 678 if (length > Context.MAX_NUMBER_OF_CHARS_IN_SELECTION) { |
| 679 return false; |
| 680 } |
| 681 if (length == 1 && selectionText.codePointAt(0) < 256) { |
| 682 return false; |
| 683 } |
| 684 |
| 685 var rangeCount = selection.rangeCount; |
| 686 for (var rangeIndex = 0; rangeIndex < rangeCount; rangeIndex++) { |
| 687 // Test if the selection is inside an invalid element. |
| 688 var range = window.getSelection().getRangeAt(rangeIndex); |
| 689 var element = range.commonAncestorContainer; |
| 690 while (element) { |
| 691 if (element.nodeType == element.ELEMENT_NODE && |
| 692 !Context.isValidElement(element)) { |
| 693 return false; |
| 694 } |
| 695 element = element.parentElement; |
| 696 } |
| 697 |
| 698 // Test if the selection contains an invalid element. |
| 699 var startNode = range.startContainer.childNodes[range.startOffset] || |
| 700 range.startContainer; |
| 701 var endNode = range.endContainer.childNodes[range.endOffset] || |
| 702 range.endContainer; |
| 703 element = startNode; |
| 704 while (element) { |
| 705 if (element.nodeType == element.ELEMENT_NODE && |
| 706 !Context.isValidElement(element)) { |
| 707 return false; |
| 708 } |
| 709 element = Context.getNextNode(element, endNode, false); |
| 710 } |
| 711 } |
| 712 return true; |
| 713 }; |
| 714 |
| 715 /** |
| 716 * Forwards the selection changed notification to the controller class. |
| 717 */ |
| 718 Context.selectionChanged = function() { |
| 719 var newSelection = window.getSelection(); |
| 720 if (!newSelection.toString()) { |
| 721 Context.previousSelection = null; |
| 722 return; |
| 723 } |
| 724 var updated = false; |
| 725 if (Context.previousSelection) { |
| 726 updated = |
| 727 (Context.previousSelection.anchorNode == newSelection.anchorNode && |
| 728 Context.previousSelection.anchorOffset == newSelection.anchorOffset) || |
| 729 (Context.previousSelection.focusNode == newSelection.focusNode && |
| 730 Context.previousSelection.focusOffset == newSelection.focusOffset); |
| 731 } |
| 732 var selectionText = newSelection.toString(); |
| 733 var valid = true; |
| 734 if (!Context.isSelectionValid(newSelection)) { |
| 735 // Mark selection as invalid. |
| 736 selectionText = ''; |
| 737 valid = false; |
| 738 } |
| 739 |
| 740 __gCrWeb.message.invokeOnHost( |
| 741 {'command' : 'contextualSearch.selectionChanged', |
| 742 'text' : selectionText, |
| 743 'updated' : updated, |
| 744 'valid' : valid |
| 745 }); |
| 746 |
| 747 // Snapshot the selection for comparison. |
| 748 Context.previousSelection = { |
| 749 'anchorNode' : newSelection.anchorNode, |
| 750 'anchorOffset' : newSelection.anchorOffset, |
| 751 'focusNode' : newSelection.focusNode, |
| 752 'focusOffset' : newSelection.focusOffset |
| 753 }; |
| 754 }; |
| 755 |
| 756 /** |
| 757 * Gets the data necessary to create a Contextual Search from a given point |
| 758 * in the window. |
| 759 * @param {number} x The point's x coordinate. |
| 760 * @param {number} y The point's y coordinate. |
| 761 * @return {ContextData} The object describing the context. |
| 762 */ |
| 763 Context.getContextDataFromPoint = function(x, y) { |
| 764 var contextData = Context.contextFromPoint(x, y); |
| 765 |
| 766 if (contextData.error) { |
| 767 return contextData; |
| 768 } |
| 769 var range = contextData.range = Context.getWordRangeFromPoint(x, y); |
| 770 if (range) { |
| 771 contextData.selectedText = range.toString(); |
| 772 contextData.url = location.href; |
| 773 Context.extractSurroundingDataFromRange(contextData, range); |
| 774 Context.highlightRange = range; |
| 775 } |
| 776 |
| 777 return contextData; |
| 778 }; |
| 779 |
| 780 /** |
| 781 * Checks whether the context in a given point is valid. A context will be |
| 782 * valid when the element at the given point is not interactive or editable. |
| 783 * @param {number} x The point's x coordinate. |
| 784 * @param {number} y The point's y coordinate. |
| 785 * @return {ContextData} Context data from the point, an error set if invalid. |
| 786 * @private |
| 787 */ |
| 788 Context.contextFromPoint = function(x, y) { |
| 789 // Should this use core.js's elementFromPoint_() instead? |
| 790 var contextData = new ContextData(); |
| 791 var element = document.elementFromPoint(x, y); |
| 792 if (!element) { |
| 793 contextData.error = "Failed: Couldn't locate an element at " + x + ', ' + y; |
| 794 return contextData; |
| 795 } |
| 796 |
| 797 var d = new Date(); |
| 798 var lastDOM = d.getTime() - Context.lastDOMMutationMillisecond; |
| 799 if (lastDOM <= Context.DOMMutationTimeoutMillisecond) { |
| 800 Context.processMutations(function(mutationId, mutationTime) { |
| 801 var mutatedElement = document.getElementById(mutationId); |
| 802 if (!mutatedElement) { |
| 803 Context.clearMutation(mutationId); |
| 804 } else { |
| 805 var lastElementMutation = d.getTime() - mutationTime; |
| 806 if (lastElementMutation < 0 || |
| 807 (lastElementMutation > Context.DOMMutationTimeoutMillisecond)) { |
| 808 return false; // mutation expired, continue. |
| 809 } |
| 810 if (element.contains(mutatedElement) || |
| 811 mutatedElement.contains(element)) { |
| 812 contextData.error = 'Failed: Tap was in element mutated ' + |
| 813 lastElementMutation + 'ms ago (<' + |
| 814 Context.DOMMutationTimeoutMillisecond + 'ms interval)'; |
| 815 return true; // break from processing mutations |
| 816 } |
| 817 } |
| 818 return false; // continue processing mutations |
| 819 }); |
| 820 if (contextData.error) |
| 821 return contextData; |
| 822 } |
| 823 |
| 824 while (element) { |
| 825 if (element.nodeType == element.ELEMENT_NODE && |
| 826 !Context.isValidElement(element, false)) { |
| 827 contextData.error = |
| 828 'Failed: Tap was in an invalid (' + element.nodeName + ') element'; |
| 829 return contextData; |
| 830 } |
| 831 element = element.parentElement; |
| 832 } |
| 833 |
| 834 return contextData; |
| 835 }; |
| 836 |
| 837 /** |
| 838 * Checks whether the given element can be used as a touch target. |
| 839 * @see Context.isValidContextFromPoint_ |
| 840 * @param {Element} element The element in question. |
| 841 * @param {boolean} forDisplay Whether we are testing if an element is valid |
| 842 * for tap handling (false) or for display (true). |
| 843 * @return {boolean} Whether the element is a valid context. |
| 844 * @private |
| 845 */ |
| 846 Context.isValidElement = function(element, forDisplay) { |
| 847 |
| 848 if (element.nodeName == 'A') { |
| 849 return forDisplay; |
| 850 } |
| 851 |
| 852 if (Context.invalidElements_.indexOf(element.nodeName) != -1) { |
| 853 __gCrWeb['contextualSearch'].cxlog( |
| 854 'Failed: ' + element.nodeName + ' element was invalid'); |
| 855 return false; |
| 856 } |
| 857 |
| 858 if (element.getAttribute('contenteditable')) { |
| 859 __gCrWeb['contextualSearch'].cxlog( |
| 860 'Failed: ' + element.nodeName + ' element was editable'); |
| 861 return false; |
| 862 } |
| 863 |
| 864 var role = element.getAttribute('role'); |
| 865 if (Context.widgetRoles_.indexOf(role) != -1 || |
| 866 Context.compositeWidgetRoles_.indexOf(role) != -1) { |
| 867 __gCrWeb['contextualSearch'].cxlog( |
| 868 'Failed: ' + element.nodeName + ' role ' + role + ' was invalid'); |
| 869 return false; |
| 870 } |
| 871 |
| 872 if (forDisplay) { |
| 873 var style = window.getComputedStyle(element); |
| 874 if (style.display === 'none') { |
| 875 __gCrWeb['contextualSearch'].cxlog( |
| 876 'Failed: ' + element.nodeName + ' hidden'); |
| 877 return false; |
| 878 } |
| 879 } |
| 880 |
| 881 return true; |
| 882 }; |
| 883 |
| 884 /** |
| 885 * Gets the word range located at a given point. This method will find the |
| 886 * word whose bounding rectangle contains the given point. |
| 887 * @param {number} x The point's x coordinate. |
| 888 * @param {number} y The point's y coordinate. |
| 889 * @return {Range} The word range at the given point. |
| 890 * @private |
| 891 */ |
| 892 Context.getWordRangeFromPoint = function(x, y) { |
| 893 var element = document.elementFromPoint(x, y); |
| 894 var range = null; |
| 895 try { |
| 896 range = Context.findWordRangeFromPointRecursive(element, x, y); |
| 897 } catch (e) { |
| 898 __gCrWeb['contextualSearch'].cxlog( |
| 899 'Recursive word find failed: ' + e.message); |
| 900 } |
| 901 |
| 902 return range; |
| 903 }; |
| 904 |
| 905 /** |
| 906 * Recursively gets the word range located at a given point. |
| 907 * @see Context.getWordRangeFromPoint_ |
| 908 * @param {Node} node The node being inspected. |
| 909 * @param {number} x The point's x coordinate. |
| 910 * @param {number} y The point's y coordinate. |
| 911 * @return {Range} The word range at the given point. |
| 912 * @private |
| 913 */ |
| 914 Context.findWordRangeFromPointRecursive = function(node, x, y) { |
| 915 if (!node) { |
| 916 return null; |
| 917 } |
| 918 |
| 919 if (node.nodeType == node.TEXT_NODE) { |
| 920 var position = Context.findCharacterPositionInTextFromPoint(node, x, y); |
| 921 if (position == -1) { |
| 922 return null; |
| 923 } |
| 924 |
| 925 var range = node.ownerDocument.createRange(); |
| 926 range.setStart(node, position); |
| 927 range.setEnd(node, position); |
| 928 range.expand('word'); |
| 929 |
| 930 if (Context.rangeContainsPoint(range, x, y)) { |
| 931 return range; |
| 932 } |
| 933 |
| 934 if (range) { |
| 935 range.detach(); |
| 936 } |
| 937 } else if (node.nodeType == node.ELEMENT_NODE) { |
| 938 var childNodes = node.childNodes; |
| 939 var childNodesLength = childNodes.length; |
| 940 for (var i = 0, length = childNodesLength; i < length; i++) { |
| 941 var childNode = childNodes[i]; |
| 942 var range = childNode.ownerDocument.createRange(); |
| 943 range.selectNodeContents(childNode); |
| 944 |
| 945 if (Context.rangeContainsPoint(range, x, y)) { |
| 946 range.detach(); |
| 947 return Context.findWordRangeFromPointRecursive(childNode, x, y); |
| 948 } else { |
| 949 range.detach(); |
| 950 } |
| 951 } |
| 952 } |
| 953 |
| 954 return null; |
| 955 }; |
| 956 |
| 957 /** |
| 958 * Gets the position of the character range located at a given point. This |
| 959 * method will find the single character whose bounding rectangle contains |
| 960 * the given point and return the position of that character in the text |
| 961 * node string. If not character is found this method returns -1. |
| 962 * @see Context.findWordRangeFromPointRecursive_ |
| 963 * @param {number} x The point's x coordinate. |
| 964 * @param {number} y The point's y coordinate. |
| 965 * @param {Text} node The text node being inspected. |
| 966 * @return {number} The position of the character in the text node string. |
| 967 * @private |
| 968 */ |
| 969 Context.findCharacterPositionInTextFromPoint = function(node, x, y) { |
| 970 var startTime = new Date().getTime(); |
| 971 |
| 972 var start = 0; |
| 973 var end = node.textContent.length - 1; |
| 974 |
| 975 // Performs a binary search to find a single character whose bouding |
| 976 // rectangle contains the given point. |
| 977 var range = document.createRange(); |
| 978 while (!found || (end - start + 1) > 1) { |
| 979 if ((new Date().getTime() - startTime) > Context.GET_RANGE_TIMEOUT_MS) { |
| 980 __gCrWeb['contextualSearch'].cxlog('Timed out!'); |
| 981 break; |
| 982 } |
| 983 |
| 984 var middle = Math.floor((start + end) / 2); |
| 985 range.setStart(node, start); |
| 986 range.setEnd(node, middle + 1); // + 1 because end point is non-inclusive. |
| 987 |
| 988 var found = Context.rangeContainsPoint(range, x, y); |
| 989 if (found) { |
| 990 end = middle; |
| 991 } else { |
| 992 start = middle + 1; |
| 993 } |
| 994 } |
| 995 |
| 996 if (found) { |
| 997 var text = range.toString(); |
| 998 // Tests if the character is actually a word character (a letter, digit, |
| 999 // underscore, or any Unicode letter). If the character is not a word |
| 1000 // character it means the given point is in a whitespace, punctuation or |
| 1001 // other non-relevant characters, and in this case it should not be |
| 1002 // considered a successful finding. |
| 1003 if (!Context.reWordCharacter_.test(text)) { |
| 1004 found = false; |
| 1005 } |
| 1006 } |
| 1007 |
| 1008 range.detach(); |
| 1009 |
| 1010 return found ? start : -1; |
| 1011 }; |
| 1012 |
| 1013 /** |
| 1014 * Gets the surrounding data from a given range. |
| 1015 * @param {ContextData} contextData Object where the data will be written. |
| 1016 * @param {Range} range A text range. |
| 1017 * @private |
| 1018 */ |
| 1019 Context.extractSurroundingDataFromRange = function(contextData, range) { |
| 1020 var surroundingRange = range.cloneRange(); |
| 1021 var length = surroundingRange.toString().length; |
| 1022 while (length < Context.NUMBER_OF_CHARS_IN_SURROUNDING_SENTENCES) { |
| 1023 surroundingRange.expand('sentence'); |
| 1024 var oldLength = length; |
| 1025 length = surroundingRange.toString().length; |
| 1026 if (oldLength == length) { |
| 1027 break; |
| 1028 } |
| 1029 } |
| 1030 |
| 1031 var textNodeType = range.startContainer.TEXT_NODE; |
| 1032 var selectionStartOffset = 0; |
| 1033 var selectionStartNode = range.startContainer; |
| 1034 if (range.startContainer.nodeType == textNodeType) { |
| 1035 selectionStartOffset = range.startOffset; |
| 1036 } else { |
| 1037 selectionStartNode = range.startContainer.childNodes[range.startOffset]; |
| 1038 } |
| 1039 |
| 1040 var surroundingStartOffset = 0; |
| 1041 |
| 1042 if (surroundingRange.startContainer.nodeType == textNodeType) { |
| 1043 surroundingStartOffset = surroundingRange.startOffset; |
| 1044 } |
| 1045 var surroundingRemoveAtEnd = 0; |
| 1046 if (surroundingRange.endContainer.nodeType == textNodeType) { |
| 1047 surroundingRemoveAtEnd = surroundingRange.endContainer.textContent.length - |
| 1048 surroundingRange.endOffset; |
| 1049 } |
| 1050 |
| 1051 // It is possible that invalid nodes are present inside the surrounding range. |
| 1052 // Extract text to make sure this is not the case. |
| 1053 var textNodes = Context.textNodesFromRange(surroundingRange); |
| 1054 |
| 1055 var offset = 0; |
| 1056 var index = 0; |
| 1057 var surroundingString = ''; |
| 1058 var foundStart = false; |
| 1059 for (index = 0; index < textNodes.length; index++) { |
| 1060 if (textNodes[index] === ' ') { |
| 1061 surroundingString += ' '; |
| 1062 if (!foundStart) { |
| 1063 offset += 1; |
| 1064 } |
| 1065 continue; |
| 1066 } |
| 1067 if (textNodes[index] == selectionStartNode) { |
| 1068 foundStart = true; |
| 1069 } |
| 1070 if (!foundStart) { |
| 1071 offset += textNodes[index].textContent.length; |
| 1072 } |
| 1073 surroundingString += textNodes[index].textContent; |
| 1074 } |
| 1075 offset += selectionStartOffset - surroundingStartOffset; |
| 1076 |
| 1077 surroundingString = surroundingString.substring(surroundingStartOffset, |
| 1078 surroundingString.length - surroundingRemoveAtEnd); |
| 1079 |
| 1080 contextData.surroundingText = surroundingString; |
| 1081 contextData.offsetStart = offset; |
| 1082 contextData.offsetEnd = offset + range.toString().length; |
| 1083 |
| 1084 Context.surroundingRange = { |
| 1085 startContainer: surroundingRange.startContainer, |
| 1086 startOffset: surroundingRange.startOffset, |
| 1087 endContainer: surroundingRange.endContainer, |
| 1088 endOffset: surroundingRange.endOffset, |
| 1089 highlightStartOffset: contextData.offsetStart, |
| 1090 highlightEndOffset: contextData.offsetEnd |
| 1091 }; |
| 1092 |
| 1093 surroundingRange.detach(); |
| 1094 }; |
| 1095 |
| 1096 /** |
| 1097 * Returns whether character is a whitespace. |
| 1098 * @param {string} character The character to test. |
| 1099 * @return {boolean} Whether |character| is a whitespace. |
| 1100 */ |
| 1101 Context.isCharSpace = function(character) { |
| 1102 return character.trim().length == 0; |
| 1103 }; |
| 1104 |
| 1105 /** |
| 1106 * Find next node in a DFS order. A parent is returned before its children. |
| 1107 * @param {Node} node The current node. |
| 1108 * @param {Node} endNode The right limit of the DFS. |
| 1109 * @param {boolean} onlyValid Whether invalid node should be skipped in DFS. |
| 1110 * @return {Node} The node coming after |node|. Null is endNode is reached. |
| 1111 */ |
| 1112 Context.getNextNode = function(node, endNode, onlyValid) { |
| 1113 if (!node) |
| 1114 return null; |
| 1115 |
| 1116 if (node.childNodes.length > 0) { |
| 1117 node = node.childNodes[0]; |
| 1118 if (node.nodeType == node.TEXT_NODE || |
| 1119 (node.nodeType == node.ELEMENT_NODE && |
| 1120 (!onlyValid || Context.isValidElement(node, true)))) { |
| 1121 return node; |
| 1122 } |
| 1123 if (node.nodeType == node.ELEMENT_NODE && node.contains(endNode)) { |
| 1124 return null; |
| 1125 } |
| 1126 } |
| 1127 |
| 1128 while (node != null) { |
| 1129 if (!node.nextSibling) { |
| 1130 node = node.parentNode; |
| 1131 continue; |
| 1132 } |
| 1133 if (node.contains(endNode)) |
| 1134 return null; |
| 1135 node = node.nextSibling; |
| 1136 if (node.nodeType == node.TEXT_NODE || |
| 1137 (node.nodeType == node.ELEMENT_NODE && |
| 1138 (!onlyValid || Context.isValidElement(node, true)))) { |
| 1139 return node; |
| 1140 } |
| 1141 } |
| 1142 return null; |
| 1143 }; |
| 1144 |
| 1145 /** |
| 1146 * Creates the list of text nodes that are part of range. The list will contain |
| 1147 * whitespace strings to replace block elements. |
| 1148 * @param {Range} range The range containing the text information. |
| 1149 * @return {Array.<Node>} The array of text nodes in the range. |
| 1150 */ |
| 1151 Context.textNodesFromRange = function(range) { |
| 1152 var blockStack = []; |
| 1153 |
| 1154 var startNode = range.startContainer.childNodes[range.startOffset] || |
| 1155 range.startContainer; |
| 1156 var endNode = range.endContainer.childNodes[range.endOffset] || |
| 1157 range.endContainer; |
| 1158 |
| 1159 if (startNode == endNode && startNode.childNodes.length === 0) { |
| 1160 return [startNode]; |
| 1161 } |
| 1162 |
| 1163 var textNodes = []; |
| 1164 var node = startNode; |
| 1165 // Do not add a space as first node. |
| 1166 var addSpace = false; |
| 1167 var lastNodeAddedHasSpace = true; |
| 1168 |
| 1169 do { |
| 1170 if (node.nodeType == node.TEXT_NODE && node.textContent.length > 0) { |
| 1171 while (blockStack.length && !blockStack[0].contains(node)) { |
| 1172 addSpace = true; |
| 1173 blockStack.shift(); |
| 1174 } |
| 1175 if (addSpace && !lastNodeAddedHasSpace && |
| 1176 !Context.isCharSpace(node.textContent[0])) { |
| 1177 textNodes.push(' '); |
| 1178 addSpace = false; |
| 1179 } |
| 1180 textNodes.push(node); |
| 1181 lastNodeAddedHasSpace = Context.isCharSpace( |
| 1182 node.textContent[node.textContent.length - 1]); |
| 1183 } else if (node.nodeType == node.ELEMENT_NODE) { |
| 1184 var style = window.getComputedStyle(node); |
| 1185 if (style && style.display != 'inline') { |
| 1186 addSpace = true; |
| 1187 blockStack.unshift(node); |
| 1188 } |
| 1189 } |
| 1190 node = Context.getNextNode(node, endNode, true); |
| 1191 } while (node && node != endNode); |
| 1192 |
| 1193 if (node == endNode && node.nodeType == node.TEXT_NODE) { |
| 1194 if (addSpace && !lastNodeAddedHasSpace && |
| 1195 !Context.isCharSpace(node.textContent[0])) { |
| 1196 textNodes.push(' '); |
| 1197 } |
| 1198 textNodes.push(node); |
| 1199 } |
| 1200 return textNodes; |
| 1201 }; |
| 1202 |
| 1203 /** |
| 1204 * Checks if a particular range contains a given point. |
| 1205 * @param {Range} range A text range. |
| 1206 * @param {number} x The point's x coordinate. |
| 1207 * @param {number} y The point's y coordinate. |
| 1208 * @return {boolean} whether the range contains the given point. |
| 1209 */ |
| 1210 Context.rangeContainsPoint = function(range, x, y) { |
| 1211 var rects = range.getClientRects(); |
| 1212 var rectsLength = rects.length; |
| 1213 for (var i = 0, length = rectsLength; i < length; i++) { |
| 1214 var rect = rects[i]; |
| 1215 var contains = Context.rectContainsPoint(rect, x, y); |
| 1216 if (contains) { |
| 1217 return true; |
| 1218 } |
| 1219 } |
| 1220 return false; |
| 1221 }; |
| 1222 |
| 1223 /** |
| 1224 * Checks if a particular rectangle contains a given point. |
| 1225 * @param {Rect} rect A rectangle. |
| 1226 * @param {number} x The point's x coordinate. |
| 1227 * @param {number} y The point's y coordinate. |
| 1228 * @return {boolean} whether the rectangle contains the given point. |
| 1229 * @private |
| 1230 */ |
| 1231 Context.rectContainsPoint = function(rect, x, y) { |
| 1232 if (x >= rect.left && x <= rect.right && |
| 1233 y >= rect.top && y <= rect.bottom) { |
| 1234 return true; |
| 1235 } |
| 1236 return false; |
| 1237 }; |
| 1238 |
| 1239 /** |
| 1240 * Create a new range to the [startOffset, endOffset] in the surrounding |
| 1241 * range returned by Context.extractSurroundingDataFromRange. |
| 1242 * @param {number} startOffset first character to include in the range. |
| 1243 * @param {number} endOffset last character to include in the range. |
| 1244 * @return {Range} the range including [startOffset, endOffset]. |
| 1245 * @private |
| 1246 */ |
| 1247 Context.createSurroundingRange = function(startOffset, endOffset) { |
| 1248 if ((startOffset == Context.surroundingRange.highlightStartOffset && |
| 1249 endOffset == Context.surroundingRange.highlightEndOffset) || |
| 1250 startOffset >= endOffset) { |
| 1251 return; |
| 1252 } |
| 1253 var range = document.createRange(); |
| 1254 range.setStart(Context.surroundingRange.startContainer, |
| 1255 Context.surroundingRange.startOffset); |
| 1256 range.setEnd(Context.surroundingRange.endContainer, |
| 1257 Context.surroundingRange.endOffset); |
| 1258 |
| 1259 var textNodes = Context.textNodesFromRange(range); |
| 1260 var highlightRange = document.createRange(); |
| 1261 var length = textNodes.length; |
| 1262 var offset = 0; |
| 1263 if (length > 0 && Context.surroundingRange.startContainer == textNodes[0]) { |
| 1264 // Ignore the text inside |startContainer| before the range. |
| 1265 offset -= Context.surroundingRange.startOffset; |
| 1266 } |
| 1267 var startSet = false; |
| 1268 for (var index = 0; index < length; index++) { |
| 1269 var node = textNodes[index]; |
| 1270 if (node === ' ') { |
| 1271 offset += 1; |
| 1272 continue; |
| 1273 } |
| 1274 if (!startSet && offset + node.textContent.length > startOffset) { |
| 1275 startSet = true; |
| 1276 highlightRange.setStart(node, startOffset - offset); |
| 1277 } |
| 1278 if (offset + node.textContent.length >= endOffset) { |
| 1279 highlightRange.setEnd(node, endOffset - offset); |
| 1280 break; |
| 1281 } |
| 1282 offset += node.textContent.length; |
| 1283 } |
| 1284 return highlightRange; |
| 1285 }; |
| 1286 |
| 1287 /** |
| 1288 * Returns the string contained in the parameter |range|. Text rules are the |
| 1289 * same as textNodesFromRange. |
| 1290 * @param {Range} range the range to convert to string. |
| 1291 * @return {string} the string contained in range. |
| 1292 * @private |
| 1293 */ |
| 1294 Context.rangeToString = function(range) { |
| 1295 var textNodes = Context.textNodesFromRange(range); |
| 1296 var string = ''; |
| 1297 var length = textNodes.length; |
| 1298 for (var index = 0; index < length; index++) { |
| 1299 var node = textNodes[index]; |
| 1300 if (node === ' ') { |
| 1301 string += ' '; |
| 1302 continue; |
| 1303 } |
| 1304 var nodeString = node.textContent; |
| 1305 if (node == range.endNode) { |
| 1306 nodeString = nodeString.substring(0, range.endOffset); |
| 1307 } |
| 1308 if (node == range.startNode) { |
| 1309 nodeString.substring(range.startOffset, nodeString.length); |
| 1310 } |
| 1311 string += nodeString; |
| 1312 } |
| 1313 return string; |
| 1314 }; |
| 1315 |
| 1316 /** |
| 1317 * Create the text client rects contained in the current |highlightRange|. |
| 1318 * @return {string} A string containing a comma separated list of rects in |
| 1319 * the format 'top bottom left right'. Coordinates are page based (not screen |
| 1320 * based). |
| 1321 * @private |
| 1322 */ |
| 1323 Context.getHighlightRects = function() { |
| 1324 if (Context.highlightRange == null) { |
| 1325 return ''; |
| 1326 } |
| 1327 var rectsArray = new Array(); |
| 1328 var rects = Context.highlightRange.getClientRects(); |
| 1329 var rectsLength = rects.length; |
| 1330 for (var i = 0, length = rectsLength; i < length; i++) { |
| 1331 var top = rects[i].top + document.body.scrollTop; |
| 1332 var bottom = rects[i].bottom + document.body.scrollTop; |
| 1333 var left = rects[i].left + document.body.scrollLeft; |
| 1334 var right = rects[i].right + document.body.scrollLeft; |
| 1335 rectsArray[i] = '' + top + ' ' + bottom + ' ' + left + ' ' + right; |
| 1336 } |
| 1337 return rectsArray.join(','); |
| 1338 }; |
| 1339 |
| 1340 /* Anyonymizing block end */ |
| 1341 } |
| OLD | NEW |