/*! * 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 = $('
') .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 = $('') .addClass(monyearClass) .css( $.extend({}, cellCSS, { borderWidth: borderSize + ' 0 0 ' + borderSize }) ) .append( $('') .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 = $('') .addClass(monyearClass + 'title') .css( $.extend({}, cellCSS, { width: titleWidth + 'px', borderTopWidth: borderSize, marginLeft: '-' + (borderSize) }) ); var nextCell = $('') .addClass(monyearClass) .css( $.extend({}, cellCSS, { marginLeft: '-' + (borderSize), borderWidth: borderSize + ' ' + borderSize + ' 0 0' }) ) .append( $('') .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 = $('') 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 = $('') .hide() .change(onYearMonthSelect); // Build year selector var yearSelect = $('') .hide() .change(onYearMonthSelect); // Build month label var monthText = $('') .html(monthNames[firstDateMonth]) .mousedown(function() { return false; }) .click(function(e) { e.stopPropagation(); toggleYearMonthSelect(false); }); // Build year label var yearText = $('') .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 = $('').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 = $('').html(v).attr('value', v); if(v == firstDateYear) { o.attr('selected', 'selected'); } yearSelect.append(o); } }); var titleYearMonth = $('') .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); } })(); })();