April 21st, 2008 - by Golgotha

Greasemonkey is a wonderful thing. I enjoy the little (or sometimes not so little) challenges posed by websites that I want to modify. One of the most difficult scripts to write for is Gmail, because of its dynamic nature and its reliance on very large helpings of obfuscated JavaScript and HTML. In addition, you can’t use Firebug to debug scripts, which is a major annoyance, so alert and GM_log become one’s best debugging friends. To make writing Greasemonkey scripts a bit easier, the chaps on the Gmail team supplied an API, which one commenter describes as “truly incredible, forward thinking”. It really is quite nice.

Yesterday I decided to add a little bit of functionality to Gmail. I wanted to be able to isolate messages from a mailing list I subscribe to that nobody had replied to. This would require marking as read “conversations” or messages containing “Re:” at the start of the subject line. The obvious place to put links to do this is along with the other “selectors” above the thread list (Figure 1).

Figure 1
New selectors added

I call them “selectors” simply because Gmail has given each “link” (actually a span) a custom selector attribute. The original markup before adding the links after “Unstarred” looks like this (it isn’t nicely indented in Gmail’s code though):

[HTML]

Select:

All,
None,
Read,
Unread,
Starred,
Unstarred

[/HTML]

So, it looks pretty simple to add a couple of commas and two more spans. And it is, but how to access the containing div is what the Gmail API makes easier. This is the complete script, and we’ll start off with the main loading business and the function that handles the adding of these two new links, addlinks:

