/*!
* 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.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.DOWN:
case $.ui.keyCode.UP:
clearTimeout( this.closeTimer );
this._delay(function() {
this.open( event );
this.grid.focus();
}, 1);
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.date.refresh();
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++ ) {
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 "
";
},
_buildDayLink: function( day ) {
var link,
classes = [ "ui-state-default" ],
labels = Globalize.localize( "datepicker" );
if ( day.date === this.date.day() ) {
classes.push( "ui-state-focus" );
}
if ( day.current ) {
classes.push( "ui-state-active" );
}
if ( day.today ) {
classes.push( "ui-state-highlight" );
}
if ( day.extraClasses ) {
classes.push( day.extraClasses.split( " " ) );
}
link = "" +
day.date + "";
if ( day.today ) {
link += ", " + labels.currentText + "";
}
return link;
},
_buildDayDisplay: function( day ) {
var classes = [];
if ( day.current ) {
classes.push( "ui-state-active" );
}
if ( day.today ) {
classes.push( "ui-state-highlight" );
}
if ( day.extraClasses ) {
classes.push( day.extraClasses.split( " " ) );
}
return "" +
day.date + "";
},
_buildButtons: function() {
var labels = Globalize.localize( "datepicker" );
return "
" +
"" +
"" +
"
";
},
_focusTrigger: function() {
suppressExpandOnFocus = true;
this.element.focus();
},
// Refreshing the entire datepicker during interaction confuses screen readers, specifically
// because the grid heading is marked up as a live region and will often not update if it's
// destroyed and recreated instead of just having its text change. Additionally, interacting
// with the prev and next links would cause loss of focus issues because the links being
// interacted with will disappear while focused.
refresh: function() {
//determine which day gridcell to focus after refresh
//TODO: Prevent disabled cells from being focused
this.date.refresh();
if ( this.options.numberOfMonths === 1 ) {
this.grid = $( this._buildGrid() );
$( ".ui-datepicker-title", this.picker ).html( this._buildTitle() );
$( ".ui-datepicker-calendar", this.picker ).replaceWith( this.grid );
} else {
this._refreshMultiplePicker();
}
},
_refreshMultiplePicker: function() {
for ( var i = 0; i < this.options.numberOfMonths; i++ ) {
$( ".ui-datepicker-title", this.picker ).eq( i ).html( this._buildTitle() );
$( ".ui-datepicker-calendar", this.picker ).eq( i ).html( this._buildGrid() );
this.date.adjust( "M", 1 );
}
this.date.adjust( "M", -this.options.numberOfMonths );
// TODO: This assumes focus is on the first grid. For multi pickers, the widget needs
// to maintain the currently focused grid and base queries like this off of it.
$( this.picker ).find( ".ui-state-focus" ).not( ":first" ).removeClass( "ui-state-focus" );
},
open: function( event ) {
if ( this.inline || this.isOpen ) {
return;
}
if ( this._trigger( "beforeOpen", event ) === false ) {
return;
}
// TODO explain this
this.date = $.date( this.element.val(), this.options.dateFormat );
this.date.eachDay = this.options.eachDay;
this.date.select();
this.refresh();
var position = $.extend( {}, {
of: this.element
}, this.options.position );
this.picker
.attr( "aria-hidden", "false" )
.attr( "aria-expanded", "true" )
.show()
.position( position )
.hide();
this._show( this.picker, this.options.show );
// take trigger out of tab order to allow shift-tab to skip trigger
// TODO does this really make sense? related bug: tab-shift moves focus to last element on page
this.element.attr( "tabindex", -1 );
this.isOpen = true;
this._trigger( "open", event );
},
close: function( event ) {
if ( this.inline ) {
return;
}
this._setHiddenPicker();
this._hide( this.picker, this.options.hide );
this.element.attr( "tabindex" , 0 );
this.isOpen = false;
this._trigger( "close", event );
},
_setHiddenPicker: function() {
this.picker
.attr( "aria-hidden", "true" )
.attr( "aria-expanded", "false" );
},
select: function( event, time ) {
this.date.setTime( time ).select();
this.refresh();
if ( !this.inline ) {
this.element.val( this.date.format() );
this.close();
this._focusTrigger();
}
this._trigger( "select", event, {
// TODO replace with value option to initialise and read
date: this.date.format()
});
},
_destroy: function() {
if ( !this.inline ) {
this.picker.remove();
this.element
.removeAttr( "aria-haspopup" )
.removeAttr( "aria-owns" );
}
},
widget: function() {
return this.picker;
}
});
}));