/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. 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.
* 3. Neither the name of "Scrivo" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.
*
* $Id: Calendar.js 616 2013-04-22 23:48:38Z geert $
*/
"use strict";
SUI.dialog.Calendar = SUI.defineClass(
/** @lends SUI.dialog.Calendar.prototype */{
/** @ignore */ baseClass: SUI.dialog.OKCancelDialog,
/**
* @class
* SUI.dialog.Calendar is a class to display a date/time dialog. Depending
* of the given arguments a date, time or datetime selection box is shown.
*
* @augments SUI.dialog.OKCancelDialog
*
* @description
* Show a modal dialog for date and/or time selection.
*
* @constructs
* @param see base class
* @param {boolean} arg.iso Use the ISO 8601 week day system
* (true/default) or or the American (false) style
* @param {Date} arg.date Initial date selection
* @param {String} arg.type Type of the dialog "time", "date" (default)
* or "datetime"
*/
initializer: function(arg) {
SUI.dialog.Calendar.initializeBase(this, arg);
// use iso unless told not to
this.iso = arg.iso === undefined ? true : arg.iso;
// get the initial date
this._selDate = arg.date || new Date();
// display the calendar for the selected month and year
this._monthYear = this._selDate;
// should we need draw a date or time box or both
if (arg.type == "datetime") {
this._type = 3;
} else if (arg.type == "time") {
this._type = 1;
} else {
this._type = 2;
}
// set the appropriate caption
var t = SUI.i18n.captionDate;
if (this._type ==- 3) {
t = SUI.i18n.captionDatetime;
} else if (this._type == 1) {
t = SUI.i18n.captionTime;
}
this.caption(t);
// if we have a date box also initialize the date scrolling actions
if (this._type & 2) {
this._initCalendarActions();
}
},
/**
* Border width of borders around cells
*/
BORDER_WIDTH: 1,
/**
* (Standard) width of the calendar cells (including margin)
*/
BOX_WIDTH: 26,
/**
* (Standard) height of the calendar cells (including margin)
*/
BOX_HEIGHT: 20,
/**
* Margin between cells
*/
BOX_MARGIN: 2,
/**
* Size of the date navigation buttons
*/
BUTTON_SIZE: 22,
/**
* A little extra margin around the month calendar to give more space to
* the navigation bar
*/
BUTTON_EXTRA_SIDE_MARGIN: 1,
/**
* Margin of the month calendar
*/
DATE_MARGIN: 7,
/**
* Height of the date navigation bar or the time header
*/
HEADER_HEIGHT: 23,
/**
* Top of text in the header
*/
HEADER_TEXT_TOP: 3,
/**
* fraction to calculate width for the month and year scrollers
*/
HEADER_SPLIT_AT: .533,
/**
* Width of the margin between the date and time box
*/
SPLIT_MARGIN: 10,
/**
* Side margins of the time box (when there is no date box)
*/
TIME_SIDE_MARGIN: 24,
/**
* Bottom margin of the time box
*/
TIME_BOTTOM_MARGIN: 5,
/**
* Top margin of the date an time boxes
*/
TOP_MARGIN: 1,
/**
* Return the form's selected value
*/
formToData: function() {
this.close();
return this._selDate;
},
/**
* Show the calendar dialog centered on the screen
*/
show: function() {
this._drawCalendar();
this._setDialogSize();
this.center();
SUI.dialog.Calendar.parentMethod(this, "show");
},
// reference to the box with the date selector
_dateBox: null,
// month and year of the currently displayed calendar page
_mohthYear: null,
// currently selected date
_selDate: null,
// reference to the box with the time selector
_timeBox: null,
// binary selector 1 time, 2 date, 3 datetime
_type: true,
// add a CSS mouseover hover on a calender cell
_addHovers: function(cell) {
// set a CSS class on mouseover
SUI.browser.addEventListener(cell.el(), "mouseover",
function(e) {
if (!cell.addClass("sui-cal-day-hover")) {
SUI.browser.noPropagation(e);
}
}
);
// remove a CSS class on mouseout
SUI.browser.addEventListener(cell.el(), "mouseout",
function(e) {
if (!cell.removeClass("sui-cal-day-hover")) {
SUI.browser.noPropagation(e);
}
}
);
},
// set the _selectDate handler to the onclick of a cell
_addSelectDate: function(cell, val) {
var that = this;
SUI.browser.addEventListener(cell.el(), "click",
function(e) {
if (!that._selectDate(val)) {
SUI.browser.noPropagation(e);
}
}
);
},
// set the _selectHour handler to the onclick of a cell
_addSelectHour: function(cell, val) {
var that = this;
SUI.browser.addEventListener(cell.el(), "click",
function(e) {
if (!that._selectHour(val)) {
SUI.browser.noPropagation(e);
}
}
);
},
// set the _selectMinutes handler to the onclick of a cell
_addSelectMinutes: function(cell, val) {
var that = this;
SUI.browser.addEventListener(cell.el(), "click",
function(e) {
if (!that._selectMinutes(val)) {
SUI.browser.noPropagation(e);
}
}
);
},
// construct a date calendar box in which the user can select a date
_buildDateBox: function() {
var date = this._monthYear;
// create a new box for the month table
var cont = new SUI.Box({
width: 8 * this.BOX_WIDTH + this.BOX_MARGIN +
2 * this.BUTTON_EXTRA_SIDE_MARGIN,
height: this.HEADER_HEIGHT + 7 * this.BOX_HEIGHT +
this.BUTTON_EXTRA_SIDE_MARGIN
});
// add the navigation header to the box
this._drawDateNavigation(cont);
// set top and left position for drawing the week-day headers
var l = this.BUTTON_EXTRA_SIDE_MARGIN;
var t = this.HEADER_HEIGHT-this.BOX_MARGIN;
// start with the week number header
var dv = new SUI.TextBox({
parent: cont,
text: SUI.i18n.weekShort,
top: t + this.BOX_MARGIN,
left: l + this.BOX_MARGIN,
width: this.BOX_WIDTH - this.BOX_MARGIN,
height: this.BOX_HEIGHT - this.BOX_MARGIN,
border: new SUI.Border(this.BORDER_WIDTH),
padding: new SUI.Padding(0, 0, 0, this.BOX_MARGIN)
});
dv.addClass("sui-cal-header");
dv.display();
// move to the next column
l += this.BOX_WIDTH;
// now draw the week-day headers
for (var i=0; i<SUI.i18n.arrDaysShort.length; i++) {
dv = new SUI.TextBox({
parent: cont,
text: (this.iso) ? SUI.i18n.arrDaysShort[i]
: SUI.i18n.arrDaysShort[i?i-1:6],
top: t + this.BOX_MARGIN,
left: l,
width: this.BOX_WIDTH,
height: this.BOX_HEIGHT - this.BOX_MARGIN,
border: new SUI.Border(this.BORDER_WIDTH),
padding: new SUI.Padding(0, 0, 0, this.BOX_MARGIN)
});
dv.addClass("sui-cal-header");
dv.display();
// move to the next column
l += this.BOX_WIDTH;
}
// move to the next row
t += this.BOX_HEIGHT;
this._drawMonthTable(cont, t, this.BUTTON_EXTRA_SIDE_MARGIN);
return cont;
},
// construct a time table box in which the user can select a time
_buildTimeBox: function(date) {
// create a box for the time table
var cont = new SUI.Box({
width: 6 * this.BOX_WIDTH + this.BOX_MARGIN,
height: 6 * this.BOX_HEIGHT + 2 * this.HEADER_HEIGHT
});
// create a box for the time table title
var tm = new SUI.TextBox({
parent: cont,
top: this.HEADER_TEXT_TOP,
width: cont.width(),
height: this.HEADER_HEIGHT,
text: SUI.i18n.time
});
tm.el().style.textAlign = "center";
// create a box for the table section of the time table
var tt = new SUI.Box({
parent: cont,
top: this.HEADER_HEIGHT-this.BOX_MARGIN,
width: cont.width(),
height: this.BOX_HEIGHT*6
});
// draw the hours table
this._drawHoursTable(tt);
// draw the minutes table
var selsecs = this._drawMinutesTable(tt, 4 * this.BOX_HEIGHT);
// create a box for the the exact time row
var et = new SUI.Box({
parent: cont,
top: 6 * this.BOX_HEIGHT + this.HEADER_HEIGHT,
left: 0,
width: cont.width()-this.BOX_MARGIN,
height: this.HEADER_HEIGHT
});
et.el().style.textAlign = "right";
// the value for the minutes input field: don't use the multiples of 5
var min = this._selDate.getMinutes();
var v = (min % 5) ? min : "";
// create a label for the input box
var lbl = new SUI.form.Label({
title: SUI.i18n.exactMinutes+": ",
parent: et
});
lbl.el().style.position = "relative";
// create the input box
var inp = new SUI.form.Input({
width: this.BOX_WIDTH-2*this.BOX_MARGIN,
parent: et
});
inp.el().value = v;
inp.el().style.position = "relative";
// point the label to the input box
lbl.forBox(inp);
// set the handler to retrieve exact minutes input from the user
var that = this;
SUI.browser.addEventListener(inp.el(), "input",
function(e) {
if (!that._inputMinutes(inp.el().value, selsecs)) {
SUI.browser.noPropagation(e);
}
}
);
// display the elements of the time table
et.display();
tt.display();
tm.display();
inp.display();
return cont;
},
// draw the dialog content
_drawCalendar: function() {
// if we need to draw a date box ...
if (this._type & 2) {
// ... remove it if it was previously drawn
if (this._dateBox) {
this._dateBox.removeBox();
}
// create a new date box
this._dateBox = this._buildDateBox();
this._dateBox.top(this.TOP_MARGIN);
this._dateBox.left(this.DATE_MARGIN);
this._dateBox.parent(this.clientPanel.clientBox());
// and display it
this._dateBox.display();
}
// if we need to draw a time box ...
if (this._type & 1) {
// ... remove it if it was previously drawn
if (this._timeBox) {
this._timeBox.removeBox();
}
// create a new time box
this._timeBox = this._buildTimeBox();
this._timeBox.top(this.TOP_MARGIN);
// the left margin depends if there is a date box too
this._timeBox.left(this._dateBox
? this._dateBox.width() + this.DATE_MARGIN + this.SPLIT_MARGIN
: this.TIME_SIDE_MARGIN);
this._timeBox.parent(this.clientPanel.clientBox());
// and display it
this._timeBox.display();
}
},
// draw a bar with year and month scrollers and a button to go to the
// current date
_drawDateNavigation: function(cont) {
// width of the navigation header
var w = this.BOX_WIDTH * 8 + this.BOX_MARGIN +
this.BUTTON_EXTRA_SIDE_MARGIN * 2;
// object that holds the position of the toolbar buttons
var pos = {
"cal.prevm": 0,
"cal.nextm": Math.ceil(w * this.HEADER_SPLIT_AT, 10) -
this.BUTTON_SIZE,
"cal.prevy": Math.ceil(w * this.HEADER_SPLIT_AT, 10),
"cal.nexty": w - 2 * this.BUTTON_SIZE,
"cal.today": w - this.BUTTON_SIZE
};
// set the positions and actions of the toolbar buttons
for (var i in pos) {
var b = new SUI.ToolbarButton({
actionId: i,
left: pos[i],
width: this.BUTTON_SIZE,
height: this.BUTTON_SIZE
});
b.parent(cont);
b.setAction(this._actionList);
b.draw();
}
// create the month text box
var m = new SUI.TextBox({
parent: cont,
text: SUI.i18n.arrMonths[this._monthYear.getMonth()],
top: this.HEADER_TEXT_TOP,
left: this.BUTTON_SIZE,
width: pos["cal.nextm"] - this.BUTTON_SIZE,
height: this.HEADER_HEIGHT
});
m.el().style.textAlign = "center";
m.display();
// create the year text box
var y = new SUI.TextBox({
parent: cont,
text: this._monthYear.getFullYear(),
top: this.HEADER_TEXT_TOP,
left: pos["cal.prevy"] + this.BUTTON_SIZE,
width: pos["cal.nexty"] - this.BUTTON_SIZE-pos["cal.prevy"],
height: this.HEADER_HEIGHT
});
y.el().style.textAlign = "center";
y.display();
},
// draw a table of 4 by 6 cells for the hours of the day
_drawHoursTable: function(tt) {
var l = 0;
var t = 0;
// 24 hours in 4 rows ...
for (var i=0; i<4; i++) {
l = 0;
// ... of 6 cells
for (var j=0; j<6; j++) {
// current hour
var x = i*6+j;
// get the style
var st = "sui-cal-day";
if (x == this._selDate.getHours() ) {
st += " sui-cal-selected";
}
var dv = new SUI.TextBox({
parent: tt,
text: String(x),
top: t+this.BOX_MARGIN,
left: l+this.BOX_MARGIN,
width: this.BOX_WIDTH-this.BOX_MARGIN,
height: this.BOX_HEIGHT-this.BOX_MARGIN,
border: new SUI.Border(this.BORDER_WIDTH)
});
dv.addClass(st);
dv.display();
// add the event handlers (hover and onclick)
this._addHovers(dv);
this._addSelectHour(dv, x);
// move to the next cell
l += this.BOX_WIDTH;
}
// move to the next row
t += this.BOX_HEIGHT;
}
},
// draw a table of 2 by 6 cells for the minutes of the day
_drawMinutesTable: function(tt, t) {
var l = 0;
var selsecs = null;
// 12 increments of 5 minutes in 2 rows ...
for (var i=0; i<2; i++) {
l = 0;
// ... of 6 cells
for (var j=0; j<6; j++) {
// current minutes
var x = (i*6+j)*5;
// get the style
var st = "sui-cal-weekend";
if (x == this._selDate.getMinutes() ) {
st += " sui-cal-selected";
}
// create zero padded variant of x
var x1 = x<10 ? ":0"+x : ":"+x;
var cm = new SUI.TextBox({
parent: tt,
text: x1,
top: t+this.BOX_MARGIN,
left: l+this.BOX_MARGIN,
width: this.BOX_WIDTH-this.BOX_MARGIN,
height: this.BOX_HEIGHT-this.BOX_MARGIN,
border: new SUI.Border(this.BORDER_WIDTH)
});
cm.addClass(st);
cm.display();
// add the event handlers (hover and onclick)
this._addHovers(cm);
this._addSelectMinutes(cm, x);
if (x == this._selDate.getMinutes() ) {
selsecs = cm;
}
// move to the next cell
l+=this.BOX_WIDTH;
}
// move to the next row
t += this.BOX_HEIGHT;
}
return selsecs;
},
// draw a table of x by 8 (week no and 7 days) cells for the days of
// the month
_drawMonthTable: function(cont, top, left) {
var date = this._monthYear;
// get a data structure containing the data for this month
var cal = this._getCalendar(date, this.iso);
for (var wn in cal) {
var l = left;
// draw the week number
var dv = new SUI.TextBox({
parent: cont,
text: wn,
top: top,
left: l + this.BOX_MARGIN,
width: this.BOX_WIDTH - this.BOX_MARGIN,
height: this.BOX_HEIGHT,
border: new SUI.Border(this.BORDER_WIDTH),
padding: new SUI.Padding(this.BOX_MARGIN, 0)
});
dv.addClass("sui-cal-weekno");
dv.display();
// move to the next column
l += this.BOX_WIDTH;
for (var i=0; i<cal[wn].length; i++) {
// get the day we're drawing
var d = cal[wn][i];
// determine the CSS style, does it belong to the month?
if (d.getMonth() == date.getMonth()) {
var st = "sui-cal-day";
} else {
var st = "sui-cal-otherday";
}
// is it a day of the week or weekend ?
if (d.getDay() == 0 || d.getDay() == 6) {
st += " sui-cal-weekend";
}
// is it the selected day ?
if (d.getDate() == this._selDate.getDate() &&
d.getMonth() == this._selDate.getMonth() &&
d.getFullYear() == this._selDate.getFullYear()) {
st += " sui-cal-selected";
}
dv = new SUI.TextBox({
parent: cont,
text: d.getDate(),
top: top + this.BOX_MARGIN,
left: l + this.BOX_MARGIN,
width: this.BOX_WIDTH - this.BOX_MARGIN,
height: this.BOX_HEIGHT - this.BOX_MARGIN,
border: new SUI.Border(this.BORDER_WIDTH)
});
dv.addClass(st);
dv.display();
// add the event handlers (hover and onclick)
this._addHovers(dv);
this._addSelectDate(dv, d);
// move to the next column
l += this.BOX_WIDTH;
}
// move to the next row
top += this.BOX_HEIGHT;
}
},
// Return a an object with week rows (each member name is a week no) in
// which each row has 7 elements, each representing a weekday. The whole
// array is representing a month. To render a calendar you'll just need
// to loop through this array.
_getCalendar: function(d, iso) {
var m = d.getMonth();
var y = d.getFullYear();
// get the first day (ISO: Monday, American: Sunday) of the first week
// of the year (start week numbering). ISO: the week with the fourth
// of January, American the week with the first of January
var x = this.iso ? 4 : 1;
var w1 = new Date(y, 0, x);
for (; w1.getDay() != this.iso ? 1 : 0; x--) {
w1 = new Date(y, 0, x);
}
// get the first day (ISO: Monday, American: Sunday) of the first week
// of current month
x = 2;
do {
x--;
var fd = new Date(y, m, x);
} while (fd.getDay() != this.iso ? 1 : 0);
// get the week number of the first week of the month
var wn = Math.round((fd.getTime() - w1.getTime()) / 604800000) + 1;
var cal = {};
do {
// set the week array (wn is the week no) ...
cal[wn] = [];
// ... fill it with the save Date()'s of the week
for (var i=0; i<7; i++) {
// store 'first day' ...
cal[wn].push(fd);
// ... and move to the next day
x++;
fd = new Date(y, m, x);
}
wn ++;
// as long as we're in the current month
} while (fd.getMonth() == m);
return cal;
},
// Set the date scrolling actions: next/prev month, prev/next year and
// current date. All actions set the _mohthYear value and display the
// calendar based on that value. The current date selection also selects
// the current date.
_initCalendarActions: function() {
var that = this;
this._actionList = new SUI.ActionList([{
actionId: "cal.prevm",
icon: SUI.resource.calPrev,
title: SUI.i18n.prevMonth,
handler: function() {
// display the previous month
that._monthYear = new Date(that._monthYear.getFullYear(),
that._monthYear.getMonth()-1, 1);
that._drawCalendar();
}
},{
actionId: "cal.nextm",
icon: SUI.resource.calNext,
title: SUI.i18n.nextMonth,
handler: function() {
// display the next month
that._monthYear = new Date(that._monthYear.getFullYear(),
that._monthYear.getMonth()+1, 1);
that._drawCalendar();
}
},{
actionId: "cal.prevy",
icon: SUI.resource.calPrev,
title: SUI.i18n.prevYear,
handler: function() {
// display the previous year
that._monthYear = new Date(that._monthYear.getFullYear()-1,
that._monthYear.getMonth(), 1);
that._drawCalendar();
}
},{
actionId: "cal.nexty",
icon: SUI.resource.calNext,
title: SUI.i18n.nextYear,
handler: function() {
// display the next year
that._monthYear = new Date(that._monthYear.getFullYear()+1,
that._monthYear.getMonth(), 1);
that._drawCalendar();
}
},{
actionId: "cal.today",
icon: SUI.resource.calToday,
title: SUI.i18n.goToday,
handler: function() {
// display and select the current date
that._monthYear = that._selDate = new Date();
that._drawCalendar();
}
}
]);
},
// get the input from the user on exact minutes entry
_inputMinutes: function(value, selsecs) {
// if a minutes cell was selected, remove the CSS class
if (selsecs) {
selsecs.removeClass("sui-cal-selected");
}
// try to get a valid value from user input ...
var m = parseInt(value, 10);
if (isNaN(m)) {
m = 0;
}
m = m < 0 ? 0 : m > 59 ? 59 : m;
// ... and use that to set the selected minutes
this._selDate.setMinutes(m);
},
// set the dialog's date selection and redraw the calendar
_selectDate: function(v) {
this._selDate.setFullYear(v.getFullYear(), v.getMonth(), v.getDate());
this._monthYear = this._selDate;
this._drawCalendar();
},
// set the dialog's hour selection and redraw the calendar
_selectHour: function(val) {
this._selDate.setHours(val);
this._drawCalendar();
},
// set the dialog's minutes selection and redraw the calendar
_selectMinutes: function(val) {
this._selDate.setMinutes(val);
this._drawCalendar();
},
// determine the initial dialog size
_setDialogSize: function() {
var w = 0;
var h = 0;
// if there's only a date box ...
if (this._dateBox) {
// ... add the margins to get to dialog width an height
w = this._dateBox.width() + 2 * this.DATE_MARGIN;
h = this.TOP_MARGIN + this._dateBox.height() + this.DATE_MARGIN;
}
// if there is a time box ...
if (this._timeBox) {
// ... add the margins to the height of the box ...
var h2 = this.TOP_MARGIN + this._timeBox.height()
+ this.TIME_BOTTOM_MARGIN;
// ... and use that if it is larger (isn't it always?)
if (h2 > h) {
h = h2;
}
// if there is width set by the date box already ...
if (w) {
// ... add date box like margins to the time box left to
// get the dialog width ...
w = this._timeBox.left() + this._timeBox.width()
+ this.DATE_MARGIN + this.BUTTON_EXTRA_SIDE_MARGIN;
} else {
// ... else add margins to the time box
w = this._timeBox.width() + 2 * this.TIME_SIDE_MARGIN;
}
}
// set the dialogs client size
this.setClientWidth(w);
this.setClientHeight(h);
}
});