// Script: ddcal (Drag and Drop Calendar) v2.10
// Company: Interaxis
// Author: dynamicreport.com
// Website: http://dynamicreport.com/ddcal.html
// License: Commercial (original author info. must remain in all files)

// default English language	text strings
var ddcal_months = new Array ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');
var ddcal_days = new Array ('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday');
var ddcal_week = "Wk";
var ddcal_today = "Today";
var ddcal_daylen = 1;
var ddcal_path = "ddobj/ddcal/";



//input spinner control (increment or decrement an input control's value in a cyclic fashion)
function ctrlspin (delay, mdelay, ddelay, sobj, sctrl, min, max)
{ // initial delay, minimum delay, delay decrease, object control variable name, 
 // default input control ID, minimum value, maximum value of default input control

	this.tid = null; this.accel = 0; this.taccel = delay; this.delay = delay;
	this.mdelay = mdelay; this.ddelay = ddelay; this.ctrl = null; this.sctrl = sctrl;
	this.val = "";
	this.sobj = sobj; this.min = min; this.max = max;

	this.set = function (sctrl, min, max)
	{
		this.min = min;
		this.max = max;
		this.sctrl = sctrl;
		this.ctrl = document.getElementById (this.sctrl);
	};
	
	this.inc = function (step)
	{
		var val = 1 * this.ctrl.value + step;
		if (step > 0 && val > this.max)
			val = this.min;
		else if (step < 0 && val < this.min)
			val = this.max;

		this.ctrl.value = this.val = (val < 10 ? "0" + val : val);
		this.accel++;
		if (!(this.accel % 2))
		{
			this.taccel -= this.ddelay;
			if (this.taccel < this.mdelay)
				this.taccel = this.mdelay;
			this.stop ();
			this.start (false, step);
		}
	};
	
	this.start = function (init, step)
	{
		if (this.sctrl) // default control ID provided
			this.ctrl = document.getElementById (this.sctrl);
		else
			return false;
		if (init)
			this.inc (step);
		this.tid = setInterval (this.sobj + ".inc(" + step + ")", this.taccel);
		return true;
	};

	this.stop = function (end)
	{
		if (this.tid != null)
			clearInterval (this.tid);
		this.accel = 0;
		if (end)
			this.taccel = this.delay;
	};
}

function input_filter_n (str, min, max)
{ // filter string inputs to positive numeric amounts within range
	var rgx = /([^\d]{0,})/g;
	str = str.replace (rgx, '');
	var val = 1 * str;
	if (min == max);
	else if (val < min) val = min;
	else if (val > max) val = max;
	str = (val < 10 ? "0" + val : String (val));
	return str;
}


