2005-08-22

More calendar widgetry

This is a follow-up article to my recent entry on setting up Blogger with calendar navigation. I've been polishing it some more to better fit my preferences.

Actually, I was just following my own suggested further improvement, in adding the popup variant, which has some really nifty keyboard navigation added in, right out of the box --

  • ←, →, ↑, ↓ -- select date
  • CTRL + ←,→ -- select month
  • CTRL + ↑,↓ -- select year
  • SPACE -- go to today's date
  • ENTER -- go to the selected date
  • ESC -- cancel selection
-- but come that far, I noticed that all the disabled dates in view could still be focused, only just not visibly so. So it was really a bit like navigating with a paper with holes punched for the legal choices stuck in front of the screen. You'd notice when you hit an available date, but otherwise be generally lost. Not pretty.

So I set about tweaking the code to allow my preferred mode of browsing -- focus snapping only to dates that can actually be focused and selected. As it turns out, jscalendar is really a SourceForge project too, besides being LGPL, so I took the liberty of submitting a patch with my changes when I was happy with them.

An example setup is running on my real life blog; click the calendar icon in the sidebar to take it out for a spin, if I still haven't already updated the template in this blog too, by the time you read this.

So, what changes do you need to do to get this baby running?

Not much, really. It's mostly a matter of adding a method that will seek out a date to snap to, and provide it during Calendar setup. I did it like this:

  Calendar.setup(
{
step : 1, // show *every* year in the year menus
date : thisDate, // the date selected by default
align : 'br', // below/right from top/left corner
button : 'navigate', // click this element to open
onUpdate : dateChanged, // navigates to chosen entry
pickPrevNext : pickPrevNext, // can only focus our dates
dateStatusFunc: disableDateP // which dates to show/hide how
});

where most is kept intact from before (I've added an <img id="navigation" style="cursor:hand;cursor:pointer;" .../> to the mix, to trigger the popup, too -- the ugly style rule is to get a mouseover hint that works in most browsers too while hovering the icon).

You also have to provide the pickPrevNext method, though, and in my case it looks like this (together with the slightly changed disableDateP and init code):

// prevP == true: find the last value in ok to satisfy ok[n] <= pick,
// prevP != true: find the first value in ok to satisfy ok[n] >= pick
function pickPrevNext( pick, prevP )
{
var ok = top.notelist;
pick = pick.getTime();
if( prevP ) // find last x <= pick
{
for( var i=ok.length-1; i>=0; i-- )
if( ok[i] <= pick )
return new Date( ok[i] );
}
else // find first x >= pick
for( var i=0; i<ok.length; i++ )
if( ok[i] >= pick )
return new Date( ok[i] );
return null;
}

function disableDateP( date, y, m, d )
{
var now = new Date;
if( (y == now.getFullYear()) &&
(m == now.getMonth()) &&
(d == now.getDate()) )
return false;
var t = new Date( y, m, d );
return noteFromDate( t ) ? false : true;
}

function noteFromDate( date )
{
return top.notes[date.getTime()];
}

function renderCal()
{
var archive = document.getElementById( 'archive' );
if( archive )
{
archive.style.display = 'none';
var notes = {};
top.notelist = [];
var links = archive.getElementsByTagName( 'a' );
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 );
m = parseInt( date[1], 10 );
d = parseInt( date[2], 10 );
t = (new Date( y, m-1, d )).getTime();
notes[t] = node.href;
top.notelist.push(t);
}
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;
// the above Calendar.setup() call goes here.
}
}

That wasn't very hard either, was it? Yes, one more thing -- I changed the init method to read renderCal, after having read in the docs that the popup code might want to zap window.calendar, hence not making that a very good name to use for our init method. So you'll want to update your body tag to read <body onload="renderCal()"> as well.

It's still not quite all I expect of it -- for instance, the dropdown menus don't adhere to the same navigation principle -- you can still pick months there which have no legal dates, or even if you pick one that does, you won't arrive at the note closest to the note you came from. I'll just have to leave that for another day, or another hacker.
blog comments powered by Disqus