Files
2026-08-27 10:50:54 -06:00

403 lines
12 KiB
JavaScript

/*
* Mobi Pick - An Android-style datepicker widget for jQuery Mobile.
*
* Created by Christoph Baudson.
*
* Please report issues at https://github.com/sustainablepace/mobipick/issues
*
* Version 0.9
*
* Licensed under MIT license, see MIT-license.txt
*/
(function( factory ) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define( ['jquery', 'jquery.mobile'], factory );
} else {
// Browser globals
factory( jQuery, jQuery.mobile );
}
}( function( $, $mobile, undefined ){
$.widget( "sustainablepace.mobipick", $mobile.widget, {
options: {
date : null,
dateFormat : "yyyy-MM-dd",
dateFormatMonth : "yyyy-MM",
dateFormatYear : "yyyy",
inputOrder : "d m y",
locale : "en",
intlStdDate : true,
buttonTheme : "b",
popup : {
dismissible: false,
history: false,
overlayTheme: "a",
positionTo: "window",
theme: "a",
transition: "none"
}
},
// Controller
_picker: $( [] ),
widgetEventPrefix: "mobipick",
destroy: function() {
this._close();
this.element.off( "tap click" );
this._picker.popup( "destroy" );
$.Widget.prototype.destroy.call( this );
},
_create: function() {
this._initOptions(); // parses options
this._createView(); // inserts markup into DOM
this.element.on( "tap click", $.proxy( this.open, this ) );
},
_initOptions: function() {
var date = this.element.val() || this.options.date,
minDate = this.element.attr( "min" ) || this.options.minDate,
maxDate = this.element.attr( "max" ) || this.options.maxDate;
if( date ) {
this._setOption( "date", date );
}
// Min and max dates can be set in the markup.
// See http://dev.w3.org/html5/markup/input.date.html
if( maxDate ) {
this._setOption( "maxDate", maxDate );
}
if( minDate ) {
this._setOption( "minDate", minDate );
}
this._setOption( "locale", this.options.locale );
},
_init: function() {
// fill input field with default value
if( this._getDate() !== null ) {
this.updateDateInput();
}
},
open: function( evt ) {
if( evt ) {
evt.stopPropagation();
evt.preventDefault();
}
var date = this._getDate();
if( !this._isXDate( date ) ) {
date = new XDate();
}
this._setOption( "date", this._fitDate( date ) );
this._setOption( "originalDate", this._getDate() );
this._showView();
this._updateView();
this._bindEvents();
},
_bindEvents: function() {
var self = this,
p = this._picker,
confirm = $.proxy( this._confirmDate, this ),
cancel = $.proxy( this._cancelDate, this ),
esc = $.proxy( this._cancelDateOnEsc, this );
// Set and Cancel buttons
p.find( ".mobipick-set" ).off().on( "tap", confirm );
p.find( ".mobipick-cancel" ).off().on( "tap", cancel );
$( document ).off( "keyup", esc ).on( "keyup", esc );
// +/- Buttons
var selectorMap = {
".mobipick-prev-day" : "_prevDay",
".mobipick-prev-month" : "_prevMonth",
".mobipick-prev-year" : "_prevYear",
".mobipick-next-day" : "_nextDay",
".mobipick-next-month" : "_nextMonth",
".mobipick-next-year" : "_nextYear"
};
for( var selector in selectorMap ) {
(function() {
var handler = self[ selectorMap[ selector ] ];
p.find( selector ).off().on( "tap", $.proxy( function() {
return self._handleDate( handler );
}, self ));
})();
}
},
_close: function() {
this._hideView();
},
_handleDate: function( dateHandler ) {
this._setOption( "date", this._fitDate( dateHandler.apply( this ) ) );
return false;
},
_confirmDate: function() {
var proceed = true,
dateDiff = this._getDate().diffDays( this.options.originalDate ),
hasChanged = dateDiff !== 0 || this.element.val() === "";
if( this.options.close && typeof this.options.close === "function" ) {
proceed = this.options.close.call() !== false;
}
if( proceed && hasChanged ) {
this._setOption( "originalDate", this._getDate() );
this.updateDateInput();
this.element.trigger("change");
} else {
this._setOption( "date", this.options.originalDate );
}
this._close();
return false;
},
_cancelDate: function() {
this._setOption( "date", this.options.originalDate );
this._close();
return false;
},
_cancelDateOnEsc: function( evt ) {
if( evt.keyCode === 27 ) {
this._cancelDate();
}
},
_setOption: function( key, val ) {
switch( key ) {
case "date":
var sane = this._sanitizeDate( val );
this.options[ key ] = sane ? sane.toDate() : sane;
break;
case "originalDate":
this.options[ key ] = this._sanitizeDate( val ).toDate();
break;
case "maxDate":
this.options[ key ] = this._sanitizeMaxDate( val ).toDate();
break;
case "minDate":
this.options[ key ] = this._sanitizeMinDate( val ).toDate();
break;
case "intlStdDate":
this.options[ key ] = !!val;
break;
case "locale":
this.options[ key ] = this._isValidLocale( val ) ? val : "en";
break;
default:
// Do not update view!
return $.Widget.prototype._setOption.apply( this, arguments );
}
this._updateView();
},
//
// Model
//
_sanitizeDate: function( date ) {
if( date === null ) {
return null;
}
var d = date;
if( typeof d === "string" ) {
d = new XDate( d );
}
if( this._isXDate( d ) ) {
d = d.toDate();
}
if( !this._isDate( d ) ) {
throw "Parameter 'date' must be a Date.";
}
return new XDate( d.getFullYear(), d.getMonth(), d.getDate() );
},
_sanitizeMinDate: function( date ) {
var minDate = this._sanitizeDate( date );
if( this._isAfterMaxDate( minDate ) ) {
throw "Min date must be before max date.";
}
return minDate;
},
_sanitizeMaxDate: function( date ) {
var maxDate = this._sanitizeDate( date );
if( this._isBeforeMinDate( maxDate ) ) {
throw "Max date must be after min date.";
}
return maxDate;
},
_getDateFormat: function() {
switch( this.options.accuracy ) {
case "month":
return "dateFormatMonth";
case "year":
return "dateFormatYear";
default:
return "dateFormat";
}
},
dateString: function() {
var format = this._getDateFormat(),
date = this._getDate();
return !date ? '' : date.toString( this.options[ format ] );
},
localeString: function() {
var l = this.options.locale,
a = this._getDateFormat(),
date = this._getDate();
return !date ? '' : date.toString(
this._isValidLocale( l ) ? XDate.locales[ l ][ a ] : this.options[ a ]
);
},
_fitDate: function( d ) {
return this._isAfterMaxDate( d ) ? this._getMaxDate() :
( this._isBeforeMinDate( d ) ? this._getMinDate() : d );
},
_isAfterMaxDate: function( d ) {
var maxDate = this._getMaxDate();
return this._isXDate( maxDate ) && d.diffDays( maxDate ) < 0;
},
_isBeforeMinDate: function( d ) {
var m = this._getMinDate();
return this._isXDate( m ) && m.diffDays( d ) < 0;
},
_isValidLocale: function( l ) {
return typeof l === "string" && XDate.locales && XDate.locales[ l ];
},
_isDate: function( d ) {
return typeof d === "object" && d !== null && d.constructor === Date;
},
_isXDate: function( x ) {
return typeof x === "object" && x !== null && x.constructor === XDate;
},
_getMaxDate: function() {
var maxDate = this.options.maxDate;
return this._isDate( maxDate ) ? new XDate( maxDate ) : null;
},
_getMinDate: function() {
var minDate = this.options.minDate;
return this._isDate( minDate ) ? new XDate( minDate ) : null;
},
_getDate: function() {
var date = this.options.date;
return this._isDate( date ) ? new XDate( date ) : null;
},
_prevDay: function() {
return this._addDay( -1 );
},
_nextDay: function() {
return this._addDay( 1 );
},
_prevMonth: function() {
return this._addMonth( -1 );
},
_nextMonth: function() {
return this._addMonth( 1 );
},
_prevYear: function() {
return this._addYear( -1 );
},
_nextYear: function() {
return this._addYear( 1 );
},
_addYear: function( y ) {
return this._getDate().addYears( y, true );
},
_addMonth: function( m ) {
var date = this._getDate(),
month = date.getMonth(),
newMonth = ( 12 + month + m ) % 12;
return date.setMonth( newMonth, true );
},
_addDay: function( d ) {
var dt = this._getDate(),
n = XDate.getDaysInMonth( dt.getFullYear(), dt.getMonth() );
return dt.setDate( ( dt.getDate() - 1 + n + d ) % n + 1 );
},
//
// View
//
_markup_header: "<div class='mobipick-main'><div class='mobipick-date-formatted'>Date</div><ul class='mobipick-groups'>",
_markup_d: "<li><ul><li><a class='mobipick-next-day'>+</a></li><li><input type='text' class='mobipick-input mobipick-day' /></li><li><a class='mobipick-prev-day'>-</a></li></ul></li>",
_markup_m: "<li><ul><li><a class='mobipick-next-month'>+</a></li><li><input type='text' class='mobipick-input mobipick-month' /></li><li><a class='mobipick-prev-month'>-</a></li></ul></li>",
_markup_y: "<li><ul><li><a class='mobipick-next-year'>+</a></li><li><input type='text' class='mobipick-input mobipick-year' /></li><li><a class='mobipick-prev-year'>-</a></li></ul></li>",
_markup_trailer: "</ul><ul class='mobipick-buttons'><li><a class='mobipick-set'>Set</a></li><li><a class='mobipick-cancel'>Cancel</a></li></ul></div>",
_applyTheme: function() {
var p = this._picker,
buttons = {
"bottom": "ul.mobipick-groups ul > li:first-child > a",
"top": "ul.mobipick-groups ul > li:last-child > a"
};
for( var key in buttons ) {
p.find( buttons[ key ] ).addClass( "ui-corner-all" )
.css( "border-" + key + "-left-radius", "0")
.css( "border-" + key + "-right-radius", "0");
}
p.addClass( "ui-body-" + this.options.popup.theme + " ui-corner-all" );
p.find( "a" ).attr( "href", "#" )
.addClass( "ui-body-" + this.options.buttonTheme );
p.find( "ul.mobipick-buttons a" ).addClass( "ui-corner-all" );
p.find( "input" )
.attr( "readonly", "readonly" ).addClass( "ui-shadow-inset" );
},
_createView: function() {
this.element.attr( "readonly", "readonly" );
var markup = this._markup_header +
this.options.inputOrder.split(" ").map(function(v, i, a) {
return this["_markup_" + v];
}, this).join("") +
this._markup_trailer;
this._picker = $( markup ).popup( this.options.popup );
$.data( this.element, "mobipick", this );
this._applyTheme();
},
_updateView: function() {
var date = this._getDate(),
p = this._picker;
if( this._isXDate( date ) ) {
p.find( ".mobipick-year" ).val( date.toString( "yyyy" ) );
p.find( ".mobipick-month" ).val( date.toString( "MMM" ) );
p.find( ".mobipick-day" ).val( date.toString( "dd" ) );
p.find( ".mobipick-date-formatted" ).text( this.localeString() );
}
var locale = {};
if( this._isValidLocale( this.options.locale ) ) {
XDate.defaultLocale = this.options.locale;
locale = XDate.locales[ this.options.locale ];
}
p.find( ".mobipick-set" ).text( locale.ok || "Set" );
p.find( ".mobipick-cancel" ).text( locale.cancel || "Cancel" );
// Display items based on accuracy setting
var columns = p.find( ".mobipick-groups > li" )
.removeClass( "mobipick-hide" )
.addClass( "mobipick-inline-block" );
if( this.options.accuracy === "month" ) {
p.css( "max-width", "280px" )
.find( ".mobipick-groups > li:first-child" )
.addClass( "mobipick-hide" )
.removeClass( "mobipick-inline-block" );
} else if( this.options.accuracy === "year" ) {
p.css( "max-width", "200px" )
.find( ".mobipick-groups > li:last-child" )
.siblings().addClass( "mobipick-hide" )
.removeClass( "mobipick-inline-block" );
} else {
p.css( "max-width", "300px" );
}
// minus 1% margin (left and right) per column
var columnCount = columns.filter( ":visible" ).size(),
width = ( ( 100 - columnCount * 2 ) / columnCount ) + "%";
columns.css( "width", width );
},
_showView: function() {
this._picker.show().popup("open").focus();
},
_hideView: function() {
this._picker.popup("close");
},
updateDateInput: function() {
this.element.val( this.options.intlStdDate ?
this.dateString() : this.localeString()
);
}
});
}));