Social Javascript (cross-site ajax)

Filed: Sun, Aug 16 2009 under Programming|| Tags: ajax xdomainrequest domain social

Introduction

It’s been a while since I’ve written a blog post, so go easy on me.

A friend of mine is writing a social bookmark service, mostly as an educational exercise but then google started out the same way. The problem with a bookmark site is that it’s pretty useless without browser integration. To use the service a user would have to copy the url into his copy and paste buffer, go to the bookmark site and then paste the url in. It’s easy enough but it’s not easy enough to encourage widespread adoption.

If you’ve been following this blog then you realize the answer is to use a bookmarklet, and if you’ve been following the browser advances then you know that modern browsers allow for cross-domain AJAX (with, as usual, IE off to the side marching in retarded circles). So all the ingredients for a social javascript application is in place, and I finally had an interesting topic for a new article.

To get my friend started I wrote a generic framework that would be very easy to write and extend to his own purposes. I start off with a small bookmarklet which is a small javascript function stuffed into a link. This link (when bookmarked, or preferably placed on the browser’s bookmark bar), when clicked will insert a div at the top of the page, attach a style sheet, and use ajax to get some html to populate the new div. With a few exceptions this bookmark will work on ANY page on the net.

The exceptions are:

  • I block execution of this script on secure (https://) pages. I consider the fact that javascript bookmarks can execute on a secure page, a security hole in the https:// security models.
  • The script won’t work on pages that use <framesets>. It’s possible to insert a frame into a frameset but it would triple the size of the application and was more trouble than it was worth.
  • The script looks for either a javascript variable named noRemoteSidebars or a division with an id of noRemoteSidebars and if either exists (IE the page owner doesn’t want these sort of applications running), the script aborts.

This framework isn’t just for bookmarking sites, it’s a great kickstart for every cross-site social application you can think of. And as always, the code that makes it all work has been released to the public domain so you’re free to use it as you please without restrictions (but if you do make it big, I’m not above taking a tip!).

An example

You'll need a modern 2009 browser to use this example. To see this example in action simply drag the link below to your bookmark bar (or right click on it and bookmark it). IE users will need to right click and add it to their favorites (preferably the favorites bar). Once you've bookmarked the link you can visit any site on the net and see it in action.

Social Javascript Example

Setting up the server

Cross-domain ajax is a new wrinkle, really only becoming viable with the 2008 suite of browsers. They all work on an “opt-in” model which means your web-server has to opt-in to serving cross-site ajax. Really this doesn’t make much sense to me since images, iframes, css, and javascript could all pull cross-site without jumping through any hoops, but we play with the hand we’re given so to enable ajax to work you need to modify your webserver to serve the following header:

Access-Control-Allow-Origin *

You can modify your .htacces file thusly:

header add Access-Control-Allow-Origin *

or you can have your php set the header when your ajax script is called:

header(“Access-Control-Allow-Origin *”);

Basically this says to the browser, I don’t care where the user is on the net, they can access this page. If you replace * with a domain name you can restrict this application to only working on specific pages.

Once this is done you’re ready to start coding.

The Bookmarklet

The next step is to give the user a bookmark to run the script. To do this insert the following code anywhere on an existing HTML page (or a page you create).

http://www.hunlock.com/tmp/test.html
<a href='javascript:(
   function() {
      try {
         jsRemoteSidebar.toggle();
      } catch (err) {
         var s=document.createElement("script");
             s.src="http://hunlock.com/tmp/test.js";
         document.body.appendChild(s);
      }
   })();'>My Remote Sidebar</a>

This is pretty straight forward. When the user clicks on the link it attempts to call jsRemoteSidebar.toggle() and if it doesn’t exist it appends the remote javascript (which creates jsRemoteSidebar). To modify this you’ll need to replace http://hunlock.com/tmp/test.js with your own remote javascript URL (which we’ll create in the next block).

Since only modern browsers can use this you might want to dynamically show this link only if the user is running IE8, firefox 3.5, chrome, or a modern version of safari. In all browsers save IE the user can simply drag the link to the bookmark bar, IE users have to right click and add to favorites (and they’ll get a security warning – which is a good thing) and can save it in their favorites bar.

Once the link is on the bookmarks bar it can be easily used by the user on pretty much any page on the net.

Setting up the remote script

Next we need to create the javascript the bookmarklet actually calls. (This is what the URL in the bookmarklet above will call).

