/*! * 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 "
" + this._buildPreviousLink() + this._buildNextLink() + this._buildTitlebar() + "
"; }, _buildPreviousLink: function() { var labels = Globalize.translate( "datepicker" ); return ""; }, _buildNextLink: function() { var labels = Globalize.translate( "datepicker" ); return ""; }, _buildTitlebar: function() { var labels = Globalize.translate( "datepicker" ); return "
" + "
" + this._buildTitle() + "
" + ", " + labels.datePickerRole + "" + "
"; }, _buildTitle: function() { return "" + this.date.monthName() + " " + "" + this.date.year() + ""; }, _buildGrid: function() { return "" + this._buildGridHeading() + this._buildGridBody() + "
"; }, _buildGridHeading: function() { var cells = "", i = 0, labels = Globalize.translate( "datepicker" ); if ( this.options.showWeek ) { cells += "" + labels.weekHeader + ""; } for ( i; i < this.date.weekdays().length; i++ ) { cells += this._buildGridHeaderCell( this.date.weekdays()[i] ); } return "" + "" + cells + "" + ""; }, _buildGridHeaderCell: function( day ) { return "" + "" + day.shortname + "" + ""; }, _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 "" + cells + ""; }, _buildDayCell: function( day ) { var contents = "", idAttribute = day.render ? ( "id=" + this.id + "-" + day.date ) : "", ariaSelectedAttribute = "aria-selected=" + ( day.current ? "true" : "false" ), ariaDisabledAttribute = day.selectable ? "" : "aria-disabled=true"; if ( day.render ) { if ( day.selectable ) { contents = this._buildDayLink( day ); } else { contents = this._buildDayDisplay( day ); } } return "" + contents + ""; }, _buildDayLink: function( day ) { var link, classes = [ "ui-state-default" ], labels = Globalize.translate( "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.translate( "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 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(); this.picker .attr( "aria-hidden", "false" ) .attr( "aria-expanded", "true" ) .show() .position( this._buildPosition() ) .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" ); }, _buildPosition: function() { return $.extend( {}, { of: this.element }, this.options.position ); }, 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() }); }, _value: function( value ) { this.date.setTime( value ).select(); if ( !this.inline ) { this.element.val( this.date.format() ); } this.refresh(); }, value: function( value ) { if ( arguments.length ) { this._value( value ); } else { return this.isValid() ? this.date.format() : this.element.val(); } }, valueAsDate: function( value ) { if ( arguments.length ) { this._value( value ); } else { return this.isValid() ? this.date.date() : null; } }, isValid: function() { if ( this.inline ) { return true; } return Globalize.parseDate( this.element.val(), this.options.dateFormat ) !== null; }, _destroy: function() { if ( this.inline ) { this.picker.empty(); } else { this.picker.remove(); this.element .removeAttr( "aria-haspopup" ) .removeAttr( "aria-owns" ); } }, widget: function() { return this.picker; }, _setOption: function( key, value ) { this._super( key, value ); if ( key === "appendTo" ) { this.picker.appendTo( this._appendTo() ); } if ( key === "eachDay" ) { this.date.eachDay = this.options.eachDay; this.refresh(); } if ( key === "dateFormat" ) { this.date.setFormat( this.options.dateFormat ); if ( !this.inline ) { this.element.val( this.date.format() ); } } if ( key === "showWeek" ) { this.refresh(); } if ( key === "position" ) { this.picker.position( this._buildPosition() ); } } }); }));