UIMenuItem with a UIImage in UIWebView

Ever wondered how iBooks shows their highlight-color UIImages when you select a paragraph? Below's a tutorial on how to achieve this in a UIWebView.

Since the initial release of ActionNotes, we used the Omni Group's OmniUI-library. It leverages CoreText as it's render engine for the text editor which we use in ActionNotes. In ActionNotes we need fine level control on what (x,y) coordinates each line of text is placed, as we need to overlay that line with a highlight marker.

The problem with making your own text editor is that you need to implement everything yourself. Want text selection? Do it yourself! Want cut/copy/paste? DIY! Want to show a loupe on a long touch? DIY. So, after a long investigation, I've changed the underlying editing system in ActionNotes with UIWebView. Yes, that's right. Since iOS5 you can use the contentEditable HTML attribute in order to have an editable divin your HTML. And with some Javascript, it's possible to get the (x, y)coordinates of HTML element, exactly what we need to show the marker highlight. Maybe I'll write a blog post later on to describe in detail how I've achieved to implement all ActionNotes' features into a UIWebView with JavaScript/JQuery.

Now, back to iBook's UIMenuItem. With the standard UIMenuItem, you cannot show any UImage's. That's where UImenuItem+CXAImageSupport comes in. It does some ObjC magic in order to show an image where normally the text is rendered! Now that's only part of the solution, as the location of the updated UIMenuController is wrong.

In order to overcome this glitch, I use 2 techniques: I first try to get the selection rectangle using Javascript insertion. In case no text is selected, it returns an empty rectangle and so I fallback to the last tapped location in the UIWebView.

All combined in code, this looks more or less like this:

Objective-C part

-(void)viewDidLoad {
        UIMenuItem *changeTextColorMI = [[UIMenuItem alloc] initWithTitle:@"Text Color" action:@selector(changeTextColor:)];
        [[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:changeTextColorMI]];
}

- (CGRect)rectForSelectedText {
    CGRect selectedTextFrame = CGRectFromString([self.noteView stringByEvaluatingJavaScriptFromString:@"getRectForSelectedText()"]);
    return selectedTextFrame;
}

-(void)changeTextColor:(UIMenuController*)sender {
        isShowingSubmenu = YES;
       
        UIMenuController *mc = [UIMenuController sharedMenuController];

    [mc setMenuItems:@[[[UIMenuItem alloc] cxa_initWithTitle:@"Red" action:@selector(redColor:) image:[UIImage imageNamed:@"red.png"]],
         [[UIMenuItem alloc] cxa_initWithTitle:@"Blue" action:@selector(blueColor:) image:[UIImage imageNamed:@"blue.png"]],
         [[UIMenuItem alloc] cxa_initWithTitle:@"Green" action:@selector(greenColor:) image:[UIImage imageNamed:@"green.png"]],
         [[UIMenuItem alloc] cxa_initWithTitle:@"Orange" action:@selector(orangeColor:) image:[UIImage imageNamed:@"orange.png"]],
         [[UIMenuItem alloc] cxa_initWithTitle:@"Grey" action:@selector(greyColor:) image:[UIImage imageNamed:@"grey.png"]],
         [[UIMenuItem alloc] cxa_initWithTitle:@"Pink" action:@selector(pinkColor:) image:[UIImage imageNamed:@"pink.png"]],
         ]];

        if ([self rectForSelectedText].size.width)
                [mc setTargetRect:[self rectForSelectedText] inView:self.noteView];
        else
                [mc setTargetRect:CGRectMake(lastTapInBackground.x, lastTapInBackground.y, 10, 10) inView:self.noteView];
        [mc setMenuVisible:YES animated:NO];
}

Javascript part

function getRectForSelectedText() {
        var selection = window.getSelection();
        var range = selection.getRangeAt(0);
        var rect = range.getBoundingClientRect();
        return "{{" + rect.left + "," + rect.top + "}, {" + rect.width + "," + rect.height + "}}";
}