http://www.hunlock.com/tmp/test.js
var jsRemoteSidebar = function() {

  // This block, until the return statement, is private
  
  // Conventions: _ indicates an out of scope variable
  //              UPPER_CASE indicates a constant (or flag), also maybe out of scope.
  
  // "constants"
  var HTML_URL = "http://www.hunlock.com/tmp/data.html";
  var CSS_URL = "http://www.hunlock.com/tmp/test.css";
  var FRAME_HEIGHT = 150;   // in pixels
  var SLIDE_TIMING = 10;    // in miliseconds
  
  // scratch variables, do not modify or remove please.
  var _remoteDiv = document.getElementById("jsSidebar90210");
  
  if (document.getElementsByTagName('frameset').length) {
    // disallow use on pages constructed with framesets.
    // it's possible to insert a new frame and work inside that
    // but that will be version 2.0 of the script (if ever)
    alert('This service will not work on framed pages');
    return undefined;
  }
  
  if (window.noRemoteSidebars||document.getElementById('noRemoteSidebars')) {
    // Check to see if the remote site has disallowed remote sidebars
    // This is my convention, chances are this code will never be tripped.
    alert('This site has disallowed remote services.');
    return undefined;
  }
  
  if (/^https/i.test(document.location.href)) {
    // If you remove this block, your script WILL work on encrypted sites
    // but it really shouldn't.  Reading secured sites is also a legal liability.
    // if you take data from an encrypted page even with angelic intentions and
    // it gets out you could get in trouble, maybe a lot.  So leave this check in.
    // Besides IE8 doesn't allow cross domain ajax requests from https sites.
    //    (the only thing they seemed to have gotten right)
  
    alert('This service will not work on encrypted sites.');
    return undefined;
  }
  
  ajaxObject = function (url, callbackFunction) {
    // see http://www.hunlock.com/blogs/The_Ultimate_Ajax_Object
    // for documentation on this ajax object
    // Modified slightly to use IE8's xdomain scripting.  !@#$ IE
    var that=this;
    that.isIE8 = false;
    this.updating = false;
    this.abort = function() {
      if (that.updating) {
        that.updating=false;
        that.AJAX.abort();
        that.AJAX=null;
      }
    }
    this.update = function(passData,postMethod) {
      if (that.updating) { return false; }
      that.AJAX = null;
      if (window.XDomainRequest) {
        that.AJAX = new XDomainRequest();
        that.isIE8 = true;
      } else {
        if (window.XMLHttpRequest) {
          that.AJAX=new XMLHttpRequest();
        } else {
          // This probably won't work but maybe MS will hotfix
          // their browser to actually work.
          that.AJAX=new ActiveXObject("Microsoft.XMLHTTP");
        }
      }
      if (that.AJAX==null) {
        return false;
      } else {
        that.AJAX.onreadystatechange = function() {
          if (that.AJAX.readyState==4) {
            that.updating=false;
            that.callback(that.AJAX.responseText,that.AJAX.status,that.AJAX.responseXML);
            that.AJAX=null;
          }
        }
        // For IE8
        if (that.isIE8) {
          that.AJAX.onload = function() {
            that.updating=false;
            that.callback(that.AJAX.responseText);
            that.AJAX=null;
          }
        }
        that.updating = new Date();
        if (/post/i.test(postMethod)) {
          var uri=urlCall+'?'+that.updating.getTime();
          that.AJAX.open("POST", uri, true);
          that.AJAX.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
          that.AJAX.setRequestHeader("Content-Length", passData.length);
          that.AJAX.send(passData);
        } else {
          var uri=urlCall+'?'+passData+'&timestamp='+(that.updating.getTime());
          that.AJAX.open("GET", uri, true);
          that.AJAX.send(null);
        }
        return true;
      }
    }
    var urlCall = url;
    this.callback = callbackFunction || function () { };
  }
  
  var init = function () {
  
    // The constructor method
  
    if (!_remoteDiv) {
  
      //Load the style sheet
  
      var cssNode = document.createElement('link');
      cssNode.type = 'text/css';
      cssNode.rel = 'stylesheet';
      cssNode.href = CSS_URL;
      cssNode.media = 'screen';
      document.body.appendChild(cssNode);
  
      // Make our division
  
      var divNode=document.createElement("div");
      divNode.id="jsSidebar90210";
      divNode.style.height="0px";
      document.body.insertBefore(divNode,document.body.firstChild);
      // why do another getElementById?
      _remoteDiv = divNode;
  
      // Get our HTML to fill out the division
      ajaxGetData = new ajaxObject(HTML_URL);
      ajaxGetData.callback = function(remoteData) {
        _remoteDiv.innerHTML = remoteData;
        jsRemoteSidebar.scroller();
      }
      ajaxGetData.update();
    }
  
  }
  
  init();
  
  // Return a public interface.
  
  return {
    toggle : function () {
  
      // Show and hide the remote div.
  
      if (parseInt(_remoteDiv.style.height)) {
        jsRemoteSidebar.scroller('up');
      } else {
        jsRemoteSidebar.scroller('down');
      }
    },
    scroller : function (dir) {
  
      // The code that actually scrolls the div up and down
  
      currHeight =  parseInt(_remoteDiv.style.height);
      if (dir=='up') {
        if (currHeight > 0) {
          currHeight-=SLIDE_TIMING;
          _remoteDiv.style.height=(currHeight<0) ? "0px" : currHeight+"px";
          setTimeout("jsRemoteSidebar.scroller('up')", 5);
        } else {
          _remoteDiv.style.display='none';
        }
      } else {
        _remoteDiv.style.display='block';
        if (currHeight < FRAME_HEIGHT) {
          currHeight+=SLIDE_TIMING;
          _remoteDiv.style.height=currHeight+"px";
          setTimeout("jsRemoteSidebar.scroller()", 5);
        } else {
          _remoteDiv.focus();  // ensure there are no text cursors in the background
        }
      }
    }
  }

}();

