/*!
* jQuery UI Datepicker @VERSION
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Datepicker
//>>group: Widgets
//>>description: Displays a calendar from an input or inline for selecting dates.
//>>docs: http://api.jqueryui.com/datepicker/
//>>demos: http://jqueryui.com/datepicker/
(function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define([
"jquery",
"./core",
"./widget",
"./button",
"./position"
], factory );
} else {
// Browser globals
factory( jQuery );
}
}(function( $ ) {
// TODO use uniqueId, if possible
var idIncrement = 0,
// TODO move this to the instance
suppressExpandOnFocus = false;
$.widget( "ui.datepicker", {
options: {
appendTo: null,
dateFormat: null,
// TODO review
eachDay: $.noop,
numberOfMonths: 1,
position: {
my: "left top",
at: "left bottom"
},
showWeek: false,
show: true,
hide: true,
// callbacks
beforeOpen: null,
close: null,
open: null,
select: null
},
_create: function() {
this.options.dateFormat = this.options.dateFormat || { date: "short" };
this.date = $.date( null, this.options.dateFormat );
this.date.eachDay = this.options.eachDay;
this.id = "ui-datepicker-" + idIncrement;
idIncrement++;
if ( this.element.is( "input" ) ) {
this._createPicker();
} else {
this.inline = true;
this.picker = this.element;
}
this._on( this.picker, {
"click .ui-datepicker-prev": function( event ) {
event.preventDefault();
this.date.adjust( "M", -this.options.numberOfMonths );
this.refresh();
},
"click .ui-datepicker-next": function( event ) {
event.preventDefault();
this.date.adjust( "M", this.options.numberOfMonths );
this.refresh();
},
"click .ui-datepicker-current": function( event ) {
event.preventDefault();
this.select( event, new Date().getTime() );
},
"click .ui-datepicker-close": function( event ) {
event.preventDefault();
this.close( event );
},
"mousedown .ui-datepicker-calendar a": function( event ) {
event.preventDefault();
// TODO exclude clicks on lead days or handle them correctly
// TODO store/read more then just date, also required for multi month picker
this.select( event, $( event.currentTarget ).data( "timestamp" ) );
if ( this.inline ) {
this.grid.focus();
}
},
"keydown .ui-datepicker-calendar": "_handleKeydown"
});
// TODO use hoverable (no delegation support)? convert to _on?
this.picker.delegate( ".ui-datepicker-header a, .ui-datepicker-calendar a", "mouseenter.datepicker mouseleave.datepicker", function() {
$( this ).toggleClass( "ui-state-hover" );
});
this._createTmpl();
},
_handleKeydown: function( event ) {
if ( jQuery.inArray( event.keyCode, [ 13, 33, 34, 35, 36, 37, 38, 39, 40 ] ) === -1 ) {
//only interested navigation keys
return;
}
event.preventDefault();
var newId, newCell,
activeCell = $( "#" + this.grid.attr( "aria-activedescendant" ) ),
oldMonth = this.date.month(),
oldYear = this.date.year();
// TODO: Handle for pickers with multiple months
switch ( event.keyCode ) {
case $.ui.keyCode.ENTER:
activeCell.children( "a:first" ).mousedown();
return;
case $.ui.keyCode.PAGE_UP:
this.date.adjust( event.altKey ? "Y" : "M", 1 );
break;
case $.ui.keyCode.PAGE_DOWN:
this.date.adjust( event.altKey ? "Y" : "M", -1 );
break;
case $.ui.keyCode.END:
this.date.setDay( this.date.daysInMonth() );
break;
case $.ui.keyCode.HOME:
this.date.setDay( 1 );
break;
case $.ui.keyCode.LEFT:
this.date.adjust( "D", -1 );
break;
case $.ui.keyCode.UP:
this.date.adjust( "D", -7 );
break;
case $.ui.keyCode.RIGHT:
this.date.adjust( "D", 1 );
break;
case $.ui.keyCode.DOWN:
this.date.adjust( "D", 7 );
break;
default:
return;
}
if ( this.date.month() !== oldMonth || this.date.year() !== oldYear ) {
this.refresh();
this.grid.focus();
} else {
newId = this.id + "-" + this.date.day();
newCell = $( "#" + newId );
// TODO unnecessary optimization? is it really needed?
if ( !newCell.length ) {
return;
}
this.grid.attr("aria-activedescendant", newId);
this.grid.find( ".ui-state-focus" ).removeClass( "ui-state-focus" );
newCell.children( "a" ).addClass( "ui-state-focus" );
}
},
_createPicker: function() {
this.picker = $( "
" )
.addClass( "ui-front" )
// TODO add a ui-datepicker-popup class, move position:absolte to that
.css( "position", "absolute" )
.uniqueId()
.hide();
this._setHiddenPicker();
this.picker.appendTo( this._appendTo() );
this.element
.attr( "aria-haspopup", "true" )
.attr( "aria-owns", this.picker.attr( "id" ) );
this._on({
keydown: function( event ) {
switch ( event.keyCode ) {
case $.ui.keyCode.TAB:
// Waiting for close() will make popup hide too late, which breaks tab key behavior
this.picker.hide();
this.close( event );
break;
case $.ui.keyCode.ESCAPE:
if ( this.isOpen ) {
this.close( event );
}
break;
case $.ui.keyCode.ENTER:
this._handleKeydown( event );
break;
case $.ui.keyCode.DOWN:
case $.ui.keyCode.UP:
clearTimeout( this.closeTimer );
this._delay(function() {
this.open( event );
this.grid.focus();
}, 1);
break;
case $.ui.keyCode.HOME:
if ( event.ctrlKey ) {
this.date.setTime( new Date() );
event.preventDefault();
if ( this.isOpen ) {
this.refresh();
} else {
this.open( event );
}
}
break;
case $.ui.keyCode.END:
if ( event.ctrlKey ) {
this.element.val( "" );
event.preventDefault();
if ( this.isOpen ) {
this.close( event );
}
}
break;
}
},
mousedown: function( event ) {
if (this.isOpen) {
suppressExpandOnFocus = true;
this.close();
return;
}
this.open( event );
clearTimeout( this.closeTimer );
},
focus: function( event ) {
if ( !suppressExpandOnFocus ) {
this._delay( function() {
if ( !this.isOpen ) {
this.open( event );
}
}, 1);
}
this._delay( function() {
suppressExpandOnFocus = false;
}, 100);
},
blur: function() {
suppressExpandOnFocus = false;
}
});
this._on( this.picker, {
focusout: function( event ) {
// use a timer to allow click to clear it and letting that
// handle the closing instead of opening again
// also allows tabbing inside the calendar without it closing
this.closeTimer = this._delay( function() {
this.close( event );
}, 150);
},
focusin: function() {
clearTimeout( this.closeTimer );
},
mouseup: function() {
clearTimeout( this.closeTimer );
},
// TODO on TAB (or shift TAB), make sure it ends up on something useful in DOM order
keyup: function( event ) {
if ( event.keyCode === $.ui.keyCode.ESCAPE && this.picker.is( ":visible" ) ) {
this.close( event );
this._focusTrigger();
}
}
});
this._on( this.document, {
click: function( event ) {
if ( this.isOpen && !$( event.target ).closest( this.element.add( this.picker ) ).length ) {
this.close( event );
}
}
});
},
_appendTo: function() {
var element = this.options.appendTo;
if ( element ) {
element = element.jquery || element.nodeType ?
$( element ) :
this.document.find( element ).eq( 0 );
}
if ( !element ) {
element = this.element.closest( ".ui-front" );
}
if ( !element.length ) {
element = this.document[0].body;
}
return element;
},
_createTmpl: function() {
this._createDatepicker();
this.picker.find( "button" ).button();
if ( this.inline ) {
this.picker.children().addClass( "ui-datepicker-inline" );
}
// against display:none in datepicker.css
this.picker.find( ".ui-datepicker" ).css( "display", "block" );
this.grid = this.picker.find( ".ui-datepicker-calendar" );
},
_createDatepicker: function() {
var multiClasses = [],
pickerHtml = "";
if (this.options.numberOfMonths === 1 ) {
pickerHtml = this._buildHeader() + this._buildGrid() + this._buildButtons();
} else {
pickerHtml = this._buildMultiplePicker();
multiClasses.push( "ui-datepicker-multi" );
multiClasses.push( "ui-datepicker-multi-" + this.options.numberOfMonths );
}
$( "
" )
.addClass( "ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all" )
.addClass( multiClasses.join( " " ) )
.attr({
role: "region",
"aria-labelledby": this.id + "-title"
})
.html( pickerHtml )
.appendTo( this.picker );
},
_buildMultiplePicker: function() {
var headerClass,
html = "",
currentDate = this.date,
months = this.date.months( this.options.numberOfMonths - 1 ),
i = 0;
for ( i; i < months.length; i++ ) {
// TODO Shouldn't we pass date as a parameter to build* fns instead of setting this.date?
this.date = months[ i ];
headerClass = months[ i ].first ? "ui-corner-left" :
months[ i ].last ? "ui-corner-right" : "";
html += "
" +
"
";
if ( months[i].first ) {
html += this._buildPreviousLink();
}
if ( months[i].last ) {
html += this._buildNextLink();
}
html += this._buildTitlebar();
html += "
";
html += this._buildGrid();
html += "
";
}
html += "";
html += this._buildButtons();
this.date = currentDate;
return html;
},
_buildHeader: function() {
return "
";
},
_buildGridBody: function() {
// this.date.days() is not cached, and it has O(n^2) complexity. It is run O(n) times. So, it equals O(n^3). Not good at all. Caching.
var days = this.date.days(),
i = 0,
rows = "";
for ( i; i < days.length; i++ ) {
rows += this._buildWeekRow( days[i] );
}
return "" + rows + "";
},
_buildWeekRow: function( week ) {
var cells = "",
i = 0;
if ( this.options.showWeek ) {
cells += "
" + week.number + "
";
}
for ( i; i < week.days.length; i++ ) {
cells += this._buildDayCell( week.days[i] );
}
return "