/**
* 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">«</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">»</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;
})();