Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Browser Extension: Arrange Tumblr Posts in Chronological Order

Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

Creating little browser extensions is a great way to learn JavaScript. You don't need to worry about cross-browser compatibility, you can make the project as small or large as you like, and you can fix problems in other people's websites that bug you, specifically. In this post, we'll create a short script that automatically rearranges Tumblr archive pages to read in chronological order - and we'll learn about NodeList on the way.


The Problem

This week, Ryan North started a new Tumblr, in which he's going through the novelization of the first Back to the Future movie, one page at a time. This is a thing I wish to read.

BtotheFClick to read.

However, Tumblr displays its posts in reverse chronological order - that is, with the most recent posts first. This makes sense for a lot of sites on Tumblr, but in this instance, I really want to read them in the order they were written. The current site layout means that, to do that, I have to scroll down to the top of the first (first written) post, scroll down as I read it, and then scroll up to the top of the second post, and scroll down through that...

This is a tiny, tiny first world problem - totally trivial and almost embarrassing to mention. But that doesn't mean I don't want to fix it.

By the end of this tutorial, you'll make this Tumblr - or any Tumblr you choose - display its posts in chronological order on the page, automatically.


How Can We Do This?

We don't have control over the BtotheF site design; it offers no options to us, so we can't change the order in which the posts are displayed on the page server-side.

But once the HTML is sent to the browser, it's ours. We can manipulate it however we want. And, of course, the language with which we can do this is JavaScript.

I'll give a quick demo. I'll be using Chrome, and recommend that you do, too - if you can't, or don't want to, then grab a copy of Firebug.

Browse to http://btothef.tumblr.com/ and open the JavaScript console (Tools > JavaScript Console, in Chrome). Type:

alert("Hi");

...and hit Enter.

JavaScript Console Demo

Wow! ... Okay, fine, that didn't prove much. Try this:

document.body.appendChild(document.createTextNode("Hi"));

This will take the HTML document's <body> element, and append a new text node - a piece of text, basically - containing the word "Hi". If you didn't follow any of that, get up to speed with HTML.

To see the result, scroll right down to the bottom of the Tumblr:

JavaScript Console Demo

Slightly more impressive, since we've actually changed the contents of the page. The new text node is right at the bottom because we've used appendChild() - which adds the specified node after everything else inside the <body>. As far as our browser is concerned, the last few lines of HTML now look like this:

<!-- END TUMBLR CODE -->

Hi
</body>
</html>

So! Now we have two issues:

  1. How do we use JavaScript to rearrange all the posts in the page?
  2. How do we make this happen automatically, without having to mess around in the console every time we read the Tumblr?

We'll address these in order.


Rearranging the Posts

First, let's figure out how to "find" all of the posts in the HTML. In Chrome, right-click somewhere in the top post, and select "Inspect element". (Firebug should add the same shortcut to Firefox; for other browsers, try the magnifying glass icon.)

Inspect Element (Chrome)

The Elements tab of the Developer Tools will appear, with the element you selected highlighted:

Elements Tab

As you hover over different elements in this HTML view, you'll see them highlighted in the page itself. Eventually, you'll find that the entire post is contained in a div with a class of post. As you can see from the screenshot above, there are several other such div elements, and they are all contained in a master div with an id of contents.

We can select all of these post elements at once with a single line of code; type this into the JavaScript console:

document.getElementById("contents").getElementsByClassName("post");

(Technically, we could have just written document.getElementsByClassName("post"), but who cares?)

You'll see - again, in Chrome - some feedback:

All the posts

Great! This indicates that it worked. If you click the arrows, you'll see the contents of each div.

We might as well assign this to a variable, to save us having to type it out over and over:

var posts = document.getElementById("contents").getElementsByClassName("post");

(This time, the console will return undefined; you can check that it assigned the variable correctly by just typing the word posts in the console.)

We can loop through all of the elements in posts as if it were an array:

