Usable directory listings with a little Dojo

I think we’ve all seen Apache directory listings? They are a list of links + icons that detail the contents of the directory. You can go wild with a custom handler to format directory listing requests however you want. But for most cases they work just fine out of the box. They are kind of tedious to browse through though: scroll, scroll, click, or – worse – tab, tab, tab (tab, tab,) enter. A little Dojo magic might go a long way here.

This tutorial shows you how to upgrade those plain vanilla pages to make getting around a little faster and along the way introduce you to some of the most useful bits of Dojo, and practical techniques for working with them. We’ll touch on: dojo.query, dojo.data, the dojo parser and dijit (specifically the FilteringSelect widget.)

We’re adding a keyboard-aware suggest-box control to directory index page. The keyboard interaction is hugely improved: tab, u, enter takes you straight to the “United States of America” page.

Getting Started

If you want to follow along, you’ll need access to an Apache web server that allows you to configure indexes – via the main configuration file or a .htaccess file in the directory of your choice. We’re going to create a new header file (see the HeaderName directive) – which is a file that will get included at the top of the generated html that represents each directory listing.

Step 1: Get Configured

Here’s the incantations you’ll need in your .htaccess file:

Options +Indexes
IndexIgnore _header.html
HeaderName /_header.html

Using a root-relative path for the header file means you wont have to duplicate the same file down into every subdirectory. The IndexIgnore directive allows us to exclude the header file from the listing. You could also just use a dot-prefixed filename (but then your OS will likely hide it too.)

Create your _header.html file, and to make sure we’re on track, start with something simple like:

Here's my custom directory index header

Custom header

You should see something like the directory structure in this screenshot.

Step 2: Adding Dojo

Now the fun begins. We’ll need some JavaScript and CSS to add style to our suggest box. You can host your own build of Dojo, but for simplicity, we’ll use one from AOL’s CDN.

Here's my custom directory index header.

With Dojo

In Firebug, we can now see there’s a “dojo” object ready and waiting:

The suggest box we want to use comes from Dijit, Dojo’s widget system, and is called the dijit.form.FilteringSelect widget. It’s designed with a clean separation of the presentation and behavior layer from the actual data it is tasked with displaying. First, let’s get it on the page, before we worry about how we’re going to feed data to it.

We style the indexHeader, and arrange for the suggest-box to sit in the top-right corner of the page. The .dj_ie and .dj_gecko classes are a little Dijit magic – it adds a class to the <html> element which identifies the browser and makes this kind of CSS branching really easy and hack-free.

Shortcut:

We’ve added a script tag to pull in the dijit.xd.js file. That’s a “layer” in Dojo build terminology, and it rolls up a lot of the common dependencies for working with widgets into one optimized file. We add the requires to pull in the specific widget we want to use, and ensure the parser module is loaded. We add some CSS and markup, and add the dojoType to the element that’s destined to become our FilteringSelect widget.

To configure the widget, we need to define some attributes:

store=”linksStore”

where to get its data. Should be a reference to a data store that supports at minimum the Read api

id=”fileSuggestBox”

so we can easily get a reference to it later

searchAttr=”id”

what data item attribute should we be matching against when we search with each keypress

labelAttr=”label”

what data item attribute should we use as the displayed option label in the dropdown

invalidMessage=”No such file in this directory”

message to display when no matches are found

Container but no widget

After doing all that, now we have a container but no combo box. Eh?

Step 3: Tame the parser

The Dojo parser allows us to add directives right into the markup where they make most sense. The dojoType attribute is there for the parser to find, but we haven’t told it to run yet. In Dojo 1.* auto-parsing of the document for dojoType’d elements is off by default. That’s good. Blindly crawling the entire DOM of a potentially large page isn’t something you want to do unless you need to for performance reasns. If you want to have it run automatically when the page loads, you can set djConfig.parseOnLoad to true, either with a djConfig attribute in the dojo.js script tag itself, or by defining a djConfig variable before the dojo script tag, and setting it there.

As it happens, we know where any widgets will be – in our indexHeader div. So, we kick off the parser, and pass it the container element we’re working with:

Did that? Right now you’re looking at an error that says linksStore is not defined. It is time to feed the widget.

Step 4: Wire it up

Let’s create that missing linksStore:

The dojo.data.ItemFile*Store classes are fairly general purpose data store implementations that accept json data as input. We require dojo.data.ItemFileWriteStore – we’re going to be dynamically adding items to this store, and this store implements the Write API which gives us an easy way to do that.

We add a hidden div to have the parser trigger creation of the store (We could have done this with a statement in the script too – but this puts everything in one place).
The jsId attribute allows us to provide an identifier for the store object – it should match the store attribute on the FilteringSelect instance.

