Finder-like column view from hierarchical lists with jQuery

Subject:

Mac OS X's Finder features a nifty NeXT throwback - the column view. This lets you browse through a hierarchy of files in a relatively compact space, and still see your path through directory structure.

There are a couple of jQuery plugins in the archive that claim to do this, but none really fit my core needs:

The script should be unobtrusive, and let you transform a hierarchy of unordered lists of links (like a Drupal menu) into a column view, without requiring altering the underlying markup.

The script shouldn't require a bunch of support files - css, images, etc.

The output should work basically like a Finder list view - allow keyboard navigation with arrow keys, show when items have submenus (i.e. differentiate between "folders" and "files").

I think I've achieved two out of three - keyboard navigation doesn't seem to work in Webkit browsers (Safari and Chrome), and I got lazy and used the excellent Livequery plugin rather than rebinding events - but otherwise, it transforms this:

I chose to include these styles in the script rather than as an external file to avoid having to reference external files and worry about their placement on the server.

I should note that instead of including images for the little triangle widgets, I'm using Canvas to draw them, where available. Where it's not (in Internet Explorer), I put in a little ASCII triangle. I have to admit I did this as much to play around with Canvas as anything else.

To actually do something with the menu, I chose to use double-clicks, in keeping with the OS X style UI. Here's a sample handler:

Note: I have only tested this with jQuery 1.2.6, though I'd expect it to work with 1.3.x as well. Of course, 1.3 includes the live() method, which might be used in place of LiveQuery. For now, I'm focusing on 1.2.6, as this is what I'm running with all of my Drupal installations.

Update: I've updated the script to work with 1.3.x (tested against 1.3.2) and removed the dependency on the Live Query plugin. We're using the live() method now. Downloads added above.

I've tested this script in Safari 3.x and 4.x, Chrome, Firefox 3.x, and IE 6 and 7. As previously noted, keyboard navigation doesn't work yet in Safari and Chrome, and due to silly IE css handling, the width of submenus is fixed (via css) at 200px rather than shrinking to fit the content.

Update: The latest version of Columnview now supports jQuery version 1.2.x, 1.3.x and 1.4.x. Additionally, keyboard navigation is now available on all browsers when using jQuery 1.3 or later. The Livequery plugin is no longer required, but keyboard navigation is not supported with jQuery 1.2 (at the moment).

Update 19 April 2010: New features added to Columnview

Added control/command- and shift- select options. Shift-select requires jQuery 1.4.x. Multi-selection is disabled by default, but you can enable it in two ways:

When calling the method: jQuery("yourselector").columnview({multi:true});

Yes, I noticed that I'd broken the horizontal scrolling somewhere in my development process. I've refactored the code in the 1.2.x version so that it uses absolute positioning of sub-menus rather than trying to deal with float and inline-block inconsistencies between browsers. This scrolls horizontally as expected in FF 2/3, Safari 3/4, IE 6/7. Still need to test in FF2, but I don't expect it to work any differently there.

It would be interesting to try to add some custom html to the leaf display, like how the os x finder displays file information (size, date of last modification etc) in the rightmost pane when you select a file. I tried to add some html inside the leaf <li> items, but it's not carried over.

Also, it would be an improvement if the selection of the path was encoded as an anchor, so that the selected path could persist between page reloads.

Any plans to make it keyboard-navigable on Webkit? I'd really love to build off of this and that would be the first thing on my list to fix, but I don't know enough about Webkit to know where to start.

@obsessiveListMaker - I'm not sure what part of that app you're wanting to replicate. It would certainly be possible to add other jQuery click behaviors to the list elements after they're included in the widget, and with the livequery plugin for 1.2.x or live() for 1.3, you'd be able to add items dynamically ... I think.

@alooster To enable multi-selection you'd need to refactor some of the code in the click handling function to prevent hiding lower-level elements and deselecting when the control/shift key is down, around line 97. Currently the behavior is to remove the .active class from all other elements and remove child columns. You'd also need to handle selecting items between mousedown points programmatically for shift-selecting if you wanted both contiguous and noncontiguous selection ability.

To track the selection, you have two options - the easiest would be to bind a function to whatever interface element (say a Submit button) you're using that just iterates through the elements in your menu to find the "active" class:

@Josef - I haven't tried this, but since we're binding the loading of each level in the hierarchy to the click event, you should be able to use the .trigger() method to "click" through the hierarchy programmatically on page load or by triggering another event to load the particular item you want.

zOMG this is cool. However, it looks like the entire tree must be generated in HTML first, then parsed with JS.

I've got some "trees" with upwards of 30,000 nodes an am wondering how well this performs under that sort of load. Is there any way to fetch the "subtree" stuff from the database (e.g. Controller) when clicked? Thoughts?

Hi, this is a good piece of code that I have been looking for. However, there are few things that I believe can make it more Finder-like:

When you come to the leaf, it should not show the leaf node again on the next panel, but instead let you open that leaf immediate. For example, click "Create content"->Map should open the Map page instead of showing Map again.

Better yet, show some information on that extra panel if possible. Then, the page "Map" can be open directly by double click. So, single click shows information, double click open the link.

The other one is, Finder only have three panels. When you come to the last panel (the right one) and click to expand a subfolder, it should replace the right panel with the new subfolder, and go on.

