2005-11-10

Blogger Del.icio.us categorizer script

Update


I have now abandoned this version of the script in favour of its much upgraded, extended and bug fixed successor script, which does the same thing and more. If you are having trouble with this script, or miss features, I would recommend upgrading first.

I've spent the better part of the night and morning working on a Greasemonkey script that helps me organize my blog posts into categories, using Del.icio.us as a remote metadata storage for my posts, with or without category tags alike. (Since I am planning to eventually roll out some additional navigational aid code which will rely on having access to post titles, dates and timestamps, besides tags, to all posts in the blog. Blogger doesn't do half of that now and I won't hold my breath waiting for any of that to appear anytime soon.)

Anyway, what I have made is a script that extends an already rather nice tag adder script by Jasper de Vries. Jasper's version adds Technorati tags to posts, mine adds Del.icio.us tags. Just type in your tags in the Tags field added to the posts page (space or comma separated), and any post with tags will get a <div class="tags"> widh an unordered list of all tags, linked to your appropriate Del.icio.us tags.

Blogger editor with Tags field

Here is the nifty bit. When your then publish your post, the "published successfully" page has been blessed with two additional buttons; a "Link at Del.icio.us" link, and a "View Post" I threw in for good measure (since I have often missed a link there to visit the exact post I just (re)published, rather than going to the blog index page).

Del.icio.us link on published page

It's the Del.icio.us link that is the primary component, though, and when you click it, you are brought to the standard Del.icio.us "add bookmark" page, all fields filled in. (Post title, post permalink URL and timestamp, plus the tags you already typed in. This way you can keep both the tags on your posts and at Del.icio.us in sync easily, which is a good start for making even nicer category browsing hacks such those Greg has deployed in his Speccy and Vent blogs.

