Finder-like column view from hierarchical lists with jQuery

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.

Ok already, just show me the downloads!

OS X Column View

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

  1. 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.
  2. The script shouldn't require a bunch of support files - css, images, etc.
  3. 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:

Into this:

Usage is pretty basic:

jQuery('#columnized').columnview();

There are no options. The aesthetics and behavior of the menu are determined by overriding the CSS that it provides, and providing a double-click handler.

The script provides few CSS classes that can be overridden to change the way the script is displayed.

/*Top level container - set the width, height and border here*/
.containerobj {
  border: 1px solid #ccc;
  height:5em;
  overflow-x:auto;
  overflow-y:hidden;
  white-space:nowrap;
}
/*Div containing an individual level of the menu hierarchy*/
.containerobj div {
  height:100%;
  overflow-y:scroll;
  overflow-x:hidden;
  float:left;
  min-width:150px;
}
/*Link*/
.containerobj a {
  display:block;
  clear:both;
  white-space:nowrap;
}
.containerobj a canvas{
  padding-left:1em;
}
/*A bottom-level element (the furthest down in the hierarchy) is displayed as a 
link, but could be overriden*/
.containerobj .feature {
  min-width:200px;
}
.containerobj .feature a {
  white-space:normal;
}
/*If you want to display links as folders vs. files, you can apply styles to the
.hasChildMenu class*/
.containerobj .hasChildMenu {
}
.containerobj .active {
  background-color:#3671cf;
  color:#fff;
}
/*You can override the color of the triangles here*/
.containerobj .hasChildMenu .widget{
  color:black;
  float:right;
  text-decoration:none;
  font-size:0.7em;
}

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:

