You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

819 lines
25 KiB
JavaScript

/*!
* glDatePicker v2.0
* http://glad.github.com/glDatePicker/
*
* Copyright (c) 2013 Gautam Lad. All rights reserved.
* Released under the MIT license.
*
* Date: Tue Jan 1 2013
*/
;(function() {
$.fn.glDatePicker = function(options) {
var pluginName = 'glDatePicker';
// Find the plugin attached to the element
var instance = this.data(pluginName);
// If the instance wasn't found, create it...
if(!instance) {
// Return the element being bound to
return this.each(function() {
return $(this).data(pluginName, new glDatePicker(this, options));
});
}
// ...otherwise if the user passes true to the plugin (on the second call),
// then return the instance of the plugin itself
return (options === true) ? instance : this;
};
// Default options
$.fn.glDatePicker.defaults =
{
// Style to use for the calendar. This name must match the name used in
// the stylesheet, using the class naming convention "gldp-cssName".
cssName: 'default',
// The z-index for the calendar control.
zIndex: 1000,
// Thickness of border (in pixels)
borderSize: 1,
// The number of pixels to offset the calendar's position on the page.
calendarOffset: { x: 0, y: 1 },
// Set to true if you want the calendar to be visible at all times.
// NOTE: If your target element is hidden, the calendar will be hidden as well.
showAlways: false,
// Hide the calendar when a date is selected (only if showAlways is set to false).
hideOnClick: true,
// Allow selection of months by clicking on the month in the title.
allowMonthSelect: true,
// Allow selection of years by clicking on the year in the title.
allowYearSelect: true,
// The date that will be treated as 'today'.
todayDate: new Date(),
// The date that will appear selected when the calendar renders.
// By default it will be set to todayDate.
selectedDate: null,
// Arrows used for the Previous and Next month buttons on the title.
// Set these to blank to hide the arrows completely.
prevArrow: '\u25c4',
nextArrow: '\u25ba',
// A collection of dates that can be selectable by the user.
// The dates can be a one-time selection or made repeatable by setting
// the repeatYear or repeatMonth flag to true.
// By default repeatYear and repeatMonth are false.
//
// This example creates 4-individual dates that can be selected;
// The first date will repeat every year, the second date will repeat every
// month and year, the third date will repeat every month and the fourth date
// will only be selectable one-time and not repeat:
//
// selectableDates: [
// { date: new Date(0, 8, 5), repeatYear: true },
// { date: new Date(0, 0, 14), repeatMonth: true, repeatYear: true },
// { date: new Date(2013, 0, 24), repeatMonth: true },
// { date: new Date(2013, 11, 25) },
// ]
selectableDates: null,
// A collection of date ranges that are selectable by the user.
// The ranges can be made to repeat by setting repeatYear to true
// (repeatMonth is not supported).
//
// This example will create 3-sets of selectable date ranges with
// specific from and to ranges. The 4th and 5th ranges don't specify
// the "to" date in which case the "to" date will be the maximum days for
// the month specified in "from". The 4th and 5th ranges also repeat every year:
//
// selectableDateRange: [
// { from: new Date(2013, 1, 1), to: newDate (2013, 2, 1) },
// { from: new Date(2013, 4, 1), to: newDate (2013, 8, 1) },
// { from: new Date(2013, 7, 10), to: newDate (2013, 9, 10) },
// { from: new Date(0, 8, 10), repeatYear: true }
// { from: new Date(0, 9, 1), repeatYear: true }
// ]
selectableDateRange: null,
// Mark certain dates as special dates. Similar to selectableDates, this
// property supports both repeatYear and repeatMonth flags.
// Each special date can be styled using custom style names and can have
// data attached to it that will be returned in the onClick callback.
// The data field can be any custom (JSON style) object.
//
// This example creates two (repeatable by year) dates with special data in them.
// The first date also assigns a special class (which you will have to define).
// specialDates: [
// {
// date: new Date(0, 8, 5),
// data: { message: 'Happy Birthday!' },
// repeatYear: true,
// cssClass: 'special-bday'
// },
// {
// date: new Date(2013, 0, 8),
// data: { message: 'Meeting every day 8 of the month' },
// repeatMonth: true
// }
// ]
specialDates: null,
// List of months that can be selectable, including when the user clicks
// on the title to select from the dropdown.
// This example only makes two months visible; September and December:
// selectableMonths: [8, 11]
selectableMonths : null,
// List of selectable years. If not provided, will default to 5-years
// back and forward.
// This example only allows selection of dates that have year 2012, 2013, 2015
// selectableYears: [2012, 2013, 2015]
selectableYears: null,
// List of selectable days of the week. 0 is Sunday, 1 is Monday, and so on.
// This example allows only Sunday, Tuesday, Thursday:
// selectableDOW: [0, 2, 4]
selectableDOW : null,
// Names of the month that will be shown in the title.
// Will default to long-form names:
// January, February, March, April, May, June, July,
// August, September, October, November, December
monthNames: null,
// Names of the days of the Week that will be shown below the title.
// Will default to short-form names:
// Sun, Mon, Tue, Wed, Thu, Fri, Sat
dowNames: null,
// The day of the week to start the calendar on. 0 is Sunday, 1 is Monday and so on.
dowOffset: 0,
// Callback that will trigger when the user clicks a selectable date.
// Parameters that are passed to the callback:
// el : The input element the date picker is bound to
// cell : The cell on the calendar that triggered this event
// date : The date associated with the cell
// data : Special data associated with the cell (if available, otherwise, null)
onClick: (function(el, cell, date, data) {
el.val(date.toLocaleDateString());
}),
// Callback that will trigger when the user hovers over a selectable date.
// This callback receives the same set of parameters as onClick.
onHover: function(el, cell, date, data) {},
// Callback that will trigger when the calendar needs to show.
// You can use this callback to animate the opening of the calendar.
onShow: function(calendar) { calendar.show(); },
// Callback that will trigger when the calendar needs to hide.
// You can use this callback to animate the hiding of the calendar.
onHide: function(calendar) { calendar.hide(); },
// First date of the month.
firstDate: null
};
// Our plugin object
var glDatePicker = (function() {
// Main entry point. Initialize the plugin
function glDatePicker(element, userOptions) {
// Grab handle to this
var self = this;
// Save bound element to el
self.el = $(element);
var el = self.el;
// Merge user options into default options
self.options = $.extend(true, {}, $.fn.glDatePicker.defaults, userOptions);
var options = self.options;
// Find the calendar element if the user provided one
self.calendar = $($.find('[gldp-el=' + el.attr('gldp-id') + ' ]'));
// Default first date to selected
options.selectedDate = options.selectedDate || options.todayDate;
options.firstDate = (new Date((options.firstDate || options.selectedDate)))._first();
if(!(el.attr('gldp-id') || '').length) {
el.attr('gldp-id', 'gldp-' + Math.round(Math.random() * 1e10))
}
// Show the plugin on focus
el
.addClass('gldp-el')
.bind('click', function(e) { self.show(e); })
.bind('focus', function(e) { self.show(e); });
// If the user is defining the container and it exists, hide it on initial creation.
// The update function will handle showing if it's showAlways = true
if(self.calendar.length && !options.showAlways) {
self.calendar.hide();
}
// Hide the plugin on mouse up outside of the plugin
$(document).bind('mouseup', function(e) {
var target = e.target;
var calendar = self.calendar;
if(!el.is(target) && !calendar.is(target) && calendar.has(target).length === 0 && calendar.is(':visible')) {
self.hide();
}
});
// Render calendar
self.render();
};
// Public methods
glDatePicker.prototype =
{
show: function() {
// Hide others and show this calendar
$.each($('.gldp-el').not(this.el), function(i, o) {
if(o.length) { o.options.onHide(o.calendar) ; }
});
// Show this calendar
this.options.onShow(this.calendar);
},
hide: function() {
if(this.options && !this.options.showAlways) {
this.options.onHide(this.calendar);
}
},
// Render the calendar
render: function(renderCalback) {
var self = this;
var el = self.el;
var options = self.options;
var calendar = self.calendar;
// Build a core class (with border) that every element would have
var coreClass = ' core border ';
var cssName = 'gldp-' + options.cssName;
// Get today
var todayVal = options.todayDate._val();
var todayTime = todayVal.time;
// Constants
var maxRow = 6;
var maxCol = 7;
var borderSize = options.borderSize + 'px';
// Helper function to build selectable list
var getSelectableList = function(min, max, userList) {
// Build a default list using min/max
var resultList = [];
for(var i = min; i <= max; i++) { resultList.push(i); }
// If user provided a collection, sanitize list by ensuring it's within range and unique
if(userList) {
var newList = [];
$.each(userList, function(i, v) {
if(v >= min && v <= max && newList._indexOf(v) < 0) {
newList.push(v);
}
});
resultList = newList.length ? newList : resultList;
};
// Sort the values before returning it
resultList.sort();
return resultList;
};
// Selectable (constants)
var selectableMonths = getSelectableList(0, 11, options.selectableMonths);
var selectableYears = getSelectableList(todayVal.year - 5, todayVal.year + 5, options.selectableYears);
var selectableDOW = getSelectableList(0, 6, options.selectableDOW);
var dowNames = options.dowNames || [ '日', '一', '二', '三', '四', '五', '六' ];
var monthNames = options.monthNames || [ '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月' ];
// Create cell width based on el size
var containerWidth = el.outerWidth();
var containerHeight = containerWidth;
// Create cell size based on container size
var getCellSize = function(_size, _count) {
return (_size / _count) + ((options.borderSize / _count) * (_count - 1));
};
var cellWidth = getCellSize(containerWidth, maxCol);
var cellHeight = getCellSize(containerHeight, maxRow + 2);
// If calendar doesn't exist, create it and re-assign it to self
if(!calendar.length) {
self.calendar = calendar = $('<div/>')
.attr('gldp-el', el.attr('gldp-id'))
.data('is', true)
.css(
{
display: (options.showAlways ? undefined : 'none'),
zIndex: options.zIndex,
width: (cellWidth * maxCol) + 'px'
});
$('body').append(calendar);
}
else {
if(!eval(calendar.data('is'))) {
containerWidth = calendar.outerWidth();
containerHeight = calendar.outerHeight();
cellWidth = getCellSize(containerWidth, maxCol);
cellHeight = getCellSize(containerHeight, maxRow + 2);
}
}
// Hide calendar if the target element isn't visible
if(!el.is(':visible')) { calendar.hide(); }
// Add core classes and remove calendar's children
calendar
.removeClass()
.addClass(cssName)
.children().remove();
// Bind to resize event to position calendar
var onResize = function() {
var elPos = el.offset();
calendar.css(
{
top: (elPos.top + el.outerHeight() + options.calendarOffset.y) + 'px',
left: (elPos.left + options.calendarOffset.x) + 'px'
});
};
$(window).resize(onResize);
onResize();
// Create variables for cells
var cellCSS =
{
width: cellWidth + 'px',
height: cellHeight + 'px',
lineHeight: cellHeight + 'px'
};
// Helper function to setDate
var setFirstDate = function(_date) {
if(_date) {
// Get first date
options.firstDate = _date;
// Update the calendar
self.render();
}
};
var getFirstDate = function(_offset) {
// Create start date as the first date of the month
var _date = new Date(options.firstDate);
// Default to no offset
_offset = _offset || 0;
// Find out which months are selectable
while(true) {
// Adjust date for month offset
_date.setMonth(_date.getMonth() + _offset);
_date.setDate(Math.min(1, _date._max()));
// If not an offset, break out of the loop
if(_offset == 0) { break; }
// Get _date's value
var dateVal = _date._val();
// Get local vars
var dateMonth = dateVal.month;
var dateYear = dateVal.year;
// Find the month first
if(selectableMonths._indexOf(dateMonth) != -1) {
// If year is in our collection, break...
if(selectableYears._indexOf(dateYear) != -1) {
break;
}
else {
// ...otherwise, if it's out of bounds, exit loop
if(dateYear < selectableYears[0] || dateYear > selectableYears[selectableYears.length - 1]) {
return null;
}
}
}
}
return _date;
};
// Get the previous, next first dates
var prevFirstDate = getFirstDate(-1);
var nextFirstDate = getFirstDate(1);
// Get the first date for the current month being rendered
var firstDate = (options.firstDate = getFirstDate());
var firstDateVal = firstDate._val();
var firstDateMonth = firstDateVal.month;
var firstDateYear = firstDateVal.year;
// Get the start date in the calendar
var startDate = new Date(firstDate);
// Sanitize days of the week offset
var dowOffset = Math.abs(Math.min(6, Math.max(0, options.dowOffset)));
// Offset weekdays
var startOffset = startDate.getDay() - dowOffset;
startOffset = startOffset < 1 ? -7 - startOffset : -startOffset;
dowNames = (dowNames.concat(dowNames))
.slice(dowOffset, dowOffset + 7);
// Offset the start date
startDate._add(startOffset);
// Gather flags for prev/next arrows
var showPrev = (prevFirstDate);
var showNext = (nextFirstDate);
// Create the arrows and title
var monyearClass = coreClass + 'monyear ';
var prevCell = $('<div/>')
.addClass(monyearClass)
.css(
$.extend({}, cellCSS,
{
borderWidth: borderSize + ' 0 0 ' + borderSize
})
)
.append(
$('<a/>')
.addClass('prev-arrow' + (showPrev ? '' : '-off'))
.html(options.prevArrow)
)
.mousedown(function() { return false; })
.click(function(e) {
if(options.prevArrow != '' && showPrev) {
e.stopPropagation();
setFirstDate(prevFirstDate);
}
});
var titleCellCount = maxCol - 2;
var titleWidth = (cellWidth * titleCellCount) - (titleCellCount * options.borderSize) + (options.borderSize);
var titleCell = $('<div/>')
.addClass(monyearClass + 'title')
.css(
$.extend({}, cellCSS,
{
width: titleWidth + 'px',
borderTopWidth: borderSize,
marginLeft: '-' + (borderSize)
})
);
var nextCell = $('<div/>')
.addClass(monyearClass)
.css(
$.extend({}, cellCSS,
{
marginLeft: '-' + (borderSize),
borderWidth: borderSize + ' ' + borderSize + ' 0 0'
})
)
.append(
$('<a/>')
.addClass('next-arrow' + (showNext ? '' : '-off'))
.html(options.nextArrow)
)
.mousedown(function() { return false; })
.click(function(e) {
if(options.nextArrow != '' && showNext) {
e.stopPropagation();
setFirstDate(nextFirstDate);
}
});
// Add cells for prev/title/next
calendar
.append(prevCell)
.append(titleCell)
.append(nextCell);
// Add all the cells to the calendar
for(var row = 0, cellIndex = 0; row < maxRow + 1; row++) {
for(var col = 0; col < maxCol; col++, cellIndex++) {
var cellDate = new Date(startDate);
var cellClass = 'day';
var cellZIndex = options.zIndex + (cellIndex);
var cell = $('<div/>')
if(!row) {
cellClass = 'dow';
cell.html(dowNames[col]);
cellDate = null;
}
else {
// Get the new date for this cell
cellDate._add(col + ((row - 1) * maxCol));
// Get value for this date
var cellDateVal = cellDate._val();
var cellDateTime = cellDateVal.time;
// Variable to hold special data
var specialData = null;
// Determine if this date is selectable
var isSelectable = true;
// Helper function to get repeat friendly date against current date
var getRepeatDate = function(v, date) {
// If repeating, set the date's year and month accordingly
if(v.repeatYear === true) { date.setYear(cellDateVal.year); }
if(v.repeatMonth === true) { date.setMonth(cellDateVal.month); }
return date._val();
};
// Assign date for the cell
cell.html(cellDateVal.date);
// If we have selectable date ranges
if(options.selectableDateRange) {
isSelectable = false;
$.each(options.selectableDateRange, function(i, v) {
var dateFrom = v.from;
var dateTo = (v.to || null);
// If to is not specified, default to max days in the from month
dateTo = dateTo || new Date(v.from.getFullYear(), v.from.getMonth(), v.from._max());
// If repeating year, set the from and two to the current date's year
dateFrom = getRepeatDate(v, dateFrom);
dateTo = getRepeatDate(v, dateTo);
// Test to see if this date is selectable
if(cellDateTime >= dateFrom.time && cellDateTime <= dateTo.time) {
isSelectable = true;
return true;
}
});
}
// Handle date ranges and collections
if(options.selectableDates) {
if((options.selectableDateRange && !isSelectable) || (isSelectable && !options.selectableDateRange)) {
isSelectable = false;
}
$.each(options.selectableDates, function(i, v) {
var vDate = getRepeatDate(v, v.date);
if(vDate.time == cellDateTime) {
return (isSelectable = true);
}
});
}
// If not active or if not within selectableMonths, set to noday otherwise evaluate accordingly
if(!isSelectable ||
selectableYears._indexOf(cellDateVal.year) < 0 ||
selectableMonths._indexOf(cellDateVal.month) < 0 ||
selectableDOW._indexOf(cellDateVal.day) < 0) {
cellClass = 'noday';
}
else {
// Handle active dates and weekends
cellClass = ([ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ])[cellDateVal.day];
// Handle today or selected dates
if(firstDateMonth != cellDateVal.month) { cellClass += ' outday'; }
if(todayTime == cellDateTime) { cellClass = 'today'; cellZIndex += 50; }
if(options.selectedDate._time() == cellDateTime) { cellClass = 'selected'; cellZIndex += 51; }
// Handle special dates
if(options.specialDates) {
$.each(options.specialDates, function(i, v) {
var vDate = getRepeatDate(v, v.date);
if(vDate.time == cellDateTime) {
cellClass = (v.cssClass || 'special');
cellZIndex += 52;
specialData = v.data;
}
});
}
cell
.mousedown(function() { return false; })
.hover(function(e) {
e.stopPropagation();
// Get the data from this cell
var hoverData = $(this).data('data');
// Call callback
options.onHover(el, cell, hoverData.date, hoverData.data);
})
.click(function(e) {
e.stopPropagation();
// Get the data from this cell
var clickedData = $(this).data('data');
// Save date to selected and first
options.selectedDate = options.firstDate = clickedData.date;
// Update calendar (and auto-hide if necessary)
self.render(function() {
if(!options.showAlways && options.hideOnClick) {
self.hide();
}
});
// Call callback
options.onClick(el, $(this), clickedData.date, clickedData.data);
});
}
}
// Update the css for the cell
$.extend(cellCSS,
{
borderTopWidth: borderSize,
borderBottomWidth: borderSize,
borderLeftWidth: (row > 0 || (!row && !col)) ? borderSize : 0,
borderRightWidth: (row > 0 || (!row && col == 6)) ? borderSize : 0,
marginLeft: (col > 0) ? '-' + (borderSize) : 0,
marginTop: (row > 0) ? '-' + (borderSize) : 0,
zIndex: cellZIndex
});
// Assign other properties to the cell
cell
.data('data', { date: cellDate, data: specialData})
.addClass(coreClass + cellClass)
.css(cellCSS);
// Add cell to calendar
calendar.append(cell);
}
}
// Render the month / year title
// Helper function for toggling select and text
var toggleYearMonthSelect = function(showYear) {
var show = 'inline-block';
var hide = 'none';
if(options.allowMonthSelect) {
monthText.css({ display: !showYear ? hide : show });
monthSelect.css({ display: !showYear ? show : hide });
}
if(options.allowYearSelect) {
yearText.css({ display: showYear ? hide : show });
yearSelect.css({ display: showYear ? show : hide });
}
};
// Helper function when select is updated
var onYearMonthSelect = function() {
options.firstDate = new Date(yearSelect.val(), monthSelect.val(), 1);
self.render();
};
// Build month selector
var monthSelect = $('<select/>')
.hide()
.change(onYearMonthSelect);
// Build year selector
var yearSelect = $('<select/>')
.hide()
.change(onYearMonthSelect);
// Build month label
var monthText = $('<span/>')
.html(monthNames[firstDateMonth])
.mousedown(function() { return false; })
.click(function(e) {
e.stopPropagation();
toggleYearMonthSelect(false);
});
// Build year label
var yearText = $('<span/>')
.html(firstDateYear)
.mousedown(function() { return false; })
.click(function(e) {
e.stopPropagation();
toggleYearMonthSelect(true);
});
// Populate month select
$.each(monthNames, function(i, v) {
if(options.allowMonthSelect && selectableMonths._indexOf(i) != -1) {
var o = $('<option/>').html(v).attr('value', i);
if(i == firstDateMonth) { o.attr('selected', 'selected');}
monthSelect.append(o);
}
});
// Populate year select
$.each(selectableYears, function(i, v) {
if(options.allowYearSelect) {
var o = $('<option/>').html(v).attr('value', v);
if(v == firstDateYear) { o.attr('selected', 'selected'); }
yearSelect.append(o);
}
});
var titleYearMonth = $('<div/>')
.append(monthText)
.append(monthSelect)
.append(yearText)
.append(yearSelect);
// Add to title
titleCell.children().remove();
titleCell.append(titleYearMonth);
// Run the callback signaling end of the render
renderCalback = renderCalback || (function() {});
renderCalback();
}
};
// Return the plugin
return glDatePicker;
})();
// One time initialization of useful prototypes
(function() {
Date.prototype._clear = function() {
this.setHours(0);
this.setMinutes(0);
this.setSeconds(0);
this.setMilliseconds(0);
return this;
};
Date.prototype._time = function() {
return this._clear().getTime();
};
Date.prototype._max = function() {
var isLeapYear = (new Date(this.getYear(), 1, 29).getMonth() == 1) ? 1 : 0;
var days = [31, 28 + isLeapYear, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return days[this.getMonth()];
};
Date.prototype._add = function(days) {
this.setDate(this.getDate() + days);
};
Date.prototype._first = function() {
var date = new Date(this);
date.setDate(1);
return date;
};
Date.prototype._val = function() {
this._clear();
return {
year: this.getFullYear(),
month: this.getMonth(),
date: this.getDate(),
time: this.getTime(),
day: this.getDay()
};
};
Array.prototype._indexOf = function(value) {
return $.inArray(value, this);
}
})();
})();