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
]);