[JavaScript]window.addEventListener(‘load’, function() {
if (unsafeWindow.gmonkey) {
unsafeWindow.gmonkey.load(‘1.0’, function(gmail) {
var tl, win = window.top.document.getElementById(‘canvas_frame’).contentWindow;
function addLinks() {
if (gmail.getActiveViewType() !== ‘tl’) return;
tl = gmail.getActiveViewElement();
var selectors = win.document.evaluate(‘//div[starts-with(text(), “Select:”)]’, tl, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
if (selectors.snapshotLength > 0) {
for (var j = 0; j < selectors.snapshotLength; j++) { var selector = selectors.snapshotItem(j); if (selector.lastChild.childNodes.length > 11) continue;
var items = {‘Conversations’: document.createElement(‘span’), ‘Replies’: document.createElement(‘span’)};
for (var i in items) {
items[i].appendChild(document.createTextNode(i));
items[i].setAttribute(‘selector’, i.toLowerCase());
selector.lastChild.appendChild(document.createTextNode(‘, ‘));
selector.lastChild.appendChild(items[i]);
items[i].addEventListener(‘click’, selectItems, false);
}
}
}
}
gmail.registerViewChangeCallback(addLinks);
addLinks();
});
}
}, false);[/JavaScript]

The first three lines load the API, which is simply a bunch of methods belonging to the gmonkey object (a global variable). Since Greasemonkey operates in its own nice and safe environment, accessing the page’s own JavaScript is done via unsafeWindow, which is in fact the window object in the actual page’s DOM.

To check that we are only applying the script when a list of messages is shown (e.g. inbox, search results), gmail.getActiveViewType() lets us limit ourselves to “tl” (thread list) mode. gmail.getActiveViewElement() gives us the main box that contains the links and form controls shown in Figure 1, as well as the list of messages (Figure 2 below). It is rather enormous markup-wise and getting to our target div would require tedious and fragile DOM-walking. It is thus sensible to use XPath, which Mozilla supports quite well. One of the nice things about writing Greasemonkey scripts is that you only have to worry about supporting one browser!

Oddly enough, XPath sometimes works with Gmail when used with the normal document.evaluate but other times it doesn’t, and this is one of those cases. I don’t know why it is so erratic. To ensure it works, we have to get a reference to the right document, located in the frame containing the markup we’re interested in. If you view-source Gmail, you will see a bare-bones HTML document with lots of embedded JavaScript and also four iframes. The one with all the content in it has the id “canvas_frame”. contentWindow is a reference to the window object in this iframe and gets us “inside” the iframe.

We need to get a list of the div elements containing the links (green boxes in Figure 2 below). Initially there are 2, because there is one above and another below the thread list (in red, Figure 2). If you search for something, Gmail creates a separate “thread list”, where there are 2 more of these divs.

Figure 2
Main content area in Gmail

The XPath expression, using tl (the thread list, in red above) as the reference node, puts the nodes it finds into a special collection, the contents of which have to be looped through and accessed using snapshotItem(). Within the loop, we check to see if the current div‘s last child (the span containing the “selectors”) has more than 11 child nodes (the original six spans within, plus the five text nodes containing comma and space). If so, we move on to the next div, because the current one already has our new links appended to it. The rest of the addLinks function is fairly standard creation of elements and appending them to the DOM, which I won’t go into.

Our new selector links are set to listen for click events, which will call the selectItems function, not shown in the code block above. Here it is:

[JavaScript]function selectItems() {
var xp = this.getAttribute(‘selector’) === ‘conversations’ ?
‘//tr/td[child::div/text()[last()][starts-with(., ” (“)]]/preceding-sibling::td//input[@type=”checkbox”]’ :
‘//tr/td[child::div/div/div/span/text()[starts-with(., “re:”) or starts-with(., “Re:”) or starts-with(., “RE:”)]]/preceding-sibling::td//input[@type=”checkbox”]’
var checkboxes = win.document.evaluate(xp, tl.getElementsByTagName(‘table’)[0], null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
var chlen = checkboxes.snapshotLength;
if (!chlen) return;
var scrol = win.pageYOffset;
win.document.body.style.position = ‘fixed’;
for (var i = 0; i < chlen; i++) { checkboxes.snapshotItem(i).click(); } win.document.body.style.position = ''; win.scroll(0,scrol); }[/JavaScript]

This was the tricky bit. First of all we use the this keyword to establish whether the “Conversations” or the “Replies” link was clicked. Use of the ternary operator (? 🙂 allows us to choose an XPath expression to target the checkboxes in rows that are conversations/replies. The XPath expressions look a little horrific, so I’ll try to explain them. The markup below, which is for a single message in the thread list, is both a conversation (between Steve and me) and a reply (since the subject line, in a span, starts with “Re:”).

[HTML]

Steve,
me (3)
» 
Re: Sale of Apple, Inc.
Thanks for your request. Unfortunately Apple is not for sale at his time. If you…

Apr 13

[/HTML]

XPath expression:

//tr/td[child::div/text()[last()][starts-with(., " (")]]/preceding-sibling::td//input[@type="checkbox"]

  1. //tr/td finds all td elements in tr elements
  2. The opening square bracket is a predicate, which means “the td elements that satisfy the condition within these square brackets”.
  3. The condition is that:
    • child::div – The td must have a child that is a div;
    • /text()[last()][starts-with(., " (")] – This div must contain text nodes: text() refers to all text nodes, and we want those that satisfy two conditions (as there are two predicates): it must be the last one, and it must start with the string ” (“. The dot within starts-with() refers to the current node.

    So far this has matched the text node containing “(3)” in the HTML.

  4. /preceding-sibling::td – This matches any preceding td to our original td
  5. //input[@type="checkbox"] – Finally, we match any ancestor (using // rather than /) that is an input element of type “checkbox”.

Hopefully you will be able to work out what the other XPath expression means by yourself! Armed with these, we can now get a collection of the checkboxes we need to check/uncheck and cycle through them, accessing each using snapshotItem.

Checking the checkboxes by setting the checked attribute to “checked” does not work, as Gmail appears to have attached event listeners to them in order to control the contents of the “More Actions” dropdown menu (see Figure 1). Since it is impossible for us to attach these event listeners ourselves (due to the obfuscation of Gmail’s code), we can turn to the click method, which emulates a user actually clicking an input element (it doesn’t work for other elements).

The problem with this is if there are any checkboxes further down the page than we can see, the page will drunkenly scroll down with each one being checked (for some reason click() is very slow). To counter this, I tried many things and finally settled on using CSS’s position:fixed on the body element. After the checkboxes are “clicked”, the window is scrolled (using scroll()) to the position it was at before.

Finally, two lines need to be added after the declaration of selectItems:

[JavaScript]gmail.registerViewChangeCallback(addLinks);
addLinks();[/JavaScript]

The first one is part of the Gmail API and simple allows the script to call addLinks whenever one switches from viewing the thread list (e.g. inbox) to “compose”, “search”, etc. The second line simply runs addLinks when the gmonkey object is first loaded.

The drawbacks of this script (which you can download and install) are that it is slow (due to click()) and that because of the use of position:fixed, an area of whiteness can appear at the top of the page if it has been scrolled. Unfortunately, given the circumstances, there is not much that can be done about this, unless you can suggest something!

I hope this was a useful case study into writing a Greasemonkey script and the use of XPath for very specific targeting of DOM elements. The amount of time it took me to write this versus its usefulness is dubious but, like with all Greasemonkey scripts, they can pose an interesting challenge and at the end of the day can make websites you visit regularly a little bit easier to use.

4 Responses to “Greasing Gmail”

1 Golgotha

Hey Raffles, that’s a good first post. Welcome to Search-This – we’re glad to have you join us. Anyone with your JavaScript knowledge is a welcome addition!

I’ve never thought of modifying Gmail; didn’t even know you could. So now I have to give this some thought and think about what changes I would like to see or make within Gmail 🙂

2 Raffles

Thanks. There are loads of scripts to mod Gmail over at userscripts.org, chances are someone’s already written a script for what you want to do to it!

3 Paul OB

Hi Raffles, good to see you here and good first post 🙂

4 Sylwester w Górach

Well this is quite interesting i alwso didnt know that i can modify gmail. Maybe this will convince me to it becouse i dont like overall design of GM for me it is so boring, maybe i find something that will make GM more friendly to my eye.

Thank you Raffles for shering your knowdle.

mulberry sale spyder womens jacket cheap new balance 574 mulberry outlet cheap new balance 574 arcteryx outlet mulberry sale spyder womens jacket mulberry sale spyder womens jacket mulberry outlet mulberry outlet new balance 574

Popular Articles

Top 10 Commentators


Subscribe to this feed! Subscribe by Email!

Random Bits Podcast

You need to download the Flash player from Adobe

Blogs Worth Reading