Grab selected text in Iframe and highlight it using Rangy


I’ve been recently working on some project wherein I wanted to grab the selected text and highlight it. The best approach in my opinion would be put markers around selected text and calculate their position starting from <body> tag.

Failed Attempt

In this approach, I decided to take advantage of Range Object to grab selected text and wrap it in markers in order to highlight it. This works pretty well until we select texts from different ranges. This is the code I’ve used, Notice I’m wrapping the selected text by <chucknorris> tag which acts as a marker in this case but unfortunately range.surroundContents() method does not allow to wrap texts of different ranges and returns an error.

Uncaught Error: BAD_BOUNDARYPOINTS_ERR: DOM Range Exception 1

function grabSelectedText() {
  var t = '',
  window = $('#iframe')[0].contentWindow.window,
  document = $('#iframe')[0].contentWindow.document,
  chucknorris = document.createElement("chucknorris");

  if (window.getSelection) {
    t = window.getSelection();
  } else if (document.getSelection) {
    t = document.getSelection();
  } else if (document.selection) {
    t = document.selection.createRange().text;
  }

  var range = t.getRangeAt(0).cloneRange();
  range.surroundContents(chucknorris); // Does not work for different ranges
  t.removeAllRanges();
  t.addRange(range);

  return t;
}

Range differs when selected texts belong to different HTML tags. For example, If we select and wrap Bold Italic text from <strong>Bold </strong><em>Italic</em> string, we’ll have 2 different ranges because both the words wrapped inside different tags but for individual words, it will work.
Demo (keep your Firebug console open to see errors)

Successful Attempt

After googling a lot, I’d come across Rangy – an awesome cross-browser range and selection library in Javascript. It provides a simple standards-based API for performing common DOM Range and Selection tasks in all major browsers. So, above function changes to below:

getSelectedText: function () {
  var window = $('#iframe')[0].contentWindow.window, 
    selection = rangy.getSelection($('#iframe')[0]);

  markerApplier = rangy.createCssClassApplier(null, {
    elementTagName: 'chucknorris'
  });
  markerApplier.toggleSelection(window);

  return selection; 
}

Usage

I’ve written a wrapper named Rangee on top of Rangy to simply grab and highlight the selected text.

  // For Iframe's content and Storing the selection details in localStorage
  var objSel = rangee.init('iframe', iframe.contentWindow.window || iframe.contentDocument.defaultView).grabSelection();
  selection1Storage.push(objSel);
  localStorage['selection1'] = JSON.stringify(selection1Storage);  

  // For parent window and Storing the selection details in localStorage
  var objSel = rangee.init('div', window).grabSelection();
  selection2Storage.push(objSel);
  localStorage['selection2'] = JSON.stringify(selection2Storage);

  // Highlighting previous selection (stored in localStorage)
  rangee.init('iframe', iframe.contentWindow.window || iframe.contentDocument.defaultView);
  rangee.highlight(JSON.parse(localStorage['selection1']) );
  rangee.init('div', window);
  rangee.highlight(JSON.parse(localStorage['selection2']) );

Final Demo

View Demo

References

http://code.google.com/p/rangy/
http://stackoverflow.com/q/11537032/415057

Advertisements

Writing your first application using Backbone.js


Getting Started

As we know, writing a Single Page application using just jQuery is not quite a good idea especially when our application grows bigger and we need to modularize our codebase. To resolve this problem, there are many MV* frameworks available for us.

What is Backbone.js

Backbone.js is one of the many Javascript frameworks for creating MVC-like web applications which comes with a default template engine Underscore.js. Backbone.js lets us split our code into Model, Collection and View. The general idea is to organize an interface into logical views, backed by models, each of which can be updated independently when the model changes, without having to redraw the page.

On the front-end, it’s my architectural framework of choice as it’s both mature, relatively lightweight and can be easily tested using third-party toolkits such as Jasmine or QUnit.
– Developing Backbone.js Applications by Addy Osmani.

What is MVC

In this case, each contact is our Model, all the contacts group together by Collection and View renders the contacts details. We are going to use contacts mysql table for this application so contacts table is our Collection and each row exists in the table is our Model.

Let us begin

