/*

Javascript DOM Calendar
Version 0.5
Copyright (C) 2005-2006 Greg Methvin (greg@methvin.net)
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.

* The name of the author may not be used to endorse or promote
  products derived from this software without specific prior written
  permission

THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.


This script allows you to create a simple calendar/date picker
that you can customize using CSS. It offers the ability to pass
the date as an argument to a function that you specify, so you
can use that to change properties of the calendar element or
modify other elements on the page.

*/

/*
addEvent: Add an event listener to an element

Boolean addEvent(Element el, String ev, Function fn)

Parameters:
    * el: Required. The Element to which to apply the event listener
    * ev: Required. A String specifying the event type.
    * fn: Required. A Function to call when the event occurs.
Returns:
    true if successful, false otherwise.
*/
var addEvent = function(){};

if (document.getElementById && document.createElement)
    if (window.addEventListener) // W3C DOM
        addEvent = function(el, ev, fn) { el.addEventListener(ev, fn, false); return true; };
    else if (window.attachEvent) // IE
        addEvent = function(el, ev, fn) { return el.attachEvent("on"+ev, fn); };

/*
cancelEvent: Cancel the default action of an event

void cancelEvent(Event e)

Parameters:
    * e: The Event to cancel (passed as a parameter to event handlers). Optional in IE, where window.event is used.
*/
function cancelEvent(e)
{
    if (e && e.cancelable && e.preventDefault) // W3C DOM
        e.preventDefault();
    else if (window.event) // IE
        window.event.returnValue = true;
}

/*
DateUtils Object
Used for static utility methods for working with dates.
*/
function DateUtils() {}

/*
getWeekNumber: Return the week number for a given date.

Number getWeekNumber(Date date [, Number firstDayOfWeek])

Parameters:
    * date: Optional. A Date for which to get the week number. The default is the current date.
    * year: Optional. A Number specifying the full year for which the week should be counted.
    * firstDayOfWeek: Optional. The day number (0-6) of the day to use as the first day of the week.
Returns:
    A Number that gives the number of full weeks since the beginning of the last week of the previous year.
    (Week 1 is the first full week in the year. An incomplete week starting a year is week 0.)
*/
DateUtils.getWeekNumber = function(date, year, firstDayOfWeek)
{
    date = date || new Date();
    year = year || date.getFullYear();
    var beginDate = this.getDateFromWeekNumber(year, 0, firstDayOfWeek || 0);
    return Math.floor(((date.getTime() - beginDate.getTime()) / (24*60*60*1000) + 0.1) / 7);
};

/*
getDateFromWeekNumber: Return the date of the beginning of a week with a known week number.

Date getDateFromWeekNumber(Number year, Number weekNum [, Number firstDayOfWeek])

Parameters:
    * year: Required. A Number specifing the year for which the week is counted.
    * weekNum: Required. A Number specifing the week number.
    * firstDayOfWeek: Optional. The day number (0-6) of the day to use as the first day of the week.
Returns:
    A Date at the beginning of the week specified.
*/
DateUtils.getDateFromWeekNumber = function(year, weekNum, firstDayOfWeek)
{
    var beginDate = new Date(year, 0, 1);
    var firstDay = (beginDate.getDay() - (firstDayOfWeek || 0) % 7) % 7 || 7;
    beginDate.setTime(beginDate.getTime() - firstDay * (24*60*60*1000));
    var date = new Date();
    date.setTime(beginDate.getTime() + weekNum * (7*24*60*60*1000));
    return date;
};


// Default options for Calendar
Calendar.monthNames = ["January","February","March","April","May","June","July","August","September","October","November","December"];
Calendar.dayNames = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
Calendar.firstDayOfWeek = 0;

