Skip to content


Search and highlight text in UIWebView

Several iPhone Apps (like my “iCab Mobile” or “NewsTap” Apps) provide a search feature which allows to search for text in the content that is currently displayed within a UIWebView. The found occurrences of the searched text are highlighted with a yellow background, so the search result can be visually located very easy.

This blog post describes how this can be implemented. I’m implementing this feature as a category for the UIWebView class, so you can use the new search feature for all UIWebView objects in your Apps very easily.

First of all, UIWebView doesn’t allow us to access its content directly, so we have to use JavaScript again. But if you’ve read my other blog posts, you already know this approach.

Our goal is to implement two methods for our new UIWebView category. One method should start the search and highlights the search results. As a result this method should return the number of occurrences of the text we’ve searched. The other method should remove all the highlighted search results again to restore the original layout of the web page.

What we need to do is to write the main code in JavaScript and a wrapper code in Objective C which is simply calling the JavaScript code.

We start with the JavaScript code that is doing the real work. As I’ve already described in the blog post WebKit on the iPhone, the JavaScript code will be saved as resource file in the XCode project. This way it can be loaded from within the Objective C code from the application bundle very easily, and we don’t mix the code of multiple programming languages (JavaScript and Objective C) in the same files.

The following code is the the JavaScript implementation; below I’ll explain what it is doing and how it works:

SearchWebView.js:

// We're using a global variable to store the number of occurrences
var MyApp_SearchResultCount = 0;

// helper function, recursively searches in elements and their child nodes
function MyApp_HighlightAllOccurencesOfStringForElement(element,keyword) {
  if (element) {
    if (element.nodeType == 3) {        // Text node
      while (true) {
        var value = element.nodeValue;  // Search for keyword in text node
        var idx = value.toLowerCase().indexOf(keyword);

        if (idx < 0) break;             // not found, abort

        var span = document.createElement("span");
        var text = document.createTextNode(value.substr(idx,keyword.length));
        span.appendChild(text);
        span.setAttribute("class","MyAppHighlight");
        span.style.backgroundColor="yellow";
        span.style.color="black";
        text = document.createTextNode(value.substr(idx+keyword.length));
        element.deleteData(idx, value.length - idx);
        var next = element.nextSibling;
        element.parentNode.insertBefore(span, next);
        element.parentNode.insertBefore(text, next);
        element = text;
        MyApp_SearchResultCount++;	// update the counter
      }
    } else if (element.nodeType == 1) { // Element node
      if (element.style.display != "none" && element.nodeName.toLowerCase() != 'select') {
        for (var i=element.childNodes.length-1; i>=0; i--) {
          MyApp_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword);
        }
      }
    }
  }
}

// the main entry point to start the search
function MyApp_HighlightAllOccurencesOfString(keyword) {
  MyApp_RemoveAllHighlights();
  MyApp_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase());
}

// helper function, recursively removes the highlights in elements and their childs
function MyApp_RemoveAllHighlightsForElement(element) {
  if (element) {
    if (element.nodeType == 1) {
      if (element.getAttribute("class") == "MyAppHighlight") {
        var text = element.removeChild(element.firstChild);
        element.parentNode.insertBefore(text,element);
        element.parentNode.removeChild(element);
        return true;
      } else {
        var normalize = false;
        for (var i=element.childNodes.length-1; i>=0; i--) {
          if (MyApp_RemoveAllHighlightsForElement(element.childNodes[i])) {
            normalize = true;
          }
        }
        if (normalize) {
          element.normalize();
        }
      }
    }
  }
  return false;
}

// the main entry point to remove the highlights
function MyApp_RemoveAllHighlights() {
  MyApp_SearchResultCount = 0;
  MyApp_RemoveAllHighlightsForElement(document.body);
}

The basic principle of searching the text and removing the highlighted search results is the same: We’re working at DOM level (Document Object Model), which means the HTML document is represented as a tree structure where each HTML element, text, comment etc. is represented as a node. All the nodes are linked together with parent and child connections. The root element of each HTML document is the element that is created by the HTML tag. This element has usually two children: The HEAD element and the BODY element. Only the content of the BODY element is visible and displayed on screen, so we only need to process this part of the document tree.

