diff --git a/README.md b/README.md
index 840929f..7ba5f46 100644
--- a/README.md
+++ b/README.md
@@ -183,7 +183,19 @@ You will have 50 love gems on 01/16/2016. Good things come to those who wait!
## The Web App
-You asked for a web app, and here it is! The [web_app](web_app) directory contains a (mostly) fully featured web app version of SIF Tools. The best part is, you don't need a web server to run it. You should just be able to open the [sif_tools.html](web_app/sif_tools.html) file in your local web browser and run it right on your own computer. You will need to have JavaScript enabled in your browser however. (Of course you can host these files on a web server just like any other website, if you happen to have access to a web server.)
+You asked for a web app, and here it is! The [web_app](web_app) directory contains a fully featured web app version of SIF Tools. In fact, the web app actually does some things that the original Python scripts don't! (yet.) The best part is, you don't need a web server to run it. You should just be able to open the [sif_tools.html](web_app/sif_tools.html) file in your local web browser and run it right on your own computer. You will need to have JavaScript enabled in your browser however. (Of course you can host these files on a web server just like any other website, if you happen to have access to a web server.) Or you can run the copy hosted on my own web server [here](https://beta.DonaldBurr.com/sif_tools/sif_tools.html).
+
+## Credits
+
+The web app uses the following third-party Javascript libraries:
+
+* [jQuery](https://jquery.com) ([License](https://jquery.org/license/))
+* [jQuery UI](https://jqueryui.com) ([License](https://github.com/jquery/jquery-ui/blob/master/LICENSE.txt))
+* [jQuery Keypad](http://keith-wood.name/keypad.html) ([License](http://keith-wood.name/licence.html))
+* [Moment.js](http://momentjs.com) ([License](https://github.com/moment/moment/blob/develop/LICENSE))
+* [jquery.timepicker](http://jonthornton.github.io/jquery-timepicker/) ([License](https://opensource.org/licenses/MIT))
+* [node-sprintf](https://github.com/maritz/node-sprintf) ([License](https://github.com/maritz/node-sprintf#copyrightlicense))
+* [twitterFetcher](https://github.com/jasonmayes/Twitter-Post-Fetcher) ([License](https://github.com/jasonmayes/Twitter-Post-Fetcher/blob/master/License.txt))
## Bugs? Need help? Got any suggestions/ideas for new features? Or want to chat?
diff --git a/web_app/css/external/jquery-ui-timepicker-addon.css b/web_app/css/external/jquery-ui-timepicker-addon.css
deleted file mode 100644
index 2d9e031..0000000
--- a/web_app/css/external/jquery-ui-timepicker-addon.css
+++ /dev/null
@@ -1,27 +0,0 @@
-.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
-.ui-timepicker-div dl { text-align: left; }
-.ui-timepicker-div dl dt { float: left; clear:left; padding: 0 0 0 5px; }
-.ui-timepicker-div dl dd { margin: 0 10px 10px 40%; }
-.ui-timepicker-div td { font-size: 90%; }
-.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
-.ui-timepicker-div .ui_tpicker_unit_hide{ display: none; }
-
-.ui-timepicker-rtl{ direction: rtl; }
-.ui-timepicker-rtl dl { text-align: right; padding: 0 5px 0 0; }
-.ui-timepicker-rtl dl dt{ float: right; clear: right; }
-.ui-timepicker-rtl dl dd { margin: 0 40% 10px 10px; }
-
-/* Shortened version style */
-.ui-timepicker-div.ui-timepicker-oneLine { padding-right: 2px; }
-.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time,
-.ui-timepicker-div.ui-timepicker-oneLine dt { display: none; }
-.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time_label { display: block; padding-top: 2px; }
-.ui-timepicker-div.ui-timepicker-oneLine dl { text-align: right; }
-.ui-timepicker-div.ui-timepicker-oneLine dl dd,
-.ui-timepicker-div.ui-timepicker-oneLine dl dd > div { display:inline-block; margin:0; }
-.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_minute:before,
-.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_second:before { content:':'; display:inline-block; }
-.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_millisec:before,
-.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_microsec:before { content:'.'; display:inline-block; }
-.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide,
-.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide:before{ display: none; }
\ No newline at end of file
diff --git a/web_app/css/external/jquery.timepicker.css b/web_app/css/external/jquery.timepicker.css
new file mode 100644
index 0000000..cd75f13
--- /dev/null
+++ b/web_app/css/external/jquery.timepicker.css
@@ -0,0 +1,72 @@
+.ui-timepicker-wrapper {
+ overflow-y: auto;
+ height: 150px;
+ width: 6.5em;
+ background: #fff;
+ border: 1px solid #ddd;
+ -webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);
+ -moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);
+ box-shadow:0 5px 10px rgba(0,0,0,0.2);
+ outline: none;
+ z-index: 10001;
+ margin: 0;
+}
+
+.ui-timepicker-wrapper.ui-timepicker-with-duration {
+ width: 13em;
+}
+
+.ui-timepicker-wrapper.ui-timepicker-with-duration.ui-timepicker-step-30,
+.ui-timepicker-wrapper.ui-timepicker-with-duration.ui-timepicker-step-60 {
+ width: 11em;
+}
+
+.ui-timepicker-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.ui-timepicker-duration {
+ margin-left: 5px; color: #888;
+}
+
+.ui-timepicker-list:hover .ui-timepicker-duration {
+ color: #888;
+}
+
+.ui-timepicker-list li {
+ padding: 3px 0 3px 5px;
+ cursor: pointer;
+ white-space: nowrap;
+ color: #000;
+ list-style: none;
+ margin: 0;
+}
+
+.ui-timepicker-list:hover .ui-timepicker-selected {
+ background: #fff; color: #000;
+}
+
+li.ui-timepicker-selected,
+.ui-timepicker-list li:hover,
+.ui-timepicker-list .ui-timepicker-selected:hover {
+ background: #1980EC; color: #fff;
+}
+
+li.ui-timepicker-selected .ui-timepicker-duration,
+.ui-timepicker-list li:hover .ui-timepicker-duration {
+ color: #ccc;
+}
+
+.ui-timepicker-list li.ui-timepicker-disabled,
+.ui-timepicker-list li.ui-timepicker-disabled:hover,
+.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled {
+ color: #888;
+ cursor: default;
+}
+
+.ui-timepicker-list li.ui-timepicker-disabled:hover,
+.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled {
+ background: #f2f2f2;
+}
diff --git a/web_app/css/sif-tools.css b/web_app/css/sif_tools.css
similarity index 66%
rename from web_app/css/sif-tools.css
rename to web_app/css/sif_tools.css
index 67a87c1..a1d7680 100644
--- a/web_app/css/sif-tools.css
+++ b/web_app/css/sif_tools.css
@@ -1,13 +1,12 @@
body {
font: 100% "Trebuchet MS", sans-serif;
- margin: 50px;
+ font-family: Verdana,Arial,sans-serif;
+ font-size: 14px;
+ margin: 0px;
background-color: #000000;
color: #ffffff;
-}
-
-body {
- font-family: Verdana,Arial,sans-serif;
- font-size: 14px;
+ /*height: 100%;*/
+ /*min-height: 480px;*/
}
p {
diff --git a/web_app/index.html b/web_app/index.html
index 7c11c2c..9c94c0e 100644
--- a/web_app/index.html
+++ b/web_app/index.html
@@ -1,10 +1,13 @@
+
-
';
- html += '';
-
- // Create the elements from string
- html += '
';
- var $tp = $(html);
-
- // if we only want time picker...
- if (o.timeOnly === true) {
- $tp.prepend('
' + '
' + o.timeOnlyTitle + '
' + '
');
- $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide();
- }
-
- // add sliders, adjust grids, add events
- for (i = 0, l = tp_inst.units.length; i < l; i++) {
- litem = tp_inst.units[i];
- uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1);
- show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem];
-
- // add the slider
- tp_inst[litem + '_slider'] = tp_inst.control.create(tp_inst, $tp.find('.ui_tpicker_' + litem + '_slider'), litem, tp_inst[litem], o[litem + 'Min'], max[litem], o['step' + uitem]);
-
- // adjust the grid and add click event
- if (show && o[litem + 'Grid'] > 0) {
- size = 100 * gridSize[litem] * o[litem + 'Grid'] / (max[litem] - o[litem + 'Min']);
- $tp.find('.ui_tpicker_' + litem + ' table').css({
- width: size + "%",
- marginLeft: o.isRTL ? '0' : ((size / (-2 * gridSize[litem])) + "%"),
- marginRight: o.isRTL ? ((size / (-2 * gridSize[litem])) + "%") : '0',
- borderCollapse: 'collapse'
- }).find("td").click(function (e) {
- var $t = $(this),
- h = $t.html(),
- n = parseInt(h.replace(/[^0-9]/g), 10),
- ap = h.replace(/[^apm]/ig),
- f = $t.data('for'); // loses scope, so we use data-for
-
- if (f === 'hour') {
- if (ap.indexOf('p') !== -1 && n < 12) {
- n += 12;
- }
- else {
- if (ap.indexOf('a') !== -1 && n === 12) {
- n = 0;
- }
- }
- }
-
- tp_inst.control.value(tp_inst, tp_inst[f + '_slider'], litem, n);
-
- tp_inst._onTimeChange();
- tp_inst._onSelectHandler();
- }).css({
- cursor: 'pointer',
- width: (100 / gridSize[litem]) + '%',
- textAlign: 'center',
- overflow: 'hidden'
- });
- } // end if grid > 0
- } // end for loop
-
- // Add timezone options
- this.timezone_select = $tp.find('.ui_tpicker_timezone').append('').find("select");
- $.fn.append.apply(this.timezone_select,
- $.map(o.timezoneList, function (val, idx) {
- return $("").val(typeof val === "object" ? val.value : val).text(typeof val === "object" ? val.label : val);
- }));
- if (typeof(this.timezone) !== "undefined" && this.timezone !== null && this.timezone !== "") {
- var local_timezone = (new Date(this.inst.selectedYear, this.inst.selectedMonth, this.inst.selectedDay, 12)).getTimezoneOffset() * -1;
- if (local_timezone === this.timezone) {
- selectLocalTimezone(tp_inst);
- } else {
- this.timezone_select.val(this.timezone);
- }
- } else {
- if (typeof(this.hour) !== "undefined" && this.hour !== null && this.hour !== "") {
- this.timezone_select.val(o.timezone);
- } else {
- selectLocalTimezone(tp_inst);
- }
- }
- this.timezone_select.change(function () {
- tp_inst._onTimeChange();
- tp_inst._onSelectHandler();
- tp_inst._afterInject();
- });
- // End timezone options
-
- // inject timepicker into datepicker
- var $buttonPanel = $dp.find('.ui-datepicker-buttonpane');
- if ($buttonPanel.length) {
- $buttonPanel.before($tp);
- } else {
- $dp.append($tp);
- }
-
- this.$timeObj = $tp.find('.ui_tpicker_time');
-
- if (this.inst !== null) {
- var timeDefined = this.timeDefined;
- this._onTimeChange();
- this.timeDefined = timeDefined;
- }
-
- // slideAccess integration: http://trentrichardson.com/2011/11/11/jquery-ui-sliders-and-touch-accessibility/
- if (this._defaults.addSliderAccess) {
- var sliderAccessArgs = this._defaults.sliderAccessArgs,
- rtl = this._defaults.isRTL;
- sliderAccessArgs.isRTL = rtl;
-
- setTimeout(function () { // fix for inline mode
- if ($tp.find('.ui-slider-access').length === 0) {
- $tp.find('.ui-slider:visible').sliderAccess(sliderAccessArgs);
-
- // fix any grids since sliders are shorter
- var sliderAccessWidth = $tp.find('.ui-slider-access:eq(0)').outerWidth(true);
- if (sliderAccessWidth) {
- $tp.find('table:visible').each(function () {
- var $g = $(this),
- oldWidth = $g.outerWidth(),
- oldMarginLeft = $g.css(rtl ? 'marginRight' : 'marginLeft').toString().replace('%', ''),
- newWidth = oldWidth - sliderAccessWidth,
- newMarginLeft = ((oldMarginLeft * newWidth) / oldWidth) + '%',
- css = { width: newWidth, marginRight: 0, marginLeft: 0 };
- css[rtl ? 'marginRight' : 'marginLeft'] = newMarginLeft;
- $g.css(css);
- });
- }
- }
- }, 10);
- }
- // end slideAccess integration
-
- tp_inst._limitMinMaxDateTime(this.inst, true);
- }
- },
-
- /*
- * This function tries to limit the ability to go outside the
- * min/max date range
- */
- _limitMinMaxDateTime: function (dp_inst, adjustSliders) {
- var o = this._defaults,
- dp_date = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay);
-
- if (!this._defaults.showTimepicker) {
- return;
- } // No time so nothing to check here
-
- if ($.datepicker._get(dp_inst, 'minDateTime') !== null && $.datepicker._get(dp_inst, 'minDateTime') !== undefined && dp_date) {
- var minDateTime = $.datepicker._get(dp_inst, 'minDateTime'),
- minDateTimeDate = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), 0, 0, 0, 0);
-
- if (this.hourMinOriginal === null || this.minuteMinOriginal === null || this.secondMinOriginal === null || this.millisecMinOriginal === null || this.microsecMinOriginal === null) {
- this.hourMinOriginal = o.hourMin;
- this.minuteMinOriginal = o.minuteMin;
- this.secondMinOriginal = o.secondMin;
- this.millisecMinOriginal = o.millisecMin;
- this.microsecMinOriginal = o.microsecMin;
- }
-
- if (dp_inst.settings.timeOnly || minDateTimeDate.getTime() === dp_date.getTime()) {
- this._defaults.hourMin = minDateTime.getHours();
- if (this.hour <= this._defaults.hourMin) {
- this.hour = this._defaults.hourMin;
- this._defaults.minuteMin = minDateTime.getMinutes();
- if (this.minute <= this._defaults.minuteMin) {
- this.minute = this._defaults.minuteMin;
- this._defaults.secondMin = minDateTime.getSeconds();
- if (this.second <= this._defaults.secondMin) {
- this.second = this._defaults.secondMin;
- this._defaults.millisecMin = minDateTime.getMilliseconds();
- if (this.millisec <= this._defaults.millisecMin) {
- this.millisec = this._defaults.millisecMin;
- this._defaults.microsecMin = minDateTime.getMicroseconds();
- } else {
- if (this.microsec < this._defaults.microsecMin) {
- this.microsec = this._defaults.microsecMin;
- }
- this._defaults.microsecMin = this.microsecMinOriginal;
- }
- } else {
- this._defaults.millisecMin = this.millisecMinOriginal;
- this._defaults.microsecMin = this.microsecMinOriginal;
- }
- } else {
- this._defaults.secondMin = this.secondMinOriginal;
- this._defaults.millisecMin = this.millisecMinOriginal;
- this._defaults.microsecMin = this.microsecMinOriginal;
- }
- } else {
- this._defaults.minuteMin = this.minuteMinOriginal;
- this._defaults.secondMin = this.secondMinOriginal;
- this._defaults.millisecMin = this.millisecMinOriginal;
- this._defaults.microsecMin = this.microsecMinOriginal;
- }
- } else {
- this._defaults.hourMin = this.hourMinOriginal;
- this._defaults.minuteMin = this.minuteMinOriginal;
- this._defaults.secondMin = this.secondMinOriginal;
- this._defaults.millisecMin = this.millisecMinOriginal;
- this._defaults.microsecMin = this.microsecMinOriginal;
- }
- }
-
- if ($.datepicker._get(dp_inst, 'maxDateTime') !== null && $.datepicker._get(dp_inst, 'maxDateTime') !== undefined && dp_date) {
- var maxDateTime = $.datepicker._get(dp_inst, 'maxDateTime'),
- maxDateTimeDate = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), 0, 0, 0, 0);
-
- if (this.hourMaxOriginal === null || this.minuteMaxOriginal === null || this.secondMaxOriginal === null || this.millisecMaxOriginal === null) {
- this.hourMaxOriginal = o.hourMax;
- this.minuteMaxOriginal = o.minuteMax;
- this.secondMaxOriginal = o.secondMax;
- this.millisecMaxOriginal = o.millisecMax;
- this.microsecMaxOriginal = o.microsecMax;
- }
-
- if (dp_inst.settings.timeOnly || maxDateTimeDate.getTime() === dp_date.getTime()) {
- this._defaults.hourMax = maxDateTime.getHours();
- if (this.hour >= this._defaults.hourMax) {
- this.hour = this._defaults.hourMax;
- this._defaults.minuteMax = maxDateTime.getMinutes();
- if (this.minute >= this._defaults.minuteMax) {
- this.minute = this._defaults.minuteMax;
- this._defaults.secondMax = maxDateTime.getSeconds();
- if (this.second >= this._defaults.secondMax) {
- this.second = this._defaults.secondMax;
- this._defaults.millisecMax = maxDateTime.getMilliseconds();
- if (this.millisec >= this._defaults.millisecMax) {
- this.millisec = this._defaults.millisecMax;
- this._defaults.microsecMax = maxDateTime.getMicroseconds();
- } else {
- if (this.microsec > this._defaults.microsecMax) {
- this.microsec = this._defaults.microsecMax;
- }
- this._defaults.microsecMax = this.microsecMaxOriginal;
- }
- } else {
- this._defaults.millisecMax = this.millisecMaxOriginal;
- this._defaults.microsecMax = this.microsecMaxOriginal;
- }
- } else {
- this._defaults.secondMax = this.secondMaxOriginal;
- this._defaults.millisecMax = this.millisecMaxOriginal;
- this._defaults.microsecMax = this.microsecMaxOriginal;
- }
- } else {
- this._defaults.minuteMax = this.minuteMaxOriginal;
- this._defaults.secondMax = this.secondMaxOriginal;
- this._defaults.millisecMax = this.millisecMaxOriginal;
- this._defaults.microsecMax = this.microsecMaxOriginal;
- }
- } else {
- this._defaults.hourMax = this.hourMaxOriginal;
- this._defaults.minuteMax = this.minuteMaxOriginal;
- this._defaults.secondMax = this.secondMaxOriginal;
- this._defaults.millisecMax = this.millisecMaxOriginal;
- this._defaults.microsecMax = this.microsecMaxOriginal;
- }
- }
-
- if (dp_inst.settings.minTime!==null) {
- var tempMinTime=new Date("01/01/1970 " + dp_inst.settings.minTime);
- if (this.hourtempMaxTime.getHours()) {
- this.hour=this._defaults.hourMax=tempMaxTime.getHours();
- this.minute=this._defaults.minuteMax=tempMaxTime.getMinutes();
- } else if (this.hour===tempMaxTime.getHours() && this.minute>tempMaxTime.getMinutes()) {
- this.minute=this._defaults.minuteMax=tempMaxTime.getMinutes();
- } else {
- if (this._defaults.hourMax>tempMaxTime.getHours()) {
- this._defaults.hourMax=tempMaxTime.getHours();
- this._defaults.minuteMax=tempMaxTime.getMinutes();
- } else if (this._defaults.hourMax===tempMaxTime.getHours()===this.hour && this._defaults.minuteMax>tempMaxTime.getMinutes()) {
- this._defaults.minuteMax=tempMaxTime.getMinutes();
- } else {
- this._defaults.minuteMax=59;
- }
- }
- }
-
- if (adjustSliders !== undefined && adjustSliders === true) {
- var hourMax = parseInt((this._defaults.hourMax - ((this._defaults.hourMax - this._defaults.hourMin) % this._defaults.stepHour)), 10),
- minMax = parseInt((this._defaults.minuteMax - ((this._defaults.minuteMax - this._defaults.minuteMin) % this._defaults.stepMinute)), 10),
- secMax = parseInt((this._defaults.secondMax - ((this._defaults.secondMax - this._defaults.secondMin) % this._defaults.stepSecond)), 10),
- millisecMax = parseInt((this._defaults.millisecMax - ((this._defaults.millisecMax - this._defaults.millisecMin) % this._defaults.stepMillisec)), 10),
- microsecMax = parseInt((this._defaults.microsecMax - ((this._defaults.microsecMax - this._defaults.microsecMin) % this._defaults.stepMicrosec)), 10);
-
- if (this.hour_slider) {
- this.control.options(this, this.hour_slider, 'hour', { min: this._defaults.hourMin, max: hourMax, step: this._defaults.stepHour });
- this.control.value(this, this.hour_slider, 'hour', this.hour - (this.hour % this._defaults.stepHour));
- }
- if (this.minute_slider) {
- this.control.options(this, this.minute_slider, 'minute', { min: this._defaults.minuteMin, max: minMax, step: this._defaults.stepMinute });
- this.control.value(this, this.minute_slider, 'minute', this.minute - (this.minute % this._defaults.stepMinute));
- }
- if (this.second_slider) {
- this.control.options(this, this.second_slider, 'second', { min: this._defaults.secondMin, max: secMax, step: this._defaults.stepSecond });
- this.control.value(this, this.second_slider, 'second', this.second - (this.second % this._defaults.stepSecond));
- }
- if (this.millisec_slider) {
- this.control.options(this, this.millisec_slider, 'millisec', { min: this._defaults.millisecMin, max: millisecMax, step: this._defaults.stepMillisec });
- this.control.value(this, this.millisec_slider, 'millisec', this.millisec - (this.millisec % this._defaults.stepMillisec));
- }
- if (this.microsec_slider) {
- this.control.options(this, this.microsec_slider, 'microsec', { min: this._defaults.microsecMin, max: microsecMax, step: this._defaults.stepMicrosec });
- this.control.value(this, this.microsec_slider, 'microsec', this.microsec - (this.microsec % this._defaults.stepMicrosec));
- }
- }
-
- },
-
- /*
- * when a slider moves, set the internal time...
- * on time change is also called when the time is updated in the text field
- */
- _onTimeChange: function () {
- if (!this._defaults.showTimepicker) {
- return;
- }
- var hour = (this.hour_slider) ? this.control.value(this, this.hour_slider, 'hour') : false,
- minute = (this.minute_slider) ? this.control.value(this, this.minute_slider, 'minute') : false,
- second = (this.second_slider) ? this.control.value(this, this.second_slider, 'second') : false,
- millisec = (this.millisec_slider) ? this.control.value(this, this.millisec_slider, 'millisec') : false,
- microsec = (this.microsec_slider) ? this.control.value(this, this.microsec_slider, 'microsec') : false,
- timezone = (this.timezone_select) ? this.timezone_select.val() : false,
- o = this._defaults,
- pickerTimeFormat = o.pickerTimeFormat || o.timeFormat,
- pickerTimeSuffix = o.pickerTimeSuffix || o.timeSuffix;
-
- if (typeof(hour) === 'object') {
- hour = false;
- }
- if (typeof(minute) === 'object') {
- minute = false;
- }
- if (typeof(second) === 'object') {
- second = false;
- }
- if (typeof(millisec) === 'object') {
- millisec = false;
- }
- if (typeof(microsec) === 'object') {
- microsec = false;
- }
- if (typeof(timezone) === 'object') {
- timezone = false;
- }
-
- if (hour !== false) {
- hour = parseInt(hour, 10);
- }
- if (minute !== false) {
- minute = parseInt(minute, 10);
- }
- if (second !== false) {
- second = parseInt(second, 10);
- }
- if (millisec !== false) {
- millisec = parseInt(millisec, 10);
- }
- if (microsec !== false) {
- microsec = parseInt(microsec, 10);
- }
- if (timezone !== false) {
- timezone = timezone.toString();
- }
-
- var ampm = o[hour < 12 ? 'amNames' : 'pmNames'][0];
-
- // If the update was done in the input field, the input field should not be updated.
- // If the update was done using the sliders, update the input field.
- var hasChanged = (
- hour !== parseInt(this.hour,10) || // sliders should all be numeric
- minute !== parseInt(this.minute,10) ||
- second !== parseInt(this.second,10) ||
- millisec !== parseInt(this.millisec,10) ||
- microsec !== parseInt(this.microsec,10) ||
- (this.ampm.length > 0 && (hour < 12) !== ($.inArray(this.ampm.toUpperCase(), this.amNames) !== -1)) ||
- (this.timezone !== null && timezone !== this.timezone.toString()) // could be numeric or "EST" format, so use toString()
- );
-
- if (hasChanged) {
-
- if (hour !== false) {
- this.hour = hour;
- }
- if (minute !== false) {
- this.minute = minute;
- }
- if (second !== false) {
- this.second = second;
- }
- if (millisec !== false) {
- this.millisec = millisec;
- }
- if (microsec !== false) {
- this.microsec = microsec;
- }
- if (timezone !== false) {
- this.timezone = timezone;
- }
-
- if (!this.inst) {
- this.inst = $.datepicker._getInst(this.$input[0]);
- }
-
- this._limitMinMaxDateTime(this.inst, true);
- }
- if (this.support.ampm) {
- this.ampm = ampm;
- }
-
- // Updates the time within the timepicker
- this.formattedTime = $.datepicker.formatTime(o.timeFormat, this, o);
- if (this.$timeObj) {
- if (pickerTimeFormat === o.timeFormat) {
- this.$timeObj.text(this.formattedTime + pickerTimeSuffix);
- }
- else {
- this.$timeObj.text($.datepicker.formatTime(pickerTimeFormat, this, o) + pickerTimeSuffix);
- }
- }
-
- this.timeDefined = true;
- if (hasChanged) {
- this._updateDateTime();
- //this.$input.focus(); // may automatically open the picker on setDate
- }
- },
-
- /*
- * call custom onSelect.
- * bind to sliders slidestop, and grid click.
- */
- _onSelectHandler: function () {
- var onSelect = this._defaults.onSelect || this.inst.settings.onSelect;
- var inputEl = this.$input ? this.$input[0] : null;
- if (onSelect && inputEl) {
- onSelect.apply(inputEl, [this.formattedDateTime, this]);
- }
- },
-
- /*
- * update our input with the new date time..
- */
- _updateDateTime: function (dp_inst) {
- dp_inst = this.inst || dp_inst;
- var dtTmp = (dp_inst.currentYear > 0?
- new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay) :
- new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)),
- dt = $.datepicker._daylightSavingAdjust(dtTmp),
- //dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)),
- //dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay)),
- dateFmt = $.datepicker._get(dp_inst, 'dateFormat'),
- formatCfg = $.datepicker._getFormatConfig(dp_inst),
- timeAvailable = dt !== null && this.timeDefined;
- this.formattedDate = $.datepicker.formatDate(dateFmt, (dt === null ? new Date() : dt), formatCfg);
- var formattedDateTime = this.formattedDate;
-
- // if a slider was changed but datepicker doesn't have a value yet, set it
- if (dp_inst.lastVal === "") {
- dp_inst.currentYear = dp_inst.selectedYear;
- dp_inst.currentMonth = dp_inst.selectedMonth;
- dp_inst.currentDay = dp_inst.selectedDay;
- }
-
- /*
- * remove following lines to force every changes in date picker to change the input value
- * Bug descriptions: when an input field has a default value, and click on the field to pop up the date picker.
- * If the user manually empty the value in the input field, the date picker will never change selected value.
- */
- //if (dp_inst.lastVal !== undefined && (dp_inst.lastVal.length > 0 && this.$input.val().length === 0)) {
- // return;
- //}
-
- if (this._defaults.timeOnly === true && this._defaults.timeOnlyShowDate === false) {
- formattedDateTime = this.formattedTime;
- } else if ((this._defaults.timeOnly !== true && (this._defaults.alwaysSetTime || timeAvailable)) || (this._defaults.timeOnly === true && this._defaults.timeOnlyShowDate === true)) {
- formattedDateTime += this._defaults.separator + this.formattedTime + this._defaults.timeSuffix;
- }
-
- this.formattedDateTime = formattedDateTime;
-
- if (!this._defaults.showTimepicker) {
- this.$input.val(this.formattedDate);
- } else if (this.$altInput && this._defaults.timeOnly === false && this._defaults.altFieldTimeOnly === true) {
- this.$altInput.val(this.formattedTime);
- this.$input.val(this.formattedDate);
- } else if (this.$altInput) {
- this.$input.val(formattedDateTime);
- var altFormattedDateTime = '',
- altSeparator = this._defaults.altSeparator !== null ? this._defaults.altSeparator : this._defaults.separator,
- altTimeSuffix = this._defaults.altTimeSuffix !== null ? this._defaults.altTimeSuffix : this._defaults.timeSuffix;
-
- if (!this._defaults.timeOnly) {
- if (this._defaults.altFormat) {
- altFormattedDateTime = $.datepicker.formatDate(this._defaults.altFormat, (dt === null ? new Date() : dt), formatCfg);
- }
- else {
- altFormattedDateTime = this.formattedDate;
- }
-
- if (altFormattedDateTime) {
- altFormattedDateTime += altSeparator;
- }
- }
-
- if (this._defaults.altTimeFormat !== null) {
- altFormattedDateTime += $.datepicker.formatTime(this._defaults.altTimeFormat, this, this._defaults) + altTimeSuffix;
- }
- else {
- altFormattedDateTime += this.formattedTime + altTimeSuffix;
- }
- this.$altInput.val(altFormattedDateTime);
- } else {
- this.$input.val(formattedDateTime);
- }
-
- this.$input.trigger("change");
- },
-
- _onFocus: function () {
- if (!this.$input.val() && this._defaults.defaultValue) {
- this.$input.val(this._defaults.defaultValue);
- var inst = $.datepicker._getInst(this.$input.get(0)),
- tp_inst = $.datepicker._get(inst, 'timepicker');
- if (tp_inst) {
- if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) {
- try {
- $.datepicker._updateDatepicker(inst);
- } catch (err) {
- $.timepicker.log(err);
- }
- }
- }
- }
- },
-
- /*
- * Small abstraction to control types
- * We can add more, just be sure to follow the pattern: create, options, value
- */
- _controls: {
- // slider methods
- slider: {
- create: function (tp_inst, obj, unit, val, min, max, step) {
- var rtl = tp_inst._defaults.isRTL; // if rtl go -60->0 instead of 0->60
- return obj.prop('slide', null).slider({
- orientation: "horizontal",
- value: rtl ? val * -1 : val,
- min: rtl ? max * -1 : min,
- max: rtl ? min * -1 : max,
- step: step,
- slide: function (event, ui) {
- tp_inst.control.value(tp_inst, $(this), unit, rtl ? ui.value * -1 : ui.value);
- tp_inst._onTimeChange();
- },
- stop: function (event, ui) {
- tp_inst._onSelectHandler();
- }
- });
- },
- options: function (tp_inst, obj, unit, opts, val) {
- if (tp_inst._defaults.isRTL) {
- if (typeof(opts) === 'string') {
- if (opts === 'min' || opts === 'max') {
- if (val !== undefined) {
- return obj.slider(opts, val * -1);
- }
- return Math.abs(obj.slider(opts));
- }
- return obj.slider(opts);
- }
- var min = opts.min,
- max = opts.max;
- opts.min = opts.max = null;
- if (min !== undefined) {
- opts.max = min * -1;
- }
- if (max !== undefined) {
- opts.min = max * -1;
- }
- return obj.slider(opts);
- }
- if (typeof(opts) === 'string' && val !== undefined) {
- return obj.slider(opts, val);
- }
- return obj.slider(opts);
- },
- value: function (tp_inst, obj, unit, val) {
- if (tp_inst._defaults.isRTL) {
- if (val !== undefined) {
- return obj.slider('value', val * -1);
- }
- return Math.abs(obj.slider('value'));
- }
- if (val !== undefined) {
- return obj.slider('value', val);
- }
- return obj.slider('value');
- }
- },
- // select methods
- select: {
- create: function (tp_inst, obj, unit, val, min, max, step) {
- var sel = '';
-
- obj.children('select').remove();
-
- $(sel).appendTo(obj).change(function (e) {
- tp_inst._onTimeChange();
- tp_inst._onSelectHandler();
- tp_inst._afterInject();
- });
-
- return obj;
- },
- options: function (tp_inst, obj, unit, opts, val) {
- var o = {},
- $t = obj.children('select');
- if (typeof(opts) === 'string') {
- if (val === undefined) {
- return $t.data(opts);
- }
- o[opts] = val;
- }
- else { o = opts; }
- return tp_inst.control.create(tp_inst, obj, $t.data('unit'), $t.val(), o.min>=0 ? o.min : $t.data('min'), o.max || $t.data('max'), o.step || $t.data('step'));
- },
- value: function (tp_inst, obj, unit, val) {
- var $t = obj.children('select');
- if (val !== undefined) {
- return $t.val(val);
- }
- return $t.val();
- }
- }
- } // end _controls
-
- });
-
- $.fn.extend({
- /*
- * shorthand just to use timepicker.
- */
- timepicker: function (o) {
- o = o || {};
- var tmp_args = Array.prototype.slice.call(arguments);
-
- if (typeof o === 'object') {
- tmp_args[0] = $.extend(o, {
- timeOnly: true
- });
- }
-
- return $(this).each(function () {
- $.fn.datetimepicker.apply($(this), tmp_args);
- });
- },
-
- /*
- * extend timepicker to datepicker
- */
- datetimepicker: function (o) {
- o = o || {};
- var tmp_args = arguments;
-
- if (typeof(o) === 'string') {
- if (o === 'getDate' || (o === 'option' && tmp_args.length === 2 && typeof (tmp_args[1]) === 'string')) {
- return $.fn.datepicker.apply($(this[0]), tmp_args);
- } else {
- return this.each(function () {
- var $t = $(this);
- $t.datepicker.apply($t, tmp_args);
- });
- }
- } else {
- return this.each(function () {
- var $t = $(this);
- $t.datepicker($.timepicker._newInst($t, o)._defaults);
- });
- }
- }
- });
-
- /*
- * Public Utility to parse date and time
- */
- $.datepicker.parseDateTime = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) {
- var parseRes = parseDateTimeInternal(dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings);
- if (parseRes.timeObj) {
- var t = parseRes.timeObj;
- parseRes.date.setHours(t.hour, t.minute, t.second, t.millisec);
- parseRes.date.setMicroseconds(t.microsec);
- }
-
- return parseRes.date;
- };
-
- /*
- * Public utility to parse time
- */
- $.datepicker.parseTime = function (timeFormat, timeString, options) {
- var o = extendRemove(extendRemove({}, $.timepicker._defaults), options || {}),
- iso8601 = (timeFormat.replace(/\'.*?\'/g, '').indexOf('Z') !== -1);
-
- // Strict parse requires the timeString to match the timeFormat exactly
- var strictParse = function (f, s, o) {
-
- // pattern for standard and localized AM/PM markers
- var getPatternAmpm = function (amNames, pmNames) {
- var markers = [];
- if (amNames) {
- $.merge(markers, amNames);
- }
- if (pmNames) {
- $.merge(markers, pmNames);
- }
- markers = $.map(markers, function (val) {
- return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&');
- });
- return '(' + markers.join('|') + ')?';
- };
-
- // figure out position of time elements.. cause js cant do named captures
- var getFormatPositions = function (timeFormat) {
- var finds = timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|c{1}|t{1,2}|z|'.*?')/g),
- orders = {
- h: -1,
- m: -1,
- s: -1,
- l: -1,
- c: -1,
- t: -1,
- z: -1
- };
-
- if (finds) {
- for (var i = 0; i < finds.length; i++) {
- if (orders[finds[i].toString().charAt(0)] === -1) {
- orders[finds[i].toString().charAt(0)] = i + 1;
- }
- }
- }
- return orders;
- };
-
- var regstr = '^' + f.toString()
- .replace(/([hH]{1,2}|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) {
- var ml = match.length;
- switch (match.charAt(0).toLowerCase()) {
- case 'h':
- return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})';
- case 'm':
- return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})';
- case 's':
- return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})';
- case 'l':
- return '(\\d?\\d?\\d)';
- case 'c':
- return '(\\d?\\d?\\d)';
- case 'z':
- return '(z|[-+]\\d\\d:?\\d\\d|\\S+)?';
- case 't':
- return getPatternAmpm(o.amNames, o.pmNames);
- default: // literal escaped in quotes
- return '(' + match.replace(/\'/g, "").replace(/(\.|\$|\^|\\|\/|\(|\)|\[|\]|\?|\+|\*)/g, function (m) { return "\\" + m; }) + ')?';
- }
- })
- .replace(/\s/g, '\\s?') +
- o.timeSuffix + '$',
- order = getFormatPositions(f),
- ampm = '',
- treg;
-
- treg = s.match(new RegExp(regstr, 'i'));
-
- var resTime = {
- hour: 0,
- minute: 0,
- second: 0,
- millisec: 0,
- microsec: 0
- };
-
- if (treg) {
- if (order.t !== -1) {
- if (treg[order.t] === undefined || treg[order.t].length === 0) {
- ampm = '';
- resTime.ampm = '';
- } else {
- ampm = $.inArray(treg[order.t].toUpperCase(), $.map(o.amNames, function (x,i) { return x.toUpperCase(); })) !== -1 ? 'AM' : 'PM';
- resTime.ampm = o[ampm === 'AM' ? 'amNames' : 'pmNames'][0];
- }
- }
-
- if (order.h !== -1) {
- if (ampm === 'AM' && treg[order.h] === '12') {
- resTime.hour = 0; // 12am = 0 hour
- } else {
- if (ampm === 'PM' && treg[order.h] !== '12') {
- resTime.hour = parseInt(treg[order.h], 10) + 12; // 12pm = 12 hour, any other pm = hour + 12
- } else {
- resTime.hour = Number(treg[order.h]);
- }
- }
- }
-
- if (order.m !== -1) {
- resTime.minute = Number(treg[order.m]);
- }
- if (order.s !== -1) {
- resTime.second = Number(treg[order.s]);
- }
- if (order.l !== -1) {
- resTime.millisec = Number(treg[order.l]);
- }
- if (order.c !== -1) {
- resTime.microsec = Number(treg[order.c]);
- }
- if (order.z !== -1 && treg[order.z] !== undefined) {
- resTime.timezone = $.timepicker.timezoneOffsetNumber(treg[order.z]);
- }
-
-
- return resTime;
- }
- return false;
- };// end strictParse
-
- // First try JS Date, if that fails, use strictParse
- var looseParse = function (f, s, o) {
- try {
- var d = new Date('2012-01-01 ' + s);
- if (isNaN(d.getTime())) {
- d = new Date('2012-01-01T' + s);
- if (isNaN(d.getTime())) {
- d = new Date('01/01/2012 ' + s);
- if (isNaN(d.getTime())) {
- throw "Unable to parse time with native Date: " + s;
- }
- }
- }
-
- return {
- hour: d.getHours(),
- minute: d.getMinutes(),
- second: d.getSeconds(),
- millisec: d.getMilliseconds(),
- microsec: d.getMicroseconds(),
- timezone: d.getTimezoneOffset() * -1
- };
- }
- catch (err) {
- try {
- return strictParse(f, s, o);
- }
- catch (err2) {
- $.timepicker.log("Unable to parse \ntimeString: " + s + "\ntimeFormat: " + f);
- }
- }
- return false;
- }; // end looseParse
-
- if (typeof o.parse === "function") {
- return o.parse(timeFormat, timeString, o);
- }
- if (o.parse === 'loose') {
- return looseParse(timeFormat, timeString, o);
- }
- return strictParse(timeFormat, timeString, o);
- };
-
- /**
- * Public utility to format the time
- * @param {string} format format of the time
- * @param {Object} time Object not a Date for timezones
- * @param {Object} [options] essentially the regional[].. amNames, pmNames, ampm
- * @returns {string} the formatted time
- */
- $.datepicker.formatTime = function (format, time, options) {
- options = options || {};
- options = $.extend({}, $.timepicker._defaults, options);
- time = $.extend({
- hour: 0,
- minute: 0,
- second: 0,
- millisec: 0,
- microsec: 0,
- timezone: null
- }, time);
-
- var tmptime = format,
- ampmName = options.amNames[0],
- hour = parseInt(time.hour, 10);
-
- if (hour > 11) {
- ampmName = options.pmNames[0];
- }
-
- tmptime = tmptime.replace(/(?:HH?|hh?|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) {
- switch (match) {
- case 'HH':
- return ('0' + hour).slice(-2);
- case 'H':
- return hour;
- case 'hh':
- return ('0' + convert24to12(hour)).slice(-2);
- case 'h':
- return convert24to12(hour);
- case 'mm':
- return ('0' + time.minute).slice(-2);
- case 'm':
- return time.minute;
- case 'ss':
- return ('0' + time.second).slice(-2);
- case 's':
- return time.second;
- case 'l':
- return ('00' + time.millisec).slice(-3);
- case 'c':
- return ('00' + time.microsec).slice(-3);
- case 'z':
- return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, false);
- case 'Z':
- return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, true);
- case 'T':
- return ampmName.charAt(0).toUpperCase();
- case 'TT':
- return ampmName.toUpperCase();
- case 't':
- return ampmName.charAt(0).toLowerCase();
- case 'tt':
- return ampmName.toLowerCase();
- default:
- return match.replace(/'/g, "");
- }
- });
-
- return tmptime;
- };
-
- /*
- * the bad hack :/ override datepicker so it doesn't close on select
- // inspired: http://stackoverflow.com/questions/1252512/jquery-datepicker-prevent-closing-picker-when-clicking-a-date/1762378#1762378
- */
- $.datepicker._base_selectDate = $.datepicker._selectDate;
- $.datepicker._selectDate = function (id, dateStr) {
- var inst = this._getInst($(id)[0]),
- tp_inst = this._get(inst, 'timepicker'),
- was_inline;
-
- if (tp_inst && inst.settings.showTimepicker) {
- tp_inst._limitMinMaxDateTime(inst, true);
- was_inline = inst.inline;
- inst.inline = inst.stay_open = true;
- //This way the onSelect handler called from calendarpicker get the full dateTime
- this._base_selectDate(id, dateStr);
- inst.inline = was_inline;
- inst.stay_open = false;
- this._notifyChange(inst);
- this._updateDatepicker(inst);
- } else {
- this._base_selectDate(id, dateStr);
- }
- };
-
- /*
- * second bad hack :/ override datepicker so it triggers an event when changing the input field
- * and does not redraw the datepicker on every selectDate event
- */
- $.datepicker._base_updateDatepicker = $.datepicker._updateDatepicker;
- $.datepicker._updateDatepicker = function (inst) {
-
- // don't popup the datepicker if there is another instance already opened
- var input = inst.input[0];
- if ($.datepicker._curInst && $.datepicker._curInst !== inst && $.datepicker._datepickerShowing && $.datepicker._lastInput !== input) {
- return;
- }
-
- if (typeof(inst.stay_open) !== 'boolean' || inst.stay_open === false) {
-
- this._base_updateDatepicker(inst);
-
- // Reload the time control when changing something in the input text field.
- var tp_inst = this._get(inst, 'timepicker');
- if (tp_inst) {
- tp_inst._addTimePicker(inst);
- }
- }
- };
-
- /*
- * third bad hack :/ override datepicker so it allows spaces and colon in the input field
- */
- $.datepicker._base_doKeyPress = $.datepicker._doKeyPress;
- $.datepicker._doKeyPress = function (event) {
- var inst = $.datepicker._getInst(event.target),
- tp_inst = $.datepicker._get(inst, 'timepicker');
-
- if (tp_inst) {
- if ($.datepicker._get(inst, 'constrainInput')) {
- var ampm = tp_inst.support.ampm,
- tz = tp_inst._defaults.showTimezone !== null ? tp_inst._defaults.showTimezone : tp_inst.support.timezone,
- dateChars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')),
- datetimeChars = tp_inst._defaults.timeFormat.toString()
- .replace(/[hms]/g, '')
- .replace(/TT/g, ampm ? 'APM' : '')
- .replace(/Tt/g, ampm ? 'AaPpMm' : '')
- .replace(/tT/g, ampm ? 'AaPpMm' : '')
- .replace(/T/g, ampm ? 'AP' : '')
- .replace(/tt/g, ampm ? 'apm' : '')
- .replace(/t/g, ampm ? 'ap' : '') +
- " " + tp_inst._defaults.separator +
- tp_inst._defaults.timeSuffix +
- (tz ? tp_inst._defaults.timezoneList.join('') : '') +
- (tp_inst._defaults.amNames.join('')) + (tp_inst._defaults.pmNames.join('')) +
- dateChars,
- chr = String.fromCharCode(event.charCode === undefined ? event.keyCode : event.charCode);
- return event.ctrlKey || (chr < ' ' || !dateChars || datetimeChars.indexOf(chr) > -1);
- }
- }
-
- return $.datepicker._base_doKeyPress(event);
- };
-
- /*
- * Fourth bad hack :/ override _updateAlternate function used in inline mode to init altField
- * Update any alternate field to synchronise with the main field.
- */
- $.datepicker._base_updateAlternate = $.datepicker._updateAlternate;
- $.datepicker._updateAlternate = function (inst) {
- var tp_inst = this._get(inst, 'timepicker');
- if (tp_inst) {
- var altField = tp_inst._defaults.altField;
- if (altField) { // update alternate field too
- var altFormat = tp_inst._defaults.altFormat || tp_inst._defaults.dateFormat,
- date = this._getDate(inst),
- formatCfg = $.datepicker._getFormatConfig(inst),
- altFormattedDateTime = '',
- altSeparator = tp_inst._defaults.altSeparator ? tp_inst._defaults.altSeparator : tp_inst._defaults.separator,
- altTimeSuffix = tp_inst._defaults.altTimeSuffix ? tp_inst._defaults.altTimeSuffix : tp_inst._defaults.timeSuffix,
- altTimeFormat = tp_inst._defaults.altTimeFormat !== null ? tp_inst._defaults.altTimeFormat : tp_inst._defaults.timeFormat;
-
- altFormattedDateTime += $.datepicker.formatTime(altTimeFormat, tp_inst, tp_inst._defaults) + altTimeSuffix;
- if (!tp_inst._defaults.timeOnly && !tp_inst._defaults.altFieldTimeOnly && date !== null) {
- if (tp_inst._defaults.altFormat) {
- altFormattedDateTime = $.datepicker.formatDate(tp_inst._defaults.altFormat, date, formatCfg) + altSeparator + altFormattedDateTime;
- }
- else {
- altFormattedDateTime = tp_inst.formattedDate + altSeparator + altFormattedDateTime;
- }
- }
- $(altField).val( inst.input.val() ? altFormattedDateTime : "");
- }
- }
- else {
- $.datepicker._base_updateAlternate(inst);
- }
- };
-
- /*
- * Override key up event to sync manual input changes.
- */
- $.datepicker._base_doKeyUp = $.datepicker._doKeyUp;
- $.datepicker._doKeyUp = function (event) {
- var inst = $.datepicker._getInst(event.target),
- tp_inst = $.datepicker._get(inst, 'timepicker');
-
- if (tp_inst) {
- if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) {
- try {
- $.datepicker._updateDatepicker(inst);
- } catch (err) {
- $.timepicker.log(err);
- }
- }
- }
-
- return $.datepicker._base_doKeyUp(event);
- };
-
- /*
- * override "Today" button to also grab the time.
- */
- $.datepicker._base_gotoToday = $.datepicker._gotoToday;
- $.datepicker._gotoToday = function (id) {
- var inst = this._getInst($(id)[0]),
- $dp = inst.dpDiv;
- var tp_inst = this._get(inst, 'timepicker');
- selectLocalTimezone(tp_inst);
- var now = new Date();
- this._setTime(inst, now);
- this._setDate(inst, now);
- this._base_gotoToday(id);
- };
-
- /*
- * Disable & enable the Time in the datetimepicker
- */
- $.datepicker._disableTimepickerDatepicker = function (target) {
- var inst = this._getInst(target);
- if (!inst) {
- return;
- }
-
- var tp_inst = this._get(inst, 'timepicker');
- $(target).datepicker('getDate'); // Init selected[Year|Month|Day]
- if (tp_inst) {
- inst.settings.showTimepicker = false;
- tp_inst._defaults.showTimepicker = false;
- tp_inst._updateDateTime(inst);
- }
- };
-
- $.datepicker._enableTimepickerDatepicker = function (target) {
- var inst = this._getInst(target);
- if (!inst) {
- return;
- }
-
- var tp_inst = this._get(inst, 'timepicker');
- $(target).datepicker('getDate'); // Init selected[Year|Month|Day]
- if (tp_inst) {
- inst.settings.showTimepicker = true;
- tp_inst._defaults.showTimepicker = true;
- tp_inst._addTimePicker(inst); // Could be disabled on page load
- tp_inst._updateDateTime(inst);
- }
- };
-
- /*
- * Create our own set time function
- */
- $.datepicker._setTime = function (inst, date) {
- var tp_inst = this._get(inst, 'timepicker');
- if (tp_inst) {
- var defaults = tp_inst._defaults;
-
- // calling _setTime with no date sets time to defaults
- tp_inst.hour = date ? date.getHours() : defaults.hour;
- tp_inst.minute = date ? date.getMinutes() : defaults.minute;
- tp_inst.second = date ? date.getSeconds() : defaults.second;
- tp_inst.millisec = date ? date.getMilliseconds() : defaults.millisec;
- tp_inst.microsec = date ? date.getMicroseconds() : defaults.microsec;
-
- //check if within min/max times..
- tp_inst._limitMinMaxDateTime(inst, true);
-
- tp_inst._onTimeChange();
- tp_inst._updateDateTime(inst);
- }
- };
-
- /*
- * Create new public method to set only time, callable as $().datepicker('setTime', date)
- */
- $.datepicker._setTimeDatepicker = function (target, date, withDate) {
- var inst = this._getInst(target);
- if (!inst) {
- return;
- }
-
- var tp_inst = this._get(inst, 'timepicker');
-
- if (tp_inst) {
- this._setDateFromField(inst);
- var tp_date;
- if (date) {
- if (typeof date === "string") {
- tp_inst._parseTime(date, withDate);
- tp_date = new Date();
- tp_date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
- tp_date.setMicroseconds(tp_inst.microsec);
- } else {
- tp_date = new Date(date.getTime());
- tp_date.setMicroseconds(date.getMicroseconds());
- }
- if (tp_date.toString() === 'Invalid Date') {
- tp_date = undefined;
- }
- this._setTime(inst, tp_date);
- }
- }
-
- };
-
- /*
- * override setDate() to allow setting time too within Date object
- */
- $.datepicker._base_setDateDatepicker = $.datepicker._setDateDatepicker;
- $.datepicker._setDateDatepicker = function (target, _date) {
- var inst = this._getInst(target);
- var date = _date;
- if (!inst) {
- return;
- }
-
- if (typeof(_date) === 'string') {
- date = new Date(_date);
- if (!date.getTime()) {
- this._base_setDateDatepicker.apply(this, arguments);
- date = $(target).datepicker('getDate');
- }
- }
-
- var tp_inst = this._get(inst, 'timepicker');
- var tp_date;
- if (date instanceof Date) {
- tp_date = new Date(date.getTime());
- tp_date.setMicroseconds(date.getMicroseconds());
- } else {
- tp_date = date;
- }
-
- // This is important if you are using the timezone option, javascript's Date
- // object will only return the timezone offset for the current locale, so we
- // adjust it accordingly. If not using timezone option this won't matter..
- // If a timezone is different in tp, keep the timezone as is
- if (tp_inst && tp_date) {
- // look out for DST if tz wasn't specified
- if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) {
- tp_inst.timezone = tp_date.getTimezoneOffset() * -1;
- }
- date = $.timepicker.timezoneAdjust(date, tp_inst.timezone);
- tp_date = $.timepicker.timezoneAdjust(tp_date, tp_inst.timezone);
- }
-
- this._updateDatepicker(inst);
- this._base_setDateDatepicker.apply(this, arguments);
- this._setTimeDatepicker(target, tp_date, true);
- };
-
- /*
- * override getDate() to allow getting time too within Date object
- */
- $.datepicker._base_getDateDatepicker = $.datepicker._getDateDatepicker;
- $.datepicker._getDateDatepicker = function (target, noDefault) {
- var inst = this._getInst(target);
- if (!inst) {
- return;
- }
-
- var tp_inst = this._get(inst, 'timepicker');
-
- if (tp_inst) {
- // if it hasn't yet been defined, grab from field
- if (inst.lastVal === undefined) {
- this._setDateFromField(inst, noDefault);
- }
-
- var date = this._getDate(inst);
- var currDT = $.trim((tp_inst.$altInput && tp_inst._defaults.altFieldTimeOnly) ? tp_inst.$input.val() + ' ' + tp_inst.$altInput.val() : tp_inst.$input.val());
- if (date && tp_inst._parseTime(currDT, !inst.settings.timeOnly)) {
- date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
- date.setMicroseconds(tp_inst.microsec);
-
- // This is important if you are using the timezone option, javascript's Date
- // object will only return the timezone offset for the current locale, so we
- // adjust it accordingly. If not using timezone option this won't matter..
- if (tp_inst.timezone != null) {
- // look out for DST if tz wasn't specified
- if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) {
- tp_inst.timezone = date.getTimezoneOffset() * -1;
- }
- date = $.timepicker.timezoneAdjust(date, tp_inst.timezone);
- }
- }
- return date;
- }
- return this._base_getDateDatepicker(target, noDefault);
- };
-
- /*
- * override parseDate() because UI 1.8.14 throws an error about "Extra characters"
- * An option in datapicker to ignore extra format characters would be nicer.
- */
- $.datepicker._base_parseDate = $.datepicker.parseDate;
- $.datepicker.parseDate = function (format, value, settings) {
- var date;
- try {
- date = this._base_parseDate(format, value, settings);
- } catch (err) {
- // Hack! The error message ends with a colon, a space, and
- // the "extra" characters. We rely on that instead of
- // attempting to perfectly reproduce the parsing algorithm.
- if (err.indexOf(":") >= 0) {
- date = this._base_parseDate(format, value.substring(0, value.length - (err.length - err.indexOf(':') - 2)), settings);
- $.timepicker.log("Error parsing the date string: " + err + "\ndate string = " + value + "\ndate format = " + format);
- } else {
- throw err;
- }
- }
- return date;
- };
-
- /*
- * override formatDate to set date with time to the input
- */
- $.datepicker._base_formatDate = $.datepicker._formatDate;
- $.datepicker._formatDate = function (inst, day, month, year) {
- var tp_inst = this._get(inst, 'timepicker');
- if (tp_inst) {
- tp_inst._updateDateTime(inst);
- return tp_inst.$input.val();
- }
- return this._base_formatDate(inst);
- };
-
- /*
- * override options setter to add time to maxDate(Time) and minDate(Time). MaxDate
- */
- $.datepicker._base_optionDatepicker = $.datepicker._optionDatepicker;
- $.datepicker._optionDatepicker = function (target, name, value) {
- var inst = this._getInst(target),
- name_clone;
- if (!inst) {
- return null;
- }
-
- var tp_inst = this._get(inst, 'timepicker');
- if (tp_inst) {
- var min = null,
- max = null,
- onselect = null,
- overrides = tp_inst._defaults.evnts,
- fns = {},
- prop,
- ret,
- oldVal,
- $target;
- if (typeof name === 'string') { // if min/max was set with the string
- if (name === 'minDate' || name === 'minDateTime') {
- min = value;
- } else if (name === 'maxDate' || name === 'maxDateTime') {
- max = value;
- } else if (name === 'onSelect') {
- onselect = value;
- } else if (overrides.hasOwnProperty(name)) {
- if (typeof (value) === 'undefined') {
- return overrides[name];
- }
- fns[name] = value;
- name_clone = {}; //empty results in exiting function after overrides updated
- }
- } else if (typeof name === 'object') { //if min/max was set with the JSON
- if (name.minDate) {
- min = name.minDate;
- } else if (name.minDateTime) {
- min = name.minDateTime;
- } else if (name.maxDate) {
- max = name.maxDate;
- } else if (name.maxDateTime) {
- max = name.maxDateTime;
- }
- for (prop in overrides) {
- if (overrides.hasOwnProperty(prop) && name[prop]) {
- fns[prop] = name[prop];
- }
- }
- }
- for (prop in fns) {
- if (fns.hasOwnProperty(prop)) {
- overrides[prop] = fns[prop];
- if (!name_clone) { name_clone = $.extend({}, name); }
- delete name_clone[prop];
- }
- }
- if (name_clone && isEmptyObject(name_clone)) { return; }
- if (min) { //if min was set
- if (min === 0) {
- min = new Date();
- } else {
- min = new Date(min);
- }
- tp_inst._defaults.minDate = min;
- tp_inst._defaults.minDateTime = min;
- } else if (max) { //if max was set
- if (max === 0) {
- max = new Date();
- } else {
- max = new Date(max);
- }
- tp_inst._defaults.maxDate = max;
- tp_inst._defaults.maxDateTime = max;
- } else if (onselect) {
- tp_inst._defaults.onSelect = onselect;
- }
-
- // Datepicker will override our date when we call _base_optionDatepicker when
- // calling minDate/maxDate, so we will first grab the value, call
- // _base_optionDatepicker, then set our value back.
- if(min || max){
- $target = $(target);
- oldVal = $target.datetimepicker('getDate');
- ret = this._base_optionDatepicker.call($.datepicker, target, name_clone || name, value);
- $target.datetimepicker('setDate', oldVal);
- return ret;
- }
- }
- if (value === undefined) {
- return this._base_optionDatepicker.call($.datepicker, target, name);
- }
- return this._base_optionDatepicker.call($.datepicker, target, name_clone || name, value);
- };
-
- /*
- * jQuery isEmptyObject does not check hasOwnProperty - if someone has added to the object prototype,
- * it will return false for all objects
- */
- var isEmptyObject = function (obj) {
- var prop;
- for (prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- return false;
- }
- }
- return true;
- };
-
- /*
- * jQuery extend now ignores nulls!
- */
- var extendRemove = function (target, props) {
- $.extend(target, props);
- for (var name in props) {
- if (props[name] === null || props[name] === undefined) {
- target[name] = props[name];
- }
- }
- return target;
- };
-
- /*
- * Determine by the time format which units are supported
- * Returns an object of booleans for each unit
- */
- var detectSupport = function (timeFormat) {
- var tf = timeFormat.replace(/'.*?'/g, '').toLowerCase(), // removes literals
- isIn = function (f, t) { // does the format contain the token?
- return f.indexOf(t) !== -1 ? true : false;
- };
- return {
- hour: isIn(tf, 'h'),
- minute: isIn(tf, 'm'),
- second: isIn(tf, 's'),
- millisec: isIn(tf, 'l'),
- microsec: isIn(tf, 'c'),
- timezone: isIn(tf, 'z'),
- ampm: isIn(tf, 't') && isIn(timeFormat, 'h'),
- iso8601: isIn(timeFormat, 'Z')
- };
- };
-
- /*
- * Converts 24 hour format into 12 hour
- * Returns 12 hour without leading 0
- */
- var convert24to12 = function (hour) {
- hour %= 12;
-
- if (hour === 0) {
- hour = 12;
- }
-
- return String(hour);
- };
-
- var computeEffectiveSetting = function (settings, property) {
- return settings && settings[property] ? settings[property] : $.timepicker._defaults[property];
- };
-
- /*
- * Splits datetime string into date and time substrings.
- * Throws exception when date can't be parsed
- * Returns {dateString: dateString, timeString: timeString}
- */
- var splitDateTime = function (dateTimeString, timeSettings) {
- // The idea is to get the number separator occurrences in datetime and the time format requested (since time has
- // fewer unknowns, mostly numbers and am/pm). We will use the time pattern to split.
- var separator = computeEffectiveSetting(timeSettings, 'separator'),
- format = computeEffectiveSetting(timeSettings, 'timeFormat'),
- timeParts = format.split(separator), // how many occurrences of separator may be in our format?
- timePartsLen = timeParts.length,
- allParts = dateTimeString.split(separator),
- allPartsLen = allParts.length;
-
- if (allPartsLen > 1) {
- return {
- dateString: allParts.splice(0, allPartsLen - timePartsLen).join(separator),
- timeString: allParts.splice(0, timePartsLen).join(separator)
- };
- }
-
- return {
- dateString: dateTimeString,
- timeString: ''
- };
- };
-
- /*
- * Internal function to parse datetime interval
- * Returns: {date: Date, timeObj: Object}, where
- * date - parsed date without time (type Date)
- * timeObj = {hour: , minute: , second: , millisec: , microsec: } - parsed time. Optional
- */
- var parseDateTimeInternal = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) {
- var date,
- parts,
- parsedTime;
-
- parts = splitDateTime(dateTimeString, timeSettings);
- date = $.datepicker._base_parseDate(dateFormat, parts.dateString, dateSettings);
-
- if (parts.timeString === '') {
- return {
- date: date
- };
- }
-
- parsedTime = $.datepicker.parseTime(timeFormat, parts.timeString, timeSettings);
-
- if (!parsedTime) {
- throw 'Wrong time format';
- }
-
- return {
- date: date,
- timeObj: parsedTime
- };
- };
-
- /*
- * Internal function to set timezone_select to the local timezone
- */
- var selectLocalTimezone = function (tp_inst, date) {
- if (tp_inst && tp_inst.timezone_select) {
- var now = date || new Date();
- tp_inst.timezone_select.val(-now.getTimezoneOffset());
- }
- };
-
- /*
- * Create a Singleton Instance
- */
- $.timepicker = new Timepicker();
-
- /**
- * Get the timezone offset as string from a date object (eg '+0530' for UTC+5.5)
- * @param {number} tzMinutes if not a number, less than -720 (-1200), or greater than 840 (+1400) this value is returned
- * @param {boolean} iso8601 if true formats in accordance to iso8601 "+12:45"
- * @return {string}
- */
- $.timepicker.timezoneOffsetString = function (tzMinutes, iso8601) {
- if (isNaN(tzMinutes) || tzMinutes > 840 || tzMinutes < -720) {
- return tzMinutes;
- }
-
- var off = tzMinutes,
- minutes = off % 60,
- hours = (off - minutes) / 60,
- iso = iso8601 ? ':' : '',
- tz = (off >= 0 ? '+' : '-') + ('0' + Math.abs(hours)).slice(-2) + iso + ('0' + Math.abs(minutes)).slice(-2);
-
- if (tz === '+00:00') {
- return 'Z';
- }
- return tz;
- };
-
- /**
- * Get the number in minutes that represents a timezone string
- * @param {string} tzString formatted like "+0500", "-1245", "Z"
- * @return {number} the offset minutes or the original string if it doesn't match expectations
- */
- $.timepicker.timezoneOffsetNumber = function (tzString) {
- var normalized = tzString.toString().replace(':', ''); // excuse any iso8601, end up with "+1245"
-
- if (normalized.toUpperCase() === 'Z') { // if iso8601 with Z, its 0 minute offset
- return 0;
- }
-
- if (!/^(\-|\+)\d{4}$/.test(normalized)) { // possibly a user defined tz, so just give it back
- return tzString;
- }
-
- return ((normalized.substr(0, 1) === '-' ? -1 : 1) * // plus or minus
- ((parseInt(normalized.substr(1, 2), 10) * 60) + // hours (converted to minutes)
- parseInt(normalized.substr(3, 2), 10))); // minutes
- };
-
- /**
- * No way to set timezone in js Date, so we must adjust the minutes to compensate. (think setDate, getDate)
- * @param {Date} date
- * @param {string} toTimezone formatted like "+0500", "-1245"
- * @return {Date}
- */
- $.timepicker.timezoneAdjust = function (date, toTimezone) {
- var toTz = $.timepicker.timezoneOffsetNumber(toTimezone);
- if (!isNaN(toTz)) {
- date.setMinutes(date.getMinutes() + -date.getTimezoneOffset() - toTz);
- }
- return date;
- };
-
- /**
- * Calls `timepicker()` on the `startTime` and `endTime` elements, and configures them to
- * enforce date range limits.
- * n.b. The input value must be correctly formatted (reformatting is not supported)
- * @param {Element} startTime
- * @param {Element} endTime
- * @param {Object} options Options for the timepicker() call
- * @return {jQuery}
- */
- $.timepicker.timeRange = function (startTime, endTime, options) {
- return $.timepicker.handleRange('timepicker', startTime, endTime, options);
- };
-
- /**
- * Calls `datetimepicker` on the `startTime` and `endTime` elements, and configures them to
- * enforce date range limits.
- * @param {Element} startTime
- * @param {Element} endTime
- * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`,
- * a boolean value that can be used to reformat the input values to the `dateFormat`.
- * @param {string} method Can be used to specify the type of picker to be added
- * @return {jQuery}
- */
- $.timepicker.datetimeRange = function (startTime, endTime, options) {
- $.timepicker.handleRange('datetimepicker', startTime, endTime, options);
- };
-
- /**
- * Calls `datepicker` on the `startTime` and `endTime` elements, and configures them to
- * enforce date range limits.
- * @param {Element} startTime
- * @param {Element} endTime
- * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`,
- * a boolean value that can be used to reformat the input values to the `dateFormat`.
- * @return {jQuery}
- */
- $.timepicker.dateRange = function (startTime, endTime, options) {
- $.timepicker.handleRange('datepicker', startTime, endTime, options);
- };
-
- /**
- * Calls `method` on the `startTime` and `endTime` elements, and configures them to
- * enforce date range limits.
- * @param {string} method Can be used to specify the type of picker to be added
- * @param {Element} startTime
- * @param {Element} endTime
- * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`,
- * a boolean value that can be used to reformat the input values to the `dateFormat`.
- * @return {jQuery}
- */
- $.timepicker.handleRange = function (method, startTime, endTime, options) {
- options = $.extend({}, {
- minInterval: 0, // min allowed interval in milliseconds
- maxInterval: 0, // max allowed interval in milliseconds
- start: {}, // options for start picker
- end: {} // options for end picker
- }, options);
-
- // for the mean time this fixes an issue with calling getDate with timepicker()
- var timeOnly = false;
- if(method === 'timepicker'){
- timeOnly = true;
- method = 'datetimepicker';
- }
-
- function checkDates(changed, other) {
- var startdt = startTime[method]('getDate'),
- enddt = endTime[method]('getDate'),
- changeddt = changed[method]('getDate');
-
- if (startdt !== null) {
- var minDate = new Date(startdt.getTime()),
- maxDate = new Date(startdt.getTime());
-
- minDate.setMilliseconds(minDate.getMilliseconds() + options.minInterval);
- maxDate.setMilliseconds(maxDate.getMilliseconds() + options.maxInterval);
-
- if (options.minInterval > 0 && minDate > enddt) { // minInterval check
- endTime[method]('setDate', minDate);
- }
- else if (options.maxInterval > 0 && maxDate < enddt) { // max interval check
- endTime[method]('setDate', maxDate);
- }
- else if (startdt > enddt) {
- other[method]('setDate', changeddt);
- }
- }
- }
-
- function selected(changed, other, option) {
- if (!changed.val()) {
- return;
- }
- var date = changed[method].call(changed, 'getDate');
- if (date !== null && options.minInterval > 0) {
- if (option === 'minDate') {
- date.setMilliseconds(date.getMilliseconds() + options.minInterval);
- }
- if (option === 'maxDate') {
- date.setMilliseconds(date.getMilliseconds() - options.minInterval);
- }
- }
-
- if (date.getTime) {
- other[method].call(other, 'option', option, date);
- }
- }
-
- $.fn[method].call(startTime, $.extend({
- timeOnly: timeOnly,
- onClose: function (dateText, inst) {
- checkDates($(this), endTime);
- },
- onSelect: function (selectedDateTime) {
- selected($(this), endTime, 'minDate');
- }
- }, options, options.start));
- $.fn[method].call(endTime, $.extend({
- timeOnly: timeOnly,
- onClose: function (dateText, inst) {
- checkDates($(this), startTime);
- },
- onSelect: function (selectedDateTime) {
- selected($(this), startTime, 'maxDate');
- }
- }, options, options.end));
-
- checkDates(startTime, endTime);
-
- selected(startTime, endTime, 'minDate');
- selected(endTime, startTime, 'maxDate');
-
- return $([startTime.get(0), endTime.get(0)]);
- };
-
- /**
- * Log error or data to the console during error or debugging
- * @param {Object} err pass any type object to log to the console during error or debugging
- * @return {void}
- */
- $.timepicker.log = function () {
- if (window.console) {
- window.console.log.apply(window.console, Array.prototype.slice.call(arguments));
- }
- };
-
- /*
- * Add util object to allow access to private methods for testability.
- */
- $.timepicker._util = {
- _extendRemove: extendRemove,
- _isEmptyObject: isEmptyObject,
- _convert24to12: convert24to12,
- _detectSupport: detectSupport,
- _selectLocalTimezone: selectLocalTimezone,
- _computeEffectiveSetting: computeEffectiveSetting,
- _splitDateTime: splitDateTime,
- _parseDateTimeInternal: parseDateTimeInternal
- };
-
- /*
- * Microsecond support
- */
- if (!Date.prototype.getMicroseconds) {
- Date.prototype.microseconds = 0;
- Date.prototype.getMicroseconds = function () { return this.microseconds; };
- Date.prototype.setMicroseconds = function (m) {
- this.setMilliseconds(this.getMilliseconds() + Math.floor(m / 1000));
- this.microseconds = m % 1000;
- return this;
- };
- }
-
- /*
- * Keep up with the version
- */
- $.timepicker.version = "@@version";
-
-}));
diff --git a/web_app/js/external/jquery.cookie.js b/web_app/js/external/jquery.cookie.js
new file mode 100644
index 0000000..c7f3a59
--- /dev/null
+++ b/web_app/js/external/jquery.cookie.js
@@ -0,0 +1,117 @@
+/*!
+ * jQuery Cookie Plugin v1.4.1
+ * https://github.com/carhartl/jquery-cookie
+ *
+ * Copyright 2013 Klaus Hartl
+ * Released under the MIT license
+ */
+(function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD
+ define(['jquery'], factory);
+ } else if (typeof exports === 'object') {
+ // CommonJS
+ factory(require('jquery'));
+ } else {
+ // Browser globals
+ factory(jQuery);
+ }
+}(function ($) {
+
+ var pluses = /\+/g;
+
+ function encode(s) {
+ return config.raw ? s : encodeURIComponent(s);
+ }
+
+ function decode(s) {
+ return config.raw ? s : decodeURIComponent(s);
+ }
+
+ function stringifyCookieValue(value) {
+ return encode(config.json ? JSON.stringify(value) : String(value));
+ }
+
+ function parseCookieValue(s) {
+ if (s.indexOf('"') === 0) {
+ // This is a quoted cookie as according to RFC2068, unescape...
+ s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
+ }
+
+ try {
+ // Replace server-side written pluses with spaces.
+ // If we can't decode the cookie, ignore it, it's unusable.
+ // If we can't parse the cookie, ignore it, it's unusable.
+ s = decodeURIComponent(s.replace(pluses, ' '));
+ return config.json ? JSON.parse(s) : s;
+ } catch(e) {}
+ }
+
+ function read(s, converter) {
+ var value = config.raw ? s : parseCookieValue(s);
+ return $.isFunction(converter) ? converter(value) : value;
+ }
+
+ var config = $.cookie = function (key, value, options) {
+
+ // Write
+
+ if (value !== undefined && !$.isFunction(value)) {
+ options = $.extend({}, config.defaults, options);
+
+ if (typeof options.expires === 'number') {
+ var days = options.expires, t = options.expires = new Date();
+ t.setTime(+t + days * 864e+5);
+ }
+
+ return (document.cookie = [
+ encode(key), '=', stringifyCookieValue(value),
+ options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+ options.path ? '; path=' + options.path : '',
+ options.domain ? '; domain=' + options.domain : '',
+ options.secure ? '; secure' : ''
+ ].join(''));
+ }
+
+ // Read
+
+ var result = key ? undefined : {};
+
+ // To prevent the for loop in the first place assign an empty array
+ // in case there are no cookies at all. Also prevents odd result when
+ // calling $.cookie().
+ var cookies = document.cookie ? document.cookie.split('; ') : [];
+
+ for (var i = 0, l = cookies.length; i < l; i++) {
+ var parts = cookies[i].split('=');
+ var name = decode(parts.shift());
+ var cookie = parts.join('=');
+
+ if (key && key === name) {
+ // If second argument (value) is a function it's a converter...
+ result = read(cookie, value);
+ break;
+ }
+
+ // Prevent storing a cookie that we couldn't decode.
+ if (!key && (cookie = read(cookie)) !== undefined) {
+ result[name] = cookie;
+ }
+ }
+
+ return result;
+ };
+
+ config.defaults = {};
+
+ $.removeCookie = function (key, options) {
+ if ($.cookie(key) === undefined) {
+ return false;
+ }
+
+ // Must not alter options, thus extending a fresh object...
+ $.cookie(key, '', $.extend({}, options, { expires: -1 }));
+ return !$.cookie(key);
+ };
+
+}));
diff --git a/web_app/js/external/jquery.keypad.package-2.0.1.zip b/web_app/js/external/jquery.keypad.package-2.0.1.zip
deleted file mode 100644
index 913a4da..0000000
Binary files a/web_app/js/external/jquery.keypad.package-2.0.1.zip and /dev/null differ
diff --git a/web_app/js/external/jquery.timepicker.js b/web_app/js/external/jquery.timepicker.js
new file mode 100644
index 0000000..1861978
--- /dev/null
+++ b/web_app/js/external/jquery.timepicker.js
@@ -0,0 +1,1195 @@
+/*!
+ * jquery-timepicker v1.8.2 - A jQuery timepicker plugin inspired by Google Calendar. It supports both mouse and keyboard navigation.
+ * Copyright (c) 2015 Jon Thornton - http://jonthornton.github.com/jquery-timepicker/
+ * License: MIT
+ */
+
+
+(function (factory) {
+ if (typeof exports === "object" && exports &&
+ typeof module === "object" && module && module.exports === exports) {
+ // Browserify. Attach to jQuery module.
+ factory(require("jquery"));
+ } else if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['jquery'], factory);
+ } else {
+ // Browser globals
+ factory(jQuery);
+ }
+}(function ($) {
+ var _baseDate = _generateBaseDate();
+ var _ONE_DAY = 86400;
+ var _lang = {
+ am: 'am',
+ pm: 'pm',
+ AM: 'AM',
+ PM: 'PM',
+ decimal: '.',
+ mins: 'mins',
+ hr: 'hr',
+ hrs: 'hrs'
+ };
+
+ var methods = {
+ init: function(options)
+ {
+ return this.each(function()
+ {
+ var self = $(this);
+
+ // pick up settings from data attributes
+ var attributeOptions = [];
+ for (var key in $.fn.timepicker.defaults) {
+ if (self.data(key)) {
+ attributeOptions[key] = self.data(key);
+ }
+ }
+
+ var settings = $.extend({}, $.fn.timepicker.defaults, attributeOptions, options);
+
+ if (settings.lang) {
+ _lang = $.extend(_lang, settings.lang);
+ }
+
+ settings = _parseSettings(settings);
+ self.data('timepicker-settings', settings);
+ self.addClass('ui-timepicker-input');
+
+ if (settings.useSelect) {
+ _render(self);
+ } else {
+ self.prop('autocomplete', 'off');
+ if (settings.showOn) {
+ for (i in settings.showOn) {
+ self.on(settings.showOn[i]+'.timepicker', methods.show);
+ }
+ }
+ self.on('change.timepicker', _formatValue);
+ self.on('keydown.timepicker', _keydownhandler);
+ self.on('keyup.timepicker', _keyuphandler);
+ if (settings.disableTextInput) {
+ self.on('keypress.timepicker', function(e) { e.preventDefault(); });
+ }
+
+ _formatValue.call(self.get(0));
+ }
+ });
+ },
+
+ show: function(e)
+ {
+ var self = $(this);
+ var settings = self.data('timepicker-settings');
+
+ if (e) {
+ e.preventDefault();
+ }
+
+ if (settings.useSelect) {
+ self.data('timepicker-list').focus();
+ return;
+ }
+
+ if (_hideKeyboard(self)) {
+ // block the keyboard on mobile devices
+ self.blur();
+ }
+
+ var list = self.data('timepicker-list');
+
+ // check if input is readonly
+ // if (self.prop('readonly')) {
+ // return;
+ // }
+
+ // check if list needs to be rendered
+ if (!list || list.length === 0 || typeof settings.durationTime === 'function') {
+ _render(self);
+ list = self.data('timepicker-list');
+ }
+
+ if (_isVisible(list)) {
+ return;
+ }
+
+ self.data('ui-timepicker-value', self.val());
+ _setSelected(self, list);
+
+ // make sure other pickers are hidden
+ methods.hide();
+
+ // position the dropdown relative to the input
+ list.show();
+ var listOffset = {};
+
+ if (settings.orientation.match(/r/)) {
+ // right-align the dropdown
+ listOffset.left = self.offset().left + self.outerWidth() - list.outerWidth() + parseInt(list.css('marginLeft').replace('px', ''), 10);
+ } else {
+ // left-align the dropdown
+ listOffset.left = self.offset().left + parseInt(list.css('marginLeft').replace('px', ''), 10);
+ }
+
+ var verticalOrientation;
+ if (settings.orientation.match(/t/)) {
+ verticalOrientation = 't';
+ } else if (settings.orientation.match(/b/)) {
+ verticalOrientation = 'b';
+ } else if ((self.offset().top + self.outerHeight(true) + list.outerHeight()) > $(window).height() + $(window).scrollTop()) {
+ verticalOrientation = 't';
+ } else {
+ verticalOrientation = 'b';
+ }
+
+ if (verticalOrientation == 't') {
+ // position the dropdown on top
+ list.addClass('ui-timepicker-positioned-top');
+ listOffset.top = self.offset().top - list.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10);
+ } else {
+ // put it under the input
+ list.removeClass('ui-timepicker-positioned-top');
+ listOffset.top = self.offset().top + self.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10);
+ }
+
+ list.offset(listOffset);
+
+ // position scrolling
+ var selected = list.find('.ui-timepicker-selected');
+
+ if (!selected.length) {
+ if (_getTimeValue(self)) {
+ selected = _findRow(self, list, _time2int(_getTimeValue(self)));
+ } else if (settings.scrollDefault) {
+ selected = _findRow(self, list, settings.scrollDefault());
+ }
+ }
+
+ if (selected && selected.length) {
+ var topOffset = list.scrollTop() + selected.position().top - selected.outerHeight();
+ list.scrollTop(topOffset);
+ } else {
+ list.scrollTop(0);
+ }
+
+ // prevent scroll propagation
+ if(settings.stopScrollPropagation) {
+ $(document).on('wheel.ui-timepicker', '.ui-timepicker-wrapper', function(e){
+ e.preventDefault();
+ var currentScroll = $(this).scrollTop();
+ $(this).scrollTop(currentScroll + e.originalEvent.deltaY);
+ });
+ }
+
+ // attach close handlers
+ $(document).on('touchstart.ui-timepicker mousedown.ui-timepicker', _closeHandler);
+ $(window).on('resize.ui-timepicker', _closeHandler);
+ if (settings.closeOnWindowScroll) {
+ $(document).on('scroll.ui-timepicker', _closeHandler);
+ }
+
+ self.trigger('showTimepicker');
+
+ return this;
+ },
+
+ hide: function(e)
+ {
+ var self = $(this);
+ var settings = self.data('timepicker-settings');
+
+ if (settings && settings.useSelect) {
+ self.blur();
+ }
+
+ $('.ui-timepicker-wrapper').each(function() {
+ var list = $(this);
+ if (!_isVisible(list)) {
+ return;
+ }
+
+ var self = list.data('timepicker-input');
+ var settings = self.data('timepicker-settings');
+
+ if (settings && settings.selectOnBlur) {
+ _selectValue(self);
+ }
+
+ list.hide();
+ self.trigger('hideTimepicker');
+ });
+
+ return this;
+ },
+
+ option: function(key, value)
+ {
+ return this.each(function(){
+ var self = $(this);
+ var settings = self.data('timepicker-settings');
+ var list = self.data('timepicker-list');
+
+ if (typeof key == 'object') {
+ settings = $.extend(settings, key);
+
+ } else if (typeof key == 'string' && typeof value != 'undefined') {
+ settings[key] = value;
+
+ } else if (typeof key == 'string') {
+ return settings[key];
+ }
+
+ settings = _parseSettings(settings);
+
+ self.data('timepicker-settings', settings);
+
+ if (list) {
+ list.remove();
+ self.data('timepicker-list', false);
+ }
+
+ if (settings.useSelect) {
+ _render(self);
+ }
+ });
+ },
+
+ getSecondsFromMidnight: function()
+ {
+ return _time2int(_getTimeValue(this));
+ },
+
+ getTime: function(relative_date)
+ {
+ var self = this;
+
+ var time_string = _getTimeValue(self);
+ if (!time_string) {
+ return null;
+ }
+
+ var offset = _time2int(time_string);
+ if (offset === null) {
+ return null;
+ }
+
+ if (!relative_date) {
+ relative_date = new Date();
+ }
+
+ // construct a Date with today's date, and offset's time
+ var time = new Date(relative_date);
+ time.setHours(offset / 3600);
+ time.setMinutes(offset % 3600 / 60);
+ time.setSeconds(offset % 60);
+ time.setMilliseconds(0);
+
+ return time;
+ },
+
+ setTime: function(value)
+ {
+ var self = this;
+ var settings = self.data('timepicker-settings');
+
+ if (settings.forceRoundTime) {
+ var prettyTime = _roundAndFormatTime(_time2int(value), settings)
+ } else {
+ var prettyTime = _int2time(_time2int(value), settings);
+ }
+
+ if (value && prettyTime === null && settings.noneOption) {
+ prettyTime = value;
+ }
+
+ _setTimeValue(self, prettyTime);
+ if (self.data('timepicker-list')) {
+ _setSelected(self, self.data('timepicker-list'));
+ }
+
+ return this;
+ },
+
+ remove: function()
+ {
+ var self = this;
+
+ // check if this element is a timepicker
+ if (!self.hasClass('ui-timepicker-input')) {
+ return;
+ }
+
+ var settings = self.data('timepicker-settings');
+ self.removeAttr('autocomplete', 'off');
+ self.removeClass('ui-timepicker-input');
+ self.removeData('timepicker-settings');
+ self.off('.timepicker');
+
+ // timepicker-list won't be present unless the user has interacted with this timepicker
+ if (self.data('timepicker-list')) {
+ self.data('timepicker-list').remove();
+ }
+
+ if (settings.useSelect) {
+ self.show();
+ }
+
+ self.removeData('timepicker-list');
+
+ return this;
+ }
+ };
+
+ // private methods
+
+ function _isVisible(elem)
+ {
+ var el = elem[0];
+ return el.offsetWidth > 0 && el.offsetHeight > 0;
+ }
+
+ function _parseSettings(settings)
+ {
+ if (settings.minTime) {
+ settings.minTime = _time2int(settings.minTime);
+ }
+
+ if (settings.maxTime) {
+ settings.maxTime = _time2int(settings.maxTime);
+ }
+
+ if (settings.durationTime && typeof settings.durationTime !== 'function') {
+ settings.durationTime = _time2int(settings.durationTime);
+ }
+
+ if (settings.scrollDefault == 'now') {
+ settings.scrollDefault = function() {
+ return settings.roundingFunction(_time2int(new Date()), settings);
+ }
+ } else if (settings.scrollDefault && typeof settings.scrollDefault != 'function') {
+ var val = settings.scrollDefault;
+ settings.scrollDefault = function() {
+ return settings.roundingFunction(_time2int(val), settings);
+ }
+ } else if (settings.minTime) {
+ settings.scrollDefault = function() {
+ return settings.roundingFunction(settings.minTime, settings);
+ }
+ }
+
+ if ($.type(settings.timeFormat) === "string" && settings.timeFormat.match(/[gh]/)) {
+ settings._twelveHourTime = true;
+ }
+
+ if (settings.showOnFocus === false && settings.showOn.indexOf('focus') != -1) {
+ settings.showOn.splice(settings.showOn.indexOf('focus'), 1);
+ }
+
+ if (settings.disableTimeRanges.length > 0) {
+ // convert string times to integers
+ for (var i in settings.disableTimeRanges) {
+ settings.disableTimeRanges[i] = [
+ _time2int(settings.disableTimeRanges[i][0]),
+ _time2int(settings.disableTimeRanges[i][1])
+ ];
+ }
+
+ // sort by starting time
+ settings.disableTimeRanges = settings.disableTimeRanges.sort(function(a, b){
+ return a[0] - b[0];
+ });
+
+ // merge any overlapping ranges
+ for (var i = settings.disableTimeRanges.length-1; i > 0; i--) {
+ if (settings.disableTimeRanges[i][0] <= settings.disableTimeRanges[i-1][1]) {
+ settings.disableTimeRanges[i-1] = [
+ Math.min(settings.disableTimeRanges[i][0], settings.disableTimeRanges[i-1][0]),
+ Math.max(settings.disableTimeRanges[i][1], settings.disableTimeRanges[i-1][1])
+ ];
+ settings.disableTimeRanges.splice(i, 1);
+ }
+ }
+ }
+
+ return settings;
+ }
+
+ function _render(self)
+ {
+ var settings = self.data('timepicker-settings');
+ var list = self.data('timepicker-list');
+
+ if (list && list.length) {
+ list.remove();
+ self.data('timepicker-list', false);
+ }
+
+ if (settings.useSelect) {
+ list = $('', { 'class': 'ui-timepicker-select' });
+ var wrapped_list = list;
+ } else {
+ list = $('
', { 'class': 'ui-timepicker-list' });
+
+ var wrapped_list = $('', { 'class': 'ui-timepicker-wrapper', 'tabindex': -1 });
+ wrapped_list.css({'display':'none', 'position': 'absolute' }).append(list);
+ }
+
+ if (settings.noneOption) {
+ if (settings.noneOption === true) {
+ settings.noneOption = (settings.useSelect) ? 'Time...' : 'None';
+ }
+
+ if ($.isArray(settings.noneOption)) {
+ for (var i in settings.noneOption) {
+ if (parseInt(i, 10) == i){
+ var noneElement = _generateNoneElement(settings.noneOption[i], settings.useSelect);
+ list.append(noneElement);
+ }
+ }
+ } else {
+ var noneElement = _generateNoneElement(settings.noneOption, settings.useSelect);
+ list.append(noneElement);
+ }
+ }
+
+ if (settings.className) {
+ wrapped_list.addClass(settings.className);
+ }
+
+ if ((settings.minTime !== null || settings.durationTime !== null) && settings.showDuration) {
+ var stepval = typeof settings.step == 'function' ? 'function' : settings.step;
+ wrapped_list.addClass('ui-timepicker-with-duration');
+ wrapped_list.addClass('ui-timepicker-step-'+settings.step);
+ }
+
+ var durStart = settings.minTime;
+ if (typeof settings.durationTime === 'function') {
+ durStart = _time2int(settings.durationTime());
+ } else if (settings.durationTime !== null) {
+ durStart = settings.durationTime;
+ }
+ var start = (settings.minTime !== null) ? settings.minTime : 0;
+ var end = (settings.maxTime !== null) ? settings.maxTime : (start + _ONE_DAY - 1);
+
+ if (end < start) {
+ // make sure the end time is greater than start time, otherwise there will be no list to show
+ end += _ONE_DAY;
+ }
+
+ if (end === _ONE_DAY-1 && $.type(settings.timeFormat) === "string" && settings.show2400) {
+ // show a 24:00 option when using military time
+ end = _ONE_DAY;
+ }
+
+ var dr = settings.disableTimeRanges;
+ var drCur = 0;
+ var drLen = dr.length;
+
+ var stepFunc = settings.step;
+ if (typeof stepFunc != 'function') {
+ stepFunc = function() {
+ return settings.step;
+ }
+ }
+
+ for (var i=start, j=0; i <= end; j++, i += stepFunc(j)*60) {
+ var timeInt = i;
+ var timeString = _int2time(timeInt, settings);
+
+ if (settings.useSelect) {
+ var row = $('', { 'value': timeString });
+ row.text(timeString);
+ } else {
+ var row = $('');
+ row.data('time', (timeInt <= 86400 ? timeInt : timeInt % 86400));
+ row.text(timeString);
+ }
+
+ if ((settings.minTime !== null || settings.durationTime !== null) && settings.showDuration) {
+ var durationString = _int2duration(i - durStart, settings.step);
+ if (settings.useSelect) {
+ row.text(row.text()+' ('+durationString+')');
+ } else {
+ var duration = $('', { 'class': 'ui-timepicker-duration' });
+ duration.text(' ('+durationString+')');
+ row.append(duration);
+ }
+ }
+
+ if (drCur < drLen) {
+ if (timeInt >= dr[drCur][1]) {
+ drCur += 1;
+ }
+
+ if (dr[drCur] && timeInt >= dr[drCur][0] && timeInt < dr[drCur][1]) {
+ if (settings.useSelect) {
+ row.prop('disabled', true);
+ } else {
+ row.addClass('ui-timepicker-disabled');
+ }
+ }
+ }
+
+ list.append(row);
+ }
+
+ wrapped_list.data('timepicker-input', self);
+ self.data('timepicker-list', wrapped_list);
+
+ if (settings.useSelect) {
+ if (self.val()) {
+ list.val(_roundAndFormatTime(_time2int(self.val()), settings));
+ }
+
+ list.on('focus', function(){
+ $(this).data('timepicker-input').trigger('showTimepicker');
+ });
+ list.on('blur', function(){
+ $(this).data('timepicker-input').trigger('hideTimepicker');
+ });
+ list.on('change', function(){
+ _setTimeValue(self, $(this).val(), 'select');
+ });
+
+ _setTimeValue(self, list.val(), 'initial');
+ self.hide().after(list);
+ } else {
+ var appendTo = settings.appendTo;
+ if (typeof appendTo === 'string') {
+ appendTo = $(appendTo);
+ } else if (typeof appendTo === 'function') {
+ appendTo = appendTo(self);
+ }
+ appendTo.append(wrapped_list);
+ _setSelected(self, list);
+
+ list.on('mousedown', 'li', function(e) {
+
+ // hack: temporarily disable the focus handler
+ // to deal with the fact that IE fires 'focus'
+ // events asynchronously
+ self.off('focus.timepicker');
+ self.on('focus.timepicker-ie-hack', function(){
+ self.off('focus.timepicker-ie-hack');
+ self.on('focus.timepicker', methods.show);
+ });
+
+ if (!_hideKeyboard(self)) {
+ self[0].focus();
+ }
+
+ // make sure only the clicked row is selected
+ list.find('li').removeClass('ui-timepicker-selected');
+ $(this).addClass('ui-timepicker-selected');
+
+ if (_selectValue(self)) {
+ self.trigger('hideTimepicker');
+
+ list.on('mouseup.timepicker', 'li', function(e) {
+ list.off('mouseup.timepicker');
+ wrapped_list.hide();
+ });
+ }
+ });
+ }
+ }
+
+ function _generateNoneElement(optionValue, useSelect)
+ {
+ var label, className, value;
+
+ if (typeof optionValue == 'object') {
+ label = optionValue.label;
+ className = optionValue.className;
+ value = optionValue.value;
+ } else if (typeof optionValue == 'string') {
+ label = optionValue;
+ } else {
+ $.error('Invalid noneOption value');
+ }
+
+ if (useSelect) {
+ return $('', {
+ 'value': value,
+ 'class': className,
+ 'text': label
+ });
+ } else {
+ return $('', {
+ 'class': className,
+ 'text': label
+ }).data('time', value);
+ }
+ }
+
+ function _roundAndFormatTime(seconds, settings)
+ {
+ seconds = settings.roundingFunction(seconds, settings);
+ if (seconds !== null) {
+ return _int2time(seconds, settings);
+ }
+ }
+
+ function _generateBaseDate()
+ {
+ return new Date(1970, 1, 1, 0, 0, 0);
+ }
+
+ // event handler to decide whether to close timepicker
+ function _closeHandler(e)
+ {
+ var target = $(e.target);
+ var input = target.closest('.ui-timepicker-input');
+ if (input.length === 0 && target.closest('.ui-timepicker-wrapper').length === 0) {
+ methods.hide();
+ $(document).unbind('.ui-timepicker');
+ $(window).unbind('.ui-timepicker');
+ }
+ }
+
+ function _hideKeyboard(self)
+ {
+ var settings = self.data('timepicker-settings');
+ return ((window.navigator.msMaxTouchPoints || 'ontouchstart' in document) && settings.disableTouchKeyboard);
+ }
+
+ function _findRow(self, list, value)
+ {
+ if (!value && value !== 0) {
+ return false;
+ }
+
+ var settings = self.data('timepicker-settings');
+ var out = false;
+ var value = settings.roundingFunction(value, settings);
+
+ // loop through the menu items
+ list.find('li').each(function(i, obj) {
+ var jObj = $(obj);
+ if (typeof jObj.data('time') != 'number') {
+ return;
+ }
+
+ if (jObj.data('time') == value) {
+ out = jObj;
+ return false;
+ }
+ });
+
+ return out;
+ }
+
+ function _setSelected(self, list)
+ {
+ list.find('li').removeClass('ui-timepicker-selected');
+
+ var timeValue = _time2int(_getTimeValue(self), self.data('timepicker-settings'));
+ if (timeValue === null) {
+ return;
+ }
+
+ var selected = _findRow(self, list, timeValue);
+ if (selected) {
+
+ var topDelta = selected.offset().top - list.offset().top;
+
+ if (topDelta + selected.outerHeight() > list.outerHeight() || topDelta < 0) {
+ list.scrollTop(list.scrollTop() + selected.position().top - selected.outerHeight());
+ }
+
+ selected.addClass('ui-timepicker-selected');
+ }
+ }
+
+
+ function _formatValue(e, origin)
+ {
+ if (this.value === '' || origin == 'timepicker') {
+ return;
+ }
+
+ var self = $(this);
+
+ if (self.is(':focus') && (!e || e.type != 'change')) {
+ return;
+ }
+
+ var settings = self.data('timepicker-settings');
+ var seconds = _time2int(this.value, settings);
+
+ if (seconds === null) {
+ self.trigger('timeFormatError');
+ return;
+ }
+
+ var rangeError = false;
+ // check that the time in within bounds
+ if (settings.minTime !== null && seconds < settings.minTime) {
+ rangeError = true;
+ } else if (settings.maxTime !== null && seconds > settings.maxTime) {
+ rangeError = true;
+ }
+
+ // check that time isn't within disabled time ranges
+ $.each(settings.disableTimeRanges, function(){
+ if (seconds >= this[0] && seconds < this[1]) {
+ rangeError = true;
+ return false;
+ }
+ });
+
+ if (settings.forceRoundTime) {
+ seconds = settings.roundingFunction(seconds, settings);
+ }
+
+ var prettyTime = _int2time(seconds, settings);
+
+ if (rangeError) {
+ if (_setTimeValue(self, prettyTime, 'error')) {
+ self.trigger('timeRangeError');
+ }
+ } else {
+ _setTimeValue(self, prettyTime);
+ }
+ }
+
+ function _getTimeValue(self)
+ {
+ if (self.is('input')) {
+ return self.val();
+ } else {
+ // use the element's data attributes to store values
+ return self.data('ui-timepicker-value');
+ }
+ }
+
+ function _setTimeValue(self, value, source)
+ {
+ if (self.is('input')) {
+ self.val(value);
+
+ var settings = self.data('timepicker-settings');
+ if (settings.useSelect && source != 'select' && source != 'initial') {
+ self.data('timepicker-list').val(_roundAndFormatTime(_time2int(value), settings));
+ }
+ }
+
+ if (self.data('ui-timepicker-value') != value) {
+ self.data('ui-timepicker-value', value);
+ if (source == 'select') {
+ self.trigger('selectTime').trigger('changeTime').trigger('change', 'timepicker');
+ } else if (source != 'error') {
+ self.trigger('changeTime');
+ }
+
+ return true;
+ } else {
+ self.trigger('selectTime');
+ return false;
+ }
+ }
+
+ /*
+ * Keyboard navigation via arrow keys
+ */
+ function _keydownhandler(e)
+ {
+ var self = $(this);
+ var list = self.data('timepicker-list');
+
+ if (!list || !_isVisible(list)) {
+ if (e.keyCode == 40) {
+ // show the list!
+ methods.show.call(self.get(0));
+ list = self.data('timepicker-list');
+ if (!_hideKeyboard(self)) {
+ self.focus();
+ }
+ } else {
+ return true;
+ }
+ }
+
+ switch (e.keyCode) {
+
+ case 13: // return
+ if (_selectValue(self)) {
+ methods.hide.apply(this);
+ }
+
+ e.preventDefault();
+ return false;
+
+ case 38: // up
+ var selected = list.find('.ui-timepicker-selected');
+
+ if (!selected.length) {
+ list.find('li').each(function(i, obj) {
+ if ($(obj).position().top > 0) {
+ selected = $(obj);
+ return false;
+ }
+ });
+ selected.addClass('ui-timepicker-selected');
+
+ } else if (!selected.is(':first-child')) {
+ selected.removeClass('ui-timepicker-selected');
+ selected.prev().addClass('ui-timepicker-selected');
+
+ if (selected.prev().position().top < selected.outerHeight()) {
+ list.scrollTop(list.scrollTop() - selected.outerHeight());
+ }
+ }
+
+ return false;
+
+ case 40: // down
+ selected = list.find('.ui-timepicker-selected');
+
+ if (selected.length === 0) {
+ list.find('li').each(function(i, obj) {
+ if ($(obj).position().top > 0) {
+ selected = $(obj);
+ return false;
+ }
+ });
+
+ selected.addClass('ui-timepicker-selected');
+ } else if (!selected.is(':last-child')) {
+ selected.removeClass('ui-timepicker-selected');
+ selected.next().addClass('ui-timepicker-selected');
+
+ if (selected.next().position().top + 2*selected.outerHeight() > list.outerHeight()) {
+ list.scrollTop(list.scrollTop() + selected.outerHeight());
+ }
+ }
+
+ return false;
+
+ case 27: // escape
+ list.find('li').removeClass('ui-timepicker-selected');
+ methods.hide();
+ break;
+
+ case 9: //tab
+ methods.hide();
+ break;
+
+ default:
+ return true;
+ }
+ }
+
+ /*
+ * Time typeahead
+ */
+ function _keyuphandler(e)
+ {
+ var self = $(this);
+ var list = self.data('timepicker-list');
+ var settings = self.data('timepicker-settings');
+
+ if (!list || !_isVisible(list) || settings.disableTextInput) {
+ return true;
+ }
+
+ switch (e.keyCode) {
+
+ case 96: // numpad numerals
+ case 97:
+ case 98:
+ case 99:
+ case 100:
+ case 101:
+ case 102:
+ case 103:
+ case 104:
+ case 105:
+ case 48: // numerals
+ case 49:
+ case 50:
+ case 51:
+ case 52:
+ case 53:
+ case 54:
+ case 55:
+ case 56:
+ case 57:
+ case 65: // a
+ case 77: // m
+ case 80: // p
+ case 186: // colon
+ case 8: // backspace
+ case 46: // delete
+ if (settings.typeaheadHighlight) {
+ _setSelected(self, list);
+ } else {
+ list.hide();
+ }
+ break;
+ }
+ }
+
+ function _selectValue(self)
+ {
+ var settings = self.data('timepicker-settings');
+ var list = self.data('timepicker-list');
+ var timeValue = null;
+
+ var cursor = list.find('.ui-timepicker-selected');
+
+ if (cursor.hasClass('ui-timepicker-disabled')) {
+ return false;
+ }
+
+ if (cursor.length) {
+ // selected value found
+ timeValue = cursor.data('time');
+ }
+
+ if (timeValue !== null) {
+ if (typeof timeValue != 'string') {
+ timeValue = _int2time(timeValue, settings);
+ }
+
+ _setTimeValue(self, timeValue, 'select');
+ }
+
+ return true;
+ }
+
+ function _int2duration(seconds, step)
+ {
+ seconds = Math.abs(seconds);
+ var minutes = Math.round(seconds/60),
+ duration = [],
+ hours, mins;
+
+ if (minutes < 60) {
+ // Only show (x mins) under 1 hour
+ duration = [minutes, _lang.mins];
+ } else {
+ hours = Math.floor(minutes/60);
+ mins = minutes%60;
+
+ // Show decimal notation (eg: 1.5 hrs) for 30 minute steps
+ if (step == 30 && mins == 30) {
+ hours += _lang.decimal + 5;
+ }
+
+ duration.push(hours);
+ duration.push(hours == 1 ? _lang.hr : _lang.hrs);
+
+ // Show remainder minutes notation (eg: 1 hr 15 mins) for non-30 minute steps
+ // and only if there are remainder minutes to show
+ if (step != 30 && mins) {
+ duration.push(mins);
+ duration.push(_lang.mins);
+ }
+ }
+
+ return duration.join(' ');
+ }
+
+ function _int2time(seconds, settings)
+ {
+ if (seconds === null) {
+ return null;
+ }
+
+ var time = new Date(_baseDate.valueOf() + (seconds*1000));
+
+ if (isNaN(time.getTime())) {
+ return null;
+ }
+
+ if ($.type(settings.timeFormat) === "function") {
+ return settings.timeFormat(time);
+ }
+
+ var output = '';
+ var hour, code;
+ for (var i=0; i 11) ? _lang.pm : _lang.am;
+ break;
+
+ case 'A':
+ output += (time.getHours() > 11) ? _lang.PM : _lang.AM;
+ break;
+
+ case 'g':
+ hour = time.getHours() % 12;
+ output += (hour === 0) ? '12' : hour;
+ break;
+
+ case 'G':
+ hour = time.getHours();
+ if (seconds === _ONE_DAY) hour = 24;
+ output += hour;
+ break;
+
+ case 'h':
+ hour = time.getHours() % 12;
+
+ if (hour !== 0 && hour < 10) {
+ hour = '0'+hour;
+ }
+
+ output += (hour === 0) ? '12' : hour;
+ break;
+
+ case 'H':
+ hour = time.getHours();
+ if (seconds === _ONE_DAY) hour = 24;
+ output += (hour > 9) ? hour : '0'+hour;
+ break;
+
+ case 'i':
+ var minutes = time.getMinutes();
+ output += (minutes > 9) ? minutes : '0'+minutes;
+ break;
+
+ case 's':
+ seconds = time.getSeconds();
+ output += (seconds > 9) ? seconds : '0'+seconds;
+ break;
+
+ case '\\':
+ // escape character; add the next character and skip ahead
+ i++;
+ output += settings.timeFormat.charAt(i);
+ break;
+
+ default:
+ output += code;
+ }
+ }
+
+ return output;
+ }
+
+ function _time2int(timeString, settings)
+ {
+ if (timeString === '') return null;
+ if (!timeString || timeString+0 == timeString) return timeString;
+
+ if (typeof(timeString) == 'object') {
+ return timeString.getHours()*3600 + timeString.getMinutes()*60 + timeString.getSeconds();
+ }
+
+ timeString = timeString.toLowerCase().replace(/[\s\.]/g, '');
+
+ // if the last character is an "a" or "p", add the "m"
+ if (timeString.slice(-1) == 'a' || timeString.slice(-1) == 'p') {
+ timeString += 'm';
+ }
+
+ var ampmRegex = '(' +
+ _lang.am.replace('.', '')+'|' +
+ _lang.pm.replace('.', '')+'|' +
+ _lang.AM.replace('.', '')+'|' +
+ _lang.PM.replace('.', '')+')?';
+
+ // try to parse time input
+ var pattern = new RegExp('^'+ampmRegex+'([0-2]?[0-9])\\W?([0-5][0-9])?\\W?([0-5][0-9])?'+ampmRegex+'$');
+
+ var time = timeString.match(pattern);
+ if (!time) {
+ return null;
+ }
+
+ var hour = parseInt(time[2]*1, 10);
+ var ampm = time[1] || time[5];
+ var hours = hour;
+
+ if (hour <= 12 && ampm) {
+ var isPm = (ampm == _lang.pm || ampm == _lang.PM);
+
+ if (hour == 12) {
+ hours = isPm ? 12 : 0;
+ } else {
+ hours = (hour + (isPm ? 12 : 0));
+ }
+ }
+
+ var minutes = ( time[3]*1 || 0 );
+ var seconds = ( time[4]*1 || 0 );
+ var timeInt = hours*3600 + minutes*60 + seconds;
+
+ // if no am/pm provided, intelligently guess based on the scrollDefault
+ if (hour < 12 && !ampm && settings && settings._twelveHourTime && settings.scrollDefault) {
+ var delta = timeInt - settings.scrollDefault();
+ if (delta < 0 && delta >= _ONE_DAY / -2) {
+ timeInt = (timeInt + (_ONE_DAY / 2)) % _ONE_DAY;
+ }
+ }
+
+ return timeInt;
+ }
+
+ function _pad2(n) {
+ return ("0" + n).slice(-2);
+ }
+
+ // Plugin entry
+ $.fn.timepicker = function(method)
+ {
+ if (!this.length) return this;
+ if (methods[method]) {
+ // check if this element is a timepicker
+ if (!this.hasClass('ui-timepicker-input')) {
+ return this;
+ }
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+ }
+ else if(typeof method === "object" || !method) { return methods.init.apply(this, arguments); }
+ else { $.error("Method "+ method + " does not exist on jQuery.timepicker"); }
+ };
+ // Global defaults
+ $.fn.timepicker.defaults = {
+ className: null,
+ minTime: null,
+ maxTime: null,
+ durationTime: null,
+ step: 30,
+ showDuration: false,
+ showOnFocus: true,
+ showOn: ['click', 'focus'],
+ timeFormat: 'g:ia',
+ scrollDefault: null,
+ selectOnBlur: false,
+ disableTextInput: false,
+ disableTouchKeyboard: false,
+ forceRoundTime: false,
+ roundingFunction: function(seconds, settings) {
+ if (seconds === null) {
+ return null;
+ } else {
+ var offset = seconds % (settings.step*60); // step is in minutes
+
+ if (offset >= settings.step*30) {
+ // if offset is larger than a half step, round up
+ seconds += (settings.step*60) - offset;
+ } else {
+ // round down
+ seconds -= offset;
+ }
+
+ return seconds;
+ }
+ },
+ appendTo: 'body',
+ orientation: 'l',
+ disableTimeRanges: [],
+ closeOnWindowScroll: false,
+ typeaheadHighlight: true,
+ noneOption: false,
+ show2400: false,
+ stopScrollPropagation: false
+ };
+}));
diff --git a/web_app/js/external/jquery.timepicker.min.js b/web_app/js/external/jquery.timepicker.min.js
new file mode 100644
index 0000000..eedd332
--- /dev/null
+++ b/web_app/js/external/jquery.timepicker.min.js
@@ -0,0 +1,7 @@
+/*!
+ * jquery-timepicker v1.8.2 - A jQuery timepicker plugin inspired by Google Calendar. It supports both mouse and keyboard navigation.
+ * Copyright (c) 2015 Jon Thornton - http://jonthornton.github.com/jquery-timepicker/
+ * License: MIT
+ */
+
+!function(a){"object"==typeof exports&&exports&&"object"==typeof module&&module&&module.exports===exports?a(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],a):a(jQuery)}(function(a){function b(a){var b=a[0];return b.offsetWidth>0&&b.offsetHeight>0}function c(b){if(b.minTime&&(b.minTime=u(b.minTime)),b.maxTime&&(b.maxTime=u(b.maxTime)),b.durationTime&&"function"!=typeof b.durationTime&&(b.durationTime=u(b.durationTime)),"now"==b.scrollDefault)b.scrollDefault=function(){return b.roundingFunction(u(new Date),b)};else if(b.scrollDefault&&"function"!=typeof b.scrollDefault){var c=b.scrollDefault;b.scrollDefault=function(){return b.roundingFunction(u(c),b)}}else b.minTime&&(b.scrollDefault=function(){return b.roundingFunction(b.minTime,b)});if("string"===a.type(b.timeFormat)&&b.timeFormat.match(/[gh]/)&&(b._twelveHourTime=!0),b.showOnFocus===!1&&-1!=b.showOn.indexOf("focus")&&b.showOn.splice(b.showOn.indexOf("focus"),1),b.disableTimeRanges.length>0){for(var d in b.disableTimeRanges)b.disableTimeRanges[d]=[u(b.disableTimeRanges[d][0]),u(b.disableTimeRanges[d][1])];b.disableTimeRanges=b.disableTimeRanges.sort(function(a,b){return a[0]-b[0]});for(var d=b.disableTimeRanges.length-1;d>0;d--)b.disableTimeRanges[d][0]<=b.disableTimeRanges[d-1][1]&&(b.disableTimeRanges[d-1]=[Math.min(b.disableTimeRanges[d][0],b.disableTimeRanges[d-1][0]),Math.max(b.disableTimeRanges[d][1],b.disableTimeRanges[d-1][1])],b.disableTimeRanges.splice(d,1))}return b}function d(b){var c=b.data("timepicker-settings"),d=b.data("timepicker-list");if(d&&d.length&&(d.remove(),b.data("timepicker-list",!1)),c.useSelect){d=a("",{"class":"ui-timepicker-select"});var g=d}else{d=a("
",{"class":"ui-timepicker-list"});var g=a("",{"class":"ui-timepicker-wrapper",tabindex:-1});g.css({display:"none",position:"absolute"}).append(d)}if(c.noneOption)if(c.noneOption===!0&&(c.noneOption=c.useSelect?"Time...":"None"),a.isArray(c.noneOption)){for(var h in c.noneOption)if(parseInt(h,10)==h){var i=e(c.noneOption[h],c.useSelect);d.append(i)}}else{var i=e(c.noneOption,c.useSelect);d.append(i)}if(c.className&&g.addClass(c.className),(null!==c.minTime||null!==c.durationTime)&&c.showDuration){"function"==typeof c.step?"function":c.step;g.addClass("ui-timepicker-with-duration"),g.addClass("ui-timepicker-step-"+c.step)}var k=c.minTime;"function"==typeof c.durationTime?k=u(c.durationTime()):null!==c.durationTime&&(k=c.durationTime);var m=null!==c.minTime?c.minTime:0,n=null!==c.maxTime?c.maxTime:m+w-1;m>n&&(n+=w),n===w-1&&"string"===a.type(c.timeFormat)&&c.show2400&&(n=w);var p=c.disableTimeRanges,q=0,v=p.length,x=c.step;"function"!=typeof x&&(x=function(){return c.step});for(var h=m,z=0;n>=h;z++,h+=60*x(z)){var A=h,B=t(A,c);if(c.useSelect){var C=a("",{value:B});C.text(B)}else{var C=a("");C.data("time",86400>=A?A:A%86400),C.text(B)}if((null!==c.minTime||null!==c.durationTime)&&c.showDuration){var D=s(h-k,c.step);if(c.useSelect)C.text(C.text()+" ("+D+")");else{var E=a("",{"class":"ui-timepicker-duration"});E.text(" ("+D+")"),C.append(E)}}v>q&&(A>=p[q][1]&&(q+=1),p[q]&&A>=p[q][0]&&A",{value:f,"class":e,text:d}):a("",{"class":e,text:d}).data("time",f)}function f(a,b){return a=b.roundingFunction(a,b),null!==a?t(a,b):void 0}function g(){return new Date(1970,1,1,0,0,0)}function h(b){var c=a(b.target),d=c.closest(".ui-timepicker-input");0===d.length&&0===c.closest(".ui-timepicker-wrapper").length&&(y.hide(),a(document).unbind(".ui-timepicker"),a(window).unbind(".ui-timepicker"))}function j(a){var b=a.data("timepicker-settings");return(window.navigator.msMaxTouchPoints||"ontouchstart"in document)&&b.disableTouchKeyboard}function k(b,c,d){if(!d&&0!==d)return!1;var e=b.data("timepicker-settings"),f=!1,d=e.roundingFunction(d,e);return c.find("li").each(function(b,c){var e=a(c);if("number"==typeof e.data("time"))return e.data("time")==d?(f=e,!1):void 0}),f}function l(a,b){b.find("li").removeClass("ui-timepicker-selected");var c=u(n(a),a.data("timepicker-settings"));if(null!==c){var d=k(a,b,c);if(d){var e=d.offset().top-b.offset().top;(e+d.outerHeight()>b.outerHeight()||0>e)&&b.scrollTop(b.scrollTop()+d.position().top-d.outerHeight()),d.addClass("ui-timepicker-selected")}}}function m(b,c){if(""!==this.value&&"timepicker"!=c){var d=a(this);if(!d.is(":focus")||b&&"change"==b.type){var e=d.data("timepicker-settings"),f=u(this.value,e);if(null===f)return void d.trigger("timeFormatError");var g=!1;null!==e.minTime&&fe.maxTime&&(g=!0),a.each(e.disableTimeRanges,function(){return f>=this[0]&&f0?(f=a(c),!1):void 0}),f.addClass("ui-timepicker-selected")),!1;case 40:return f=e.find(".ui-timepicker-selected"),0===f.length?(e.find("li").each(function(b,c){return a(c).position().top>0?(f=a(c),!1):void 0}),f.addClass("ui-timepicker-selected")):f.is(":last-child")||(f.removeClass("ui-timepicker-selected"),f.next().addClass("ui-timepicker-selected"),f.next().position().top+2*f.outerHeight()>e.outerHeight()&&e.scrollTop(e.scrollTop()+f.outerHeight())),!1;case 27:e.find("li").removeClass("ui-timepicker-selected"),y.hide();break;case 9:y.hide();break;default:return!0}}function q(c){var d=a(this),e=d.data("timepicker-list"),f=d.data("timepicker-settings");if(!e||!b(e)||f.disableTextInput)return!0;switch(c.keyCode){case 96:case 97:case 98:case 99:case 100:case 101:case 102:case 103:case 104:case 105:case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 65:case 77:case 80:case 186:case 8:case 46:f.typeaheadHighlight?l(d,e):e.hide()}}function r(a){var b=a.data("timepicker-settings"),c=a.data("timepicker-list"),d=null,e=c.find(".ui-timepicker-selected");return e.hasClass("ui-timepicker-disabled")?!1:(e.length&&(d=e.data("time")),null!==d&&("string"!=typeof d&&(d=t(d,b)),o(a,d,"select")),!0)}function s(a,b){a=Math.abs(a);var c,d,e=Math.round(a/60),f=[];return 60>e?f=[e,x.mins]:(c=Math.floor(e/60),d=e%60,30==b&&30==d&&(c+=x.decimal+5),f.push(c),f.push(1==c?x.hr:x.hrs),30!=b&&d&&(f.push(d),f.push(x.mins))),f.join(" ")}function t(b,c){if(null===b)return null;var d=new Date(v.valueOf()+1e3*b);if(isNaN(d.getTime()))return null;if("function"===a.type(c.timeFormat))return c.timeFormat(d);for(var e,f,g="",h=0;h11?x.pm:x.am;break;case"A":g+=d.getHours()>11?x.PM:x.AM;break;case"g":e=d.getHours()%12,g+=0===e?"12":e;break;case"G":e=d.getHours(),b===w&&(e=24),g+=e;break;case"h":e=d.getHours()%12,0!==e&&10>e&&(e="0"+e),g+=0===e?"12":e;break;case"H":e=d.getHours(),b===w&&(e=24),g+=e>9?e:"0"+e;break;case"i":var i=d.getMinutes();g+=i>9?i:"0"+i;break;case"s":b=d.getSeconds(),g+=b>9?b:"0"+b;break;case"\\":h++,g+=c.timeFormat.charAt(h);break;default:g+=f}return g}function u(a,b){if(""===a)return null;if(!a||a+0==a)return a;if("object"==typeof a)return 3600*a.getHours()+60*a.getMinutes()+a.getSeconds();a=a.toLowerCase().replace(/[\s\.]/g,""),("a"==a.slice(-1)||"p"==a.slice(-1))&&(a+="m");var c="("+x.am.replace(".","")+"|"+x.pm.replace(".","")+"|"+x.AM.replace(".","")+"|"+x.PM.replace(".","")+")?",d=new RegExp("^"+c+"([0-2]?[0-9])\\W?([0-5][0-9])?\\W?([0-5][0-9])?"+c+"$"),e=a.match(d);if(!e)return null;var f=parseInt(1*e[2],10),g=e[1]||e[5],h=f;if(12>=f&&g){var i=g==x.pm||g==x.PM;h=12==f?i?12:0:f+(i?12:0)}var j=1*e[3]||0,k=1*e[4]||0,l=3600*h+60*j+k;if(12>f&&!g&&b&&b._twelveHourTime&&b.scrollDefault){var m=l-b.scrollDefault();0>m&&m>=w/-2&&(l=(l+w/2)%w)}return l}var v=g(),w=86400,x={am:"am",pm:"pm",AM:"AM",PM:"PM",decimal:".",mins:"mins",hr:"hr",hrs:"hrs"},y={init:function(b){return this.each(function(){var e=a(this),f=[];for(var g in a.fn.timepicker.defaults)e.data(g)&&(f[g]=e.data(g));var h=a.extend({},a.fn.timepicker.defaults,f,b);if(h.lang&&(x=a.extend(x,h.lang)),h=c(h),e.data("timepicker-settings",h),e.addClass("ui-timepicker-input"),h.useSelect)d(e);else{if(e.prop("autocomplete","off"),h.showOn)for(i in h.showOn)e.on(h.showOn[i]+".timepicker",y.show);e.on("change.timepicker",m),e.on("keydown.timepicker",p),e.on("keyup.timepicker",q),h.disableTextInput&&e.on("keypress.timepicker",function(a){a.preventDefault()}),m.call(e.get(0))}})},show:function(c){var e=a(this),f=e.data("timepicker-settings");if(c&&c.preventDefault(),f.useSelect)return void e.data("timepicker-list").focus();j(e)&&e.blur();var g=e.data("timepicker-list");if(!e.prop("readonly")&&(g&&0!==g.length&&"function"!=typeof f.durationTime||(d(e),g=e.data("timepicker-list")),!b(g))){e.data("ui-timepicker-value",e.val()),l(e,g),y.hide(),g.show();var i={};f.orientation.match(/r/)?i.left=e.offset().left+e.outerWidth()-g.outerWidth()+parseInt(g.css("marginLeft").replace("px",""),10):i.left=e.offset().left+parseInt(g.css("marginLeft").replace("px",""),10);var m;m=f.orientation.match(/t/)?"t":f.orientation.match(/b/)?"b":e.offset().top+e.outerHeight(!0)+g.outerHeight()>a(window).height()+a(window).scrollTop()?"t":"b","t"==m?(g.addClass("ui-timepicker-positioned-top"),i.top=e.offset().top-g.outerHeight()+parseInt(g.css("marginTop").replace("px",""),10)):(g.removeClass("ui-timepicker-positioned-top"),i.top=e.offset().top+e.outerHeight()+parseInt(g.css("marginTop").replace("px",""),10)),g.offset(i);var o=g.find(".ui-timepicker-selected");if(o.length||(n(e)?o=k(e,g,u(n(e))):f.scrollDefault&&(o=k(e,g,f.scrollDefault()))),o&&o.length){var p=g.scrollTop()+o.position().top-o.outerHeight();g.scrollTop(p)}else g.scrollTop(0);return f.stopScrollPropagation&&a(document).on("wheel.ui-timepicker",".ui-timepicker-wrapper",function(b){b.preventDefault();var c=a(this).scrollTop();a(this).scrollTop(c+b.originalEvent.deltaY)}),a(document).on("touchstart.ui-timepicker mousedown.ui-timepicker",h),a(window).on("resize.ui-timepicker",h),f.closeOnWindowScroll&&a(document).on("scroll.ui-timepicker",h),e.trigger("showTimepicker"),this}},hide:function(c){var d=a(this),e=d.data("timepicker-settings");return e&&e.useSelect&&d.blur(),a(".ui-timepicker-wrapper").each(function(){var c=a(this);if(b(c)){var d=c.data("timepicker-input"),e=d.data("timepicker-settings");e&&e.selectOnBlur&&r(d),c.hide(),d.trigger("hideTimepicker")}}),this},option:function(b,e){return this.each(function(){var f=a(this),g=f.data("timepicker-settings"),h=f.data("timepicker-list");if("object"==typeof b)g=a.extend(g,b);else if("string"==typeof b&&"undefined"!=typeof e)g[b]=e;else if("string"==typeof b)return g[b];g=c(g),f.data("timepicker-settings",g),h&&(h.remove(),f.data("timepicker-list",!1)),g.useSelect&&d(f)})},getSecondsFromMidnight:function(){return u(n(this))},getTime:function(a){var b=this,c=n(b);if(!c)return null;var d=u(c);if(null===d)return null;a||(a=new Date);var e=new Date(a);return e.setHours(d/3600),e.setMinutes(d%3600/60),e.setSeconds(d%60),e.setMilliseconds(0),e},setTime:function(a){var b=this,c=b.data("timepicker-settings");if(c.forceRoundTime)var d=f(u(a),c);else var d=t(u(a),c);return a&&null===d&&c.noneOption&&(d=a),o(b,d),b.data("timepicker-list")&&l(b,b.data("timepicker-list")),this},remove:function(){var a=this;if(a.hasClass("ui-timepicker-input")){var b=a.data("timepicker-settings");return a.removeAttr("autocomplete","off"),a.removeClass("ui-timepicker-input"),a.removeData("timepicker-settings"),a.off(".timepicker"),a.data("timepicker-list")&&a.data("timepicker-list").remove(),b.useSelect&&a.show(),a.removeData("timepicker-list"),this}}};a.fn.timepicker=function(b){return this.length?y[b]?this.hasClass("ui-timepicker-input")?y[b].apply(this,Array.prototype.slice.call(arguments,1)):this:"object"!=typeof b&&b?void a.error("Method "+b+" does not exist on jQuery.timepicker"):y.init.apply(this,arguments):this},a.fn.timepicker.defaults={className:null,minTime:null,maxTime:null,durationTime:null,step:30,showDuration:!1,showOnFocus:!0,showOn:["click","focus"],timeFormat:"g:ia",scrollDefault:null,selectOnBlur:!1,disableTextInput:!1,disableTouchKeyboard:!1,forceRoundTime:!1,roundingFunction:function(a,b){if(null===a)return null;var c=a%(60*b.step);return c>=30*b.step?a+=60*b.step-c:a-=c,a},appendTo:"body",orientation:"l",disableTimeRanges:[],closeOnWindowScroll:!1,typeaheadHighlight:!0,noneOption:!1,show2400:!1,stopScrollPropagation:!1}});
\ No newline at end of file
diff --git a/web_app/js/sif_tools.js b/web_app/js/sif_tools.js
index bb164d7..bc89759 100644
--- a/web_app/js/sif_tools.js
+++ b/web_app/js/sif_tools.js
@@ -20,98 +20,240 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
-// Tweets by @sifen_trackbot
+// TODO:
+// =====
+// * General improvements:
+// - needs a lot more error/bounds checking in general
+// * Card Level Calc:
+// - maybe also the super (1.5) and ultra (2.0) success bonuses too?
+// * Event Tracker:
+// - @sifen_trackbot update code gets "stuck" sometimes - not sure if the problem
+// is in the twitter fetcher or elsewhere
// Set to 0 to disable debugging, 1+ to enable debugging (higher = more verbose)
var DEBUG_LEVEL = 0;
// EXP tables
-var exp_table_n = [-1, 0, 6, 18, 28, 40, 51, 61, 72, 82, 93, 104, 114, 124, 135, 145, 156, 165, 176, 187, 196, 207, 217, 226, 238, 247, 257, 268, 277, 288, 297, 308, 317, 328, 337, 348, 358, 367, 377, 388, 397];
-var exp_table_r = [-1, 0, 14, 31, 45, 55, 67, 76, 85, 94, 103, 110, 119, 125, 134, 140, 148, 155, 161, 168, 174, 181, 187, 193, 199, 206, 211, 217, 223, 228, 235, 240, 245, 251, 256, 262, 267, 272, 277, 283, 288, 292, 298, 303, 308, 313, 317, 323, 327, 332, 337, 342, 346, 351, 356, 360, 365, 370, 374, 378, 383];
-var exp_table_sr = [-1, 0, 54, 98, 127, 150, 169, 187, 203, 218, 232, 245, 257, 269, 281, 291, 302, 311, 322, 331, 340, 349, 358, 366, 374, 383, 391, 398, 406, 413, 421, 428, 435, 442, 449, 456, 462, 469, 475, 482, 488, 494, 500, 507, 512, 518, 524, 530, 536, 541, 547, 552, 558, 563, 568, 574, 579, 584, 590, 594, 600, 605, 609, 615, 619, 625, 629, 634, 639, 643, 648, 653, 657, 662, 667, 670, 676, 680, 684, 689, 693];
-var exp_table_ur = [-1, 0, 201, 294, 345, 382, 411, 438, 460, 481, 499, 517, 532, 547, 561, 574, 587, 598, 611, 621, 631, 642, 651, 661, 670, 679, 687, 696, 704, 712, 720, 727, 734, 742, 749, 755, 763, 769, 775, 782, 788, 794, 800, 806, 812, 818, 823, 829, 834, 840, 845, 850, 856, 860, 866, 870, 875, 880, 885, 890, 894, 899, 903, 908, 912, 917, 921, 925, 930, 933, 938, 942, 946, 950, 954, 959, 961, 966, 970, 974, 977, 981, 985, 988, 992, 996, 999, 1003, 1006, 1010, 1013, 1017, 1020, 1024, 1027, 1030, 1034, 1037, 1040, 1043, 1047];
+// from: http://www59.atwiki.jp/lovelive-sif/pages/32.html
+var exp_table_n = [ -1, 0, 6, 18, 28, 40, 51, 61, 72, 82, 93, 104, 114, 124, 135, 145, 156, 165, 176, 187, 196, 207, 217, 226, 238, 247, 257, 268, 277, 288, 297, 308, 317, 328, 337, 348, 358, 367, 377, 388, 397 ];
+
+var exp_table_r = [ -1, 0, 14, 31, 45, 55, 67, 76, 85, 94, 103, 110, 119, 125, 134, 140, 148, 155, 161, 168, 174, 181, 187, 193, 199, 206, 211, 217, 223, 228, 235, 240, 245, 251, 256, 262, 267, 272, 277, 283, 288, 292, 298, 303, 308, 313, 317, 323, 327, 332, 337, 342, 346, 351, 356, 360, 365, 370, 374, 378, 383 ];
+
+var exp_table_sr = [ -1, 0, 54, 98, 127, 150, 169, 187, 203, 218, 232, 245, 257, 269, 281, 291, 302, 311, 322, 331, 340, 349, 358, 366, 374, 383, 391, 398, 406, 413, 421, 428, 435, 442, 449, 456, 462, 469, 475, 482, 488, 494, 500, 507, 512, 518, 524, 530, 536, 541, 547, 552, 558, 563, 568, 574, 579, 584, 590, 594, 600, 605, 609, 615, 619, 625, 629, 634, 639, 643, 648, 653, 657, 662, 667, 670, 676, 680, 684, 689, 693 ];
+
+var exp_table_ur = [ -1, 0, 201, 294, 345, 382, 411, 438, 460, 481, 499, 517, 532, 547, 561, 574, 587, 598, 611, 621, 631, 642, 651, 661, 670, 679, 687, 696, 704, 712, 720, 727, 734, 742, 749, 755, 763, 769, 775, 782, 788, 794, 800, 806, 812, 818, 823, 829, 834, 840, 845, 850, 856, 860, 866, 870, 875, 880, 885, 890, 894, 899, 903, 908, 912, 917, 921, 925, 930, 933, 938, 942, 946, 950, 954, 959, 961, 966, 970, 974, 977, 981, 985, 988, 992, 996, 999, 1003, 1006, 1010, 1013, 1017, 1020, 1024, 1027, 1030, 1034, 1037, 1040, 1043, 1047 ];
+
+// global variable to keep event state, because we need it to live between function calls.
+// YES I KNOW THIS IS BAD. SO SUE ME. IT WORKS THOUGH. :P
+// 1 = token event, 2 = score match, 3 = medfes
+var current_type_of_event = 1;
// debug logging
-function LOG(level, msg)
-{
- if (DEBUG_LEVEL > 0 && DEBUG_LEVEL >= level) {
- console.log(msg);
- }
+function LOG(level, msg) {
+ if (DEBUG_LEVEL > 0 && DEBUG_LEVEL >= level) {
+ console.log(msg);
+ }
}
// Main function, runs automatically at document-ready (i.e. when the page is finished loading)
-$(document).ready(function(){
- // set up UI (buttons, etc.)
- setup_ui_elements();
- // set up button handlers
- setup_button_handlers();
- // set up slider handlers
+$(document).ready(function() {
+ // Hide the address bar on mobile browsers
+ setTimeout(function() {
+ // some sites suggest 0,0 and others 0,1 - not sure which is correct
+ window.scrollTo(0, 0);
+ }, 0);
+ // set up UI (buttons, etc.)
+ setup_ui_elements();
+ // set up button handlers
+ setup_button_handlers();
+ // set up slider handlers
window.timerInterval = 0;
});
// Set up UI elements (tabs, buttons, etc.)
-function setup_ui_elements()
-{
- LOG(1, "setup_ui_elements()");
-
- $( "#tabs" ).tabs({
- active: 0,
- create: function(event, ui) {
- var theTab = ui.tab.index();
- LOG(3, "INIT tab created " + theTab);
- set_up_tab(theTab);
- },
- activate: function(event, ui) {
- var theTab = ui.newTab.index();
- LOG(3, "INIT tab selected " + theTab);
- set_up_tab(theTab);
- }
- });
-
- // set up keypads
- $( "#current_rank" ).keypad(); // {prompt: 'Enter here'}
- $( "#current_exp" ).keypad(); // {prompt: 'Enter here'}
- $( "#desired_rank" ).keypad(); // {prompt: 'Enter here'}
- $( "#current_gems" ).keypad(); // {prompt: 'Enter here'}
- $( "#gem_desired_gems" ).keypad(); // {prompt: 'Enter here'}
- $( "#card_current_level" ).keypad(); // {prompt: 'Enter here'}
- $( "#card_current_exp" ).keypad(); // {prompt: 'Enter here'}
- $( "#card_desired_level" ).keypad(); // {prompt: 'Enter here'}
- $( "#card_feed_exp" ).keypad(); // {prompt: 'Enter here'}
-
+function setup_ui_elements() {
+ LOG(1, "setup_ui_elements()");
+ var QueryString = function() {
+ // http://stackoverflow.com/a/979995
+ // This function is anonymous, is executed immediately and
+ // the return value is assigned to QueryString!
+ var query_string = {};
+ var query = window.location.search.substring(1);
+ var vars = query.split("&");
+ for (var i = 0; i < vars.length; i++) {
+ var pair = vars[i].split("=");
+ // If first entry with this name
+ if (typeof query_string[pair[0]] === "undefined") {
+ query_string[pair[0]] = decodeURIComponent(pair[1]);
+ } else if (typeof query_string[pair[0]] === "string") {
+ var arr = [ query_string[pair[0]], decodeURIComponent(pair[1]) ];
+ query_string[pair[0]] = arr;
+ } else {
+ query_string[pair[0]].push(decodeURIComponent(pair[1]));
+ }
+ }
+ return query_string;
+ }();
+ var default_tab = 0;
+ if (!isNaN(QueryString.tab)) {
+ if (QueryString.tab >= 1 && QueryString.tab <= 4) {
+ default_tab = QueryString.tab - 1;
+ }
+ } else {
+ var default_tab_from_cookie = $.cookie("default_tab");
+ if (isNaN(default_tab_from_cookie)) {
+ default_tab = 0;
+ } else {
+ default_tab = default_tab_from_cookie;
+ }
+ }
+ $("#tabs").tabs({
+ active: default_tab,
+ create: function(event, ui) {
+ var theTab = ui.tab.index();
+ LOG(3, "INIT tab created " + theTab);
+ set_up_tab(theTab);
+ },
+ activate: function(event, ui) {
+ var theTab = ui.newTab.index();
+ LOG(3, "INIT tab selected " + theTab);
+ set_up_tab(theTab);
+ }
+ });
+ // only set up keypads on mobile browsers
+ // not sure what the best way of doing this is
+ var pageWidth = $(window).width();
+ if (pageWidth < 1024) {
+ $("#current_rank").prop("readonly", true);
+ $("#current_rank").keypad();
+ // {prompt: 'Enter here'}
+ $("#current_exp").prop("readonly", true);
+ $("#current_exp").keypad();
+ // {prompt: 'Enter here'}
+ $("#desired_rank").prop("readonly", true);
+ $("#desired_rank").keypad();
+ // {prompt: 'Enter here'}
+ $("#current_gems").prop("readonly", true);
+ $("#current_gems").keypad();
+ // {prompt: 'Enter here'}
+ $("#gem_desired_gems").prop("readonly", true);
+ $("#gem_desired_gems").keypad();
+ // {prompt: 'Enter here'}
+ $("#card_current_level").prop("readonly", true);
+ $("#card_current_level").keypad();
+ // {prompt: 'Enter here'}
+ $("#card_current_exp").prop("readonly", true);
+ $("#card_current_exp").keypad();
+ // {prompt: 'Enter here'}
+ $("#card_desired_level").prop("readonly", true);
+ $("#card_desired_level").keypad();
+ // {prompt: 'Enter here'}
+ $("#card_feed_exp").prop("readonly", true);
+ $("#card_feed_exp").keypad();
+ } else {
+ $("#current_rank").prop("readonly", false);
+ $("#current_exp").prop("readonly", false);
+ $("#desired_rank").prop("readonly", false);
+ $("#current_gems").prop("readonly", false);
+ $("#gem_desired_gems").prop("readonly", false);
+ $("#card_current_level").prop("readonly", false);
+ $("#card_current_exp").prop("readonly", false);
+ $("#card_desired_level").prop("readonly", false);
+ $("#card_feed_exp").prop("readonly", false);
+ }
// set up date/time pickers
- $( "#gem_desired_date" ).datepicker();
- $( "#event_end_date" ).datepicker();
- $( "#event_end_time" ).timepicker();
-
- // set up buttons
- ["calculate-rank", "reset-rank", "calculate-gems", "reset-gems", "calculate-card", "reset-card", "start-stop-timer", "clear-timer"].forEach(function(entry) {
- var selector = "#button-" + entry;
- LOG(1, "setting up " + selector);
- $(selector).button();
- });
-
+ $("#gem_desired_date").datepicker();
+ $("#event_end_date").datepicker();
+ // $( "#event_end_time" ).timepicker();
+ $("#event_end_time").timepicker({
+ timeFormat: "H:i",
+ disableTextInput: true
+ });
+ // set up buttons
+ [ "calculate-rank", "reset-rank", "calculate-gems", "reset-gems", "card-max-level", "calculate-card", "reset-card", "start-stop-timer", "clear-timer" ].forEach(function(entry) {
+ var selector = "#button-" + entry;
+ LOG(1, "setting up " + selector);
+ $(selector).button();
+ });
// hide result divs
- ["rank-calc-result-area", "gem-calc-result-area", "card-calc-result-area"].forEach(function(entry) {
+ [ "rank-calc-result-area", "gem-calc-result-area", "card-calc-result-area" ].forEach(function(entry) {
var selector = "#" + entry;
LOG(1, "setting up " + selector);
$(selector).hide();
});
-
// set up radio button listeners
$("input[name=gem-mode]").change(handle_gem_mode_select);
$("input[name=card-mode]").change(handle_card_mode_select);
-
- // hide non-selected option divs
- ["gem-desired-gems-area", "card-exp-area"].forEach(function(entry) {
- var selector = "#" + entry;
- LOG(1, "setting up " + selector);
- $(selector).hide();
+ // set up checkbox change event handler
+ $("#gems_include_events").change(function() {
+ update_ui();
});
+ // set up gem event calc note dialog
+ // $("#dialog").dialog({ autoOpen: false });
+ $("a#gem_event_readme").click(function(e) {
+ e.preventDefault();
+ $("#gem_event_readme_dialog").dialog({
+ height: 300
+ });
+ });
+ $("a#gem_quest_readme").click(function(e) {
+ e.preventDefault();
+ $("#gem_quest_readme_dialog").dialog({
+ height: 300
+ });
+ });
+ // set up show/hide JP-only options
+ $("#gem_game_version").on("change", function(e) {
+ update_ui();
+ });
+ // update the UI based on what is selected
+ update_ui();
}
-function setup_button_handlers()
+// show/hide option div's based on which modes are selected
+function update_ui()
{
+ // gem screen - show/hide the options based on game version
+ var gem_game_version = $("#gem_game_version").val();
+ if (gem_game_version === "JP") {
+ $("#gem_jp_daily_gems").show();
+ } else {
+ $("#gem_jp_daily_gems").hide();
+ }
+ // gem screen - hide event tier selector
+ var gem_events_selected = $("#gems_include_events").is(":checked");
+ if (gem_events_selected) {
+ $("#gem-event-options-area").show();
+ } else {
+ $("#gem-event-options-area").hide();
+ }
+ // gem screen - mode
+ var gem_mode = $("#gem-mode").val();
+ switch(gem_mode) {
+ case "DATE":
+ $("#gem-date-area").show();
+ $("#gem-desired-gems-area").hide();
+ break;
+ case "GEMS":
+ $("#gem-date-area").hide();
+ $("#gem-desired-gems-area").show();
+ break;
+ }
+ // card screen - mode
+ var card_mode = $("#card-mode").val();
+ switch(card_mode) {
+ case "LEVEL":
+ $("#card-level-area").show();
+ $("#card-exp-area").hide();
+ break;
+ case "EXP":
+ $("#card-level-area").hide();
+ $("#card-exp-area").show();
+ break;
+ }
+}
+
+function setup_button_handlers() {
$("#button-calculate-rank").click(function(evt) {
calculate_rank();
});
@@ -124,6 +266,9 @@ function setup_button_handlers()
$("#button-reset-gems").click(function(evt) {
reset_gems();
});
+ $("#button-card-max-level").click(function(evt) {
+ card_set_max_level();
+ });
$("#button-calculate-card").click(function(evt) {
calculate_card();
});
@@ -139,39 +284,47 @@ function setup_button_handlers()
}
// tab functions
+function set_up_tab(tab) {
+ switch (tab) {
+ case 0:
+ rank_calc_tab_selected();
+ break;
-function set_up_tab(tab)
-{
- switch(tab) {
- case 0: rank_calc_tab_selected(); break;
- case 1: love_gem_calc_tab_selected(); break;
- case 2: card_level_calc_tab_selected(); break;
- case 3: event_end_calc_tab_selected(); break;
+ case 1:
+ love_gem_calc_tab_selected();
+ break;
+
+ case 2:
+ card_level_calc_tab_selected();
+ break;
+
+ case 3:
+ event_end_calc_tab_selected();
+ break;
}
}
-function rank_calc_tab_selected()
-{
+function rank_calc_tab_selected() {
LOG(1, "rank_calc_tab_selected");
+ $.cookie("default_tab", 0);
}
-function love_gem_calc_tab_selected()
-{
- LOG(1, "love_gem_calc_tab_selected");
+function love_gem_calc_tab_selected() {
+ LOG(1, "love_gem_calc_tab_selected");
+ $.cookie("default_tab", 1);
}
-function card_level_calc_tab_selected()
-{
- LOG(1, "card_level_calc_tab_selected");
+function card_level_calc_tab_selected() {
+ LOG(1, "card_level_calc_tab_selected");
+ $.cookie("default_tab", 2);
}
-function event_end_calc_tab_selected()
-{
- LOG(1, "event_end_calc_tab_selected");
+function event_end_calc_tab_selected() {
+ LOG(1, "event_end_calc_tab_selected");
+ $.cookie("default_tab", 3);
}
-function calculate_rank()
-{
+function calculate_rank() {
// validate data
var current_rank = parseInt($("#current_rank").val());
var current_exp_input = $("#current_exp").val();
@@ -190,17 +343,17 @@ function calculate_rank()
} else if (desired_rank <= current_rank) {
window.alert("Error: desired rank must be greater than current rank.");
} else {
- var required_exp = 0
- for (rank = current_rank ; rank < desired_rank ; rank++) {
- var required_exp_for_next_rank = Math.round(34.45 * rank - 551)
+ var required_exp = 0;
+ for (rank = current_rank; rank < desired_rank; rank++) {
+ var required_exp_for_next_rank = Math.round(34.45 * rank - 551);
// account for half EXP on JP (only if rank < 100)
if (game_version === "JP" && rank < 100) {
required_exp_for_next_rank /= 2;
}
- required_exp = required_exp + required_exp_for_next_rank
+ required_exp = required_exp + required_exp_for_next_rank;
}
// account for exp we already have
- required_exp -= current_exp
+ required_exp -= current_exp;
// convert to integer
required_exp = Math.round(required_exp);
// now calc the # of songs needed
@@ -209,17 +362,33 @@ function calculate_rank()
var normal_count = Math.round(required_exp / 26);
var hard_count = Math.round(required_exp / 46);
var expert_count = Math.round(required_exp / 83);
+ var m3_easy_count = Math.round(required_exp / (12*3));
+ var m3_normal_count = Math.round(required_exp / (26*3));
+ var m3_hard_count = Math.round(required_exp / (46*3));
+ var m3_expert_count = Math.round(required_exp / (83*3));
+ // Increases the amount of EXP gained by 10%.
+ var m3b_easy_count = Math.round(required_exp / (12*1.1*3));
+ var m3b_normal_count = Math.round(required_exp / (26*1.1*3));
+ var m3b_hard_count = Math.round(required_exp / (46*1.1*3));
+ var m3b_expert_count = Math.round(required_exp / (83*1.1*3));
// calc LP and FP
- var LP = 25 + Math.floor(Math.min(desired_rank, 300) / 2) + Math.floor(Math.max(desired_rank - 300, 0) / 3)
+ var LP = 25 + Math.floor(Math.min(desired_rank, 300) / 2) + Math.floor(Math.max(desired_rank - 300, 0) / 3);
// calc friend slots
- var friend_slots = 10 + Math.floor(Math.min(desired_rank, 50) / 5) + Math.floor(Math.max(desired_rank - 50, 0) / 10)
-
+ var friend_slots = 10 + Math.floor(Math.min(desired_rank, 50) / 5) + Math.floor(Math.max(desired_rank - 50, 0) / 10);
// display the results
$("#rank-result-exp").text(required_exp);
$("#rank-result-songs-easy").text(easy_count);
$("#rank-result-songs-normal").text(normal_count);
$("#rank-result-songs-hard").text(hard_count);
$("#rank-result-songs-expert").text(expert_count);
+ $("#rank-result-songs-easy-mf").text(m3_easy_count);
+ $("#rank-result-songs-normal-mf").text(m3_normal_count);
+ $("#rank-result-songs-hard-mf").text(m3_hard_count);
+ $("#rank-result-songs-expert-mf").text(m3_expert_count);
+ $("#rank-result-songs-easy-mfb").text(m3b_easy_count);
+ $("#rank-result-songs-normal-mfb").text(m3b_normal_count);
+ $("#rank-result-songs-hard-mfb").text(m3b_hard_count);
+ $("#rank-result-songs-expert-mfb").text(m3b_expert_count);
// rank-results-lp">- LP and 2 || game_version === "JP" && current_type_of_event > 3) {
+ current_type_of_event = 1;
+ }
+ }
+ LOG(1, "end event type is " + current_type_of_event);
+ LOG(1, return_tuple);
+ // now return what we got
+ return return_tuple;
+}
+
+function calculate_gems() {
+ // reset current event indicator (start out with a token event)
+ current_type_of_event = 1;
+ var verbose = $("#gems_verbose").is(":checked");
var current_gems_text = $("#current_gems").val();
var current_gems = 0;
if (current_gems_text != "") {
@@ -366,36 +641,43 @@ function calculate_gems()
alert("Error: invalid number of current gems. Please check your input and try again.");
return;
}
+ var tier = parseInt($("#gems_tier_level").val());
+ var game_version = $("#gem_game_version").val();
+ var calc_daily_quest_gems = false;
+ var calc_event_gems = $("#gems_include_events").is(":checked");
+ if (game_version === "JP") {
+ calc_daily_quest_gems = $("#gems_include_daily_gems").is(":checked");
+ }
var mode = $("input[name=gem-mode]:checked").val();
- if (mode === "DATE") {
+ if (mode === "DATE") {
var target_date = $("#gem_desired_date").val();
if (target_date === "") {
alert("Error: invalid date. Please check and try again.");
return;
}
var target_date_object = moment(new Date(target_date));
- if (!target_date_object.isValid()) {
+ if (!target_date_object.isValid()) {
alert("Error: invalid date. Please check and try again.");
return;
}
-
var now = moment(new Date());
if (target_date_object.isBefore(now) || is_same_day(now, target_date_object)) {
window.alert("Error: the date must be in the future.");
return;
}
-
// ready to rock
var resultsString = sprintf("Today is %02d/%02d/%04d and you currently have %d love gems. (Assuming you collected any gems you got today and already counted those.)", month(now), day(now), year(now), current_gems);
var verboseText = "";
- var gems = current_gems
- now = now.add(1, 'days')
+ if (calc_daily_quest_gems) {
+ verboseText = "(Including daily 'quest' gems in the calculation. There will not be a separate daily entry for each one.)
";
+ }
+ var gems = current_gems;
+ now = now.add(1, "days");
while (now.isBefore(target_date_object) || is_same_day(now, target_date_object)) {
// is it a login bonus?
if (is_gem_day(now)) {
gems += 1;
}
-
// is it a birthday?
var birthday_tuple = is_muse_members_birthday(now);
var is_bday = birthday_tuple[0];
@@ -403,25 +685,72 @@ function calculate_gems()
if (is_bday) {
gems += 5;
}
-
- // record verbose output if desired
- if (verbose) {
- if (is_gem_day(now) && is_bday) {
- verboseText += sprintf("%02d/%02d/%04d Free gem as login bonus AND it's %s's birthday! You get 6 gems, which brings you to %d gems.
", month(now), day(now), year(now), name, gems);
- }
-
- if (is_bday && !is_gem_day(now)) {
- verboseText += sprintf("%02d/%02d/%04d It's %s's birthday! You get 5 gems, which brings you to %d gems.
", month(now), day(now), year(now), name, gems);
- }
-
- if (is_gem_day(now) && !is_bday) {
- verboseText = verboseText + sprintf("%02d/%02d/%04d Free gem as login bonus, which brings you to %d gems.
", month(now), day(now), year(now), gems);
+ // account for daily login quest gem
+ if (calc_daily_quest_gems) {
+ gems++;
+ }
+ if (calc_event_gems) {
+ // account for event
+ // format of returned tuple:
+ // tuple[0] - was this an event day? (boolean, duh)
+ // tuple[1] - name of event, or "" if none (string)
+ // tuple[2] - amount of gems spent (int)
+ // tuple[3] - amount of gems gained (int)
+ var event_results = calculate_event(day(now), game_version, tier);
+ var is_event = event_results[0];
+ var event_name = "";
+ var spent_gems = 0;
+ var won_gems = 0;
+ if (is_event) {
+ event_name = event_results[1];
+ spent_gems = event_results[2];
+ won_gems = event_results[3];
+ // did any gems get spent?
+ if (spent_gems > 0) {
+ // do we have enough to cover it?
+ if (gems >= spent_gems) {
+ // spend the gems
+ gems -= spent_gems;
+ // now reap the winnings
+ gems += won_gems;
+ } else {
+ // flag to indicate that we didn't have the gems
+ spent_gems = -1;
+ }
+ } else {
+ gems += won_gems;
+ }
}
}
-
- now = now.add(1, 'days')
+ // record verbose output if desired
+ if (verbose) {
+ if (is_gem_day(now) || is_bday || is_event) {
+ verboseText += sprintf("%02d/%02d/%04d ", month(now), day(now), year(now));
+ if (is_gem_day(now)) {
+ verboseText += "Free gem as login bonus! ";
+ }
+ if (is_bday) {
+ verboseText += sprintf("It's %s's birthday! You get 5 gems! ", name);
+ }
+ // account for events
+ if (is_event) {
+ verboseText += sprintf("A " + event_name + " just ended! ", month(now), day(now), year(now), event_name);
+ if (spent_gems == -1) {
+ verboseText += "You didn't have enough gems to participate. ";
+ } else {
+ if (spent_gems == 0) {
+ verboseText += sprintf("You didn't have to spend any gems, and you won %d gems! ", won_gems);
+ } else {
+ verboseText += sprintf("You spent %d gems, and you won %d gems. ", spent_gems, won_gems);
+ }
+ }
+ }
+ // add a newline
+ verboseText += sprintf("That brings you to %d gems!
", gems);
+ }
+ }
+ now = now.add(1, "days");
}
-
resultsString = resultsString + sprintf(" You will have %d love gems on %02d/%02d/%04d. Good things come to those who wait!", gems, month(target_date_object), day(target_date_object), year(target_date_object));
$("#gem-result-summary").html(resultsString);
if (verbose) {
@@ -431,27 +760,29 @@ function calculate_gems()
$("#gem-result-verbose-area").html(verboseText);
$("#gem-result-textarea").hide();
}
- } else if (mode === "GEMS") {
+ } else if (mode === "GEMS") {
var target_gems = parseInt($("#gem_desired_gems").val());
if (isNaN(target_gems)) {
window.alert("Error: you have entered an invalid (non-numeric) value. Please check your input and try again.");
return;
}
-
var now = moment(new Date());
+ // arbitrary limit to make sure we don't go wander off into infinity
+ var cutoff = moment(now).add(5, "years");
var resultsString = sprintf("Today is %02d/%02d/%04d and you currently have %d love gems. (Assuming you collected any gems you got today and already counted those.)", month(now), day(now), year(now), current_gems);
var verboseText = "";
-
- var gems = current_gems
-
- while (gems < target_gems) {
- now = now.add(1, 'days')
-
+ if (calc_daily_quest_gems) {
+ verboseText = "(Including daily 'quest' gems in the calculation. There will not be a separate daily entry for each one.)
";
+ }
+ // make sure we don't go off into infinity (i.e. user gives input that is impossible to calculate)
+ var abort = false;
+ var gems = current_gems;
+ while (gems < target_gems && !abort) {
+ now = now.add(1, "days");
// is it a login bonus?
if (is_gem_day(now)) {
gems += 1;
}
-
// is it a birthday?
var birthday_tuple = is_muse_members_birthday(now);
var is_bday = birthday_tuple[0];
@@ -459,25 +790,80 @@ function calculate_gems()
if (is_bday) {
gems += 5;
}
-
- // record verbose output if desired
- if (verbose) {
- if (is_gem_day(now) && is_bday) {
- verboseText += sprintf("%02d/%02d/%04d Free gem as login bonus AND it's %s's birthday! You get 6 gems, which brings you to %d gems.
", month(now), day(now), year(now), name, gems);
- }
-
- if (is_bday && !is_gem_day(now)) {
- verboseText += sprintf("%02d/%02d/%04d It's %s's birthday! You get 5 gems, which brings you to %d gems.
", month(now), day(now), year(now), name, gems);
- }
-
- if (is_gem_day(now) && !is_bday) {
- verboseText = verboseText + sprintf("%02d/%02d/%04d Free gem as login bonus, which brings you to %d gems.
", month(now), day(now), year(now), gems);
+ // account for daily login quest gem
+ if (calc_daily_quest_gems) {
+ gems++;
+ }
+ if (calc_event_gems) {
+ // account for event
+ // format of returned tuple:
+ // tuple[0] - was this an event day? (boolean, duh)
+ // tuple[1] - name of event, or "" if none (string)
+ // tuple[2] - amount of gems spent (int)
+ // tuple[3] - amount of gems gained (int)
+ var event_results = calculate_event(day(now), game_version, tier);
+ var is_event = event_results[0];
+ var event_name = "";
+ var spent_gems = 0;
+ var won_gems = 0;
+ if (is_event) {
+ event_name = event_results[1];
+ spent_gems = event_results[2];
+ won_gems = event_results[3];
+ // did any gems get spent?
+ if (spent_gems > 0) {
+ // do we have enough to cover it?
+ if (gems >= spent_gems) {
+ // spend the gems
+ gems -= spent_gems;
+ // now reap the winnings
+ gems += won_gems;
+ } else {
+ // flag to indicate that we didn't have the gems
+ spent_gems = -1;
+ }
+ } else {
+ gems += won_gems;
+ }
}
}
+ // record verbose output if desired
+ if (verbose) {
+ if (is_gem_day(now) || is_bday || is_event) {
+ verboseText += sprintf("%02d/%02d/%04d ", month(now), day(now), year(now));
+ if (is_gem_day(now)) {
+ verboseText += "Free gem as login bonus! ";
+ }
+ if (is_bday) {
+ verboseText += sprintf("It's %s's birthday! You get 5 gems! ", name);
+ }
+ // account for events
+ if (is_event) {
+ verboseText += sprintf("A " + event_name + " just ended! ", month(now), day(now), year(now), event_name);
+ if (spent_gems == -1) {
+ verboseText += "You didn't have enough gems to participate. ";
+ } else {
+ if (spent_gems == 0) {
+ verboseText += sprintf("You didn't have to spend any gems, and you won %d gems! ", won_gems);
+ } else {
+ verboseText += sprintf("You spent %d gems, and you won %d gems. ", spent_gems, won_gems);
+ }
+ }
+ }
+ // add a newline
+ verboseText += sprintf("That brings you to %d gems!
", gems);
+ }
+ }
+ // do we abort?
+ if (now.isAfter(cutoff)) {
+ verboseText += sprintf("(Cannot proceed, this appears to be an unattainable amount of gems given your constraints.)");
+ abort = true;
+ }
}
-
resultsString = resultsString + sprintf(" You will have %d love gems on %02d/%02d/%04d. Good things come to those who wait!", gems, month(now), day(now), year(now));
-
+ if (abort) {
+ resultsString += " (Note: the calculation was stopped because it appears that you will be unable to attain the desired amount of gems given your constraints.)";
+ }
$("#gem-result-summary").html(resultsString);
if (verbose) {
$("#gem-result-verbose-area").html(verboseText);
@@ -487,27 +873,24 @@ function calculate_gems()
$("#gem-result-textarea").hide();
}
}
-
$("#gem-calc-result-area").show();
}
-function reset_gems()
-{
+function reset_gems() {
$("#gem-calc-result-area").hide();
$("#gem-result-summary").text("-");
$("#gem-desired-gems-area").hide();
$("#gem-date-area").hide();
$("#current_gems").val(0);
- var $radios = $('input:radio[name=gem-mode]');
- $radios.filter('[value=DATE]').prop('checked', true);
+ var $radios = $("input:radio[name=gem-mode]");
+ $radios.filter("[value=DATE]").prop("checked", true);
$("#gem_desired_date").val("");
$("#gem_desired_gems").val("");
- $('#gems_verbose').prop('checked', false);
+ $("#gems_verbose").prop("checked", false);
$("#gem-date-area").show();
}
-function get_level_cap(rarity)
-{
+function get_level_cap(rarity) {
if (rarity === "N") {
return 40;
} else if (rarity === "R") {
@@ -521,40 +904,51 @@ function get_level_cap(rarity)
return -1;
}
-function is_valid_level(rarity, level)
-{
+function is_valid_level(rarity, level) {
var return_value = false;
if (level >= 1) {
- return_value = (level <= get_level_cap(rarity));
+ return_value = level <= get_level_cap(rarity);
}
return return_value;
}
-function is_valid_exp(rarity, level, exp)
-{
+function is_valid_exp(rarity, level, exp) {
var return_value = false;
if (rarity === "N" && is_valid_level(rarity, level)) {
- return_value = (exp >= 0 && exp < exp_table_n[level+1]);
+ return_value = exp >= 0 && exp < exp_table_n[level + 1];
} else if (rarity === "R" && is_valid_level(rarity, level)) {
- return_value = (exp >= 0 && exp < exp_table_r[level+1]);
- } else if (rarity === "SR" && is_valid_level(rarity, level+1)) {
- return_value = (exp >= 0 && exp < exp_table_sr[level+1]);
+ return_value = exp >= 0 && exp < exp_table_r[level + 1];
+ } else if (rarity === "SR" && is_valid_level(rarity, level + 1)) {
+ return_value = exp >= 0 && exp < exp_table_sr[level + 1];
} else if (rarity === "UR" && is_valid_level(rarity, level)) {
- return_value = (exp >= 0 && exp < exp_table_ur[level+1]);
+ return_value = exp >= 0 && exp < exp_table_ur[level + 1];
}
- return return_value
+ return return_value;
}
-function calculate_card()
-{
+function get_exp_table_entry(rarity, level) {
+ var exp = 0;
+ if (rarity === "N") {
+ exp = exp_table_n[level];
+ } else if (rarity === "R") {
+ exp = exp_table_r[level];
+ } else if (rarity === "SR") {
+ exp = exp_table_sr[level];
+ } else if (rarity === "UR") {
+ exp = exp_table_ur[level];
+ }
+ return exp;
+}
+
+function calculate_card() {
var current_level = parseInt($("#card_current_level").val());
var current_exp_input = $("#card_current_exp").val();
+ var same_attribute = $("#card_same_attribute").is(":checked"); // 1.2x bonus for feeding same attribute cards
var current_exp = 0;
if (current_exp_input != "") {
current_exp = parseInt(current_exp_input);
}
var rarity = $("#card_rarity").val();
-
if (isNaN(current_level) || !is_valid_level(rarity, current_level)) {
alert("Error: invalid level. Please check your input and try again.");
return;
@@ -563,96 +957,79 @@ function calculate_card()
alert("Error: invalid EXP. Please check your input and try again.");
return;
}
-
var mode = $("input[name=card-mode]:checked").val();
- if (mode === "LEVEL") {
+ if (mode === "LEVEL") {
var target_level = parseInt($("#card_desired_level").val());
if (isNaN(target_level) || !is_valid_level(rarity, target_level)) {
window.alert("Error: the desired level is invalid.");
return;
}
-
// ready to rock
var required_exp = 0;
var resultsString = "";
- for (level = current_level+1; level <= target_level; level++) {
- if (rarity === "N") {
- required_exp += exp_table_n[level];
- } else if (rarity === "R") {
- required_exp += exp_table_r[level];
- } else if (rarity === "SR") {
- required_exp += exp_table_sr[level];
- } else if (rarity === "UR") {
- required_exp += exp_table_ur[level];
- }
+ for (level = current_level + 1; level <= target_level; level++) {
+ required_exp += get_exp_table_entry(rarity, level);
}
-
// subtract what we have
required_exp -= current_exp;
-
var resultString = sprintf("To get a %s card from level %d (with %d EXP) to %d requires %d EXP. ", rarity, current_level, current_exp, target_level, required_exp);
-
// calculate equiv N cards
- var number_of_n_cards = Math.round(required_exp / 100) + 1;
+ // FINDME
+ if (same_attribute) {
+ n_card_factor = 120;
+ } else {
+ n_card_factor = 100;
+ }
+ var number_of_n_cards = Math.round(required_exp / n_card_factor) + 1;
resultString += sprintf("(the equivalent of about %d level-1 N cards fed to it)", number_of_n_cards);
-
// output the result
$("#card-result-summary").html(resultString);
- } else if (mode === "EXP") {
+ } else if (mode === "EXP") {
var exp_to_feed = parseInt($("#card_feed_exp").val());
if (isNaN(exp_to_feed)) {
window.alert("Error: you have entered an invalid (non-numeric) value. Please check your input and try again.");
return;
}
-
+ var real_exp_to_feed = exp_to_feed;
// ready to rock
- var resultsString = "FOO";
-
+ var resultsString = "";
// XXX do some calculating
var exp_tally = current_exp;
var level = 0;
- for (level = current_level+1; level <= get_level_cap(rarity); level++) {
- if (rarity === "N") {
- exp_tally += exp_table_n[level];
- } else if (rarity === "R") {
- exp_tally += exp_table_r[level];
- } else if (rarity === "SR") {
- exp_tally += exp_table_sr[level];
- } else if (rarity === "UR") {
- exp_tally += exp_table_ur[level];
- }
- if (exp_tally > exp_to_feed) {
+ for (level = current_level + 1; level <= get_level_cap(rarity); level++) {
+ exp_tally += get_exp_table_entry(rarity, level);
+ if (exp_tally > real_exp_to_feed) {
break;
}
}
-
level--;
- var resultString = sprintf("If you feed a %s card at level %d (with %d EXP) a total of %d EXP, it will end up at level %d.%s", rarity, current_level, current_exp, exp_to_feed, level, (level == get_level_cap(rarity) ? " (MAX LEVEL!)" : ""));
-
+ var resultString = "";
+ if (exp_to_feed > exp_tally) {
+ resultString = sprintf("If you feed a %s card at level %d (with %d EXP) a total of %d EXP, it will end up at level %d.%s This is way overkill, you fed %d more EXP than was necessary.", rarity, current_level, current_exp, exp_to_feed, level, (level == get_level_cap(rarity) ? " (MAX LEVEL!)" : ""), exp_to_feed - exp_tally);
+ } else {
+ resultString = sprintf("If you feed a %s card at level %d (with %d EXP) a total of %d EXP, it will end up at level %d.%s", rarity, current_level, current_exp, exp_to_feed, level, level == get_level_cap(rarity) ? " (MAX LEVEL!)" : "");
+ }
// output the result
$("#card-result-summary").html(resultString);
}
-
$("#card-calc-result-area").show();
}
-function reset_card()
-{
+function reset_card() {
$("#card-calc-result-area").hide();
$("#card-result-summary").text("-");
$("#card-level-area").hide();
$("#card-exp-area").hide();
$("#card_current_level").val("");
$("#card_current_exp").val("");
- var $radios = $('input:radio[name=card-mode]');
- $radios.filter('[value=LEVEL]').prop('checked', true);
+ var $radios = $("input:radio[name=card-mode]");
+ $radios.filter("[value=LEVEL]").prop("checked", true);
$("#card_desired_level").val("");
$("#card_feed_exp").val("");
$("#card-level-area").show();
}
-function start_stop_timer()
-{
+function start_stop_timer() {
if (window.timerInterval != 0) {
// stop it
clearInterval(window.timerInterval);
@@ -662,7 +1039,7 @@ function start_stop_timer()
// 2013-02-08 09:30 # An hour and minute time part
var dateString = $("#event_end_date").val() + " " + $("#event_end_time").val() + "Z";
window.the_end = moment(dateString, "MM/DD/YYYY HH:mmZ");
- window.timerInterval = setInterval(run_timer, 1000);
+ window.timerInterval = setInterval(run_timer, 1e3);
$("#button-start-stop-timer span").text("Stop Timer");
// run the first update ourselves, so that it doesn't stay blank until the timer kicks in
window.immediately_refresh_tier_cutoffs = true;
@@ -670,97 +1047,80 @@ function start_stop_timer()
}
}
-function run_timer()
-{
+function run_timer() {
var now = moment(new Date());
var end = window.the_end;
var string = "
CURRENT TIME
" + now.utc().format("MM/DD/YYYY HH:mm:ss") + " UTC
EVENT ENDS:
" + end.utc().format("MM/DD/YYYY HH:mm:ss") + " UTC
";
- if (now.isBefore(end)) {
+ if (now.isBefore(end)) {
var ms = end.diff(now.utc());
var t = moment.duration(ms).asMilliseconds();
- var seconds = Math.floor( (t/1000) % 60 );
- var minutes = Math.floor( (t/1000/60) % 60 );
- var hours = Math.floor( (t/(1000*60*60)) % 24 );
- var days = Math.floor( t/(1000*60*60*24) );
- var total_hours = (days * 24) + hours;
-
+ var seconds = Math.floor(t / 1e3 % 60);
+ var minutes = Math.floor(t / 1e3 / 60 % 60);
+ var hours = Math.floor(t / (1e3 * 60 * 60) % 24);
+ var days = Math.floor(t / (1e3 * 60 * 60 * 24));
+ var total_hours = days * 24 + hours;
var time_till = "
";
}
$("#timer_output_area").html(string);
-
- // dumb ass way to fetch and display @sifen_trackbot tier cutoff tweets hourly
- // using this twitter fetcher: http://jasonmayes.com/projects/twitterApi/#sthash.budgYosd.dpbs
- var config5 = {
- "id": '654587648904794112',
- "domId": '',
- "maxTweets": 1,
- "enableLinks": false,
- "showUser": true,
- "showTime": true,
- "dateFunction": '',
- "showRetweet": false,
- "customCallback": handleTweets,
- "showInteraction": false
- };
-
- function handleTweets(tweets) {
- if (tweets.length > 0) {
- var tweet = tweets[0];
- // parse it
- console.log("GOT TWEET: " + tweet);
- // this is kinda crappy
- var splitTweet = tweet.split("\n");
- // this is VERY LAZY
- // we always assume Tier1 is on line 11, Tier2 is line 12 and Date is 13
- var tier1 = splitTweet[11];
- var tier2 = splitTweet[12];
- var updateTime = splitTweet[13];
- $("#tier_info_output_area").html("
Latest Tier Cutoffs as of " + updateTime + ": " + tier1 + " " + tier2 + "
");
- }
- }
-
- // @sifen_trackbot updates come out at 36 minutes past the hour, fetch them at 37 minutes to allow for some slop
- if (minute(now) == 37 || window.immediately_refresh_tier_cutoffs) {
+ // @sifen_trackbot updates come out at 36 minutes past the hour, fetch them at 38 minutes to allow for some slop
+ if (minute(now) == 38 && second(now) == 0 || window.immediately_refresh_tier_cutoffs) {
$("#tier_info_output_area").html("
Updating tier cutoff data, please wait...
");
- twitterFetcher.fetch(config5);
+ // dumb ass way to fetch and display @sifen_trackbot tier cutoff tweets hourly
+ // using this twitter fetcher: http://jasonmayes.com/projects/twitterApi/#sthash.budgYosd.dpbs
+ twitterFetcher.fetch({
+ id: "654587648904794112",
+ domId: "",
+ maxTweets: 1,
+ enableLinks: false,
+ showUser: true,
+ showTime: true,
+ dateFunction: "",
+ showRetweet: false,
+ customCallback: function(tweets) {
+ if (tweets.length > 0) {
+ // we only care about the first one
+ var tweet = tweets[0];
+ LOG(1, "GOT TWEET: " + tweet);
+ // now parse it
+ // !!! QUICK & DIRTY HACK ALERT !!!
+ // this is kinda crappy, @sifen_trackbot tweets come out as multiple lines separated by newlines
+ // so for now we just split the incoming tweet into an array of strings (one per line) and pull the values out using explicit line numbers
+ // we always assume Tier1 value is on line 11, Tier2 is line 12 and Date/time/% complete is line 13
+ // this may (in fact it probably will) change in the future
+ // eventually I would like to come up with a better (i.e. less crappy) algorithm to parse the string and extract the values dynamically
+ // but for now this will do :P
+ var splitTweet = tweet.split("\n");
+ var tier1 = splitTweet[11];
+ var tier2 = splitTweet[12];
+ var updateTime = splitTweet[13];
+ $("#tier_info_output_area").html("
Latest Tier Cutoffs as of UTC " + updateTime + ": " + tier1 + " " + tier2 + "
+ Current Rank:
+ Current EXP:
+ Desired Rank:
+ Game Version:
+
+
+ Calculate
+
+
+
+
Results
EXP required: -
+
+ You will need to play the following number of songs in order to get this amount of EXP:
+ (Single / 3xMedFes / 3xMedFes w/EXP boost)
+ EASY: - / - / -
+ NORMAL: - / - / -
+ HARD: - / - / -
+ EXPERT: - / - / -
+
+ At this rank you will have - LP and - friend slots.
+
+
+ Reset
+
+
+
-
+
+
+ Current Gems:
+ Game Version:
+
+ Include daily "quest" gems?
+ (What is this?)
+
+
+
On JP, you can now obtain a gem every day by completing the following steps, in the following order:
+
+
Scout three Normal students. (The daily free scout counts.)
+
Clear five live shows (any difficulty.)
+
Practice ("feed") 5 cards of any kind.
+
Play a live show (any difficulty) and get a Full Combo.
Due to the variable nature of events, calculation of gems gotten through events is at best an approximation. To make the calculations simpler,
+ it assumes that each month has 2 events, ending on the 1st and the 15th, and events alternate between token events and score matches (and medley
+ festivals if on JP.) Also, if you are tiering, it assumes that you will spend some gems to tier, and will use an estimate based on your average
+ tier, but this will only be an estimate.)
+
Average tier:
+
+ Mode:
+ Number of gems you'll have on a date?
+ Date you will have this many gems?
+
+
+ Date:
+
+
+ Desired gems:
+
+
+ Verbose Mode
+
+
+
+ Calculate
+
+
+
+
Results
-
+
+
+
+ -
+
+
+
+ Reset
+
+
+
-
Clear Timer
-
-
-
-
-
-
-
-
+
+
+ Card Rarity:
+ Current Level:
+ Current EXP:
+
+ Mode:
+ EXP needed to get card to a level?
+ Final level after feeding an amount of EXP?
+
+