Upstreaming browser/ui/uikit_ui_util from iOS.
[chromium-blink-merge.git] / ios / chrome / browser / find_in_page / resources / find_in_page.js
blobae6289837ae422898a3d9074ffd332a154f6c444
1 // Copyright 2012 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.
5 /**
6  * Based heavily on code from the Google iOS app.
7  *
8  * @fileoverview A find in page tool.  It scans the DOM for elements with the
9  * text being search for, and wraps them with a span that highlights them.
10  */
12 /**
13  * Namespace for this file.  Depends on __gCrWeb having already been injected.
14  */
15 __gCrWeb['findInPage'] = {};
17 /**
18  * Index of the current highlighted choice.  -1 means none.
19  * @type {number}
20  */
21 __gCrWeb['findInPage']['index'] = -1;
23 /**
24  * The list of found searches in span form.
25  * @type {Array<Element>}
26  */
27 __gCrWeb['findInPage']['spans'] = [];
29 /**
30  * The list of frame documents.
31  * TODO(justincohen): x-domain frames won't work.
32  * @type {Array<Document>}
33  */
34 __gCrWeb['findInPage'].frameDocs = [];
36 /**
37  * Associate array to stash element styles while the element is highlighted.
38  * @type {Object<Element,Object<string,string>>}
39  */
40 __gCrWeb['findInPage'].savedElementStyles = {};
42 /**
43  * The style DOM element that we add.
44  * @type {Element}
45  */
46 __gCrWeb['findInPage'].style = null;
48 /**
49  * Width we expect the page to be.  For example (320/480) for iphone,
50  * (1024/768) for ipad.
51  * @type {number}
52  */
53 __gCrWeb['findInPage'].pageWidth = 320;
55 /**
56  * Height we expect the page to be.
57  * @type {number}
58  */
59 __gCrWeb['findInPage'].pageHeight = 480;
61 /**
62  * Maximum number of visible elements to count
63  * @type {number}
64  */
65 __gCrWeb['findInPage'].maxVisibleElements = 100;
67 /**
68  * A search is in progress.
69  * @type {boolean}
70  */
71 __gCrWeb['findInPage'].searchInProgress = false;
73 /**
74  * Node names that are not going to be processed.
75  * @type {Object}
76  */
77 __gCrWeb['findInPage'].ignoreNodeNames = {
78  'SCRIPT': 1,
79  'STYLE': 1,
80  'EMBED': 1,
81  'OBJECT': 1
84 /**
85  * Class name of CSS element.
86  * @type {string}
87  */
88 __gCrWeb['findInPage'].CSS_CLASS_NAME = 'find_in_page';
90 /**
91  * ID of CSS style.
92  * @type {string}
93  */
94 __gCrWeb['findInPage'].CSS_STYLE_ID = '__gCrWeb.findInPageStyle';
96 /**
97  * Result passed back to app to indicate no results for the query.
98  * @type {string}
99  */
100 __gCrWeb['findInPage'].NO_RESULTS = '[0,[0,0,0]]';
103  * Regex to escape regex special characters in a string.
104  * @type {RegExp}
105  */
106 __gCrWeb['findInPage'].REGEX_ESCAPER = /([.?*+^$[\]\\(){}|-])/g;
108 __gCrWeb['findInPage'].getCurrentSpan = function() {
109   return __gCrWeb['findInPage']['spans'][__gCrWeb['findInPage']['index']];
113  * Creates the regex needed to find the text.
114  * @param {string} findText Phrase to look for.
115  * @param {boolean} opt_split True to split up the phrase.
116  * @return {RegExp} regex needed to find the text.
117  */
118 __gCrWeb['findInPage'].getRegex = function(findText, opt_split) {
119   var regexString = '';
120   if (opt_split) {
121     var words = [];
122     var split = findText.split(' ');
123     for (var i = 0; i < split.length; i++) {
124       words.push(__gCrWeb['findInPage'].escapeRegex(split[i]));
125     }
126     var joinedWords = words.join('|');
127     regexString = '(' +
128         // Match at least one word.
129         '\\b(?:' + joinedWords + ')' +
130         // Include zero or more additional words separated by whitespace.
131         '(?:\\s*\\b(?:' + joinedWords + '))*' +
132         ')';
133   } else {
134     regexString = '(' + __gCrWeb['findInPage'].escapeRegex(findText) + ')';
135   }
136   return new RegExp(regexString, 'ig');
140  * Get current timestamp.
141  * @return {number} timestamp.
142  */
143 __gCrWeb['findInPage'].time = function() {
144   return (new Date).getTime();
148  * After |timeCheck| iterations, return true if |now| - |start| is greater than
149  * |timeout|.
150  * @return {boolean} Find in page needs to return.
151  */
152 __gCrWeb['findInPage'].overTime = function() {
153   return (__gCrWeb['findInPage'].time() - __gCrWeb['findInPage'].startTime >
154           __gCrWeb['findInPage'].timeout);
158  * Looks for a phrase in the DOM.
159  * @param {string} findText Phrase to look for like "ben franklin".
160  * @param {boolean} opt_split True to split up the words and look for any
161  *     of them.  False to require the full phrase to be there.
162  *     Undefined will try the full phrase, and if nothing is found do the split.
163  * @param {number} timeout Maximum time to run.
164  * @return {number} How many results there are in the page.
165  */
166 __gCrWeb['findInPage']['highlightWord'] =
167     function(findText, opt_split, timeout) {
168   if (__gCrWeb['findInPage']['spans'] &&
169       __gCrWeb['findInPage']['spans'].length) {
170     // Clean up a previous run.
171     __gCrWeb['findInPage']['clearHighlight']();
172   }
173   if (!findText || !findText.replace(/\u00a0|\s/g, '')) {
174     // No searching for emptyness.
175     return __gCrWeb['findInPage'].NO_RESULTS;
176   }
178   // Store all DOM modifications to do them in a tight loop at once.
179   __gCrWeb['findInPage'].replacements = [];
181   // Node is what we are currently looking at.
182   __gCrWeb['findInPage'].node = document.body;
184   // Holds what nodes we have not processed yet.
185   __gCrWeb['findInPage'].stack = [];
187   // Push frames into stack too.
188   for (var i = __gCrWeb['findInPage'].frameDocs.length - 1; i >= 0; i--) {
189     var doc = __gCrWeb['findInPage'].frameDocs[i];
190     __gCrWeb['findInPage'].stack.push(doc);
191   }
193   // Number of visible elements found.
194   __gCrWeb['findInPage'].visibleFound = 0;
196   // Index tracking variables so search can be broken up into multiple calls.
197   __gCrWeb['findInPage'].visibleIndex = 0;
198   __gCrWeb['findInPage'].replacementsIndex = 0;
199   __gCrWeb['findInPage'].replacementNewNodesIndex = 0;
201   __gCrWeb['findInPage'].regex =
202       __gCrWeb['findInPage'].getRegex(findText, opt_split);
204   __gCrWeb['findInPage'].searchInProgress = true;
206   return __gCrWeb['findInPage']['pumpSearch'](timeout);
210  * Break up find in page DOM regex, DOM manipulation and visibility check
211  * into sections that can be stopped and restarted later.  Because the js runs
212  * in the main UI thread, anything over timeout will cause the UI to lock up.
213  * @param {number} timeout Only run find in page until timeout.
214  * @return {boolean} Whether find in page completed.
215  */
216 __gCrWeb['findInPage']['pumpSearch'] = function(timeout) {
217   var opt_split = false;
218   // TODO(justincohen): It would be better if this DCHECKed.
219   if (__gCrWeb['findInPage'].searchInProgress == false)
220     return __gCrWeb['findInPage'].NO_RESULTS;
222   __gCrWeb['findInPage'].timeout = timeout;
223   __gCrWeb['findInPage'].startTime = __gCrWeb['findInPage'].time();
225   var regex = __gCrWeb['findInPage'].regex;
226   // Go through every node in DFS fashion.
227   while (__gCrWeb['findInPage'].node) {
228     var node = __gCrWeb['findInPage'].node;
229     var children = node.childNodes;
230     if (children && children.length) {
231       // add all (reasonable) children
232       for (var i = children.length - 1; i >= 0; --i) {
233         var child = children[i];
234         if ((child.nodeType == 1 || child.nodeType == 3) &&
235             !__gCrWeb['findInPage'].ignoreNodeNames[child.nodeName]) {
236           __gCrWeb['findInPage'].stack.push(children[i]);
237         }
238       }
239     }
240     if (node.nodeType == 3 && node.parentNode) {
241       var strIndex = 0;
242       var nodes = [];
243       var match;
244       while (match = regex.exec(node.textContent)) {
245         try {
246           var matchText = match[0];
248           // If there is content before this match, add it to a new text node.
249           if (match.index > 0) {
250             var nodeSubstr = node.textContent.substring(strIndex,
251                                                         match.index);
252             nodes.push(node.ownerDocument.createTextNode(nodeSubstr));
253           }
255           // Now create our matched element.
256           var element = node.ownerDocument.createElement('chrome_find');
257           element.setAttribute('class', __gCrWeb['findInPage'].CSS_CLASS_NAME);
258           element.innerHTML = __gCrWeb['findInPage'].escapeHTML(matchText);
259           nodes.push(element);
261           strIndex = match.index + matchText.length;
262         } catch (e) {
263           // Do nothing.
264         }
265       }
266       if (nodes.length) {
267         // Add any text after our matches to a new text node.
268         if (strIndex < node.textContent.length) {
269           var substr = node.textContent.substring(strIndex,
270                                                   node.textContent.length);
271           nodes.push(node.ownerDocument.createTextNode(substr));
272         }
273         __gCrWeb['findInPage'].replacements.push(
274             {oldNode: node, newNodes: nodes});
275         regex.lastIndex = 0;
276       }
278     }
280     if (__gCrWeb['findInPage'].overTime())
281       return '[false]';
283     if (__gCrWeb['findInPage'].stack.length > 0) {
284       __gCrWeb['findInPage'].node = __gCrWeb['findInPage'].stack.pop();
285     } else {
286       __gCrWeb['findInPage'].node = null;
287     }
288   }
290   // Insert each of the replacement nodes into the old node's parent, then
291   // remove the old node.
292   var replacements = __gCrWeb['findInPage'].replacements;
294   // Last position in replacements array.
295   var rIndex = __gCrWeb['findInPage'].replacementsIndex;
296   var rMax = replacements.length;
297   for (; rIndex < rMax; rIndex++) {
298     var replacement = replacements[rIndex];
299     var parent = replacement.oldNode.parentNode;
300     if (parent == null)
301       continue;
302     var rNodesMax = replacement.newNodes.length;
303     for (var rNodesIndex = __gCrWeb['findInPage'].replacementNewNodesIndex;
304          rNodesIndex < rNodesMax; rNodesIndex++) {
305       if (__gCrWeb['findInPage'].overTime()) {
306         __gCrWeb['findInPage'].replacementsIndex = rIndex;
307         __gCrWeb['findInPage'].replacementNewNodesIndex = rNodesIndex;
308         return __gCrWeb.stringify([false]);
309       }
310       parent.insertBefore(replacement.newNodes[rNodesIndex],
311                           replacement.oldNode);
312     }
313     parent.removeChild(replacement.oldNode);
314     __gCrWeb['findInPage'].replacementNewNodesIndex = 0;
315   }
316   // Save last position in replacements array.
317   __gCrWeb['findInPage'].replacementsIndex = rIndex;
319   __gCrWeb['findInPage']['spans'] =
320       __gCrWeb['findInPage'].getAllElementsByClassName(
321           __gCrWeb['findInPage'].CSS_CLASS_NAME);
323   // Count visible elements.
324   var max = __gCrWeb['findInPage']['spans'].length;
325   var maxVisible = __gCrWeb['findInPage'].maxVisibleElements;
326   for (var index = __gCrWeb['findInPage'].visibleIndex; index < max; index++) {
327     var elem = __gCrWeb['findInPage']['spans'][index];
328     if (__gCrWeb['findInPage'].overTime()) {
329       __gCrWeb['findInPage'].visibleIndex = index;
330       return __gCrWeb.stringify([false]);
331     }
333     // Stop after |maxVisible| elements.
334     if (__gCrWeb['findInPage'].visibleFound > maxVisible) {
335       __gCrWeb['findInPage']['spans'][index].visibleIndex = maxVisible;
336       continue;
337     }
339     if (__gCrWeb['findInPage'].isVisible(elem)) {
340       __gCrWeb['findInPage'].visibleFound++;
341       __gCrWeb['findInPage']['spans'][index].visibleIndex =
342           __gCrWeb['findInPage'].visibleFound;
343     }
344   }
346   __gCrWeb['findInPage'].searchInProgress = false;
348   var pos;
349   // Try again flow.
350   // If opt_split is true, we are done since we won't do any better.
351   // If opt_split is false, they were explicit about wanting the full thing
352   // so do not try with a split.
353   // If opt_split is undefined and we did not find an answer, go ahead and try
354   // splitting the terms.
355   if (__gCrWeb['findInPage']['spans'].length == 0 && opt_split === undefined) {
356     // Try to be more aggressive:
357     return __gCrWeb['findInPage']['highlightWord'](findText, true);
358   } else {
359     pos = __gCrWeb['findInPage']['goNext']();
360     if (pos) {
361       return '[' + __gCrWeb['findInPage'].visibleFound + ',' + pos + ']';
362     } else if (opt_split === undefined) {
363       // Nothing visible, go ahead and be more aggressive.
364       return __gCrWeb['findInPage']['highlightWord'](findText, true);
365     } else {
366       return __gCrWeb['findInPage'].NO_RESULTS;
367     }
368   }
372  * Converts a node list to an array.
373  * @param {object} nodeList DOM node list.
374  * @return {object} array.
375  */
376 __gCrWeb['findInPage'].toArray = function(nodeList) {
377   var array = [];
378   for (var i = 0; i < nodeList.length; i++)
379     array[i] = nodeList[i];
380   return array;
384  * Return all elements of class name, spread out over various frames.
385  * @param {string} name of class.
386  * @return {object} array of elements matching class name.
387  */
388 __gCrWeb['findInPage'].getAllElementsByClassName = function(name) {
389   var nodeList = document.getElementsByClassName(name);
390   var elements = __gCrWeb['findInPage'].toArray(nodeList);
391   for (var i = __gCrWeb['findInPage'].frameDocs.length - 1; i >= 0; i--) {
392     var doc = __gCrWeb['findInPage'].frameDocs[i];
393     nodeList = doc.getElementsByClassName(name);
394     elements = elements.concat(__gCrWeb['findInPage'].toArray(nodeList));
395   }
396   return elements;
400  * Removes all currently highlighted spans.
401  * Note: It does not restore previous state, just removes the class name.
402  */
403 __gCrWeb['findInPage']['clearHighlight'] = function() {
404   if (__gCrWeb['findInPage']['index'] >= 0) {
405     __gCrWeb['findInPage'].removeSelectHighlight(
406         __gCrWeb['findInPage'].getCurrentSpan());
407   }
408   // Store all DOM modifications to do them in a tight loop.
409   var modifications = [];
410   var length = __gCrWeb['findInPage']['spans'].length;
411   var prevParent = null;
412   for (var i = length - 1; i >= 0; i--) {
413     var elem = __gCrWeb['findInPage']['spans'][i];
414     var parentNode = elem.parentNode;
415     // Safari has an occasional |elem.innerText| bug that drops the trailing
416     // space.  |elem.innerText| would be more correct in this situation, but
417     // since we only allow text in this element, grabbing the HTML value should
418     // not matter.
419     var nodeText = elem.innerHTML;
420     // If this element has the same parent as the previous, check if we should
421     // add this node to the previous one.
422     if (prevParent && prevParent.isSameNode(parentNode) &&
423         elem.nextSibling.isSameNode(
424             __gCrWeb['findInPage']['spans'][i + 1].previousSibling)) {
425       var prevMod = modifications[modifications.length - 1];
426       prevMod.nodesToRemove.push(elem);
427       var elemText = elem.innerText;
428       if (elem.previousSibling) {
429         prevMod.nodesToRemove.push(elem.previousSibling);
430         elemText = elem.previousSibling.textContent + elemText;
431       }
432       prevMod.replacement.textContent =
433           elemText + prevMod.replacement.textContent;
434     }
435     else { // Element isn't attached to previous, so create a new modification.
436       var nodesToRemove = [elem];
437       if (elem.previousSibling && elem.previousSibling.nodeType == 3) {
438         nodesToRemove.push(elem.previousSibling);
439         nodeText = elem.previousSibling.textContent + nodeText;
440       }
441       if (elem.nextSibling && elem.nextSibling.nodeType == 3) {
442         nodesToRemove.push(elem.nextSibling);
443         nodeText = nodeText + elem.nextSibling.textContent;
444       }
445       var textNode = elem.ownerDocument.createTextNode(nodeText);
446       modifications.push({nodesToRemove: nodesToRemove, replacement: textNode});
447     }
448     prevParent = parentNode;
449   }
450   var numMods = modifications.length;
451   for (i = numMods - 1; i >= 0; i--) {
452     var mod = modifications[i];
453     for (var j = 0; j < mod.nodesToRemove.length; j++) {
454       var existing = mod.nodesToRemove[j];
455       if (j == 0) {
456         existing.parentNode.replaceChild(mod.replacement, existing);
457       } else {
458         existing.parentNode.removeChild(existing);
459       }
460     }
461   }
463   __gCrWeb['findInPage']['spans'] = [];
464   __gCrWeb['findInPage']['index'] = -1;
468  * Increments the index of the current highlighted span or, if the index is
469  * already at the end, sets it to the index of the first span in the page.
470  */
471 __gCrWeb['findInPage']['incrementIndex'] = function() {
472   if (__gCrWeb['findInPage']['index'] >=
473       __gCrWeb['findInPage']['spans'].length - 1) {
474     __gCrWeb['findInPage']['index'] = 0;
475   } else {
476     __gCrWeb['findInPage']['index']++;
477   }
481  * Switches to the next result, animating a little highlight in the process.
482  * @return {string} JSON encoded array of coordinates to scroll to, or blank if
483  *     nothing happened.
484  */
485 __gCrWeb['findInPage']['goNext'] = function() {
486   if (!__gCrWeb['findInPage']['spans'] ||
487       __gCrWeb['findInPage']['spans'].length == 0) {
488     return '';
489   }
490   if (__gCrWeb['findInPage']['index'] >= 0) {
491     // Remove previous highlight.
492     __gCrWeb['findInPage'].removeSelectHighlight(
493         __gCrWeb['findInPage'].getCurrentSpan());
494   }
495   // Iterate through to the next index, but because they might not be visible,
496   // keep trying until you find one that is.  Make sure we don't loop forever by
497   // stopping on what we are currently highlighting.
498   var oldIndex = __gCrWeb['findInPage']['index'];
499   __gCrWeb['findInPage']['incrementIndex']();
500   while (!__gCrWeb['findInPage'].isVisible(
501               __gCrWeb['findInPage'].getCurrentSpan())) {
502     if (oldIndex === __gCrWeb['findInPage']['index']) {
503       // Checked all spans but didn't find anything else visible.
504       return '';
505     }
506     __gCrWeb['findInPage']['incrementIndex']();
507     if (0 === __gCrWeb['findInPage']['index'] && oldIndex < 0) {
508       // Didn't find anything visible and haven't highlighted anything yet.
509       return '';
510     }
511   }
512   // Return scroll dimensions.
513   return __gCrWeb['findInPage'].findScrollDimensions();
517  * Decrements the index of the current highlighted span or, if the index is
518  * already at the beginning, sets it to the index of the last span in the page.
519  */
520 __gCrWeb['findInPage']['decrementIndex'] = function() {
521   if (__gCrWeb['findInPage']['index'] <= 0) {
522     __gCrWeb['findInPage']['index'] =
523         __gCrWeb['findInPage']['spans'].length - 1;
524   } else {
525     __gCrWeb['findInPage']['index']--;
526   }
530  * Switches to the previous result, animating a little highlight in the process.
531  * @return {string} JSON encoded array of coordinates to scroll to, or blank if
532  *     nothing happened.
533  */
534 __gCrWeb['findInPage']['goPrev'] = function() {
535   if (!__gCrWeb['findInPage']['spans'] ||
536       __gCrWeb['findInPage']['spans'].length == 0) {
537     return '';
538   }
539   if (__gCrWeb['findInPage']['index'] >= 0) {
540     // Remove previous highlight.
541     __gCrWeb['findInPage'].removeSelectHighlight(
542         __gCrWeb['findInPage'].getCurrentSpan());
543   }
544   // Iterate through to the next index, but because they might not be visible,
545   // keep trying until you find one that is.  Make sure we don't loop forever by
546   // stopping on what we are currently highlighting.
547   var old = __gCrWeb['findInPage']['index'];
548   __gCrWeb['findInPage']['decrementIndex']();
549   while (!__gCrWeb['findInPage'].isVisible(
550              __gCrWeb['findInPage'].getCurrentSpan())) {
551     __gCrWeb['findInPage']['decrementIndex']();
552     if (old == __gCrWeb['findInPage']['index']) {
553       // Checked all spans but didn't find anything.
554       return '';
555     }
556   }
558   // Return scroll dimensions.
559   return __gCrWeb['findInPage'].findScrollDimensions();
563  * Adds the special highlighting to the result at the index.
564  * @param {number} opt_index Index to replace __gCrWeb['findInPage']['index']
565  *                 with.
566  */
567 __gCrWeb['findInPage'].addHighlightToIndex = function(opt_index) {
568   if (opt_index !== undefined) {
569     __gCrWeb['findInPage']['index'] = opt_index;
570   }
571   __gCrWeb['findInPage'].addSelectHighlight(
572       __gCrWeb['findInPage'].getCurrentSpan());
576  * Updates the elements style, while saving the old style in
577  * __gCrWeb['findInPage'].savedElementStyles.
578  * @param {Element} element Element to update.
579  * @param {string} style Name of the style to update.
580  * @param {string} value New style value.
581  */
582 __gCrWeb['findInPage'].updateElementStyle = function(element, style, value) {
583   if (!__gCrWeb['findInPage'].savedElementStyles[element]) {
584     __gCrWeb['findInPage'].savedElementStyles[element] = {};
585   }
586   // We need to keep the original style setting for this element, so if we've
587   // already saved a value for this style don't update it.
588   if (!__gCrWeb['findInPage'].savedElementStyles[element][style]) {
589     __gCrWeb['findInPage'].savedElementStyles[element][style] =
590         element.style[style];
591   }
592   element.style[style] = value;
596  * Adds selected highlight style to the specified element.
597  * @param {Element} element Element to highlight.
598  */
599 __gCrWeb['findInPage'].addSelectHighlight = function(element) {
600   element.className = (element.className || '') + ' findysel';
604  * Removes selected highlight style from the specified element.
605  * @param {Element} element Element to remove highlighting from.
606  */
607 __gCrWeb['findInPage'].removeSelectHighlight = function(element) {
608   element.className = (element.className || '').replace(/\sfindysel/g, '');
610   // Restore any styles we may have saved when adding the select highlighting.
611   var savedStyles = __gCrWeb['findInPage'].savedElementStyles[element];
612   if (savedStyles) {
613     for (var style in savedStyles) {
614       element.style[style] = savedStyles[style];
615     }
616     delete __gCrWeb['findInPage'].savedElementStyles[element];
617   }
621  * Normalize coordinates according to the current document dimensions. Don't go
622  * too far off the screen in either direction. Try to center if possible.
623  * @param {Element} elem Element to find normalized coordinates for.
624  * @return {Array<number>} Normalized coordinates.
625  */
626 __gCrWeb['findInPage'].getNormalizedCoordinates = function(elem) {
627   var fip = __gCrWeb['findInPage'];
628   var pos = fip.findAbsolutePosition(elem);
629   var maxX =
630       Math.max(fip.getBodyWidth(), pos[0] + elem.offsetWidth);
631   var maxY =
632       Math.max(fip.getBodyHeight(), pos[1] + elem.offsetHeight);
633   // Don't go too far off the screen in either direction.  Try to center if
634   // possible.
635   var xPos = Math.max(0,
636       Math.min(maxX - window.innerWidth,
637                pos[0] - (window.innerWidth / 2)));
638   var yPos = Math.max(0,
639       Math.min(maxY - window.innerHeight,
640                pos[1] - (window.innerHeight / 2)));
641   return [xPos, yPos];
645  * Scale coordinates according to the width of the screen, in case the screen
646  * is zoomed out.
647  * @param {Array<number>} coordinates Coordinates to scale.
648  * @return {Array<number>} Scaled coordinates.
649  */
650 __gCrWeb['findInPage'].scaleCoordinates = function(coordinates) {
651   var scaleFactor = __gCrWeb['findInPage'].pageWidth / window.innerWidth;
652   return [coordinates[0] * scaleFactor, coordinates[1] * scaleFactor];
656  * Finds the position of the result and scrolls to it.
657  * @param {number} opt_index Index to replace __gCrWeb['findInPage']['index']
658  *                 with.
659  * @return {string} JSON encoded array of the scroll coordinates "[x, y]".
660  */
661 __gCrWeb['findInPage'].findScrollDimensions = function(opt_index) {
662   if (opt_index !== undefined) {
663     __gCrWeb['findInPage']['index'] = opt_index;
664   }
665   var elem = __gCrWeb['findInPage'].getCurrentSpan();
666   if (!elem) {
667     return '';
668   }
669   var normalized = __gCrWeb['findInPage'].getNormalizedCoordinates(elem);
670   var xPos = normalized[0];
671   var yPos = normalized[1];
673   // Perform the scroll.
674   //window.scrollTo(xPos, yPos);
676   if (xPos < window.pageXOffset ||
677       xPos >= (window.pageXOffset + window.innerWidth) ||
678       yPos < window.pageYOffset ||
679       yPos >= (window.pageYOffset + window.innerHeight)) {
680     // If it's off the screen.  Wait a bit to start the highlight animation so
681     // that scrolling can get there first.
682     window.setTimeout(__gCrWeb['findInPage'].addHighlightToIndex, 250);
683   } else {
684     __gCrWeb['findInPage'].addHighlightToIndex();
685   }
686   var scaled = __gCrWeb['findInPage'].scaleCoordinates(normalized);
687   var index = __gCrWeb['findInPage'].getCurrentSpan().visibleIndex;
688   scaled.unshift(index);
689   return __gCrWeb.stringify(scaled);
693  * Initialize the __gCrWeb['findInPage'] module.
694  * @param {number} width Width of page.
695  * @param {number} height Height of page.
697  */
698 __gCrWeb['findInPage']['init'] = function(width, height) {
699   if (__gCrWeb['findInPage'].hasInitialized) {
700     return;
701   }
702   __gCrWeb['findInPage'].pageWidth = width;
703   __gCrWeb['findInPage'].pageHeight = height;
704   __gCrWeb['findInPage'].frameDocs = __gCrWeb['findInPage'].frameDocuments();
705   __gCrWeb['findInPage'].enable();
706   __gCrWeb['findInPage'].hasInitialized = true;
710  * When the GSA app detects a zoom change, we need to update our css.
711  * @param {number} width Width of page.
712  * @param {number} height Height of page.
713  */
714 __gCrWeb['findInPage']['fixZoom'] = function(width, height) {
715   __gCrWeb['findInPage'].pageWidth = width;
716   __gCrWeb['findInPage'].pageHeight = height;
717   if (__gCrWeb['findInPage'].style) {
718     __gCrWeb['findInPage'].removeStyle();
719     __gCrWeb['findInPage'].addStyle();
720   }
724  * Enable the __gCrWeb['findInPage'] module.
725  * Mainly just adds the style for the classes.
726  */
727 __gCrWeb['findInPage'].enable = function() {
728   if (__gCrWeb['findInPage'].style) {
729     // Already enabled.
730     return;
731   }
732   __gCrWeb['findInPage'].addStyle();
736  * Gets the scale ratio between the application window and the web document.
737  * @return {number} Scale.
738  */
739 __gCrWeb['findInPage'].getPageScale = function() {
740   return (__gCrWeb['findInPage'].pageWidth /
741       __gCrWeb['findInPage'].getBodyWidth());
745  * Maximum padding added to a highlighted item when selected.
746  * @type {number}
747  */
748 __gCrWeb['findInPage'].MAX_HIGHLIGHT_PADDING = 10;
751  * Adds the appropriate style element to the page.
752  */
753 __gCrWeb['findInPage'].addStyle = function() {
754   __gCrWeb['findInPage'].addDocumentStyle(document);
755   for (var i = __gCrWeb['findInPage'].frameDocs.length - 1; i >= 0; i--) {
756     var doc = __gCrWeb['findInPage'].frameDocs[i];
757     __gCrWeb['findInPage'].addDocumentStyle(doc);
758   }
761 __gCrWeb['findInPage'].addDocumentStyle = function(thisDocument) {
762   var styleContent = [];
763   function addCSSRule(name, style) {
764     styleContent.push(name, '{', style, '}');
765   };
766   var scale = __gCrWeb['findInPage'].getPageScale();
767   var zoom = (1.0 / scale);
768   var left = ((1 - scale) / 2 * 100);
769   addCSSRule('.' + __gCrWeb['findInPage'].CSS_CLASS_NAME,
770              'background-color:#ffff00 !important;' +
771              'padding:0px;margin:0px;' +
772              'overflow:visible !important;');
773   addCSSRule('.findysel',
774              'background-color:#ff9632 !important;' +
775              'padding:0px;margin:0px;' +
776              'overflow:visible !important;');
777   __gCrWeb['findInPage'].style = thisDocument.createElement('style');
778   __gCrWeb['findInPage'].style.id = __gCrWeb['findInPage'].CSS_STYLE_ID;
779   __gCrWeb['findInPage'].style.setAttribute('type', 'text/css');
780   __gCrWeb['findInPage'].style.appendChild(
781       thisDocument.createTextNode(styleContent.join('')));
782   thisDocument.body.appendChild(__gCrWeb['findInPage'].style);
786  * Removes the style element from the page.
787  */
788 __gCrWeb['findInPage'].removeStyle = function() {
789   if (__gCrWeb['findInPage'].style) {
790     __gCrWeb['findInPage'].removeDocumentStyle(document);
791     for (var i = __gCrWeb['findInPage'].frameDocs.length - 1; i >= 0; i--) {
792       var doc = __gCrWeb['findInPage'].frameDocs[i];
793       __gCrWeb['findInPage'].removeDocumentStyle(doc);
794     }
795     __gCrWeb['findInPage'].style = null;
796   }
799 __gCrWeb['findInPage'].removeDocumentStyle = function(thisDocument) {
800   var style = thisDocument.getElementById(__gCrWeb['findInPage'].CSS_STYLE_ID);
801   thisDocument.body.removeChild(style);
805  * Disables the __gCrWeb['findInPage'] module.
806  * Basically just removes the style and class names.
807  */
808 __gCrWeb['findInPage']['disable'] = function() {
809   if (__gCrWeb['findInPage'].style) {
810     __gCrWeb['findInPage'].removeStyle();
811     window.setTimeout(__gCrWeb['findInPage']['clearHighlight'], 0);
812   }
813   __gCrWeb['findInPage'].hasInitialized = false;
817 * Returns the width of the document.body.  Sometimes though the body lies to
818 * try to make the page not break rails, so attempt to find those as well.
819 * An example: wikipedia pages for the ipad.
820 * @return {number} Width of the document body.
822 __gCrWeb['findInPage'].getBodyWidth = function() {
823   var body = document.body;
824   var documentElement = document.documentElement;
825   return Math.max(body.scrollWidth, documentElement.scrollWidth,
826                   body.offsetWidth, documentElement.offsetWidth,
827                   body.clientWidth, documentElement.clientWidth);
831  * Returns the height of the document.body.  Sometimes though the body lies to
832  * try to make the page not break rails, so attempt to find those as well.
833  * An example: wikipedia pages for the ipad.
834  * @return {number} Height of the document body.
835  */
836 __gCrWeb['findInPage'].getBodyHeight = function() {
837   var body = document.body;
838   var documentElement = document.documentElement;
839   return Math.max(body.scrollHeight, documentElement.scrollHeight,
840                   body.offsetHeight, documentElement.offsetHeight,
841                   body.clientHeight, documentElement.clientHeight);
845  * Helper function that determines if an element is visible.
846  * @param {Element} elem Element to check.
847  * @return {boolean} Whether elem is visible or not.
848  */
849 __gCrWeb['findInPage'].isVisible = function(elem) {
850   if (!elem) {
851     return false;
852   }
853   var top = 0;
854   var left = 0;
855   var bottom = Infinity;
856   var right = Infinity;
858   var originalElement = elem;
859   var nextOffsetParent = originalElement.offsetParent;
860   var computedStyle =
861       elem.ownerDocument.defaultView.getComputedStyle(elem, null);
863   // We are currently handling all scrolling through the app, which means we can
864   // only scroll the window, not any scrollable containers in the DOM itself. So
865   // for now this function returns false if the element is scrolled outside the
866   // viewable area of its ancestors.
867   // TODO(justincohen): handle scrolling within the DOM.
868   var pageHeight = __gCrWeb['findInPage'].getBodyHeight();
869   var pageWidth = __gCrWeb['findInPage'].getBodyWidth();
871   while (elem && elem.nodeName != 'BODY') {
872     if (elem.style.display === 'none' ||
873         elem.style.visibility === 'hidden' ||
874         elem.style.opacity === 0 ||
875         computedStyle.display === 'none' ||
876         computedStyle.visibility === 'hidden' ||
877         computedStyle.opacity === 0) {
878       return false;
879     }
881     // For the original element and all ancestor offsetParents, trim down the
882     // visible area of the original element.
883     if (elem.isSameNode(originalElement) || elem.isSameNode(nextOffsetParent)) {
884       var visible = elem.getBoundingClientRect();
885       if (elem.style.overflow === 'hidden' &&
886           (visible.width === 0 || visible.height === 0))
887         return false;
889       top = Math.max(top, visible.top + window.pageYOffset);
890       bottom = Math.min(bottom, visible.bottom + window.pageYOffset);
891       left = Math.max(left, visible.left + window.pageXOffset);
892       right = Math.min(right, visible.right + window.pageXOffset);
894       // The element is not within the original viewport.
895       var notWithinViewport = top < 0 || left < 0;
897       // The element is flowing off the boundary of the page. Note this is
898       // not comparing to the size of the window, but the calculated offset
899       // size of the document body. This can happen if the element is within
900       // a scrollable container in the page.
901       var offPage = right > pageWidth || bottom > pageHeight;
902       if (notWithinViewport || offPage) {
903         return false;
904       }
905       nextOffsetParent = elem.offsetParent;
906     }
908     elem = elem.parentNode;
909     computedStyle = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
910   }
911   return true;
915  * Helper function to find the absolute position of an element on the page.
916  * @param {Element} elem Element to check.
917  * @return {Array<number>} [x, y] positions.
918  */
919 __gCrWeb['findInPage'].findAbsolutePosition = function(elem) {
920   var boundingRect = elem.getBoundingClientRect();
921   return [boundingRect.left + window.pageXOffset,
922       boundingRect.top + window.pageYOffset];
926  * @param {string} text Text to escape.
927  * @return {string} escaped text.
928  */
929 __gCrWeb['findInPage'].escapeHTML = function(text) {
930   var unusedDiv = document.createElement('div');
931   unusedDiv.innerText = text;
932   return unusedDiv.innerHTML;
936  * Escapes regexp special characters.
937  * @param {string} text Text to escape.
938  * @return {string} escaped text.
939  */
940 __gCrWeb['findInPage'].escapeRegex = function(text) {
941   return text.replace(__gCrWeb['findInPage'].REGEX_ESCAPER, '\\$1');
945  * Gather all iframes in the main window.
946  * @return {Array<Document>} frames.
947  */
948 __gCrWeb['findInPage'].frameDocuments = function() {
949   var windowsToSearch = [window];
950   var documents = [];
951   while (windowsToSearch.length != 0) {
952     var win = windowsToSearch.pop();
953     for (var i = win.frames.length - 1; i >= 0; i--) {
954       if (win.frames[i].document) {
955         documents.push(win.frames[i].document);
956         windowsToSearch.push(win.frames[i]);
957       }
958     }
959   }
960   return documents;