Now that we’ve got a store (albeit empty) our FilteringSelect widget works. While we’re here we’ll define what we want to happen when the value changes (i.e. the user types a value and hits enter, or selects an item from the dropdown.)

We use the dojo/method script block to override the onChange method of the widget. It is directly equivalent to the following code block:

Using the “args” attribute we assign the arguments[0] of this function to a “itemId” variable, that will be scoped to the function. We look up the item in the store, and redirect to the url the item provides.

Step5: Populating the Store

Ok. To finish this up, all we need now is data, and that means populating the store with items that have the same attributes as the links in our directory listing. You can imagine a lot of ways to do this. You could write a new index handler say in PHP that could return the listing in a json format to populate the dropdown list. Ugh, that’s a lot of work. Or you could write that data inline into an array in a script block in the header file.

But it is work that’s already been done by the built-in mod_autoindex module in Apache. The data we need is in the page, and it is there in a readily queryable format (DOM) with a little help from Dojo.

dojo.require("dijit.form.FilteringSelect");
dojo.require("dojo.data.ItemFileWriteStore");
dojo.require("dojo.parser");
dojo.addOnLoad(function() {
var count=0;
// to pick up the theme on the combobox drop-down
// we need this class on the body
dojo.addClass(dojo.body(), "tundra");
// there's no need to parse the whole page for widgets
dojo.parser.parse( dojo.byId("indexHeader") );
// the query selected all "a" elements whose href
// property doesn't begin with '?'. This filters out
// the column sorting links that the
// mod_autoindex's FancyIndexing option creates
var query = 'a[href]';
dojo.query(query).forEach(function(elm) {
var url = elm.href;
var linkLabel = elm.text || elm.innerText;
// use the index as the item id in the store
var itemId = count++;
// create the data object to create an item from
var itemData = {
label: itemId + ": " + linkLabel,
url: url,
// the link text is a filename, so it is
// guaranteed to be unique.
// we use it for the id, which is also
// the field used to search on when
// typing in the select box
id: linkLabel
};
linksStore.newItem(itemData);
})
});

To get that we use dojo.query, to find all <a> elements that have an href attribute – that’s the query variable – a standard CSS3 selector. Then, using the forEach method of the dojo.NodeList that query returns, we iterate over the results.

Each node (<a> element) in the NodeList will be passed into the anonymous function we supply to forEach. We build up a data object with the properties we want to be able to retrieve later, and an (arbitrary) id, and call the Write API-standard newItem on our store to add the data, and make it available to the widget.

Step 6: Add Polish

There’s a couple of finishing touches needed:

#indexHeader {
visibility: hidden;
...
}

We can avoid any distracting flicker as it loads and populates, by setting visibility initially to hidden, and only show when we’re ready

Depending on how you’ve got your directory listings configured, the icons might also be linked, so you’d end up with duplicate entries for each member of the directory. So we use an object as a lookup to only add each item once (we could have also queried the store itself.)

Also, we can refine that query a bit more. We can exclude the column headers that allow sorting of the directory listing with a not clause in the CSS3 selector that is our query, as those links are distinguished by having a querystring (“?“) in the href attribute.

Summary

So there you have it. We extended and augmented a page to make it faster and easier to use, leveraging out-of-the-box goodness to make it all happen with just about 40 (nicely formatted) lines of code.

Next steps might be to allow sorting of the listing without a page refresh, and perhaps a Tree widget to allow us to drill deep into a directory structure without even leaving the page. But that’s for another day.

In response to Seth … with ItemFile*Store, you don’t have to specify an identifier attribute. What the store will do if you have not specified one, is generate an identifier for each item anyway. Basically, it just numbers the items from 0 … N. In other words, as long as your initial dataset leaves identifier undefined, the ItemFile*Store will go into ‘auto-id’ mode and make up one for you. It won’t be accessible as an attribute of course, but is always accessible via store.getIdentity(item).

http://www.sitepen.com sfoster

@Karl – yeah :(, Opera is not officially a supported browser for the dijit project (though it is for Dojo core). In most cases it just work, but not here unfortunately. We’d love to add Opera support – its a matter of (wo)manpower to accomodate the extra testing necessary.

@Seth – I could give each item an id, but the store would just overwrite the previous item with that id IIRC. It seems faster and cheaper to pre-filter, and let the store only do its heavy-lifting where necessary

Emil

What you did is already implemented in firefox/well, not so fancy/. Try this:
1. hit “/”(quick find)
2. type part of the link text
3. Hit enter
Voila!