Background
Unsatisfied with existing implementations of Miller Columns (used to edit hierarchical data), I decided to re-invent the wheel.
Problem
Most of the existing implementations are too complex, offer extraneous functionality, reliance on complex (or unavailable) frameworks, have numerous dependencies, are riddled with severe bugs, or require non-hierarchical data.
Source
The source code has two parts. The JavaScript code performs a breadth-first traversal of an arbitrarily nested set of lists and wraps them into a consecutive, flattened hierarchy surrounded by div
tags. The CSS code places the div
s in left-floating columns (and applies superficial presentation items).
(function( $ ) {
$.fn.millerColumns = function() {
var $list = $(this).first();
var $columns = $(this);
// Breadth-first traversal to rearrange list items into
// consecutively ordered div wrapper elements.
while( ($list = $list.children()).length ) {
$list.each( function( index, element ) {
var $parent = $(element).parent();
if( $(element).is( "li" ) ) {
$parent = $($parent).parent();
}
// Store the parent id for showing child columns.
var id = $($parent).attr( "id" );
if( $(element).is( "ul" ) ) {
// The parent element shall be marked as 0.
if( id === undefined ) {
id = 0;
}
var $item = $("li#" + id);
$item.addClass( "parent" );
$item.on( "click", function() {
// Hide everything.
$("div.column[data-parent!=0]").addClass( "collapsed" );
$("li").removeClass( "selection" );
// The "id" for the clicked list item becomes the start of
// the ancestral chain.
var $child = $("div.column[data-parent=" + id + "]" );
$child.removeClass( "collapsed" );
var $li = $("li.parent[id=" + id + "]");
var $ancestor = $li.parent().parent();
var ancestor_id = $ancestor.attr( "data-parent" );
while( ancestor_id !== undefined ) {
$li.addClass( "selection" );
$ancestor.removeClass( "collapsed" );
$li = $("li.parent[id=" + ancestor_id + "]");
$ancestor = $li.parent().parent();
ancestor_id = $ancestor.attr( "data-parent" );
}
$li.addClass( "selection" );
var $breadcrumb = $("div.breadcrumb");
$breadcrumb.empty();
// Add the breadcrumb trail.
$("li.selection").each( function( _, crumb ) {
$breadcrumb.append( "<span>" + $(crumb).text() + "</span>" );
});
});
var $div = $("<div>").attr( "data-parent", id );
var $wrapped = $(element).wrapAll( $div );
// Unnest the list items into contiguous div elements.
$wrapped.parent().detach().appendTo( $columns ).addClass( "column" );
}
});
}
$columns.prepend( $("<div>").addClass( "breadcrumb" ) );
$columns.append( $("<div>").addClass( "toolbar" ) );
// Hide all the columns except the root.
$("div.column[data-parent!=0]").addClass( "collapsed" );
return this;
};
$("div.columns").millerColumns();
}(jQuery));
div.columns,
div.breadcrumb {
border-top-left-radius: 0.4em 0.4em;
border-top-right-radius: 0.4em 0.4em;
}
div.columns,
div.toolbar {
border-bottom-left-radius: 0.4em 0.4em;
border-bottom-right-radius: 0.4em 0.4em;
}
div.columns {
margin-top: 1em;
margin-bottom: 1em;
min-height: 150px;
overflow: auto;
background: #949494;
}
div.column {
float: left;
display: inline-block;
background: #FFF;
border-right: 1px solid #666;
font-size: 0.9em;
font-family: Arial;
}
div.collapsed {
display: none;
}
li.parent:hover {
color: black;
background-color: #DDE4E8;
}
/* Ensure the selection hierarchy is highlighted. */
li.selection {
background-color: #08C !important;
color: white !important;
}
div.breadcrumb,
div.toolbar {
height: 1.25em;
}
div.breadcrumb,
div.toolbar {
background: linear-gradient(#f0f0f0, #d8d8d8);
background: -webkit-linear-gradient(#f0f0f0, #d8d8d8);
background: -o-linear-gradient(#f0f0f0, #d8d8d8);
background: -moz-linear-gradient(#f0f0f0, #d8d8d8);
}
div.breadcrumb {
border-bottom: 1px solid #666;
}
div.breadcrumb > span {
height: 1.25em;
line-height: 1.25em;
}
div.toolbar {
clear: both;
border-top: 1px solid #666;
}
div.breadcrumb > span {
font-size: 0.7em;
font-weight: bold;
padding-left: 0.25em;
color: #666;
}
div.breadcrumb > span::after {
content: " \25B8";
}
div.breadcrumb > span:first-child {
padding-left: 1em;
}
div.breadcrumb > span:last-child:after {
content: "";
}
ul {
padding: 0;
margin: 0;
overflow: auto;
}
li {
list-style: none;
padding-left: 0.5em;
padding-right: 0.5em;
padding-top: 0.25em;
padding-bottom: 0.25em;
min-width: 150px;
}
/* Use an arrow to indicate that the parent has child elements. */
li.parent:after {
content: "\25B8";
float: right;
}
/* Zebra stripes. */
li:nth-child(odd) {
background-color: #EEE;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div class="columns">
<ul>
<li id="1">Item A</li>
<li id="2">Item B
<ul>
<li id="21">Item BA</li>
<li id="22">Item BB</li>
</ul>
</li>
<li id="3">Item C
<ul>
<li id="31">Item CA</li>
<li id="32">Item CB</li>
<li id="33">Item CC
<ul>
<li id="331">Item CCA</li>
<li id="332">Item CCB</li>
<li id="333">Item CCC</li>
</ul>
</li>
<li id="34">Item CD</li>
<li id="35">Item CE</li>
</ul>
</li>
</ul>
</div>
Question
The JavaScript code, with comments, is 80 lines long.
How can the algorithm to produce the drillable columns be simplified?
For example, the following lines are virtually duplicated within the while loop:
var $li = $("li.parent[id=" + id + "]");
var $ancestor = $li.parent().parent();
var ancestor_id = $ancestor.attr( "data-parent" );
Notes
Note that the code, as presented, has a few known issues, including:
- Horizontal scrolling does not work.
- Vertical scrolling does not work.
- Any column that does not fit the width will appear below the previous column.
- There is no keyboard interface.