Let’s start by writing our first Backbone.js app – Addressbook in Backbone.js. Here is our 2-column layout, one for side links on the left and another to load forms/grid.

addressbookMVC/index.html – Main Layout

<div class='table abWrapper'>
  <div class='row'>
    <div class='cell abLinks'>
      <a href='#add_new_contact'>Add New Contact</a><br />
      <a href='#list_contacts'>List all Contacts</a><br />
      <a href='#search_contacts'>Search any Contact</a><br />
    </div>
    <div class='cell abPanel'>Loading... Please Wait.</div>
  </div>
</div>

Add New Contact – Backbone.View and Backbone.Model

We’ll write our first view addView and load it inside div.abPanel. We can write our custom view by extending Backbone.View.extend() method and can keep our templates either into a same page or can load it from a separate html file. In this case, we’ll simply wrap our template in <script> tag by giving it a special type and an id:

addressbookMVC/index.html – addNewContact Template

<script type='text/template' id='addContactTemplate'>
  <h2> Add New Contact</h2>
  <form id='frmAddContact'>
    <div>Full Name:</div> <input type='text' id='full_name' class='input' /> <span class='false full_name_error'></span><br />
    <div>Email Id:</div> <input type='text' id='email' class='input' /> <span class='false email_error'></span><br />
    <div>Phone:</div> <input type='text' id='phone' class='input' /> <span class='false phone_error'></span><br />
    <div>Address:</div> <textarea id='address' class='textarea'>
    <input type='submit' value='Save Contact Details' class='button' />
    <span class='success'></span>
    <input type='hidden' id='id' class='input' />
  </form>
</script>

Backbone View allows us to specify custom methods and properties but there are some predefined attributes you should use to simplify the flow and maintain the unity between all backbone apps.
el – specify the DOM element selector where you want to load your template within a page. In our case its div.abPanel.
template – The _.template method in Underscore compiles JavaScript template into function which can be evaluated for rendering.
initialize – The constructor to keep a piece of code to be executed while creating an instance of a view.

Below is our AB.addView backbone view which compiles the template mentioned above  and load it into el. 

addressbookMVC/js/script.js – addNewContact View

AB.addView = Backbone.View.extend({
  el: 'div.abPanel',
  template: _.template($('#addContactTemplate').html()),
  initialize: function () {
    _.bindAll(this, 'addContactPage');
  },
  addContactPage: function () {
    this.$el.html(this.template());
  }
});

In above code, we have used the _.bindAll method in Underscore which fixes the loss of context for this within methods. So we have to specify all those method names wherein we’ll use this.any_method or this.any_property i.e. this.$el, this.template etc.

In backbone.js, there is no need to grab elements the jquery way and attach some events to them. There is a special attribute events which allows us to attach event listeners to either custom selectors or directly to el if no selector is provided. It takes the form {‘eventName selector’ : ‘callbackMethod’}, So we can attach the submit event to our <form> in order to prevent the default form submission and fetch the form data. And once we get the data, we can simply call model.save() method to save the record into the DB which invokes Backbone.sync function every time we attempt to read or save a model to the server by making a RESTful JSON request.

Lets write our contactModel first. Notice that if a passed JSON data to model.save() contains an id key then a method name will be update in model.sync() else it will be create.

addressbookMVC/js/script.js – Contact Model

AB.contactModel = Backbone.Model.extend({
  sync: function (method, model, options) {
    if (method === 'create' || method === 'update') {
      return $.ajax({
        dataType: 'json',
        url: '../php/addNewContact.php',
        data: {
          id: (this.get('id') || ''),
          full_name: (this.get('full_name') || ''),
          email: (this.get('email') || ''),
          phone: (this.get('phone') || ''),
          address: (this.get('address') || '')
        },
        success: function (data) {
          // put your code after the contact is saved/updated.
        }
      });
    }
  }
});

Now we’ll create an instance of above model in our view and call the save() method.

addressbookMVC/js/script.js – addNewContact View

