Calendar action

Last edited by JavaWoman:
Modified links pointing to docs server
Mon, 28 Jan 2008 00:14 UTC


History
GmBowen posted a nice little Calendar action on GmBowenCalendar which drew a lot of comments (and suggestions), and which JsnX proposed to include in the upcoming (1.1.6.0) version of Wikka. I commented that I'd like to see the code "cleaned up" before inclusion, and offered to do that. The result is here.

Different output


Data table markup
The original (including the ultimate original which we've traced the code back to - see the comments on GmBowenCalendar) code uses a table to present the calendar but the markup is that for a "layout" table, not a data table.

What's the difference? A layout table has nothing but a table tag (table), table rows (tr), and table data cells (td) within the rows. However, a data table is to present data in relation to each other; a calendar clearly is a data table, showing dates in a month with day names labelling groups of dates (and possibly more). In order to show relationships between data, a data table uses not only table data cells (td), but also header cells (th) to label the data, and preferably a caption (caption) that labels what the whole thing is about.

A good article about marking up data tables:
Looking at a common "calendar face" the candidates for header cells and caption are obvious: clearly the (abbreviated) day names are headers, and the name of the month at the top logically is a caption. That leaves the navigation, however, which I've moved to a separate section at the bottom. While I was at it, I took up DarTar's suggestion for a link (back) to the current month since moving the navigation links to the bottom left a space in the middle.

The result looks like this (now that my code is active, this page can have multiple calendars - see another example below):
December 2017
Sun Mon Tue Wed Thu Fri Sat
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            
<< = >>
implemented with {{calendar}}


Making it accessible
Accessible table code starts with proper data table markup, but requires a bit more. To start with, the table should have a summary attribute to explain what it's about; also, the header cells should actually be "linked" to the data cells they refer to, and obviously the navigation links (being just symbols here) need an explanation as well, which is added in the form of a title attribute. Similarly, the data cell for "today" (if shown) gets a title as well (since we should not depend on color alone to convey information). Finally a little trick suggested on an accessibility mailing list: using the abbr attribute on the headers to expand the day names (an inverse of the original purpose of this attribute, but some screen readers used by people with a visual impairment can make use of this).

More about accessible table markup:

Extra functionality


"Dynamic" and "static" calendars
My variant of the Calendar action does all that the original GmBowenCalendar does, and one thing extra:
The result is that you can show a dynamic calendar which "reacts" to URL parameters in addition to any number of static calendars for a specific month.

I'll include code for a static month below - if (as long as) my proposed code is implemented, you should see here a calendar for March 2006:
March 2006
Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
implemented with {{calendar month="3" year="2006"}}


Different code


A pattern for an action
The new code follows a specific "pattern" which I use for all my (new or modified) action code. In general it goes like this:
  1. Constants section: A section where all constants are defined; this includes constants used as defaults for possible optional acrion parameters as well as constants defining user-interface strings (so that whatever needs to be translated for i18n is grouped together), and any other constants needed. Also "alsmost" constants like lookup tables are defined here.
  2. Parameters section: A section where parameters are read, and validated, using whatever is defined as defaults for optional parameters (constants, or dynamic defaults such as "current month"). In this case, invalid or otherwise unusable parameters are silently ignored (using defaults instead); depending on the purpose of an action, an invalid parameter (or missing required parameter) may generate an error message instead.
  3. Data preparation section: A section where input is further processed into all variables needed to generate the output.
  4. Output section: At this point all data is prepared, so the final section does nothing but generate output: no new data is generated here, we only look up data prepared in the parameter or data preparation section (we can loop through an array though). (This is the same approach as with using a templating solution such as Smarty.)

This results in a clear and consistent logic of the code, and separation of process and data from presentation of the data.

Dealing with multiple calendars
The original code had a little function to derive the last day in a month; not only was this a somewhat inefficient, but having a function conflicted with including the code multiple times. The function has been replaced by (simpler) code in the data preparation section.

Calendar generating logic
Another difference is in the logic used for the start of the first week (when we may need to generate "blank" cells): this is taken out of the (original) loop, resulting in more logical and efficient code. The inverse of that is added at the end so that where necessary blank cells are added at the end of the table (having fewer cells in a row is not valid markup!).

Internationalization
Apart from two or three strings (defined in the constants section), the output produced by the action is completely internationalized: by using the appropriate functions the names for months and days (short and long) are already corresponding to the defined locale.

Documentation
First, there is a documentation block at the start in phpDocumentor format; from this documention block (combined with that for other Wikka code) we will not only be able to generate developer's documentation but also (with a special little Wikka parser) dynamic end user's documentation for the action.

In addtion, there are lots of other comments throughout the code; for instance, every line that contains code dealing with internationalization is marked with 'i18n'.

The code


PHP code for the action
Note that I've classified this as "version 0.8" since there are a few (non-essential) things left to do (see @todo in the documentation block). Further explanations above...

<?php
/**
 * Display a calendar face for a specified or the current month.
 *
 * Specifying a month and/or year in the action itself results in a "static" calendar face without
 * navigation; conversely, providing no parameters in the action results in a calendar face with
 * navigation links to previous, current and next month, with URL parameters determining which
 * month is shown (with the current month as default).
 *
 * You can have one "dynamic" (navigable) calendar on a page (multiple ones would just be the same)
 * and any number of "static" calendars.
 *
 * The current date (if visible) gets a special class to allow a different styling with CSS.
 *
 * Credit:
 * This action was inspired mainly by the "Calendar Menu" code written by
 * {@link http://www.blazonry.com/about.php Marcus Kazmierczak}
 * (© 1998-2002 Astonish Inc.) which we traced back as being the ultimate origin of this code
 * although our starting point was actually a (probably second-hand) variant found on the web which
 * did not contain any attribution.
 * However, not much of the original code is left in this version. Nevertheless, credit to
 * Marcus Kazmierczak for the original that inspired this, however indirectly: Thanks!
 *
 * @package     Actions
 * @subpackage  Date and Time
 * @name        Calendar
 *
 * @author      {@link http://wikka.jsnx.com/GmBowen GmBowen} (first draft)
 * @author      {@link http://wikka.jsnx.com/JavaWoman JavaWoman} (more modifications)
 * @version     0.8
 * @since       Wikka 1.1.6.0
 *
 * @input       integer  $year  optional: 4-digit year of the month to be displayed;
 *              default: current year
 *              the default can be overridden by providing a URL parameter 'year'
 * @input       integer  $month  optional: number of month (1 or 2 digits) to be displayed;
 *              default: current month
 *              the default can be overridden by providing a URL parameter 'month'
 * @output      data table for specified or current month
 *
 * @todo        - take care we don't go over date limits for PHP with navigation links
 *              - configurable first day of week
 */


// ***** CONSTANTS section *****
define('MIN_DATETIME', strtotime('1970-01-01 00:00:00 GMT'));       # earliest timestamp PHP can handle (Windows and some others - to be safe)
define('MAX_DATETIME', strtotime('2038-01-19 03:04:07 GMT'));       # latest timestamp PHP can handle
define('MIN_YEAR', date('Y',MIN_DATETIME));
define('MAX_YEAR', date('Y',MAX_DATETIME)-1);                       # don't include partial January 2038
// not-quite-constants
$daysInMonth = array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
define('CUR_YEAR', date('Y',mktime()));
define('CUR_MONTH', date('n',mktime()));
// format string for locale-specific month (%B) + 4-digit year (%Y) used for caption and title attributes
// NOTE: monthname is locale-specific but order of month and year may need to be switched: hence the double quotes!
define('LOC_MON_YEAR', "%B %Y");                                                                    # i18n
define('FMT_SUMMARY', "Calendar for %s");                                                           # i18n
define('TODAY', "today");                                                                           # i18n
// ***** END CONSTANTS section *****

// ***** (ACTION) PARAMETERS Interface *****
// set parameter defaults: current year and month
$year   = CUR_YEAR;
$month  = CUR_MONTH;

// get and interpret parameters
// 1) overrride defaults with parameters provided in URL (accept only valid values)
if (isset($_GET['year']))
{
    $uYear = (int)$_GET['year'];
    if ($uYear >= MIN_YEAR && $uYear <= MAX_YEAR) $year = $uYear;
}
if (isset($_GET['month']))
{
    $uMonth = (int)$_GET['month'];
    if ($uMonth >= 1 && $uMonth <= 12) $month = $uMonth;
}
// 2) override with parameters provided in action itself (accept only valid values)
$hasActionParams = FALSE;
if (is_array($vars))
{
    foreach ($vars as $param => $value)
    {
        switch ($param)
        {
            case 'year':
                $uYear = (int)trim($value);
                if ($uYear >= MIN_YEAR && $uYear <= MAX_YEAR)
                {
                    $year = $uYear;
                    $hasActionParams = TRUE;
                }
                break;
            case 'month':
                $uMonth = (int)trim($value);
                if ($uMonth >= 1 && $uMonth <= 12)
                {
                    $month = $uMonth;
                    $hasActionParams = TRUE;
                }
                break;
        }
    }
}
// ***** (ACTION) PARAMETERS Interface *****

// ***** DERIVED VARIABLES *****
// derive which weekday the first is on
$datemonthfirst = sprintf('%4d-%02d-%02d',$year,$month,1);
$firstwday = strftime('%w',strtotime($datemonthfirst));                                             # i18n

// derive (locale-specific) caption text
$monthYear  = strftime(LOC_MON_YEAR,strtotime($datemonthfirst));                                    # i18n
$summary    = sprintf(FMT_SUMMARY, $monthYear);                                                     # i18n

// derive last day of month
$lastmday = $daysInMonth[$month - 1];
if (2 == $month)                                                    # correct for leap year if necessary
{
    if (1 == date('L',strtotime(sprintf('%4d-%02d-%02d',$year,1,1)))) $lastmday++;
}

// derive "today" to detect when to mark this up in the calendar face
$today = date("Y:m:d",mktime());

// build navigation variables - locale-specific (%B gets full month name)
// FIXME: @@@ take care we don't go over date limits for PHP
if (!$hasActionParams)
{
    // previous month
    $monthPrev  = ($month-1 < 1) ? 12 : $month-1;
    $yearPrev   = ($month-1 < 1) ? $year-1 : $year;
    $parPrev    = "month=$monthPrev&amp;year=$yearPrev";
    $urlPrev    = $this->Href('', '', $parPrev);
    $titlePrev  = strftime(LOC_MON_YEAR,strtotime(sprintf('%4d-%02d-%02d',$yearPrev,$monthPrev,1)));# i18n
    // current month
    $parCur     = 'month='.CUR_MONTH.'&amp;year='.CUR_YEAR;
    $urlCur     = $this->Href('', '', $parCur);
    $titleCur   = strftime(LOC_MON_YEAR,strtotime(sprintf('%4d-%02d-%02d',CUR_YEAR,CUR_MONTH,1)))# i18n
    // next month
    $monthNext  = ($month+1 > 12) ? 1 : $month+1;
    $yearNext   = ($month+1 > 12) ? $year+1 : $year;
    $parNext    = "month=$monthNext&amp;year=$yearNext";
    $urlNext    = $this->Href('', '', $parNext);
    $titleNext  = strftime(LOC_MON_YEAR,strtotime(sprintf('%4d-%02d-%02d',$yearNext,$monthNext,1)));# i18n
}

// build array with names of weekdays (locale-specific)
$tmpTime    = strtotime("this Sunday");         # get a starting date that is a Sunday
$tmpDate    = date('d',$tmpTime);
$tmpMonth   = date('m',$tmpTime);
$tmpYear    = date('Y',$tmpTime);
for ($i=0; $i<=6; $i++)
{
    $aWeekdaysShort[$i] = strftime('%a',mktime(0,0,0,$tmpMonth,$tmpDate+$i,$tmpYear));
    $aWeekdaysLong[$i]  = strftime('%A',mktime(0,0,0,$tmpMonth,$tmpDate+$i,$tmpYear));
}
// ***** END DERIVED VARIABLES *****

// ***** OUTPUT SECTION *****
?>

<table cellpadding="2" cellspacing="1" class="calendar" summary="<?php echo $summary;?>">
<caption><?php echo $monthYear;?></caption>
<thead>
    <tr>
<?php
for ($i=0; $i<=6; $i++)
{
?>
        <th scope="col" width="26" abbr="<?php echo $aWeekdaysLong[$i];?>"><?php echo $aWeekdaysShort[$i];?></th>
<?php
}
?>
    </tr>
</thead>
<tbody class="face">
<?php
    // start row for first week (if it doesn't start on Sunday)
    if ($firstwday > 0)
    {
        echo "  <tr>\n";
    }
    // fill start of first week with blank cells before start of month
    for ($i=1; $i<=$firstwday; $i++)
    {
        echo '      <td>&nbsp;</td>'."\n";
    }

    // loop through all the days of the month
    $day = 1;
    $wday = $firstwday;
    while ($day <= $lastmday)
    {
        // start week row
        if ($wday == 0)
        {
            echo "  <tr>\n";
        }
        // handle markup for current day or any other day
        $calday = sprintf('%4d:%02d:%02d',$year,$month,$day);
        if ($calday == $today)
        {
            echo '      <td title="'.TODAY.'" class="currentday">'.$day."</td>\n";
        }
        else
        {
            echo '      <td>'.$day."</td>\n";
        }
        // end week row
        if ($wday == 6)
        {
            echo "  </tr>\n";
        }
        // next day
        $wday = ++$wday % 7;
        $day++;

    }

    // fill week with blank cells after end of month
    if ($wday > 0)
    {
        for ($i=$wday; $i<=6; $i++)
        {
            echo '      <td>&nbsp;</td>'."\n";
        }
    }
    // end row for last week
    if ($wday < 6)
    {
        echo "  </tr>\n";
    }
?>
</tbody>
<?php
// generate navigation only for calendar without (valid) action parameters!
// FIXME: @@@ take care we don't go over date limits for PHP
if ($hasActionParams === FALSE)
{
?>
<tbody class="calnav">
    <tr>
        <td colspan="3" align="left" class="prevmonth"><a href="<?php echo $urlPrev;?>" title="<?php echo $titlePrev;?>">&lt;&lt;</a></td>
        <td align="center" class="curmonth"><a href="<?php echo $urlCur;?>" title="<?php echo $titleCur;?>">=</a></td>
        <td colspan="3" align="right" class="nextmonth"><a href="<?php echo $urlNext;?>" title="<?php echo $titleNext;?>">&gt;&gt;</a></td>
    </tr>
</tbody>
<?php
}
?>
</table>
<?php
// ***** END OUTPUT SECTION *****
?>


Corresponding CSS code
The code provides a nice layout for the data table and has some comments with hints for variants. To be added at the end of wikka.css.
/* Calendar styling - added 2004-11-30 - updated 2004-12-01 */
/* general styling */
table.calendar {
    color: #000000;
    background-color: #CCCCCC;      /* comment out to have space between cells same color as page background */
    /*border-collapse: collapse;*/  /* would make single-width borders, ignoring cell-spacing */
}
table.calendar caption {
    background-color: #CCCCCC;
    font-weight: bold;
    line-height: 1.6em;
}
table.calendar thead {
    background-color: #CCCCCC;
}
table.calendar tbody.face {
    background-color: #CCCCCC;
}
table.calendar tbody.calnav {
    background-color: #CCCCCC;
}
/* styling for some specific elements */
table.calendar thead th {
    /*border: 1px solid #000000;*/  /* uncomment to have border around day name headers (will be page background if table background is undefined) */
    padding: 1px;
    text-align: center;
    font-size: 85%;
    width: 26px;
}
table.calendar tbody.face td {
    border: 1px solid #000000;
    text-align: right;
}
table.calendar td.currentday {
    color: #993333;
    background-color: #AAAAAA;
    font-weight: bold;
}
/* styling of calendar navigation */
table.calendar tbody.calnav {
    font-weight: bold;
}
table.calendar td.prevmonth {
    text-align: left;
    font-size: 85%;
}
table.calendar td.curmonth {
    text-align: center;
}
table.calendar td.nextmonth {
    text-align: right;
    font-size: 85%;
}
table.calendar a:link {
    color: #993333;
    text-decoration: none;
}
table.calendar a:visited {
    color: #993333;
    text-decoration: none;
}
table.calendar a:hover {
    color: #993333;
}
table.calendar a:active {
    color: #993333;
    text-decoration: none;
}


Comments?

Comments and suggestion welcome. And please give it a good workout (using combinations of URL parameters and action parameters!) before including it in the upcoming release.

Finally: The @since tag in the documentation block assumes this will be included in Wikka release 1.1.6.0 - adapt or remove as needed...

--JavaWoman
There are 10 comments on this page. [Show comments]
Valid XHTML :: Valid CSS: :: Powered by WikkaWiki