2005-08-20

Adding calendar navigation to your Blogger template

A later follow-up article is available on this topic, where I dig into making a pop-open calendar with keyboard bindings for navigating back and forth through your post archives, among other things.

Not caring much for Blogger's idea of navigation for my personal diary, today I set about adding a calendar widget similar to one I have been accustomed to having last time I was blogging, in a self-hosted do-it-all-yourself environment. Lazy-bastardness has changed that situation since, but I digress -- let's get down to business, shall we?

Someone probably made a decent calendar widget in the past few years, right? Right. Good; then I won't waste a lot of time boring my head off tweaking HTML+CSS, which I, quite honestly, detest.

Okay, download and unzip the 1.0 release, and dig in. First things first, we put it in a suitable location on some hosting of our own, something like http://www.example.com/jscalendar-1.0/ (you probably have it on some different location, yadda yadda) and toss in the necessary bits to add it to your template's <head> portion. I tucked mine in just before the end of the head section, since we will be tweaking other things in the vicinity very soon anyway. So here we go; add these lines:

<style type="text/css">@import url("/skins/aqua/theme.css");</style>
<script type="text/javascript" src="/calendar.js"></script>
<script type="text/javascript" src="/lang/calendar-en.js"></script>
<script type="text/javascript" src="/calendar-setup.js"></script>

(don't forget to prepend your URL prefix to the src/url arguments!) and we now have a beautiful bit of GNU LGPL calendar code loaded and ready, just aching to get to do something for us. So, pick a spot in your template where you want the finished calendar widget to go, and add an empty <div id="calendar-container"></div> tag there. If you're eager to try out a dummy right away, by all means go ahead: temporarily add this right after the div:

<script type="text/javascript"><!--
Calendar.setup( {flat:'calendar-container'} );
//--></script>

(Tada!)

Okay, not very interesting before it does anything; I agree. Before we continue, though, we need to tweak some settings (or you need to rewrite some of my code to fit your own preferences). First, track down Settings -> Archiving -> Archive Frequency and set it to "Daily". (You need this to get each entry in your blog listed in the page - and hence to show up in the calendar.) Then (and the order you perform these changes is important) set Settings -> Formatting -> Archive Index Date Format to 2005-08-20 (ISO YYYY-MM-DD format; very nice to parse). In case you're alienated by practical date formats, don't panic; the visitor will not see these dates anyway. Setting Date Header Format will however be visible, but my example code further down will assume you did, so if you are lazy or share my preferences you'll want to have ISO dates here, too. These are the dates shown near the head of a post in your average Blogger template, and they are used by the script to detect which date should be highlighted as "this post". But now I'm getting ahead of myself.

Now dive into your template and look up the archive portion - it's probably surrounded by a pair of <MainOrArchivePage></MainOrArchivePage> tags. Replace those with a <div id="archive"></div> tag (since you want your navigation widget on all pages, including the single-day's-posts page), and since we want the archive portion easily targetable for hiding.

That could be done with a CSS style rule #archive { display:none; }, but I opted to perform the hiding from javascript instead, knowing that there still are a few who navigate the web with javascript turned off. This way, you won't condemn them to live without navigation, even though they don't get the benefit of your spiffy DHTML generated-live calendar widget. A shame, but that's their choice, and it's very decent of you to degrade gracefully.

Make sure the contents of that div contains something like

<BloggerArchives>
<li><a href="<$BlogArchiveURL$>"><$BlogArchiveName$></a></li>
</BloggerArchives>

(the <li> and any other additional tags besides the <a> tag aren't needed, but don't hurt either). Just make sure the div swallows up every bit you want to hide for the crows who can see your nifty calendar. Now drop the proof-of-concept <script> tag you might have tossed in below the other div, and finally add this chunk at the end of your <head> tag:

<script type="text/javascript"><!--
function calendar()
{
var archive = document.getElementById( 'archive' );
if( archive )
{
archive.style.display = 'none';
var notes = {};
var links = archive.getElementsByTagName( 'a' );
if( !links.length ) return;
var i, j, node, date, y, m, d;
for( i=0; i<links.length; i++ )
{
node = links[i];
date = node.innerHTML.split('-'); // YYYY-MM-DD
y = parseInt( date[0], 10 ); if(!notes[y]) notes[y] = {};
m = parseInt( date[1], 10 ); if(!notes[y][m]) notes[y][m] = {};
d = parseInt( date[2], 10 ); notes[y][m][d] = node.href;
}
var dates = document.getElementsByTagName( 'h2' ), thisDate;
for( i=0; i<dates.length; i++ )
if( dates[i].className == 'date-header' )
{
var ymd = dates[i].innerHTML.split('-'); // YYYY-MM-DD
thisDate = new Date( parseInt( ymd[0], 10 ),
parseInt( ymd[1], 10 )-1,
parseInt( ymd[2], 10 ) );
break;
}
top.notes = notes;
Calendar.setup(
{
step : 1, // show every year in the year menus
date : thisDate, // selected by default
flat : 'calendar-container', // div element
range : [ parseInt(links[0].innerHTML), y ],
showOthers : true, // show whole first/last week of month
flatCallback : dateChanged, // what to do on date selection
dateStatusFunc: disableDateP // which dates to show/hide how
});
}
}

// Returns true for all dates lacking a note, false or a css style for those having one.
// Exception: today does not return true, even if it lacks a note. (improves navigation)
function disableDateP( date, y, m, d )
{
var now = new Date;
if( (y == now.getFullYear()) &&
(m == now.getMonth()) &&
(d == now.getDate()) )
return false;
return noteFromDate( date ) ? false : true;
}

function noteFromDate( date )
{
var note = top.notes[date.getFullYear()] || {};
note = note[date.getMonth()+1] || {};
return note[date.getDate()];
}

function dateChanged( calendar )
{
if( calendar.dateClicked )
{
var note = noteFromDate( calendar.date );
if( note )
window.location = note;
}
}//--></script>

and edit the <body> tag following it to read <body onload="calendar()"> -- and you're all set to go! Dates with entries on them are clickable, and you can navigate around among the years and months as you well please, without any time-consuming web server roundtrips too.

Of course, feel free to experiment, perhaps most easily with the parameters in the Calendar.setup() call, or indeed with any other aspect; having the date-header parsing code match your date listing preferences or whatnot. Another good exercise is probably trying out a layout where you add the widget as a popup rather than the flat embedded one - that way you'll get extremely nifty keyboard navigation via the arrow (and control) keys too in the widget.

Share and enjoy! Spread the love! :-) And feel free to link your creative results.

Note: you always have to click "Republish Entire Blog" when adding a new date that previously had no entry, if you want your visitors to be able to click back in time and then find their way back to the future again using the calendar. Remember, already published pages won't know of your latest entry otherwise, and will thus not make it clickable.
blog comments powered by Disqus