Commit d9a155a6 authored by droger's avatar droger Committed by Commit bot

[iOS] Upstream ios/chrome/browser/find_in_page

BUG=452495, 452492

Review URL: https://codereview.chromium.org/1023813003

Cr-Commit-Position: refs/heads/master@{#322349}
parent bb148160
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_BROWSER_FIND_IN_PAGE_FIND_IN_PAGE_CONTROLLER_H_
#define IOS_CHROME_BROWSER_FIND_IN_PAGE_FIND_IN_PAGE_CONTROLLER_H_
#import <Foundation/Foundation.h>
#include "base/ios/block_types.h"
namespace web {
class WebState;
}
@class FindInPageModel;
extern NSString* const kFindBarTextFieldWillBecomeFirstResponderNotification;
extern NSString* const kFindBarTextFieldDidResignFirstResponderNotification;
@protocol FindInPageControllerDelegate<NSObject>
// Informs the delegate when the scroll position is about to be changed on the
// page.
- (void)willAdjustScrollPosition;
@end
@interface FindInPageController : NSObject
// Designated initializer.
- (id)initWithWebState:(web::WebState*)webState
delegate:(id<FindInPageControllerDelegate>)delegate;
// Inject the find in page scripts into the web state.
- (void)initFindInPage;
// Find In Page model. TODO(justincohen) consider using find_tab_helper.cc.
- (FindInPageModel*)findInPageModel;
// Is Find In Page available right now (given the state of the WebState)?
- (BOOL)canFindInPage;
// Find |query| in page, update model with results of find. Calls
// |completionHandler| after the find operation is complete. |completionHandler|
// can be nil.
- (void)findStringInPage:(NSString*)query
completionHandler:(ProceduralBlock)completionHandler;
// Move to the next find result based on |-findInPageModel|, and scroll to
// match. Calls |completionHandler| when the next string has been found.
// |completionHandler| can be nil.
- (void)findNextStringInPageWithCompletionHandler:
(ProceduralBlock)completionHandler;
// Move to the previous find result based on |-findInPageModel|. Calls
// |completionHandler| when the previous string has been found.
// |completionHandler| can be nil.
- (void)findPreviousStringInPageWithCompletionHandler:
(ProceduralBlock)completionHandler;
// Disable find in page script and model. Calls |completionHandler| once the
// model has been disabled and cleanup is complete. |completionHandler| can be
// nil.
- (void)disableFindInPageWithCompletionHandler:
(ProceduralBlock)completionHandler;
// Save search term to Paste UIPasteboard.
- (void)saveSearchTerm;
// Restore search term from Paste UIPasteboard, updating findInPageModel.
- (void)restoreSearchTerm;
// Instructs the controller to detach itself from the web state.
- (void)detachFromWebState;
@end
#endif // IOS_CHROME_BROWSER_FIND_IN_PAGE_FIND_IN_PAGE_CONTROLLER_H_
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/find_in_page/find_in_page_controller.h"
#import <UIKit/UIKit.h>
#include "base/ios/ios_util.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_nsobject.h"
#import "ios/chrome/browser/find_in_page/find_in_page_model.h"
#import "ios/chrome/browser/find_in_page/js_findinpage_manager.h"
#import "ios/chrome/browser/web/dom_altering_lock.h"
#import "ios/web/public/web_state/crw_web_view_proxy.h"
#import "ios/web/public/web_state/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#import "ios/web/public/web_state/web_state.h"
#import "ios/web/public/web_state/web_state_observer_bridge.h"
NSString* const kFindBarTextFieldWillBecomeFirstResponderNotification =
@"kFindBarTextFieldWillBecomeFirstResponderNotification";
NSString* const kFindBarTextFieldDidResignFirstResponderNotification =
@"kFindBarTextFieldDidResignFirstResponderNotification";
namespace {
// The delay (in secs) after which the find in page string will be pumped again.
const NSTimeInterval kRecurringPumpDelay = .01;
}
@interface FindInPageController () <DOMAltering, CRWWebStateObserver>
// The find in page controller delegate.
@property(nonatomic, readonly) id<FindInPageControllerDelegate> delegate;
// The web view's scroll view.
@property(nonatomic, readonly) CRWWebViewScrollViewProxy* webViewScrollView;
// Convenience method to obtain UIPasteboardNameFind from UIPasteBoard.
- (UIPasteboard*)findPasteboard;
// Find in Page text field listeners.
- (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note;
- (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note;
// Keyboard listeners.
- (void)keyboardDidShow:(NSNotification*)note;
- (void)keyboardWillHide:(NSNotification*)note;
// Constantly injects the find string in page until
// |disableFindInPageWithCompletionHandler:| is called or the find operation is
// complete. Calls |completionHandler| if the find operation is complete.
// |completionHandler| can be nil.
- (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler;
// Gives find in page more time to complete. Calls |completionHandler| with
// a BOOL indicating if the find operation was successfull. |completionHandler|
// can be nil.
- (void)pumpFindStringInPageWithCompletionHandler:
(void (^)(BOOL))completionHandler;
// Processes the result of a single find in page pump. Calls |completionHandler|
// if pumping is done. Re-pumps if necessary.
- (void)processPumpResult:(BOOL)finished
scrollPoint:(CGPoint)scrollPoint
completionHandler:(ProceduralBlock)completionHandler;
// Returns the associated web state. May be null.
- (web::WebState*)webState;
@end
@implementation FindInPageController {
@private
// Object that manages find_in_page.js injection into the web view.
JsFindinpageManager* _findInPageJsManager;
id<FindInPageControllerDelegate> _delegate;
// Access to the web view from the web state.
base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy;
// True when a find is in progress. Used to avoid running JavaScript during
// disable when there is nothing to clear.
BOOL _findStringStarted;
// Bridge to observe the web state from Objective-C.
scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
}
@synthesize delegate = _delegate;
- (id)initWithWebState:(web::WebState*)webState
delegate:(id<FindInPageControllerDelegate>)delegate {
self = [super init];
if (self) {
DCHECK(delegate);
_findInPageJsManager = base::mac::ObjCCastStrict<JsFindinpageManager>(
[webState->GetJSInjectionReceiver()
instanceOfClass:[JsFindinpageManager class]]);
_delegate = delegate;
_webStateObserverBridge.reset(
new web::WebStateObserverBridge(webState, self));
_webViewProxy.reset([webState->GetWebViewProxy() retain]);
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(findBarTextFieldWillBecomeFirstResponder:)
name:kFindBarTextFieldWillBecomeFirstResponderNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(findBarTextFieldDidResignFirstResponder:)
name:kFindBarTextFieldDidResignFirstResponderNotification
object:nil];
DOMAlteringLock::CreateForWebState(webState);
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
- (FindInPageModel*)findInPageModel {
return [_findInPageJsManager findInPageModel];
}
- (BOOL)canFindInPage {
return [_webViewProxy hasSearchableTextContent];
}
- (void)initFindInPage {
[_findInPageJsManager inject];
// Initialize the module with our frame size.
CGRect frame = [_webViewProxy bounds];
[_findInPageJsManager setWidth:frame.size.width height:frame.size.height];
}
- (CRWWebViewScrollViewProxy*)webViewScrollView {
return [_webViewProxy scrollViewProxy];
}
- (void)processPumpResult:(BOOL)finished
scrollPoint:(CGPoint)scrollPoint
completionHandler:(ProceduralBlock)completionHandler {
if (finished) {
[_delegate willAdjustScrollPosition];
[[_webViewProxy scrollViewProxy] setContentOffset:scrollPoint animated:YES];
if (completionHandler)
completionHandler();
} else {
[self performSelector:@selector(startPumpingWithCompletionHandler:)
withObject:completionHandler
afterDelay:kRecurringPumpDelay];
}
}
- (void)findStringInPage:(NSString*)query
completionHandler:(ProceduralBlock)completionHandler {
ProceduralBlockWithBool lockAction = ^(BOOL hasLock) {
if (!hasLock) {
if (completionHandler) {
completionHandler();
}
return;
}
// Cancel any previous pumping.
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self initFindInPage];
// Keep track of whether a find is in progress so to avoid running
// JavaScript during disable if unnecessary.
_findStringStarted = YES;
base::WeakNSObject<FindInPageController> weakSelf(self);
[_findInPageJsManager findString:query
completionHandler:^(BOOL finished, CGPoint point) {
[weakSelf processPumpResult:finished
scrollPoint:point
completionHandler:completionHandler];
}];
};
DOMAlteringLock::FromWebState([self webState])->Acquire(self, lockAction);
}
- (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler {
base::WeakNSObject<FindInPageController> weakSelf(self);
id completionHandlerBlock = ^void(BOOL findFinished) {
if (findFinished) {
// Pumping complete. Nothing else to do.
if (completionHandler)
completionHandler();
return;
}
// Further pumping is required.
[weakSelf performSelector:@selector(startPumpingWithCompletionHandler:)
withObject:completionHandler
afterDelay:kRecurringPumpDelay];
};
[self pumpFindStringInPageWithCompletionHandler:completionHandlerBlock];
}
- (void)pumpFindStringInPageWithCompletionHandler:
(void (^)(BOOL))completionHandler {
base::WeakNSObject<FindInPageController> weakSelf(self);
[_findInPageJsManager pumpWithCompletionHandler:^(BOOL finished,
CGPoint point) {
base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
if (finished) {
[[strongSelf delegate] willAdjustScrollPosition];
[[strongSelf webViewScrollView] setContentOffset:point animated:YES];
}
completionHandler(finished);
}];
}
- (void)findNextStringInPageWithCompletionHandler:
(ProceduralBlock)completionHandler {
[self initFindInPage];
base::WeakNSObject<FindInPageController> weakSelf(self);
[_findInPageJsManager nextMatchWithCompletionHandler:^(CGPoint point) {
base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
[[strongSelf delegate] willAdjustScrollPosition];
CGFloat contentHeight = [strongSelf webViewScrollView].contentSize.height;
CGFloat frameHeight = [strongSelf webViewScrollView].frame.size.height;
CGFloat maxScroll = fmax(0, contentHeight - frameHeight);
if (point.y > maxScroll) {
point.y = maxScroll;
}
[[strongSelf webViewScrollView] setContentOffset:point animated:YES];
if (completionHandler)
completionHandler();
}];
}
// Highlight the previous search match, update model and scroll to match.
- (void)findPreviousStringInPageWithCompletionHandler:
(ProceduralBlock)completionHandler {
[self initFindInPage];
base::WeakNSObject<FindInPageController> weakSelf(self);
[_findInPageJsManager previousMatchWithCompletionHandler:^(CGPoint point) {
base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
[[strongSelf delegate] willAdjustScrollPosition];
[[strongSelf webViewScrollView] setContentOffset:point animated:YES];
if (completionHandler)
completionHandler();
}];
}
// Remove highlights from the page and disable the model.
- (void)disableFindInPageWithCompletionHandler:
(ProceduralBlock)completionHandler {
if (![self canFindInPage])
return;
// Cancel any queued calls to |recurringPumpWithCompletionHandler|.
[NSObject cancelPreviousPerformRequestsWithTarget:self];
base::WeakNSObject<FindInPageController> weakSelf(self);
ProceduralBlock handler = ^{
web::WebState* webState = [self webState];
if (webState)
DOMAlteringLock::FromWebState(webState)->Release(self);
if (completionHandler)
completionHandler();
};
// Only run JSFindInPageManager disable if there is a string in progress to
// avoid WKWebView crash on deallocation due to outstanding completion
// handler.
if (_findStringStarted) {
[_findInPageJsManager disableWithCompletionHandler:handler];
_findStringStarted = NO;
} else {
handler();
}
}
- (void)saveSearchTerm {
[self findPasteboard].string = [[self findInPageModel] text];
}
- (void)restoreSearchTerm {
NSString* term = [self findPasteboard].string;
[[self findInPageModel] updateQuery:(term ? term : @"") matches:0];
}
- (UIPasteboard*)findPasteboard {
return [UIPasteboard pasteboardWithName:UIPasteboardNameFind create:NO];
}
- (web::WebState*)webState {
return _webStateObserverBridge ? _webStateObserverBridge->web_state()
: nullptr;
}
#pragma mark - Notification listeners
- (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note {
// Listen to the keyboard appearance notifications.
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification
object:nil];
[defaultCenter addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note {
// Resign from the keyboard appearance notifications on the next turn of the
// runloop.
dispatch_async(dispatch_get_main_queue(), ^{
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter removeObserver:self
name:UIKeyboardDidShowNotification
object:nil];
[defaultCenter removeObserver:self
name:UIKeyboardWillHideNotification
object:nil];
});
}
- (void)keyboardDidShow:(NSNotification*)note {
NSDictionary* info = [note userInfo];
CGSize kbSize =
[[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
UIInterfaceOrientation orientation =
[[UIApplication sharedApplication] statusBarOrientation];
CGFloat kbHeight = kbSize.height;
// Prior to iOS 8, the keyboard frame was not dependent on interface
// orientation, so height and width need to be swapped in landscape mode.
if (UIInterfaceOrientationIsLandscape(orientation) &&
!base::ios::IsRunningOnIOS8OrLater()) {
kbHeight = kbSize.width;
}
UIEdgeInsets insets = UIEdgeInsetsZero;
insets.bottom = kbHeight;
[_webViewProxy registerInsets:insets forCaller:self];
}
- (void)keyboardWillHide:(NSNotification*)note {
[_webViewProxy unregisterInsetsForCaller:self];
}
- (void)detachFromWebState {
_webStateObserverBridge.reset();
}
#pragma mark - CRWWebStateObserver Methods
- (void)webStateDestroyed:(web::WebState*)webState {
[self detachFromWebState];
}
#pragma mark - DOMAltering Methods
- (BOOL)canReleaseDOMLock {
return NO;
}
- (void)releaseDOMLockWithCompletionHandler:(ProceduralBlock)completionHandler {
NOTREACHED();
}
@end
// Copyright 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_BROWSER_FIND_IN_PAGE_FIND_IN_PAGE_MODEL_H_
#define IOS_CHROME_BROWSER_FIND_IN_PAGE_FIND_IN_PAGE_MODEL_H_
#import <UIKit/UIKit.h>
#include "base/mac/scoped_nsobject.h"
// This is a simplified version of find_tab_helper.cc.
@interface FindInPageModel : NSObject {
@private
// Should find in page be displayed.
BOOL enabled_;
// The current search string.
base::scoped_nsobject<NSString> text_;
// The number of matches for |text_|
NSUInteger matches_;
// The currently higlighted index.
NSUInteger currentIndex_;
// The content offset needed to display the |currentIndex_| match.
CGPoint currentPoint_;
}
@property(nonatomic, readwrite, assign) BOOL enabled;
@property(nonatomic, readonly) NSString* text;
@property(nonatomic, readonly) NSUInteger matches;
@property(nonatomic, readonly) NSUInteger currentIndex;
@property(nonatomic, readonly) CGPoint currentPoint;
// Update the query string and the number of matches.
- (void)updateQuery:(NSString*)query matches:(NSUInteger)matches;
// Update the current match index and its found position.
- (void)updateIndex:(NSInteger)index atPoint:(CGPoint)point;
@end
#endif // IOS_CHROME_BROWSER_FIND_IN_PAGE_FIND_IN_PAGE_MODEL_H_
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/find_in_page/find_in_page_model.h"
@implementation FindInPageModel
@synthesize enabled = enabled_;
@synthesize matches = matches_;
@synthesize currentIndex = currentIndex_;
@synthesize currentPoint = currentPoint_;
- (NSString*)text {
return text_;
}
- (void)setEnabled:(BOOL)enabled {
enabled_ = enabled;
matches_ = 0;
currentIndex_ = 0;
currentPoint_ = CGPointZero;
}
- (void)updateQuery:(NSString*)query matches:(NSUInteger)matches {
if (query)
text_.reset([query copy]);
matches_ = matches;
currentIndex_ = 0;
}
- (void)updateIndex:(NSInteger)index atPoint:(CGPoint)point {
currentIndex_ = index;
currentPoint_ = point;
}
@end
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_BROWSER_FIND_IN_PAGE_JS_FINDINPAGE_MANAGER_H_
#define IOS_CHROME_BROWSER_FIND_IN_PAGE_JS_FINDINPAGE_MANAGER_H_
#include <CoreGraphics/CGBase.h>
#include <CoreGraphics/CGGeometry.h>
#include "base/ios/block_types.h"
#import "base/mac/scoped_nsobject.h"
#import "ios/web/public/web_state/js/crw_js_injection_manager.h"
// Data from find in page.
typedef struct FindInPageEntry {
CGPoint point; // Scroll offset required to center the highlighted item.
NSInteger index; // Currently higlighted search term.
} FindInPageEntry;
// Constant for "not found".
extern FindInPageEntry FindInPageEntryZero;
@class CRWJSInjectionReceiver;
@class FindInPageModel;
// Manager for the injection of the Find In Page JavaScript.
@interface JsFindinpageManager : CRWJSInjectionManager {
@private
// Model for find in page.
base::scoped_nsobject<FindInPageModel> findInPageModel_;
}
// Find In Page model. TODO(justincohen) consider using find_tab_helper.cc.
@property(nonatomic, readonly) FindInPageModel* findInPageModel;
// Sets the width and height of the window.
- (void)setWidth:(CGFloat)width height:(CGFloat)height;
// Runs injected JavaScript to find |query| string. Calls |completionHandler|
// with YES if the find operation completed, it is called with NO otherwise.
// If the find operation was successfiul the first match to scroll to is
// also called with. If the |completionHandler| is called with NO, another
// call to |pumpWithCompletionHandler:| is required. |completionHandler| cannot
// be nil.
- (void)findString:(NSString*)query
completionHandler:(void (^)(BOOL, CGPoint))completionHandler;
// Searches for more matches. Calls |completionHandler| with a success BOOL and
// scroll position if pumping was successfull. If the pumping was unsuccessfull
// another pumping call maybe required. |completionHandler| cannot be nil.
// TODO(shreyasv): Consider folding the logic for re-pumping into this class
// instead of having clients having to do it.
- (void)pumpWithCompletionHandler:(void (^)(BOOL, CGPoint))completionHandler;
// Moves to the next matched location and executes the completion handler with
// the new scroll position passed in. The |completionHandler| can be nil.
- (void)nextMatchWithCompletionHandler:(void (^)(CGPoint))completionHandler;
// Moves to the previous matched location and executes the completion handle
// with the new scroll position passed in. The |completionHandler| can be nil.
- (void)previousMatchWithCompletionHandler:(void (^)(CGPoint))completionHandler;
// Stops find in page and calls |completionHandler| once find in page is
// stopped. |completionHandler| cannot be nil.
- (void)disableWithCompletionHandler:(ProceduralBlock)completionHandler;
@end
#endif // IOS_CHROME_BROWSER_FIND_IN_PAGE_JS_FINDINPAGE_MANAGER_H_
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/find_in_page/js_findinpage_manager.h"
#include <string>
#import "base/ios/weak_nsobject.h"
#include "base/json/json_reader.h"
#include "base/json/string_escape.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/strings/sys_string_conversions.h"
#include "base/values.h"
#import "ios/chrome/browser/find_in_page/find_in_page_model.h"
#import "ios/web/public/web_state/js/crw_js_early_script_manager.h"
namespace {
// Global variable defined in find_in_page.js that can be used for testing
// whether JavaScript bas heen loaded.
NSString* const kFindInPageBeacon = @"window.__gCrWeb.findInPage";
// Initializes Find In Page JavaScript with the width and height of the window.
NSString* const kFindInPageInit = @"window.__gCrWeb.findInPage && "
"window.__gCrWeb.findInPage.init(%.f, %.f);";
// This will only do verbatim matches.
// The timeout of 100ms is hardcoded into this string so we don't have
// to spend any time at runtime to format this constant into another constant.
NSString* const kFindInPageVerbatim =
@"window.__gCrWeb.findInPage && "
"window.__gCrWeb.findInPage.highlightWord(%@, false, 100.0);";
// The timeout of 100ms is hardcoded into this string so we don't have
// to spend any time at runtime to format this constant into another constant.
NSString* const kFindInPagePump =
@"window.__gCrWeb.findInPage && "
"window.__gCrWeb.findInPage.pumpSearch(100.0);";
NSString* const kFindInPagePrev = @"window.__gCrWeb.findInPage && "
"window.__gCrWeb.findInPage.goPrev();";
NSString* const kFindInPageNext = @"window.__gCrWeb.findInPage && "
"window.__gCrWeb.findInPage.goNext();";
NSString* const kFindInPageDisable = @"window.__gCrWeb.findInPage && "
"window.__gCrWeb.findInPage.disable();";
NSString* const kFindInPagePending = @"[false]";
const FindInPageEntry kFindInPageEntryZero = {{0.0, 0.0}, 0};
} // namespace
@interface JsFindinpageManager ()
// Update find in page model with results, return true if fip completes or
// false if still pending and requires pumping. If |point| is not nil, it will
// contain the scroll position upon return.
- (BOOL)processFindInPageResult:(NSString*)result
scrollPosition:(CGPoint*)point;
// Updates find in page model with results. Calls |completionHandler| with the
// the result of the processing and the new scroll position if successfull. If
// |completionHandler| is called with NO, further pumping is required.
// |completionHandler| cannot be nil.
- (void)processFindInPagePumpResult:(NSString*)result
completionHandler:(void (^)(BOOL, CGPoint))completionHandler;
// Helper functions to extract FindInPageEntry from JSON.
- (FindInPageEntry)findInPageEntryForJson:(NSString*)jsonStr;
- (FindInPageEntry)entryForListValue:(base::ListValue*)position;
// Executes |script| which is a piece of JavaScript to move to the next or
// previous element in the page and executes |completionHandler| after moving
// with the new scroll position passed in.
- (void)moveHighlightByEvaluatingJavaScript:(NSString*)script
completionHandler:
(void (^)(CGPoint))completionHandler;
// Updates the current match index and its found position in the model.
- (void)updateIndex:(NSInteger)index atPoint:(CGPoint)point;
@end
@implementation JsFindinpageManager
- (FindInPageModel*)findInPageModel {
if (!findInPageModel_)
findInPageModel_.reset([[FindInPageModel alloc] init]);
return findInPageModel_.get();
}
- (void)setWidth:(CGFloat)width height:(CGFloat)height {
NSString* javaScript =
[NSString stringWithFormat:kFindInPageInit, width, height];
[self evaluate:javaScript stringResultHandler:nil];
}
- (void)findString:(NSString*)query
completionHandler:(void (^)(BOOL, CGPoint))completionHandler {
DCHECK(completionHandler);
// Save the query in the model before searching.
[findInPageModel_ updateQuery:query matches:0];
// Escape |query| before passing to js.
std::string escapedJson;
base::EscapeJSONString(base::SysNSStringToUTF16(query), true, &escapedJson);
NSString* jsonQuery =
[NSString stringWithFormat:kFindInPageVerbatim,
base::SysUTF8ToNSString(escapedJson.c_str())];
base::WeakNSObject<JsFindinpageManager> weakSelf(self);
[self evaluate:jsonQuery
stringResultHandler:^(NSString* result, NSError* error) {
[weakSelf processFindInPagePumpResult:result
completionHandler:completionHandler];
}];
}
- (void)pumpWithCompletionHandler:(void (^)(BOOL, CGPoint))completionHandler {
DCHECK(completionHandler);
base::WeakNSObject<JsFindinpageManager> weakSelf(self);
[self evaluate:kFindInPagePump
stringResultHandler:^(NSString* result, NSError* error) {
// TODO(shreyasv): What to do here if this returns an NSError in the
// WKWebView version.
[weakSelf processFindInPagePumpResult:result
completionHandler:completionHandler];
}];
}
- (void)nextMatchWithCompletionHandler:(void (^)(CGPoint))completionHandler {
[self moveHighlightByEvaluatingJavaScript:kFindInPageNext
completionHandler:completionHandler];
}
- (void)previousMatchWithCompletionHandler:
(void (^)(CGPoint))completionHandler {
[self moveHighlightByEvaluatingJavaScript:kFindInPagePrev
completionHandler:completionHandler];
}
- (void)moveHighlightByEvaluatingJavaScript:(NSString*)script
completionHandler:
(void (^)(CGPoint))completionHandler {
base::WeakNSObject<JsFindinpageManager> weakSelf(self);
[self evaluate:script
stringResultHandler:^(NSString* result, NSError* error) {
base::WeakNSObject<JsFindinpageManager> strongSelf([weakSelf retain]);
if (!strongSelf)
return;
DCHECK(!error);
FindInPageEntry entry = kFindInPageEntryZero;
if (![result isEqualToString:kFindInPagePending])
entry = [strongSelf findInPageEntryForJson:result];
CGPoint newPoint = entry.point;
[strongSelf updateIndex:entry.index atPoint:newPoint];
if (completionHandler)
completionHandler(newPoint);
}];
}
- (void)disableWithCompletionHandler:(ProceduralBlock)completionHandler {
DCHECK(completionHandler);
base::WeakNSObject<FindInPageModel> weakFindInPageModel(findInPageModel_);
[self evaluate:kFindInPageDisable
stringResultHandler:^(NSString* result, NSError* error) {
[weakFindInPageModel setEnabled:NO];
completionHandler();
}];
}
#pragma mark -
#pragma mark FindInPageEntry
- (BOOL)processFindInPageResult:(NSString*)result
scrollPosition:(CGPoint*)point {
if (!result)
return NO;
// Parse JSONs.
std::string json([result UTF8String]);
scoped_ptr<base::Value> root(base::JSONReader::Read(json, false));
if (!root.get())
return YES;
if (!root->IsType(base::Value::TYPE_LIST))
return YES;
base::ListValue* resultList = static_cast<base::ListValue*>(root.get());
DCHECK(resultList);
if (resultList) {
if (resultList->GetSize() == 2) {
int numHighlighted = 0;
if (resultList->GetInteger(0, &numHighlighted)) {
if (numHighlighted > 0) {
base::ListValue* position;
if (resultList->GetList(1, &position)) {
[findInPageModel_ updateQuery:nil matches:numHighlighted];
// Scroll to first match.
FindInPageEntry entry = [self entryForListValue:position];
[findInPageModel_ updateIndex:entry.index atPoint:entry.point];
if (point)
*point = entry.point;
}
}
}
}
}
return YES;
}
- (void)processFindInPagePumpResult:(NSString*)result
completionHandler:(void (^)(BOOL, CGPoint))completionHandler {
CGPoint point = CGPointZero;
if ([result isEqualToString:kFindInPagePending]) {
completionHandler(NO, point);
}
// TODO(shreyasv): Inline this call from the logic from the above function
// and remove the above function.
BOOL processFIPResult =
[self processFindInPageResult:result scrollPosition:&point];
completionHandler(processFIPResult, point);
}
- (void)updateIndex:(NSInteger)index atPoint:(CGPoint)point {
[findInPageModel_ updateIndex:index atPoint:point];
}
- (FindInPageEntry)findInPageEntryForJson:(NSString*)jsonStr {
std::string json([jsonStr UTF8String]);
scoped_ptr<base::Value> root(base::JSONReader::Read(json, false));
if (!root.get())
return kFindInPageEntryZero;
if (!root->IsType(base::Value::TYPE_LIST))
return kFindInPageEntryZero;
base::ListValue* position = static_cast<base::ListValue*>(root.get());
return [self entryForListValue:position];
}
- (FindInPageEntry)entryForListValue:(base::ListValue*)position {
if (!position)
return kFindInPageEntryZero;
// Position should always be of length 3, from [index,x,y].
DCHECK(position->GetSize() == 3);
if (position->GetSize() != 3)
return kFindInPageEntryZero;
// The array position comes from the JSON string [index, x, y], which
// represents the index of the currently found string, and the x and y
// position necessary to center that string. Pull out that data into a
// FindInPageEntry struct.
int index;
double x = 0, y = 0;
position->GetInteger(0, &index);
position->GetDouble(1, &x);
position->GetDouble(2, &y);
FindInPageEntry entry;
entry.index = index;
entry.point.x = x;
entry.point.y = y;
return entry;
}
#pragma mark -
#pragma mark ProtectedMethods
- (NSString*)scriptPath {
return @"find_in_page";
}
- (NSString*)presenceBeacon {
return kFindInPageBeacon;
}
- (NSArray*)directDependencies {
return @[ [CRWJSEarlyScriptManager class] ];
}
@end
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* This file is lifted from the GoogleMobile find tool.
*
* @fileoverview A find in page tool. It scans the DOM for elements with the
* text being search for, and wraps them with a span that highlights them.
*
* @author bmcmahan@google.com (Benjamin McMahan)
*
*/
/**
* Namespace for this file. Depends on __gCrWeb having already been injected.
*/
__gCrWeb['findInPage'] = {};
/**
* Index of the current highlighted choice. -1 means none.
* @type {number}
*/
__gCrWeb['findInPage']['index'] = -1;
/**
* The list of found searches in span form.
* @type {Array.<Element>}
*/
__gCrWeb['findInPage']['spans'] = [];
/**
* The list of frame documents.
* TODO(justincohen): x-domain frames won't work.
* @type {Array.<Document>}
*/
__gCrWeb['findInPage'].frameDocs = [];
/**
* Associate array to stash element styles while the element is highlighted.
* @type {Object.<Element,Object<string,string>>}
*/
__gCrWeb['findInPage'].savedElementStyles = {};
/**
* The style DOM element that we add.
* @type {Element}
*/
__gCrWeb['findInPage'].style = null;
/**
* Width we expect the page to be. For example (320/480) for iphone,
* (1024/768) for ipad.
* @type {number}
*/
__gCrWeb['findInPage'].pageWidth = 320;
/**
* Height we expect the page to be.
* @type {number}
*/
__gCrWeb['findInPage'].pageHeight = 480;
/**
* Maximum number of visible elements to count
* @type {number}
*/
__gCrWeb['findInPage'].maxVisibleElements = 100;
/**
* A search is in progress.
* @type {boolean}
*/
__gCrWeb['findInPage'].searchInProgress = false;
/**
* Node names that are not going to be processed.
* @type {Object}
*/
__gCrWeb['findInPage'].ignoreNodeNames = {
'SCRIPT': 1,
'STYLE': 1,
'EMBED': 1,
'OBJECT': 1
};
/**
* Class name of CSS element.
* @type {string}
*/
__gCrWeb['findInPage'].CSS_CLASS_NAME = 'find_in_page';
/**
* ID of CSS style.
* @type {string}
*/
__gCrWeb['findInPage'].CSS_STYLE_ID = '__gCrWeb.findInPageStyle';
/**
* Result passed back to app to indicate no results for the query.
* @type {string}
*/
__gCrWeb['findInPage'].NO_RESULTS = '[0,[0,0,0]]';
/**
* Regex to escape regex special characters in a string.
* @type {RegExp}
*/
__gCrWeb['findInPage'].REGEX_ESCAPER = /([.?*+^$[\]\\(){}|-])/g;
__gCrWeb['findInPage'].getCurrentSpan = function() {
return __gCrWeb['findInPage']['spans'][__gCrWeb['findInPage']['index']];
};
/**
* Creates the regex needed to find the text.
* @param {string} findText Phrase to look for.
* @param {boolean} opt_split True to split up the phrase.
* @return {RegExp} regex needed to find the text.
*/
__gCrWeb['findInPage'].getRegex = function(findText, opt_split) {
var regexString = '';
if (opt_split) {
var words = [];
var split = findText.split(' ');
for (var i = 0; i < split.length; i++) {
words.push(__gCrWeb['findInPage'].escapeRegex(split[i]));
}
var joinedWords = words.join('|');
regexString = '(' +
// Match at least one word.
'\\b(?:' + joinedWords + ')' +
// Include zero or more additional words separated by whitespace.
'(?:\\s*\\b(?:' + joinedWords + '))*' +
')';
} else {
regexString = '(' + __gCrWeb['findInPage'].escapeRegex(findText) + ')';
}
return new RegExp(regexString, 'ig');
};
/**
* Get current timestamp.
* @return {number} timestamp.
*/
__gCrWeb['findInPage'].time = function() {
return (new Date).getTime();
};
/**
* After |timeCheck| iterations, return true if |now| - |start| is greater than
* |timeout|.
* @return {boolean} Find in page needs to return.
*/
__gCrWeb['findInPage'].overTime = function() {
return (__gCrWeb['findInPage'].time() - __gCrWeb['findInPage'].startTime >
__gCrWeb['findInPage'].timeout);
};
/**
* Looks for a phrase in the DOM.
* @param {string} findText Phrase to look for like "ben franklin".
* @param {boolean} opt_split True to split up the words and look for any
* of them. False to require the full phrase to be there.
* Undefined will try the full phrase, and if nothing is found do the split.
* @param {number} timeout Maximum time to run.
* @return {number} How many results there are in the page.
*/
__gCrWeb['findInPage']['highlightWord'] =
function(findText, opt_split, timeout) {
if (__gCrWeb['findInPage']['spans'] &&
__gCrWeb['findInPage']['spans'].length) {
// Clean up a previous run.
__gCrWeb['findInPage']['clearHighlight']();
}
if (!findText || !findText.replace(/\u00a0|\s/g, '')) {
// No searching for emptyness.
return __gCrWeb['findInPage'].NO_RESULTS;
}
// Store all DOM modifications to do them in a tight loop at once.
__gCrWeb['findInPage'].replacements = [];
// Node is what we are currently looking at.
__gCrWeb['findInPage'].node = document.body;
// Holds what nodes we have not processed yet.
__gCrWeb['findInPage'].stack = [];
// Push frames into stack too.
for (var i = __gCrWeb['findInPage'].frameDocs.length - 1; i >= 0; i--) {
var doc = __gCrWeb['findInPage'].frameDocs[i];
__gCrWeb['findInPage'].stack.push(doc);
}
// Number of visible elements found.
__gCrWeb['findInPage'].visibleFound = 0;
// Index tracking variables so search can be broken up into multiple calls.
__gCrWeb['findInPage'].visibleIndex = 0;
__gCrWeb['findInPage'].replacementsIndex = 0;
__gCrWeb['findInPage'].replacementNewNodesIndex = 0;
__gCrWeb['findInPage'].regex =
__gCrWeb['findInPage'].getRegex(findText, opt_split);
__gCrWeb['findInPage'].searchInProgress = true;
return __gCrWeb['findInPage']['pumpSearch'](timeout);
};
/**
* Break up find in page DOM regex, DOM manipulation and visibility check
* into sections that can be stopped and restarted later. Because the js runs
* in the main UI thread, anything over timeout will cause the UI to lock up.
* @param {number} timeout Only run find in page until timeout.
* @return {boolean} Whether find in page completed.
*/
__gCrWeb['findInPage']['pumpSearch'] = function(timeout) {
var opt_split = false;
// TODO(justincohen): It would be better if this DCHECKed.
if (__gCrWeb['findInPage'].searchInProgress == false)
return __gCrWeb['findInPage'].NO_RESULTS;
__gCrWeb['findInPage'].timeout = timeout;
__gCrWeb['findInPage'].startTime = __gCrWeb['findInPage'].time();
var regex = __gCrWeb['findInPage'].regex;
// Go through every node in DFS fashion.
while (__gCrWeb['findInPage'].node) {
var node = __gCrWeb['findInPage'].node;
var children = node.childNodes;
if (children && children.length) {
// add all (reasonable) children
for (var i = children.length - 1; i >= 0; --i) {
var child = children[i];
if ((child.nodeType == 1 || child.nodeType == 3) &&
!__gCrWeb['findInPage'].ignoreNodeNames[child.nodeName]) {
__gCrWeb['findInPage'].stack.push(children[i]);
}
}
}
if (node.nodeType == 3 && node.parentNode) {
var strIndex = 0;
var nodes = [];
var match;
while (match = regex.exec(node.textContent)) {
try {
var matchText = match[0];
// If there is content before this match, add it to a new text node.
if (match.index > 0) {
var nodeSubstr = node.textContent.substring(strIndex,
match.index);
nodes.push(node.ownerDocument.createTextNode(nodeSubstr));
}
// Now create our matched element.
var element = node.ownerDocument.createElement('chrome_find');
element.setAttribute('class', __gCrWeb['findInPage'].CSS_CLASS_NAME);
element.innerHTML = __gCrWeb['findInPage'].escapeHTML(matchText);
nodes.push(element);
strIndex = match.index + matchText.length;
} catch (e) {
// Do nothing.
}
}
if (nodes.length) {
// Add any text after our matches to a new text node.
if (strIndex < node.textContent.length) {
var substr = node.textContent.substring(strIndex,
node.textContent.length);
nodes.push(node.ownerDocument.createTextNode(substr));
}
__gCrWeb['findInPage'].replacements.push(
{oldNode: node, newNodes: nodes});
regex.lastIndex = 0;
}
}
if (__gCrWeb['findInPage'].overTime())
return '[false]';
if (__gCrWeb['findInPage'].stack.length > 0) {
__gCrWeb['findInPage'].node = __gCrWeb['findInPage'].stack.pop();
} else {
__gCrWeb['findInPage'].node = null;
}
}
// Insert each of the replacement nodes into the old node's parent, then
// remove the old node.
var replacements = __gCrWeb['findInPage'].replacements;
// Last position in replacements array.
var rIndex = __gCrWeb['findInPage'].replacementsIndex;
var rMax = replacements.length;
for (; rIndex < rMax; rIndex++) {
var replacement = replacements[rIndex];
var parent = replacement.oldNode.parentNode;
if (parent == null)
continue;
var rNodesMax = replacement.newNodes.length;
for (var rNodesIndex = __gCrWeb['findInPage'].replacementNewNodesIndex;
rNodesIndex < rNodesMax; rNodesIndex++) {
if (__gCrWeb['findInPage'].overTime()) {
__gCrWeb['findInPage'].replacementsIndex = rIndex;
__gCrWeb['findInPage'].replacementNewNodesIndex = rNodesIndex;
return __gCrWeb.stringify([false]);
}
parent.insertBefore(replacement.newNodes[rNodesIndex],
replacement.oldNode);
}
parent.removeChild(replacement.oldNode);
__gCrWeb['findInPage'].replacementNewNodesIndex = 0;
}
// Save last position in replacements array.
__gCrWeb['findInPage'].replacementsIndex = rIndex;
__gCrWeb['findInPage']['spans'] =
__gCrWeb['findInPage'].getAllElementsByClassName(
__gCrWeb['findInPage'].CSS_CLASS_NAME);
// Count visible elements.
var max = __gCrWeb['findInPage']['spans'].length;
var maxVisible = __gCrWeb['findInPage'].maxVisibleElements;
for (var index = __gCrWeb['findInPage'].visibleIndex; index < max; index++) {
var elem = __gCrWeb['findInPage']['spans'][index];
if (__gCrWeb['findInPage'].overTime()) {
__gCrWeb['findInPage'].visibleIndex = index;
return __gCrWeb.stringify([false]);
}
// Stop after |maxVisible| elements.
if (__gCrWeb['findInPage'].visibleFound > maxVisible) {
__gCrWeb['findInPage']['spans'][index].visibleIndex = maxVisible;
continue;
}
if (__gCrWeb['findInPage'].isVisible(elem)) {
__gCrWeb['findInPage'].visibleFound++;
__gCrWeb['findInPage']['spans'][index].visibleIndex =
__gCrWeb['findInPage'].visibleFound;
}
}
__gCrWeb['findInPage'].searchInProgress = false;
var pos;
// Try again flow.
// If opt_split is true, we are done since we won't do any better.
// If opt_split is false, they were explicit about wanting the full thing
// so do not try with a split.
// If opt_split is undefined and we did not find an answer, go ahead and try
// splitting the terms.
if (__gCrWeb['findInPage']['spans'].length == 0 && opt_split === undefined) {
// Try to be more aggressive:
return __gCrWeb['findInPage']['highlightWord'](findText, true);
} else {
pos = __gCrWeb['findInPage']['goNext']();
if (pos) {
return '[' + __gCrWeb['findInPage'].visibleFound + ',' + pos + ']';
} else if (opt_split === undefined) {
// Nothing visible, go ahead and be more aggressive.
return __gCrWeb['findInPage']['highlightWord'](findText, true);
} else {
return __gCrWeb['findInPage'].NO_RESULTS;
}
}
};
/**
* Converts a node list to an array.
* @param {object} nodeList DOM node list.
* @return {object} array.
*/
__gCrWeb['findInPage'].toArray = function(nodeList) {
var array = [];
for (var i = 0; i < nodeList.length; i++)
array[i] = nodeList[i];
return array;
};
/**
* Return all elements of class name, spread out over various frames.
* @param {string} name of class.
* @return {object} array of elements matching class name.
*/
__gCrWeb['findInPage'].getAllElementsByClassName = function(name) {
var nodeList = document.getElementsByClassName(name);
var elements = __gCrWeb['findInPage'].toArray(nodeList);
for (var i = __gCrWeb['findInPage'].frameDocs.length - 1; i >= 0; i--) {
var doc = __gCrWeb['findInPage'].frameDocs[i];
nodeList = doc.getElementsByClassName(name);
elements = elements.concat(__gCrWeb['findInPage'].toArray(nodeList));
}
return elements;
};
/**
* Removes all currently highlighted spans.
* Note: It does not restore previous state, just removes the class name.
*/
__gCrWeb['findInPage']['clearHighlight'] = function() {
if (__gCrWeb['findInPage']['index'] >= 0) {
__gCrWeb['findInPage'].removeSelectHighlight(
__gCrWeb['findInPage'].getCurrentSpan());
}
// Store all DOM modifications to do them in a tight loop.
var modifications = [];
var length = __gCrWeb['findInPage']['spans'].length;
var prevParent = null;
for (var i = length - 1; i >= 0; i--) {
var elem = __gCrWeb['findInPage']['spans'][i];
var parentNode = elem.parentNode;
// Safari has an occasional |elem.innerText| bug that drops the trailing
// space. |elem.innerText| would be more correct in this situation, but
// since we only allow text in this element, grabbing the HTML value should
// not matter.
var nodeText = elem.innerHTML;
// If this element has the same parent as the previous, check if we should
// add this node to the previous one.
if (prevParent && prevParent.isSameNode(parentNode) &&
elem.nextSibling.isSameNode(
__gCrWeb['findInPage']['spans'][i + 1].previousSibling)) {
var prevMod = modifications[modifications.length - 1];
prevMod.nodesToRemove.push(elem);
var elemText = elem.innerText;
if (elem.previousSibling) {
prevMod.nodesToRemove.push(elem.previousSibling);
elemText = elem.previousSibling.textContent + elemText;
}
prevMod.replacement.textContent =
elemText + prevMod.replacement.textContent;
}
else { // Element isn't attached to previous, so create a new modification.
var nodesToRemove = [elem];
if (elem.previousSibling && elem.previousSibling.nodeType == 3) {
nodesToRemove.push(elem.previousSibling);
nodeText = elem.previousSibling.textContent + nodeText;
}
if (elem.nextSibling && elem.nextSibling.nodeType == 3) {
nodesToRemove.push(elem.nextSibling);
nodeText = nodeText + elem.nextSibling.textContent;
}
var textNode = elem.ownerDocument.createTextNode(nodeText);
modifications.push({nodesToRemove: nodesToRemove, replacement: textNode});
}
prevParent = parentNode;
}
var numMods = modifications.length;
for (i = numMods - 1; i >= 0; i--) {
var mod = modifications[i];
for (var j = 0; j < mod.nodesToRemove.length; j++) {
var existing = mod.nodesToRemove[j];
if (j == 0) {
existing.parentNode.replaceChild(mod.replacement, existing);
} else {
existing.parentNode.removeChild(existing);
}
}
}
__gCrWeb['findInPage']['spans'] = [];
__gCrWeb['findInPage']['index'] = -1;
};
/**
* Increments the index of the current highlighted span or, if the index is
* already at the end, sets it to the index of the first span in the page.
*/
__gCrWeb['findInPage']['incrementIndex'] = function() {
if (__gCrWeb['findInPage']['index'] >=
__gCrWeb['findInPage']['spans'].length - 1) {
__gCrWeb['findInPage']['index'] = 0;
} else {
__gCrWeb['findInPage']['index']++;
}
};
/**
* Switches to the next result, animating a little highlight in the process.
* @return {string} JSON encoded array of coordinates to scroll to, or blank if
* nothing happened.
*/
__gCrWeb['findInPage']['goNext'] = function() {
if (!__gCrWeb['findInPage']['spans'] ||
__gCrWeb['findInPage']['spans'].length == 0) {
return '';
}
if (__gCrWeb['findInPage']['index'] >= 0) {
// Remove previous highlight.
__gCrWeb['findInPage'].removeSelectHighlight(
__gCrWeb['findInPage'].getCurrentSpan());
}
// Iterate through to the next index, but because they might not be visible,
// keep trying until you find one that is. Make sure we don't loop forever by
// stopping on what we are currently highlighting.
var oldIndex = __gCrWeb['findInPage']['index'];
__gCrWeb['findInPage']['incrementIndex']();
while (!__gCrWeb['findInPage'].isVisible(
__gCrWeb['findInPage'].getCurrentSpan())) {
if (oldIndex === __gCrWeb['findInPage']['index']) {
// Checked all spans but didn't find anything else visible.
return '';
}
__gCrWeb['findInPage']['incrementIndex']();
if (0 === __gCrWeb['findInPage']['index'] && oldIndex < 0) {
// Didn't find anything visible and haven't highlighted anything yet.
return '';
}
}
// Return scroll dimensions.
return __gCrWeb['findInPage'].findScrollDimensions();
};
/**
* Decrements the index of the current highlighted span or, if the index is
* already at the beginning, sets it to the index of the last span in the page.
*/
__gCrWeb['findInPage']['decrementIndex'] = function() {
if (__gCrWeb['findInPage']['index'] <= 0) {
__gCrWeb['findInPage']['index'] =
__gCrWeb['findInPage']['spans'].length - 1;
} else {
__gCrWeb['findInPage']['index']--;
}
};
/**
* Switches to the previous result, animating a little highlight in the process.
* @return {string} JSON encoded array of coordinates to scroll to, or blank if
* nothing happened.
*/
__gCrWeb['findInPage']['goPrev'] = function() {
if (!__gCrWeb['findInPage']['spans'] ||
__gCrWeb['findInPage']['spans'].length == 0) {
return '';
}
if (__gCrWeb['findInPage']['index'] >= 0) {
// Remove previous highlight.
__gCrWeb['findInPage'].removeSelectHighlight(
__gCrWeb['findInPage'].getCurrentSpan());
}
// Iterate through to the next index, but because they might not be visible,
// keep trying until you find one that is. Make sure we don't loop forever by
// stopping on what we are currently highlighting.
var old = __gCrWeb['findInPage']['index'];
__gCrWeb['findInPage']['decrementIndex']();
while (!__gCrWeb['findInPage'].isVisible(
__gCrWeb['findInPage'].getCurrentSpan())) {
__gCrWeb['findInPage']['decrementIndex']();
if (old == __gCrWeb['findInPage']['index']) {
// Checked all spans but didn't find anything.
return '';
}
}
// Return scroll dimensions.
return __gCrWeb['findInPage'].findScrollDimensions();
};
/**
* Adds the special highlighting to the result at the index.
* @param {number} opt_index Index to replace __gCrWeb['findInPage']['index']
* with.
*/
__gCrWeb['findInPage'].addHighlightToIndex = function(opt_index) {
if (opt_index !== undefined) {
__gCrWeb['findInPage']['index'] = opt_index;
}
__gCrWeb['findInPage'].addSelectHighlight(
__gCrWeb['findInPage'].getCurrentSpan());
};
/**
* Updates the elements style, while saving the old style in
* __gCrWeb['findInPage'].savedElementStyles.
* @param {Element} element Element to update.
* @param {string} style Name of the style to update.
* @param {string} value New style value.
*/
__gCrWeb['findInPage'].updateElementStyle = function(element, style, value) {
if (!__gCrWeb['findInPage'].savedElementStyles[element]) {
__gCrWeb['findInPage'].savedElementStyles[element] = {};
}
// We need to keep the original style setting for this element, so if we've
// already saved a value for this style don't update it.
if (!__gCrWeb['findInPage'].savedElementStyles[element][style]) {
__gCrWeb['findInPage'].savedElementStyles[element][style] =
element.style[style];
}
element.style[style] = value;
};
/**
* Adds selected highlight style to the specified element.
* @param {Element} element Element to highlight.
*/
__gCrWeb['findInPage'].addSelectHighlight = function(element) {
element.className = (element.className || '') + ' findysel';
};
/**
* Removes selected highlight style from the specified element.
* @param {Element} element Element to remove highlighting from.
*/
__gCrWeb['findInPage'].removeSelectHighlight = function(element) {
element.className = (element.className || '').replace(/\sfindysel/g, '');
// Restore any styles we may have saved when adding the select highlighting.
var savedStyles = __gCrWeb['findInPage'].savedElementStyles[element];
if (savedStyles) {
for (var style in savedStyles) {
element.style[style] = savedStyles[style];
}
delete __gCrWeb['findInPage'].savedElementStyles[element];
}
};
/**
* Normalize coordinates according to the current document dimensions. Don't go
* too far off the screen in either direction. Try to center if possible.
* @param {Element} elem Element to find normalized coordinates for.
* @return {Array.<number>} Normalized coordinates.
*/
__gCrWeb['findInPage'].getNormalizedCoordinates = function(elem) {
var fip = __gCrWeb['findInPage'];
var pos = fip.findAbsolutePosition(elem);
var maxX =
Math.max(fip.getBodyWidth(), pos[0] + elem.offsetWidth);
var maxY =
Math.max(fip.getBodyHeight(), pos[1] + elem.offsetHeight);
// Don't go too far off the screen in either direction. Try to center if
// possible.
var xPos = Math.max(0,
Math.min(maxX - window.innerWidth,
pos[0] - (window.innerWidth / 2)));
var yPos = Math.max(0,
Math.min(maxY - window.innerHeight,
pos[1] - (window.innerHeight / 2)));
return [xPos, yPos];
};
/**
* Scale coordinates according to the width of the screen, in case the screen
* is zoomed out.
* @param {Array.<number>} coordinates Coordinates to scale.
* @return {Array.<number>} Scaled coordinates.
*/
__gCrWeb['findInPage'].scaleCoordinates = function(coordinates) {
var scaleFactor = __gCrWeb['findInPage'].pageWidth / window.innerWidth;
return [coordinates[0] * scaleFactor, coordinates[1] * scaleFactor];
};
/**
* Finds the position of the result and scrolls to it.
* @param {number} opt_index Index to replace __gCrWeb['findInPage']['index']
* with.
* @return {string} JSON encoded array of the scroll coordinates "[x, y]".
*/
__gCrWeb['findInPage'].findScrollDimensions = function(opt_index) {
if (opt_index !== undefined) {
__gCrWeb['findInPage']['index'] = opt_index;
}
var elem = __gCrWeb['findInPage'].getCurrentSpan();
if (!elem) {
return '';
}
var normalized = __gCrWeb['findInPage'].getNormalizedCoordinates(elem);
var xPos = normalized[0];
var yPos = normalized[1];
// Perform the scroll.
//window.scrollTo(xPos, yPos);
if (xPos < window.pageXOffset ||
xPos >= (window.pageXOffset + window.innerWidth) ||
yPos < window.pageYOffset ||
yPos >= (window.pageYOffset + window.innerHeight)) {
// If it's off the screen. Wait a bit to start the highlight animation so
// that scrolling can get there first.
window.setTimeout(__gCrWeb['findInPage'].addHighlightToIndex, 250);
} else {
__gCrWeb['findInPage'].addHighlightToIndex();
}
var scaled = __gCrWeb['findInPage'].scaleCoordinates(normalized);
var index = __gCrWeb['findInPage'].getCurrentSpan().visibleIndex;
scaled.unshift(index);
return __gCrWeb.stringify(scaled);
};
/**
* Initialize the __gCrWeb['findInPage'] module.
* @param {number} width Width of page.
* @param {number} height Height of page.
*/
__gCrWeb['findInPage']['init'] = function(width, height) {
if (__gCrWeb['findInPage'].hasInitialized) {
return;
}
__gCrWeb['findInPage'].pageWidth = width;
__gCrWeb['findInPage'].pageHeight = height;
__gCrWeb['findInPage'].frameDocs = __gCrWeb['findInPage'].frameDocuments();
__gCrWeb['findInPage'].enable();
__gCrWeb['findInPage'].hasInitialized = true;
};
/**
* When the GSA app detects a zoom change, we need to update our css.
* @param {number} width Width of page.
* @param {number} height Height of page.
*/
__gCrWeb['findInPage']['fixZoom'] = function(width, height) {
__gCrWeb['findInPage'].pageWidth = width;
__gCrWeb['findInPage'].pageHeight = height;
if (__gCrWeb['findInPage'].style) {
__gCrWeb['findInPage'].removeStyle();
__gCrWeb['findInPage'].addStyle();
}
};
/**
* Enable the __gCrWeb['findInPage'] module.
* Mainly just adds the style for the classes.
*/
__gCrWeb['findInPage'].enable = function() {
if (__gCrWeb['findInPage'].style) {
// Already enabled.
return;
}
__gCrWeb['findInPage'].addStyle();
};
/**
* Gets the scale ratio between the application window and the web document.
* @return {number} Scale.
*/
__gCrWeb['findInPage'].getPageScale = function() {
return (__gCrWeb['findInPage'].pageWidth /
__gCrWeb['findInPage'].getBodyWidth());
};
/**
* Maximum padding added to a highlighted item when selected.
* @type {number}
*/
__gCrWeb['findInPage'].MAX_HIGHLIGHT_PADDING = 10;
/**
* Adds the appropriate style element to the page.
*/
__gCrWeb['findInPage'].addStyle = function() {
__gCrWeb['findInPage'].addDocumentStyle(document);
for (var i = __gCrWeb['findInPage'].frameDocs.length - 1; i >= 0; i--) {
var doc = __gCrWeb['findInPage'].frameDocs[i];
__gCrWeb['findInPage'].addDocumentStyle(doc);
}
};
__gCrWeb['findInPage'].addDocumentStyle = function(thisDocument) {
var styleContent = [];
function addCSSRule(name, style) {
styleContent.push(name, '{', style, '}');
};
var scale = __gCrWeb['findInPage'].getPageScale();
var zoom = (1.0 / scale);
var left = ((1 - scale) / 2 * 100);
addCSSRule('.' + __gCrWeb['findInPage'].CSS_CLASS_NAME,
'background-color:#ffff00 !important;' +
'padding:0px;margin:0px;' +
'overflow:visible !important;');
addCSSRule('.findysel',
'background-color:#ff9632 !important;' +
'padding:0px;margin:0px;' +
'overflow:visible !important;');
__gCrWeb['findInPage'].style = thisDocument.createElement('style');
__gCrWeb['findInPage'].style.id = __gCrWeb['findInPage'].CSS_STYLE_ID;
__gCrWeb['findInPage'].style.setAttribute('type', 'text/css');
__gCrWeb['findInPage'].style.appendChild(
thisDocument.createTextNode(styleContent.join('')));
thisDocument.body.appendChild(__gCrWeb['findInPage'].style);
};
/**
* Removes the style element from the page.
*/
__gCrWeb['findInPage'].removeStyle = function() {
if (__gCrWeb['findInPage'].style) {
__gCrWeb['findInPage'].removeDocumentStyle(document);
for (var i = __gCrWeb['findInPage'].frameDocs.length - 1; i >= 0; i--) {
var doc = __gCrWeb['findInPage'].frameDocs[i];
__gCrWeb['findInPage'].removeDocumentStyle(doc);
}
__gCrWeb['findInPage'].style = null;
}
};
__gCrWeb['findInPage'].removeDocumentStyle = function(thisDocument) {
var style = thisDocument.getElementById(__gCrWeb['findInPage'].CSS_STYLE_ID);
thisDocument.body.removeChild(style);
};
/**
* Disables the __gCrWeb['findInPage'] module.
* Basically just removes the style and class names.
*/
__gCrWeb['findInPage']['disable'] = function() {
if (__gCrWeb['findInPage'].style) {
__gCrWeb['findInPage'].removeStyle();
window.setTimeout(__gCrWeb['findInPage']['clearHighlight'], 0);
}
__gCrWeb['findInPage'].hasInitialized = false;
};
/**
* Returns the width of the document.body. Sometimes though the body lies to
* try to make the page not break rails, so attempt to find those as well.
* An example: wikipedia pages for the ipad.
* @return {number} Width of the document body.
*/
__gCrWeb['findInPage'].getBodyWidth = function() {
var body = document.body;
var documentElement = document.documentElement;
return Math.max(body.scrollWidth, documentElement.scrollWidth,
body.offsetWidth, documentElement.offsetWidth,
body.clientWidth, documentElement.clientWidth);
};
/**
* Returns the height of the document.body. Sometimes though the body lies to
* try to make the page not break rails, so attempt to find those as well.
* An example: wikipedia pages for the ipad.
* @return {number} Height of the document body.
*/
__gCrWeb['findInPage'].getBodyHeight = function() {
var body = document.body;
var documentElement = document.documentElement;
return Math.max(body.scrollHeight, documentElement.scrollHeight,
body.offsetHeight, documentElement.offsetHeight,
body.clientHeight, documentElement.clientHeight);
};
/**
* Helper function that determines if an element is visible.
* @param {Element} elem Element to check.
* @return {boolean} Whether elem is visible or not.
*/
__gCrWeb['findInPage'].isVisible = function(elem) {
if (!elem) {
return false;
}
var top = 0;
var left = 0;
var bottom = Infinity;
var right = Infinity;
var originalElement = elem;
var nextOffsetParent = originalElement.offsetParent;
var computedStyle =
elem.ownerDocument.defaultView.getComputedStyle(elem, null);
// We are currently handling all scrolling through the app, which means we can
// only scroll the window, not any scrollable containers in the DOM itself. So
// for now this function returns false if the element is scrolled outside the
// viewable area of its ancestors.
// TODO (jonwall): handle scrolling within the DOM.
var pageHeight = __gCrWeb['findInPage'].getBodyHeight();
var pageWidth = __gCrWeb['findInPage'].getBodyWidth();
while (elem && elem.nodeName != 'BODY') {
if (elem.style.display === 'none' ||
elem.style.visibility === 'hidden' ||
elem.style.opacity === 0 ||
computedStyle.display === 'none' ||
computedStyle.visibility === 'hidden' ||
computedStyle.opacity === 0) {
return false;
}
// For the original element and all ancestor offsetParents, trim down the
// visible area of the original element.
if (elem.isSameNode(originalElement) || elem.isSameNode(nextOffsetParent)) {
var visible = elem.getBoundingClientRect();
if (elem.style.overflow === 'hidden' &&
(visible.width === 0 || visible.height === 0))
return false;
top = Math.max(top, visible.top + window.pageYOffset);
bottom = Math.min(bottom, visible.bottom + window.pageYOffset);
left = Math.max(left, visible.left + window.pageXOffset);
right = Math.min(right, visible.right + window.pageXOffset);
// The element is not within the original viewport.
var notWithinViewport = top < 0 || left < 0;
// The element is flowing off the boundary of the page. Note this is
// not comparing to the size of the window, but the calculated offset
// size of the document body. This can happen if the element is within
// a scrollable container in the page.
var offPage = right > pageWidth || bottom > pageHeight;
if (notWithinViewport || offPage) {
return false;
}
nextOffsetParent = elem.offsetParent;
}
elem = elem.parentNode;
computedStyle = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
}
return true;
};
/**
* Helper function to find the absolute position of an element on the page.
* @param {Element} elem Element to check.
* @return {Array.<number>} [x, y] positions.
*/
__gCrWeb['findInPage'].findAbsolutePosition = function(elem) {
var boundingRect = elem.getBoundingClientRect();
return [boundingRect.left + window.pageXOffset,
boundingRect.top + window.pageYOffset];
};
/**
* @param {string} text Text to escape.
* @return {string} escaped text.
*/
__gCrWeb['findInPage'].escapeHTML = function(text) {
var unusedDiv = document.createElement('div');
unusedDiv.innerText = text;
return unusedDiv.innerHTML;
};
/**
* Escapes regexp special characters.
* @param {string} text Text to escape.
* @return {string} escaped text.
*/
__gCrWeb['findInPage'].escapeRegex = function(text) {
return text.replace(__gCrWeb['findInPage'].REGEX_ESCAPER, '\\$1');
};
/**
* Gather all iframes in the main window.
* @return {Array.<Document>} frames.
*/
__gCrWeb['findInPage'].frameDocuments = function() {
var windowsToSearch = [window];
var documents = [];
while (windowsToSearch.length != 0) {
var win = windowsToSearch.pop();
for (var i = win.frames.length - 1; i >= 0; i--) {
if (win.frames[i].document) {
documents.push(win.frames[i].document);
windowsToSearch.push(win.frames[i]);
}
}
}
return documents;
};
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
'../../url/url.gyp:url_lib', '../../url/url.gyp:url_lib',
'../provider/ios_provider_chrome.gyp:ios_provider_chrome_browser', '../provider/ios_provider_chrome.gyp:ios_provider_chrome_browser',
'../web/ios_web.gyp:ios_web', '../web/ios_web.gyp:ios_web',
'injected_js',
'ios_chrome_resources.gyp:ios_theme_resources_gen', 'ios_chrome_resources.gyp:ios_theme_resources_gen',
], ],
'link_settings': { 'link_settings': {
...@@ -82,6 +83,12 @@ ...@@ -82,6 +83,12 @@
'browser/dom_distiller/dom_distiller_service_factory.h', 'browser/dom_distiller/dom_distiller_service_factory.h',
'browser/experimental_flags.h', 'browser/experimental_flags.h',
'browser/experimental_flags.mm', 'browser/experimental_flags.mm',
'browser/find_in_page/find_in_page_controller.h',
'browser/find_in_page/find_in_page_controller.mm',
'browser/find_in_page/find_in_page_model.h',
'browser/find_in_page/find_in_page_model.mm',
'browser/find_in_page/js_findinpage_manager.h',
'browser/find_in_page/js_findinpage_manager.mm',
'browser/infobars/confirm_infobar_controller.h', 'browser/infobars/confirm_infobar_controller.h',
'browser/infobars/confirm_infobar_controller.mm', 'browser/infobars/confirm_infobar_controller.mm',
'browser/infobars/infobar.h', 'browser/infobars/infobar.h',
...@@ -161,5 +168,20 @@ ...@@ -161,5 +168,20 @@
'browser/web_resource/ios_web_resource_service.h', 'browser/web_resource/ios_web_resource_service.h',
], ],
}, },
{
'target_name': 'injected_js',
'type': 'none',
'sources': [
'browser/find_in_page/resources/find_in_page.js',
],
'includes': [
'../../ios/web/js_compile.gypi',
],
'link_settings': {
'mac_bundle_resources': [
'<(SHARED_INTERMEDIATE_DIR)/find_in_page.js',
],
},
},
], ],
} }
...@@ -67,6 +67,7 @@ ...@@ -67,6 +67,7 @@
'public/web_state/crw_web_delegate.h', 'public/web_state/crw_web_delegate.h',
'public/web_state/crw_web_user_interface_delegate.h' 'public/web_state/crw_web_user_interface_delegate.h'
'public/web_state/crw_web_view_proxy.h' 'public/web_state/crw_web_view_proxy.h'
'public/web_state/crw_web_view_scroll_view_proxy.h'
'public/web_state/js/crw_js_base_manager.h', 'public/web_state/js/crw_js_base_manager.h',
'public/web_state/js/crw_js_early_script_manager.h', 'public/web_state/js/crw_js_early_script_manager.h',
'public/web_state/js/crw_js_injection_evaluator.h', 'public/web_state/js/crw_js_injection_evaluator.h',
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_WEB_PUBLIC_WEB_STATE_CRW_WEB_VIEW_SCROLL_VIEW_PROXY_H_
#define IOS_WEB_PUBLIC_WEB_STATE_CRW_WEB_VIEW_SCROLL_VIEW_PROXY_H_
#import <UIKit/UIKit.h>
@protocol CRWWebViewScrollViewProxyObserver;
// Provides an interface for web state observers to access the WebState's
// UIScrollView in a limited and controlled manner.
// This class is designed to limit lifetime of the UIScrollView such that it is
// not retained beyond WebState. It is also a way to tunnel UIScrollViewDelegate
// callbacks.
// NOTE: The API exposed by the proxy class isn't intended to be restrictive.
// The features needing to access other UIScrollView properties and methods
// needed to drive the UIScrollView are free to extend the proxy class as
// needed.
// The class forwards some of the methods onto the UIScrollView. For more
// information look at the UIScrollView documentation.
@interface CRWWebViewScrollViewProxy : NSObject<UIScrollViewDelegate>
@property(nonatomic, assign) CGPoint contentOffset;
@property(nonatomic, assign) UIEdgeInsets contentInset;
@property(nonatomic, readonly) BOOL isZooming;
@property(nonatomic, assign) UIEdgeInsets scrollIndicatorInsets;
@property(nonatomic, assign) CGSize contentSize;
@property(nonatomic, readonly) CGRect frame;
@property(nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
@property(nonatomic, assign) BOOL bounces;
@property(nonatomic, readonly) UIPanGestureRecognizer* panGestureRecognizer;
// Returns the scrollview's gesture recognizers.
@property(nonatomic, readonly) NSArray* gestureRecognizers;
// Calls UIScrollView's implementation of setContentInset: directly. This
// bypasses a very slow update path in UIWebView.
- (void)setContentInsetFast:(UIEdgeInsets)contentInset;
- (void)addGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer;
- (void)removeGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer;
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;
// Used by the CRWWebViewProxy to set the UIScrollView to be managed.
- (void)setScrollView:(UIScrollView*)scrollView;
// Copies all display properties that can be set on a CRWWebViewScrollViewProxy
// from the underlying UIScrollView into |scrollView|.
- (void)copyPropertiesToScrollView:(UIScrollView*)scrollView;
// Adds |observer| to subscribe to change notifications.
- (void)addObserver:(id<CRWWebViewScrollViewProxyObserver>)observer;
// Removes |observer| as a subscriber for change notifications.
- (void)removeObserver:(id<CRWWebViewScrollViewProxyObserver>)observer;
@end
// A protocol to be implemented by objects to listen for changes to the
// UIScrollView.
// This is an exact mirror of the UIScrollViewDelegate callbacks. For more
// information look at the UIScrollViewDelegate documentation.
@protocol CRWWebViewScrollViewObserver<NSObject>
@optional
- (void)webViewScrollViewDidScroll:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
- (void)webViewScrollViewWillBeginDragging:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
- (void)webViewScrollViewWillEndDragging:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint*)targetContentOffset;
- (void)webViewScrollViewDidEndDragging:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy
willDecelerate:(BOOL)decelerate;
- (void)webViewScrollViewDidEndScrollingAnimation:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
- (void)webViewScrollViewDidEndDecelerating:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
- (BOOL)webViewScrollViewShouldScrollToTop:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
@end
// A protocol to be implemented by objects to listen for changes to the
// CRWWebViewScrollViewProxyObserver.
// It inherit from CRWWebViewScrollViewScrollViewObserver which only implements
// methods for listening to scrollview changes.
@protocol CRWWebViewScrollViewProxyObserver<CRWWebViewScrollViewObserver>
@optional
// Called when the underlying scrollview of the proxy is set.
- (void)webViewScrollViewProxyDidSetScrollView:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
@end
#endif // IOS_WEB_PUBLIC_WEB_STATE_CRW_WEB_VIEW_SCROLL_VIEW_PROXY_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment