Highlight text in WebView with Long Tap

cbanks

Active Member
Licensed User
Longtime User
I've made an app that amounts to a book reader. What code would be necessary to allow someone to long tap on a paragraph and it would highlight that paragraph? Any time they go to that paragraph it would be highlighted. If they long tap it again then the highlight will remove. Thanks.
 

warwound

Expert
Licensed User
Longtime User
Hi.

Take a look at this page using the Android browser or load it in a WebView:

Toggle highlight HTML element on click

A click event listener detects a click and changes the CSS class of the clicked element - unless the document body is clicked in which case it does nothing.

B4X:
<style type="text/css">
.highlight{
   background-color: black;
   color: white;
}
</style>
<script type="text/javascript">
function createListener(){
   document.addEventListener('click', function(event){
      var element=event.target;
      if(element!==document.body){
         if(element.className===''){
            element.className='highlight';
         } else {
            element.className='';
         }
      }
   }, false);
}
</script>

Using the click event is far simpler than using the newer 'touchstart', 'touchmove' and 'touchend' events.

If you want the element to highlight on a long touch you have to detect touchstart, and get the time of that touchstart event.
Then decide how much time should be considered to a be a long touch.
After that period of time if a touchend event hasn't fired you can treat the touchstart as a long touch.

Not impossible but far from as simple as the single click listener.

Martin.
 
Upvote 0

cbanks

Active Member
Licensed User
Longtime User
That's awesome. Thank you.

How would I then know what paragraph was highlighted so that the next time they load that page I could make sure that paragraph was still highlighted? It seems like I'd have to know what paragraph they clicked on and then save that info to a file and then pull from that file when that page is loaded again.
 
Last edited:
Upvote 0

warwound

Expert
Licensed User
Longtime User
I guess you'd need to get the HTML from the loaded page and save it to the file system.

That HTML would contain the added CSS class names.

Then when your user next loads the page, check if a cached version of the file exists - if a cached version exists then load that else load the original.

You can get the HTML that is loaded by using my WebViewExtras library.
An example can be found here: http://www.b4x.com/forum/basic4andr.../9400-save-webview-html-file-2.html#post56406

Note that that example uses the older JSInterface library - you can use the same code with WebViewExtras but need an extra parameter in the javascript that uses CallSub.

Change:

B4X:
jsStatement="B4A.CallSub('processHTML', document.documentElement.outerHTML)"

To:

B4X:
jsStatement="B4A.CallSub('processHTML', true, document.documentElement.outerHTML)"


See the WebViewExtras thread for documentation of this callUIThread parameter.

Martin.
 
Upvote 0

warwound

Expert
Licensed User
Longtime User
Hmmm looks like the post you made yesterday at 10:40PM has been deleted...

I'll post this anyway.

B4X:
'Activity module
Sub Process_Globals
   'These global variables will be declared once when the application starts.
   'These variables can be accessed from all modules.

End Sub

Sub Globals
   'These global variables will be redeclared each time the activity is created.
   'These variables can only be accessed from this module.

   Dim Domain, ModifiedWebPage, WebPageFilename As String
   Dim WebView1 As WebView
   Dim WebViewExtras1 As WebViewExtras
End Sub

Sub Activity_Create(FirstTime As Boolean)
   Domain="http://code.martinpearman.co.uk/deleteme/cbanks/"
   ModifiedWebPage=""
   WebPageFilename="highlight_on_click.php"
   
   WebView1.Initialize("WebView1")
   WebViewExtras1.addJavascriptInterface(WebView1, "B4A")
   
   Activity.AddView(WebView1, 0, 0, 100%x, 100%y)
   
End Sub

Sub Activity_Resume
   If File.Exists(File.DirInternalCache, WebPageFilename) Then
      '   GetText assumes the file is encoded using UTF8
      '   the cached webpage will contain no ad code and requires no 'ads' key/value
      WebView1.LoadHtml(File.GetText(File.DirInternalCache, WebPageFilename))
      File.Delete(File.DirInternalCache, WebPageFilename)
      Log("Cached webpage loaded")
   Else
      WebView1.LoadUrl(Domain&WebPageFilename&"?ads=no")
      Log("webpage loaded from internet")
   End If
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   If ModifiedWebPage="" Then
      Log("The webpage contains no highlighted elements so there is no need to save it")
   Else
      Log("The webpage has been modified - cache it to the filesystem")
      File.WriteString(File.DirInternalCache, WebPageFilename, ModifiedWebPage)
   End If
End Sub

Sub SaveHtml(Html As String)
   '   this Sub is called only by the WebView1 click event listener
   '   Html will be either the entire modified webpage html or (if the webpage is NOT modified) an empty String
   ModifiedWebPage=Html
End Sub

I've updated the webpage's click event listener:

B4X:
function createListener(){
   document.addEventListener('click', function(event){
      var element=event.target;
      var tagName=element.tagName.toLowerCase();
      if(tagName!=='html' && tagName!=='body'){
         if(element.className===''){
            element.className='highlight';
         } else {
            element.className='';
         }
      }
      var modifiedElements=document.getElementsByClassName('highlight'), text;
      if(modifiedElements.length>0){
         //   some elements have been highlighted so send the entire modofied page to a B4A Sub SaveHtml
         text=document.documentElement.outerHTML;
      } else {
         //   the page currently contains no highlighted elements send an empty string to the B4A Sub SaveHtml to indicate there is no need to save the page
         text='';
      }
      B4A.CallSub('SaveHtml', true, text);
   }, false);
}

Each time the click event listener modifies the page it sends a String to the B4A Sub SaveHtml.
That String is empty if the page is currently NOT modified otherwise the String is the entire modified page.

The String is stored in a global variable named ModifiedWebPage.

When your activity is paused the String is written to the cache - if the String is not empty.

And when your activity is resumed the code checks for a cached page and loads that cached page if it exists, otherwise it loads the original page from the internet.

If you try it on an emulator you may notice it doesn't always work properly - that is a known bug with the emulator on orientation change: Android emulator bug while changing orientation | Vikas Patel's Blog.

On a real device it works perfectly.

Now i'm wondering if you want to hardcode that javascript into all of your pages?

You could use a method similar to your other thread - put the javascript into a PHP include file and include that file only if the page is being viewed within your B4A application.

If you add that javascript to a page that is not being viewed within your B4A application - a page viewed with a desktop browser - then it will generate an error each time the user clicks the page.

The page will be trying to execute B4A.CallSub('SaveHtml', true, text); and that function is only part of your B4A WebView as it's been added with the javascript interface.

Martin.
 

Attachments

  • WebViewStuff.zip
    5.9 KB · Views: 434
Upvote 0

cbanks

Active Member
Licensed User
Longtime User
How do I obtain the Domain and WebPageFilename without statically adding them like in the code?

Also, if I have this in my app: myInterface.addJSInterface(WebView1, "B4A") then I get this error: Sub SaveHtml signature does not match expected signature when I click on a paragraph to highlight it.

If I take that code out and just have this in my app: WebViewExtras1.addJavascriptInterface(WebView1,"B4A") then when I tap on a paragraph it doesn't highlight.
 
Last edited:
Upvote 0

warwound

Expert
Licensed User
Longtime User
Hi.

Look at the updated code - i've removed the PHP no ads code etc.

B4X:
'Activity module
Sub Process_Globals
   'These global variables will be declared once when the application starts.
   'These variables can be accessed from all modules.

End Sub

Sub Globals
   'These global variables will be redeclared each time the activity is created.
   'These variables can only be accessed from this module.

   Dim ModifiedWebPage, WebPageFilename As String
   Dim WebView1 As WebView
   Dim WebViewExtras1 As WebViewExtras
End Sub

Sub Activity_Create(FirstTime As Boolean)
   ModifiedWebPage=""
   WebPageFilename="my_webpage.htm"
   
   WebView1.Initialize("WebView1")
   WebViewExtras1.addJavascriptInterface(WebView1, "B4A")
   
   Activity.AddView(WebView1, 0, 0, 100%x, 100%y)
   
End Sub

Sub Activity_Resume
   If File.Exists(File.DirInternalCache, WebPageFilename) Then
      '   GetText assumes the file is encoded using UTF8
      '   the cached webpage will contain no ad code and requires no 'ads' key/value
      WebView1.LoadHtml(File.GetText(File.DirInternalCache, WebPageFilename))
      File.Delete(File.DirInternalCache, WebPageFilename)
      Log("Modified webpage loaded")
   Else
      WebView1.LoadUrl("file:///android_asset/"&WebPageFilename)
      Log("Original webpage loaded")
   End If
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   If ModifiedWebPage="" Then
      Log("The webpage contains no highlighted elements so there is no need to save it")
   Else
      Log("The webpage has been modified - cache it to the filesystem")
      File.WriteString(File.DirInternalCache, WebPageFilename, ModifiedWebPage)
   End If
End Sub

Sub SaveHtml(Html As String)
   '   this Sub is called only by the WebView1 click event listener
   '   Html will be either the entire modified webpage html or (if the webpage is NOT modified) an empty String
   ModifiedWebPage=Html
End Sub

The webpage is now in the android assets folder.

So the code has just the filename of the webpage you wish to load.
It checks for a cached/modified version of the page and loads that if it exists.
Otherwise it loads the original unmodified webpage.

When a page has been modified and needs to be saved it is saved with it's original filename in the activity's cache folder.
You might want to change that to File.DirInternal.

Files in File.DirInternalCache can be deleted at any time by the OS if the OS needs to reclaim memory space for other activities.

Reading your last post it looks as though you have bits of JSInterface and bits of WebViewExras code mixed up.

Get rid of the older JSInterface code such as addJSInterface, and stick with WebViewExras addJavascriptInterface.

Martin.
 

Attachments

  • cbanks_20120201.zip
    8.3 KB · Views: 374
Upvote 0

cbanks

Active Member
Licensed User
Longtime User
Ok, now it seems to be working properly. I removed the older JSInterface code. Thanks for your help!

But, I want it to load a cached page any time a page is loaded and it has had a paragraph highlighted previously, not just when Resuming. Where would I put the code for that? In the overrideUrl sub? What would it look like? I probably would need to save the url beforehand and then retrieve url? I can't think of a better name for that cache file variable.

Also, I don't want it to highlight when user clicks on a link on a page, because it does that right now.
 
Last edited:
Upvote 0

cbanks

Active Member
Licensed User
Longtime User
The following code is causing a problem because in my web pages I also have <a name> tags at the beginning of each paragraph for quicker navigation. Is there a way to modify the code so that it lets me highlight paragraphs with <a name> tags in them, but not highlight <a href> links? Thanks.

function createListener(){
document.addEventListener('click', function(event){
var element=event.target;
var tagName=element.tagName.toLowerCase();
if(tagName!=='html' && tagName!=='body' && tagName!=='a'){
if(element.className===''){
element.className='highlight';
} else {
element.className='';
}
}
var modifiedElements=document.getElementsByClassName('highlight'), text;
if(modifiedElements.length>0){
// some elements have been highlighted so send the entire modofied page to a B4A Sub SaveHtml
text=document.documentElement.outerHTML;
} else {
// the page currently contains no highlighted elements send an empty string to the B4A Sub SaveHtml to indicate there is no need to save the page
text='';
}
B4A.CallSub('SaveHtml', true, text);
}, false);
}
 
Upvote 0

warwound

Expert
Licensed User
Longtime User
Hi.

Try this:

B4X:
function createListener(){
   //   code to add the highlight on click event listener
   document.addEventListener('click', function(event){
      var element=event.target;
      var tagName=element.tagName.toLowerCase();
      //   detect HTML tags that should NOT be highlighted
      if(tagName!=='html' && tagName!=='body' && (tagName!=='a' || element.name!=='')){
         if(element.className===''){
            element.className='highlight';
         } else {
            element.className='';
         }
      }
      
      var modifiedElements=document.getElementsByClassName('highlight'), text;
      if(modifiedElements.length>0){
         //   some elements have been highlighted so send the entire modofied page to a B4A Sub SaveHtml
         text=document.documentElement.outerHTML;
      } else {
         //   the page currently contains no highlighted elements send an empty string to the B4A Sub SaveHtml to indicate there is no need to save the page
         text='';
      }
      B4A.CallSub('SaveHtml', true, text);
   }, false);
}

