iosobjective-cmacoscocoawebkit

How do I get the selected text from a WKWebView from objective-C


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


Solution

  • 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.