From cd4ff6c4ea94be4b127dbfb4a1c9988715e2558d Mon Sep 17 00:00:00 2001 From: Adirelle Date: Fri, 9 Jan 2015 08:51:35 +0100 Subject: [PATCH] Localisation support for dates throughout the front-end using moment.js. Closes #734 Closes #732 --- PHPCI/Helper/Lang.php | 29 + PHPCI/View/Home/index.phtml | 4 +- PHPCI/View/SummaryTable.phtml | 8 +- PHPCI/View/layout.phtml | 1 + Tests/PHPCI/Helper/LangTest.php | 28 + .../daterangepicker/daterangepicker-bs3.css | 85 +- public/assets/js/phpci.js | 9 + .../daterangepicker/daterangepicker.js | 1061 +++++++++++------ 8 files changed, 869 insertions(+), 356 deletions(-) create mode 100644 Tests/PHPCI/Helper/LangTest.php diff --git a/PHPCI/Helper/Lang.php b/PHPCI/Helper/Lang.php index 1877426c..c86de1aa 100644 --- a/PHPCI/Helper/Lang.php +++ b/PHPCI/Helper/Lang.php @@ -13,6 +13,7 @@ use b8\Config; /** * Languages Helper Class - Handles loading strings files and the strings within them. + * * @package PHPCI\Helper */ class Lang @@ -23,6 +24,7 @@ class Lang /** * Get a specific string from the language file. + * * @param $string * @return mixed|string */ @@ -48,6 +50,7 @@ class Lang /** * Get the currently active language. + * * @return string|null */ public static function getLanguage() @@ -57,7 +60,9 @@ class Lang /** * Try and load a language, and if successful, set it for use throughout the system. + * * @param $language + * * @return bool */ public static function setLanguage($language) @@ -73,6 +78,7 @@ class Lang /** * Return a list of available languages and their names. + * * @return array */ public static function getLanguageOptions() @@ -90,6 +96,7 @@ class Lang /** * Get the strings for the currently active language. + * * @return string[] */ public static function getStrings() @@ -99,6 +106,7 @@ class Lang /** * Initialise the Language helper, try load the language file for the user's browser or the configured default. + * * @param Config $config */ public static function init(Config $config) @@ -137,6 +145,7 @@ class Lang /** * Load a specific language file. + * * @return string[]|null */ protected static function loadLanguage() @@ -168,4 +177,24 @@ class Lang } } } + + /** + * Create a time tag for localization. + * + * See http://momentjs.com/docs/#/displaying/format/ for a list of supported formats. + * + * @param \DateTime $dateTime The dateTime to represent. + * @param string $format The moment.js format to use. + * + * @return string The formatted tag. + */ + public static function formatDateTime(\DateTime $dateTime, $format = 'lll') + { + return sprintf( + '', + $dateTime->format(\DateTime::ISO8601), + $format, + $dateTime->format(\DateTime::RFC2822) + ); + } } diff --git a/PHPCI/View/Home/index.phtml b/PHPCI/View/Home/index.phtml index 11db43cf..6e27bb79 100644 --- a/PHPCI/View/Home/index.phtml +++ b/PHPCI/View/Home/index.phtml @@ -48,7 +48,7 @@ ?>
  • - format('M j Y'); ?> +
  • @@ -58,7 +58,7 @@
  • - format('H:i'); ?> +

    getProject()->getTitle(); ?> diff --git a/PHPCI/View/SummaryTable.phtml b/PHPCI/View/SummaryTable.phtml index d8ee8a56..10782a5b 100644 --- a/PHPCI/View/SummaryTable.phtml +++ b/PHPCI/View/SummaryTable.phtml @@ -27,12 +27,12 @@ foreach($projects as $project): case 2: $successes++; $statuses[] = 'ok'; - $success = is_null($success) && !is_null($build->getFinished()) ? $build->getFinished()->format('M j Y g:ia') : $success; + $success = is_null($success) && !is_null($build->getFinished()) ? Lang::formatDateTime($build->getFinished()) : $success; break; case 3: $failures++; $statuses[] = 'failed'; - $failure = is_null($failure) && !is_null($build->getFinished()) ? $build->getFinished()->format('M j Y g:ia') : $failure; + $failure = is_null($failure) && !is_null($build->getFinished()) ? Lang::formatDateTime($build->getFinished()) : $failure; break; } } @@ -59,7 +59,7 @@ foreach($projects as $project): $message = Lang::get('x_of_x_failed', $failures, $buildCount); if (!is_null($lastSuccess) && !is_null($lastSuccess->getFinished())) { - $message .= Lang::get('last_successful_build', $lastSuccess->getFinished()->format('M j Y')); + $message .= Lang::get('last_successful_build', Lang::formatDateTime($lastSuccess->getFinished())); } else { $message .= Lang::get('never_built_successfully'); } @@ -68,7 +68,7 @@ foreach($projects as $project): $shortMessage = Lang::get('all_builds_passed_short', $buildCount, $buildCount); if (!is_null($lastFailure) && !is_null($lastFailure->getFinished())) { - $message .= Lang::get('last_failed_build', $lastFailure->getFinished()->format('M j Y')); + $message .= Lang::get('last_failed_build', Lang::formatDateTime($lastFailure->getFinished())); } else { $message .= Lang::get('never_failed_build'); } diff --git a/PHPCI/View/layout.phtml b/PHPCI/View/layout.phtml index 3375d9df..64ec0834 100644 --- a/PHPCI/View/layout.phtml +++ b/PHPCI/View/layout.phtml @@ -275,6 +275,7 @@ + diff --git a/Tests/PHPCI/Helper/LangTest.php b/Tests/PHPCI/Helper/LangTest.php new file mode 100644 index 00000000..492cca52 --- /dev/null +++ b/Tests/PHPCI/Helper/LangTest.php @@ -0,0 +1,28 @@ +prophesize('DateTime'); + $dateTime->format(DateTime::ISO8601)->willReturn("ISODATE"); + $dateTime->format(DateTime::RFC2822)->willReturn("RFCDATE"); + + $this->assertEquals('', Lang::formatDateTime($dateTime->reveal(), 'FORMAT')); + } + + public function testLang_UseDefaultFormat() + { + $dateTime = $this->prophesize('DateTime'); + $dateTime->format(DateTime::ISO8601)->willReturn("ISODATE"); + $dateTime->format(DateTime::RFC2822)->willReturn("RFCDATE"); + + $this->assertEquals('', Lang::formatDateTime($dateTime->reveal())); + } +} diff --git a/public/assets/css/daterangepicker/daterangepicker-bs3.css b/public/assets/css/daterangepicker/daterangepicker-bs3.css index eed1e9f4..9ddabb1f 100755 --- a/public/assets/css/daterangepicker/daterangepicker-bs3.css +++ b/public/assets/css/daterangepicker/daterangepicker-bs3.css @@ -18,11 +18,16 @@ margin: 4px; } -.daterangepicker.opensright .ranges, .daterangepicker.opensright .calendar { +.daterangepicker.opensright .ranges, .daterangepicker.opensright .calendar, +.daterangepicker.openscenter .ranges, .daterangepicker.openscenter .calendar { float: right; margin: 4px; } +.daterangepicker.single .ranges, .daterangepicker.single .calendar { + float: none; +} + .daterangepicker .ranges { width: 160px; text-align: left; @@ -41,6 +46,14 @@ max-width: 270px; } +.daterangepicker.show-calendar .calendar { + display: block; +} + +.daterangepicker .calendar.single .calendar-date { + border: none; +} + .daterangepicker .calendar th, .daterangepicker .calendar td { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; white-space: nowrap; @@ -48,7 +61,8 @@ min-width: 32px; } -.daterangepicker .ranges label { +.daterangepicker .daterangepicker_start_input label, +.daterangepicker .daterangepicker_end_input label { color: #333; display: block; font-size: 11px; @@ -66,7 +80,6 @@ } .daterangepicker .ranges .input-mini { - background-color: #eee; border: 1px solid #ccc; border-radius: 4px; color: #555; @@ -153,6 +166,37 @@ content: ''; } +.daterangepicker.openscenter:before { + position: absolute; + top: -7px; + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.daterangepicker.openscenter:after { + position: absolute; + top: -6px; + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; + content: ''; +} + .daterangepicker.opensright:before { position: absolute; top: -7px; @@ -196,7 +240,7 @@ color: #999; } -.daterangepicker td.disabled { +.daterangepicker td.disabled, .daterangepicker option.disabled { color: #999; } @@ -211,6 +255,24 @@ border-radius: 0; } +.daterangepicker td.start-date { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.daterangepicker td.end-date { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.daterangepicker td.start-date.end-date { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + .daterangepicker td.active, .daterangepicker td.active:hover { background-color: #357ebd; border-color: #3071a9; @@ -239,7 +301,20 @@ width: 40%; } -.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.ampmselect { +.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { width: 50px; margin-bottom: 0; } + +.daterangepicker_start_input { + float: left; +} + +.daterangepicker_end_input { + float: left; + padding-left: 11px +} + +.daterangepicker th.month { + width: auto; +} diff --git a/public/assets/js/phpci.js b/public/assets/js/phpci.js index f98ea959..7e0b0f8b 100644 --- a/public/assets/js/phpci.js +++ b/public/assets/js/phpci.js @@ -3,7 +3,16 @@ var PHPCI = { intervals: {}, init: function () { + // Setup the date locale + moment.locale(PHPCI_LANGUAGE); + $(document).ready(function () { + // Format datetimes + $('time[datetime]').each(function() { + var $this = $(this); + $this.text(moment(this.dateTime).format($this.data('format') || 'lll')); + }); + // Update latest builds every 5 seconds: PHPCI.getBuilds(); PHPCI.intervals.getBuilds = setInterval(PHPCI.getBuilds, 5000); diff --git a/public/assets/js/plugins/daterangepicker/daterangepicker.js b/public/assets/js/plugins/daterangepicker/daterangepicker.js index 11a00fa3..891a1bf4 100755 --- a/public/assets/js/plugins/daterangepicker/daterangepicker.js +++ b/public/assets/js/plugins/daterangepicker/daterangepicker.js @@ -1,60 +1,39 @@ -// moment.js -// version : 2.1.0 -// author : Tim Wood -// license : MIT -// momentjs.com -!function(t){function e(t,e){return function(n){return u(t.call(this,n),e)}}function n(t,e){return function(n){return this.lang().ordinal(t.call(this,n),e)}}function s(){}function i(t){a(this,t)}function r(t){var e=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,s=t.weeks||t.week||t.w||0,i=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,o=t.seconds||t.second||t.s||0,u=t.milliseconds||t.millisecond||t.ms||0;this._input=t,this._milliseconds=u+1e3*o+6e4*a+36e5*r,this._days=i+7*s,this._months=n+12*e,this._data={},this._bubble()}function a(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function o(t){return 0>t?Math.ceil(t):Math.floor(t)}function u(t,e){for(var n=t+"";n.lengthn;n++)~~t[n]!==~~e[n]&&r++;return r+i}function f(t){return t?ie[t]||t.toLowerCase().replace(/(.)s$/,"$1"):t}function l(t,e){return e.abbr=t,x[t]||(x[t]=new s),x[t].set(e),x[t]}function _(t){if(!t)return H.fn._lang;if(!x[t]&&A)try{require("./lang/"+t)}catch(e){return H.fn._lang}return x[t]}function m(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,n,s=t.match(E);for(e=0,n=s.length;n>e;e++)s[e]=ue[s[e]]?ue[s[e]]:m(s[e]);return function(i){var r="";for(e=0;n>e;e++)r+=s[e]instanceof Function?s[e].call(i,t):s[e];return r}}function M(t,e){function n(e){return t.lang().longDateFormat(e)||e}for(var s=5;s--&&N.test(e);)e=e.replace(N,n);return re[e]||(re[e]=y(e)),re[e](t)}function g(t,e){switch(t){case"DDDD":return V;case"YYYY":return X;case"YYYYY":return $;case"S":case"SS":case"SSS":case"DDD":return I;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return R;case"a":case"A":return _(e._l)._meridiemParse;case"X":return B;case"Z":case"ZZ":return j;case"T":return q;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return J;default:return new RegExp(t.replace("\\",""))}}function p(t){var e=(j.exec(t)||[])[0],n=(e+"").match(ee)||["-",0,0],s=+(60*n[1])+~~n[2];return"+"===n[0]?-s:s}function D(t,e,n){var s,i=n._a;switch(t){case"M":case"MM":i[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":s=_(n._l).monthsParse(e),null!=s?i[1]=s:n._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(i[2]=~~e);break;case"YY":i[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":i[0]=~~e;break;case"a":case"A":n._isPm=_(n._l).isPM(e);break;case"H":case"HH":case"h":case"hh":i[3]=~~e;break;case"m":case"mm":i[4]=~~e;break;case"s":case"ss":i[5]=~~e;break;case"S":case"SS":case"SSS":i[6]=~~(1e3*("0."+e));break;case"X":n._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":n._useUTC=!0,n._tzm=p(e)}null==e&&(n._isValid=!1)}function Y(t){var e,n,s=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=~~((t._tzm||0)/60),s[4]+=~~((t._tzm||0)%60),n=new Date(0),t._useUTC?(n.setUTCFullYear(s[0],s[1],s[2]),n.setUTCHours(s[3],s[4],s[5],s[6])):(n.setFullYear(s[0],s[1],s[2]),n.setHours(s[3],s[4],s[5],s[6])),t._d=n}}function w(t){var e,n,s=t._f.match(E),i=t._i;for(t._a=[],e=0;eo&&(u=o,s=n);a(t,s)}function v(t){var e,n=t._i,s=K.exec(n);if(s){for(t._f="YYYY-MM-DD"+(s[2]||" "),e=0;4>e;e++)if(te[e][1].exec(n)){t._f+=te[e][0];break}j.exec(n)&&(t._f+=" Z"),w(t)}else t._d=new Date(n)}function T(e){var n=e._i,s=G.exec(n);n===t?e._d=new Date:s?e._d=new Date(+s[1]):"string"==typeof n?v(e):d(n)?(e._a=n.slice(0),Y(e)):e._d=n instanceof Date?new Date(+n):new Date(n)}function b(t,e,n,s,i){return i.relativeTime(e||1,!!n,t,s)}function S(t,e,n){var s=W(Math.abs(t)/1e3),i=W(s/60),r=W(i/60),a=W(r/24),o=W(a/365),u=45>s&&["s",s]||1===i&&["m"]||45>i&&["mm",i]||1===r&&["h"]||22>r&&["hh",r]||1===a&&["d"]||25>=a&&["dd",a]||45>=a&&["M"]||345>a&&["MM",W(a/30)]||1===o&&["y"]||["yy",o];return u[2]=e,u[3]=t>0,u[4]=n,b.apply({},u)}function F(t,e,n){var s,i=n-e,r=n-t.day();return r>i&&(r-=7),i-7>r&&(r+=7),s=H(t).add("d",r),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function O(t){var e=t._i,n=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=_().preparse(e)),H.isMoment(e)?(t=a({},e),t._d=new Date(+e._d)):n?d(n)?k(t):w(t):T(t),new i(t))}function z(t,e){H.fn[t]=H.fn[t+"s"]=function(t){var n=this._isUTC?"UTC":"";return null!=t?(this._d["set"+n+e](t),H.updateOffset(this),this):this._d["get"+n+e]()}}function C(t){H.duration.fn[t]=function(){return this._data[t]}}function L(t,e){H.duration.fn["as"+t]=function(){return+this/e}}for(var H,P,U="2.1.0",W=Math.round,x={},A="undefined"!=typeof module&&module.exports,G=/^\/?Date\((\-?\d+)/i,Z=/(\-)?(\d*)?\.?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,E=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,N=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,J=/\d\d?/,I=/\d{1,3}/,V=/\d{3}/,X=/\d{1,4}/,$=/[+\-]?\d{1,6}/,R=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,j=/Z|[\+\-]\d\d:?\d\d/i,q=/T/i,B=/[\+\-]?\d+(\.\d{1,3})?/,K=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,Q="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ee=/([\+\-]|\d\d)/gi,ne="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),se={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ie={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",w:"week",M:"month",y:"year"},re={},ae="DDD w W M D d".split(" "),oe="M D H h m s w W".split(" "),ue={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return u(this.year()%100,2)},YYYY:function(){return u(this.year(),4)},YYYYY:function(){return u(this.year(),5)},gg:function(){return u(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return u(this.weekYear(),5)},GG:function(){return u(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return u(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return u(~~(this.milliseconds()/10),2)},SSS:function(){return u(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(t/60),2)+":"+u(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}};ae.length;)P=ae.pop(),ue[P+"o"]=n(ue[P],P);for(;oe.length;)P=oe.pop(),ue[P+P]=e(ue[P],2);for(ue.DDDD=e(ue.DDD,3),s.prototype={set:function(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,n,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(n=H([2e3,e]),s="^"+this.months(n,"")+"|^"+this.monthsShort(n,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,n,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(n=H([2e3,1]).day(e),s="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase()[0]},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var n=this._calendar[t];return"function"==typeof n?n.apply(e):n},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,n,s){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,s):i.replace(/%d/i,t)},pastFuture:function(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return F(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6}},H=function(t,e,n){return O({_i:t,_f:e,_l:n,_isUTC:!1})},H.utc=function(t,e,n){return O({_useUTC:!0,_isUTC:!0,_l:n,_i:t,_f:e})},H.unix=function(t){return H(1e3*t)},H.duration=function(t,e){var n,s,i=H.isDuration(t),a="number"==typeof t,o=i?t._input:a?{}:t,u=Z.exec(t);return a?e?o[e]=t:o.milliseconds=t:u&&(n="-"===u[1]?-1:1,o={y:0,d:~~u[2]*n,h:~~u[3]*n,m:~~u[4]*n,s:~~u[5]*n,ms:~~u[6]*n}),s=new r(o),i&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},H.version=U,H.defaultFormat=Q,H.updateOffset=function(){},H.lang=function(t,e){return t?(e?l(t,e):x[t]||_(t),H.duration.fn._lang=H.fn._lang=_(t),void 0):H.fn._lang._abbr},H.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),_(t)},H.isMoment=function(t){return t instanceof i},H.isDuration=function(t){return t instanceof r},H.fn=i.prototype={clone:function(){return H(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return M(H(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!c(this._a,(this._isUTC?H.utc(this._a):H(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=M(this,t||H.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,1),this},subtract:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,-1),this},diff:function(t,e,n){var s,i,r=this._isUTC?H(t).zone(this._offset||0):H(t).local(),a=6e4*(this.zone()-r.zone());return e=f(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+r.daysInMonth()),i=12*(this.year()-r.year())+(this.month()-r.month()),i+=(this-H(this).startOf("month")-(r-H(r).startOf("month")))/s,i-=6e4*(this.zone()-H(this).startOf("month").zone()-(r.zone()-H(r).startOf("month").zone()))/s,"year"===e&&(i/=12)):(s=this-r,i="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-a)/864e5:"week"===e?(s-a)/6048e5:s),n?i:o(i)},from:function(t,e){return H.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(H(),t)},calendar:function(){var t=this.diff(H().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+H(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+H(t).startOf(e)},isSame:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)===+H(t).startOf(e)},min:function(t){return t=H.apply(null,arguments),this>t?this:t},max:function(t){return t=H.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=p(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&h(this,H.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},daysInMonth:function(){return H.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=W((H(this).startOf("day")-H(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=F(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=F(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=F(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this._d.getDay()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},lang:function(e){return e===t?this._lang:(this._lang=_(e),this)}},P=0;P

    ' + - '
    ' + + '
    ' + + '
    ' + '
    ' + '
    ' + - '
    ' + - '' + - '' + + '
    ' + + '' + + '' + '
    ' + - '
    ' + - '' + - '' + + '
    ' + + '' + + '' + '
    ' + - ' ' + - '' + + ' ' + + '' + '
    ' + '
    ' + '
    '; - this.parentEl = (hasOptions && options.parentEl && $(options.parentEl)) || $(this.parentEl); - //the date range picker + //custom options + if (typeof options !== 'object' || options === null) + options = {}; + + this.parentEl = (typeof options === 'object' && options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); this.container = $(DRPTemplate).appendTo(this.parentEl); - if (hasOptions) { + this.setOptions(options, cb); - if (typeof options.format == 'string') + //apply CSS classes and labels to buttons + var c = this.container; + $.each(this.buttonClasses, function (idx, val) { + c.find('button').addClass(val); + }); + this.container.find('.daterangepicker_start_input label').html(this.locale.fromLabel); + this.container.find('.daterangepicker_end_input label').html(this.locale.toLabel); + if (this.applyClass.length) + this.container.find('.applyBtn').addClass(this.applyClass); + if (this.cancelClass.length) + this.container.find('.cancelBtn').addClass(this.cancelClass); + this.container.find('.applyBtn').html(this.locale.applyLabel); + this.container.find('.cancelBtn').html(this.locale.cancelLabel); + + //event listeners + + this.container.find('.calendar') + .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this)) + .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this)) + .on('click.daterangepicker', 'td.available', $.proxy(this.clickDate, this)) + .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) + .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this)) + .on('change.daterangepicker', 'select.yearselect', $.proxy(this.updateMonthYear, this)) + .on('change.daterangepicker', 'select.monthselect', $.proxy(this.updateMonthYear, this)) + .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.updateTime, this)); + + this.container.find('.ranges') + .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) + .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)) + .on('click.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this)) + .on('change.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsChanged, this)) + .on('keydown.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsKeydown, this)) + .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this)) + .on('mouseenter.daterangepicker', 'li', $.proxy(this.enterRange, this)) + .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this)); + + if (this.element.is('input')) { + this.element.on({ + 'click.daterangepicker': $.proxy(this.show, this), + 'focus.daterangepicker': $.proxy(this.show, this), + 'keyup.daterangepicker': $.proxy(this.updateFromControl, this) + }); + } else { + this.element.on('click.daterangepicker', $.proxy(this.toggle, this)); + } + + }; + + DateRangePicker.prototype = { + + constructor: DateRangePicker, + + setOptions: function(options, callback) { + + this.startDate = moment().startOf('day'); + this.endDate = moment().endOf('day'); + this.timeZone = moment().zone(); + this.minDate = false; + this.maxDate = false; + this.dateLimit = false; + + this.showDropdowns = false; + this.showWeekNumbers = false; + this.timePicker = false; + this.timePickerSeconds = false; + this.timePickerIncrement = 30; + this.timePicker12Hour = true; + this.singleDatePicker = false; + this.ranges = {}; + + this.opens = 'right'; + if (this.element.hasClass('pull-right')) + this.opens = 'left'; + + this.buttonClasses = ['btn', 'btn-small btn-sm']; + this.applyClass = 'btn-success'; + this.cancelClass = 'btn-default'; + + this.format = 'MM/DD/YYYY'; + this.separator = ' - '; + + this.locale = { + applyLabel: 'Apply', + cancelLabel: 'Cancel', + fromLabel: 'From', + toLabel: 'To', + weekLabel: 'W', + customRangeLabel: 'Custom Range', + daysOfWeek: moment.weekdaysMin(), + monthNames: moment.monthsShort(), + firstDay: moment.localeData()._week.dow + }; + + this.cb = function () { }; + + if (typeof options.format === 'string') this.format = options.format; - if (typeof options.separator == 'string') + if (typeof options.separator === 'string') this.separator = options.separator; - if (typeof options.startDate == 'string') + if (typeof options.startDate === 'string') this.startDate = moment(options.startDate, this.format); - if (typeof options.endDate == 'string') + if (typeof options.endDate === 'string') this.endDate = moment(options.endDate, this.format); - if (typeof options.minDate == 'string') + if (typeof options.minDate === 'string') this.minDate = moment(options.minDate, this.format); - if (typeof options.maxDate == 'string') + if (typeof options.maxDate === 'string') this.maxDate = moment(options.maxDate, this.format); - if (typeof options.startDate == 'object') + if (typeof options.startDate === 'object') this.startDate = moment(options.startDate); - if (typeof options.endDate == 'object') + if (typeof options.endDate === 'object') this.endDate = moment(options.endDate); - if (typeof options.minDate == 'object') + if (typeof options.minDate === 'object') this.minDate = moment(options.minDate); - if (typeof options.maxDate == 'object') + if (typeof options.maxDate === 'object') this.maxDate = moment(options.maxDate); - if (typeof options.ranges == 'object') { - for (var range in options.ranges) { + if (typeof options.applyClass === 'string') + this.applyClass = options.applyClass; - var start = moment(options.ranges[range][0]); - var end = moment(options.ranges[range][1]); + if (typeof options.cancelClass === 'string') + this.cancelClass = options.cancelClass; + + if (typeof options.dateLimit === 'object') + this.dateLimit = options.dateLimit; + + if (typeof options.locale === 'object') { + + if (typeof options.locale.daysOfWeek === 'object') { + // Create a copy of daysOfWeek to avoid modification of original + // options object for reusability in multiple daterangepicker instances + this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + } + + if (typeof options.locale.monthNames === 'object') { + this.locale.monthNames = options.locale.monthNames.slice(); + } + + if (typeof options.locale.firstDay === 'number') { + this.locale.firstDay = options.locale.firstDay; + } + + if (typeof options.locale.applyLabel === 'string') { + this.locale.applyLabel = options.locale.applyLabel; + } + + if (typeof options.locale.cancelLabel === 'string') { + this.locale.cancelLabel = options.locale.cancelLabel; + } + + if (typeof options.locale.fromLabel === 'string') { + this.locale.fromLabel = options.locale.fromLabel; + } + + if (typeof options.locale.toLabel === 'string') { + this.locale.toLabel = options.locale.toLabel; + } + + if (typeof options.locale.weekLabel === 'string') { + this.locale.weekLabel = options.locale.weekLabel; + } + + if (typeof options.locale.customRangeLabel === 'string') { + this.locale.customRangeLabel = options.locale.customRangeLabel; + } + } + + if (typeof options.opens === 'string') + this.opens = options.opens; + + if (typeof options.showWeekNumbers === 'boolean') { + this.showWeekNumbers = options.showWeekNumbers; + } + + if (typeof options.buttonClasses === 'string') { + this.buttonClasses = [options.buttonClasses]; + } + + if (typeof options.buttonClasses === 'object') { + this.buttonClasses = options.buttonClasses; + } + + if (typeof options.showDropdowns === 'boolean') { + this.showDropdowns = options.showDropdowns; + } + + if (typeof options.singleDatePicker === 'boolean') { + this.singleDatePicker = options.singleDatePicker; + if (this.singleDatePicker) { + this.endDate = this.startDate.clone(); + } + } + + if (typeof options.timePicker === 'boolean') { + this.timePicker = options.timePicker; + } + + if (typeof options.timePickerSeconds === 'boolean') { + this.timePickerSeconds = options.timePickerSeconds; + } + + if (typeof options.timePickerIncrement === 'number') { + this.timePickerIncrement = options.timePickerIncrement; + } + + if (typeof options.timePicker12Hour === 'boolean') { + this.timePicker12Hour = options.timePicker12Hour; + } + + // update day names order to firstDay + if (this.locale.firstDay != 0) { + var iterator = this.locale.firstDay; + while (iterator > 0) { + this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); + iterator--; + } + } + + var start, end, range; + + //if no start/end dates set, check if an input element contains initial values + if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { + if ($(this.element).is('input[type=text]')) { + var val = $(this.element).val(), + split = val.split(this.separator); + + start = end = null; + + if (split.length == 2) { + start = moment(split[0], this.format); + end = moment(split[1], this.format); + } else if (this.singleDatePicker && val !== "") { + start = moment(val, this.format); + end = moment(val, this.format); + } + if (start !== null && end !== null) { + this.startDate = start; + this.endDate = end; + } + } + } + + // bind the time zone used to build the calendar to either the timeZone passed in through the options or the zone of the startDate (which will be the local time zone by default) + if (typeof options.timeZone === 'string' || typeof options.timeZone === 'number') { + this.timeZone = options.timeZone; + this.startDate.zone(this.timeZone); + this.endDate.zone(this.timeZone); + } else { + this.timeZone = moment(this.startDate).zone(); + } + + if (typeof options.ranges === 'object') { + for (range in options.ranges) { + + if (typeof options.ranges[range][0] === 'string') + start = moment(options.ranges[range][0], this.format); + else + start = moment(options.ranges[range][0]); + + if (typeof options.ranges[range][1] === 'string') + end = moment(options.ranges[range][1], this.format); + else + end = moment(options.ranges[range][1]); // If we have a min/max date set, bound this range // to it, but only if it would otherwise fall @@ -173,169 +365,123 @@ } var list = '
      '; - for (var range in this.ranges) { + for (range in this.ranges) { list += '
    • ' + range + '
    • '; } list += '
    • ' + this.locale.customRangeLabel + '
    • '; list += '
    '; + this.container.find('.ranges ul').remove(); this.container.find('.ranges').prepend(list); } - if (typeof options.dateLimit == 'object') - this.dateLimit = options.dateLimit; + if (typeof callback === 'function') { + this.cb = callback; + } - // update day names order to firstDay - if (typeof options.locale == 'object') { + if (!this.timePicker) { + this.startDate = this.startDate.startOf('day'); + this.endDate = this.endDate.endOf('day'); + } - if (typeof options.locale.daysOfWeek == 'object') { + if (this.singleDatePicker) { + this.opens = 'right'; + this.container.addClass('single'); + this.container.find('.calendar.right').show(); + this.container.find('.calendar.left').hide(); + if (!this.timePicker) { + this.container.find('.ranges').hide(); + } else { + this.container.find('.ranges .daterangepicker_start_input, .ranges .daterangepicker_end_input').hide(); + } + if (!this.container.find('.calendar.right').hasClass('single')) + this.container.find('.calendar.right').addClass('single'); + } else { + this.container.removeClass('single'); + this.container.find('.calendar.right').removeClass('single'); + this.container.find('.ranges').show(); + } - // Create a copy of daysOfWeek to avoid modification of original - // options object for reusability in multiple daterangepicker instances - this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + this.oldChosenLabel = this.chosenLabel; + + this.leftCalendar = { + month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute(), this.startDate.second()]), + calendar: [] + }; + + this.rightCalendar = { + month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute(), this.endDate.second()]), + calendar: [] + }; + + if (this.opens == 'right' || this.opens == 'center') { + //swap calendar positions + var first = this.container.find('.calendar.first'); + var second = this.container.find('.calendar.second'); + + if (second.hasClass('single')) { + second.removeClass('single'); + first.addClass('single'); } - if (typeof options.locale.firstDay == 'number') { - this.locale.firstDay = options.locale.firstDay; - var iterator = options.locale.firstDay; - while (iterator > 0) { - this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); - iterator--; - } + first.removeClass('left').addClass('right'); + second.removeClass('right').addClass('left'); + + if (this.singleDatePicker) { + first.show(); + second.hide(); } } - if (typeof options.opens == 'string') - this.opens = options.opens; - - if (typeof options.showWeekNumbers == 'boolean') { - this.showWeekNumbers = options.showWeekNumbers; + if (typeof options.ranges === 'undefined' && !this.singleDatePicker) { + this.container.addClass('show-calendar'); } - if (typeof options.buttonClasses == 'string') { - this.buttonClasses = [options.buttonClasses]; - } + this.container.addClass('opens' + this.opens); - if (typeof options.buttonClasses == 'object') { - this.buttonClasses = options.buttonClasses; - } + this.updateView(); + this.updateCalendars(); - if (typeof options.showDropdowns == 'boolean') { - this.showDropdowns = options.showDropdowns; - } + }, - if (typeof options.timePicker == 'boolean') { - this.timePicker = options.timePicker; - } + setStartDate: function(startDate) { + if (typeof startDate === 'string') + this.startDate = moment(startDate, this.format).zone(this.timeZone); - if (typeof options.timePickerIncrement == 'number') { - this.timePickerIncrement = options.timePickerIncrement; - } + if (typeof startDate === 'object') + this.startDate = moment(startDate); - if (typeof options.timePicker12Hour == 'boolean') { - this.timePicker12Hour = options.timePicker12Hour; - } + if (!this.timePicker) + this.startDate = this.startDate.startOf('day'); - } + this.oldStartDate = this.startDate.clone(); - if (!this.timePicker) { - this.startDate = this.startDate.startOf('day'); - this.endDate = this.endDate.startOf('day'); - } + this.updateView(); + this.updateCalendars(); + this.updateInputText(); + }, - //apply CSS classes to buttons - var c = this.container; - $.each(this.buttonClasses, function (idx, val) { - c.find('button').addClass(val); - }); + setEndDate: function(endDate) { + if (typeof endDate === 'string') + this.endDate = moment(endDate, this.format).zone(this.timeZone); - if (this.opens == 'right') { - //swap calendar positions - var left = this.container.find('.calendar.left'); - var right = this.container.find('.calendar.right'); - left.removeClass('left').addClass('right'); - right.removeClass('right').addClass('left'); - } + if (typeof endDate === 'object') + this.endDate = moment(endDate); - if (typeof options == 'undefined' || typeof options.ranges == 'undefined') { - this.container.find('.calendar').show(); - this.move(); - } + if (!this.timePicker) + this.endDate = this.endDate.endOf('day'); - if (typeof cb == 'function') - this.cb = cb; + this.oldEndDate = this.endDate.clone(); - this.container.addClass('opens' + this.opens); - - //try parse date if in text input - if (!hasOptions || (typeof options.startDate == 'undefined' && typeof options.endDate == 'undefined')) { - if ($(this.element).is('input[type=text]')) { - var val = $(this.element).val(); - var split = val.split(this.separator); - var start, end; - if (split.length == 2) { - start = moment(split[0], this.format); - end = moment(split[1], this.format); - } - if (start != null && end != null) { - this.startDate = start; - this.endDate = end; - } - } - } - - //state - this.oldStartDate = this.startDate.clone(); - this.oldEndDate = this.endDate.clone(); - - this.leftCalendar = { - month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute()]), - calendar: [] - }; - - this.rightCalendar = { - month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute()]), - calendar: [] - }; - - //event listeners - this.container.on('mousedown', $.proxy(this.mousedown, this)); - - this.container.find('.calendar') - .on('click', '.prev', $.proxy(this.clickPrev, this)) - .on('click', '.next', $.proxy(this.clickNext, this)) - .on('click', 'td.available', $.proxy(this.clickDate, this)) - .on('mouseenter', 'td.available', $.proxy(this.enterDate, this)) - .on('mouseleave', 'td.available', $.proxy(this.updateFormInputs, this)) - .on('change', 'select.yearselect', $.proxy(this.updateMonthYear, this)) - .on('change', 'select.monthselect', $.proxy(this.updateMonthYear, this)) - .on('change', 'select.hourselect,select.minuteselect,select.ampmselect', $.proxy(this.updateTime, this)); - - this.container.find('.ranges') - .on('click', 'button.applyBtn', $.proxy(this.clickApply, this)) - .on('click', 'button.cancelBtn', $.proxy(this.clickCancel, this)) - .on('click', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this)) - .on('click', 'li', $.proxy(this.clickRange, this)) - .on('mouseenter', 'li', $.proxy(this.enterRange, this)) - .on('mouseleave', 'li', $.proxy(this.updateFormInputs, this)); - - this.element.on('keyup', $.proxy(this.updateFromControl, this)); - - this.updateView(); - this.updateCalendars(); - - }; - - DateRangePicker.prototype = { - - constructor: DateRangePicker, - - mousedown: function (e) { - e.stopPropagation(); + this.updateView(); + this.updateCalendars(); + this.updateInputText(); }, updateView: function () { - this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()); - this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()); + this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute()); + this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute()); this.updateFormInputs(); }, @@ -354,11 +500,20 @@ if (!this.element.is('input')) return; if (!this.element.val().length) return; - var dateString = this.element.val().split(this.separator); - var start = moment(dateString[0], this.format); - var end = moment(dateString[1], this.format); + var dateString = this.element.val().split(this.separator), + start = null, + end = null; + + if(dateString.length === 2) { + start = moment(dateString[0], this.format).zone(this.timeZone); + end = moment(dateString[1], this.format).zone(this.timeZone); + } + + if (this.singleDatePicker || start === null || end === null) { + start = moment(this.element.val(), this.format).zone(this.timeZone); + end = start; + } - if (start == null || end == null) return; if (end.isBefore(start)) return; this.oldStartDate = this.startDate.clone(); @@ -375,18 +530,24 @@ notify: function () { this.updateView(); - this.cb(this.startDate, this.endDate); + this.cb(this.startDate, this.endDate, this.chosenLabel); }, move: function () { - var parentOffset = { - top: this.parentEl.offset().top - (this.parentEl.is('body') ? 0 : this.parentEl.scrollTop()), - left: this.parentEl.offset().left - (this.parentEl.is('body') ? 0 : this.parentEl.scrollLeft()) - }; + var parentOffset = { top: 0, left: 0 }; + var parentRightEdge = $(window).width(); + if (!this.parentEl.is('body')) { + parentOffset = { + top: this.parentEl.offset().top - this.parentEl.scrollTop(), + left: this.parentEl.offset().left - this.parentEl.scrollLeft() + }; + parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; + } + if (this.opens == 'left') { this.container.css({ top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, - right: $(window).width() - this.element.offset().left - this.element.outerWidth() - parentOffset.left, + right: parentRightEdge - this.element.offset().left - this.element.outerWidth(), left: 'auto' }); if (this.container.offset().left < 0) { @@ -395,6 +556,19 @@ left: 9 }); } + } else if (this.opens == 'center') { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, + left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 + - this.container.outerWidth() / 2, + right: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } } else { this.container.css({ top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, @@ -410,20 +584,58 @@ } }, + toggle: function (e) { + if (this.element.hasClass('active')) { + this.hide(); + } else { + this.show(); + } + }, + show: function (e) { + if (this.isShowing) return; + + this.element.addClass('active'); this.container.show(); this.move(); - if (e) { - e.stopPropagation(); - e.preventDefault(); - } + // Create a click proxy that is private to this instance of datepicker, for unbinding + this._outsideClickProxy = $.proxy(function (e) { this.outsideClick(e); }, this); + // Bind global datepicker mousedown for hiding and + $(document) + .on('mousedown.daterangepicker', this._outsideClickProxy) + // also support mobile devices + .on('touchend.daterangepicker', this._outsideClickProxy) + // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them + .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) + // and also close when focus changes to outside the picker (eg. tabbing between controls) + .on('focusin.daterangepicker', this._outsideClickProxy); - $(document).on('mousedown', $.proxy(this.hide, this)); - this.element.trigger('shown', {target: e.target, picker: this}); + this.isShowing = true; + this.element.trigger('show.daterangepicker', this); + }, + + outsideClick: function (e) { + var target = $(e.target); + // if the page is clicked anywhere except within the daterangerpicker/button + // itself then call this.hide() + if ( + // ie modal dialog fix + e.type == "focusin" || + target.closest(this.element).length || + target.closest(this.container).length || + target.closest('.calendar-date').length + ) return; + this.hide(); }, hide: function (e) { + if (!this.isShowing) return; + + $(document) + .off('.daterangepicker'); + + this.element.removeClass('active'); this.container.hide(); if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) @@ -432,11 +644,12 @@ this.oldStartDate = this.startDate.clone(); this.oldEndDate = this.endDate.clone(); - $(document).off('mousedown', this.hide); - this.element.trigger('hidden', { picker: this }); + this.isShowing = false; + this.element.trigger('hide.daterangepicker', this); }, enterRange: function (e) { + // mouse pointer has entered a range label var label = e.target.innerHTML; if (label == this.locale.customRangeLabel) { this.updateView(); @@ -448,17 +661,51 @@ }, showCalendars: function() { - this.container.find('.calendar').show(); + this.container.addClass('show-calendar'); this.move(); + this.element.trigger('showCalendar.daterangepicker', this); + }, + + hideCalendars: function() { + this.container.removeClass('show-calendar'); + this.element.trigger('hideCalendar.daterangepicker', this); + }, + + // when a date is typed into the start to end date textboxes + inputsChanged: function (e) { + var el = $(e.target); + var date = moment(el.val(), this.format); + if (!date.isValid()) return; + + var startDate, endDate; + if (el.attr('name') === 'daterangepicker_start') { + startDate = date; + endDate = this.endDate; + } else { + startDate = this.startDate; + endDate = date; + } + this.setCustomDates(startDate, endDate); + }, + + inputsKeydown: function(e) { + if (e.keyCode === 13) { + this.inputsChanged(e); + this.notify(); + } }, updateInputText: function() { - if (this.element.is('input')) + if (this.element.is('input') && !this.singleDatePicker) { this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format)); + } else if (this.element.is('input')) { + this.element.val(this.endDate.format(this.format)); + } }, clickRange: function (e) { var label = e.target.innerHTML; + this.chosenLabel = label; if (label == this.locale.customRangeLabel) { this.showCalendars(); } else { @@ -469,7 +716,7 @@ if (!this.timePicker) { this.startDate.startOf('day'); - this.endDate.startOf('day'); + this.endDate.endOf('day'); } this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute()); @@ -478,17 +725,18 @@ this.updateInputText(); - this.container.find('.calendar').hide(); + this.hideCalendars(); this.hide(); + this.element.trigger('apply.daterangepicker', this); } }, clickPrev: function (e) { var cal = $(e.target).parents('.calendar'); if (cal.hasClass('left')) { - this.leftCalendar.month.subtract('month', 1); + this.leftCalendar.month.subtract(1, 'month'); } else { - this.rightCalendar.month.subtract('month', 1); + this.rightCalendar.month.subtract(1, 'month'); } this.updateCalendars(); }, @@ -496,15 +744,14 @@ clickNext: function (e) { var cal = $(e.target).parents('.calendar'); if (cal.hasClass('left')) { - this.leftCalendar.month.add('month', 1); + this.leftCalendar.month.add(1, 'month'); } else { - this.rightCalendar.month.add('month', 1); + this.rightCalendar.month.add(1, 'month'); } this.updateCalendars(); }, - enterDate: function (e) { - + hoverDate: function (e) { var title = $(e.target).attr('data-title'); var row = title.substr(1, 1); var col = title.substr(3, 1); @@ -515,7 +762,19 @@ } else { this.container.find('input[name=daterangepicker_end]').val(this.rightCalendar.calendar[row][col].format(this.format)); } + }, + setCustomDates: function(startDate, endDate) { + this.chosenLabel = this.locale.customRangeLabel; + if (startDate.isAfter(endDate)) { + var difference = this.endDate.diff(this.startDate); + endDate = moment(startDate).add(difference, 'ms'); + } + this.startDate = startDate; + this.endDate = endDate; + + this.updateView(); + this.updateCalendars(); }, clickDate: function (e) { @@ -524,19 +783,20 @@ var col = title.substr(3, 1); var cal = $(e.target).parents('.calendar'); + var startDate, endDate; if (cal.hasClass('left')) { - var startDate = this.leftCalendar.calendar[row][col]; - var endDate = this.endDate; - if (typeof this.dateLimit == 'object') { + startDate = this.leftCalendar.calendar[row][col]; + endDate = this.endDate; + if (typeof this.dateLimit === 'object') { var maxDate = moment(startDate).add(this.dateLimit).startOf('day'); if (endDate.isAfter(maxDate)) { endDate = maxDate; } } } else { - var startDate = this.startDate; - var endDate = this.rightCalendar.calendar[row][col]; - if (typeof this.dateLimit == 'object') { + startDate = this.startDate; + endDate = this.rightCalendar.calendar[row][col]; + if (typeof this.dateLimit === 'object') { var minDate = moment(endDate).subtract(this.dateLimit).startOf('day'); if (startDate.isBefore(minDate)) { startDate = minDate; @@ -544,72 +804,72 @@ } } - cal.find('td').removeClass('active'); - - if (startDate.isSame(endDate) || startDate.isBefore(endDate)) { - $(e.target).addClass('active'); - this.startDate = startDate; - this.endDate = endDate; - } else if (startDate.isAfter(endDate)) { - $(e.target).addClass('active'); - this.startDate = startDate; - this.endDate = moment(startDate).add('day', 1).startOf('day'); + if (this.singleDatePicker && cal.hasClass('left')) { + endDate = startDate.clone(); + } else if (this.singleDatePicker && cal.hasClass('right')) { + startDate = endDate.clone(); } - this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()); - this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()); - this.updateCalendars(); + cal.find('td').removeClass('active'); + + $(e.target).addClass('active'); + + this.setCustomDates(startDate, endDate); + + if (!this.timePicker) + endDate.endOf('day'); + + if (this.singleDatePicker && !this.timePicker) + this.clickApply(); }, clickApply: function (e) { this.updateInputText(); this.hide(); + this.element.trigger('apply.daterangepicker', this); }, clickCancel: function (e) { this.startDate = this.oldStartDate; this.endDate = this.oldEndDate; + this.chosenLabel = this.oldChosenLabel; this.updateView(); this.updateCalendars(); this.hide(); + this.element.trigger('cancel.daterangepicker', this); }, updateMonthYear: function (e) { - - var isLeft = $(e.target).closest('.calendar').hasClass('left'); - var cal = this.container.find('.calendar.left'); - if (!isLeft) - cal = this.container.find('.calendar.right'); + var isLeft = $(e.target).closest('.calendar').hasClass('left'), + leftOrRight = isLeft ? 'left' : 'right', + cal = this.container.find('.calendar.'+leftOrRight); // Month must be Number for new moment versions var month = parseInt(cal.find('.monthselect').val(), 10); var year = cal.find('.yearselect').val(); - if (isLeft) { - this.leftCalendar.month.month(month).year(year); - } else { - this.rightCalendar.month.month(month).year(year); - } - + this[leftOrRight+'Calendar'].month.month(month).year(year); this.updateCalendars(); - }, updateTime: function(e) { - var isLeft = $(e.target).closest('.calendar').hasClass('left'); - var cal = this.container.find('.calendar.left'); - if (!isLeft) - cal = this.container.find('.calendar.right'); + var cal = $(e.target).closest('.calendar'), + isLeft = cal.hasClass('left'); - var hour = parseInt(cal.find('.hourselect').val()); - var minute = parseInt(cal.find('.minuteselect').val()); + var hour = parseInt(cal.find('.hourselect').val(), 10); + var minute = parseInt(cal.find('.minuteselect').val(), 10); + var second = 0; + + if (this.timePickerSeconds) { + second = parseInt(cal.find('.secondselect').val(), 10); + } if (this.timePicker12Hour) { var ampm = cal.find('.ampmselect').val(); - if (ampm == 'PM' && hour < 12) + if (ampm === 'PM' && hour < 12) hour += 12; - if (ampm == 'AM' && hour == 12) + if (ampm === 'AM' && hour === 12) hour = 0; } @@ -617,25 +877,31 @@ var start = this.startDate.clone(); start.hour(hour); start.minute(minute); + start.second(second); this.startDate = start; - this.leftCalendar.month.hour(hour).minute(minute); + this.leftCalendar.month.hour(hour).minute(minute).second(second); + if (this.singleDatePicker) + this.endDate = start.clone(); } else { var end = this.endDate.clone(); end.hour(hour); end.minute(minute); + end.second(second); this.endDate = end; - this.rightCalendar.month.hour(hour).minute(minute); + if (this.singleDatePicker) + this.startDate = end.clone(); + this.rightCalendar.month.hour(hour).minute(minute).second(second); } + this.updateView(); this.updateCalendars(); - }, updateCalendars: function () { - this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), 'left'); - this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), 'right'); - this.container.find('.calendar.left').html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate)); - this.container.find('.calendar.right').html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.startDate, this.maxDate)); + this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), this.leftCalendar.month.second(), 'left'); + this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), this.rightCalendar.month.second(), 'right'); + this.container.find('.calendar.left').empty().html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate, 'left')); + this.container.find('.calendar.right').empty().html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.singleDatePicker ? this.minDate : this.startDate, this.maxDate, 'right')); this.container.find('.ranges li').removeClass('active'); var customRange = true; @@ -644,34 +910,44 @@ if (this.timePicker) { if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) { customRange = false; - this.container.find('.ranges li:eq(' + i + ')').addClass('active'); + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')') + .addClass('active').html(); } } else { //ignore times when comparing dates if time picker is not enabled if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { customRange = false; - this.container.find('.ranges li:eq(' + i + ')').addClass('active'); + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')') + .addClass('active').html(); } } i++; } - if (customRange) - this.container.find('.ranges li:last').addClass('active'); + if (customRange) { + this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html(); + this.showCalendars(); + } }, - buildCalendar: function (month, year, hour, minute, side) { - + buildCalendar: function (month, year, hour, minute, second, side) { + var daysInMonth = moment([year, month]).daysInMonth(); var firstDay = moment([year, month, 1]); - var lastMonth = moment(firstDay).subtract('month', 1).month(); - var lastYear = moment(firstDay).subtract('month', 1).year(); + var lastDay = moment([year, month, daysInMonth]); + var lastMonth = moment(firstDay).subtract(1, 'month').month(); + var lastYear = moment(firstDay).subtract(1, 'month').year(); var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); var dayOfWeek = firstDay.day(); + var i; + //initialize a 6 rows x 7 columns array for the calendar var calendar = []; - for (var i = 0; i < 6; i++) { + calendar.firstDay = firstDay; + calendar.lastDay = lastDay; + + for (i = 0; i < 6; i++) { calendar[i] = []; } @@ -683,25 +959,39 @@ if (dayOfWeek == this.locale.firstDay) startDay = daysInLastMonth - 6; - var curDate = moment([lastYear, lastMonth, startDay, 12, minute]); - for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add('hour', 24)) { - if (i > 0 && col % 7 == 0) { + var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]).zone(this.timeZone); + + var col, row; + for (i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { + if (i > 0 && col % 7 === 0) { col = 0; row++; } calendar[row][col] = curDate.clone().hour(hour); curDate.hour(12); + + if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') { + calendar[row][col] = this.minDate.clone(); + } + + if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') { + calendar[row][col] = this.maxDate.clone(); + } + } return calendar; - }, renderDropdowns: function (selected, minDate, maxDate) { var currentMonth = selected.month(); + var currentYear = selected.year(); + var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); + var minYear = (minDate && minDate.year()) || (currentYear - 50); + var monthHtml = '"; - var currentYear = selected.year(); - var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); - var minYear = (minDate && minDate.year()) || (currentYear - 50); var yearHtml = ''; + + // Disallow selections before the minDate or after the maxDate + var min_hour = 0; + var max_hour = 23; + + if (minDate && (side == 'left' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == minDate.format('YYYY-MM-DD')) { + min_hour = minDate.hour(); + if (selected.hour() < min_hour) + selected.hour(min_hour); + if (this.timePicker12Hour && min_hour >= 12 && selected.hour() >= 12) + min_hour -= 12; + if (this.timePicker12Hour && min_hour == 12) + min_hour = 1; + } + + if (maxDate && (side == 'right' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == maxDate.format('YYYY-MM-DD')) { + max_hour = maxDate.hour(); + if (selected.hour() > max_hour) + selected.hour(max_hour); + if (this.timePicker12Hour && max_hour >= 12 && selected.hour() >= 12) + max_hour -= 12; + } + var start = 0; var end = 23; var selected_hour = selected.hour(); @@ -822,13 +1133,16 @@ end = 12; if (selected_hour >= 12) selected_hour -= 12; - if (selected_hour == 0) + if (selected_hour === 0) selected_hour = 12; } - for (var i = start; i <= end; i++) { + for (i = start; i <= end; i++) { + if (i == selected_hour) { html += ''; + } else if (i < min_hour || i > max_hour) { + html += ''; } else { html += ''; } @@ -838,12 +1152,30 @@ html += ' '; + if (this.timePickerSeconds) { + html += ': '; + } + if (this.timePicker12Hour) { html += ''; } @@ -867,6 +1229,14 @@ return html; + }, + + remove: function() { + + this.container.remove(); + this.element.off('.daterangepicker'); + this.element.removeData('daterangepicker'); + } }; @@ -874,10 +1244,11 @@ $.fn.daterangepicker = function (options, cb) { this.each(function () { var el = $(this); - if (!el.data('daterangepicker')) - el.data('daterangepicker', new DateRangePicker(el, options, cb)); + if (el.data('daterangepicker')) + el.data('daterangepicker').remove(); + el.data('daterangepicker', new DateRangePicker(el, options, cb)); }); return this; }; -}(window.jQuery); +}));