This is part two of my custom Date / Time Picker solution. You can see part one here.
In part one, I split up the Date & TimeOfDay properties and placed them into separate text boxes. I then created a custom ASP.NET MVC Model Binder to join the two values back together as a single DateTime when the form is submitted. Now I will focus on the UI.
Attach a jQuery Datepicker to the Date TextBox
In part one, I created an ASP.NET MVC Editor Template to display DateTime variables:
~/Views/Shared/EditorTemplates/DateTime.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<System.DateTime>" %>
<%: Html.TextBox("", Model.ToShortDateString(), new { @class = "datepicker", @maxlength = "10" })%>
The DateTime Editor Template automatically applies the “.datepicker” CSS class to the TextBox. All we need to do is link the jQuery UI library (containing the Datepicker widget) to Site.master and then call .datepicker() on any TextBoxes referencing the “.datepicker” CSS class.
Site.Master (excerpt)
<link href="../../Content/ui-lightness/jquery-ui-1.8.5.custom.css" rel="stylesheet" type="text/css" /> <script src="../../Scripts/jquery-1.4.2.min.js" type="text/javascript"></script> <script src="../../Scripts/jquery-ui-1.8.5.custom.min.js" type="text/javascript"></script> <script src="../../Scripts/Site.js" type="text/javascript"></script>
Site.js
$(document).ready(function () {
$(".datepicker").datepicker({
showAnim: '',
dateFormat: 'm/d/yy',
showOn: 'button',
buttonImageOnly: true,
buttonImage: '../Scripts/txtdropdown/txtdropdown-btn.png',
buttonText: 'Select a date'
});
});
That was easy…
Attach a jQuery Timedropdown to the Time TextBox
My goal was to create a similar behavior to the time dropdown in Microsoft Outlook:
In Outlook, you can either edit the time directly in the textbox or you can click the dropdown and select a time. I tried a few different methods to mimic this behavior, like positioning a dropdown directly over a textbox, etc. The approach that works best for me is creating a hidden unnumbered list (<ul>) directly below the textbox, and a trigger button to show/hide the list.
Not bad, right? The code is separated into two functions:
- timedropdown() – Appends a <ul> to the textbox containing the time values, and calls txtdropdown().
- txtdropdown() – Adds the trigger button, finds the first <ul> after the textbox, formats it, and hides it.
I also used Ariel Flesler’s scrollTo() plugin to make the dropdown scroll to the correct time when shown.
timedropdown.js
(function ($) {
$.fn.timedropdown = function () {
return this.each(function () {
var txt = $(this);
with (txt) {
// Create a ul after the textbox containing the time values
// for the dropdown (this list could be made dynamic)
parent().append('<ul><li>12:00 AM</li><li>12:30 AM</li><li>1:00 AM</li><li>1:30 AM</li></ul>');
// Build the dropdown, attach a callback
txtdropdown(timedropdown_shown);
}
});
};
})(jQuery);
// Callback function to auto-scroll the dropdown to the nearest time
function timedropdown_shown(txt, ddl) {
// If unable to parse time, default position is...
var timeIndex = 0;
// Parse the time value in the textbox
var time = new Date('1/1/2010 ' + txt.val());
if (!isNaN(time)) {
// Determine the index of the li with the nearest time (round down)
// We assume the times are static, every half-hour, starting with 12:00 AM
timeIndex = (time.getHours() * 2) + (time.getMinutes() / 30);
}
// Select the li at the matching index and scroll to it
ddl.scrollTo(ddl.children('li').eq(timeIndex));
}
txtdropdown.js
(function ($) {
$.fn.txtdropdown = function (onshown) {
return this.each(function () {
var txt = $(this);
// The first ul after the textbox becomes the dropdown
var ddl = txt.next('ul:first');
with (ddl) {
// Apply CSS
addClass('txtdropdown-ddl');
// Position the dropdown directly below the textbox
css('position', 'absolute');
css('width', txt.width() - 2);
css('left', txt.position().left);
css('top', txt.position().top + txt.height() + 4);
// Hide the dropdown
hide();
css('visibility', 'visible');
// Prevent the dropdown from auto-hiding when clicking the scroll buttons
click(function (e) {
e.stopPropagation();
});
// Clicking an li sets the textbox val and hides the dropdown
find('li').click(function () {
txt.val($(this).html()).select().focus().change();
txtdropdown_hide();
});
}
with (txt) {
// Surround the textbox with an outer div
var outer = wrap('<div class="txtdropdown-outer" style="width: ' + width() + 'px"></div>').parent();
// Create a "button" div inside the outer div
var btn = outer.prepend('<div class="txtdropdown-btn"> </div>').find('.txtdropdown-btn');
btn.click(function () {
with (ddl) {
if (is(':visible')) {
txtdropdown_hide();
}
else {
txtdropdown_hide();
show();
if (onshown != null) onshown(txt, ddl);
}
}
});
// Make the textbox width smaller to make room for the button
width(width() - btn.width() - 4);
css('border-right', '0');
// Keypress in the textbox hides the dropdown
keypress(function () {
txtdropdown_hide();
});
// Change in the textbox hides the dropdown
change(function () {
txtdropdown_hide();
});
// Prevent the dropdown from auto-hiding when clicking the textbox
outer.click(function (e) {
e.stopPropagation();
});
}
// Auto-hide the dropdown(s)
$(document).click(function () {
txtdropdown_hide();
});
});
};
})(jQuery);
function txtdropdown_hide() {
$('.txtdropdown-ddl').hide();
}
I am not posting the CSS here but you can download the project and take a look. I will be adding some client-side validation functions to ensure that the start / end values “follow” each other.