jQuery

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:

$('#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:

$('#columnize a').livequery('dblclick',function(){
  window.location = $(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: $("yourselector").columnview({multi:true});
    2. Prior to calling the columnview method:
      $.fn.columnview.defaults.multi = true;
      $("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.

Finally something to show for my seven months on Mars

So I quit my job in newspapers last May and hauled my then-pregnant wife across the country to work for NASA.

I've been a busy little bee, working on all sorts of projects, mostly involving Drupal and/or jQuery. But sadly, not one has been allowed to see the light of day.

Well, no more. Though it's not officially released, I'm putting my latest work out there - a unified Mars Image Explorer that'll eventually replace a half dozen or more different kludgy sites with a single easy-to-use interface for images from a variety of instruments orbiting Mars.

Mars Image Explorer

I'll eventually write up a case study, but it's an interesting mix of technologies:

Drupal powers the page generation, user management and access control, jQuery runs the AJAX interface, Python/web.py manipulates images in real time efficiently, and Java servlets deliver instrument data.

So there. I actually have been doing something.


Semantic Tabs with jQuery

This plugin creates tabbed panels from semantic markup. What does this mean?

Many (most?) javascript tab solutions tend to take the following approach: In the markup, create a list of elements to use as the tabs themselves, then create a list of elements to use as the tab panels, like so:

<ul>
  <li>Tab1</li>
  <li>Tab2</li>
  <li>Tab3</li>
</ul>
<div>
  Panel 1
</div>
<div>
  Panel 2
</div>
<div>
  Panel 3
</div>

This works OK for most users, but with javascript disabled, or using a limited platform (like a cell phone) or to a search engine spider, the headers are disconnected from the content they label.

This script allows you to structure your markup like so:

<div id="mytabset">
  <div class="panel">
    <h3>Tab1</h3>
     Panel stuff 1
  </div>
  <div class="panel">
    <h3>Tab2</h3>
     Panel stuff 2
  </div>
  <div class="panel">
    <h3>Tab3</h3>
     Panel stuff 3
  </div>
</div>

So that to a spider or device with limited rendering capability, the markup is semantically correct.

To turn the above markup into a tab set, just apply the following:

$("#mytabset").semantictabs({
  panel:'panel',                //-- Selector of individual panel body
  head:'h3',                    //-- Selector of element containing panel head
  active:':first'               //-- Selector of panel to activate by default
});

This will 'tabify' the 'mytabset' div, turning the text contained in the H3 elements into the tabs. Styling these is a an exercise for the reader, but generally the following works pretty well:

/*semantic tabs*/
ul.semtabs {
  margin:0 auto;
  clear:both;
  border-bottom: 4px solid #4c77b3;
  height:25px;
  list-style:none !important;
}
ul.semtabs li {
  float:left;
  height:30px;
  display:block;
  margin:0 !important;
  background-image:none;
}
ul.semtabs li a {
/*  height:15px;*/
  line-height:15px;
  display:block;
  padding: 5px 5em;
  text-decoration:none;
  font-weight:bold;
  background-color:#e6eeee;
}
ul.semtabs li.active a {
  background-color: #4c77b3;
  color: #fff;
}
/*end semantic tabs*/

You can also activate a tab programmatically, like so:

$("#appcontainer").semantictabs({activate:2});

Where 2 is the index of the tab you which to activate (starting from zero, of course).

See it in action at http://viewer.mars.asu.edu.

Download from the jQuery Plugins page.

Multi-column lists with jQuery, an alternative method

So I needed a method to take a long, nested list and turning it into a compact, multiple acolumn list, in order to display it as sort of a site map for the home page for a site I'm working on.

Being a huge fan of jQuery, it was naturally my go-to library of choice.

Scanning the plugins site, I found a possible solution from a feller called Ingo Schommer called columnizeList.

Score, right? Well... not exactly, at least for my case.

Ingo used some of the methodoligies outlined in this article on multi-column lists on A List Apart. One of the caveats of his methodology is that each list item has to be the same height. This works Ok for a lot of use cases, but since I'm using a Drupal menu as the source for the list, it could contain arbitrary text I don't control.

So, I started from scratch. Instead of relying on consistent line heights, and applying different margin settings to list elements, I instead decided to decompose the large source list into several smaller lists (one for each column) and then use a css float parameter to make them all appear side-by-side.

Here's a sample list for a demonstration, cribbed from Ingo's example:

  1. harold (3550)
  2. horatio (1320)
  3. hitler (1120)
  4. henry (784)
  5. hector (358)
  6. haploid (315)
  7. hopping (50)
  8. herbert mulroney (44)
  9. hopscotching (29)
  10. hominibus (19)
  11. honkey (19)
  12. hermoine (18)
  13. hieronymus (13)
  14. halliburton (12)
  15. hummer (10)
  16. harlod (10)
  17. heironymious (9)
  18. hemorrhoids (7)
  19. hammersack (6)

(apparently a list of the most common fillers for the middle initial in Jesus H. Christ)

Anyway, here's what my script does to the above list:

  1. harold (3550)
  2. horatio (1320)
  3. hitler (1120)
  4. henry (784)
  5. hector (358)
  6. haploid (315)
  7. hopping (50)
  8. herbert mulroney (44)
  9. hopscotching (29)
  10. hominibus (19)
  11. honkey (19)
  12. hermoine (18)
  13. hieronymus (13)
  14. halliburton (12)
  15. hummer (10)
  16. harlod (10)
  17. heironymious (9)
  18. hemorrhoids (7)
  19. hammersack (6)

And here's the code:


/*
Copyright (c) 2007 Christian yates
christianyates.com
chris [at] christianyates [dot] com
Licensed under the MIT License: 
http://www.opensource.org/licenses/mit-license.php
 
Inspired by work of Ingo Schommer
http://chillu.com/2007/9/30/jquery-columnizelist-plugin
*/
(function($){
  $.fn.columnizeList = function(settings){
    settings = $.extend({
      cols: 3,
      constrainWidth: 0
    }, settings);
    // var type=this.getNodeType();
    var container = this;
    if (container.length == 0) { return; }
    var prevColNum = 10000; // Start high to avoid appending to the wrong column
    var size = $('li',this).size();
    var percol = Math.ceil(size/settings.cols);
    var tag = container[0].tagName.toLowerCase();
    var classN = container[0].className;
    var colwidth = Math.floor($(container).width()/settings.cols);
    var maxheight = 0;
    // Prevent stomping on existing ids with pseudo-random string
    var rand = Math.floor(Math.random().toPrecision(6)*10e6);
    $('<ul id="container'+rand+'" class="'+classN+'"></ul>').css({width:$(container).width()+'px'}).insertBefore(container);
    $('li',this).each(function(i) {
      var currentColNum = Math.floor(i/percol);
      if(prevColNum != currentColNum) {
        if ($("#col" + rand + "-" + prevColNum).height() > maxheight) { maxheight = $("#col" + rand + "-" + prevColNum).height(); }
        $("#container"+rand).append('<li class="list-column-processed"><'+tag+' id="col'+rand+'-'+currentColNum+'"></'+tag+'></li>');
      }
      $(this).attr("value",i+1).appendTo("#col"+rand+'-'+currentColNum);
      prevColNum = currentColNum;
    });
    $("li.list-column-processed").css({
      'float':'left',
      'list-style':'none',
      'margin':0,
      'padding':0
    });
    if (settings.constrainWidth) {
      $(".list-column-processed").css({'width':colwidth + "px"});
    };
    $("#container"+rand).after('<div style="clear: both;"></div>');
    $("#container"+rand+" "+tag).height(maxheight);
    // Add CSS to columns
    this.remove();        
    return this;
  };
})(jQuery);

Download

There are only two parameters - cols, the number of columns to break the list into, and constrainWidth a boolean (defaulting to false) to specify whether you want all columns to be the same width.

I've tested with IE 6&7, FF3, Safari3 and Opera 9.something (for the three Opera users on the planet). The code could use a bit of refactoring perhaps for the purpose of beautification.

Update: I've added this to the jQuery Plugin site.

How to: Save a crapload of money converting from print to web

We spend lots of dough each year converting material from our dead-tree editions into web-friendlier versions for our web sites. We crank out a bunch of PDFs, and send them through the ether to somewhere where the labor is cheap and the workday long, like Vietnam, Indonesia or Canada or something. Then some poor soul slices 'n dices them into jpegs and links and such, and sends 'em on back, and posts them on our site.

So I thought to myself, "Self, you can do that without having to do something silly like use people and worse, pay for it.

It's a work in progress, but it goes something like...

  • Export PDFs of ads from our DTI advertising system, and page PDFs from our Newsway prepress system.
  • Multiplex the PDFs through xpdf, imagemagick and swftools to extract text, convert to bitmaps and convert to Flash files respectively, with some proprietary workflow software. Maybe we'll OCR them with Tesseract if we can get a box with enough CPU horsepower, rather than the virtual machine it's running on, for extra text-extraction points.
  • Combine the files into an XML feed.
  • Send the files to the front-end system. Probably Drupal, but possibly a Rails app, or McClatchy's own Workbench CMS.
  • Display to the user with a combination of flash, jquery and CSS like so:

Shazaam! $35k saved.

Not to mention, jquery almost makes coding javascript fun. Almost.