| Index: ios/chrome/browser/ui/contextual_search/resources/contextualsearch.js
|
| diff --git a/ios/chrome/browser/ui/contextual_search/resources/contextualsearch.js b/ios/chrome/browser/ui/contextual_search/resources/contextualsearch.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ae36f5981389e757b8e605153df2e8512a34da73
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/contextual_search/resources/contextualsearch.js
|
| @@ -0,0 +1,1341 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +/**
|
| + * @fileoverview Support code for the Contextual Search feature. Given a tap
|
| + * location, locates and highlights the word at that location, and retuns
|
| + * some contextual data about the selection.
|
| + *
|
| + */
|
| +
|
| +
|
| +/**
|
| + * Namespace for this file. Depends on __gCrWeb having already been injected.
|
| + */
|
| +__gCrWeb['contextualSearch'] = {};
|
| +
|
| +/* Anyonymizing block */
|
| +new function() {
|
| +
|
| +/**
|
| + * Utility loggging function.
|
| + * @param {string} message Text of log message to be sent to application.
|
| + */
|
| +__gCrWeb['contextualSearch'].cxlog = function(message) {
|
| + if (Context.debugMode) {
|
| + console.log('[CS] ' + message);
|
| + }
|
| +};
|
| +
|
| +/**
|
| + * Enables or disabled the selection change notification forward to the
|
| + * controller.
|
| + * @param {boolean} enabled whether to turn on (true) or off (false).
|
| + */
|
| +__gCrWeb['contextualSearch'].enableSelectionChangeListener = function(enabled) {
|
| + if (enabled) {
|
| + document.addEventListener('selectionchange',
|
| + Context.selectionChanged,
|
| + true);
|
| + } else {
|
| + document.removeEventListener('selectionchange',
|
| + Context.selectionChanged,
|
| + true);
|
| + }
|
| +};
|
| +
|
| +__gCrWeb['contextualSearch'].getMutatedElementCount = function() {
|
| + return Context.mutationCount;
|
| +};
|
| +
|
| +/**
|
| + * Enables the DOM mutation event listener.
|
| + * @param {number} delay after a mutation event before a tap event can be
|
| + * handled.
|
| + */
|
| +__gCrWeb['contextualSearch'].setMutationObserverDelay = function(delay) {
|
| + Context.DOMMutationTimeoutMillisecond = delay;
|
| +
|
| + // select the target node
|
| + var target = document.body;
|
| +
|
| + // create an observer instance
|
| + Context.DOMMutationObserver = new MutationObserver(function(mutations) {
|
| + // Clear any mutation records older than |DOMMutationTimeoutMillisecond|.
|
| + // Only do this every |DOMMutationTimeoutMillisecond|s to avoid thrashing
|
| + // on pages with (for example) continuous animations.
|
| +
|
| + var d = new Date();
|
| + if ((d.getTime() - Context.lastMutationPrune) >
|
| + Context.DOMMutationTimeoutMillisecond) {
|
| + Context.processMutations(function(mutationId, mutationTime) {
|
| + if ((d.getTime() - mutationTime) <=
|
| + Context.DOMMutationTimeoutMillisecond) {
|
| + Context.clearMutation(mutationId);
|
| + }
|
| + return false;
|
| + });
|
| + Context.lastMutationPrune = d.getTime();
|
| + }
|
| +
|
| + mutations.forEach(function(mutation) {
|
| + // Don't count mutations with invalid targets.
|
| + if (!mutation.target) {
|
| + return;
|
| + }
|
| + // Don't count mutations to invisible elements.
|
| + if (mutation.type != 'characterData' && !mutation.target.offsetParent) {
|
| + return;
|
| + }
|
| + // Don't count attribute mutations where the attribute's current and
|
| + // old values are the same. Also don't count attribute mutation where
|
| + // the current and old values are both "false-ish" (so changing a null to
|
| + // an empty string has no effect).
|
| + if (mutation.type == 'attributes') {
|
| + if (mutation.target.getAttribute(mutation.attributeName) ==
|
| + mutation.oldValue) {
|
| + return;
|
| + }
|
| + if (!mutation.target.getAttribute(mutation.attributeName) &&
|
| + !mutation.oldValue) {
|
| + return;
|
| + }
|
| + // Don't count the attribute mutation from setting an ID for tracking or
|
| + // if it is unsetting the ID.
|
| + if (mutation.attributeName == 'id' &&
|
| + (mutation.target.id.match(/__CTXSM___\d__\d+/) ||
|
| + mutation.target.id == '')) {
|
| + return;
|
| + }
|
| + }
|
| +
|
| + if (mutation.type == 'characterData' &&
|
| + mutation.target.textContent == mutation.oldValue) {
|
| + return;
|
| + }
|
| +
|
| + // If this is the first mutation after tap, and the mutation target
|
| + // intersects with the highlighted elements, forward it to the Chrome
|
| + // application to check if CS must be dismissed.
|
| + if (Context.highlightRange &&
|
| + !Context.mutationEventForwarded &&
|
| + Context.highlightRange.intersectsNode(mutation.target)) {
|
| + Context.mutationEventForwarded = true;
|
| + __gCrWeb.message.invokeOnHost(
|
| + {'command' : 'contextualSearch.mutationEvent'});
|
| + }
|
| +
|
| + Context.lastDOMMutationMillisecond = d.getTime();
|
| + // If the mutated item isn't an element, find its parent.
|
| + // If the element doesn't have an ID, assign one to it.
|
| + var idContainer = mutation.target;
|
| + if (mutation.type == 'characterData') {
|
| + idContainer = idContainer.parentElement;
|
| + }
|
| + var id = idContainer.id;
|
| + if (!id) {
|
| + id = Context.newID(Context.lastDOMMutationMillisecond);
|
| + idContainer.id = id;
|
| + }
|
| + Context.recordMutation(id, Context.lastDOMMutationMillisecond);
|
| + });
|
| + });
|
| +
|
| + // configuration of the observer:
|
| + var config = {
|
| + attributes: true,
|
| + characterData: true,
|
| + subtree: true,
|
| + attributeOldValue: true,
|
| + characterDataOldValue: true
|
| + };
|
| +
|
| + // pass in the target node, as well as the observer options
|
| + Context.DOMMutationObserver.observe(target, config);
|
| +};
|
| +
|
| +/**
|
| + * Enables the body touch end event listener. This will catch touch events that
|
| + * don't call preventDefault.
|
| + * @param {number} delay after a mutation event before a tap event can be
|
| + * handled.
|
| + */
|
| +__gCrWeb['contextualSearch'].setBodyTouchListenerDelay = function(delay) {
|
| + Context.touchEventTimeoutMillisecond = delay;
|
| + Context.bodyTouchEndEventListener = function(event) {
|
| + if (!event.defaultPrevented) {
|
| + var d = new Date();
|
| + Context.lastTouchEventMillisecond = d.getTime();
|
| + } else {
|
| + __gCrWeb['contextualSearch'].cxlog('Touch default prevented');
|
| + }
|
| + };
|
| + document.body.addEventListener('touchend', Context.bodyTouchEndEventListener,
|
| + false);
|
| +};
|
| +
|
| +/**
|
| + * Disables the DOM mutation listener.
|
| + */
|
| +__gCrWeb['contextualSearch'].disableMutationObserver = function() {
|
| + Context.DOMMutationObserver.disconnect();
|
| + Context.DOMMutationObserver = null;
|
| + Context.lastDOMMutationMillisecond = 0;
|
| +};
|
| +
|
| +/**
|
| + * Disables the body touchend listener.
|
| + */
|
| +__gCrWeb['contextualSearch'].disableBodyTouchListener = function() {
|
| + document.body.removeEventListener('touchend',
|
| + Context.bodyTouchEndEventListener, false);
|
| + Context.lastTouchEventMillisecond = 0;
|
| +};
|
| +
|
| +/**
|
| + * Expands the highlight to [startOffset, endOffset] in the surrounding range.
|
| + * @param {number} startOffset first character to include in the range.
|
| + * @param {number} endOffset last character to include in the range.
|
| + * @return {JSON} new highlighted rects.
|
| + */
|
| +__gCrWeb['contextualSearch'].expandHighlight =
|
| + function(startOffset, endOffset) {
|
| + if ((startOffset == Context.surroundingRange.highlightStartOffset &&
|
| + endOffset == Context.surroundingRange.highlightEndOffset) ||
|
| + startOffset > endOffset) {
|
| + return;
|
| + }
|
| + var range = Context.createSurroundingRange(startOffset, endOffset);
|
| + Context.highlightRange = range;
|
| + return __gCrWeb['contextualSearch'].highlightRects();
|
| +};
|
| +
|
| +/**
|
| + * Returns the rects to draw the current highlight.
|
| + * @return {JSON} hilighted rects.
|
| + */
|
| +__gCrWeb['contextualSearch'].highlightRects = function() {
|
| + return __gCrWeb.stringify(
|
| + {'rects' : Context.getHighlightRects(),
|
| + 'size': {'width' : document.documentElement.scrollWidth,
|
| + 'height' : document.documentElement.scrollHeight
|
| + }});
|
| +};
|
| +
|
| +/**
|
| + * Clears the current highlight.
|
| + */
|
| +__gCrWeb['contextualSearch'].clearHighlight = function() {
|
| + Context.highlightRange = null;
|
| +};
|
| +
|
| +/**
|
| + * Retrieve the currently highlighted string.
|
| + * This is used for test purposes only.
|
| + * @return {string} the currently highlighted string.
|
| + */
|
| +__gCrWeb['contextualSearch'].retrieveHighlighted = function() {
|
| + return Context.rangeToString(Context.highlightRange);
|
| +};
|
| +
|
| +/**
|
| + * Prepares Contextual Search for a given point in the window. This method
|
| + * will find which word is located at the given point, extract the context
|
| + * data for that word, and (if a word was located), pass the context data
|
| + * back to the calling application.
|
| + * @param {number} x The point's x coordinate as a ratio of page width.
|
| + * @param {number} y The point's y coordinate as a ratio of page height.
|
| + * @return {object} Empty if no word was found, or the context, x and y if
|
| + * a word was found.
|
| + */
|
| +__gCrWeb['contextualSearch'].handleTapAtPoint = function(x, y) {
|
| + var tapResults;
|
| + if (window.getSelection().toString()) {
|
| + tapResults = new ContextData();
|
| + tapResults.error = 'Failed: selection is not empty.';
|
| + return __gCrWeb.stringify({'context' : tapResults.returnContext()});
|
| + }
|
| + var d = new Date();
|
| + var lastTouchDelta = d.getTime() - Context.lastTouchEventMillisecond;
|
| + if (Context.touchEventTimeoutMillisecond &&
|
| + lastTouchDelta > Context.touchEventTimeoutMillisecond) {
|
| + tapResults = new ContextData();
|
| + tapResults.error = 'Failed: last touch was ' + lastTouchDelta +
|
| + 'ms ago (>' + Context.touchEventTimeoutMillisecond + 'ms timeout)';
|
| + } else {
|
| + var absoluteX =
|
| + x * document.documentElement.scrollWidth - document.body.scrollLeft;
|
| + var absoluteY =
|
| + y * document.documentElement.scrollHeight - document.body.scrollTop;
|
| + tapResults = Context.getContextDataFromPoint(absoluteX, absoluteY);
|
| +
|
| + if (!tapResults.error) {
|
| + var range = tapResults.range;
|
| + if (!range) {
|
| + tapResults.error = 'Failed: context data range was empty';
|
| + } else if (tapResults.surroundingText.length < tapResults.offsetEnd) {
|
| + tapResults.error =
|
| + 'Failed: surrounding text is shorter than text offset';
|
| + } else if (tapResults.getSelectedText() != tapResults.selectedText) {
|
| + tapResults.error = 'Failed: offsets do not match selected text: (' +
|
| + tapResults.getSelectedText() + ') vs. (' +
|
| + tapResults.selectedText + ')';
|
| + }
|
| + }
|
| + }
|
| + Context.mutationEventForwarded = false;
|
| + return __gCrWeb.stringify({'context' : tapResults.returnContext()});
|
| +};
|
| +
|
| +//------------------------------------------------------------------------------
|
| +// ContextData
|
| +//------------------------------------------------------------------------------
|
| +
|
| +/**
|
| + * @constructor
|
| + */
|
| +var ContextData = function() {};
|
| +
|
| +/**
|
| + * An error message, if any, associated with the context.
|
| + * @type {?string}
|
| + */
|
| +ContextData.prototype.error = null;
|
| +
|
| +/**
|
| + * The range containing the selected text.
|
| + * @type {?Range}
|
| + */
|
| +ContextData.prototype.range = null;
|
| +
|
| +/**
|
| + * The URL from where the context was extracted.
|
| + * @type {?string}
|
| + */
|
| +ContextData.prototype.url = null;
|
| +
|
| +/**
|
| + * The selected text.
|
| + * @type {?string}
|
| + */
|
| +ContextData.prototype.selectedText = null;
|
| +
|
| +/**
|
| + * The surrounding text.
|
| + * @type {?string}
|
| + */
|
| +ContextData.prototype.surroundingText = null;
|
| +
|
| +/**
|
| + * The start position of the selected text relative to the surrounding text.
|
| + * @type {?number}
|
| + */
|
| +ContextData.prototype.offsetStart = null;
|
| +
|
| +/**
|
| + * The end position of the selected text relative to the surrounding text.
|
| + * @type {?number}
|
| + */
|
| +ContextData.prototype.offsetEnd = null;
|
| +
|
| +/**
|
| + * The rewritten query.
|
| + * @type {?string}
|
| + */
|
| +ContextData.prototype.rewrittenQuery = null;
|
| +
|
| +/**
|
| + * Gets the search query for the context.
|
| + * @return {?string} The search query.
|
| + */
|
| +ContextData.prototype.getQuery = function() {
|
| + return this.rewrittenQuery || this.selectedText;
|
| +};
|
| +
|
| +/**
|
| + * @return {string} The part of the surrounding text before the selected text.
|
| + */
|
| +ContextData.prototype.getTextBefore = function() {
|
| + var selectedText = this.selectedText;
|
| + var surroundingText = this.surroundingText;
|
| + var result = '';
|
| + if (!this.rewrittenQuery && surroundingText) {
|
| + result = surroundingText.substring(0, this.offsetStart);
|
| + }
|
| + return result;
|
| +};
|
| +
|
| +/**
|
| + * @return {string} The part of the surrounding text after the selected text.
|
| + */
|
| +ContextData.prototype.getTextAfter = function() {
|
| + var selectedText = this.selectedText;
|
| + var surroundingText = this.surroundingText;
|
| + var result = '';
|
| + if (!this.rewrittenQuery && selectedText && surroundingText) {
|
| + result = surroundingText.substring(this.offsetEnd);
|
| + }
|
| + return result;
|
| +};
|
| +
|
| +/**
|
| + * @return {string} The selected text as indicated by offsetStart and
|
| + * offsetEnd. This should be the same as selectedText
|
| + */
|
| +ContextData.prototype.getSelectedText = function() {
|
| + var surroundingText = this.surroundingText;
|
| + var result = '';
|
| + if (!this.rewrittenQuery && surroundingText) {
|
| + result = surroundingText.substring(this.offsetStart, this.offsetEnd);
|
| + }
|
| + return result;
|
| +};
|
| +
|
| +/**
|
| + * @return {JSONDictionary} Context data assembeld for return to native app.
|
| + */
|
| +ContextData.prototype.returnContext = function() {
|
| + var context = {'url' : this.url,
|
| + 'selectedText' : this.selectedText,
|
| + 'surroundingText' : this.surroundingText,
|
| + 'offsetStart' : this.offsetStart,
|
| + 'offsetEnd' : this.offsetEnd,
|
| + 'rects': Context.getHighlightRects()
|
| + };
|
| + if (this.error) {
|
| + context['error'] = this.error;
|
| + }
|
| + return context;
|
| +};
|
| +
|
| +//------------------------------------------------------------------------------
|
| +// Context
|
| +//------------------------------------------------------------------------------
|
| +
|
| +var Context = {};
|
| +
|
| +/**
|
| + * Whether to send log output to the host.
|
| + * @type {bool}
|
| + */
|
| +Context.debugMode = false;
|
| +
|
| +/**
|
| + * The maximium amount of time that should be spent searching for a text range,
|
| + * in milliseconds. If the search does not finish within the specified value,
|
| + * it should terminate without returning a result.
|
| + * @const {number}
|
| + */
|
| +Context.GET_RANGE_TIMEOUT_MS = 50;
|
| +
|
| +/**
|
| + * Number of surrouding sentences when calculating the surrounding text.
|
| + * @const {number}
|
| + */
|
| +Context.NUMBER_OF_CHARS_IN_SURROUNDING_SENTENCES = 1500;
|
| +
|
| +/**
|
| + * Maximum number of chars for a selection to trigger the search.
|
| + * @const {number}
|
| + */
|
| +Context.MAX_NUMBER_OF_CHARS_IN_SELECTION = 100;
|
| +
|
| +/**
|
| + * Last range returned by Context.extractSurroundingDataFromRange.
|
| + * @type {JSONObject} Contains startContainer, endContainer, startOffset,
|
| + * endOffset of the surrounding range and relative position of the tapped word.
|
| + */
|
| +Context.surroundingRange = null;
|
| +
|
| +/**
|
| + * The range that is curently highlighted.
|
| + * @type {Range}
|
| + */
|
| +Context.highlightRange = null;
|
| +
|
| +/**
|
| + * A boolean to check if a mutation event has been forwarded after the latest
|
| + * tap.
|
| + * @type {boolean}
|
| + */
|
| +Context.mutationEventForwarded = false;
|
| +
|
| +/**
|
| + * A Regular Expression that matches Unicode word characters.
|
| + * @type {RegExp}
|
| + */
|
| +Context.reWordCharacter_ =
|
| + /[\u00C0-\u1FFF\u2C00-\uD7FF\w]/;
|
| +
|
| +/**
|
| + * An observer of the DOM mutation.
|
| + * @type {MutationObserver}
|
| + */
|
| +Context.DOMMutationObserver = null;
|
| +
|
| +/**
|
| + * The date of the last DOM mutation (in ms).
|
| + * @type {number}
|
| + */
|
| +Context.lastDOMMutationMillisecond = 0;
|
| +
|
| +/**
|
| + * A hash of timestamps keyed by element-id.
|
| + * @type {Object}
|
| + */
|
| +Context.mutatedElements = {};
|
| +
|
| +/**
|
| + * A running count of tracked mutated objects.
|
| + * @type {number}
|
| + */
|
| +Context.mutationCount = 0;
|
| +
|
| +/**
|
| + * Date of the last time the mutation list was pruned of old entries (in ms).
|
| + * @type {number}
|
| + */
|
| +Context.lastMutationPrune = 0;
|
| +
|
| +/**
|
| + * An incrementing integer for generating temporary element ids when needed.
|
| + * @type {number}
|
| + */
|
| +Context.mutationIdCounter = 0;
|
| +
|
| +/**
|
| + * A snapshot of the previous text selection (if any), used to determine if a
|
| + * selection change is a new selection or not. previousSelection stores the
|
| + * anchor and focus nodes and offsets of the previously-reported selection.
|
| + * @type {Object}
|
| + */
|
| +Context.previousSelection = null;
|
| +
|
| +/**
|
| + * Generates a string suitable for use as a temporary element id.
|
| + * @param {string} nonce A string that varies based on the current time.
|
| + * @return {string} A string to be used as an element id.
|
| + */
|
| +Context.newID = function(nonce) {
|
| + return '__CTXSM___' + (Context.mutationIdCounter++) + '__' + nonce;
|
| +};
|
| +
|
| +/**
|
| + * The timeout of DOM mutation after which a contextual search can be triggered
|
| + * (in ms)
|
| + * @type {number}
|
| + */
|
| +Context.DOMMutationTimeoutMillisecond = 200;
|
| +
|
| +/**
|
| + * An observer of body to catch unhandled touch events.
|
| + * @type {EventListener}
|
| + */
|
| +Context.bodyTouchEndEventListener = null;
|
| +
|
| +/**
|
| + * The date of the last unhandled touch event (in ms).
|
| + * @type {number}
|
| + */
|
| +Context.lastTouchEventMillisecond = 0;
|
| +
|
| +/**
|
| + * The timeout of DOM mutation after which a contextual search can be triggered
|
| + * (in ms)
|
| + * @type {number}
|
| + */
|
| +Context.touchEventTimeoutMillisecond = 0;
|
| +
|
| +/**
|
| + * List of node types whose contents should not be parsed by Contextual Search.
|
| + * @type {Array.<string>}
|
| + */
|
| +Context['invalidElements_'] = [
|
| + 'A',
|
| + 'APPLET',
|
| + 'AREA',
|
| + 'AUDIO',
|
| + 'BUTTON',
|
| + 'CANVAS',
|
| + 'EMBED',
|
| + 'FRAME',
|
| + 'FRAMESET',
|
| + 'IFRAME',
|
| + 'IMG',
|
| + 'INPUT',
|
| + 'KEYGEN',
|
| + 'LABEL',
|
| + 'MAP',
|
| + 'OBJECT',
|
| + 'OPTGROUP',
|
| + 'OPTION',
|
| + 'PROGRESS',
|
| + 'SCRIPT',
|
| + 'SELECT',
|
| + 'TEXTAREA',
|
| + 'VIDEO'
|
| +];
|
| +
|
| +/**
|
| + * List of ARIA roles that define widgets.
|
| + * For more info, see: http://www.w3.org/TR/wai-aria/roles#widget_roles
|
| + * @type {Array.<string>}
|
| + */
|
| +Context['widgetRoles_'] = [
|
| + 'alert',
|
| + 'alertdialog',
|
| + 'button',
|
| + 'checkbox',
|
| + 'dialog',
|
| + 'gridcell',
|
| + 'link',
|
| + 'log',
|
| + 'marquee',
|
| + 'menuitem',
|
| + 'menuitemcheckbox',
|
| + 'menuitemradio',
|
| + 'option',
|
| + 'progressbar',
|
| + 'radio',
|
| + 'scrollbar',
|
| + 'slider',
|
| + 'spinbutton',
|
| + 'status',
|
| + 'tab',
|
| + 'tabpanel',
|
| + 'textbox',
|
| + 'timer',
|
| + 'tooltip',
|
| + 'treeitem'
|
| +];
|
| +
|
| +/**
|
| + * List of ARIA roles that define composite widgets.
|
| + * For more info, see: http://www.w3.org/TR/wai-aria/roles#widget_roles
|
| + * @type {Array.<string>}
|
| + */
|
| +Context['compositeWidgetRoles_'] = [
|
| + 'combobox',
|
| + 'grid',
|
| + 'listbox',
|
| + 'menu',
|
| + 'menubar',
|
| + 'radiogroup',
|
| + 'tablist',
|
| + 'tree',
|
| + 'treegrid'
|
| +];
|
| +
|
| +/**
|
| + * Mutation record handling
|
| + */
|
| +
|
| +/**
|
| + * Records a DOM mutation.
|
| + * @param {string} mutationId The id of the mutated DOM element.
|
| + * @param {number} mutationTime The time of the mutation.
|
| + */
|
| +Context.recordMutation = function(mutationId, mutationTime) {
|
| + Context.mutatedElements[mutationId] = mutationTime;
|
| + Context.mutationCount += 1;
|
| +};
|
| +
|
| +/**
|
| + * Clears the record of a DOM mutation.
|
| + * @param {string} mutationId The id of the mutated DOM element.
|
| + */
|
| +Context.clearMutation = function(mutationId) {
|
| + delete Context.mutatedElements[mutationId];
|
| + Context.mutationCount -= 1;
|
| +};
|
| +
|
| +/**
|
| + * Performs some operation on all recorded mutations, passing the mutated node
|
| + * id and mutation time into func.
|
| + * @param {function} func The function to apply to the recorded mutations.
|
| + */
|
| +Context.processMutations = function(func) {
|
| + for (mutationId in Context.mutatedElements) {
|
| + var mutationTime = Context.mutatedElements[mutationId];
|
| + if (func(mutationId, mutationTime)) {
|
| + break;
|
| + }
|
| + }
|
| +};
|
| +
|
| +/**
|
| + * Returns whether the selection is valid to trigger a contextual search.
|
| + * An invalid selection is a selection that is either too long or contains a
|
| + * single latin character (there are some site that use x's or o's as crosses or
|
| + * circles), or is included or contains invalid elements.
|
| + * @param {selection} selection The current selection to test.
|
| + * @return {boolean} Whether selection should trigger contextual search.
|
| + */
|
| +Context.isSelectionValid = function(selection) {
|
| + var selectionText = selection.toString();
|
| + var length = selectionText.length;
|
| + if (length > Context.MAX_NUMBER_OF_CHARS_IN_SELECTION) {
|
| + return false;
|
| + }
|
| + if (length == 1 && selectionText.codePointAt(0) < 256) {
|
| + return false;
|
| + }
|
| +
|
| + var rangeCount = selection.rangeCount;
|
| + for (var rangeIndex = 0; rangeIndex < rangeCount; rangeIndex++) {
|
| + // Test if the selection is inside an invalid element.
|
| + var range = window.getSelection().getRangeAt(rangeIndex);
|
| + var element = range.commonAncestorContainer;
|
| + while (element) {
|
| + if (element.nodeType == element.ELEMENT_NODE &&
|
| + !Context.isValidElement(element)) {
|
| + return false;
|
| + }
|
| + element = element.parentElement;
|
| + }
|
| +
|
| + // Test if the selection contains an invalid element.
|
| + var startNode = range.startContainer.childNodes[range.startOffset] ||
|
| + range.startContainer;
|
| + var endNode = range.endContainer.childNodes[range.endOffset] ||
|
| + range.endContainer;
|
| + element = startNode;
|
| + while (element) {
|
| + if (element.nodeType == element.ELEMENT_NODE &&
|
| + !Context.isValidElement(element)) {
|
| + return false;
|
| + }
|
| + element = Context.getNextNode(element, endNode, false);
|
| + }
|
| + }
|
| + return true;
|
| +};
|
| +
|
| +/**
|
| + * Forwards the selection changed notification to the controller class.
|
| + */
|
| +Context.selectionChanged = function() {
|
| + var newSelection = window.getSelection();
|
| + if (!newSelection.toString()) {
|
| + Context.previousSelection = null;
|
| + return;
|
| + }
|
| + var updated = false;
|
| + if (Context.previousSelection) {
|
| + updated =
|
| + (Context.previousSelection.anchorNode == newSelection.anchorNode &&
|
| + Context.previousSelection.anchorOffset == newSelection.anchorOffset) ||
|
| + (Context.previousSelection.focusNode == newSelection.focusNode &&
|
| + Context.previousSelection.focusOffset == newSelection.focusOffset);
|
| + }
|
| + var selectionText = newSelection.toString();
|
| + var valid = true;
|
| + if (!Context.isSelectionValid(newSelection)) {
|
| + // Mark selection as invalid.
|
| + selectionText = '';
|
| + valid = false;
|
| + }
|
| +
|
| + __gCrWeb.message.invokeOnHost(
|
| + {'command' : 'contextualSearch.selectionChanged',
|
| + 'text' : selectionText,
|
| + 'updated' : updated,
|
| + 'valid' : valid
|
| + });
|
| +
|
| + // Snapshot the selection for comparison.
|
| + Context.previousSelection = {
|
| + 'anchorNode' : newSelection.anchorNode,
|
| + 'anchorOffset' : newSelection.anchorOffset,
|
| + 'focusNode' : newSelection.focusNode,
|
| + 'focusOffset' : newSelection.focusOffset
|
| + };
|
| +};
|
| +
|
| +/**
|
| + * Gets the data necessary to create a Contextual Search from a given point
|
| + * in the window.
|
| + * @param {number} x The point's x coordinate.
|
| + * @param {number} y The point's y coordinate.
|
| + * @return {ContextData} The object describing the context.
|
| + */
|
| +Context.getContextDataFromPoint = function(x, y) {
|
| + var contextData = Context.contextFromPoint(x, y);
|
| +
|
| + if (contextData.error) {
|
| + return contextData;
|
| + }
|
| + var range = contextData.range = Context.getWordRangeFromPoint(x, y);
|
| + if (range) {
|
| + contextData.selectedText = range.toString();
|
| + contextData.url = location.href;
|
| + Context.extractSurroundingDataFromRange(contextData, range);
|
| + Context.highlightRange = range;
|
| + }
|
| +
|
| + return contextData;
|
| +};
|
| +
|
| +/**
|
| + * Checks whether the context in a given point is valid. A context will be
|
| + * valid when the element at the given point is not interactive or editable.
|
| + * @param {number} x The point's x coordinate.
|
| + * @param {number} y The point's y coordinate.
|
| + * @return {ContextData} Context data from the point, an error set if invalid.
|
| + * @private
|
| + */
|
| +Context.contextFromPoint = function(x, y) {
|
| + // Should this use core.js's elementFromPoint_() instead?
|
| + var contextData = new ContextData();
|
| + var element = document.elementFromPoint(x, y);
|
| + if (!element) {
|
| + contextData.error = "Failed: Couldn't locate an element at " + x + ', ' + y;
|
| + return contextData;
|
| + }
|
| +
|
| + var d = new Date();
|
| + var lastDOM = d.getTime() - Context.lastDOMMutationMillisecond;
|
| + if (lastDOM <= Context.DOMMutationTimeoutMillisecond) {
|
| + Context.processMutations(function(mutationId, mutationTime) {
|
| + var mutatedElement = document.getElementById(mutationId);
|
| + if (!mutatedElement) {
|
| + Context.clearMutation(mutationId);
|
| + } else {
|
| + var lastElementMutation = d.getTime() - mutationTime;
|
| + if (lastElementMutation < 0 ||
|
| + (lastElementMutation > Context.DOMMutationTimeoutMillisecond)) {
|
| + return false; // mutation expired, continue.
|
| + }
|
| + if (element.contains(mutatedElement) ||
|
| + mutatedElement.contains(element)) {
|
| + contextData.error = 'Failed: Tap was in element mutated ' +
|
| + lastElementMutation + 'ms ago (<' +
|
| + Context.DOMMutationTimeoutMillisecond + 'ms interval)';
|
| + return true; // break from processing mutations
|
| + }
|
| + }
|
| + return false; // continue processing mutations
|
| + });
|
| + if (contextData.error)
|
| + return contextData;
|
| + }
|
| +
|
| + while (element) {
|
| + if (element.nodeType == element.ELEMENT_NODE &&
|
| + !Context.isValidElement(element, false)) {
|
| + contextData.error =
|
| + 'Failed: Tap was in an invalid (' + element.nodeName + ') element';
|
| + return contextData;
|
| + }
|
| + element = element.parentElement;
|
| + }
|
| +
|
| + return contextData;
|
| +};
|
| +
|
| +/**
|
| + * Checks whether the given element can be used as a touch target.
|
| + * @see Context.isValidContextFromPoint_
|
| + * @param {Element} element The element in question.
|
| + * @param {boolean} forDisplay Whether we are testing if an element is valid
|
| + * for tap handling (false) or for display (true).
|
| + * @return {boolean} Whether the element is a valid context.
|
| + * @private
|
| + */
|
| +Context.isValidElement = function(element, forDisplay) {
|
| +
|
| + if (element.nodeName == 'A') {
|
| + return forDisplay;
|
| + }
|
| +
|
| + if (Context.invalidElements_.indexOf(element.nodeName) != -1) {
|
| + __gCrWeb['contextualSearch'].cxlog(
|
| + 'Failed: ' + element.nodeName + ' element was invalid');
|
| + return false;
|
| + }
|
| +
|
| + if (element.getAttribute('contenteditable')) {
|
| + __gCrWeb['contextualSearch'].cxlog(
|
| + 'Failed: ' + element.nodeName + ' element was editable');
|
| + return false;
|
| + }
|
| +
|
| + var role = element.getAttribute('role');
|
| + if (Context.widgetRoles_.indexOf(role) != -1 ||
|
| + Context.compositeWidgetRoles_.indexOf(role) != -1) {
|
| + __gCrWeb['contextualSearch'].cxlog(
|
| + 'Failed: ' + element.nodeName + ' role ' + role + ' was invalid');
|
| + return false;
|
| + }
|
| +
|
| + if (forDisplay) {
|
| + var style = window.getComputedStyle(element);
|
| + if (style.display === 'none') {
|
| + __gCrWeb['contextualSearch'].cxlog(
|
| + 'Failed: ' + element.nodeName + ' hidden');
|
| + return false;
|
| + }
|
| + }
|
| +
|
| + return true;
|
| +};
|
| +
|
| +/**
|
| + * Gets the word range located at a given point. This method will find the
|
| + * word whose bounding rectangle contains the given point.
|
| + * @param {number} x The point's x coordinate.
|
| + * @param {number} y The point's y coordinate.
|
| + * @return {Range} The word range at the given point.
|
| + * @private
|
| + */
|
| +Context.getWordRangeFromPoint = function(x, y) {
|
| + var element = document.elementFromPoint(x, y);
|
| + var range = null;
|
| + try {
|
| + range = Context.findWordRangeFromPointRecursive(element, x, y);
|
| + } catch (e) {
|
| + __gCrWeb['contextualSearch'].cxlog(
|
| + 'Recursive word find failed: ' + e.message);
|
| + }
|
| +
|
| + return range;
|
| +};
|
| +
|
| +/**
|
| + * Recursively gets the word range located at a given point.
|
| + * @see Context.getWordRangeFromPoint_
|
| + * @param {Node} node The node being inspected.
|
| + * @param {number} x The point's x coordinate.
|
| + * @param {number} y The point's y coordinate.
|
| + * @return {Range} The word range at the given point.
|
| + * @private
|
| + */
|
| +Context.findWordRangeFromPointRecursive = function(node, x, y) {
|
| + if (!node) {
|
| + return null;
|
| + }
|
| +
|
| + if (node.nodeType == node.TEXT_NODE) {
|
| + var position = Context.findCharacterPositionInTextFromPoint(node, x, y);
|
| + if (position == -1) {
|
| + return null;
|
| + }
|
| +
|
| + var range = node.ownerDocument.createRange();
|
| + range.setStart(node, position);
|
| + range.setEnd(node, position);
|
| + range.expand('word');
|
| +
|
| + if (Context.rangeContainsPoint(range, x, y)) {
|
| + return range;
|
| + }
|
| +
|
| + if (range) {
|
| + range.detach();
|
| + }
|
| + } else if (node.nodeType == node.ELEMENT_NODE) {
|
| + var childNodes = node.childNodes;
|
| + var childNodesLength = childNodes.length;
|
| + for (var i = 0, length = childNodesLength; i < length; i++) {
|
| + var childNode = childNodes[i];
|
| + var range = childNode.ownerDocument.createRange();
|
| + range.selectNodeContents(childNode);
|
| +
|
| + if (Context.rangeContainsPoint(range, x, y)) {
|
| + range.detach();
|
| + return Context.findWordRangeFromPointRecursive(childNode, x, y);
|
| + } else {
|
| + range.detach();
|
| + }
|
| + }
|
| + }
|
| +
|
| + return null;
|
| +};
|
| +
|
| +/**
|
| + * Gets the position of the character range located at a given point. This
|
| + * method will find the single character whose bounding rectangle contains
|
| + * the given point and return the position of that character in the text
|
| + * node string. If not character is found this method returns -1.
|
| + * @see Context.findWordRangeFromPointRecursive_
|
| + * @param {number} x The point's x coordinate.
|
| + * @param {number} y The point's y coordinate.
|
| + * @param {Text} node The text node being inspected.
|
| + * @return {number} The position of the character in the text node string.
|
| + * @private
|
| + */
|
| +Context.findCharacterPositionInTextFromPoint = function(node, x, y) {
|
| + var startTime = new Date().getTime();
|
| +
|
| + var start = 0;
|
| + var end = node.textContent.length - 1;
|
| +
|
| + // Performs a binary search to find a single character whose bouding
|
| + // rectangle contains the given point.
|
| + var range = document.createRange();
|
| + while (!found || (end - start + 1) > 1) {
|
| + if ((new Date().getTime() - startTime) > Context.GET_RANGE_TIMEOUT_MS) {
|
| + __gCrWeb['contextualSearch'].cxlog('Timed out!');
|
| + break;
|
| + }
|
| +
|
| + var middle = Math.floor((start + end) / 2);
|
| + range.setStart(node, start);
|
| + range.setEnd(node, middle + 1); // + 1 because end point is non-inclusive.
|
| +
|
| + var found = Context.rangeContainsPoint(range, x, y);
|
| + if (found) {
|
| + end = middle;
|
| + } else {
|
| + start = middle + 1;
|
| + }
|
| + }
|
| +
|
| + if (found) {
|
| + var text = range.toString();
|
| + // Tests if the character is actually a word character (a letter, digit,
|
| + // underscore, or any Unicode letter). If the character is not a word
|
| + // character it means the given point is in a whitespace, punctuation or
|
| + // other non-relevant characters, and in this case it should not be
|
| + // considered a successful finding.
|
| + if (!Context.reWordCharacter_.test(text)) {
|
| + found = false;
|
| + }
|
| + }
|
| +
|
| + range.detach();
|
| +
|
| + return found ? start : -1;
|
| +};
|
| +
|
| +/**
|
| + * Gets the surrounding data from a given range.
|
| + * @param {ContextData} contextData Object where the data will be written.
|
| + * @param {Range} range A text range.
|
| + * @private
|
| + */
|
| +Context.extractSurroundingDataFromRange = function(contextData, range) {
|
| + var surroundingRange = range.cloneRange();
|
| + var length = surroundingRange.toString().length;
|
| + while (length < Context.NUMBER_OF_CHARS_IN_SURROUNDING_SENTENCES) {
|
| + surroundingRange.expand('sentence');
|
| + var oldLength = length;
|
| + length = surroundingRange.toString().length;
|
| + if (oldLength == length) {
|
| + break;
|
| + }
|
| + }
|
| +
|
| + var textNodeType = range.startContainer.TEXT_NODE;
|
| + var selectionStartOffset = 0;
|
| + var selectionStartNode = range.startContainer;
|
| + if (range.startContainer.nodeType == textNodeType) {
|
| + selectionStartOffset = range.startOffset;
|
| + } else {
|
| + selectionStartNode = range.startContainer.childNodes[range.startOffset];
|
| + }
|
| +
|
| + var surroundingStartOffset = 0;
|
| +
|
| + if (surroundingRange.startContainer.nodeType == textNodeType) {
|
| + surroundingStartOffset = surroundingRange.startOffset;
|
| + }
|
| + var surroundingRemoveAtEnd = 0;
|
| + if (surroundingRange.endContainer.nodeType == textNodeType) {
|
| + surroundingRemoveAtEnd = surroundingRange.endContainer.textContent.length -
|
| + surroundingRange.endOffset;
|
| + }
|
| +
|
| + // It is possible that invalid nodes are present inside the surrounding range.
|
| + // Extract text to make sure this is not the case.
|
| + var textNodes = Context.textNodesFromRange(surroundingRange);
|
| +
|
| + var offset = 0;
|
| + var index = 0;
|
| + var surroundingString = '';
|
| + var foundStart = false;
|
| + for (index = 0; index < textNodes.length; index++) {
|
| + if (textNodes[index] === ' ') {
|
| + surroundingString += ' ';
|
| + if (!foundStart) {
|
| + offset += 1;
|
| + }
|
| + continue;
|
| + }
|
| + if (textNodes[index] == selectionStartNode) {
|
| + foundStart = true;
|
| + }
|
| + if (!foundStart) {
|
| + offset += textNodes[index].textContent.length;
|
| + }
|
| + surroundingString += textNodes[index].textContent;
|
| + }
|
| + offset += selectionStartOffset - surroundingStartOffset;
|
| +
|
| + surroundingString = surroundingString.substring(surroundingStartOffset,
|
| + surroundingString.length - surroundingRemoveAtEnd);
|
| +
|
| + contextData.surroundingText = surroundingString;
|
| + contextData.offsetStart = offset;
|
| + contextData.offsetEnd = offset + range.toString().length;
|
| +
|
| + Context.surroundingRange = {
|
| + startContainer: surroundingRange.startContainer,
|
| + startOffset: surroundingRange.startOffset,
|
| + endContainer: surroundingRange.endContainer,
|
| + endOffset: surroundingRange.endOffset,
|
| + highlightStartOffset: contextData.offsetStart,
|
| + highlightEndOffset: contextData.offsetEnd
|
| + };
|
| +
|
| + surroundingRange.detach();
|
| +};
|
| +
|
| +/**
|
| + * Returns whether character is a whitespace.
|
| + * @param {string} character The character to test.
|
| + * @return {boolean} Whether |character| is a whitespace.
|
| + */
|
| +Context.isCharSpace = function(character) {
|
| + return character.trim().length == 0;
|
| +};
|
| +
|
| +/**
|
| + * Find next node in a DFS order. A parent is returned before its children.
|
| + * @param {Node} node The current node.
|
| + * @param {Node} endNode The right limit of the DFS.
|
| + * @param {boolean} onlyValid Whether invalid node should be skipped in DFS.
|
| + * @return {Node} The node coming after |node|. Null is endNode is reached.
|
| + */
|
| +Context.getNextNode = function(node, endNode, onlyValid) {
|
| + if (!node)
|
| + return null;
|
| +
|
| + if (node.childNodes.length > 0) {
|
| + node = node.childNodes[0];
|
| + if (node.nodeType == node.TEXT_NODE ||
|
| + (node.nodeType == node.ELEMENT_NODE &&
|
| + (!onlyValid || Context.isValidElement(node, true)))) {
|
| + return node;
|
| + }
|
| + if (node.nodeType == node.ELEMENT_NODE && node.contains(endNode)) {
|
| + return null;
|
| + }
|
| + }
|
| +
|
| + while (node != null) {
|
| + if (!node.nextSibling) {
|
| + node = node.parentNode;
|
| + continue;
|
| + }
|
| + if (node.contains(endNode))
|
| + return null;
|
| + node = node.nextSibling;
|
| + if (node.nodeType == node.TEXT_NODE ||
|
| + (node.nodeType == node.ELEMENT_NODE &&
|
| + (!onlyValid || Context.isValidElement(node, true)))) {
|
| + return node;
|
| + }
|
| + }
|
| + return null;
|
| +};
|
| +
|
| +/**
|
| + * Creates the list of text nodes that are part of range. The list will contain
|
| + * whitespace strings to replace block elements.
|
| + * @param {Range} range The range containing the text information.
|
| + * @return {Array.<Node>} The array of text nodes in the range.
|
| + */
|
| +Context.textNodesFromRange = function(range) {
|
| + var blockStack = [];
|
| +
|
| + var startNode = range.startContainer.childNodes[range.startOffset] ||
|
| + range.startContainer;
|
| + var endNode = range.endContainer.childNodes[range.endOffset] ||
|
| + range.endContainer;
|
| +
|
| + if (startNode == endNode && startNode.childNodes.length === 0) {
|
| + return [startNode];
|
| + }
|
| +
|
| + var textNodes = [];
|
| + var node = startNode;
|
| + // Do not add a space as first node.
|
| + var addSpace = false;
|
| + var lastNodeAddedHasSpace = true;
|
| +
|
| + do {
|
| + if (node.nodeType == node.TEXT_NODE && node.textContent.length > 0) {
|
| + while (blockStack.length && !blockStack[0].contains(node)) {
|
| + addSpace = true;
|
| + blockStack.shift();
|
| + }
|
| + if (addSpace && !lastNodeAddedHasSpace &&
|
| + !Context.isCharSpace(node.textContent[0])) {
|
| + textNodes.push(' ');
|
| + addSpace = false;
|
| + }
|
| + textNodes.push(node);
|
| + lastNodeAddedHasSpace = Context.isCharSpace(
|
| + node.textContent[node.textContent.length - 1]);
|
| + } else if (node.nodeType == node.ELEMENT_NODE) {
|
| + var style = window.getComputedStyle(node);
|
| + if (style && style.display != 'inline') {
|
| + addSpace = true;
|
| + blockStack.unshift(node);
|
| + }
|
| + }
|
| + node = Context.getNextNode(node, endNode, true);
|
| + } while (node && node != endNode);
|
| +
|
| + if (node == endNode && node.nodeType == node.TEXT_NODE) {
|
| + if (addSpace && !lastNodeAddedHasSpace &&
|
| + !Context.isCharSpace(node.textContent[0])) {
|
| + textNodes.push(' ');
|
| + }
|
| + textNodes.push(node);
|
| + }
|
| + return textNodes;
|
| +};
|
| +
|
| +/**
|
| + * Checks if a particular range contains a given point.
|
| + * @param {Range} range A text range.
|
| + * @param {number} x The point's x coordinate.
|
| + * @param {number} y The point's y coordinate.
|
| + * @return {boolean} whether the range contains the given point.
|
| + */
|
| +Context.rangeContainsPoint = function(range, x, y) {
|
| + var rects = range.getClientRects();
|
| + var rectsLength = rects.length;
|
| + for (var i = 0, length = rectsLength; i < length; i++) {
|
| + var rect = rects[i];
|
| + var contains = Context.rectContainsPoint(rect, x, y);
|
| + if (contains) {
|
| + return true;
|
| + }
|
| + }
|
| + return false;
|
| +};
|
| +
|
| +/**
|
| + * Checks if a particular rectangle contains a given point.
|
| + * @param {Rect} rect A rectangle.
|
| + * @param {number} x The point's x coordinate.
|
| + * @param {number} y The point's y coordinate.
|
| + * @return {boolean} whether the rectangle contains the given point.
|
| + * @private
|
| + */
|
| +Context.rectContainsPoint = function(rect, x, y) {
|
| + if (x >= rect.left && x <= rect.right &&
|
| + y >= rect.top && y <= rect.bottom) {
|
| + return true;
|
| + }
|
| + return false;
|
| +};
|
| +
|
| +/**
|
| + * Create a new range to the [startOffset, endOffset] in the surrounding
|
| + * range returned by Context.extractSurroundingDataFromRange.
|
| + * @param {number} startOffset first character to include in the range.
|
| + * @param {number} endOffset last character to include in the range.
|
| + * @return {Range} the range including [startOffset, endOffset].
|
| + * @private
|
| + */
|
| +Context.createSurroundingRange = function(startOffset, endOffset) {
|
| + if ((startOffset == Context.surroundingRange.highlightStartOffset &&
|
| + endOffset == Context.surroundingRange.highlightEndOffset) ||
|
| + startOffset >= endOffset) {
|
| + return;
|
| + }
|
| + var range = document.createRange();
|
| + range.setStart(Context.surroundingRange.startContainer,
|
| + Context.surroundingRange.startOffset);
|
| + range.setEnd(Context.surroundingRange.endContainer,
|
| + Context.surroundingRange.endOffset);
|
| +
|
| + var textNodes = Context.textNodesFromRange(range);
|
| + var highlightRange = document.createRange();
|
| + var length = textNodes.length;
|
| + var offset = 0;
|
| + if (length > 0 && Context.surroundingRange.startContainer == textNodes[0]) {
|
| + // Ignore the text inside |startContainer| before the range.
|
| + offset -= Context.surroundingRange.startOffset;
|
| + }
|
| + var startSet = false;
|
| + for (var index = 0; index < length; index++) {
|
| + var node = textNodes[index];
|
| + if (node === ' ') {
|
| + offset += 1;
|
| + continue;
|
| + }
|
| + if (!startSet && offset + node.textContent.length > startOffset) {
|
| + startSet = true;
|
| + highlightRange.setStart(node, startOffset - offset);
|
| + }
|
| + if (offset + node.textContent.length >= endOffset) {
|
| + highlightRange.setEnd(node, endOffset - offset);
|
| + break;
|
| + }
|
| + offset += node.textContent.length;
|
| + }
|
| + return highlightRange;
|
| +};
|
| +
|
| +/**
|
| + * Returns the string contained in the parameter |range|. Text rules are the
|
| + * same as textNodesFromRange.
|
| + * @param {Range} range the range to convert to string.
|
| + * @return {string} the string contained in range.
|
| + * @private
|
| + */
|
| +Context.rangeToString = function(range) {
|
| + var textNodes = Context.textNodesFromRange(range);
|
| + var string = '';
|
| + var length = textNodes.length;
|
| + for (var index = 0; index < length; index++) {
|
| + var node = textNodes[index];
|
| + if (node === ' ') {
|
| + string += ' ';
|
| + continue;
|
| + }
|
| + var nodeString = node.textContent;
|
| + if (node == range.endNode) {
|
| + nodeString = nodeString.substring(0, range.endOffset);
|
| + }
|
| + if (node == range.startNode) {
|
| + nodeString.substring(range.startOffset, nodeString.length);
|
| + }
|
| + string += nodeString;
|
| + }
|
| + return string;
|
| +};
|
| +
|
| +/**
|
| + * Create the text client rects contained in the current |highlightRange|.
|
| + * @return {string} A string containing a comma separated list of rects in
|
| + * the format 'top bottom left right'. Coordinates are page based (not screen
|
| + * based).
|
| + * @private
|
| + */
|
| +Context.getHighlightRects = function() {
|
| + if (Context.highlightRange == null) {
|
| + return '';
|
| + }
|
| + var rectsArray = new Array();
|
| + var rects = Context.highlightRange.getClientRects();
|
| + var rectsLength = rects.length;
|
| + for (var i = 0, length = rectsLength; i < length; i++) {
|
| + var top = rects[i].top + document.body.scrollTop;
|
| + var bottom = rects[i].bottom + document.body.scrollTop;
|
| + var left = rects[i].left + document.body.scrollLeft;
|
| + var right = rects[i].right + document.body.scrollLeft;
|
| + rectsArray[i] = '' + top + ' ' + bottom + ' ' + left + ' ' + right;
|
| + }
|
| + return rectsArray.join(',');
|
| +};
|
| +
|
| +/* Anyonymizing block end */
|
| +}
|
|
|