Writing User Scripts: The Benefit of Reusable Code

Written by sizzlemctwizzle — Last update Aug 19, 2013

12 points

So you want to change a web page?

The basic purpose of User Scripts are that they can be installed in your browser to change the look and behavior of a website. Having wrote my fair share of user scripts I've decided that it was about time I gave the community back some of the knowledge I've gathered. Perhaps I can pass along some techniques that can make learning to write user scripts more painless. I'm mainly going to focus on techniques that are very important to user scripting and that you aren't likely to find somewhere else that deals with user scripts(meaning I'm gonna exclude GM api functions and simple javascript since there is already plenty of documentation in those areas). You should read about User Script meta data and GM api functions before you read this guide.

First and foremost, as is the case with all programming, reusable code is the key to happiness. You might think that what you want to do a web page is unique, and it is, to a point. You want to change a page in specific way, but the means to change that page are general. Libraries are great. I've been known to bash the use of JQuery in user scripts, but that is only because I think it is too bloated for use within a user script. Helper function or code snippets are functions that can be used to abstract your code. I'll share some of my own personal favorites in this guide(one's I've used most often), but it doesn't really matter which one's you use. They all accomplish the same goal of letting you write more abstract code.

Getting an Element

One common task of user scripting is to get an element on the page in order to do something with it. Most beginners who lack knowledge of the Document Object Model assume that you have to parse(with complex regular expressions) the source of the page to get information off it. This is far from the case. There are special DOM functions that allow us access to elements on the page. The most common of which is document.getElementById. You can use this to get an element that has a unique id attribute. It is convenient if the element you wish to get has a unique id, but unfortunately this is rarely the case. Poor script writers have been know to use other DOM methods to get a huge list of elements and then preform tests on them to make sure they have the elements they want. A much better method is to use XPath.

Hands down the most powerful tool of user scripting is XPath. I'm not going to sit here and teach you XPath expression. Here is an okay guide for some simple expressions and here are some useful XPath functions. Assuming you can come up with an expression for the element(or elements) you want, you can use this function to simplify things:
function $x(x, t, r) {
    if (t && t.tagName) 
        var h = r, r = t, t = h;    
    var d = r ? r.ownerDocument || r : r = document, p;
    switch (t) {
    case XPathResult.NUMBER_TYPE:
        p = 'numberValue';
        break;
    case XPathResult.STRING_TYPE:
        p = 'stringValue';
        break;
    case XPathResult.BOOLEAN_TYPE:
        p = 'booleanValue';
        break;
    case XPathResult.ANY_UNORDERED_NODE_TYPE: 
    case XPathResult.FIRST_ORDERED_NODE_TYPE:
        p = 'singleNodeValue';
        break;
    default:
        return d.evaluate(x, r, null, t || XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    }
    return d.evaluate(x, r, null, t, null)[p];
}
// Optional shortcut functions I like
function $x1(x, r) { return $x(x, XPathResult.FIRST_ORDERED_NODE_TYPE , r) } 
function $xb(x, r) { return $x(x, XPathResult.BOOLEAN_TYPE, r) }

// Example usage
// $x('//div[@class="username"]', 6)
// $x('//div[@class="header"]', 9); // get the first result of this expression
The first argument is the expression, the second is the result type(defaults to snapshot type since its the most common), and the last is an optional node for relative expressions(make sure to put a '.' at the beginning of a relative expression). Mozilla has a really nice look-up-table for the different result types.

Looping Through Elements

Another common task of a user script involves looping through the elements gotten through DOM methods(usually the result of an XPath expression). You could mess around with all the iteration yourself, but here's a function that simplifies looping with elements:
function forEach(lst, cb) {
    if(!lst) 
        return;
    if (lst.snapshotItem)
        for (var i = 0, len = lst.snapshotLength, 
                 snp = lst.snapshotItem; i < len; ++i)
            cb(snp(i), i, lst);
    else if (lst.iterateNext) {
        var item, next = lst.iterateNext;
        while (item = next()) 
            cb(item, lst);
    } else if (typeof lst.length != 'undefined') 
        for (var i = 0, len = lst.length; i < len; ++i)
            cb(lst[i], i, lst);
    else if (typeof lst == "object")
        for (var i in lst) 
            cb(lst[i], i, lst);
}

// Example usage
// Loop through all divs with class = username and alert the textContent of each node
forEach($x('//div[@class="username"]'), function(username) {
	alert(username.textContent);
});

Creating New Elements

Creating elements is a real pain in JavaScript. The is the reason most script writers set innerHTML directly, however this is just as ugly and inefficient as calling document.createElement fifty times. Here is a function to create elements easier and cleaner:
function create() {
    switch(arguments.length) {
        case 1:
            var A = document.createTextNode(arguments[0]);
	    break;
        default:
            var A = document.createElement(arguments[0]),
                B = arguments[1];
            for (var b in B) {
	        if (b.indexOf("on") == 0)
		    A.addEventListener(b.substring(2), B[b], false);
		else if (",style,accesskey,id,name,src,href,which".indexOf("," +
                         b.toLowerCase()) != -1)
		    A.setAttribute(b, B[b]);
		else
		    A[b] = B[b];
            }
            for(var i = 2, len = arguments.length; i < len; ++i)
	        A.appendChild(arguments[i]);
    }
    return A;
}

// Example Usage
document.body.appendChild(create('div', { id: 'mydiv' });
create('New Text Node');
create('div', {}); // empty div, make sure to pass empty object for second arg
create('form', { id: 'mydiv' }, create('input', { type:'text' }), create('input', { type: 'submit' }));
create('a', { textContent: 'Click Me', 
	href: '#',
	onclick: function(e) { alert('Clicked!'); e.preventDefault();  }, 
	onhover: function() { alert('Hovered!') } 
});
The first argument is node type, or text content if there is no second element. The second argument is the properties of the new element, and all further arguments are elements to be appended to the new element. That last one is an example of setting event listeners.

Posting a Form with Ajax(no page reload)

One common task a user script might preform to submitting a form via Ajax. Before I give an example to do this I'll first provide you with some simple XHR functions that can be used both to get a page and make a post.
// Same domain
function xhr(url, cb, data) {
  var res =  new XMLHttpRequest();
  res.onreadystatechange = function() { if (res.readyState==4 && res.status==200) cb(res.responseText) };
  res.open(data ? 'POST' : 'GET', url, true);
  res.setRequestHeader('User-agent', window.navigator.userAgent);
  if (data) {
    res.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    res.setRequestHeader("Connection", "close");
    res.setRequestHeader("Content-length", data.length);
  }
  res.send(data||null);
}

// Cross domain
function xXhr(url, cb, data) {
    GM_xmlhttpRequest({
          method: data ? 'POST' : 'GET',
	  url: url,
	  headers: {
	    'User-agent': window.navigator.userAgent,
            'Content-type': (data) ? 'application/x-www-form-urlencoded' : null
	  },
	  data: (data) ? data : null,
	  onload: function(res) { if (res.status == 200 && cb) cb(res.responseText); }
      });
}
First you need to get the data that needs to be submitted along with the url to submit the data. Then you just make a post like this:
xhr('http://example.com/post.php', function() { alert('success') }, "key1=value1&key2=value2");

Changing Style

There three ways to change the style of an element with Javascript.
  1. element.style.display = "none": if you have a reference to an element you can set the javascript property directly. Here is a list(its the JavaScript Name column) of the properties. One disadvantaged is that you can't change as many styles this way.
  2. element.setAtrribute('style','display: none'): can set the style attribute like you would any other attribute
  3. GM_addStyle('#element_id { display: none !important; }'): This adds a stylesheet to the page. Make sure you remember !important; or else you won't override the page style.
It may also be useful to add and remove classes to change style. Here are some class manipulation functions:
// Add a new class to an element
function addClass(el,cls) {
    if (!el.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'))) 
        el.className += " "+cls;
}

// Remove a particular class from an element
function removeClass(el,cls) {
    if (el.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'))) {
        var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');
        el.className = el.className.replace(reg,' ');
    }
}

// Example Usage
addClass(element, 'test');
removeClass(element, 'test');

Query String to JSON

It could be useful to read the query string of the page to get values known as GET parameters. Here is a function you can use:
function queryStringToJSON(string)
{
  var data = {};
  string.split(/&(?!amp;)/i).forEach(function(str) { 
      var to = str.indexOf("="); 
      data[str.substring(0,to)] = decodeURI(str.substring(to+1,str.length)); 
  }); 
  return data;
}

// Example Usage
var qlen = window.location.search;
alert(JSON.stringify(queryStringToJSON(window.location.search.slice(1,qlen))));

Cross-browser User Scripts aka Don't Be a Dick

Assuming your script doesn't need to store and retrieve data across multiple domains or make XHR requests and post across multiple domains(GM_xmlhttpRequest ), there is really no reason why you have to pigeon-hole your script to only being Greasemonkey Compatible. More and more browsers are adding support for user scripts and most of the time it isn't that much of an effort to make sure your script can appeal to a larger audience. Here are some basics things to avoid:
  • EX4: if you don't know what that is then good because only Firefox supports it. The benefit of having muti-line variables is not worth the expense of making other browser choke on your code.
  • GM_addStyle: it is so easy to include this code in your script:
    if(typeof GM_addStyle == 'undefined') 
        GM_addStyle = function(css) {
            var head = document.getElementsByTagName('head')[0], style = document.createElement('style');
            if (!head) {return}
            style.type = 'text/css';
            style.textContent = css;
            head.appendChild(style);
        }
  • unsafeWindow: yes you do need this to access page variables, but if you only need to redefine functions in the page you add your code with this:
    function addScript(js) {
        var body = document.body, script = document.createElement('script');
        if (!body) {return}
        script.type = 'text/javascript';
        script.textContent = js;
        body.appendChild(script);
    }
    otherwise if you need to get data back from the page you can use this code:
    if (typeof unsafeWindow == "undefined") 
        var unsafeWindow=window;
    This unfortunately doesn't work in Chrome so fuck 'em.
  • GM_log: A great function for testing your scripts, it outputs to the error console
    if (typeof GM_log == "undefined") 
        GM_log = (window.opera) ? opera.postError : console.log;
  • GM_registerMenuCommand: no simple alternative so either instead place a link on the page in place of it or save it for GM only features.
  • GM_openInTab: it's nice, but there no sense in breaking your script, so use this to replace it:
    if (typeof GM_openInTab == "undefined")
    	GM_openInTab = window.open;
  • @require: try to avoid it, but understand that sometimes it is needed.

Update Checking

You probably don't plan on writing a script completely, releasing it and then never adding to it. You can't assume that your users will stubble back to your script page and realize that you've updated your script. This is where update checking comes in. It is generally accepted that @require updaters are the best because they are only included if a user has Greasemonkey, which is currently the only browser where update checking is possible. Here are the main @require update checking services:

@require usoCheckup Automatic Script Updater
@require updater for User Scripts: USO Updater
@require Another Auto Updater Instructions

Here's a more in depth guide to choosing and using updaters: Script AutoUpdaters: How to provide users with updates

User Configurable Options

The only real weakness in my opinion user script have is that because they are so simple it is difficult for a script writer to make their script easily customizable for users. Having variables at the top of your script that users can change is definitely the simplest for the developer, but actually pretty difficult for most users who have no knowledge of Javascript.

A graphical settings menu is the best option for the user, but often the script writer must reinvent the wheel. I've seen countless scripts where the developer wrote their menu from scratch for that particular script. When I got around to making my own menu I decided that if I was going to go through all that effort, I was going to make something easily reusable and after a lot of work by me and Joe, and input from Izzy, GM_config was born.

For those who want to learn how to leverage GM_config, there is now a wiki.

Other Guides for Advance Techniques

  • Parsing Response HTML - sometimes parsing the response html of XHR can be too complicated for Regular Expression. This guide tells you how to get a DOM from the response so you can use ordinary DOM functions.
  • Writing Scripts For Facebook - since Facebook is a very Ajax heavy website, writing scripts for it is a little more complicated then most sites. I've wrote a little library to make it simpler.
  • Tips for script optimization - A list of techniques to make your scripts for efficient and thus faster.
  • The "?:" (Ternary) Operator - A nice techniques that can keep your code more concise.
  • Javascript Events - A clean list of events. Remember to leave out the "on" part when using addEventListener.