for (var i=0; i < posts.length; i++) {
     console.log(posts[i]);
}

Hit Shift-Enter to add a new line to your code without running it, in the JavaScript console. console.log() just traces its contents to the console, which is far less annoying than an alert() and far less disruptive than adding text to the HTML DOM.

The result of this simple loop will just be a list of all the div elements. Cool. So presumably we can do something like this...

var numPosts = posts.length;
for (var i=numPosts - 1; i >= 0; i--) {
     posts.push(posts[i]);
}

Try it out! You'll see this:

TypeError: Object #<NodeList> has no method 'push'

Rats.

A NodeList Is Not an Array

When we use getElementsByClassName(), we don't retrieve a simple array of elements, we retrieve a NodeList. Like an array, this is a collection of elements; unlike an array, the collection is updated in real time. If the HTML DOM is modified, the contents of posts gets modified to match.

For example, let's create a new div with a class of post and append it to the contents div:

var newPostDiv = document.createElement("div");
newPostDiv.setAttribute("class", "post");
document.getElementById("contents").appendChild(newPostDiv);

Run that, then type posts and hit Enter. You'll see an extra div in the list - even though we haven't touched the posts variable.

Since a NodeList is not an array, it doesn't have most of the methods and properties of Array, such as push() In fact, we've already used the only property: length. The only method is .item(n), which just gets the nth item in the list.

However, our earlier demonstration with appendChild() does suggest an alternative solution: what if, instead of trying to rearrange the elements of the NodeList within the NodeList, we rearranged them within the page?

First, we'll run through the list in reverse order, and append each element in turn to the contents div; then, we'll remove the original posts.

Refresh the Tumblr page to clear out all the extra rubbish we've added to the DOM, then do the first step, of adding the elements in reverse order:

//have to define posts again, since we lost it when we refreshed
var posts = document.getElementById("contents").getElementsByClassName("post");
var numPosts = posts.length;
for (var i=numPosts - 1; i >= 0; i--) {
     document.getElementById("contents").appendChild(posts[i]);
}

Okay, now we... wait, what?

description of image

The (chronological) first post is already at the top, and the last post is at the bottom, with no posts repeated. This may seem surprising, but it makes sense: we didn't clone any elements of the NodeList, we just appended them to the contents div, in reverse order. Since the NodeList always reflects the contents of the HTML DOM, we just moved each of the 11 elements to a new position - the 11th element got added to the end (no change in position); then the 10th got moved to be after that; the 9th after that, and so on, until the 1st element was right at the end.

So, problem solved, in five simple lines of code. Now let's automate it.


Turning It Into a Browser Extension

Three of the top four browsers allow you to create browser extensions - installable add-ons - using JavaScript. (It will not surprise you to learn that Internet Explorer is the exception.) Here are the instructions for Chrome, the instructions for Firefox, and the instructions for Safari.

But we're not going to use any of those. Instead, we'll use a great add-on called Greasemonkey, which allows you to install little JS scripts without needing to package them up as full-blown browser extensions. Essentially, it injects snippets of JavaScript into the page, once it's loaded.

Chrome already has Greasemonkey support baked in. Opera has something very similar to Greasemonkey baked in: UserJS. For Firefox, you'll need to install the Greasemonkey extension. For Safari, install SIMBL, and then GreaseKit. For IE, install IE7Pro or Trixie.

Writing the Greasemonkey Script

Create a new text file, and name it ChronologicalTumblr.user.js. Make sure you remove the default .txt extension, and that you use .user.js, rather than just .js - this is the extension for a Greasemonkey script.

Copy our post rearrangement code into the file:

var posts = document.getElementById("contents").getElementsByClassName("post");
var numPosts = posts.length;
for (var i=numPosts - 1; i >= 0; i--) {
     document.getElementById("contents").appendChild(posts[i]);
}