Martin.
 
Upvote 0

cbanks

Active Member
Licensed User
Longtime User
That works, thanks!

I just noticed that the javascript/savehtml code saves the contents of the html file just fine if I save to the sdcard, but it does not save the file to File.DirInternal. Any ideas?

B4X:
Sub SaveHtml(Html As String)
    ModifiedWebPage=Html
   If File.ExternalWritable = False Then
      File.WriteString(File.DirInternal, "test.html", ModifiedWebPage)
   Else
         File.WriteString(File.DirDefaultExternal, "test.html", ModifiedWebPage)
   End If
End Sub




Also, how would I allow the user to choose the highlight color? Right now there is just one option:

<style type="text/css">
.highlight{
background-color: black;
color: white;
}
</style>
 
Last edited:
Upvote 0

warwound

Expert
Licensed User
Longtime User
Hi.

Are you sure that 'test.html' does not exist?
Or is 'Html' an empty String and when you load it the WebView displays nothing?

Try this:

B4X:
Sub SaveHtml(Html As String)
    ModifiedWebPage=Html
    If File.ExternalWritable = False Then
        File.WriteString(File.DirInternal, "test.html", ModifiedWebPage)
        Log(File.Exists(File.DirInternal, "test.html"))
    Else
        File.WriteString(File.DirDefaultExternal, "test.html", ModifiedWebPage)
    End If
End Sub

Does that log True or False?

I think if you want to allow your users to choose their own highlight style then you will need to have some sort of 'user settings' screen and when a user chooses 'background-color' or 'color' CSS styles you will have to create a CSS file on the fly.

So in your webpages replace this:

B4X:
<style type="text/css">
.highlight{
background-color: black;
color: white;
}
</style>

with this:

B4X:
<link rel="stylesheet" href="pathto/style.css" />

Then create and modify the file 'style.css' whenever the user settings change.

The contents of style.css should be the same as the contents of the html page 'style' tag:

B4X:
.highlight {
  background-color: black;
  color: white;
}

If you're unfamiliar with CSS then there's a good tutorial here: CSS Tutorial

Martin.
 
Upvote 0

cbanks

Active Member
Licensed User
Longtime User
warwound: the "highlight" code you gave in this thread has been working great. I've come across one problem that I'm wondering if you know a solution for? If I tap on a paragraph to highlight it, it works fine, unless there is html code somewhere in the middle of the paragraph, then it only highlights up to that code. I still want it to highlight the entire paragraph. For example, if there is a <sup> tag for a word or if a word in the paragraph is linked to somewhere else with an <a href> tag, then the highlight stops when it gets to that tag. Any way to modify the javascript code to fix this? Thanks.
 
Upvote 0

warwound

Expert
Licensed User
Longtime User
Hi again.

Can you upload a sample HTML page that contains the elements you're talking about?

I'll probably have to get the clicked element then recursively find it's parent element(s) until the block level parent <p> or <div> is found.
Then the highlight can be applied to the block parent element.

Martin.
 
Upvote 0

warwound

Expert
Licensed User
Longtime User
Hi - sorry for the delay in replying, been busy!

Take a look at this page: 1 Nephi 1, does that work as you want it to?

I updated the javascript so only HTML 'p' elements get highlighted.
If an element which is not a 'p' element is clicked then the javascript searches for the clicked element's first parent 'p' element and highlights that.

If the clicked element has no parent 'p' element then nothing is highlighted.

I had to modifiy the page linked to above to make it work - needed to add your CSS styles inline and needed to comment out the javascript call to CallSub.

The attached file should be exactly what you need.

Martin.
 

Attachments

  • new_highligh_code.zip
    4 KB · Views: 384
Upvote 0

cbanks

Active Member
Licensed User
Longtime User
That works great, thanks!

To take it the final step, I want my users to be able to not only select a paragraph by tapping on it, but I also want them to be able to highlight a single word or whatever words they drag over. Is there a way to accomplish that? I guess the highlighting couldn't happen as soon as something is tapped. It would have to wait until the word/words/paragraph is selected & then highlight when the user is done somehow.
 
Last edited:
Upvote 0
Top