What we need to do is to start with the body element and traverse all of its child nodes. From within the child nodes we need to go to their child nodes as well, and so on until we reach a leaf nodes, which has no child elements. Text nodes are always leaf nodes and text nodes are the nodes which might contain the text we’re looking for.

Traversing the whole HTML tree searching for all text nodes can be done by a recursive algorithm called Depth-First-Search (DFS). The DFS algorithm will traverse the tree structure starting from a root element (in our case the BODY element) to the first leaf node in a branch of the tree (for example going to the first child of the root first, from there again going to the first child, etc until a leaf node is reached). Then the algorithm goes back (backtracking) to the last node where not all child nodes were traversed yet and continues with the next unvisited child nodes etc. This way all nodes of the whole tree are traversed and we are able to find all the text nodes in which we are looking for the text we are earching.

The functions “MyApp_HighlightAllOccurencesOfStringForElement(element,keyword)” and “MyApp_RemoveAllHighlightsForElement(element)” are both implementations of this DFS algorithm. These functions are called from MyApp_HighlightAllOccurencesOfString(keyword)” and “MyApp_RemoveAllHighlights()” which are doing the necessary initialization and provide the DFS functions with the proper root element (the BODY element). The initialization for a new search is to make sure than no highlighted text from a previous search is present, so we simple call the function to remove all text highlights.

When searching for a text, we check if the currently inspected node is a text node or if it is an element node. If it is an element node it can have child nodes, and these must be inspected as well. If it is a text node, we have to find out if the text of this node contains the text we’re searching. If yes, we insert the text highlight, otherwise we are finished with this node. Also if the node is neither a text node nor an element node, there aren’t any child nodes which are interesting for us, so we are finished with this node as well.

When the text of a text node contains the searched text, we have to split the text into three parts. Part one will contain the text up to the searched text, part two contains the searched text itself and part three contains the rest of the text. A new element will be created (a SPAN element) and the second part (the searched text) will become a child node of this new element. Now we can assign StyleSheet rules to the newly created SPAN element to create the highlight effect (setting the background color to yellow, setting the text color to black, you can even increase the font size, if you want to). Now the new element is linked with part one and three so it becomes a part of the tree strucuture of the HTML tree. And because the searched text might be found multiple times in the original text node, we continue to search for the searched text in the third part of the original text node. If we find another occurrence of the searched text, we split this third part again in three parts, otherwise we are finished. When we create a SPAN element for the highlight effect, we also assign a special value (here “MyAppHighlight”) for the class attribute. This is important to be able to find these elements later again when we want to remove the highlight effects using the function “MyApp_RemoveAllHighlights()”. For this task we traverse the tree as well, but now we’re looking for elements whose class attribute has this special value. To restore the original state of the HTML document, we have to remove the elements we’ve inserted before (the ones with the special value of the class attribute) and we need to concatenate the text node we’ve split. JavaScript can help us to concatenate the text nodes again, because it provides the “normalize()” function which can do this for us.

In JavaScript we can find out the type of a node with the “nodeType” property. A value of 1 means that the node is a normal element node (like the “body” node, a “span” node etc.). A value of 3 means that the node is a text node. In this case the property nodeValue contains the text of the node. Other values for nodeType represent comment nodes (in HTML these are written as “<!– Comment –>”), attribute nodes (for HTML attributes like for example the “HREF” attribute for the “A” tag), document nodes and some more. In our case only the values 1 (element node) and 3 (text node) are important.

In the above implementation, we count the number of found occurrences in a global variable.

Note: You’ll notice that the JavaScript function names and variables and also the value for the class attribute I’m using in the above code are very lengthy and they do also have a prefix like “MyApp_”. The reason for this is to avoid any conflicts with existing function and variable names of the web page in which we inject our JavaScript code. If you’re generating the HTML code yourself that is displayed in the UIWebView object, you can choose shorter and simpler names. But if you have to deal with HTML and JavaScript code of any web pages (like in a web browser like iCab Mobile), you should use longer names and also add the name of your app as Prefix to all function and variable names to avoid any conflicts.

