From 56d471ffa2e694092da946682a28a8fcfc9c9497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=E1bor=20Czigola?= Date: Sun, 31 Oct 2010 19:56:09 +0100 Subject: [PATCH 1/3] Evolve datepicker to select time as well (a.k.a. datetimepicker). Thanks to Addy Osmani http://addyosmani.com/blog/the-missing-date-time-selector-for-jquery-ui/ --- demos/datepicker/datetime.html | 41 ++++ demos/datepicker/index.html | 1 + ui/jquery.ui.datetimepicker.js | 414 +++++++++++++++++++++++++++++++++ 3 files changed, 456 insertions(+) create mode 100644 demos/datepicker/datetime.html create mode 100644 ui/jquery.ui.datetimepicker.js diff --git a/demos/datepicker/datetime.html b/demos/datepicker/datetime.html new file mode 100644 index 00000000000..a232dcb9638 --- /dev/null +++ b/demos/datepicker/datetime.html @@ -0,0 +1,41 @@ + + + + + jQuery UI Datepicker - Select date and time as well + + + + + + + + + + + + + +
+

Date:

+
+ + +
+

Display the datepicker embedded in the page instead of in an overlay. Simply call .datepicker() on a div instead of an input.

+
+ + + diff --git a/demos/datepicker/index.html b/demos/datepicker/index.html index 37c8ef7148c..e50415225dd 100644 --- a/demos/datepicker/index.html +++ b/demos/datepicker/index.html @@ -13,6 +13,7 @@