AB.addView = Backbone.View.extend({
  events: {
    'submit form#frmAddContact': 'addContact'
  },
  addContact: function (event) {
    var full_name = $('#full_name').val(),
        email = $('#email').val(),
        phone = $('#phone').val(),
        address = $('#address').val(),
        id = $('#id').val();
        
    if (id === '') {
      var contactmodel = new AB.contactModel({
        full_name: full_name,
        email: email,
        phone: phone,
        address: address
      });
    } else {
      var contactmodel = new AB.contactModel({
        id: id,
        full_name: full_name,
        email: email,
        phone: phone,
        address: address
      });
    }
    contactmodel.save();
    return false;
  }
});

List contacts – Backbone.View & Backbone.Collection

Usually we make an AJAX request to fetch all the data in JSON format but there is a special method collection.fetch() available to fetch the default set of models for the collection from the server based on the url defined in the collection. We’ll write our collection by specifying the model and the url:

# addressbookMVC/js/script.js – Contacts Collection

AB.contactsCollection = Backbone.Collection.extend({
  model: AB.contactModel,
  url: '../php/listContacts.php'
});

And we’ll put the templates in index.html:

addressbookMVC/index.html – listContacts Template

<script type='text/template' id='listContactsTemplate'>
  <h2>List Contacts</h2>
  <table id='contactsGrid' width='100%' border='1' cellspacing='1' cellpadding='5'>
    <tr>
      <td width='25%'><b>Full Name</b></td>
      <td width='25%'><b>Email ID</b></td>
      <td width='15%'><b>Phone</b></td>
      <td width='25%'><b>Address</b></td>
      <td width='10%' align='center'><b>Action</b></td>
    </tr>
    <% if ($.isEmptyObject(contacts)) { %>
      <tr><td colspan='5'>No Record Found</td></tr>
    <% } else {
        $.each(contacts, function () { %>
          <tr data-id=<%= this.id%>>
            <td><%= this.full_name%></td>
            <td><%= this.email %></td>
            <td><%= this.phone %></td>
            <td><%= this.address %></td>
            <td align='center'><a>Edit</a> | <a>Delete</a></td>
          </tr>
    <%  });
       }
    %>
  </table>
</script>

And then call the fetch() within its View:

addressbookMVC/js/script.js – listAllContacts View

AB.listView = Backbone.View.extend({
  el: 'div.abPanel',

  template: _.template($('#listContactsTemplate').html()),

  initialize: function () {
    _.bindAll(this, 'listContactsPage', 'render');
  },

  render: function (response) {
    var self = this;

    this.$el.html(this.template({contacts: response}));
  },

  listContactsPage: function (querystring) {
    var self = this;

    AB.contactscollection.fetch({
      data: querystring,
      success: function (collection, response) {
        self.render(response);
      }
    });
  }
});

Search contacts – Backbone.View and Backbone.Collection

I will not explain this section in detail because it is pretty much similar when it comes to load a searchContacts template into a page and to show all the contacts based on the search criteria, we can reuse the same method listContactsPage({full_name: ‘alice’}) from AB.listView class. We’ll get it once we go through the codebase. Lets move on to Backbone.Router.

Backbone.Router

In Backbone, routers are used to help manage application state and for connecting URLs to application events. This is achieved using hash-tags with URL fragments, or using the browser’s pushState and History API. Some examples of routes may be seen below:
http://addressbook.com/#addNewContact
http://addressbook.com/#editContact/6
When all of our routers have been created, and all of the routes are set up properly, call Backbone.history.start() to begin monitoring hashchange events, and dispatching routes. Inside run method, we are updating the hash to #add_new_contact and triggering it immediately so that renderAddNewContactPage will be called immediately once the page is loaded.

# addressbookMVC/js/script.js – Backbone.Router

var AB = {
  run: function () {
    this.addview = new this.addView();
    this.listview = new this.listView();
    this.searchview = new this.searchView();
    this.contactscollection = new AB.contactsCollection();
    this.router = new this.Router();
    Backbone.history.start();
    this.router.navigate('add_new_contact', {trigger: true});   
  }
};