@Esente: You have the option to show whatever you'd like in the final panel using the preview callback. You can pull in more data using AJAX techniques, or grab elements from other places on the page. You can do whatever you want. If we're just looking at a list of links, the only data we have is the anchor itself, and any title attribute that has been added to that anchor, so that's what the plugin displays by default.

Also - the Mac OS X Finder shows as many columns as it has space for. Technically, my implementation isn't trying to replicate what Apple's Finder does - it's an implementation of the Miller Columns UI pattern. Feel free to contribute a patch to limit the number of columns displayed. That might be useful to some people.

First, let me echo the compliments on this script. It really works very well. Second, I had the same question as Josef and tried to use the trigger function by triggering a click event on one of the anchor tags in the first column but to no avail. Have you thought any more about this or have you already come across a solution?

@rizwan - You can use the preview callback to do this. See the demo page for an example of how this works. The preview callback is passed the child element - in this case an anchor tag linking to the "Employee" page. You can use $.load() to load the contents of that page (or a portion of it) into the preview pane when the preview callback fires.

Nice script you have there, I'm a jQuery newbie and the comments really helped.
I'm trying to use columnview in a personal project but it would be quite nice if there was a way to select an item in the list after creating it. I would like to do that because I'm using columnview as a menu and when you click on a leaf you get redirected to the appropriate page. The thing is, if you want to go back to the columnview page the item you selected is no longer selected, as the javascript object got destroyed and all..

So I'm thinking about using jQuery history plugin to save the leaf that has been clicked, the problem is I can't think of a way to select it afterwards.

I implemented this type of persistence for a project I used the column view plugin in, but I did it with the jquery storage plugin, which allows you to use HTML5 storage for complex objects like hierarchies. I'll have to look up this code to see if I can offer an example.

I checked jStore but I still feel my best option is to save the clicked anchor id and select it afterwards.
However, I can't seem to select any item from the columnview.
Each anchor has an unique id so what I'm trying to do is:

$("#").trigger("click");

That doesn't seem to work for some reason, even though I think you're binding a click event to each anchor, am I wrong?

If that worked it would only be a matter or going through all the levels, find the right anchor for each level and move on to the next.. I think

Nevermind,
I figured that you're binding the event to the div and when when an item is clicked it passes that anchor as the event target.
I tried passing the event target in .trigger extraParameters but with no luck, maybe it wasn't meant for that..

Kevin - by default the "Preview" Pane - the last pane on the right - will show the string in the title attribute of the selected anchor tag. Alternately, you can override this preview function with your own callback function to show anything you like.

Hi,
This may be a silly question, but I notice that in your source html the original list items have classes like menu, expanded, leaf, etc. Is this something that we have to add to our items for your script to work?
Thank you so much!

Sorry for the double posting, I hit the send to soon. This is my last questions, do all the items in the original list have to be links?
Would it work if they are not?
Again, I'm sure these are very basic questions, but I'm just trying to figure out how to implement your nice script.

thanks for this great work! I would like to use it in its "original" sense - as a file-picker. As the directory structure might become rather big, I'm wondering, whether there is a way to dynamically load the sub folders after clicking a folder. Is it enough to add some new levels of ul's and li's to the existing ones, or do I have to reload all the rendering?

Yes, since we are basically hiding the original array, but using it as the reference from which to pull each of the sub-menus, you can still manipulate it in the background dynamically - remember that if it had an ID attribute, you'll have to access it as $("#ORIGINALID-processed")....

Is there a way to output the entire document tree based on the selected element?
aka: If the tree is Chair > Airley > 4Leg and the selected element is 4Leg
display 'ChairAirley4Leg' rather than just '4Leg'

I have tried using .parent() $("#thecurrent").text($(element).parent().text());
however what is returned are other list items in the same <ul> not its parent(s)

I have been told the changes the context of "this" so you would have to figure out where you were in the doc by having an observer object.

Awesome plugin - does exactly what I'm after (after a bit of fiddling) - the only improvement I can suggest is that it would be nice if the last level of items would stretch to fill the available space, if the entire container isn't filled up (otherwise you get the last vertical scroll bar in the middle of a space). It would have to calculate it's width programmatically though, based on the preceeding divs. Is that something that's easily done? If that made any sense...

It sort-of does that already - though it's the "preview" pane that gets stretched, not the last level of list items. If you didn't want to show a preview pane, you could use the same logic already in the script to set the width of the final level to fill the container div.

Thanks for the awesome plugin. I'm currently integrating it into my site to display some hierarchical data with a LOT of records. To save the database being hammered I'm pulling the data as the user selects each tree branch, rather than loading it all up front. This works fine as a normal li/ul structure, but when I load the column view plugin over the top, as the underlying ul/li structure is updated with AJAX calls, the column view plugin doesn't pick up the changes.

Any idea what I'd need to do to make this work? I've tried simply adding this to my AJAX return:

I'm looking to use column view as a way to navigate through multiple years/months of news items for my company. Is there a way to display the bottom-level element as a paragraph (rather than a link) that I can add class categories and s to?

Thanks for the tip regarding scrolling to the right automatically, Ryan. Unfortunately, I can't seem to get your feature to work. Where exactly in the code should that line be placed? Does it work with the latest version of JQuery? Thanks!

As I understood that example, the list is completely created from scratch via the get call. I need to append elements to an already existing list. Also, the "load again" button is not implemented and that is the part I am having trouble with.