// note the trailing (), it tells the function to run (but you knew that already!)

This is a fairly large code block but the only thing you’ll have to change are the constants block at the top of the page.

  • HTML_URL is the url to call to populate the new division. I’m using a static page of HTML but you can easily have this call a script which generates the html.
  • CSS_URL is the url to call to set the stylesheet of the new division.
  • FRAME_HEIGHT is the height of the new division in pixels.
  • SLIDE_TIMING is how fast the new division scrolls down or up (higher=faster).

When the script is attached the rest of the script will have access to three things. jsRemoteSidebar.toggle() which will toggle the visibility of the new division. jsRemoteSidebar.scroller(dir) where if dir==’up’ it will hide the division and if its anything else it will display the division. And it will have access to the new division with an id of ‘jsSidebar90210’. Another thing your script will have access to is pretty much every single element on the page where the user clicked the bookmarket. Which means you can add your own code to the bottom of the page to scrape the dom and look for items of interest for your application.

If you modify or append this script it’s important you remember this script becomes part of a page with completely unknown qualities. Great care was taken in the script above to keep the variable and method names private so as not to collide with any javascript it may become a part of. When all is said and done only one javascript object, one stylesheet, and one container division is inserted into the visible DOM. If you don’t take extreme care with variable and method names you could break the functionality of the page you are on, or it could break your script!

Setting up the remote HTML

This example script is just using ajax to get a static text file off the server and populate the new division. For something as simple as this I could have just used innerHTML to populate the division but I wanted to keep my options open to serve dynamic html.

This is the code which will populate the interior of the new division:

http://www.hunlock.com/tmp/data.html
<br/>
<center><h1>It really works!</h1></center>
<br/>

Setting up the remote stylesheet

I attach a remote stylesheet for the new division because the division we created and inserted has inherited all the styles and properties of the page it was on. This is actually a pretty bad thing from our perspective. It means we have to go out of our way to ensure that every style for our elements is accounted for or it’s going to inherit an unpredictable style from the page above it. This is just way too much info to define in javascript so it was just easier to make a remote stylesheet, and even then I did just the bare necessities. If you want to be pro about this you need to make sure this stylesheet specifically defines EVERYTHING from font-size to text-decorations. But this stylesheet is enough to get you started.

Remember to contain all your styles to #jsSidebar90210 or you’re going to interfere with the page the script is running on.

http://www.hunlock.com/tmp/test.css
/* every element of your page needs to be defined here 
   or you risk inheriting some random styles from whatever
   page the user happens to be on */


#jsSidebar90210 {
		position: fixed;
		left: 0px;
		top: 0px;
		background-color: white;
		width: 100%;
		overflow: hidden;
		border: none;
		border-bottom: 5px solid black;
		z-index: 256;
		display: none;
}

#jsSidebar90210 h1 {
	color: blue;
}

Conclusion

And there you have it, the framework for social javascript. With this as a foundation you can easily implement a social bookmark site, a place for users to comment on the pages they visit, or whatever you can imagine.

Version 2.0 of this script might implement a frameset version (inserting a frame on the page instead of a division would eliminate most of the stylesheet concerns), but inserting a frameset into a page vastly complicates the script. If stylesheets prove to be too problematical a better solution to inserting a div into the frame would be to insert an iframe, however an iframe will be subject to the browser’s internal security models regarding javascript so extra care will be needed.