jQuery('#columnize a').livequery('dblclick',function(){
  window.location = jQuery(this).attr('href');
}

When I have time, I'll probably post up a demo page with some other examples of how to style the menu. In the meantime, you can download the script here:

For jQuery 1.2.x (requires Live Query plugin):
jquery.columnview-1.0.1.js [source]
jquery.columnview-1.0.1.min.js [minified]

For jQuery 1.3.x:
jquery.columnview-1.1.1.js [source]
jquery.columnview-1.1.1.min.js [minified]

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:
    1. When calling the method: jQuery("yourselector").columnview({multi:true});
    2. Prior to calling the columnview method:
      jQuery.fn.columnview.defaults.multi = true;
      jQuery("yourselector").columnview();
  • Now assigning ID of original hierarchical object to columnview object, to allow easier styling, etc. Old object is reassigned to ID-processed and hidden.
  • Fixed assignment of active/inpath classes so that only items that are currently selected have class=active.

Comments

That's some pretty pimpin' Javascript there, my friend. Nicely done!

May be I don't understand, but all the links in the third level deep, dosen't work :( like:

administer / content management / categories

The link doesn't show up on the fourth column.
I testeed in IE7 and Fierfox 3 for XP.

Christian's picture

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.

I'm updating the 1.3.x versions shortly.

1.3.x versions are now updated as well. Hope this helps!

This browser is excellent!

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.

Christian's picture

Good things to consider for the next release, or if you want to contribute a patch.

-chris

Hi! Can your plagin do the same with images? It will be very useful! Like something this:

Christian's picture

@ Zebotron - there's no reason it couldn't be modified to do so, if the images were within the list elements. I purposely didn't put a lot of styling into it for this reason.

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.

Christian's picture

@Steven - Yeah, I'd definitely like to make the keyboard navigation Webkit compatible. I'm not sure where it's broken, actually. I'll have to investigate this a little more.

Can it do something like this, as a checklist? www.cascadinglists.com

Christian's picture

@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.

Chris - love the code. I need to prototype multi-select miller columns (finder-like column view).

Any advice on enabling multi-select on parents and children, keeping track of selections while not viewing parent.

Christian's picture

@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:

$(your selector).find('.active').each(function() {
  // Do something with your elements here
});

The other option I can think of is to bind a handler that appends data from each click to a DOM element using the data method.

If you get a chance to work on this, I'd be interested in seeing your code and incorporating it into the main release.

Great plugin, but it seems to be incompatible with jQuery 1.4.x. Any chance to update it ?
Thanks.

Christian's picture

@Matthieu, I'll have to take a look. To be honest, I've not done any work in production with 1.4 yet, as I'm doing mostly Drupal 6 (which is still tied to 1.2.x) at the moment.

Are you seeing specific issues, or is it just that it doesn't work at all?

Any way to set the view on load of the page?

I want to use it in a prototype and want to show a folder 2 levels deep selected already. Possible?

Christian's picture

@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.

Christian's picture

@Matthieu - The plugin has now been updated for jQuery 1.4.x compatibility, among other fixes and improvements. See the revised links to download.

Was wondering if you could kindly fix the following error

Error: $(self).data("sub") is undefined
Source File: http://static.christianyates.com/columnview/jquery.columnview.js
Line: 125

Christian's picture

@AC- Please check the latest version of the code at http://columnview.googlecode.com/

Chris,

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?

Thanks!!!

--
Matt

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.

I hope it makes sense. It's hard for me to explain.

Christian's picture

@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.

Download the latest tarball from Google Code and see some examples.

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?

Thanks again!

Just in case you don't see it over there, I filed a bug over on the Google Code project related to this preview callback: http://code.google.com/p/columnview/issues/detail?id=1

Christian's picture

@Will Norris: I'm not sure what you're looking for. The Preview callback works as advertised, as far as I can tell.

See "Adding a callback to the 'Preview' pane" on the demo page.

Are you trying to achieve something else?

@Tom C: I've added another callback for onchange (though I really should rename this to avoid confusion with the native onchange event), also documented in the demo.

The current release of the script and demo is downloadable at http://code.google.com/p/columnview/downloads/detail?name=columnview-1.2.3.tar.gz.

i LOVE this script! i finally got the preview callback working and now i can continue working on this project of mine. thanks!

Thank you soooooo much for this great script. In the example below how can load the link for Employee in the preview pane?

For example:

  • Employer
  • Thanks.

    Christian's picture

    @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.

    Hope this helps

    -c

    Hi Chris,

    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.

    Any help will be appreciated :)

    Thanks,
    Hugo

    Christian's picture

    Hugo,

    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.

    Chris,

    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

    Thanks,
    Hugo

    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..

    So I stopped working on this for a few days but still have no clue on where to start..
    What would you do? I'm considering binding an event to each anchor instead of the whole div.

    ended up creating my own column view using jquery. didn't make a plugin and probably its not perfect but it suits my needs :)

    thanks for your help,

    hugo

    hey chris, i'm trying to use your script in a very simple way but for some reason i cant get rid of the last anchor tag. I'd like to get a list like this:

    Parent 1 (anchor) > Child 1 (anchor) > info (simple text).

    I've tried many ways but cant figure out the good one.
    Any clue would be really appreciated.
    Thanks in advance.

    Christian's picture

    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.

    See the example here.

    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.

    Best,

    m

    Christian's picture

    @Mauricio - No, those extra classes are merely artifacts of the Drupal menu that I used in the example.

    Currently the script depends on there being <a> tags in the list elements. For the next revision, I want to fix that. There's really no reason to require that.

    I'd like to add my thanks to the long list of kudos bestowed above.

    Like Josef and Hugo above, I'm trying to navigate programatically, rather than by user click + key events.

    Can anyone confirm that it is possible to walk through columnview in this way? My (clearly incorrect) start to trigger a Key down event in the 'columnview' is:

    var e = jQuery.Event("keydown");
    e.which = 40; // # Key down
    $('#pathPicker').trigger(e);

    I'm calling 'columnview' as follows:

    // Invoke *without* preview
    // Invoke *with* callback to #currentSelection
    $('#pathPicker').columnview({
    preview:false,
    onchange: function(element) {
    $("#currentSelection").text($(element).attr('history'));
    }
    });

    I realize there is a distinction between keypress and keydown numbers for jQuery, but haven't been able to use that information, yet.

    Thanks again.

    Christian's picture

    Kevin, you've got the right idea, but I think what you want to do is use the click trigger.

    So if you wanted to activate the first menu item in one of the demos on the demo.html doc:

    $('#demo6 div:first a:eq(0)').trigger('click');

    Here's the interesting challenge though - it would be helpful to return where we're at in the hierarchy, so you can really walk through programmatically. I'll have to ponder that part.

    Hallo,

    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?

    I would really appreciate your help!

    Greetings from Germany

    mimi

    Christian's picture

    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")....

    At the end of the click handler conditionals you can force the div to scroll hard right like apple does with Finder by adding this code:

    $(container).animate({ scrollLeft: $(container).attr("scrollWidth") }, 2000)

    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.

    Of course, this is above my head ..
    Any suggestions? Thanks.

    Christian's picture

    @Matt - The way I've done this in the past is to follow the example in the demo for "Getting selected items". Basically, in your preview callback, run something like this:

    $(".active, .inpath").each(function(el){
      console.log($(this).text())
    });

    Here, it's just printing the text of the links to the console, but you could concatenate these to a string, or do whatever.

    In the future, I'd like to log the path to (and indeed, the whole tree) HTML5 storage, but I've not had much time to play around with this lately.

    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...

    Thanks,

    Christian's picture

    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.

    Hi Christian,

    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:

    $("#question_tree").columnview();

    but it doesn't seem to force a reload.

    Thanks in advance!
    Dave

    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?

    looking for the paragraphs to be able to use < br > s as well.

    Christian's picture

    Rhonda - you have complete control over what shows up in the "Preview" pane, and can even shut it off if you like. See the demo.html file for examples.

    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!

    Can this be used with rollover instead of click? I'd like to implement this as a menu without clicking. What do you think of this approach?

    To clarify, I'd like to have keep content from scrolling, but have the width wide enough to accept 2-3 levels of menu. The user would then rollover instead of click to navigate.

    Christian's picture

    Think I addressed this here: https://gist.github.com/3874369#L104

    Although, this would be a nice feature to add (removing the hard-coded "click" behavior and replacing with something more abstract).

    Hi

    How do I add elements to the list once it has been created and attached to columnview, say via an AJAX request?

    Thanks

    Christian's picture

    Have a look at example 8 here: https://github.com/christianyates/jQuery-ColumnView/blob/master/demo.html

    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.

    How would it work if, say, I wanted to add children to "foobar"?

    Thanks

    Hi, did you solve your request? I'm interested in the same thing.

    Example 8 is clear enough...

    Thanks Michael for this G R E A T script. Applause.

    Christian, to you also!