The first time you use the script in a blog, it will ask some questions -- how you want to list the links in the blog (I use "Categories:" in mine), what the link to your Del.icio.us tags is, and whether you want to add additional tags to all posts (I add "ecmanaut" to them, so I can easily get a feed which only lists my own posts here without messing them up with any other social bookmarks of mine). You may reconfigure these options anytime by clicking the Tags: label, but remember that you must also republish old posts, if you want to change the header (since it's a part of the post text, rather than of the template itself).

Come to think of it, that might not be a very pretty arrangement; I might make another version which lets you give an id for a div where the <ul> with categories gets put, but for now this will do. (It's also easier to deploy for you, this way; no need to alter any templates, except perhaps to add some CSS for the div.tags selector and optionally its ul, li and a descendants.)

One more note, before I leave you to your own devices. When you update a post's entry and change tags on it, the link to Del.icio.us won't pick up the new categories, even though they are properly fed in the URL, so you might have to copy them manually. I'm not sure if that would count as a bug or a feature, but now that you know, you won't have to get bitten by it, either way.

Del.icio.us pre-filled-in bookmark form

Enjoy! And if it bothers you that I have more or less considered the words Tag and Category interchangably above, you may move on to Sambot, and learn why tagging is such a great thing. Because it really is.

Not satisfied with how the formatting of your tags list came out? You might find a hint or two about styling them with CSS in this follow-up article about how to make your tags line up on the same line.

2005-11-09

Google Reader: adding access keys

I like Google Reader, but find its support for keyboard navigation a bit lacking. A few days ago, I thought I would file a feature request with the developers (assuming they read their mail) for adding hotkeys for page up and page down links, but then I got impatient and sketched up a Greasemonkey solution of my own instead. It's as future proof as the google developers allow it to be, and it is fairly easy to change to your own preferences.

Bottom reader paneWhen you install my Google Reader access keys script, you will be rewarded with a few additional on-screen hints (some characters who did not use to be have become underlined), and pressing those keys will activate that link or toggle between the choice's available options.

I thought I would dive into how I accomplished this. At the top of the script, you find what looks like (and indeed is) a mapping from keyboard shortcuts to some maybe slightly scary-looking CSS selectors. Edit the key, and your version of the script will have a different set of keyboard shortcuts. To make it a bit more readable, I annotated each key with what function it defines:

var keys = { 'U':'a#queue-page-up',     // page Up
'D':'a#queue-page-down', // page Down
'B':'div#order-by', // order By
'S':'div#read-items', // Sort by
'E':'div.read-state label' // kEep unread
};

The selectors point out which element contains the control, and the text it carries. Further down in the script, I use these to find what link to "click" and which piece of text to add the underline hint to. Here is the code that adds the underlines:

var key, selector, node, label;
for( key in keys )
{
selector = keys[key];
node = getElementsBySelector( selector );
if( !node.length ) continue;
node = node[0];
if( node.nodeName.match( /a/i ) )
underline( node, key );
else if( node.nodeName.match( /div/i ) )
{
label = node.getElementsByTagName( 'label' ).item( 0 );
underline( label, key );
}
}

Then we set up the keyboard snooping, listening for our access keys:

document.addEventListener( 'keydown', keypress, false );

This will call keypress() whenever a key is pressed in the reader interface. (The details of that method will follow shortly.)

Keep unreadgetElementsBySelector is Simon Willison's document.getElementsBySelector(), and would have been massive overkill if the kind Google Reader Developers would have added id attributes for all functional components, which they unfortunately do not (...yet? -- the "Keep unread" checkbox only has a class name). If they would have, we would have just put node id:s there rather than selectors and used document.getElementById() instead, halved the script size, shaving off 4K of it in one go and been somewhat less resource intensive runtime.

My underline() method takes a node and a key (a character) parameter, and tries to find that character in the user visible text contained by the node, and add an underline to the first occurrence of it. (In case the text already had an underline, as is the case with the links, it will add another underline by using the CSS border attribute instead.) It's a very useful function; feel free to pick it up separately:

function underline( node, key )
{
if( !node ) return;
var character = new RegExp( '(<[^>]*>[^<'+ key +']*)*('+ key +')', 'i' );
var hasOne = getStyle( node, 'textDecoration' ).match( 'underline' );
var style = hasOne ? 'border-bottom:1px solid' : 'text-decoration:underline';
var underlined = '$1<span style="'+ style +';">$2</span>';
node.innerHTML = node.innerHTML.replace( character, underlined );
}

function getStyle( node, style )
{
if( node.currentStyle )
return node.currentStyle[ style ];
if( window.getComputedStyle )
return getComputedStyle( node, null )[ style ];
return '';
}

Time for the key input crunching code. First, we make sure we got a key we handled and which node it is bound to by our magic keys mapping above. If it isn't there, or the node isn't (it might become deprecated in a future version of Google Reader), just exit right away:

function keypress( event )
{
event = event || window.event;
var key = String.fromCharCode( event.which || event.keyCode );
var node = getElementsBySelector( keys[key] ), label;
if( !node.length ) return;
node = node[0];

Now we look at what node we have picked up -- if it's a plain link, or a label element (as is the "Keep unread" link -- a label with an encapsulated <input type="checkbox"> tag, to be precise), we just send it a mouse click:

  switch( node.nodeName )
{
case 'A':
case 'LABEL':
simulateClick( node );
break;

And if it was a <div> tag, we look for all its descendant <a> tags, assuming they are options to toggle between when our access key has been pressed:

    case 'DIV':
var choices = node.getElementsByTagName( 'a' ), i, current = 0;
for( i=1; i<choices.length; i++ )
if( choices[i].className.match( 'selected' ) )
current = i;
node = choices.item( ++current % choices.length );
simulateClick( node );
break;
}

Then we look through these links to see which one carries the "selected" class name, pick the next node (wrapping at the number of choices available, so it will find the first choice when the last choice was active) and send it a click.

All of this knowledge of document structure has been picked up by inspection of the page structure in the Firefox Document Inspector beforehand, by the way, so my knowledge of this page isn't anything overly magic. "inspect this" arrow Just press Ctrl+Shift+I on any page (having made sure you checked the "Developer tools" checkbox on first installing Firefox) and it will pop up. To find out the looks of the tag neighbourhood around "Show read items:", for instance, I first clicked the "inspect this" arrow, then clicked the "Show read items:" text in the main window, and was rewarded with this insight into the structural neighbourhood:

inspector DOM tree overview

If you recall the keys mapping, the Sort by key was bound to the div#read-items div, in the screen shot above listed as the parent of the label and the two <a> tags, read-items-hidden and read-items-visible. In case we wanted a key to not just toggle between these modes, we could bind either or both directly too, by tucking in another 'H':'a#read-items-hidden', line back there, for instance, following this same recipe. Neat way of adding functionality without really adding any code, right?

Finally, we make sure that the keys we heard are swallowed up, so they don't have any other effect, besides toggling visibility or clicking somewhere or whatever else we did:

  event.preventDefault && event.preventDefault();
event.cancelBubble && event.cancelBubble();
}

This odd code configuration is a safeguard in case the browser would not support these methods, in which case nothing happens and no errors are left hanging in the javascript error console. It's polite to ask before accessing methods you are not sure about how portable across browsers they are, and user scripts are known to work both in Mozilla, Opera and Internet Explorer (with funky third-part extensions) today so while I don't often try my hacks in all three nor across different platforms, I throw in a few checks like these where I'm uncertain.

The only bit of code left unspoken for now is the simulateClick function, which I picked up from the Google Reader blog eariler today.

Using this approach, you can add keyboard bindings fairly painlessly to mostly any web page or web application, not only Google Reader. It's easier when you have some kind of guarantee of the developers over there not haphazardly changing document structure and node id:s, classes and semantics of things as often as they change underwear, but even when they do you are usually only some inspection in the document inspector and a few tweaks to the CSS selectors in the keys mapping away from a new version of your own keyboard access features. The best thing, of course, is mailing your improvements to the people behind the site you inject your preferences into, but in the cases where those people are hard to reach (or persuade) the Greasemonkey approach is your ever trusty friend.

Happy accessibility hacking!

2005-11-08

"Subscribe to feed" user scripts

Ever since I started using Google Reader, I've been plotting at making a GreaseMonkey script to add "subscribe" links to all blogs featuring (one or more) syndicatable feeds. Today it was finally time, but before going about it myself, I had a quick peek at userscripts.org to see if someone had beat me to it. Someone had. Fairly nicely, too, though of course not exactly the way I had planned mine, from an aesthetic point of view.

So I started off from the Nowhere code base and retweaked the looks and functionality to my own taste, and when I was done I felt I could just as well spare another hour or so to swing together subscribe scripts for a few other common feed readers, so I did. I suggest installing at most one of these at the same time (since they occupy the same screen space, in the bottom left corner of any page with RDF, ATOM or RSS feeds):

Subscribe via Bloglines Subscribe via Bloglines
Subscribe via Google Reader Subscribe via Google Reader
Subscribe via MSN Alerts Subscribe via MSN Alerts
Subscribe via My AOL Subscribe via My AOL
Subscribe via My MSN Subscribe via My MSN
Subscribe via My Yahoo! Subscribe via My Yahoo!
Subscribe via Netvibes Subscribe via Netvibes (contributor)
Subscribe via Newsburst Subscribe via Newsburst
Subscribe via NewsGator Subscribe via NewsGator
Subscribe via Rojo Subscribe via Rojo

What each script does (once installed), is that it peeks at all <link rel="alternate"> tags tags in all pages you visit, and when it finds a link tag with a type (or href) attribute marking an RDF, ATOM or RSS feed, it adds a panel (such as the above images) at the bottom of the screen with a button for each feed it found. Click on either of these buttons and the feed is loaded in your feed reader of choice, where you may choose to subscribe to it in a comfy fashion.

I have only tested the Google Reader script to perform subscriptions, though; please tell me if there are any problems with any of the others.

Those of you who prefer the Nowhere text icon style over the Chris Nolan based version above might opt to pick up that version instead:

Subscribe via Google Reader Subscribe via Google Reader

Or perhaps the MacManX version (I think that's about enough graphical options for google reader ;-):

Subscribe via Google Reader Subscribe via Google Reader