function ddcal ()
{
	// code constant: 1 signals initially generated calendar to method generate()
	// code constant: 2 signals navigation generated calendar to method generate()

	// shifting arrays (leap year offset beyond feb.) used in various date calculations
	this.cal_dcount = new Array (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365); // assumes year isn't leap year (add offset +1 if date is beyond feb. when used)
	this.cal_mdays = new Array (31, 28, 31, 30,  31,  30,  31,  31,  30,  31,  30,  31); // assumes year isn't leap year (add offset +1 to february if leap year)

	this.sCaption = "";
	this.year = this.month = this.day = this.iyear = this.imonth = this.iday = this.daynum = 0;
	this.tyear = this.tmonth = this.tday = this.wd_start = this.ydays = this.mdays = this.index = 0;
	this.dyear = this.dmonth = this.dday = 0;
	this.hr = this.min = this.sec = 0;
	this.month_cur = this.month_next = null;
	this.sobj = this.tobj = this.ddate = this.smask = this.ctrl = this.tctrl = this.tspin = this.calobj = this.timeobj = this.sdate = this.stime = this.tdate = this.date = "";

	this.set_params = set_params;
	this.params = params;
	this.show = show;
	this.nav = nav;
	this.generate = generate;
	this.calmonth = calmonth;
	this.caltime = caltime;
	this.set_date = set_date;
	this.next_month = next_month;
	this.get_mdays = get_mdays;
	this.get_mdays_alt = get_mdays_alt;
	this.get_ydays = get_ydays;
	this.get_ydays_md = get_ydays_md;
	this.is_leapyr = is_leapyr;
	this.is_dday = is_dday;
	this.is_wkend = is_wkend;
	this.is_isodate = is_isodate;
	this.get_fixed_date = get_fixed_date;
	this.get_hour24 = get_hour24;
	this.get_hour = get_hour;
	this.type2string = type2string;
	this.date2string = date2string;
	this.time2string = time2string;
	this.string2type = string2type;
	this.string2date = string2date;
	this.string2time = string2time;
	this.modmatch = modmatch;
	this.get_num = get_num;
	this.get_input_time = get_input_time;
	this.caction = caction;
	this.oaction = oaction;
	this.uaction = uaction;


	function set_params (params)
	{
		this.index = params["INDEX"] != null ? params["INDEX"] : 0; // ddobj = drag drop object index
		this.skin = params["SKIN"] ? params["SKIN"] : 0; // skin index
		this.sobj = params["INPUT"] ? params["INPUT"] : ""; // form's date input ID for return value
		this.sdate = params["DATE"] ? params["DATE"] : "";	// null for current date, must conform to date input mask or %y/%m/%d (ISO 8601 format) default
		this.sCSS = params['CSS'] ? params["CSS"] : "arctic"; // CSS selector: custom calendar style sheet abbreviation
		this.minyr = params["MIN_YR"] ? Math.abs (params["MIN_YR"]) : 1920; // minimum year
		this.maxyr = params["MAX_YR"] ? Math.abs (params["MAX_YR"]) : 2099; // maximum year
		if (this.minyr > this.maxyr)
		{ // correct impossible range
			this.minyr = 1920;
			this.maxyr = 2099;
		}
		else
		{ // force default year boundaries if custom boundaries exceed the extremes
			if (this.maxyr > 2099)
				this.maxyr = 2099;
			else if (this.maxyr < 1920)
				this.maxyr = 1920;
			if (this.minyr < 1920)
				this.minyr = 1920;
			else if (this.minyr > 2099)
				this.minyr = 2099;
		}
		this.smask = params["MASK"] ? params["MASK"] : "%Y/%m/%d"; // date input mask
		this.tmask = params["TMASK"] ? params["TMASK"] : "%H:%i:%s"; // time input mask
		this.wd_first = params["WD_FIRST"] ? params["WD_FIRST"] : 0; // first day of week appearing on calendar (0 = Sunday, 1 = Monday, ..., 6 = Saturday)
		if (this.wd_first > 6) // check limits
			this.wd_first = 6;
		this.rows = params["ROWS"] ? params["ROWS"] : 1; // rows of months (more than 1 = adjacent months in adjacent cells)
		this.cols = params["COLS"] ? params["COLS"] : 1; // cols of months (more than 1 = adjacent months in adjacent cells)
		if (this.rows * this.cols > 12)
		{ // limit to 12 months in a calendar object (set default grid)
			this.rows = 4;
			this.cols = 3;
		}
		this.cell = params["CELL"] ? Math.abs (params["CELL"]) : 0; // target month placement in cell from left to right, top to bottom
		if (this.cell > this.rows * this.cols) // check limits
			this.cell = this.rows * this.cols - 1;
		this.width = params["WIDTH"] ? params["WIDTH"] : 600; // full calendar width
		this.caption = params["CAPTION"] ? params["CAPTION"] : "&nbsp;"; // calendar caption
		this.embed = params["EMBED"] ? params["EMBED"] : false; // embed header or footer within inline skin
		this.wkend = params["WKEND"] ? params["WKEND"] : new Array (0, 6); // weekend days
		this.adjmonth = params["ADJ_MONTH"] != null ? params["ADJ_MONTH"] : true; // enable or disable display of adjacent month days
		this.wkend_enable = params["WKEND_ALLOW"] ? params["WKEND_ALLOW"] : false; // disable weekend click event
		this.dtrunc = params["DAY_LEN"] ? params["DAY_LEN"] : ddcal_daylen; // truncate full day name to specified character length
		this.caption_style = params["CAP_DEFAULT"] != null ? params["CAP_DEFAULT"] : true; // generate the default caption style
		this.content_header = params["HEADER"] != null ? params["HEADER"] : ""; // content header above calendar content
		this.content_header = '<div id = "calheader">' + this.content_header + '</div>'; // encapsulate header by div layer
		this.content_footer = params["FOOTER"] != null ? params["FOOTER"] : ""; // content footer below calendar content
		this.content_footer = '<div id = "calfooter">' + this.content_footer + '</div>'; // encapsulate footer by div layer
		this.dlinked = params["DLINK"] != null ? params["DLINK"] : true; // link date associated content if this flag is set
		this.calobj = params["OBJ"] != null ? params["OBJ"] : "calendar"; // calendar object reference
		this.ddate = params["DDATE"] != null ? params["DDATE"] : ""; // disabled date string must conform to ISO 8601 format (yyyy-mm-dd)
		this.sfwd_y = params["FWD_Y"] != null ? params["FWD_Y"] : "&gt;&gt;"; // navigation: forward year
		this.sbwd_y = params["BWD_Y"] != null ? params["BWD_Y"] : "&lt;&lt;"; // navigation: backward year
		this.sfwd_m = params["FWD_M"] != null ? params["FWD_M"] : "&gt;"; // navigation: forward month
		this.sbwd_m = params["BWD_M"] != null ? params["BWD_M"] : "&lt;"; // navigation: backward month
		this.nav_noy = params["NAV_NOY"] ? params["NAV_NOY"] : false; // enable navigation by year if false (default)
		this.tobj = params["TIME"] ? params["TIME"] : null; // enable time input on date picker if object ID specified
		this.tnos = params["TIME_NOS"] != null ? params["TIME_NOS"] : false; // disable second time input on time picker if set to true
		this.tnom = params["TIME_NOM"] != null ? params["TIME_NOM"] : false; // disable minute input on time picker if set to true
		this.tnos = this.tnom ? this.tnom : this.tnos; // force disabled second input if minute input is disabled
		this.hour24 = params["HOUR24"] != null ? params["HOUR24"] : false; // enable time input on date picker if set to true
		this.maxhr = (!this.hour24 ? 12 : 23); // maximum hour in time input interface
		this.minhr = (!this.hour24 ? 1 : 0); // minimum hour in time input interface
		this.timeobj = this.calobj + ".tspin";
		if (this.tobj) // default control focus: hour
			this.tspin = new ctrlspin (400, 80, 100, this.timeobj, this.calobj + "_hr", this.minhr, this.maxhr);
	}

	function params (params)
	{
		this.set_params (params);
		this.m_width = (100 / this.cols) + "%"; // calendar month width

		this.ctrl = this.tctrl = null;
		if (this.sobj) // obtain form date input control for date selection input/output
			this.ctrl = document.getElementById (this.sobj);
		if (this.tobj) // obtain form time input control for time selection input/output
			this.tctrl = document.getElementById (this.tobj);
		if (this.caption_style) // default caption style with custom caption
			this.sCaption = "<table width = '100%'" + (this.caption_style ? " class = '" + this.sCSS + "_caption'" : "") + "><tr><td>" + this.caption + "</td></tr></table>";
		else // custom caption and style
			this.sCaption = this.caption;

		this.cell %= this.rows * this.cols; // limit placement cell to boundaries

		if (!this.ctrl) // invalid or no date input control supplied
			return false;
		return true;
	}

	function show ()
	{ // display the drag and drop calendar
		if (dd_obj[this.index].noreset)
			return false;
		this.sdate = ""; // clear date input string
		this.stime = ""; // clear time input string
		if (!this.ctrl && this.sobj) // obtain form date input control for date selection input/output
			this.ctrl = document.getElementById (this.sobj);
		if (!this.tobj && this.tobj) // obtain form time input control for time selection input/output
			this.tctrl = document.getElementById (this.tobj);
		if (this.ctrl)
		{ // obtain form-bound date input
			if (this.ctrl.value != "")
				this.sdate = this.ctrl.value;
		}
		if (this.tctrl)
		{ // obtain form-bound time input
			if (this.tctrl.value != "")
				this.stime = this.tctrl.value;
		}
		if (dd_obj[this.index].getVis () == set_show)
			dd_obj[this.index].hide (true); // if visible, hide object before re-displaying possibly different content in already visible object
		this.generate (this.sdate, 1); // generate initial calendar output of months and assign to ddobj layer
		dd_obj[this.index].show (this.skin); // show the ddobj object (ddcal calendar)
		dd_obj[this.index].release (); // release the object from following the mouse as a tooltip
	}

	function set_date (sdate, init)
	{
		var date;
		if (this.tdate == "" || sdate == "")
		{ // obtain current date
			date = new Date ();
			this.tyear = this.year = date.getFullYear ();
			this.tmonth = this.month = date.getMonth ();
			this.tday = this.day = date.getDate ();
			// store today's date as a string
			this.tdate = this.tyear + "-" + (this.month + 1 < 10 ? this.month + 1 : this.month) + "-" + (String (this.tday).length < 2 ? "0" + this.tday : this.tday);
		}

		if (sdate)
		{ // custom control date defined by parameter string sdate using convention: yyyy-mm-dd
			if (init == 2) // executed when the calendar is navigated by month or year
			{ // decode date by ISO 8601 format
				this.year = parseFloat (sdate.substring (0, 4));
				this.month = parseFloat (sdate.substring (5, 7)) - 1;
				this.day = parseFloat (sdate.substring (8, 10));
			}
			else
				this.string2date (this.smask, sdate); // obtain date based on date input mask
			date = new Date (this.year, this.month, this.day);
		}

		if (init == 1)
		{ // executed when the calendar is first shown only
			this.iyear = this.year;
			this.imonth = this.month;
			this.iday = this.day;

			if (this.stime)
				this.string2time (this.tmask, this.stime); // obtain time based on time input mask
			else
			{ // obtain current time on initial calendar showing
				var cdate = new Date ();
				this.hr = cdate.getHours ();
				this.min = !this.tnom ? cdate.getMinutes () : 0;
				this.sec = !this.tnos ? cdate.getSeconds () : 0;
			}

			if (this.cell >= 0)
			{ // first display month must be offset by placement cells from intended month
				date.setFullYear (this.year, this.month - this.cell, 1);
				this.year = date.getFullYear ();
				this.month = date.getMonth ();
				this.day = date.getDate ();
			}
			
			if (this.ddate)
			{ // disabled date string: ISO 8601 format (yyyy-mm-dd)
				if (!is_isodate (this.ddate))
				{ // invalid date format -- will default to current date
					this.ddate = this.tdate;
					this.dyear = this.tyear;
					this.dmonth = this.tmonth;
					this.dday = this.tday;
				}
				else
				{
					this.dyear = parseFloat (this.ddate.substring (0, 4));
					this.dmonth = parseFloat (this.ddate.substring (5, 7)) - 1;
					this.dday = parseFloat (this.ddate.substring (8, 10));
				}
			}
		}
		else if (init == 2 && this.tctrl) // convert AM/PM time input to 24 hour time
			this.get_input_time ();
		this.date = date;
	}

	function generate (sdate, init)
	{
		var sLayout = "";

		this.set_date (sdate, init);

		if (this.embed && this.content_header)
		{ // embed header within calendar template
			sLayout += '<table width = "100%" cellspacing = "0" cellpadding = "0" class = "' + this.sCSS + '_tbl"><tr><td>' +
			this.content_header + "</td></tr></table>";
		} // insert header outside of calendar template
		else sLayout += this.content_header;

		sLayout += this.nav (); // navigation bar (cycle through periods)

		sLayout += '<table width = "100%" cellspacing = "0" cellpadding = "0" class = "' + this.sCSS + '_tbl" align = "center"><tr><td>';

		var inc = 0;
		for (var r = 0; r < this.rows; r++)
		{
			for (var c = 0; c < this.cols; c++)
			{ // generate calendar month(s)
				this.date = new Date (this.year, this.month + inc, 1);
				this.next_month (); // advance to adjacent month
				sLayout += this.calmonth ();
				inc = 1; // increment to next calendar month
			}
			if (r < this.rows - 1)
				sLayout += "</td></tr><tr><td>";
		}
		if (this.tctrl) // generate time picker portion if enabled
			sLayout += "<tr><td>" + this.caltime () + "</td></tr>";
		if (this.embed && this.content_footer) // embed footer within calendar template
			sLayout += "<tr><td>" + this.content_footer;
		sLayout += "</td></tr></table>";
		if (!this.embed) // insert footer outside of calender template
			sLayout += this.content_footer;

		dd_obj[this.index].output (this.sCaption, sLayout, this.width); // output calendar to the drag and drop object container
	}

	function calmonth ()
	{
		var sCSS2 = this.adjmonth ? "_odays" : "_edays";

		var sLayout = '<table class = "' + this.sCSS + '_tbl" width = "' + this.m_width + '" align = "left">';
		sLayout += '<tr><td class = "' + this.sCSS + '_month" colspan = "7" align = "center">' + ddcal_months[this.month] + ' ' + this.year + '</td></tr>';
		sLayout += '<tr class = "' + this.sCSS + '_rowh">';
		for (var i = this.wd_first; i < this.wd_first + ddcal_days.length; i++)
			sLayout += "<td align = 'center'>" + ddcal_days[i % ddcal_days.length].substr (0, this.dtrunc) + "</td>";
		sLayout += '</tr><tr>';

		// leading blank cells (days) for first week of month
		var inc = 0; // previous month's day incrementer
		var days_backup = 0; // previous month's days behind current month's starting day
		this.pm_days = this.get_mdays (new Date (this.year, this.month - 1, 1), this.date); // days in previous month

		if (this.wd_first != this.wd_start) // month does not start on the first day of the week
		{
			for (var wk_day = this.wd_first; wk_day < this.wd_first + ddcal_days.length; wk_day++)
			{
				if ((wk_day % ddcal_days.length) == this.wd_start)
					break; // starting day of current month reached, leading cells (padded or indexed) from last month on current month completed
				days_backup = ((this.wd_start - this.wd_first));
				if (days_backup < 0) // adjust day offset if starting week day of month is greater than first week day of month
					days_backup += ddcal_days.length;
				sLayout += '<td align = "center" class = "' + this.sCSS + sCSS2 + '">' + (this.adjmonth ? (this.pm_days - days_backup + inc + 1) : "&nbsp;") + '</td>';
				inc++;
			}
		}

		var scompact;
		var smonth = (this.month + 1 < 10 ? "0" + (this.month + 1) : this.month + 1);
		var wk_day = this.wd_start;
		var wkend = false;
		var sdate, sday;
		var days = 42 - inc;
		inc = 1;
		for (var day = 1; day <= days; day++)
		{
			wk_day %= ddcal_days.length;

			if (day > this.mdays)
			{
				sday = inc;
				inc++;
			}
			else
			{
				wkend = this.is_wkend (wk_day);
				this.ydays = this.get_ydays (this.year, this.month, day); // put this in onclick action
				sdate = this.date2string (this.smask, this.year, this.month + 1, day, wk_day, this.ydays);
				if (this.ctrl && !(wkend && !this.wkend_enable)) // format date output back to form input control
				{ // compact date representation (yyyymmdd) without separators
					scompact = this.year + smonth + (day < 10 ? "0" + day : day);
					// date is an accessible link
					sday = '<a href="#" id="a' + scompact + '" onclick="javascript:' + this.calobj + '.caction(\'' + sdate + '\',' + this.year + ',' + (this.month + 1) + ',' + day + '); return false;" onmouseover="javascript:' + this.calobj + '.oaction(' + this.year + ',' + (this.month + 1) + ',' + day + ');" onmouseout="javascript:' + this.calobj + '.uaction(' + this.year + ',' + (this.month + 1) + ',' + day + ');">';
					sday += day + (!this.dlinked ? '</a>' : "") + '<div id="d' + scompact + '"></div>' + (this.dlinked ? '</a>' : "");
				}
				else // date is not an accessible link
					sday = day;
			}
			if (wk_day == this.wd_first && day != 1)
				sLayout += '</tr><tr>';
			if (day <= this.mdays)
			{
				this.day = day; // assign current processed day to calendar day property
				// day is disabled if prior to specified disabler date or in range of calendar date outer limits
				if (this.is_dday (this.year, this.month, this.day) || this.year > this.maxyr || this.year < this.minyr)
					sLayout += '<td align = "center" class = "' + this.sCSS + sCSS2 + '">' + day + '</td>';
				else if (this.tday == day && this.tmonth == this.month && this.tyear == this.year) // today's date
					sLayout += '<td align = "center" class = "' + this.sCSS + '_tday">' + sday + '</td>';
				else if (this.iday == day && this.imonth == this.month && this.iyear == this.year) // initial selection date
					sLayout += '<td align = "center" class = "' + this.sCSS + '_iday">' + sday + '</td>';
				else // all other days including weekends
					sLayout += '<td align = "center" class = "' + this.sCSS + (wkend ? '_wkend' : '_cell') + '">' + sday + '</td>';
			}
			else
				sLayout += '<td align = "center" class = "' + this.sCSS + sCSS2 + '">' + (this.adjmonth ? sday : "&nbsp;") + '</td>';
			wk_day++;
		}
		sLayout += '</tr></table>';
		return sLayout;
	}

	function get_hour24 (hr)
	{ // convert 12 hour clock to 24 hour clock
		if (this.pm == "PM" && hr > 0 && hr < 12)
			hr += 12;
		else if (this.pm == "AM" && hr == 12)
			hr = 0;
		return hr;
	}
	
	function get_hour (hr)
	{ // convert 24 hour clock to 12 hour clock
		if (hr >= 12)
		{
			this.pm = "PM";
			if (hr > 12) hr -= 12;
		}
		else
		{
			if (hr == 0) hr = 12;
			this.pm = "AM";
		}
		return hr;
	}

	function caltime (init)
	{
		var hr = this.hr;
		var pm = "";
		if (!this.hour24)
			hr = this.get_hour (hr);
		hr = hr < 10 ? "0" + (1 * hr) : hr;
		min = this.min < 10 ? "0" + (1 * this.min) : this.min;
		sec = this.sec < 10 ? "0" + (1 * this.sec) : this.sec;

		var sLayout = '<form id = "frmtime" method = "" action = "" style = "margin: 0px;"><table align = "center" border = "0">';
		sLayout += '<tr><td>' +
		'<input id = "' + this.calobj + '_hr" type = "text" class = "' + this.sCSS + '_time" value = "' + hr + '" size = "2" maxlength = "2" onclick = "' + this.timeobj + '.set (\'' + this.calobj + '_hr\', ' + this.minhr + ', ' + this.maxhr + ');" onblur = "this.value = input_filter_n (this.value, ' + this.minhr + ', ' + this.maxhr + ');">';
		if (!this.tnom) // minute control
			sLayout += ' <b>:</b> <input id = "' + this.calobj + '_min" type = "text" class = "' + this.sCSS + '_time" value = "' + min + '" size = "2" maxlength = "2" onclick = "' + this.timeobj + '.set (\'' + this.calobj + '_min\', 0, 59);" onblur = "this.value = input_filter_n (this.value, 0, 59);">';
		if (!this.tnos) // second control
			sLayout += ' <b>:</b> <input id = "' + this.calobj + '_sec" type = "text" class = "' + this.sCSS + '_time" value = "' + sec + '" size = "2" maxlength = "2" onclick = "' + this.timeobj + '.set (\'' + this.calobj + '_sec\', 0, 59);" onblur = "this.value = input_filter_n (this.value, 0, 59);">';
		sLayout += '</td>';
		if (!this.hour24)
		{
			sLayout += '<td class = "' + this.sCSS + '_pmtxt' + '">' + 
			' <input name = "' + this.calobj + '_pm" type = "radio" onclick = "' + this.calobj + '.pm = \'AM\';" class = "' + this.sCSS + '_pm" value = "AM"' + (this.pm == 'AM' ? ' checked' : '') + '> AM' + 
			' <input name = "' + this.calobj + '_pm" type = "radio" onclick = "' + this.calobj + '.pm = \'PM\';" class = "' + this.sCSS + '_pm" value = "PM"' + (this.pm == 'PM' ? ' checked' : '') + '> PM' + 
			'</td>';
		}
		sLayout += '<td class = "' + this.sCSS + '_spinner">' +
		'<a href = "#" onclick = "return false;" onmousedown = "' + this.timeobj + '.start (true, 1); return false;" onmouseout = "' + this.timeobj + '.stop (true);" onmouseup = "' + this.timeobj + '.stop (true); return false;"><img src = "' + ddcal_path + 'images/arrow_up.gif" width = "12" height = "10" border = "0"></a>' +
		'<a href = "#" onclick = "return false;" onmousedown = "' + this.timeobj + '.start (true, -1); return false;" onmouseout = "' + this.timeobj + '.stop (true);" onmouseup = "' + this.timeobj + '.stop (true); return false;"><img src = "' + ddcal_path + 'images/arrow_dn.gif" width = "12" height = "10" border = "0"></a>' +
		'</td>' +
		'</tr>' +
		'</table></form>';
		return sLayout;
	}


	function nav ()
	{ // the bulk of this function checks the calendar min/max date limits and takes into account the grid's focus month placement cell
		var lnk_fwd_m = lnk_bwd_m = lnk_fwd_y = lnk_bwd_y = d_adv = adv = "";
		var year_adv = month_adv = 0;
		var colspan = 1;

		// forward date advancement by month if within boundary
		if (this.year + parseInt ((this.month + this.rows * this.cols) / 12) > this.maxyr) // boundary: cannot move beyond maximum year
			lnk_fwd_m = '<a href = "#" onclick = "return false;">' + this.sfwd_m + '</a>';
		else
		{ // boundary: within year limit -- progress to next month
			var adv = new Date (this.year, this.month + 1, this.day); // obtain full date according to next month
			var year_adv = adv.getFullYear ();
			var month_adv = adv.getMonth () + 1;
			// forward date parameter by month (force ISO date format yyyy-mm-dd)
			d_adv = year_adv + "-" + (month_adv < 10 ? "0" + month_adv : month_adv) + "-" + "01";
			lnk_fwd_m = '<a href = "#" onclick = "' + this.calobj + '.generate (\'' + d_adv + '\', 2); return false;">' + this.sfwd_m + '</a>';
		}

		// backward date advancement by month if within boundary
		if (this.year - parseInt ((11 - (this.month - 1)) / 12) < this.minyr) // boundary: cannot move beyond minimum year
			lnk_bwd_m = '<a href = "#" onclick = "return false;">' + this.sbwd_m + '</a>';
		else
		{ // boundary: within year limit -- progress to next month
			// obtain full date according to previous month
			var adv = new Date (this.year, this.month - 1, this.day);
			var year_adv = adv.getFullYear ();
			var month_adv = adv.getMonth () + 1;
			// backward date parameter by month (force ISO date format yyyy-mm-dd)
			d_adv = year_adv + "-" + (month_adv < 10 ? "0" + month_adv : month_adv) + "-" + "01";
			lnk_bwd_m = '<a href = "#" onclick = "' + this.calobj + '.generate (\'' + d_adv + '\', 2); return false;">' + this.sbwd_m + '</a>';
		}

		if (!this.nav_noy) // enable year navigation if flag is false
		{
			// forward date advancement by year if within boundary
			if (this.year + parseInt ((this.month + (this.rows * this.cols - 1)) / 12 + 1) > this.maxyr)
			{ // display last month in last cell on maximum year boundary
				year_adv = this.maxyr;
				month_adv = 12 - this.rows * this.cols + 1;
				if (this.month + 1 == month_adv)
					lnk_fwd_y = '<a href = "#" onclick = "return false;">' + this.sfwd_y + '</a>';
			}
			else
			{ // advance normally, since boundary is not met a full year from the first visible month
				year_adv = this.year + 1;
				month_adv = this.month + 1;
			}
			d_adv = year_adv + "-" + (month_adv < 10 ? "0" + month_adv : month_adv) + "-" + "01";
			if (lnk_fwd_y == "")
				lnk_fwd_y = '<a href = "#" onclick = "' + this.calobj + '.generate (\'' + d_adv + '\', 2); return false;">' + this.sfwd_y + '</a>';
		
			// backward date advancement by year if within boundary
			if (this.year - 1 < this.minyr)
			{ // display last month in last cell on maximum year boundary
				year_adv = this.minyr;
				month_adv = 1;
				if (this.month + 1 == month_adv)
					lnk_bwd_y = '<a href = "#" onclick = "return false;">' + this.sbwd_y + '</a>';
			}
			else
			{ // advance normally, since boundary is not met a full year from the first visible month
				year_adv = this.year - 1;
				month_adv = this.month + 1;
			}
			d_adv = year_adv + "-" + (month_adv < 10 ? "0" + month_adv : month_adv) + "-" + "01";
			if (lnk_bwd_y == "")
				lnk_bwd_y = '<a href = "#" onclick = "' + this.calobj + '.generate (\'' + d_adv + '\', 2); return false;">' + this.sbwd_y + '</a>';
		}
		else // disable year navigation -- month navigation occupies two cells
			colspan = 2;

		var date = new Date (this.tyear, this.tmonth - this.cell, 1); // set today's date to the proper cell (focus cell)
		var ty = date.getFullYear ();
		var tm = date.getMonth () + 1;
		var td = date.getDate ();
		var stoday = ty + "-" + (tm < 10 ? "0" + tm : tm) + "-" + (td < 10 ? "0" + td : td);

		var sLayout = '<table class = "' + this.sCSS + '_tbl" width = "100%" align = "center">';
		sLayout += '<tr>';
		if (colspan == 1) // disable year navigation
			sLayout += '<td class = "' + this.sCSS + '_nav" align = "center">' + lnk_bwd_y + '</td>';
		sLayout += '<td class = "' + this.sCSS + '_nav" align = "center"' + (colspan == 2 ? ' colspan = "2"' : '') + '>' + lnk_bwd_m + '</td>';
		sLayout += '<td class = "' + this.sCSS + '_nav" align = "center"><a href = "#" onclick = "' + this.calobj + '.generate (\'' + stoday + '\', 2); return false;">' + ddcal_today + '</a></td>';
		sLayout += '<td class = "' + this.sCSS + '_nav" align = "center"' + (colspan == 2 ? ' colspan = "2"' : '') + '>' + lnk_fwd_m + '</td>';
		if (colspan == 1) // disable year navigation
			sLayout += '<td class = "' + this.sCSS + '_nav" align = "center">' + lnk_fwd_y + '</td></tr>';
		sLayout += '</tr>';
		sLayout += '</table>';
		return sLayout;
	}
	
	function next_month ()
	{
		this.year = this.date.getFullYear ();
		this.month = this.date.getMonth ();
		this.day = this.date.getDate ();
		this.wd_start = this.date.getDay (); // starting day of month
		this.mdays = this.get_mdays (this.date, new Date (this.year, this.month + 1, 1));
	}

	function is_isodate (sdate)
	{ // returns true if the date input matches ISO 8601 format: yyyy-mm-dd with any separating characters
		var re = new RegExp("^((19|20)\\d\\d.(0[1-9]|1[012]).(0[1-9]|[12][0-9]|3[01]))$");
		return sdate.match(re);
	}

	function is_leapyr (y)
	{ // returns true if year is a leap year, false otherwise
		return (!(y % 4) && (y % 100) || !(y % 400))
	}

	function get_ydays (y, m, d)
	{ // returns the day in the year based on the full date provided (add 1 day offset if leap year and month is beyond february
		return (d + this.cal_dcount[m] + (this.is_leapyr (y) && m >= 2 ? 1 : 0));
	}

	function get_mdays (d1, d2)
	{ // obtain correct number of days in month (regards February in a leap year)
		var days = Math.abs (Math.round ((d2.getTime () - d1.getTime ()) / 86400000)); // 1000ms * 60s * 60m * 24h
		return days;
	}
	
	function get_mdays_alt (y, m)
	{ // obtain correct number of days in month (regards February in a leap year)
		var loffset = this.is_leapyr (y) ? 1 : 0; // leap year offset day
		return (this.cal_mdays[m] + (m == 1 ? loffset : 0));
	}

	function get_ydays_md (d, y, type_m)
	{ // obtain month or days in month that the day of year falls within
		var loffset = this.is_leapyr (y) ? 1 : 0; // leap year offset day
		var start1 = start2 = adder = 0;
		for (var i = 0; i < this.cal_dcount.length - 1; i++)
		{
			start1 = this.cal_dcount[i] + (i > 1 ? loffset : 0);
			start2 = this.cal_dcount[i + 1] + (i + 1 > 1 ? loffset : 0);
			if (d >= start1 && d <= start2) // day in year falls within month index: i
			{
				if (type_m) // return month that day of year falls on
					return i + 1;
				else // return day in month that day of year falls on (includes leap year for feb.)
					return d - start1;
			}
		}
		return -1;
	}

	function is_wkend (day)
	{ // determine if the day of the week is a weekend day
		for (var i = 0; i < this.wkend.length; i++)
			if (this.wkend[i] == day)
				return true;
		return false;
	}

	function is_dday (y, m, d)
	{ // check if the specified day is enabled enabled or disabled
		if (!this.ddate) // no disable date reference provided
			return false;
		if (y < this.dyear)
			return true;
		if (y == this.dyear)
		{
			if (m < this.dmonth)
				return true;
			if (m == this.dmonth && d < this.dday)
				return true;
		}
		return false; // enabled in all other cases
	}
	
	function get_fixed_date (y, m, d)
	{ // form a fixed length compact date
		return String (y) + (m < 10 ? "0" + m : m) + (d < 10 ? "0" + d : d);
	}
	
	function type2string (sfmt, modvals)
	{
		var re_types = /%\d{0,}./g; // modifier types to match
		var re_chars = /%(\d{0,})(.)/; // index 0: mask, 1: modifier letter match, 2: value length match
	
		var types = sfmt.match (re_types);
		var mod = replacer = chars = val = null;
	
		for (var i = 0; i < types.length; i++)
		{
			val = "";
			chars = types[i].match (re_chars); // array of matches
			mod = chars[2]; // modifiers: date fragment abbreviations
			// actual value to replace mask modifier - truncate if necessary
			if (modvals[mod])
				val = parseInt (chars[1]) ? modvals[mod].substr (0, chars[1]) : modvals[mod];
			if (val)
			{ // replace the mask modifier with the actual date fragment value
				re_replace = new RegExp (types[i], 'g');
				sfmt = sfmt.replace (re_replace, val);
			}
		}
		return sfmt;
	}
	
	function date2string (sfmt, y, m, d, wd, yd)
	{ // format date output according to mask composition
		// modifiers and associated values
		var modvals = {
			d : d < 10 ? "0" + d : d, 
			D : d, 
			m : m < 10 ? "0" + m : m, 
			M : m, 
			y : String (y).substr (2, 2), 
			Y : y, 
			w : ddcal_days[wd], 
			W : wd ? wd : "0", 
			n : (yd < 10 ? "00" + yd : (yd < 100 ? "0" + yd : yd)), 
			N : yd,
			j : ddcal_months[m - 1]
		};
		return this.type2string (sfmt, modvals);
	}

	function time2string (sfmt, h, i, s, pm)
	{ // format time output according to mask composition
		// modifiers and associated values
		var	k = this.get_hour (h);
		var modvals = {
			h : h < 10 ? "0" + h : h, // 24hr
			H : String (h), // 24hr
			k : k < 10 ? "0" + k : k,
			K : String (k),
			i : i < 10 ? "0" + i : i,
			s : s < 10 ? "0" + s : s,
			p : pm
		};
		return this.type2string (sfmt, modvals);
	}

	function get_num (str)
	{
		var snum = "";
		for (var i = 0; i < str.length; i++)
		{
			if (str.charAt (i) >= '0' && str.charAt (i) <= '9')
				snum += str.charAt (i);
			else break;
		}
		return (snum * 1); // implicit numeric conversion
	}

	function modmatch (smod, str, len, type)
	{
		var arr = null;
		var ind = -1;
		var mmin = 999;

		if (type == 0) // date type modifiers
		{
			switch (smod)
			{
				case "j":
					arr = ddcal_months;
					break;
				case "D":
					this.day = this.get_num (str.substr (0, 2));
					return String (this.day).length;
				case "d":
					this.day = str.substr (0, 2) * 1; // implicit numeric conversion
					return 2;
				case "W":
					return 1;
				case "w":
					arr = ddcal_days;
					break;
				case "N":
					this.ydays = this.get_num (str.substr (0, 3));
					return String (this.ydays).length;
				case "n":
					this.ydays = str.substr (0, 3) * 1; // implicit numeric conversion
					return 3;
				case "Y":
					this.year = parseInt (str.substr (0, 4));
					return 4;
				case "y": // grab current century portion and append years
					this.year = parseInt (String (this.tyear).substr (0, 2) + str.substr (0, 2));
					return 2;
				case "m":
					this.month = str.substr (0, 2) * 1; // implicit numeric conversion
					return 2;
				case "M":
					this.month = get_num (str.substr (0, 2));
					return String (this.month).length;
				default:
					break;
			}
		}
		else // time type modifiers
		{
			switch (smod)
			{
				case "H": // 24 hr
					this.hr = get_num (str.substr (0, 2));
					return String (this.hr).length;
				case "h": // 24 hr
					this.hr = str.substr (0, 2) * 1;
					return 2;
				case "K":
					this.hr = get_num (str.substr (0, 2));
					this.hr = this.get_hour24 (this.hr);
					return String (this.h).length;
				case "k":
					this.hr = str.substr (0, 2) * 1;
					this.hr = this.get_hour24 (this.hr);
					return 2;
				case "i":
					this.min = str.substr (0, 2) * 1;
					return 2;
				case "s":
					this.sec = str.substr (0, 2) * 1;
					return 2;
				case "p":
					this.pm = str.substr (0, 2);
					return 2;
				default:
					break;
			}
		}
		if (len && arr) // match partial string value to closest definition match
			for (var i = 0; i < arr.length; i++)
			{
				if (arr[i].indexOf (str.substr (0, len)) == 0)
					ind = i + 1; // found a match (index is no longer -1)
			}

		if (ind == -1 && arr) // match entire string value to definitions
			for (var i = 0; i < arr.length; i++)
			{
				if (str.indexOf (arr[i]) == 0 && mmin > arr[i].length)
				{
					mmin = arr[i].length;
					ind = i;
					len = mmin;
				}
			}

		if (type == 0) // date type modifier results
			switch (smod)
			{ // store results of variable string matches
				case "j":
					this.month = ind;
					break;
				default:
					break;
			}

		return len;
	}

	function string2date (sfmt, sdate)
	{
		this.year = this.month = this.day = 0; // reset date
		this.string2type (sfmt, sdate, 0);

		if (!this.year || this.year < this.minyy || this.year > this.maxyy)
			this.year = this.tyear;
		if (!this.month)
		{
			if (this.ydays)
				this.month = this.get_ydays_md (this.ydays, this.year, true);
			else
				this.month = this.tmonth;
		}
		if (!this.day && this.ydays)
			this.day = this.get_ydays_md (this.ydays, this.year);
		else if (!this.day)
			this.day = this.tday;
		this.month--; // date object regards 1st month as index 0
	}

	function string2time (sfmt, stime)
	{
		this.hr = this.min = this.sec = 0; // reset time
		this.string2type (sfmt, stime, 1);
	}
	
	function string2type (sfmt, str, type)
	{
		var re_types = /%\d{0,}./g; // modifier types to match
		var re_chars = /%(\d{0,})(.)/; // index 0: mask, 1: modifier letter match, 2: value length match
	
		var types = sfmt.match(re_types);
		var chars = new Array ();
	
		var f = offset = pos = chlen = 0;
	
		for (var i = 0; i < types.length; i++)
			chars[i] = types[i].match(re_chars); // array of matches
		for (t = 0; t < chars.length; t++)
		{ // parse date based on mask -- attempt to extract each modifier value
			pos = sfmt.indexOf ('%');
			if (pos < 0)
				break;
			sfmt = sfmt.substr (pos);
			str = str.substr (pos);
			// find out the length based on actual match and modifier type
			chlen = this.modmatch (chars[t][2], str, parseInt (chars[t][1]), type);
	
			chars[t][3] = str.substr (0, chlen);
			// truncate mask and date following parsed components
			str = str.substr (chlen);
			sfmt = sfmt.substr ((chars[t][1] ? chars[t][1].length : 0) + chars[t][2].length + 1); // truncate processed beginning of mask
		}
	}

	function get_input_time ()
	{
		this.hr = document.getElementById (this.calobj + "_hr").value * 1;
		if (!this.tnom) // minutes enabled (convert to integer)
			this.min = document.getElementById (this.calobj + "_min").value * 1;
		if (!this.tnos) // seconds enabled (convert to integer)
			this.sec = document.getElementById (this.calobj + "_sec").value * 1;
		if (!this.hour24) // get 24 hour time
			this.hr = this.get_hour24 (this.hr);
		else // get am/pm designation based on 24 hour clock
			this.get_hour (this.hr);
	}

	function caction (sdate, y, m, d)
	{ // date onclick handler
		if (this.ctrl) // default date picker functionality
			this.ctrl.value = sdate;
		if (this.tctrl)
		{ // obtain time values from time input interface and place formatted results in form bound time input control
			this.get_input_time ();
			this.tctrl.value = this.time2string (this.tmask, this.hr, this.min, this.sec, this.pm);
		}
		if (!this.cbclick (y, m, d)) // call custom date onclick handler (callback)
			return false;

		// default date picker onclick functionality 
		dd_obj[this.index].hide (1); // hide calendar after selecting date
		return false;
	}

	function oaction (y, m, d)
	{ // date onmouseover handler
		if (!this.cbover (y, m, d)) // call custom date onmouseover handler (callback)
			return false;

		// default date picker onmouseover functionality
		return false;
	}

	function uaction (y, m, d)
	{ // date onmouseover handler
		if (!this.cbout (y, m, d)) // call custom date onmouseout handler (callback)
			return false;

		// default date picker onmouseout functionality
		return false;
	}
}


ddcal.prototype.cbclick = function (y, m, d)
{ // shell -- define extended custom functionality onclick date
	return true; // default execution next, false to bypass
};

ddcal.prototype.cbover = function (y, m, d)
{ // shell -- define extended custom functionality onmouseover date
	return true; // default execution next, false to bypass
};

ddcal.prototype.cbout = function (y, m, d)
{ // shell -- define extended custom functionality onmouseout date
	return true; // default execution next, false to bypass
};

var calendar = new ddcal ();
