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.
6 * Based heavily on code from the Google iOS app.
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.
13 * Namespace for this file. Depends on __gCrWeb having already been injected.
15 __gCrWeb['findInPage'] = {};
18 * Index of the current highlighted choice. -1 means none.
21 __gCrWeb['findInPage']['index'] = -1;
24 * The list of found searches in span form.
25 * @type {Array<Element>}
27 __gCrWeb['findInPage']['spans'] = [];
30 * The list of frame documents.
31 * TODO(justincohen): x-domain frames won't work.
32 * @type {Array<Document>}
34 __gCrWeb['findInPage'].frameDocs = [];
37 * Associate array to stash element styles while the element is highlighted.
38 * @type {Object<Element,Object<string,string>>}
40 __gCrWeb['findInPage'].savedElementStyles = {};
43 * The style DOM element that we add.
46 __gCrWeb['findInPage'].style = null;
49 * Width we expect the page to be. For example (320/480) for iphone,
50 * (1024/768) for ipad.
53 __gCrWeb['findInPage'].pageWidth = 320;
56 * Height we expect the page to be.
59 __gCrWeb['findInPage'].pageHeight = 480;
62 * Maximum number of visible elements to count
65 __gCrWeb['findInPage'].maxVisibleElements = 100;
68 * A search is in progress.
71 __gCrWeb['findInPage'].searchInProgress = false;
74 * Node names that are not going to be processed.
77 __gCrWeb['findInPage'].ignoreNodeNames = {
85 * Class name of CSS element.
88 __gCrWeb['findInPage'].CSS_CLASS_NAME = 'find_in_page';
94 __gCrWeb['findInPage'].CSS_STYLE_ID = '__gCrWeb.findInPageStyle';
97 * Result passed back to app to indicate no results for the query.
100 __gCrWeb['findInPage'].NO_RESULTS = '[0,[0,0,0]]';
103 * Regex to escape regex special characters in a string.
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.
118 __gCrWeb['findInPage'].getRegex = function(findText, opt_split) {
119 var regexString = '';
122 var split = findText.split(' ');
123 for (var i = 0; i < split.length; i++) {
124 words.push(__gCrWeb['findInPage'].escapeRegex(split[i]));
126 var joinedWords = words.join('|');
128 // Match at least one word.
129 '\\b(?:' + joinedWords + ')' +
130 // Include zero or more additional words separated by whitespace.
131 '(?:\\s*\\b(?:' + joinedWords + '))*' +
134 regexString = '(' + __gCrWeb['findInPage'].escapeRegex(findText) + ')';
136 return new RegExp(regexString, 'ig');
140 * Get current timestamp.
141 * @return {number} timestamp.
143 __gCrWeb['findInPage'].time = function() {
144 return (new Date).getTime();
148 * After |timeCheck| iterations, return true if |now| - |start| is greater than
150 * @return {boolean} Find in page needs to return.
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.
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']();
173 if (!findText || !findText.replace(/\u00a0|\s/g, '')) {
174 // No searching for emptyness.
175 return __gCrWeb['findInPage'].NO_RESULTS;
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);
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.
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]);
240 if (node.nodeType == 3 && node.parentNode) {
244 while (match = regex.exec(node.textContent)) {
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,
252 nodes.push(node.ownerDocument.createTextNode(nodeSubstr));
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);
261 strIndex = match.index + matchText.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));
273 __gCrWeb['findInPage'].replacements.push(
274 {oldNode: node, newNodes: nodes});
280 if (__gCrWeb['findInPage'].overTime())
283 if (__gCrWeb['findInPage'].stack.length > 0) {
284 __gCrWeb['findInPage'].node = __gCrWeb['findInPage'].stack.pop();
286 __gCrWeb['findInPage'].node = null;
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;
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]);
310 parent.insertBefore(replacement.newNodes[rNodesIndex],
311 replacement.oldNode);
313 parent.removeChild(replacement.oldNode);
314 __gCrWeb['findInPage'].replacementNewNodesIndex = 0;
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]);
333 // Stop after |maxVisible| elements.
334 if (__gCrWeb['findInPage'].visibleFound > maxVisible) {
335 __gCrWeb['findInPage']['spans'][index].visibleIndex = maxVisible;
339 if (__gCrWeb['findInPage'].isVisible(elem)) {
340 __gCrWeb['findInPage'].visibleFound++;
341 __gCrWeb['findInPage']['spans'][index].visibleIndex =
342 __gCrWeb['findInPage'].visibleFound;
346 __gCrWeb['findInPage'].searchInProgress = false;
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);
359 pos = __gCrWeb['findInPage']['goNext']();
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);
366 return __gCrWeb['findInPage'].NO_RESULTS;
372 * Converts a node list to an array.
373 * @param {object} nodeList DOM node list.
374 * @return {object} array.
376 __gCrWeb['findInPage'].toArray = function(nodeList) {
378 for (var i = 0; i < nodeList.length; i++)
379 array[i] = nodeList[i];
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.
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));
400 * Removes all currently highlighted spans.
401 * Note: It does not restore previous state, just removes the class name.
403 __gCrWeb['findInPage']['clearHighlight'] = function() {
404 if (__gCrWeb['findInPage']['index'] >= 0) {
405 __gCrWeb['findInPage'].removeSelectHighlight(
406 __gCrWeb['findInPage'].getCurrentSpan());
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
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;
432 prevMod.replacement.textContent =
433 elemText + prevMod.replacement.textContent;
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;
441 if (elem.nextSibling && elem.nextSibling.nodeType == 3) {
442 nodesToRemove.push(elem.nextSibling);
443 nodeText = nodeText + elem.nextSibling.textContent;
445 var textNode = elem.ownerDocument.createTextNode(nodeText);
446 modifications.push({nodesToRemove: nodesToRemove, replacement: textNode});
448 prevParent = parentNode;
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];
456 existing.parentNode.replaceChild(mod.replacement, existing);
458 existing.parentNode.removeChild(existing);
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.
471 __gCrWeb['findInPage']['incrementIndex'] = function() {
472 if (__gCrWeb['findInPage']['index'] >=
473 __gCrWeb['findInPage']['spans'].length - 1) {
474 __gCrWeb['findInPage']['index'] = 0;
476 __gCrWeb['findInPage']['index']++;
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
485 __gCrWeb['findInPage']['goNext'] = function() {
486 if (!__gCrWeb['findInPage']['spans'] ||
487 __gCrWeb['findInPage']['spans'].length == 0) {
490 if (__gCrWeb['findInPage']['index'] >= 0) {
491 // Remove previous highlight.
492 __gCrWeb['findInPage'].removeSelectHighlight(
493 __gCrWeb['findInPage'].getCurrentSpan());
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.
506 __gCrWeb['findInPage']['incrementIndex']();
507 if (0 === __gCrWeb['findInPage']['index'] && oldIndex < 0) {
508 // Didn't find anything visible and haven't highlighted anything yet.
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.
520 __gCrWeb['findInPage']['decrementIndex'] = function() {
521 if (__gCrWeb['findInPage']['index'] <= 0) {
522 __gCrWeb['findInPage']['index'] =
523 __gCrWeb['findInPage']['spans'].length - 1;
525 __gCrWeb['findInPage']['index']--;
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
534 __gCrWeb['findInPage']['goPrev'] = function() {
535 if (!__gCrWeb['findInPage']['spans'] ||
536 __gCrWeb['findInPage']['spans'].length == 0) {
539 if (__gCrWeb['findInPage']['index'] >= 0) {
540 // Remove previous highlight.
541 __gCrWeb['findInPage'].removeSelectHighlight(
542 __gCrWeb['findInPage'].getCurrentSpan());
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.
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']
567 __gCrWeb['findInPage'].addHighlightToIndex = function(opt_index) {
568 if (opt_index !== undefined) {
569 __gCrWeb['findInPage']['index'] = opt_index;
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.
582 __gCrWeb['findInPage'].updateElementStyle = function(element, style, value) {
583 if (!__gCrWeb['findInPage'].savedElementStyles[element]) {
584 __gCrWeb['findInPage'].savedElementStyles[element] = {};
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];
592 element.style[style] = value;
596 * Adds selected highlight style to the specified element.
597 * @param {Element} element Element to highlight.
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.
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];
613 for (var style in savedStyles) {
614 element.style[style] = savedStyles[style];
616 delete __gCrWeb['findInPage'].savedElementStyles[element];
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.
626 __gCrWeb['findInPage'].getNormalizedCoordinates = function(elem) {
627 var fip = __gCrWeb['findInPage'];
628 var pos = fip.findAbsolutePosition(elem);
630 Math.max(fip.getBodyWidth(), pos[0] + elem.offsetWidth);
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
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)));
645 * Scale coordinates according to the width of the screen, in case the screen
647 * @param {Array<number>} coordinates Coordinates to scale.
648 * @return {Array<number>} Scaled coordinates.
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']
659 * @return {string} JSON encoded array of the scroll coordinates "[x, y]".
661 __gCrWeb['findInPage'].findScrollDimensions = function(opt_index) {
662 if (opt_index !== undefined) {
663 __gCrWeb['findInPage']['index'] = opt_index;
665 var elem = __gCrWeb['findInPage'].getCurrentSpan();
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);
684 __gCrWeb['findInPage'].addHighlightToIndex();
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.
698 __gCrWeb['findInPage']['init'] = function(width, height) {
699 if (__gCrWeb['findInPage'].hasInitialized) {
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.
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();
724 * Enable the __gCrWeb['findInPage'] module.
725 * Mainly just adds the style for the classes.
727 __gCrWeb['findInPage'].enable = function() {
728 if (__gCrWeb['findInPage'].style) {
732 __gCrWeb['findInPage'].addStyle();
736 * Gets the scale ratio between the application window and the web document.
737 * @return {number} Scale.
739 __gCrWeb['findInPage'].getPageScale = function() {
740 return (__gCrWeb['findInPage'].pageWidth /
741 __gCrWeb['findInPage'].getBodyWidth());
745 * Maximum padding added to a highlighted item when selected.
748 __gCrWeb['findInPage'].MAX_HIGHLIGHT_PADDING = 10;
751 * Adds the appropriate style element to the page.
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);
761 __gCrWeb['findInPage'].addDocumentStyle = function(thisDocument) {
762 var styleContent = [];
763 function addCSSRule(name, style) {
764 styleContent.push(name, '{', style, '}');
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.
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);
795 __gCrWeb['findInPage'].style = null;
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.
808 __gCrWeb['findInPage']['disable'] = function() {
809 if (__gCrWeb['findInPage'].style) {
810 __gCrWeb['findInPage'].removeStyle();
811 window.setTimeout(__gCrWeb['findInPage']['clearHighlight'], 0);
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.
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.
849 __gCrWeb['findInPage'].isVisible = function(elem) {
855 var bottom = Infinity;
856 var right = Infinity;
858 var originalElement = elem;
859 var nextOffsetParent = originalElement.offsetParent;
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) {
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))
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) {
905 nextOffsetParent = elem.offsetParent;
908 elem = elem.parentNode;
909 computedStyle = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
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.
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.
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.
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.
948 __gCrWeb['findInPage'].frameDocuments = function() {
949 var windowsToSearch = [window];
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]);