The Cocoa/Objective C part of the implementation is very simple. We only need to declare the interface and write a simple wrapper which loads and calls the JavaScript code that is actually doing all the hard work. The interface is also simple, we only need two methods: one to start the search and which highlights the found text and one which removes all the highlights again.

SearchWebView.h:

@interface UIWebView (SearchWebView)

- (NSInteger)highlightAllOccurencesOfString:(NSString*)str;
- (void)removeAllHighlights;

@end

The typical use case would be to provide a search field where the user can enter some text. This text would be passed to the method “highlightAllOccurencesOfString:”. And when the user shakes the device, the App could call the method “removeAllHighlights” to remove all the highlighted search results again.

The implementation would look like this:

SearchWebView.m:

@implementation UIWebView (SearchWebView)

- (NSInteger)highlightAllOccurencesOfString:(NSString*)str
{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"SearchWebView" ofType:@"js"];
    NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    [self stringByEvaluatingJavaScriptFromString:jsCode];

    NSString *startSearch = [NSString stringWithFormat:@"MyApp_HighlightAllOccurencesOfString('%@')",str];
    [self stringByEvaluatingJavaScriptFromString:startSearch];

    NSString *result = [self stringByEvaluatingJavaScriptFromString:@"MyApp_SearchResultCount"];
    return [result integerValue];
}

- (void)removeAllHighlights
{
    [self stringByEvaluatingJavaScriptFromString:@"MyApp_RemoveAllHighlights()"];
}

@end

The first thing we’re doing in the method “highlightAllOccurencesOfString:” is to load the JavaScript file we’ve written above from the application bundle and inject it into the web page that is currently displayed in UIWebView. Because we’re implementing this as a category for UIWebView, we can use “self” to call the method “stringByEvaluatingJavaScriptFromString:” of the UIWebView instances.

After we’ve injected the JavaScript code we simply call the JavaScript function we’ve defined above to do the search.

And finally we access the variable we’ve defined in the JavaScript code, which represents the number of occurrences of the string that were found, and we return its integer value as the result of the method.

In the method “removeAllHighlights” we only need to call the corresponding JavaScript function we’ve defined in the JavaScript code from above. Loading the external JavaScript file and injecting it into the Web page is not necessary here. If we’ve started a search before, the code is already injected and we don’t need to do this again. And if we haven’t started a search before, we just don’t need the JavaScript code because there are no highlights which have to be removed.

As you can see, the Objective C code for the UIWebView category is just a simple wrapper code for the JavaScript code. Now, when you have a UIWebView object in your App, you can simply search for text in its content by calling “highlightAllOccurencesOfString:”, like in this example where we’re searching for the text “keyword”:

  // webView is an instance of UIWebView
  [webView highlightAllOccurencesOfString:@"keyword"];

Additional notes: In case your App has to deal with web pages which can have frames, you have to add some additional code that looks for frames. You have to traverse all the documents of all the frames to find all the text nodes in all frames. The code from above isn’t doing this to keep it as simple as possible.

Posted in iPhone & iPod Touch, Programming.

Tagged with , , , , .