Examples

  • Default functionality
  • Format date
  • Restrict date range
  • +
  • Select date and time as well
  • Localize calendar
  • Populate alternate field
  • Display inline
  • diff --git a/ui/jquery.ui.datetimepicker.js b/ui/jquery.ui.datetimepicker.js new file mode 100644 index 00000000000..5bdf0e994c3 --- /dev/null +++ b/ui/jquery.ui.datetimepicker.js @@ -0,0 +1,414 @@ +/*! + * jQuery UI Timepicker 0.2.1 + * + * Copyright (c) 2009 Martin Milesich (http://milesich.com/) + * http://addyosmani.com/blog/the-missing-date-time-selector-for-jquery-ui/ + * + * Copyright (c) 2010 Gábor Czigola (http://gablog.eu) + * + * + * $Id: timepicker.js 28 2009-08-11 20:31:23Z majlo $ + * + * Depends: + * ui.core.js + * ui.datepicker.js + * ui.slider.js + */ +(function($) { + +/** + * Extending default values + */ +$.extend($.datepicker._defaults, { + 'stepMinutes': 1, // Number of minutes to step up/down + 'stepHours': 1, // Number of hours to step up/down + 'time24h': false, // True if 24h time + 'showTime': false, // Show timepicker with datepicker + 'altTimeField': '' // Selector for an alternate field to store time into +}); + +/** + * _hideDatepicker must be called with null + */ +$.datepicker._connectDatepickerOverride = $.datepicker._connectDatepicker; +$.datepicker._connectDatepicker = function(target, inst) { + $.datepicker._connectDatepickerOverride(target, inst); + + // showButtonPanel is required with timepicker + if (this._get(inst, 'showTime')) { + inst.settings['showButtonPanel'] = true; + } + + var showOn = this._get(inst, 'showOn'); + + if (showOn == 'button' || showOn == 'both') { + // Unbind all click events + inst.trigger.unbind('click'); + + // Bind new click event + inst.trigger.click(function() { + if ($.datepicker._datepickerShowing && $.datepicker._lastInput == target) + $.datepicker._hideDatepicker(null); // This override is all about the "null" + else + $.datepicker._showDatepicker(target); + return false; + }); + } +}; + +/** + * Datepicker does not have an onShow event so I need to create it. + * What I actually doing here is copying original _showDatepicker + * method to _showDatepickerOverload method. + */ +$.datepicker._showDatepickerOverride = $.datepicker._showDatepicker; +$.datepicker._showDatepicker = function (input) { + // Call the original method which will show the datepicker + $.datepicker._showDatepickerOverride(input); + + input = input.target || input; + + // find from button/image trigger + if (input.nodeName.toLowerCase() != 'input') input = $('input', input.parentNode)[0]; + + // Do not show timepicker if datepicker is disabled + if ($.datepicker._isDisabledDatepicker(input)) return; + + // Get instance to datepicker + var inst = $.datepicker._getInst(input); + + var showTime = $.datepicker._get(inst, 'showTime'); + + // If showTime = True show the timepicker + if (showTime) $.timepicker.show(input); +}; + +/** + * Same as above. Here I need to extend the _checkExternalClick method + * because I don't want to close the datepicker when the sliders get focus. + */ +$.datepicker._checkExternalClickOverride = $.datepicker._checkExternalClick; +$.datepicker._checkExternalClick = function (event) { + if (!$.datepicker._curInst) return; + var $target = $(event.target); + + if (($target.parents('#' + $.timepicker._mainDivId).length == 0)) { + $.datepicker._checkExternalClickOverride(event); + } +}; + +/** + * Datepicker has onHide event but I just want to make it simple for you + * so I hide the timepicker when datepicker hides. + */ +$.datepicker._hideDatepickerOverride = $.datepicker._hideDatepicker; +$.datepicker._hideDatepicker = function(input, duration) { + // Some lines from the original method + var inst = this._curInst; + + if (!inst || (input && inst != $.data(input, PROP_NAME))) return; + + // Get the value of showTime property + var showTime = this._get(inst, 'showTime'); + + if (input === undefined && showTime) { + if (inst.input) { + inst.input.val(this._formatDate(inst)); + inst.input.trigger('change'); // fire the change event + } + + this._updateAlternate(inst); + + if (showTime) $.timepicker.update(this._formatDate(inst)); + } + + // Hide datepicker + $.datepicker._hideDatepickerOverride(input, duration); + + // Hide the timepicker if enabled + if (showTime) { + $.timepicker.hide(); + } +}; + +/** + * This is a complete replacement of the _selectDate method. + * If showed with timepicker do not close when date is selected. + */ +$.datepicker._selectDate = function(id, dateStr) { + var target = $(id); + var inst = this._getInst(target[0]); + var showTime = this._get(inst, 'showTime'); + dateStr = (dateStr != null ? dateStr : this._formatDate(inst)); + if (!showTime) { + if (inst.input) + inst.input.val(dateStr); + this._updateAlternate(inst); + } + var onSelect = this._get(inst, 'onSelect'); + if (onSelect) + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); // trigger custom callback + else if (inst.input && !showTime) + inst.input.trigger('change'); // fire the change event + if (inst.inline) + this._updateDatepicker(inst); + else if (!inst.stayOpen) { + if (showTime) { + this._updateDatepicker(inst); + } else { + this._hideDatepicker(null, this._get(inst, 'duration')); + this._lastInput = inst.input[0]; + if (typeof(inst.input[0]) != 'object') + inst.input[0].focus(); // restore focus + this._lastInput = null; + } + } +}; + +/** + * We need to resize the timepicker when the datepicker has been changed. + */ +$.datepicker._updateDatepickerOverride = $.datepicker._updateDatepicker; +$.datepicker._updateDatepicker = function(inst) { + $.datepicker._updateDatepickerOverride(inst); + $.timepicker.resize(); +}; + +function Timepicker() {} + +Timepicker.prototype = { + init: function() + { + this._mainDivId = 'ui-timepicker-div'; + this._inputId = null; + this._orgValue = null; + this._orgHour = null; + this._orgMinute = null; + this._colonPos = -1; + this._visible = false; + this.tpDiv = $(''); + this._generateHtml(); + }, + + show: function (input) + { + // Get instance to datepicker + var inst = $.datepicker._getInst(input); + + this._time24h = $.datepicker._get(inst, 'time24h'); + this._altTimeField = $.datepicker._get(inst, 'altTimeField'); + + var stepMinutes = parseInt($.datepicker._get(inst, 'stepMinutes'), 10) || 1; + var stepHours = parseInt($.datepicker._get(inst, 'stepHours'), 10) || 1; + + if (60 % stepMinutes != 0) { stepMinutes = 1; } + if (24 % stepHours != 0) { stepHours = 1; } + + $('#hourSlider').slider('option', 'max', 24 - stepHours); + $('#hourSlider').slider('option', 'step', stepHours); + + $('#minuteSlider').slider('option', 'max', 60 - stepMinutes); + $('#minuteSlider').slider('option', 'step', stepMinutes); + + this._inputId = input.id; + + if (!this._visible) { + this._parseTime(); + this._orgValue = $('#' + this._inputId).val(); + } + + this.resize(); + + $('#' + this._mainDivId).show(); + + this._visible = true; + + var dpDiv = $('#' + $.datepicker._mainDivId); + var dpDivPos = dpDiv.position(); + + var viewWidth = (window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth) + $(document).scrollLeft(); + var tpRight = this.tpDiv.offset().left + this.tpDiv.outerWidth(); + + if (tpRight > viewWidth) { + dpDiv.css('left', dpDivPos.left - (tpRight - viewWidth) - 5); + this.tpDiv.css('left', dpDiv.offset().left + dpDiv.outerWidth() + 'px'); + } + }, + + update: function (fd) + { + var curTime = $('#' + this._mainDivId + ' span.fragHours').text() + + ':' + + $('#' + this._mainDivId + ' span.fragMinutes').text(); + + if (!this._time24h) { + curTime += ' ' + $('#' + this._mainDivId + ' span.fragAmpm').text(); + } + + var curDate = $('#' + this._inputId).val(); + + $('#' + this._inputId).val(fd + ' ' + curTime); + + if (this._altTimeField) { + $(this._altTimeField).each(function() { $(this).val(curTime); }); + } + }, + + hide: function () + { + this._visible = false; + $('#' + this._mainDivId).hide(); + }, + + resize: function () + { + var dpDiv = $('#' + $.datepicker._mainDivId); + var dpDivPos = dpDiv.position(); + + var hdrHeight = $('#' + $.datepicker._mainDivId + ' > div.ui-datepicker-header:first-child').height(); + + $('#' + this._mainDivId + ' > div.ui-datepicker-header:first-child').css('height', hdrHeight); + + this.tpDiv.css({ + 'height': dpDiv.height(), + 'top' : dpDivPos.top, + 'left' : dpDivPos.left + dpDiv.outerWidth() + 'px' + }); + + $('#hourSlider').css('height', this.tpDiv.height() - (3.5 * hdrHeight)); + $('#minuteSlider').css('height', this.tpDiv.height() - (3.5 * hdrHeight)); + }, + + _generateHtml: function () + { + var html = ''; + + html += '
    '; + html += '
    '; + html += '08:45
    '; + html += ''; + html += ''; + html += '
    HourMinute
    '; + + this.tpDiv.empty().append(html); + $('body').append(this.tpDiv); + + var self = this; + + $('#hourSlider').slider({ + orientation: "vertical", + range: 'min', + min: 0, + max: 23, + step: 1, + slide: function(event, ui) { + self._writeTime('hour', ui.value); + }, + stop: function(event, ui) { + $('#' + self._inputId).focus(); + } + }); + + $('#minuteSlider').slider({ + orientation: "vertical", + range: 'min', + min: 0, + max: 59, + step: 1, + slide: function(event, ui) { + self._writeTime('minute', ui.value); + }, + stop: function(event, ui) { + $('#' + self._inputId).focus(); + } + }); + + $('#hourSlider > a').css('padding', 0); + $('#minuteSlider > a').css('padding', 0); + }, + + _writeTime: function (type, value) + { + if (type == 'hour') { + if (!this._time24h) { + if (value < 12) { + $('#' + this._mainDivId + ' span.fragAmpm').text('am'); + } else { + $('#' + this._mainDivId + ' span.fragAmpm').text('pm'); + value -= 12; + } + + if (value == 0) value = 12; + } else { + $('#' + this._mainDivId + ' span.fragAmpm').text(''); + } + + if (value < 10) value = '0' + value; + $('#' + this._mainDivId + ' span.fragHours').text(value); + } + + if (type == 'minute') { + if (value < 10) value = '0' + value; + $('#' + this._mainDivId + ' span.fragMinutes').text(value); + } + }, + + _parseTime: function () + { + var dt = $('#' + this._inputId).val(); + + this._colonPos = dt.search(':'); + + var m = 0, h = 0, a = ''; + + if (this._colonPos != -1) { + h = parseInt(dt.substr(this._colonPos - 2, 2), 10); + m = parseInt(dt.substr(this._colonPos + 1, 2), 10); + a = jQuery.trim(dt.substr(this._colonPos + 3, 3)); + } + + a = a.toLowerCase(); + + if (a != 'am' && a != 'pm') { + a = ''; + } + + if (h < 0) h = 0; + if (m < 0) m = 0; + + if (h > 23) h = 23; + if (m > 59) m = 59; + + if (a == 'pm' && h < 12) h += 12; + if (a == 'am' && h == 12) h = 0; + + this._setTime('hour', h); + this._setTime('minute', m); + + this._orgHour = h; + this._orgMinute = m; + }, + + _setTime: function (type, value) + { + if (isNaN(value)) value = 0; + if (value < 0) value = 0; + if (value > 23 && type == 'hour') value = 23; + if (value > 59 && type == 'minute') value = 59; + + if (type == 'hour') { + $('#hourSlider').slider('value', value); + } + + if (type == 'minute') { + $('#minuteSlider').slider('value', value); + } + + this._writeTime(type, value); + } +}; + +$.timepicker = new Timepicker(); +$('document').ready(function () {$.timepicker.init();}); + +})(jQuery); From 65586375e031bb44f31a3b4599ca61ec4ddf113f Mon Sep 17 00:00:00 2001 From: Gabor Czigola Date: Sun, 31 Oct 2010 20:06:33 +0100 Subject: [PATCH 2/3] z-index fix --- ui/jquery.ui.datetimepicker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/jquery.ui.datetimepicker.js b/ui/jquery.ui.datetimepicker.js index 5bdf0e994c3..aeb1666ea80 100644 --- a/ui/jquery.ui.datetimepicker.js +++ b/ui/jquery.ui.datetimepicker.js @@ -186,7 +186,7 @@ Timepicker.prototype = { this._orgMinute = null; this._colonPos = -1; this._visible = false; - this.tpDiv = $(''); + this.tpDiv = $(''); this._generateHtml(); }, From 83975d3f79f5dbcf4d973b51df1407c40629eac1 Mon Sep 17 00:00:00 2001 From: Gabor Czigola Date: Sun, 31 Oct 2010 22:01:12 +0100 Subject: [PATCH 3/3] Integrate bugfixes. --- ui/jquery.ui.datetimepicker.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/ui/jquery.ui.datetimepicker.js b/ui/jquery.ui.datetimepicker.js index aeb1666ea80..3827ac43a3d 100644 --- a/ui/jquery.ui.datetimepicker.js +++ b/ui/jquery.ui.datetimepicker.js @@ -1,13 +1,16 @@ /*! - * jQuery UI Timepicker 0.2.1 + * jQuery UI DateTimepicker @VERSION + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license * * Copyright (c) 2009 Martin Milesich (http://milesich.com/) * http://addyosmani.com/blog/the-missing-date-time-selector-for-jquery-ui/ * - * Copyright (c) 2010 Gábor Czigola (http://gablog.eu) - * + * Copyright (c) 2010 Gábor Czigola * - * $Id: timepicker.js 28 2009-08-11 20:31:23Z majlo $ + * http://docs.jquery.com/UI/Timepicker * * Depends: * ui.core.js @@ -92,6 +95,8 @@ $.datepicker._checkExternalClick = function (event) { if (!$.datepicker._curInst) return; var $target = $(event.target); + $.datepicker._curInst.closeButtonClicked = $target.parents('.ui-datepicker-buttonpane').length; + if (($target.parents('#' + $.timepicker._mainDivId).length == 0)) { $.datepicker._checkExternalClickOverride(event); } @@ -108,18 +113,28 @@ $.datepicker._hideDatepicker = function(input, duration) { if (!inst || (input && inst != $.data(input, PROP_NAME))) return; + // Change the field only when Done button clicked or sliders changed. + var saveOnHide = $.timepicker._modified; + try { + saveOnHide |= inst.closeButtonClicked; + } + catch (err) {} + // Get the value of showTime property var showTime = this._get(inst, 'showTime'); - if (input === undefined && showTime) { + if (saveOnHide && input === undefined && showTime) { if (inst.input) { inst.input.val(this._formatDate(inst)); - inst.input.trigger('change'); // fire the change event } this._updateAlternate(inst); if (showTime) $.timepicker.update(this._formatDate(inst)); + + if (inst.input) { + inst.input.trigger('change'); // fire the change event + } } // Hide datepicker @@ -129,6 +144,8 @@ $.datepicker._hideDatepicker = function(input, duration) { if (showTime) { $.timepicker.hide(); } + + $.timepicker._modified = false; }; /** @@ -186,6 +203,7 @@ Timepicker.prototype = { this._orgMinute = null; this._colonPos = -1; this._visible = false; + this._modified = false; this.tpDiv = $(''); this._generateHtml(); }, @@ -302,9 +320,11 @@ Timepicker.prototype = { max: 23, step: 1, slide: function(event, ui) { + self._modified = true; self._writeTime('hour', ui.value); }, stop: function(event, ui) { + self._modified = true; $('#' + self._inputId).focus(); } }); @@ -405,7 +425,7 @@ Timepicker.prototype = { } this._writeTime(type, value); - } + } }; $.timepicker = new Timepicker();