Now, we need to add some extra metadata at the top of the script - details about the script itself. All these details should go in comments, starting with ==UserScript== and ending with ==/UserScript==:

// ==UserScript==
//
// ==/UserScript==

var posts = document.getElementById("contents").getElementsByClassName("post");
var numPosts = posts.length;
for (var i=numPosts - 1; i >= 0; i--) {
     document.getElementById("contents").appendChild(posts[i]);
}

First, we'll add a name and a description:

// ==UserScript==
// @name          Chronological Tumblr
// @description	  Displays Tumblr posts in chronological order
// ==/UserScript==

var posts = document.getElementById("contents").getElementsByClassName("post");
var numPosts = posts.length;
for (var i=numPosts - 1; i >= 0; i--) {
     document.getElementById("contents").appendChild(posts[i]);
}

Then, we'll specify a namespace. The combination of the name and the namespace is what uniquely identifies the script; if you try to install another Greasemonkey script with the same name and namespace as an existing script, it will overwrite the old one - if only the name or the namespace match, it'll just install the new one alongside the old.

It's common to use a URL here, so I'll use Activetuts+'s. You should use your own, since you're in control of it:

// ==UserScript==
// @name          Chronological Tumblr
// @description	  Displays Tumblr posts in chronological order
// @namespace      http://active.tutsplus.com/
// ==/UserScript==

var posts = document.getElementById("contents").getElementsByClassName("post");
var numPosts = posts.length;
for (var i=numPosts - 1; i >= 0; i--) {
     document.getElementById("contents").appendChild(posts[i]);
}

Now we need to specify which URLs this script should affect - that is, on which web pages the JavaScript should automatically be injected into. I want this script to work on the BtotheF homepage, plus the other archive pages, like http://btothef.tumblr.com/page/2:

// ==UserScript==
// @name          Chronological Tumblr
// @description	  Displays Tumblr posts in chronological order
// @namespace      http://active.tutsplus.com/
// @include       http://btothef.tumblr.com/
// @include       http://btothef.tumblr.com/page/*
// ==/UserScript==

var posts = document.getElementById("contents").getElementsByClassName("post");
var numPosts = posts.length;
for (var i=numPosts - 1; i >= 0; i--) {
     document.getElementById("contents").appendChild(posts[i]);
}

The * is a wildcard; it ensures that the script will be inserted into every single archive page of the BtotheF Tumblr.

We could use a wildcard to make every single Tumblr's archive pages display in chronological order:

// ==UserScript==
// @name          Chronological Tumblr
// @description	  Displays Tumblr posts in chronological order
// @namespace      http://active.tutsplus.com/
// @include       http://*.tumblr.com/
// @include       http://*.tumblr.com/page/*
// ==/UserScript==

var posts = document.getElementById("contents").getElementsByClassName("post");
var numPosts = posts.length;
for (var i=numPosts - 1; i >= 0; i--) {
     document.getElementById("contents").appendChild(posts[i]);
}

In this case, it might be a good idea to improve your script to insert a button or link into the page that rearranges the posts when clicked. That way, you can use it when appropriate, without having to manually add every single Tumblr's URL as an @include parameter.

There are other metadata parameters you could add, but this is all we need for now.

Finishing Off

Installing the script in Chrome is easy: just drag and drop it from your hard drive into a browser window. It'll warn you that scripts could potentially be dangerous (just click Continue), and then check again that you want to install this specific one:

Are you sure?

Hit Install. If you're using another browser, refer to the relevant Greasemonkey script's manual - in any case, it'll be simple.

Once it's installed, you can see it in your list of extensions:

Just like any other extension.

Head back to http://btothef.tumblr.com/, or hit refresh if you still have it open.

description of image

The posts are now automatically displaying in chronological order!

It's not perfect: you may have noticed that the navigation buttons have been moved to the top of the page, since they are contained in a div with a class of post. But hey - if that bugs you, you have the tools to fix it now.

Advertisement