6

I have a WKWebView.

When the user right-clicks on it, I can customize a contextual menu in my objective-c method. I'd like to add a menu item only if the user has selected some text in the WKWebView. And of course I'll need to retrieve the selected text later on to process it.

How can I retrieve the selection from a WKWebView from objective-c, make sure it is only text and get that text ?

Thanks

AirXygène
  • 2,409
  • 15
  • 34

3 Answers3

9

Here is how I managed to do that. Not a perfect solution, but good enough.

General explanation

It seems that anything that happens inside the WKWebView must be managed in JavaScript. And Apple provides a framework for exchanging information between the JavaScript world and the Objective-C (or Swift) world. This framework is based on some messages being sent from the JavaScript world and caught in the Objective-C (or Swift) world via a message handler that can be installed in the WKWebView.

First step - Install the message handler

In the Objective-C (or Swift) world, define an object that will be responsible for receiving the messages from the JavaScript world. I used my view controller for that. The code below installs the view controller as a "user content controller" that will receive events named "newSelectionDetected" that can be sent from JavaScript

- (void)viewDidLoad
{
    [super viewDidLoad];

    //  Add self as scriptMessageHandler of the webView
    WKUserContentController *controller = self.webView.configuration.userContentController ;
    [controller addScriptMessageHandler:self
                                   name:@"newSelectionDetected"] ;
    ... the rest will come further down...

Second step - Install a JavaScript in the view

This JavaScript will detect selection change, and send the new selection through a message named "newSelectionDetected"

- (void)    viewDidLoad
{
    ...See first part up there...

    NSURL       *scriptURL      = .. URL to file DetectSelection.js...
    NSString    *scriptString   = [NSString stringWithContentsOfURL:scriptURL
                                                           encoding:NSUTF8StringEncoding
                                                              error:NULL] ;

    WKUserScript    *script = [[WKUserScript alloc] initWithSource:scriptString
                                                     injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
                                                  forMainFrameOnly:YES] ;
    [controller addUserScript:script] ;
}

and the JavaScript:

function getSelectionAndSendMessage()
{
    var txt = document.getSelection().toString() ;
    window.webkit.messageHandlers.newSelectionDetected.postMessage(txt) ;
}
document.onmouseup = getSelectionAndSendMessage ;
document.onkeyup   = getSelectionAndSendMessage ;
document.oncontextmenu  = getSelectionAndSendMessage ;

Third step - receive and treat the event

Now, every time we have a mouse up or a key up in the WKWebView, the selection (possibly empty) will be caught and send to the Objective-C world through the message.

We just need a handler in the view controller to handle that message

- (void)    userContentController:(WKUserContentController*)userContentController
          didReceiveScriptMessage:(WKScriptMessage*)message
{
    // A new selected text has been received
    if ([message.body isKindOfClass:[NSString class]])
    {
        ...Do whatever you want with message.body which is an NSString...
    }
}

I made a class which inherits from WKWebView, and has a NSString property 'selectedText'. So what I do in this handler, is to store the received NSString in this property.

Fourth step - update the contextual menu

In my daughter class of WKWebView, I just override the willOpenMenu:WithEvent: method to add a menu item if selectedText is not empty.

- (void)    willOpenMenu:(NSMenu*)menu withEvent:(NSEvent*)event
{
    if ([self.selectedText length]>0)
    {
        NSMenuItem  *item   = [[NSMenuItem alloc] initWithTitle:@"That works !!!"
                                                         action:@selector(myAction:)
                                                  keyEquivalent:@""] ;
        item.target = self ;
        [menu addItem:item] ;
    }
}

- (IBAction)    myAction:(id)sender
{
    NSLog(@"tadaaaa !!!") ;
}

Now why isn't that ideal? Well, if your web page already sets onmouseup or onkeyup, I override that.

But as I said, good enough for me.

Edit: I added the document.oncontextmenu line in the JavaScript, that solved the strange selection behavior I sometimes had.

Pang
  • 9,564
  • 146
  • 81
  • 122
AirXygène
  • 2,409
  • 15
  • 34
  • I need to do this this very thing, but I cannot seem to get text in my handler; it's nil. Can you detail how your selectedText property is being set as I suspect it's gone by the time the handler is called? – slashlos Aug 16 '18 at 22:22
  • @slashlos, well, in the handler (userContentController:didReceiveScriptMessage, I just set a property of the view, declared as strong in its header file, so it is living as long as the view itself lives. When the willOpenMenu:withEvent is called, the property is therefore still there. – AirXygène Aug 17 '18 at 06:09
  • @slashlos, if you get a nil body in your message, it may indicate that something is wrong in your Javascript. Is the userContentController:didReceiveScriptMessage handler called ? Have you checked the message and the body ? – AirXygène Aug 17 '18 at 06:12
  • I had found my issue in my setup. I'm going to post my question - with your great work as a start, to what I need, thanks! – slashlos Aug 17 '18 at 11:14
6

Swift 5 translation

webView.configuration.userContentController.add(self, name: "newSelectionDetected")
let scriptString = """
    function getSelectionAndSendMessage()
    {
        var txt = document.getSelection().toString() ;
        window.webkit.messageHandlers.newSelectionDetected.postMessage(txt);
    }
    document.onmouseup = getSelectionAndSendMessage;
    document.onkeyup = getSelectionAndSendMessage;
    document.oncontextmenu = getSelectionAndSendMessage;
"""
let script = WKUserScript(source: scriptString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(script)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    // Use message.body here
}
StuFF mc
  • 4,137
  • 2
  • 33
  • 32
0

One only needs to evaluate simple js script

NSString *script = @"window.getSelection().toString()";

using evaluateJavaScript method

[wkWebView evaluateJavaScript:script completionHandler:^(NSString *selectedString, NSError *error) {
    
}];

The Swift version

let script = "window.getSelection().toString()"
wkWebView.evaluateJavaScript(script) { selectedString, error in
        
}
Pang
  • 9,564
  • 146
  • 81
  • 122
malex
  • 9,874
  • 3
  • 56
  • 77