300 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. Alexander Alexander says

    @coder
    With “saving the webviews into strings” you probably mean that you’re saving the HTML code for the pages into strings.

    I think you don’t need to store all of these in memory all the time. You can use something like a cache: keep them in memory if there’s enough free memory, otherwise save them to disk and release the strings from memory (or just the ones which are not needed all the time) and load them from disk later, when you need them again.

  2. coder says

    well in fact no i’m not saving the html code, but i’m loading the content of the html (the actual text) into a string and saving this string into a plist.
    I needed to ask if the plist content can be accessed in anyway, if the device is jailbroken.
    thanks again for your tips.
    By the way i’m facing another problem concerning webviews. When I load the content of the epub book into the webviews, it’s loaded in a certain format, when I want to open another book with a different format, the webview is preserving the old format and applying to the new book, therefore distorting its original format. Although before loading this book, I’m releasing the old webview and creating a new one but still the problem is occuring.
    I would truly appreciate an advice,
    thank you

  3. Dammy says

    Hi Alex,

    Thanks a lot for this great tutorial. I implemented the same steps as you mentioned above it works fine.

    Now i am trying to implement Highlight feature similar to iBook.

    Some what i can able to select multiple lines in the same paragraph and mark it as bookmark with the help of index value ” var idx = value.toLowerCase().indexOf(keyword);” to uniquely identify the selected string.

    But sometimes i am getting the same index value for selected strings and i could able to accomplish this feature. The same issue comes when i select the multiple paragraphs from the webview.

    Could you please help me in what steps i need to take to support this Highlight feature similar to iBook.

    Thanks in advance,
    Dammy

  4. Alexander Alexander says

    @Dammy
    Sorry, but I haven’t fully understood your problem. Could you rephrase the problem in other words, maybe it will be more clear what you’ve meant.

  5. Dammy says

    @Alex,

    Sorry for the confusion in statements.

    I am doing an app similar to iBook.

    I can able to support functionalities Search using your code,Increasing and decreasing font size, Changing font families, Background color and so on.

    Now i am trying to implement the bookmark functionality as in iBook.

    Partially i can able to support this functionality means i can able to select the words in paragraphs and highlight with some color as done for search.the “|” marks the selection range start and end point.

    Here |starts the |selection.

    After applying javascript above statement looks like below

    Here  starts the  selection.

    but i am unable to select the words between multiple paragraphs like below

    |Here starts the selection.
    This is fully in the range.
    This only partial|

    After applying javascript above statement looks like below

    Here starts the selection.
    This is fully in the range.
    This only partial.

    Below is the script that i am using to highlight strings.

    function f() 
    {
    var userSelection;
    if (window.getSelection) {
    	userSelection = window.getSelection();
    }
    else if (document.selection) { // should come last; Opera!
    	userSelection = document.selection.createRange();
    }
    
    var selectedText = userSelection;
    if (userSelection.text)
    	selectedText = userSelection.text;
       
    var range = null;
    
    if (userSelection.getRangeAt)
    {
        range = userSelection.getRangeAt(0);
    }
    else 
    { // Safari!
        range = document.createRange();
        range.setStart(userSelection.anchorNode,userSelection.anchorOffset);
        range.setEnd(userSelection.focusNode,userSelection.focusOffset);
    }
    
    var selectionContents   = range.extractContents();
    var selectedStr = selectionContents.textContent;
    var span                = document.createElement("span");
    span.appendChild(selectionContents);
    span.setAttribute("class","uiWebviewHighlight");
    span.style.backgroundColor  = "red";
    span.style.color            = "black";
    range.insertNode(span);
    
    return selectedStr;  
    
    }
    f();
    

    Hope u clear with my statements.

    Please help me in finding the solution for the issue.

  6. Alexander Alexander says

    @Dammy
    I’m not sure if all of your original formatting of your comment got through.

    But I guess your main problem is that the selection crosses the boundaries of HTML elements, like paragraphs, you don’t get the result you’ve expected.

    Please note that text within a HTML element, like a paragraph, is located in its own text node. So it does not include the texts of adjacent paragraphs or other HTML elements. My search function does not take this into account and for simple searches within a web page, this is usually not necessary. If you have to deal with larger texts you have to be prepared that your text is split up into multiple text nodes and that these text nodes are separated by other nodes in the HTML/DOM tree. It’s best to draw a tree of a sample HTML code to understand where the text nodes are located and what they contain. Dealing with larger texts can be much more difficult than just searching for words.

  7. Dammy says

    @Alex,

    Thanks for the reply.

    var sel = window.getSelection();
    var rng = sel.getRangeAt(0);

    How can i store this entire rng object with iOS code.

    Could you please share me the snippet if u have any idea on this.

    Thanks in advance
    Dammy.

  8. Alexander Alexander says

    @Dammy
    You can’t store a JavaScript range object within your ObjectiveC code. The range object contains references to certain nodes, and these references are only valid with JavaScript.
    So you need to store these objects within the web page, for example store it in a JavaScript variable.

  9. Dammy says

    @Alex,

    Its fine. But i need those javascript variable values for the next session means after relaunching the application. How can i achieve this?

    Thanks in advance
    Dammy

  10. Alexander Alexander says

    @Dammy
    You can’t store any references to objects of a page longer than the page exists in your browser. So as soon as you click on a link and the web view loads another page, all these references become invalid.

    If you need to save something, you can use cookies, the database or the local storage of JavaScript. But you can’t store references to objects in there, because as soon as the original objects vanish when the page is no longer displayed in the browser, these references are invalid. So you may need to store all the information you need to recreate these objects again from scratch.

  11. coder says

    Hy Alexander, in the code there is MyApp_FirstOccurenceID refers to the first occurence of the highlighted word. I use window.location.hash = MyApp_FirstOccurenceID to get its location in the webview. then I get the XOffset. I was inspired by this part, and tried to apply it else where. instead of creating a span <a> with attrribute MyApp_FirstOccurenceID I manually put in the html(epub file) as follows
    </a><a>realpage0003</a>
    then I try to use window.location.hash = realpage0003 and try to get the XOffset, but its not working, any advice would be appreciated. thank u

  12. coder says

    the link I put is realpage0003 ()

    I had to add the parentheses so it shows, just ignore them

  13. coder says

    the link I put is ,,,,,,,,,,, realpage0003
    I hope it works now with the comas

  14. Alexander Alexander says

    @coder
    I’ve edited the original post so it shows what you’ve written. Sorry the comment feature of the blog interprets HTML tags as HTML tags…

    I’m not yet sure how you get the offsets from window.location.hash. If you assign a value to window.location.hash the page just scrolls to the element that is references – if the element is out of view. So if your idea is to use the scroll location as coordinates for the element, then this will fail. It might work in some cases, but in general it won’t give you accurate results. First of all, Browsers usually don’t need to scroll horizontally, because the web pages adapt to the widths of the browser window. So the horizontal scroll offset is usually always 0. And for the vertical offset, this also fails when the web page is not long enough to be able to scroll the element to the top of the screen.

  15. coder says

    Hy thank you, for your tip in fact the solution worked out, there was a syntax error with the html code. I used window.location.hash = “the name of the span”, then window.offsetX and I get the offset then dividing with the whole width I get the page where it occurs, it is pretty accurate in my case.
    Thank u for your assistance.

  16. vinay says

    Hi,

    In my application, webview contains textarea element. If my search string is inside text area then instead of highlighting the string, searched string is hided. How to solve this issue.

  17. Alexander Alexander says

    @vinay
    Yes, text areas can not be processed this way. You can’t have HTML tags within the content of text areas, and the highlighting of text is done in my code with the help of CSS and SPAN elements. And I guess this is why the text that was found within the text area was hidden: it is not surrounded by a SPAN element and because these elements are not allowed here, they are ignored together with their content, the text.

    You can fix this very easy. Before you process a child node, just check if the current node is a Textarea, if yes, ignore the child nodes. This way the content of textareas won’t be affected by the search.

    This checks needs to be done in the JS function MyApp_HighlightAllOccurencesOfStringForElement() where it loops through all child nodes of the current node. If the current node is a textarea node, then simply skip the loop.

  18. Vinay says

    Hi,
    Thanks, I have implemented the same and now searched string is not hidden from the textarea. But is there any ways is there to highlight the search string which is inside text area.

    Thanks.

  19. Alexander Alexander says

    @Vinay
    There’s no way to select the text of a textarea. If the searched text is only included once in the text area, it might be possible to define a regular text selection (for Copy & Paste), but this would not work if the text is found multiple times or if there are multiple textareas where the text is found, or if you need to have the text selection independently of the search (there can be only one text selection range within the whole web page).

  20. coder says

    Hy,
    I’m trying to make my application faster by dividing the html into chunks then loading it progressively. My question is there a way to load the html progressively file without having to reload the whole webview each time I add some content to the html file.
    what I ‘m doing is
    [tempWebView stringByEvaluatingJavaScriptFromString:@”document.documentElement.innerHTML += %@”, htmlAddition];
    //update bodyContent
    NSString *loadString = [NSString stringWithFormat:templateHTMLString,headContents,fontSize,bodyContents];
    NSURL *baseURL = [NSURL fileURLWithPath:itemPath];
    [tempWebView loadHTMLString:loadString baseURL:baseURL];

    the problem is the update only occurs when I reload the webview, is there a way to update the content without reloading the webview.
    Thank you for your assistance.

  21. coder says

    Hy again,
    can I extract from the webview the content of a certain offset?
    usually I use window.scrollTo() to go to a certain offset, what I need is the extract the content of the last offset, is there anyway to do so.
    Than you in advance.

  22. Alexander Alexander says

    @coder
    Adding new content dynamically should work fine. The WebView might eventually need to re-layout the whole page, but this should work automatically.

    But I’m not sure if your code fragment from above is really correct. You can add new content using the “+=” operator on the innerHTML property, but you need to add a string. The above code looks like you’ve forgotten the quote characters around the “%@” placeholder, so if the string you’re adding here does not contain these quotes, the whole JS statement you’re creating here might not be valid and therefore not working.

    For getting the element on a certain location, please check out the blog post http://www.icab.de/blog/2011/10/17/elementfrompoint-under-ios-5/

  23. Biswa says

    Hi,
    In the webview searched string is highlighted when the searched string is visible part of the webview. Suppose my searched string is there in webview but that part of webview is not visible then when i scroll down the webview, then its not highlighted. If I mouse over then the searched string is Highlighted.
    Please let me know how to fix this.

  24. Alexander Alexander says

    @Biswa
    Searching and highlighting the text in the way I’ve done this in this blog post is independent of the scroll location and the visibility of the area of the web page.

    The code form above searches through the whole web page and therefore processes all occurrences of the search term.

    I guess there are certain HTML structures which might not force all browsers to re-render everything when you change certain areas of the code. For example absolutely positioned elements with a fixed width and height might be excluded from the automatic re-rendering because they usually don’t change their layout and some browser-specific speed optimizations might take over. But this would be a browser-specific optimization issue and this would not be a general problem. In this case you might need to find a workaround for this browser or web engine, like changing the font size for the content of these absolutely positioned boxes slightly or something like that.

  25. sefiroths says

    hi, i’ve tried this, seems wondeftul tutorial. but i’m facing a problem:
    i have a webview in storyboard, connected to iboutlet. i followed all steps but this does not hilight anytning…
    putting nslog everywhere, seems that:
    NSLog(@”%@”,path);<—-i can see the path
    NSLog(jsCode);<—-i see javascript code
    NSLog(@"%@",[self stringByEvaluatingJavaScriptFromString:jsCode]);<– is not log anything…
    [self stringByEvaluatingJavaScriptFromString:startSearch];<—not log anything
    any ideas?
    if you want i can zip a project and put it on some file sharing site
    thanks

  26. sefiroths says

    you can delete the previous post, i putted the code to hilight in viewdidload, perhaps the page was not loaded yet, if i put it in a button the code works well, thanks

  27. Alexander Alexander says

    @sefiroths
    Yes, in viewDidLoad the code is most likely not yet loaded. UIWebView loads everything in the background, so the only way to find out when the code is loaded is by implementing the UIWebView delegate method “webViewDidFinishLoad:”. This method is called when the HTML code is loaded.

  28. AppPear says

    Hi!
    Is there a way to work this awesome feature when uiwebview contains an rtf file?

  29. Alexander Alexander says

    @AppPear
    Within the comments someone said that UIWebView will convert RTF to HTML, in which case this should work. But this is not documented by Apple and might also not be true for all iOS releases. So in general I would assume that the search feature would only work with HTML, for all other file formats, you may have luck, but there’s no guarantee.

  30. Nilesh Tripathi says

    Hey Alexander

    I want a snippet to highlight a particular text which have been selected by user in UIWebView.
    I am using AEPubreader which have same functions for search and highlight that you have provided.
    Please help me out with. Basically I am an iPhone app developer, but no idea of javascript so I am little bit confused with it.
    Thanks in advance.

  31. Nilesh Tripathi says

    This tutorial is a kind of that I cannot just walk away without saying “Thank you”. Appreciated your effort for putting this up.

  32. Alexander Alexander says

    @Nilesh Tripathi
    Unfortunately, because of the limiting API of UIWebView, you have no other choice than using JavaScript for almost everything you want to do that is not just loading a new web page. And doing this on the iPhone is a real pain, because there’s no debugger and other help for debuggin the JavaScript code.

    JavaScript is able to get the selected text as raw text but also as a “range” object, which includes the first and last nodes which are covered by the text and also the offsets within these text nodes so you know exactly where exactly with these text nodes, the selection starts. Based on this information you can process the text and nodes depending of your requirements, but this can be complicated. The range can span many different block and inline elements and when manipulating the HTML code you may need to take care about the HTML structure as well. So you not only need to know much about JavaScript, but also much about HTML as well.

    Highlighting a text the user has selected can be easy (when the text is completely inside a single text node) and you only need to add a new SPAN element like it is shown in this blog post, but it can also be extremly complicated (when the selected text spans multiple block and inline nodes). In this case you might have to create multiple SPAN elements in many of the involved nodes, make sure that inline and block element boundaries are preserved to avoid that the HTML code structure gets invalid and the layout of the page is destroyed.

  33. Nilesh Tripathi says

    Okay … Thanks to demmy.. I copied his code and its working…

    Thank you dammy and Alexander.

  34. john says

    Wanted to say big thanks for writing this blog post and for answering everyone’s questions!!! And thanks to JC for writing a followup on how he took your suggestion on search prev/next.

  35. Paul Linsay says

    Alexander, thanks for your hard work. This is a great help for the iOS app I’m developing, especially since I’m a real novice at js.

    I came across a problem that I don’t know how to solve. A keyword can be hiding in elements that aren’t displayed on the web page. When the user steps through the keywords he shouldn’t have steps where nothing appears. Apple is very smart about this and somehow avoids the hidden keywords.

    As an example, go to the link

    http://www.azleg.state.az.us/FormatDocument.asp?inDoc=/ars/47/01101.htm&Title=47&DocType=ARS

    and search for “code”. There are actually three instances of it but only two are visible. If you click on More Agencies > Executive you will see the bottom item in the dropdown menu is “Admin Code”. Your little algorithm finds this but Apple’s doesn’t, either in Safari or mobile Safari.

    Any ideas on how to solve this would be appreciated. Thanks again.

  36. Paul Linsay says

    I should have added that iCab doesn’t find the invisible instance of code either ;-)

  37. Alexander Alexander says

    @Paul Linsay
    Yes, you can always make the code smarter ;-)

    For example you can check if the element in which the text was found is one of those elements, which are never shown (like all elements within the HEAD section of the page), or if these elements have a CSS property “display” or “visibility” set to a value which makes the element invisible. For the latter use the function “getComputedStyle()” which also takes the inheritance of CSS properties into account…

      if (window.getComputedStyle(element, false).display == 'none'  ||
          window.getComputedStyle(element, false).visibility == 'hidden') {
         // element is not visible
      }
    
  38. John says

    Hi Alexander,
    It works Great….. but problem is, when it got a string which appearing at multiple places in the para. then get highlighted at every places…so what i want is… highlighting of the words with the audio itself. Kind of like the bouncing ball that bounces from one word to another.

    Thanks.

  39. Alexander Alexander says

    @John
    Sorry for the delay, your comment was stuck in the SPAM filter…

    If you want to highlight only one of the occurrences of the search term, and move the highlighting back and forward between all of the occurrences, you can do this as well. iCab Mobile is doing something like this as well: though it highlights all occurrences in yellow at the same time, you can jump between these occurrences and the current one if highlighted in another color.

    What must be done here is to save all occurrences in an array while searching. And when you want to highlight item n, then just execute the “highlight code” from above for this one item. If you jump to another item, use the “unhighlight code” to remove the highlighting of the previous item and execute the highlight code for the new item again.

  40. RockoT says

    “From within the child nodes we need to go to their child nodes as well, and so on until we reach a leaf nodes, which has no child elements. Text nodes are always leaf nodes and text nodes are the nodes which might contain the text we’re looking for.”

    This is a problem that I’ve seen with all the javascript solutions, that I’ve never seen with a browser based solution.

    Take this example of the word “intended”:

    <span>i</span>ntended

    That will be highlighted in a browsers ‘find’ function without issue, but in these javascript solutions – they can’t find it, because of the logic that child leaf nodes represent words, when they may very well only represent parts of words. The word Intented above is treated as a node of “i” and a node of “ntended” and by the logic the word ‘intended’ that is the actual word displayed to the user – is not to be found. But I think this is incorrect, it should be found. treating text nodes as words, may work in some cases, but I’m not convinced this is congruent with the philosophy of HTML, words can be marked up – it’s allowed in general, but it breaks these types of solutions which assume you won’t.

    It would take some logic to work around html nodes – nobody so far has taken up the challenge. :( But you find the topic brought up at various times, like the earlier poster wanting to find phrase matches that extend beyond a paragraph. Or someone who after highlighting a phrase like ‘quake’ finds they then can’t further highlight ‘earthquake’ – because the highlighter itself feels free to add HTML markup within a word. Apparently its not evil for the highlighter js to do this – only for the original page to do this :)

    Sigh – oh well – I haven’t found one that works yet. I suppose therefore, if I really want a solution, I’d have to write one, lol….I wish Apple would just expose the find functionality they have in their browser to the programmer – that’d be ideal.

  41. RockoT says

    in my previous comment the span tag was lost.

    there was a span tag around the letter “i” of intended.

  42. Alexander Alexander says

    @RockoT
    You are right, this solution is not covering all cases. But in most cases it works fine.
    To cover more complicated cases, you need to do a lot of more work, much more than can be done in a blog post. And of course if the web page itself relies on its original structure to do its own JS stuff, then nothing will work on the iOS platform, because the iOS doesn’t provide any way to access the render tree of the web engine directly, the only way is JavaScript.

  43. David says

    Works great! Thanks a lot!

  44. kamran says

    Thanks alot @Alexander

  45. Jerry pan says

    Hi Thank for your work, I develop on osx ,I want scroll to hightlight text. span.scrollIntoView() not work for me, window.scrollTo() works,But I can not get the span position.

  46. Alexander Alexander says

    @Jerry pan

    span.scrollIntoView(true) should work fine, Are you sure that “span” is a valid HTML element?

  47. Yedi says

    Thanks! Great work, helped a lot!

  48. jimmy says

    Thanks for the great tutorial. I have one problem. Your JS code apparently does not work nicely with arabic characters. In arabic (and persian) characters look different in the middle of the word or in the beginning of a word. for example و which sounds like V and W, looks like this in وحید. But it looks like this in توحید. Upon using your code, when i highlight و, it disoconnects it from its previous character and make the word look like this: ت‌وحید. Which is not the correct spelling. Is there a way to correct this issue?

  49. Alexander Alexander says

    @jimmy
    Technically the script simply searches for the search term at „character code“ level and surrounds the findings with SPAN tags. This works fine for most languages. But I guess when the characters change their shape under different contexts, I guess this can get very complicated. You need to know the language very well and probably add, remove or change certain characters at character code level depending of their context. The script here does not do this, and I do not know these languages and its rules to know how to fix this. I’m sorry.

  50. jimmy says

    Thanks alex, really appreciate you getting back to me so quickly :) I’ll try to work something out and maybe let you know the result so you can optimize the tutorial for a broader range of languages. All the best!

1 4 5 6



Some HTML is OK

or, reply to this post via trackback.