/* 
Calendar Object

void Calendar([Number year [, Number month]])

Parameters:
    * year: Optional. A Number specifing the year for which the week is counted.
    * month: Optional. A Number specifing the number of the month of the year starting at 0 (0 to 11).
*/
function Calendar(year, month)
{
    if (!document.getElementById || !document.createElement)
        return;
    
    var self = this; // Used where this does not refer to the object.
    
    /* monthNames: An Array of String month names to be displatyed for the months. */
    this.monthNames = Calendar.monthNames;
    
    /* dayNames: An Array of String day names to be displayed for the days. */
    this.dayNames = Calendar.dayNames;
    
    /* firstDayOfWeek: A Number (0-6) specifying the day of to use as the first day of the week. */
    this.firstDayOfWeek = Calendar.firstDayOfWeek;
    
    var now = new Date();
    
    /* year: A Number giving the full year of the month on the calendar */
    this.year = year || now.getFullYear();
    
    /* month: A Number (0-11) representing the month on the calendar */
    this.month = month || now.getMonth();
    
    var getWeekNumber = function(date, year)
    {
        return DateUtils.getWeekNumber(date, year, this.firstDayOfWeek);
    };
    
    var getDateFromWeekNumber = function(year, weekNum)
    {
        return DateUtils.getDateFromWeekNumber(year, weekNum, this.firstDayOfWeek);
    };
    
    var selectedDate = null; // The selected date
    
    /*
    onSelect: A Function to which to pass a date when a date is selected.
    The Function will be passed the Date of the selected day as the first argunment.
    */
    this.onSelect = null;
    
    /*
    selectDate: Mark a date on the calendar as selected.
    
    void selectDate([Date date])
    
    Parameters:
        * date: Optional. A Date object with the date to be selected. Defaults to the current date.
    */
    this.selectDate = function(date)
    {
        selectedDate = date || new Date();
        if (this.onSelect)
            this.onSelect(date);
    };
    
    /*
    gotoSelection: Go to the month of the selected date.
    
    void gotoSelection()
    
    */
    this.gotoSelection = function()
    {
        if (!selectedDate)
            return;
        this.month = selectedDate.getMonth();
        this.year = selectedDate.getFullYear();
        this.getElement();
    };
    
    var calendarElement = null; // The element containing the calendar.
    
    var replaceCalendar = function(newCal)
    {
        var cal = calendarElement;
        while (cal.childNodes.length)
            cal.removeChild(cal.firstChild);
        while (newCal.childNodes.length)
            cal.appendChild(newCal.removeChild(newCal.firstChild));
    };

    // gets a table cell with a link that triggers a function when clicked
    var getCellWithLink = function(text, title, onClick, colSpan)
    {
        var td = document.createElement("td")
        if (colSpan && colSpan != 1)
        {
            td.setAttribute("colSpan", colSpan);
        }
        var a = document.createElement("a");
        a.setAttribute("href", "#");
        if (title)
            a.setAttribute("title", title);
        a.appendChild(document.createTextNode(text || ""));
        td.appendChild(a);
        if (onClick)
            addEvent(a, "click", function(e)
            {
                cancelEvent(e);
                onClick(e);
                return false;
            });
        return td;
    };

    /*
    getElement: Update the calendar and return an reference to its containing element.
    The outer element stays the same after subsequent calls, so references to the outer element will still be valid.
    
    Element getElement()

    Returns:
        An Element node of a table in which the calendar has been created.
        
        The current date is given a class of "today".
        The selected date is given a class of "selected".
        Dates in the previous month have a class of "in-prev-month", those in the next month have a class of "in-next-month"
        Columns of weekends are given a class of "weekend".
    */
    this.getElement = function()
    {
        var startDate = new Date(this.year, this.month, 1);
        var year = this.year = startDate.getFullYear();
        var month = this.month = startDate.getMonth();
        var today = new Date();
        var curYear = today.getFullYear();
        var curMonth = today.getMonth();
        var curDate = today.getDate();
        today = new Date(curYear, curMonth, curDate);
        var sel = selectedDate;
        
        var table = document.createElement("table");
        
        var caption = document.createElement("caption");
        caption.appendChild(document.createTextNode(this.monthNames[month] + " " + year));
        caption.setAttribute("title", this.monthNames[month] + " " + year);
        table.appendChild(caption);
        var colgroup = document.createElement("colgroup");
        var headers = document.createElement("tr");
        for (var i = 0; i < 7; i++)
        {
            var d = (i + this.firstDayOfWeek) % 7;
            var col = document.createElement("col");
            if (d == 0 || d == this.dayNames.length - 1)
                col.className = "weekend";
            colgroup.appendChild(col);
            var header = document.createElement("th");
            header.appendChild(document.createTextNode(this.dayNames[d].substring(0, 2)));
            header.setAttribute("abbr", this.dayNames[d]);
            header.setAttribute("title", this.dayNames[d]);
            headers.appendChild(header);
        }
        var thead = document.createElement("thead");
        thead.appendChild(headers);
        var footers = document.createElement("tr");
        footers.appendChild(getCellWithLink("«", "Previous Month", function()
        {
            self.month--;
            self.getElement();
        }, 2));
        footers.appendChild(getCellWithLink("Today", "Today", function()
        {
            self.year = curYear;
            self.month = curMonth;
            self.selectDate(today);
            self.getElement();
        }, 3));
        footers.appendChild(getCellWithLink("»", "Next Month", function()
        {
            self.month++;
            self.getElement();
        }, 2));
        var tfoot = document.createElement("tfoot");
        tfoot.appendChild(footers);
        var tbody = document.createElement("tbody"); 
        var firstWeek = getWeekNumber(new Date(year, month, 0), year);
        var lastWeek = firstWeek + 5;
        for (var w = firstWeek; w <= lastWeek; w++)
        {
            var week = document.createElement("tr");
            var firstDate = getDateFromWeekNumber(year, w);
            var maxTime = getDateFromWeekNumber(year, w + 0.9).getTime();
            for (var dt = firstDate; dt.getTime() < maxTime; dt.setDate(dt.getDate() + 1))
            {
                var day = document.createElement("td");
                var dayNum = dt.getDate();
                day = getCellWithLink(dt.getDate(), "", function(e)
                {
                    e = e || window.event;
                    var el = e && (e.target || e.srcElement);
                    if (el)
                    {
                        var cls = el.parentNode.className;
                        month = month + (/\b(in-next-month)\b/.test(cls)? 1 : 0) - (/\b(in-prev-month)\b/.test(cls)? 1 : 0);
                        self.selectDate(new Date(year, month, el.firstChild.nodeValue));
                        self.gotoSelection();
                    }
                });
                var classes = [];
                if (curYear == dt.getFullYear() && curMonth == dt.getMonth() && curDate == dayNum)
                    classes.push("today");
                if (sel && sel.getFullYear() == dt.getFullYear() && sel.getMonth() == dt.getMonth() && sel.getDate() == dayNum)
                    classes.push("selected");
                if (dt.getMonth() == (month + 11) % 12)
                    classes.push("in-prev-month");
                if (dt.getMonth() == (month + 1) % 12)
                    classes.push("in-next-month");
                if (classes.length)
                    day.className = classes.join(" ");
                week.appendChild(day);
            }
            tbody.appendChild(week);
        }
        table.appendChild(colgroup);
        table.appendChild(thead);
        table.appendChild(tfoot);
        table.appendChild(tbody);
        if (calendarElement)
            replaceCalendar(table);
        else
            calendarElement = table;
        return calendarElement;
    };
}