GpsGate Server JavaScript API

Map  1.0.0

GpsGate Server JavaScript API > Map > popupcalendar.js (source view)
Search:
 
Filters
/**
 * Copyright Franson Technology AB, Sweden, 2009
 * http://gpsgate.com, http://franson.com
 *
 * author Fredrik Blomqvist
 *
 * @module UI
 *
 */

/*
 * todo: better configurability, anchor points etc
 * todo: keep state for each input box? + option to just/also return Date obj
 * todo: support adding bold or underline font to days tagged to have content data?
 * todo: rewrite to separate instance obj style? (from this flyweight style, to enable non-popup usage)
 *
 */

var Franson = Franson || {};
Franson.UI = Franson.UI || {};


/**
 * singleton<br />
 * Initially based on the "Clean Calendar Built from Scratch" by Marc Grabanski (Creative Commons Licence http://creativecommons.org/licenses/by/3.0/)
 * @class Franson.UI.PopupCalendar
 * @static
 */
Franson.UI.PopupCalendar = (function()
{
	// constants (localized on init)
	var english_monthNames = [ 'January','February','March','April','May','June','July','August','September','October','November','December' ];
	var english_dayNames = [ 'Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday' ];
	var english_shortWeekDays = [ 'S','M','T','W','T','F','S' ];

	// filled with localized names in .init
	var monthNames = [];
	var shortWeekDays = [];
	// dayNames not used

	var _calendarId = 'calendarDiv';
	var _currentTarget = null;
	var _startDate = null;

	// todo: these accessors should be possible
	// to configure from the outside (to be able
	// to hook to other elems beside input boxes and localization)
	// assumes 'elem' is an input box
	function getDate(elem)
	{
		return Franson.DateTime.fuzzyDateParse(elem.value);
	}

	// assumes 'elem' is an input box
	function setDate(elem, dayMonthYear)
	{
		function padTwo(num)
		{
			return num < 10 ? '0' + num : '' + num;
		}
		function formatDate(Day, Month, Year)
		{
			// todo: expose formatting as setting (locale)
			return Year + '-' + padTwo(Month+1) + '-' + padTwo(Day);
		}

		elem.value = formatDate(dayMonthYear[0], dayMonthYear[1], dayMonthYear[2]);
		// todo: signal an 'onupdated'-event here?
	}

	// hooked to inputbox(es)
	function popUp(e)
	{
		var calendarDiv = $(_calendarId);
		if (_currentTarget == this && calendarDiv.style.display == 'block')
			return;

		var bounds = Franson.DOM.getElementBounds(this);
		var pos = { x: bounds.x, y: bounds.y + bounds.h };

		MochiKit.Style.setElementPosition(calendarDiv, pos);
		calendarDiv.style.display = 'block';

		var startDate = getDate(this);

		if (startDate !== null)
		{
			startDate = new Date(startDate[0], startDate[1]-1, startDate[2]);
		}
		else
		{
			startDate = new Date();
		}

		_startDate = startDate;

		popUpCal.drawCalendar(this, startDate);
	}

	function hide()
	{
		$(_calendarId).style.display = 'none';
	}


	function init() // todo: add settings etc
	{
		if (monthNames.length === 0 && typeof(Franson.localize) !== 'undefined')
		{
			for (var i = 0; i < 12; ++i)
			{
				monthNames.push(Franson.localize('CALENDAR_' + english_monthNames[i].toUpperCase()));
			}
		}
		else
		{
			monthNames = english_monthNames;
		}

		if (shortWeekDays.length === 0 && typeof(Franson.localize) !== 'undefined')
		{
			for (var i = 0; i < 7; ++i)
			{
				shortWeekDays.push(Franson.localize('CALENDAR_SHORT_' + english_dayNames[i].toUpperCase()));
			}
		}
		else
		{
			shortWeekDays = english_shortWeekDays;
		}

		var calendarDiv = $(_calendarId);
		if (!calendarDiv)
		{
			calendarDiv = MochiKit.DOM.DIV({
				'id': _calendarId,
				'style': {
					'position': 'absolute',
					'z-index': 100, // lightbox layer is set to 90, need to be above that
					'display': 'none'	// initially hidden
				}
			});

			var body = Franson.DOM.body(); // "body.appendChild(calendarDiv)" results in layout change..
			body.insertBefore(calendarDiv, body.firstChild);
			body = null; // don't leak

			// todo: hmm, add keyboard control of days/week/months also? (arrows + ctrl & shift)
			connect(document, 'onkeydown', function(e)
			{
				switch (e.key().string)
				{
					case 'KEY_TAB':
					case 'KEY_ENTER':
					case 'KEY_ESCAPE':
					// .. more?
						hide();
						break;
				}
			});

			// close on all outside clicks (except target element)
			Franson.Event.onOutsideClick(calendarDiv, function(e)
			{
				if (e.target() != _currentTarget)
					hide();
			});
		}
		calendarDiv = null; // don't leak
	}

	// public API
	var popUpCal =
	{
		selectedDate: new Date(),

		// convenience
		selectedMonth: function() { return popUpCal.selectedDate.getMonth(); }, // 0-11
		selectedYear: function() { return popUpCal.selectedDate.getFullYear(); }, // 4-digit year
		selectedDay: function() { return popUpCal.selectedDate.getDate(); },

		/**
		 * @method attach
		 * @param {INPUT[]} inputBoxes
		 */
		attach: function(inputBoxes) // todo: should rather drop support for multi-input for consistency I'd say..
		{
			init();

			var events = [];

			for (var i = 0; i < inputBoxes.length; ++i)
			{
				var onFocus = Franson.Event.connectOnce(inputBoxes[i], 'onfocus', popUp);
				if (onFocus !== null)
					events.push(onFocus);

				var onClick = Franson.Event.connectOnce(inputBoxes[i], 'onclick', popUp); // test to toggle visibility when clicking same inputbox? todo: needs a delay or similar otherwise the following popUp will launch immidiately..
				if (onClick !== null)
					events.push(onClick);
			}

			return events;
		},

		drawCalendar: function(inputObj, startDate)
		{
			_currentTarget = inputObj;
			if (startDate)	// todo: selectedDate and startDate(=drawdate) are currently a bit intermingled..
			{
				popUpCal.selectedDate = startDate;
			}
			else
			{
				startDate = popUpCal.selectedDate;
			}

			// todo: rearrange table to put prev/next arrows on same rows as month-name (i.e remove one row)
			// todo: use mochi.DOM.createDOM
			var html = '';
			html += '<table id="linksTable" cellpadding="0" cellspacing="0" >';
			html += 	'<tr>';
			html += 		'<td><a id="prevMonth">&laquo</a></td>'; // '<<' // (add title="previous month"? (removed now to skip localization overhead)
		//	html += 		'<td class="calendarHeader">' + monthNames[popUpCal.selectedMonth()] + ' ' + popUpCal.selectedYear() + '</td>';
			html +=		 	'<td><a id="nextMonth">&raquo</a></td>'; // '>>' //  title="next month"
			html += 	'</tr>';
			html += '</table>';

			html += '<table id="calendar" cellpadding="0" cellspacing="0">';
			html += '<tr>';
			html += 	'<th colspan="7" class="calendarHeader">' + monthNames[popUpCal.selectedMonth()] + ' ' + popUpCal.selectedYear() + '</th>';
			html += '</tr>';

			html += '<tr class="weekDaysTitleRow">';
			for (var i = 0; i < shortWeekDays.length; ++i)
			{
				html += '<td>' + shortWeekDays[i] + '</td>';
			}

			var daysInMonth = Franson.DateTime.getDaysInMonth(popUpCal.selectedYear(), popUpCal.selectedMonth());
			var startDay = Franson.DateTime.getFirstDayofMonth(popUpCal.selectedYear(), popUpCal.selectedMonth());

			// calculate the number of rows to generate
			var numRows = startDay != 7 ? Math.ceil((startDay + 1 + daysInMonth) / 7) : 0;

			// calculate number of days before calendar starts
			var noPrintDays = startDay != 7 ? startDay + 1 : 0; // if sunday print right away

			var cellDate = new Date(popUpCal.selectedYear(), popUpCal.selectedMonth(), 1 - noPrintDays);

			// create calendar rows
			var printDate = 1;
			for (var row = 0; row < numRows; ++row)
			{
				html += '<tr class="weekDaysRow">';
				// create calendar days
				for (var column = 0; column < 7; ++column)
				{
					if (Franson.DateTime.isSameDay(cellDate, _startDate))
					{
						html += '<td id="today" class="weekDaysCell">';
					}
					else
					{
						if (column === 0 || column == 6)
							html += '<td class="weekEndCell">';
						else
							html += '<td class="weekDaysCell">';
					}

					if (noPrintDays === 0)
					{
						if (printDate <= daysInMonth)
						{
							html += '<a>' + printDate + '</a>';
						}
						++printDate;
					}
					html += '</td>';
					if (noPrintDays > 0)
						--noPrintDays;

					cellDate = new Date(cellDate.getFullYear(), cellDate.getMonth(), cellDate.getDate() + 1);
				}
				html += '</tr>';
			}

			// attach a centered [today] button at the bottom
			html += '<tr><td colspan="7"><input type="button" id="todayButton" value="Today"></input></td></tr>'; // why the tiny margin on the left?!

			html += '</table>';


			// add calendar to element to calendar Div
			// .. but do some IE GC before that.. (todo: use Mochi-Signals and event-propagation here)
			//{
				var linkTable = $('linksTable');
				if (linkTable)
				{
					var prevNextLinks = linkTable.getElementsByTagName('a');
					for (var i = 0; i < prevNextLinks.length; ++i)
						prevNextLinks[i].onclick = null;
				}
				var calDaysBlock = $('calendar');
				if (calDaysBlock)
				{
					var dayLinks = calDaysBlock.getElementsByTagName('a');
					for (var i = 0; i < dayLinks.length; ++i)
						dayLinks[i].parentNode.onclick = null;
				}
				var todayBtn = $('todayButton');
				if (todayBtn) todayBtn.onclick = null;
			//}
			$(_calendarId).innerHTML = html;

			// setup next and previous links (todo: these could be generated and connected just once I guess)
			$('prevMonth').onclick = function()
			{
				var d = popUpCal.selectedDate;
				popUpCal.drawCalendar(inputObj, new Date(d.getFullYear(), d.getMonth() - 1, d.getDate()));
			};

			$('nextMonth').onclick = function()
			{
				var d = popUpCal.selectedDate;
				popUpCal.drawCalendar(inputObj, new Date(d.getFullYear(), d.getMonth() + 1, d.getDate()));
			};

			$('todayButton').onclick = function(e)
			{
				var event = e || window.event;
				if (event.stopPropagation) {
					event.stopPropagation();
				} else {
					event.cancelBubble = true;
				}

				var today = new Date();

				// if clicking today twice (or cal is already at today) calendar is closed and today is selected.
				if (Franson.DateTime.isSameDay(today, popUpCal.selectedDate))
				{
					hide();
					setDate(inputObj, [today.getDate(), today.getMonth(), today.getFullYear()]);
				}
				else
				{
					// highlight today
					_startDate = today;
					popUpCal.drawCalendar(inputObj, today);
				}
			};

			// set up link events on calendar table
			// todo: use a single handler instead?
			var dayLinks = $('calendar').getElementsByTagName('a');
			for (var i = 0; i < dayLinks.length; ++i)
			{
				dayLinks[i].parentNode.onclick = function() // note how we hook to the parent-cell instead of link to capture entire area
				{
					hide();
					popUpCal.selectedDate = new Date(popUpCal.selectedYear(), popUpCal.selectedMonth(), parseInt(this.firstChild.innerHTML, 10));

					setDate(inputObj, [popUpCal.selectedDay(), popUpCal.selectedMonth(), popUpCal.selectedYear()]);
				};
			}
			dayLinks = null;
		} // end drawCalendar function

	}; // popCal obj

	return popUpCal;

})();

Copyright © 2009 Franson Technology AB, Sweden. All rights reserved.