views:

905

answers:

3

On windows, when the "Shell.Explorer" ActiveX control is embedded in an application it is possible to register an "external" handler - on object that implements IDispatch, such that scripts on the web page can call out to the hosting application.

<button onclick="window.external.Test('called from script code')">test</button>

Now, ive moved to Mac development and thought I could get something similar working from WebKit embedded in my Cocoa application. But, there really doesn't seem to be any facility to allow scripts to call back out to the hosting application.

One piece of advice was to hook window.alert and get scripts to pass a formatted message string as the alert string. Im also wondering if WebKit can perhaps be directed to an application hosted NPAPI plugin using NPPVpluginScriptableNPObject.

Am I missing something? Is it really this hard to host a WebView and allow scripts to interact with the host?

+1  A: 

This is very easy to do with the WebScriptObject API in combination with the JavaScriptCore framework.

Matt Lilek
+2  A: 

Its not really hard, you can either expose objects directly or use a WebPlugin (guide).

Directly exposing your objects can be done via WebViews windowObject property:

[[webView windowScriptObject] setValue:obj forKey:@"myObject"];

This exposes an object that implement the WebScripting protocol to JavaScript as a child of the window object, i.e. window.myObject.

Alternatively you can use plugins - here the scriptable object is the one returned by your WebPlugins objectForWebScript method. This scriptable object has to implement the WebScripting protocol.

WebKit of course supports NPAPI plugins too - WebPlugins are just a convenient Objective-C layer on top of the NPAPI... so if you want to have a portable plugin that not only works in WebKit and possibly across multiple platforms, consider using the NPAPI or a cross-browser/cross-platform plugin-framework like FireBreath.

Georg Fritzsche
You don't need to create a plug-in just to allow JavaScript to call methods in your app, that functionality is in WebKit already. See the WebScripting protocol reference: http://developer.apple.com/mac/library/documentation/cocoa/Reference/WebKit/Protocols/WebScripting_Protocol
Rob Keniger
Implementing the protocol for classes with one thing, but by implementing the WebScripting protocol alone you haven't exposed anything to JavaScript yet. I don't mind constructive criticism (i *do* forget things), but don't just throw half-baked facts in the room.
Georg Fritzsche
I've added an answer with some sample code.
Rob Keniger
As you might have noticed, i found it again in the mean-time. Do you care to remove the -1?
Georg Fritzsche
Okay, no problemo :-)
Rob Keniger
Thanks, also for pointing the error out in the first place - i just had some *"there is a much better way but i won't say how"* comments recently :)
Georg Fritzsche
+5  A: 

You need to implement the various WebScripting protocol methods. Here is a basic example:

@interface WebController : NSObject
{
    IBOutlet WebView* webView;
}

@end

@implementation WebController

//this returns a nice name for the method in the JavaScript environment
+(NSString*)webScriptNameForSelector:(SEL)sel
{
    if(sel == @selector(logJavaScriptString:))
        return @"log";
    return nil;
}

//this allows JavaScript to call the -logJavaScriptString: method
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)sel
{
    if(sel == @selector(logJavaScriptString:))
        return NO;
    return YES;
}

//called when the nib objects are available, so do initial setup
- (void)awakeFromNib
{
    //set this class as the web view's frame load delegate 
    //we will then be notified when the scripting environment 
    //becomes available in the page
    [webView setFrameLoadDelegate:self];

    //load a file called 'page.html' from the app bundle into the WebView
    NSString* pagePath = [[NSBundle mainBundle] pathForResource:@"page" ofType:@"html"];
    NSURL* pageURL = [NSURL fileURLWithPath:pagePath];
    [[webView mainFrame] loadRequest:[NSURLRequest requestWithURL:pageURL]];
}


//this is a simple log command
- (void)logJavaScriptString:(NSString*) logText
{
    NSLog(@"JavaScript: %@",logText);
}

//this is called as soon as the script environment is ready in the webview
- (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowScriptObject forFrame:(WebFrame *)frame
{
    //add the controller to the script environment
    //the "Cocoa" object will now be available to JavaScript
    [windowScriptObject setValue:self forKey:@"Cocoa"];
}

@end

After implementing this code in your controller, you can now call Cocoa.log('foo'); from the JavaScript environment and the logJavaScriptString: method will be called.

Rob Keniger
Does [webView mainFrame] loadData: fire the didClearWindowObject: ? I have the [webView setFrameLoadDelegate:self]; setup but this is not setting up the windowScriptObject when I try a breakpoint.
Luke
Seems the didClearWindowObject is only called if the page actually contains JavaScript. Thank you again!
Luke