garann means > totes profesh

a defunct web development blog

hierarchies, taxonomies, categories, drill-downs, etc.

Wed, 25 Feb 2009 17:31:18 +0000
I searched all over the place for a tidy pattern to deal with displaying hierarchical data for selection (as in a set of categories and sub-categories or a taxonomy), and couldn't find anything. I think everyone's aware of how this can be done with AJAX, but I wanted something that would still work without needing Javascript or a round-trip to the server each time a parent category changed. Obviously that means storing the entire taxonomy on the page, which is only realistic for some uses of this sort of control, but it's those uses I'm interested in. My solution is to load the entire taxonomy into an unordered list and include a radio button in each list item. This shows the hierarchy clearly and lets the user select a node of any depth (a requirement in my case). I use some Javascript to hide the radio buttons and show/hide child lists based on clicks on the list items themselves, then check the radio buttons behind the scenes. I'm wondering if this is the best possible solution for my circumstances. My hope is that neatly organizing the data in this way makes it more accessible, but I'm no accessibility expert. The HTML itself is pretty simple (excuse the junk data):
<div id="taxonomy">
	<ul>
		<li><input type="radio" name="ghj" value="A" /> A
			<ul>
				<li><input type="radio" name="ghj" value="I" /> I
					<ul>
						<li><input type="radio" name="ghj" value="1" /> 1
							<ul>
								<li><input type="radio" name="ghj" value="a" /> a</li>
								<li><input type="radio" name="ghj" value="b" /> b</li>
								<li><input type="radio" name="ghj" value="c" /> c</li>
								<li><input type="radio" name="ghj" value="d" /> d</li>
								<li><input type="radio" name="ghj" value="e" /> e</li>
							</ul>
						</li>
						<li><input type="radio" name="ghj" value="2" /> 2</li>
						<li><input type="radio" name="ghj" value="3" /> 3</li>
						<li><input type="radio" name="ghj" value="4" /> 4</li>
						<li><input type="radio" name="ghj" value="5" /> 5</li>
					</ul>
				</li>
				<li><input type="radio" name="ghj" value="II" /> II</li>
				<li><input type="radio" name="ghj" value="III" /> III
					<ul>
						<li><input type="radio" name="ghj" value="6" /> 6
							<ul>
								<li><input type="radio" name="ghj" value="f" /> f</li>
								<li><input type="radio" name="ghj" value="g" /> g</li>
								<li><input type="radio" name="ghj" value="h" /> h</li>
								<li><input type="radio" name="ghj" value="i" /> i</li>
								<li><input type="radio" name="ghj" value="j" /> j</li>
							</ul>
						</li>
						<li><input type="radio" name="ghj" value="7" /> 7</li>
						<li><input type="radio" name="ghj" value="8" /> 8</li>
						<li><input type="radio" name="ghj" value="9" /> 9</li>
						<li><input type="radio" name="ghj" value="10" /> 10</li>
					</ul></li>
				<li><input type="radio" name="ghj" value="IV" /> IV</li>
				<li><input type="radio" name="ghj" value="V" /> V</li>
			</ul>
		</li>
		<li><input type="radio" name="ghj" value="B" /> B</li>
		<li><input type="radio" name="ghj" value="C" /> C</li>
		<li><input type="radio" name="ghj" value="D" /> D</li>
		<li><input type="radio" name="ghj" value="E" /> E</li>
	</ul>
</div>
[caption id="attachment_83" align="aligncenter" width="500" caption="The finished product with some simple styling"]The finished product with some simple styling[/caption] Styling is split up so that positioning the child lists doesn't interfere with the display of the un-enhanced version:
	#taxonomy { padding: 50px; background-color: #fff; height: 300px; }
	#taxonomy ul {
		margin: 0px 0px 0px 10px;
		padding: 10px;
		border: 1px solid #ccc;
		-moz-border-radius: 10px;
		-webkit-border-radius: 10px;
		border-radius: 10px;
		list-style-type: none;
		background-color: #fff;
	}
	#taxonomy ul.dyn {
		position: absolute;
		left: 50px;
		top: 50px;
		height: 280px;
		width: 180px;
	}
	#taxonomy ul ul { border-color: #fff; }
	#taxonomy ul.dyn ul
	{
		position: absolute;
		top: 0px;
		left: 200px;
		display: none;
		height: 280px;
		width: 180px;
	}
	#taxonomy ul.dyn li:hover {
		border: none;
		background-color: #eee;
		-moz-border-radius: 10px;
		-webkit-border-radius: 10px;
		border-radius: 10px;
		width: 100%;
		padding-right: 30px;
		z-index: -1;
	}
	#taxonomy ul.dyn li > ul { background-color: #eee; border-color: #eee; }
	#taxonomy ul.dyn ul li:hover, #taxonomy ul.dyn ul li > ul { border-color: #ddd; background-color: #ddd; }
	#taxonomy ul.dyn ul ul li:hover, #taxonomy ul.dyn ul ul li > ul { border-color: #ccc; background-color: #ccc; }
	#taxonomy ul.dyn ul ul ul li:hover { width: auto; padding-right: 0px; }
Finally, some Javascript breaks the list up and provides its positioning:
	<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
	<script type="text/javascript">
	$(document).ready(function () {
		$("#taxonomy > ul").addClass("dyn");
		$("#taxonomy input").css("visibility","hidden");
		$("#taxonomy li").each(function (i) {
			$(this).bind("click",function(e) {
				$(this).siblings().find("ul").hide();
				$(this).children().find("ul").hide();
				$(this).children("ul:first").show();
				var rad = $(this).children("input");
				rad.attr("checked","true");
				return false;
			});
		});
	});
	</script>
Initially, I was using a noscript block to add this style: input:checked ~ ul {display:block;} and the only visual change the Javascript made was to hide the radio buttons (there was no "dyn" class). That works very well in modern browsers if your hierarchy is only two levels deep or users can only select from the lowest level, as in the classic example of vehicle make, model, and year. It didn't work for my purposes, though, because users can select from any of four levels. It's an easy change to make, though. The only thing to check is that IE6 is getting the correct styling, since it won't recognize many of the selectors used here. I'm going to create a special script to handle that, but if you weren't encumbered by that browser you could skip that step entirely. Whew! So.. Is there a better way to accomplish this?