prodio
Version:
Simplified project management
1,345 lines (1,155 loc) • 52.9 kB
JavaScript
/**
* Date selector
* @module Ink.UI.DatePicker_1
* @version 1
*/
Ink.createModule('Ink.UI.DatePicker', '1', ['Ink.UI.Common_1','Ink.Dom.Event_1','Ink.Dom.Css_1','Ink.Dom.Element_1','Ink.Dom.Selector_1','Ink.Util.Array_1','Ink.Util.Date_1', 'Ink.Dom.Browser_1'], function(Common, Event, Css, InkElement, Selector, InkArray, InkDate ) {
'use strict';
// Repeat a string. Long version of (new Array(n)).join(str);
function strRepeat(n, str) {
var ret = '';
for (var i = 0; i < n; i++) {
ret += str;
}
return ret;
}
// Clamp a number into a min/max limit
function clamp(n, min, max) {
if (n > max) { n = max; }
if (n < min) { n = min; }
return n;
}
function dateishFromYMDString(YMD) {
var split = YMD.split('-');
return dateishFromYMD(+split[0], +split[1] - 1, +split[2]);
}
function dateishFromYMD(year, month, day) {
return {_year: year, _month: month, _day: day};
}
function dateishFromDate(date) {
return {_year: date.getFullYear(), _month: date.getMonth(), _day: date.getDate()};
}
/**
* @class Ink.UI.DatePicker
* @constructor
* @version 1
*
* @param {String|DOMElement} selector
* @param {Object} [options] Options
* @param {Boolean} [options.autoOpen] Flag to automatically open the datepicker.
* @param {String} [options.cleanText] Text for the clean button. Defaults to 'Clear'.
* @param {String} [options.closeText] Text for the close button. Defaults to 'Close'.
* @param {String} [options.cssClass] CSS class to be applied on the datepicker
* @param {String|DOMElement} [options.pickerField] (if not using in an input[type="text"]) Element which displays the DatePicker when clicked. Defaults to an "open" link.
* @param {String} [options.dateRange] Enforce limits to year, month and day for the Date, ex: '1990-08-25:2020-11'
* @param {Boolean} [options.displayInSelect] Flag to display the component in a select element.
* @param {String|DOMElement} [options.dayField] (if using options.displayInSelect) `select` field with days.
* @param {String|DOMElement} [options.monthField] (if using options.displayInSelect) `select` field with months.
* @param {String|DOMElement} [options.yearField] (if using options.displayInSelect) `select` field with years.
* @param {String} [options.format] Date format string
* @param {String} [options.instance] Unique id for the datepicker
* @param {Object} [options.month] Hash of month names. Defaults to portuguese month names. January is 1.
* @param {String} [options.nextLinkText] Text for the previous button. Defaults to '«'.
* @param {String} [options.ofText] Text to show between month and year. Defaults to ' of '.
* @param {Boolean} [options.onFocus] If the datepicker should open when the target element is focused. Defaults to true.
* @param {Function} [options.onMonthSelected] Callback to execute when the month is selected.
* @param {Function} [options.onSetDate] Callback to execute when the date is set.
* @param {Function} [options.onYearSelected] Callback to execute when the year is selected.
* @param {String} [options.position] Position for the datepicker. Either 'right' or 'bottom'. Defaults to 'right'.
* @param {String} [options.prevLinkText] Text for the previous button. Defaults to '«'.
* @param {Boolean} [options.showClean] If the clean button should be visible. Defaults to true.
* @param {Boolean} [options.showClose] If the close button should be visible. Defaults to true.
* @param {Boolean} [options.shy] If the datepicker should start automatically. Defaults to true.
* @param {String} [options.startDate] Date to define initial month. Must be in yyyy-mm-dd format.
* @param {Number} [options.startWeekDay] First day of the week. Sunday is zero. Defaults to 1 (Monday).
* @param {Function} [options.validYearFn] Callback to execute when 'rendering' the month (in the month view)
* @param {Function} [options.validMonthFn] Callback to execute when 'rendering' the month (in the month view)
* @param {Function} [options.validDayFn] Callback to execute when 'rendering' the day (in the month view)
* @param {Function} [options.nextValidDateFn] Function to calculate the next valid date, given the current. Useful when there's invalid dates or time frames.
* @param {Function} [options.prevValidDateFn] Function to calculate the previous valid date, given the current. Useful when there's invalid dates or time frames.
* @param {Object} [options.wDay] Hash of week day names. Sunday is 0. Defaults to { 0:'Sunday', 1:'Monday', etc...
* @param {String} [options.yearRange] Enforce limits to year for the Date, ex: '1990:2020' (deprecated)
*
* @sample Ink_UI_DatePicker_1.html
*/
var DatePicker = function(selector, options) {
this._element = selector &&
Common.elOrSelector(selector, '[Ink.UI.DatePicker_1]: selector argument');
this._options = Common.options('Ink.UI.DatePicker_1', {
autoOpen: ['Boolean', false],
cleanText: ['String', 'Clear'],
closeText: ['String', 'Close'],
pickerField: ['Element', null],
containerElement:['Element', null],
cssClass: ['String', 'ink-calendar bottom'],
dateRange: ['String', null],
// use this in a <select>
displayInSelect: ['Boolean', false],
dayField: ['Element', null],
monthField: ['Element', null],
yearField: ['Element', null],
format: ['String', 'yyyy-mm-dd'],
instance: ['String', 'scdp_' + Math.round(99999 * Math.random())],
nextLinkText: ['String', '»'],
ofText: ['String', ' of '],
onFocus: ['Boolean', true],
onMonthSelected: ['Function', null],
onSetDate: ['Function', null],
onYearSelected: ['Function', null],
position: ['String', 'right'],
prevLinkText: ['String', '«'],
showClean: ['Boolean', true],
showClose: ['Boolean', true],
shy: ['Boolean', true],
startDate: ['String', null], // format yyyy-mm-dd,
startWeekDay: ['Number', 1],
// Validation
validDayFn: ['Function', null],
validMonthFn: ['Function', null],
validYearFn: ['Function', null],
nextValidDateFn: ['Function', null],
prevValidDateFn: ['Function', null],
yearRange: ['String', null],
// Text
month: ['Object', {
1:'January',
2:'February',
3:'March',
4:'April',
5:'May',
6:'June',
7:'July',
8:'August',
9:'September',
10:'October',
11:'November',
12:'December'
}],
wDay: ['Object', {
0:'Sunday',
1:'Monday',
2:'Tuesday',
3:'Wednesday',
4:'Thursday',
5:'Friday',
6:'Saturday'
}]
}, options || {}, this._element);
this._options.format = this._dateParsers[ this._options.format ] || this._options.format;
this._hoverPicker = false;
this._picker = this._options.pickerField || null;
this._setMinMax( this._options.dateRange || this._options.yearRange );
if(this._options.startDate) {
this.setDate( this._options.startDate );
} else if (this._element && this._element.value) {
this.setDate( this._element.value );
} else {
var today = new Date();
this._day = today.getDate( );
this._month = today.getMonth( );
this._year = today.getFullYear( );
}
if (this._options.startWeekDay < 0 || this._options.startWeekDay > 6) {
Ink.warn('Ink.UI.DatePicker_1: option "startWeekDay" must be between 0 (sunday) and 6 (saturday)');
this._options.startWeekDay = clamp(this._options.startWeekDay, 0, 6);
}
if(this._options.displayInSelect &&
!(this._options.dayField && this._options.monthField && this._options.yearField)){
throw new Error(
'Ink.UI.DatePicker: displayInSelect option enabled.'+
'Please specify dayField, monthField and yearField selectors.');
}
this._init();
};
DatePicker.prototype = {
version: '0.1',
/**
* Initialization function. Called by the constructor and receives the same parameters.
*
* @method _init
* @private
*/
_init: function(){
Ink.extendObj(this._options,this._lang || {});
this._render();
this._listenToContainerObjectEvents();
Common.registerInstance(this, this._containerObject, 'datePicker');
},
/**
* Renders the DatePicker's markup.
*
* @method _render
* @private
*/
_render: function() {
this._containerObject = document.createElement('div');
this._containerObject.id = this._options.instance;
this._containerObject.className = this._options.cssClass + ' ink-datepicker-calendar hide-all';
this._renderSuperTopBar();
var calendarTop = document.createElement("div");
calendarTop.className = 'ink-calendar-top';
this._monthDescContainer = document.createElement("div");
this._monthDescContainer.className = 'ink-calendar-month_desc';
this._monthPrev = document.createElement('div');
this._monthPrev.className = 'ink-calendar-prev';
this._monthPrev.innerHTML ='<a href="#prev" class="change_month_prev">' + this._options.prevLinkText + '</a>';
this._monthNext = document.createElement('div');
this._monthNext.className = 'ink-calendar-next';
this._monthNext.innerHTML ='<a href="#next" class="change_month_next">' + this._options.nextLinkText + '</a>';
calendarTop.appendChild(this._monthPrev);
calendarTop.appendChild(this._monthDescContainer);
calendarTop.appendChild(this._monthNext);
this._monthContainer = document.createElement("div");
this._monthContainer.className = 'ink-calendar-month';
this._containerObject.appendChild(calendarTop);
this._containerObject.appendChild(this._monthContainer);
this._monthSelector = this._renderMonthSelector();
this._containerObject.appendChild(this._monthSelector);
this._yearSelector = document.createElement('ul');
this._yearSelector.className = 'ink-calendar-year-selector';
this._containerObject.appendChild(this._yearSelector);
if(!this._options.onFocus || this._options.displayInSelect){
if(!this._options.pickerField){
this._picker = document.createElement('a');
this._picker.href = '#open_cal';
this._picker.innerHTML = 'open';
this._element.parentNode.appendChild(this._picker);
this._picker.className = 'ink-datepicker-picker-field';
} else {
this._picker = Common.elOrSelector(this._options.pickerField, 'pickerField');
}
}
this._appendDatePickerToDom();
this._renderMonth();
this._monthChanger = document.createElement('a');
this._monthChanger.href = '#monthchanger';
this._monthChanger.className = 'ink-calendar-link-month';
this._monthChanger.innerHTML = this._options.month[this._month + 1];
this._ofText = document.createElement('span');
this._ofText.innerHTML = this._options.ofText;
this._yearChanger = document.createElement('a');
this._yearChanger.href = '#yearchanger';
this._yearChanger.className = 'ink-calendar-link-year';
this._yearChanger.innerHTML = this._year;
this._monthDescContainer.innerHTML = '';
this._monthDescContainer.appendChild(this._monthChanger);
this._monthDescContainer.appendChild(this._ofText);
this._monthDescContainer.appendChild(this._yearChanger);
if (!this._options.inline) {
this._addOpenCloseEvents();
} else {
this.show();
}
this._addDateChangeHandlersToInputs();
},
_addDateChangeHandlersToInputs: function () {
var fields = this._element;
if (this._options.displayInSelect) {
fields = [
this._options.dayField,
this._options.monthField,
this._options.yearField];
}
Event.observeMulti(fields ,'change', Ink.bindEvent(function(){
this._updateDate( );
this._showDefaultView( );
this.setDate( );
if ( !this._inline && !this._hoverPicker ) {
this._hide(true);
}
},this));
},
/**
* Shows the calendar.
*
* @method show
**/
show: function () {
this._updateDate();
this._renderMonth();
Css.removeClassName(this._containerObject, 'hide-all');
},
_addOpenCloseEvents: function () {
var opener = this._picker || this._element;
Event.observe(opener, 'click', Ink.bindEvent(function(e){
Event.stop(e);
this.show();
},this));
if (this._options.autoOpen) {
this.show();
}
if(!this._options.displayInSelect){
Event.observe(opener, 'blur', Ink.bindEvent(function() {
if ( !this._hoverPicker ) {
this._hide(true);
}
},this));
}
if (this._options.shy) {
// Close the picker when clicking elsewhere.
Event.observe(document,'click',Ink.bindEvent(function(e){
var target = Event.element(e);
// "elsewhere" is outside any of these elements:
var cannotBe = [
this._options.dayField,
this._options.monthField,
this._options.yearField,
this._picker,
this._element
];
for (var i = 0, len = cannotBe.length; i < len; i++) {
if (cannotBe[i] && InkElement.descendantOf(cannotBe[i], target)) {
return;
}
}
this._hide(true);
},this));
}
},
/**
* Creates the markup of the view with months.
*
* @method _renderMonthSelector
* @private
*/
_renderMonthSelector: function () {
var selector = document.createElement('ul');
selector.className = 'ink-calendar-month-selector';
var ulSelector = document.createElement('ul');
for(var mon=1; mon<=12; mon++){
ulSelector.appendChild(this._renderMonthButton(mon));
if (mon % 4 === 0) {
selector.appendChild(ulSelector);
ulSelector = document.createElement('ul');
}
}
return selector;
},
/**
* Renders a single month button.
*/
_renderMonthButton: function (mon) {
var liMonth = document.createElement('li');
var aMonth = document.createElement('a');
aMonth.setAttribute('data-cal-month', mon);
aMonth.innerHTML = this._options.month[mon].substring(0,3);
liMonth.appendChild(aMonth);
return liMonth;
},
_appendDatePickerToDom: function () {
if(this._options.containerElement) {
var appendTarget =
Ink.i(this._options.containerElement) || // [2.3.0] maybe id; small backwards compatibility thing
Common.elOrSelector(this._options.containerElement);
appendTarget.appendChild(this._containerObject);
}
if (InkElement.findUpwardsBySelector(this._element, '.ink-form .control-group .control') === this._element.parentNode) {
// [3.0.0] Check if the <input> must be a direct child of .control, and if not, remove this block.
this._wrapper = this._element.parentNode;
this._wrapperIsControl = true;
} else {
this._wrapper = InkElement.create('div', { className: 'ink-datepicker-wrapper' });
InkElement.wrap(this._element, this._wrapper);
}
InkElement.insertAfter(this._containerObject, this._element);
},
/**
* Render the topmost bar with the "close" and "clear" buttons.
*/
_renderSuperTopBar: function () {
if((!this._options.showClose) || (!this._options.showClean)){ return; }
this._superTopBar = document.createElement("div");
this._superTopBar.className = 'ink-calendar-top-options';
if(this._options.showClean){
this._superTopBar.appendChild(InkElement.create('a', {
className: 'clean',
setHTML: this._options.cleanText
}));
}
if(this._options.showClose){
this._superTopBar.appendChild(InkElement.create('a', {
className: 'close',
setHTML: this._options.closeText
}));
}
this._containerObject.appendChild(this._superTopBar);
},
_listenToContainerObjectEvents: function () {
Event.observe(this._containerObject,'mouseover',Ink.bindEvent(function(e){
Event.stop( e );
this._hoverPicker = true;
},this));
Event.observe(this._containerObject,'mouseout',Ink.bindEvent(function(e){
Event.stop( e );
this._hoverPicker = false;
},this));
Event.observe(this._containerObject,'click',Ink.bindEvent(this._onClick, this));
},
_onClick: function(e){
var elem = Event.element(e);
if (Css.hasClassName(elem, 'ink-calendar-off')) {
Event.stopDefault(e);
return null;
}
Event.stop(e);
// Relative changers
this._onRelativeChangerClick(elem);
// Absolute changers
this._onAbsoluteChangerClick(elem);
// Mode changers
if (Css.hasClassName(elem, 'ink-calendar-link-month')) {
this._showMonthSelector();
} else if (Css.hasClassName(elem, 'ink-calendar-link-year')) {
this._showYearSelector();
} else if(Css.hasClassName(elem, 'clean')){
this._clean();
} else if(Css.hasClassName(elem, 'close')){
this._hide(false);
}
this._updateDescription();
},
/**
* Handles click events on a changer (« ») for next/prev year/month
* @method _onChangerClick
* @private
**/
_onRelativeChangerClick: function (elem) {
var changeYear = {
change_year_next: 1,
change_year_prev: -1
};
var changeMonth = {
change_month_next: 1,
change_month_prev: -1
};
if( elem.className in changeMonth ) {
this._updateCal(changeMonth[elem.className]);
} else if( elem.className in changeYear ) {
this._showYearSelector(changeYear[elem.className]);
}
},
/**
* Handles click events on an atom-changer (day button, month button, year button)
*
* @method _onAbsoluteChangerClick
* @private
*/
_onAbsoluteChangerClick: function (elem) {
var elemData = InkElement.data(elem);
if( Number(elemData.calDay) ){
this.setDate( [this._year, this._month + 1, elemData.calDay].join('-') );
this._hide();
} else if( Number(elemData.calMonth) ) {
this._month = Number(elemData.calMonth) - 1;
this._showDefaultView();
this._updateCal();
} else if( Number(elemData.calYear) ){
this._changeYear(Number(elemData.calYear));
}
},
_changeYear: function (year) {
year = +year;
if(year){
this._year = year;
if( typeof this._options.onYearSelected === 'function' ){
this._options.onYearSelected(this, {
'year': this._year
});
}
this._showMonthSelector();
}
},
_clean: function () {
if(this._options.displayInSelect){
this._options.yearField.selectedIndex = 0;
this._options.monthField.selectedIndex = 0;
this._options.dayField.selectedIndex = 0;
} else {
this._element.value = '';
}
},
/**
* Hides the DatePicker.
* If the component is shy (options.shy), behaves differently.
*
* @method _hide
* @param {Boolean} [blur] If false, forces hiding even if the component is shy.
*/
_hide: function(blur) {
blur = blur === undefined ? true : blur;
if (blur === false || (blur && this._options.shy)) {
Css.addClassName(this._containerObject, 'hide-all');
}
},
/**
* Sets the range of dates allowed to be selected in the Date Picker
*
* @method _setMinMax
* @param {String} dateRange Two dates separated by a ':'. Example: 2013-01-01:2013-12-12
* @private
*/
_setMinMax: function( dateRange ) {
var self = this;
var noMinLimit = {
_year: -Number.MAX_VALUE,
_month: 0,
_day: 1
};
var noMaxLimit = {
_year: Number.MAX_VALUE,
_month: 11,
_day: 31
};
function noLimits() {
self._min = noMinLimit;
self._max = noMaxLimit;
}
if (!dateRange) { return noLimits(); }
var dates = dateRange.split( ':' );
var rDate = /^(\d{4})((\-)(\d{1,2})((\-)(\d{1,2}))?)?$/;
InkArray.each([
{name: '_min', date: dates[0], noLim: noMinLimit},
{name: '_max', date: dates[1], noLim: noMaxLimit}
], Ink.bind(function (data) {
var lim = data.noLim;
if ( data.date.toUpperCase() === 'NOW' ) {
var now = new Date();
lim = dateishFromDate(now);
} else if (data.date.toUpperCase() === 'EVER') {
lim = data.noLim;
} else if ( rDate.test( data.date ) ) {
lim = dateishFromYMDString(data.date);
lim._month = clamp(lim._month, 0, 11);
lim._day = clamp(lim._day, 1, this._daysInMonth( lim._year, lim._month + 1 ));
}
this[data.name] = lim;
}, this));
// Should be equal, or min should be smaller
var valid = this._dateCmp(this._max, this._min) !== -1;
if (!valid) {
noLimits();
}
},
/**
* Checks if a date is between the valid range.
* Starts by checking if the date passed is valid. If not, will fallback to the 'today' date.
* Then checks if the all params are inside of the date range specified. If not, it will fallback to the nearest valid date (either Min or Max).
*
* @method _fitDateToRange
* @param {Number} year Year with 4 digits (yyyy)
* @param {Number} month Month
* @param {Number} day Day
* @return {Array} Array with the final processed date.
* @private
*/
_fitDateToRange: function( date ) {
if ( !this._isValidDate( date ) ) {
date = dateishFromDate(new Date());
}
if (this._dateCmp(date, this._min) === -1) {
return Ink.extendObj({}, this._min);
} else if (this._dateCmp(date, this._max) === 1) {
return Ink.extendObj({}, this._max);
}
return Ink.extendObj({}, date); // date is okay already, just copy it.
},
/**
* Checks whether a date is within the valid date range
* @method _dateWithinRange
* @param year
* @param month
* @param day
* @return {Boolean}
* @private
*/
_dateWithinRange: function (date) {
if (!arguments.length) {
date = this;
}
return (!this._dateAboveMax(date) &&
(!this._dateBelowMin(date)));
},
_dateAboveMax: function (date) {
return this._dateCmp(date, this._max) === 1;
},
_dateBelowMin: function (date) {
return this._dateCmp(date, this._min) === -1;
},
_dateCmp: function (self, oth) {
return this._dateCmpUntil(self, oth, '_day');
},
/**
* _dateCmp with varied precision. You can compare down to the day field, or, just to the month.
* // the following two dates are considered equal because we asked
* // _dateCmpUntil to just check up to the years.
*
* _dateCmpUntil({_year: 2000, _month: 10}, {_year: 2000, _month: 11}, '_year') === 0
*/
_dateCmpUntil: function (self, oth, depth) {
var props = ['_year', '_month', '_day'];
var i = -1;
do {
i++;
if (self[props[i]] > oth[props[i]]) { return 1; }
else if (self[props[i]] < oth[props[i]]) { return -1; }
} while (props[i] !== depth &&
self[props[i + 1]] !== undefined && oth[props[i + 1]] !== undefined);
return 0;
},
/**
* Sets the markup in the default view mode (showing the days).
* Also disables the previous and next buttons in case they don't meet the range requirements.
*
* @method _showDefaultView
* @private
*/
_showDefaultView: function(){
this._yearSelector.style.display = 'none';
this._monthSelector.style.display = 'none';
this._monthPrev.childNodes[0].className = 'change_month_prev';
this._monthNext.childNodes[0].className = 'change_month_next';
if ( !this._getPrevMonth() ) {
this._monthPrev.childNodes[0].className = 'action_inactive';
}
if ( !this._getNextMonth() ) {
this._monthNext.childNodes[0].className = 'action_inactive';
}
this._monthContainer.style.display = 'block';
},
/**
* Updates the date shown on the datepicker
*
* @method _updateDate
* @private
*/
_updateDate: function(){
var dataParsed;
if(!this._options.displayInSelect && this._element.value){
dataParsed = this._parseDate(this._element.value);
} else if (this._options.displayInSelect) {
dataParsed = {
_year: this._options.yearField[this._options.yearField.selectedIndex].value,
_month: this._options.monthField[this._options.monthField.selectedIndex].value - 1,
_day: this._options.dayField[this._options.dayField.selectedIndex].value
};
}
if (dataParsed) {
dataParsed = this._fitDateToRange(dataParsed);
this._year = dataParsed._year;
this._month = dataParsed._month;
this._day = dataParsed._day;
}
this.setDate();
this._updateDescription();
this._renderMonth();
},
/**
* Updates the date description shown at the top of the datepicker
*
* EG "12 de November"
*
* @method _updateDescription
* @private
*/
_updateDescription: function(){
this._monthChanger.innerHTML = this._options.month[ this._month + 1 ];
this._ofText.innerHTML = this._options.ofText;
this._yearChanger.innerHTML = this._year;
},
/**
* Renders the year selector view of the datepicker
*
* @method _showYearSelector
* @private
*/
_showYearSelector: function(inc){
this._incrementViewingYear(inc);
var firstYear = this._year - (this._year % 10);
var thisYear = firstYear - 1;
var str = "<li><ul>";
if (thisYear > this._min._year) {
str += '<li><a href="#year_prev" class="change_year_prev">' + this._options.prevLinkText + '</a></li>';
} else {
str += '<li> </li>';
}
for (var i=1; i < 11; i++){
if (i % 4 === 0){
str+='</ul><ul>';
}
thisYear = firstYear + i - 1;
str += this._getYearButtonHtml(thisYear);
}
if( thisYear < this._max._year){
str += '<li><a href="#year_next" class="change_year_next">' + this._options.nextLinkText + '</a></li>';
} else {
str += '<li> </li>';
}
str += "</ul></li>";
this._yearSelector.innerHTML = str;
this._monthPrev.childNodes[0].className = 'action_inactive';
this._monthNext.childNodes[0].className = 'action_inactive';
this._monthSelector.style.display = 'none';
this._monthContainer.style.display = 'none';
this._yearSelector.style.display = 'block';
},
/**
* For the year selector.
*
* Update this._year, to find the next decade or use nextValidDateFn to find it.
*/
_incrementViewingYear: function (inc) {
if (!inc) { return; }
var year = +this._year + inc*10;
year = year - year % 10;
if ( year > this._max._year || year + 9 < this._min._year){
return;
}
this._year = +this._year + inc*10;
},
_getYearButtonHtml: function (thisYear) {
if ( this._acceptableYear({_year: thisYear}) ){
var className = (thisYear === this._year) ? ' class="ink-calendar-on"' : '';
return '<li><a href="#" data-cal-year="' + thisYear + '"' + className + '>' + thisYear +'</a></li>';
} else {
return '<li><a href="#" class="ink-calendar-off">' + thisYear +'</a></li>';
}
},
/**
* Show the month selector (happens when you click a year, or the "month" link.
* @method _showMonthSelector
* @private
*/
_showMonthSelector: function () {
this._yearSelector.style.display = 'none';
this._monthContainer.style.display = 'none';
this._monthPrev.childNodes[0].className = 'action_inactive';
this._monthNext.childNodes[0].className = 'action_inactive';
this._addMonthClassNames();
this._monthSelector.style.display = 'block';
},
/**
* This function returns the given date in the dateish format
*
* @method _parseDate
* @param {String} dateStr A date on a string.
* @private
*/
_parseDate: function(dateStr){
var date = InkDate.set( this._options.format , dateStr );
if (date) {
return dateishFromDate(date);
}
return null;
},
/**
* Checks if a date is valid
*
* @method _isValidDate
* @param {Dateish} date
* @private
* @return {Boolean} True if the date is valid, false otherwise
*/
_isValidDate: function(date){
var yearRegExp = /^\d{4}$/;
var validOneOrTwo = /^\d{1,2}$/;
return (
yearRegExp.test(date._year) &&
validOneOrTwo.test(date._month) &&
validOneOrTwo.test(date._day) &&
+date._month + 1 >= 1 &&
+date._month + 1 <= 12 &&
+date._day >= 1 &&
+date._day <= this._daysInMonth(date._year, date._month + 1)
);
},
/**
* Checks if a given date is an valid format.
*
* @method _isDate
* @param {String} format A date format.
* @param {String} dateStr A date on a string.
* @private
* @return {Boolean} True if the given date is valid according to the given format
*/
_isDate: function(format, dateStr){
try {
if (typeof format === 'undefined'){
return false;
}
var date = InkDate.set( format , dateStr );
if( date && this._isValidDate( dateishFromDate(date) )) {
return true;
}
} catch (ex) {}
return false;
},
_acceptableDay: function (date) {
return this._acceptableDateComponent(date, 'validDayFn');
},
_acceptableMonth: function (date) {
return this._acceptableDateComponent(date, 'validMonthFn');
},
_acceptableYear: function (date) {
return this._acceptableDateComponent(date, 'validYearFn');
},
/** DRY base for the above 2 functions */
_acceptableDateComponent: function (date, userCb) {
if (this._options[userCb]) {
return this._callUserCallbackBool(this._options[userCb], date);
} else {
return this._dateWithinRange(date);
}
},
/**
* This method returns the date written with the format specified on the options
*
* @method _writeDateInFormat
* @private
* @return {String} Returns the current date of the object in the specified format
*/
_writeDateInFormat:function(){
return InkDate.get( this._options.format , this.getDate());
},
/**
* This method allows the user to set the DatePicker's date on run-time.
*
* @method setDate
* @param {String} dateString A date string in yyyy-mm-dd format.
* @public
*/
setDate: function( dateString ) {
if ( /\d{4}-\d{1,2}-\d{1,2}/.test( dateString ) ) {
var auxDate = dateString.split( '-' );
this._year = +auxDate[ 0 ];
this._month = +auxDate[ 1 ] - 1;
this._day = +auxDate[ 2 ];
}
this._setDate( );
},
/**
* Gets the currently selected date as a JavaScript date.
*
* @method getDate
*/
getDate: function () {
if (!this._day) {
throw 'Ink.UI.DatePicker: Still picking a date. Cannot getDate now!';
}
return new Date(this._year, this._month, this._day);
},
/**
* Sets the chosen date on the target input field
*
* @method _setDate
* @param {DOMElement} objClicked Clicked object inside the DatePicker's calendar.
* @private
*/
_setDate : function( objClicked ) {
if (objClicked) {
var data = InkElement.data(objClicked);
this._day = (+data.calDay) || this._day;
}
var dt = this._fitDateToRange(this);
this._year = dt._year;
this._month = dt._month;
this._day = dt._day;
if(!this._options.displayInSelect){
this._element.value = this._writeDateInFormat();
} else {
this._options.dayField.value = this._day;
this._options.monthField.value = this._month + 1;
this._options.yearField.value = this._year;
}
if(this._options.onSetDate) {
this._options.onSetDate( this , { date : this.getDate() } );
}
},
/**
* Makes the necessary work to update the calendar
* when choosing a different month
*
* @method _updateCal
* @param {Number} inc Indicates previous or next month
* @private
*/
_updateCal: function(inc){
if( typeof this._options.onMonthSelected === 'function' ){
this._options.onMonthSelected(this, {
'year': this._year,
'month' : this._month
});
}
if (inc && this._updateMonth(inc) === null) {
return;
}
this._renderMonth();
},
/**
* Function that returns the number of days on a given month on a given year
*
* @method _daysInMonth
* @param {Number} _y - year
* @param {Number} _m - month
* @private
* @return {Number} The number of days on a given month on a given year
*/
_daysInMonth: function(_y,_m){
var exceptions = {
2: ((_y % 400 === 0) || (_y % 4 === 0 && _y % 100 !== 0)) ? 29 : 28,
4: 30,
6: 30,
9: 30,
11: 30
};
return exceptions[_m] || 31;
},
/**
* Updates the calendar when a different month is chosen
*
* @method _updateMonth
* @param {Number} incValue - indicates previous or next month
* @private
*/
_updateMonth: function(incValue){
var date;
if (incValue > 0) {
date = this._getNextMonth();
} else if (incValue < 0) {
date = this._getPrevMonth();
}
if (!date) { return null; }
this._year = date._year;
this._month = date._month;
this._day = date._day;
},
/**
* Get the next month we can show.
*/
_getNextMonth: function (date) {
return this._tryLeap( date, 'Month', 'next', function (d) {
d._month += 1;
if (d._month > 11) {
d._month = 0;
d._year += 1;
}
return d;
});
},
/**
* Get the previous month we can show.
*/
_getPrevMonth: function (date) {
return this._tryLeap( date, 'Month', 'prev', function (d) {
d._month -= 1;
if (d._month < 0) {
d._month = 11;
d._year -= 1;
}
return d;
});
},
/**
* Get the next year we can show.
*/
_getPrevYear: function (date) {
return this._tryLeap( date, 'Year', 'prev', function (d) {
d._year -= 1;
return d;
});
},
/**
* Get the next year we can show.
*/
_getNextYear: function (date) {
return this._tryLeap( date, 'Year', 'next', function (d) {
d._year += 1;
return d;
});
},
/**
* DRY base for a function which tries to get the next or previous valid year or month.
*
* It checks if we can go forward by using _dateCmp with atomic
* precision (this means, {_year} for leaping years, and
* {_year, month} for leaping months), then it tries to get the
* result from the user-supplied callback (nextDateFn or prevDateFn),
* and when this is not present, advance the date forward using the
* `advancer` callback.
*/
_tryLeap: function (date, atomName, directionName, advancer) {
date = date || { _year: this._year, _month: this._month, _day: this._day };
var maxOrMin = directionName === 'prev' ? '_min' : '_max';
var boundary = this[maxOrMin];
// Check if we're by the boundary of min/max year/month
if (this._dateCmpUntil(date, boundary, atomName) === 0) {
return null; // We're already at the boundary. Bail.
}
var leapUserCb = this._options[directionName + 'ValidDateFn'];
if (leapUserCb) {
return this._callUserCallbackDate(leapUserCb, date);
} else {
date = advancer(date);
}
date = this._fitDateToRange(date);
return this['_acceptable' + atomName](date) ? date : null;
},
_getNextDecade: function (date) {
date = date || { _year: this._year, _month: this._month, _day: this._day };
var decade = this._getCurrentDecade(date);
if (decade + 10 > this._max._year) { return null; }
return decade + 10;
},
_getPrevDecade: function (date) {
date = date || { _year: this._year, _month: this._month, _day: this._day };
var decade = this._getCurrentDecade(date);
if (decade - 10 < this._min._year) { return null; }
return decade - 10;
},
/** Returns the decade given a date or year*/
_getCurrentDecade: function (year) {
year = year ? (year._year || year) : this._year;
return Math.floor(year / 10) * 10; // Round to first place
},
_callUserCallbackBase: function (cb, date) {
return cb.call(this, date._year, date._month + 1, date._day);
},
_callUserCallbackBool: function (cb, date) {
return !!this._callUserCallbackBase(cb, date);
},
_callUserCallbackDate: function (cb, date) {
var ret = this._callUserCallbackBase(cb, date);
return ret ? dateishFromDate(ret) : null;
},
/**
* Key-value object that (for a given key) points to the correct parsing format for the DatePicker
* @property _dateParsers
* @type {Object}
* @readOnly
*/
_dateParsers: {
'yyyy-mm-dd' : 'Y-m-d' ,
'yyyy/mm/dd' : 'Y/m/d' ,
'yy-mm-dd' : 'y-m-d' ,
'yy/mm/dd' : 'y/m/d' ,
'dd-mm-yyyy' : 'd-m-Y' ,
'dd/mm/yyyy' : 'd/m/Y' ,
'dd-mm-yy' : 'd-m-y' ,
'dd/mm/yy' : 'd/m/y' ,
'mm/dd/yyyy' : 'm/d/Y' ,
'mm-dd-yyyy' : 'm-d-Y'
},
/**
* Renders the current month
*
* @method _renderMonth
* @private
*/
_renderMonth: function(){
var month = this._month;
var year = this._year;
this._showDefaultView();
var html = '';
html += this._getMonthCalendarHeaderHtml(this._options.startWeekDay);
var counter = 0;
html+='<ul>';
var emptyHtml = '<li class="ink-calendar-empty"> </li>';
var firstDayIndex = this._getFirstDayIndex(year, month);
// Add padding if the first day of the month is not monday.
if(firstDayIndex > 0) {
counter += firstDayIndex;
html += strRepeat(firstDayIndex, emptyHtml);
}
html += this._getDayButtonsHtml(year, month);
html += '</ul>';
this._monthContainer.innerHTML = html;
},
/**
* Figure out where the first day of a month lies
* in the first row of the calendar.
*
* having options.startWeekDay === 0
*
* Su Mo Tu We Th Fr Sa
* 1 <- The "1" is in the 7th day. return 6.
* 2 3 4 5 6 7 8
* 9 10 11 12 13 14 15
* 16 17 18 19 20 21 22
* 23 24 25 26 27 28 29
* 30 31
*
* This obviously changes according to the user option "startWeekDay"
**/
_getFirstDayIndex: function (year, month) {
var wDayFirst = (new Date( year , month , 1 )).getDay(); // Sunday=0
var startWeekDay = this._options.startWeekDay || 0; // Sunday=0
var result = wDayFirst - startWeekDay;
result %= 7;
if (result < 0) {
result += 6;
}
return result;
},
_getDayButtonsHtml: function (year, month) {
var counter = this._getFirstDayIndex(year, month);
var daysInMonth = this._daysInMonth(year, month + 1);
var ret = '';
for (var day = 1; day <= daysInMonth; day++) {
if (counter === 7){ // new week
counter=0;
ret += '<ul>';
}
ret += this._getDayButtonHtml(year, month, day);
counter++;
if(counter === 7){
ret += '</ul>';
}
}
return ret;
},
/**
* Get the HTML markup for a single day in month view, given year, month, day.
*
* @method _getDayButtonHtml
* @private
*/
_getDayButtonHtml: function (year, month, day) {
var attrs = ' ';
var date = dateishFromYMD(year, month, day);
if (!this._acceptableDay(date)) {
attrs += 'class="ink-calendar-off"';
} else {
attrs += 'data-cal-day="' + day + '"';
}
if (this._day && this._dateCmp(date, this) === 0) {
attrs += 'class="ink-calendar-on" data-cal-day="' + day + '"';
}
return '<li><a href="#" ' + attrs + '>' + day + '</a></li>';
},
/** Write the top bar of the calendar (M T W T F S S) */
_getMonthCalendarHeaderHtml: function (startWeekDay) {
var ret = '<ul class="ink-calendar-header">';
var wDay;
for(var i=0; i<7; i++){
wDay = (startWeekDay + i) % 7;
ret += '<li>' +
this._options.wDay[wDay].substring(0,1) +
'</li>';
}
return ret + '</ul>';
},
/**
* This method adds class names to month buttons, to visually distinguish.
*
* @method _addMonthClassNames
* @param {DOMElement} parent DOMElement where all the months are.
* @private
*/
_addMonthClassNames: function(parent){
InkArray.forEach(
(parent || this._monthSelector).getElementsByTagName('a'),
Ink.bindMethod(this, '_addMonthButtonClassNames'));
},
/**
* Add the ink-calendar-on className if the given button is the current month,
* otherwise add the ink-calendar-off className if the given button refers to
* an unac