AB.Router = Backbone.Router.extend({
  routes: {
    'list_contacts': 'renderListContactsPage',
    'add_new_contact': 'renderAddNewContactPage',
    'search_contacts': 'renderSearchContactsPage',
    'edit_contact/:id': 'renderEditContactPage'
  },

  renderAddNewContactPage: function () {
    AB.addview.addContactPage();
  },

  renderListContactsPage: function () {
    AB.listview.setElement('div.abPanel');
    AB.listview.listContactsPage();
  },

  renderSearchContactsPage: function () {
    AB.searchview.searchContactsPage();
  },  

  renderEditContactPage: function (id) {
    AB.addview.addContactPage(id);
  }
});

Summary

I simply love to use Backbone.js in my projects, its amazing.

Download the source code from github

And do share your thoughts in the comment section below.

Updates:

Since a very long time, I wanted to try out Require.js in my projects and what could be the best way than to rewrite the addressBook in backbone.js and require.js again.
So here it is: addressBook in Backbone.js and Require.js

Happy Hacking 🙂

jQuery video series: The making of Address book


Hello friends,

I really enjoyed making these screen-casts for the first time and it was an awesome experience. I personally believed that to learn any new technology or library, one has to get acquainted with the basics and write a small generic application to get the hang of it. Below are the things that I’ve covered in this video series:

The Addressbook in jQuery
The Addressbook in jQuery
  • Basic Instinct 
    • Why to use jQuery and Where to get it from?
    • $ and beyond (overriding $)
    • Difference between document.ready and window.load
    • Selectors, dom traversing and events
    • Ajax and writing custom jquery function
  • Welcome to the jungle of jQuery
    • Selectors to fetch the values of form elements
    • Ajax calls to save the data (using php and mysql)
  • A list apart
    • List all the contacts in a tabular format
    • Delete contact functionality
  • Searching through
    • Search contacts by name or email

You can download the code from github.com.

References: 

P.S. There are many ways to improve the code. You can add edit-contact functionality or you can avoid unnecessary ajax calls to load forms/grids by simply keeping them in index.html and show/hide on demand instead of calling `$.load()` every time. Plus, its not always a valid choice to use only jQuery to write a Single Page app as there are lots of good MVC or MVVM frameworks available that can save your substantial amount of time and effort.

How to load PDF file into a webpage


I was recently working on a pdfViewer project wherein I wanted to render PDF file into a webpage and place some interactive elements (i.e. audio, video or a link) on top of it. During my research for existing JS libraries or plugins to simplify my task, I landed up making a categorized list of available tools which somewhat helped me to accomplish my job and also to know their approach to solve this problem on the web.

  • Having Adobe Acrobat or Built-In PDF viewer Dependencies: 
    •  pdfobject.com (http://pdfobject.com/): is an easy-to-use method for dynamically embedding PDF files into HTML documents. It uses JavaScript to generate and inject a standards friendly <object> element into your HTML file.
    • Using <object> or <iframe> tags : You can simply load any PDF file into an iframe or object tag which will be rendered by the PDF viewer plugin installed in your browser. It simply obviates the need of using pdfobject.js library.
  • Implementing custom PDF viewer in JavaScript:
    • HTML5 version of Flexpaper (http://flexpaper.devaldi.com/demo/) : For this to work, you have to convert entire PDF file into images using `convert` imageMagick tool (http://goo.gl/m8S9B) in Linux and rest of the things can be taken care by the viewer itself. In addition to this, it also supports custom zooming, text searching and selection, hand tool, layout options etc but unfortunately its paid.
    • Mozilla pdf.js (https://github.com/mozilla/pdf.js) : I’m really excited about this but for now it fails to render complicated PDFs (including complex tabular data). I hope that this project one day would replace the native or Adobe PDF plugin in the browser.
    • Trapeza (http://trapeze.xyrka.com/) : Faces problem while rendering complex PDFs like pdf.js.
  • For publicly available PDFs: 
    • Google Docs Viewer (https://docs.google.com/viewer) :  It’s a perfect choice (it is free) only If you have your PDFs publicly available and you believe that Google does no evil :-P.

Unfortunately, none of the above worked for me because rendering a PDF file into a webpage was not the only requirement I had. Iframe also works differently in different browsers which means Safari does not allow you to overlay other html elements on top of it but chrome does though they share the same rendering engine (webkit). And luckily bgIframe shim JavaScript plugin helps you get rid of z-index issues in IE.

Finally, ping me If something is missing in the list.  Sigh!