Cocoa/WebKit, having "window.open()" JavaScript links opening in an instance of Safari

10,996

Solution 1

I made from progress last night and pinned down part of my problem.

I am already using webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener: and I have gotten it to work with anchor tags, however the method never seems to get called when JavaScript is invoked.

However when window.open() is called webView:createWebViewWithRequest:request is called, I have tried to force the window to open in Safari here, however request is always null. So I can never read the URL out.

I have done some searching around, and this seems to be a known "misfeature" however I have not been able to find a way to work around it.

From what I understand createWebViewWithRequest gives you the ability to create the new webview, the the requested url is then sent to the new webView to be loaded. This is the best explanation I have been able to find so far.

So while many people have pointed out this problem, I have yet to see any solution which fits my needs. I will try to delve a little deeper into the decidePolicyForNewWindowAction again.

Thanks!

Solution 2

Well, I'm handling it by creating a dummy webView, setting it's frameLoad delegate to a custom class that handles

- (void)webView:decidePolicyForNavigationAction:actionInformation :request:frame:decisionListener:

and opens a new window there.

code :

- (WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request {
    //this is a hack because request URL is null here due to a bug in webkit        
    return [newWindowHandler webView];
}

and NewWindowHandler :

@implementation NewWindowHandler

-(NewWindowHandler*)initWithWebView:(WebView*)newWebView {
    webView = newWebView;

    [webView setUIDelegate:self];
    [webView setPolicyDelegate:self];  
    [webView setResourceLoadDelegate:self];

    return self;
}

- (void)webView:(WebView *)sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
    [[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]];
}

-(WebView*)webView {
    return webView;
}

Solution 3

There seems to be a bug with webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener: in that the request is always nil, but there is a robust solution that works with both normal target="_blank" links as well as javascript ones.

Basically I use another ephemeral WebView to handle the new page load in. Similar to Yoni Shalom but with a little more syntactic sugar.

To use it first set a delegate object for your WebView, in this case I'm setting myself as the delegate:

webView.UIDelegate = self;

Then just implement the webView:createWebViewWithRequest: delegate method and use my block based API to do something when a new page is loaded, in this case I'm opening the page in an external browser:

-(WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request {
    return [GBWebViewExternalLinkHandler riggedWebViewWithLoadHandler:^(NSURL *url) {
        [[NSWorkspace sharedWorkspace] openURL:url];
    }];
}

That's pretty much it. Here's the code for my class. Header:

//  GBWebViewExternalLinkHandler.h
//  TabApp2
//
//  Created by Luka Mirosevic on 13/03/2013.
//  Copyright (c) 2013 Goonbee. All rights reserved.
//

#import <Foundation/Foundation.h>

@class WebView;

typedef void(^NewWindowCallback)(NSURL *url);

@interface GBWebViewExternalLinkHandler : NSObject

+(WebView *)riggedWebViewWithLoadHandler:(NewWindowCallback)handler;

@end

Implemetation:

//  GBWebViewExternalLinkHandler.m
//  TabApp2
//
//  Created by Luka Mirosevic on 13/03/2013.
//  Copyright (c) 2013 Goonbee. All rights reserved.
//

#import "GBWebViewExternalLinkHandler.h"

#import <WebKit/WebKit.h>

@interface GBWebViewExternalLinkHandler ()

@property (strong, nonatomic) WebView                           *attachedWebView;
@property (strong, nonatomic) GBWebViewExternalLinkHandler      *retainedSelf;
@property (copy, nonatomic) NewWindowCallback                   handler;

@end

@implementation GBWebViewExternalLinkHandler

-(id)init {
    if (self = [super init]) {
        //create a new webview with self as the policyDelegate, and keep a ref to it
        self.attachedWebView = [WebView new];
        self.attachedWebView.policyDelegate = self;
    }

    return self;
}

-(void)webView:(WebView *)sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
    //execute handler
    if (self.handler) {
        self.handler(actionInformation[WebActionOriginalURLKey]);
    }

    //our job is done so safe to unretain yourself
    self.retainedSelf = nil;
}

+(WebView *)riggedWebViewWithLoadHandler:(NewWindowCallback)handler {
    //create a new handler
    GBWebViewExternalLinkHandler *newWindowHandler = [GBWebViewExternalLinkHandler new];

    //store the block
    newWindowHandler.handler = handler;

    //retain yourself so that we persist until the webView:decidePolicyForNavigationAction:request:frame:decisionListener: method has been called
    newWindowHandler.retainedSelf = newWindowHandler;

    //return the attached webview
    return newWindowHandler.attachedWebView;
}

@end

Licensed as Apache 2.

Solution 4

You don't mention what kind of erratic behaviour you are seeing. A quick possibility, is that when implementing the delegate method you forgot to tell the webview you are ignoring the click by calling the ignore method of the WebPolicyDecisionListener that was passed to your delegate, which may have put things into a weird state.

If that is not the issue, then how much control do you have over the content you are displaying? The policy delegate gives you easy mechanisms to filter all resource loads (as you have discovered), and all new window opens via webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener:. All window.open calls should funnel through that, as will anything else that triggers a new window.

If there are other window opens you want to keep inside your app, you will to do a little more work. One of the arguments passed into the delegate is a dictionary containing information about the event. Insie that dictionary the WebActionElementKey will have a dictionary containing a number of details, including the original dom content of the link. If you want to poke around in there you can grab the actual DOM element, and check the text of the href to see if it starts with window.open. That is a bit heavy weight, but if you want fine grained control it will give it to you.

Solution 5

By reading all posts, i have come up with my simple solution, all funcs are in same class,here it is, opens a link with browser.

- (WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request {

    return [self externalWebView:sender];
}




- (void)webView:(WebView *)sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener
{
    [[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]];
}

-(WebView*)externalWebView:(WebView*)newWebView
{
     WebView *webView = newWebView;

    [webView setUIDelegate:self];
    [webView setPolicyDelegate:self];
    [webView setResourceLoadDelegate:self];
    return webView;
}
Share:
10,996
FireWire
Author by

FireWire

Updated on June 23, 2022

Comments

  • FireWire
    FireWire almost 2 years

    I am building a really basic Cocoa application using WebKit, to display a Flash/Silverlight application within it. Very basic, no intentions for it to be a browser itself.

    So far I have been able to get it to open basic html links (<a href="..." />) in a new instance of Safari using

    [[NSWorkspace sharedWorkspace] openURL:[request URL]];
    

    Now my difficulty is opening a link in a new instance of Safari when window.open() is used in JavaScript. I "think" (and by this, I have been hacking away at the code and am unsure if i actually did or not) I got this kind of working by setting the WebView's policyDelegate and implementing its

    -webView:decidePolicyForNavigationAction:request:frame:decisionListener:
    

    delegate method. However this led to some erratic behavior.

    So the simple question, what do I need to do so that when window.open() is called, the link is opened in a new instance of Safari.

    Thanks

    Big point, I am normally a .NET developer, and have only been working with Cocoa/WebKit for a few days.

  • FireWire
    FireWire over 15 years
    Thanks for you feedback Louis, I checked into the last two links, and I have looked them over, unfortunately I do not think that will work because the window.open is not inline within a anchor tag. It is invoked via a Javascript method which is called from Silverlight.
  • Jason Murray
    Jason Murray about 9 years
    Old but awesome, this bug still appears to be present and this answer was exactly what I needed.