jQueryUI autocomplete in SharePoint
If you’re simply after the solution to try out, download the Visual Studio 2010 solution : PeoplePickerDemo
Sample screenshot showing a user typing in the autocomplete textbox

And once we’ve selected someone from the list, our code-behind responding to an event our user control raises

As far as solution layout, I’ve added :
- jQuery, jQueryUI and a theme
- a user control (StaffPicker.ascx) that contains the ASP.NET controls (one to search, one to display the selected payroll id)
- some jQuery (app.js) to supply our user control with the AutoComplete functionality
- a web service (PeopleService.asmx) that provides the AutoComplete with the filtered list of users
- a test application page (TestPage.aspx) that includes a sample of the user control, and how to respond to the event it raises

StaffPicker.ascx - The weirdness here is txtPostBackHelper and btnPseudoSelected. Once the user has selected someone from the AutoComplete’s list, we want to invoke a server side button-click event, via jQuery. (More on that later, further down in the app.js code)
<asp:TextBox ID="txtName" runat="server" CssClass="staffLookup"></asp:TextBox>
<asp:TextBox ID="txtPayrollID" runat="server" BackColor="LightGray" BorderColor="DarkGray"></asp:TextBox>
<asp:TextBox ID="txtPostBackHelper" runat="server"></asp:TextBox>
<asp:Button ID="btnPseudoSelected" runat="server" OnClick="btnPseudoSelected_Click"
Style="display: none;" />
<asp:HiddenField ID="hdnFirstName" runat="server" Value="" />
<asp:HiddenField ID="hdnLastName" runat="server" Value="" />
StaffPicker.ascx.cs
namespace PeoplePickerDemo
{
public partial class StaffPicker : UserControl
{
public delegate void EmployeeSelectedHandler(object sender, Employee employee);
public event EmployeeSelectedHandler EmployeeSelected;
public string PayrollID
{
get { return txtPayrollID.Text; }
}
public string FirstName
{
get { return hdnFirstName.Value; }
}
public string LastName
{
get { return hdnLastName.Value; }
}
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
txtPayrollID.Attributes.Add("readonly", "readonly");
txtPostBackHelper.Attributes.Add("style", "display: none;");
var cs = this.Page.ClientScript;
txtPostBackHelper.Text = cs.GetPostBackEventReference(btnPseudoSelected, "");
}
}
protected void btnPseudoSelected_Click(object sender, EventArgs e)
{
EmployeeSelected(this, PeopleService.GetEmployee(txtPayrollID.Text));
}
}
}
PeopleService.cs - In our web service, note that we’re using cacheing for performance. If you wish to do searches against the SharePoint User Profile, definitely consider a similar approach as every user working with the autocomplete will incur a search. The penalty though, is that there will be a static collection on every front-end server, containing every user. Even if you have 10,000 people I think this is a fair compromise.
Also note we’re decorating with [ScriptService] and [WebMethod], in order for the AutoComplete to receive a JSON datatype from our service.
namespace PeoplePickerDemo
{
[WebService(Namespace = "http://peoplepickerdemo")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class PeopleService
{
private static readonly object locker = new object();
private static EmployeeCollection _hrCache
{
get
{
if (HttpRuntime.Cache["_hrCache"] == null)
HttpRuntime.Cache.Insert("_hrCache", new EmployeeCollection(), null, DateTime.Now.AddDays(1), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
return ((EmployeeCollection)(HttpRuntime.Cache["_hrCache"]));
}
set
{
HttpRuntime.Cache["_hrCache"] = value;
}
}
private static EmployeeCollection HRCache
{
get
{
if (_hrCache.Count == 0)
{
lock (locker)
{
_hrCache = new EmployeeCollection();
}
}
return _hrCache;
}
}
[WebMethod]
public static Employee GetEmployee(string payrollID)
{
return HRCache.Where(itm => itm.PayrollId == payrollID).SingleOrDefault();
}
[WebMethod]
public Dictionary<string, string> StaffLookupBasic(string prefixText, int count)
{
return StaffLookup(prefixText, count).ToDictionary(x => x.PayrollId, x => x.Fullname);
}
[WebMethod]
public List<Employee> StaffLookup(string prefixText, int count)
{
IOrderedEnumerable<Employee> result;
if (prefixText.Contains(" ")) // search for firstname / lastname
{
var split = prefixText.Split(' ');
var firstName = "xyz";
var surname = "xyz";
if (split.Length == 2)
{
firstName = split[0].Trim();
surname = split[1].Trim();
}
result = from itm in HRCache
where ((itm.Firstname.ToLower().StartsWith(firstName.ToLower())) && (itm.Surname.ToLower().StartsWith(surname.ToLower())))
orderby itm.Firstname, itm.Surname
select itm;
}
else // just searching for phrase
{
result = from itm in HRCache
where (itm.Fullname.ToLower().Contains(prefixText.ToLower()))
orderby itm.Firstname, itm.Surname
select itm;
}
try
{
return result.Take(count).ToList();
}
catch
{
return null;
}
}
}
public class EmployeeCollection : List<Employee>
{
public EmployeeCollection()
{
// Random name generator : http://www.kleimo.com/random/name.cfm
this.Add(new Employee() { PayrollId = "001", Firstname = "Lance", Surname = "Monn" });
this.Add(new Employee() { PayrollId = "002", Firstname = "Margery", Surname = "Vitello" });
...
...
...
this.Add(new Employee() { PayrollId = "029", Firstname = "Tyrone", Surname = "Bartee" });
this.Add(new Employee() { PayrollId = "030", Firstname = "Clinton", Surname = "Elswick" });
}
}
public class Employee
{
public string Firstname { get; set; }
public string Surname { get; set; }
public string PayrollId { get; set; }
public string Fullname
{
get { return Firstname + " " + Surname; }
}
}
}
app.js - Here’s the AutoComplete, and the StaffLookupBasic service it’s relying on. In order to pretend our ‘btnPseudoSelected’ asp:button was clicked, we’re getting the PostBackEventReference (hidden in txtPostBackHelper) and ‘executing’ it client-side, using Javascript’s eval function.
// ******************************************************
// set defaults for the page. Note this function is _not_ called when UpdatePanels refresh.
// ******************************************************
$(document).ready(function () {
prepForm();
});
// ******************************************************
// UpdatePanels look for a pageLoad function, and if present, execute on every partial postback
// We need to re-bind event listeners, etc in this function because UpdatePanels wipe the content
// from the DOM, and recreate it, which kills any event listeners we may have had.
// ******************************************************
function pageLoad() {
prepForm();
}
// ******************************************************
// Hook up validators, autocompletes, etc
// ******************************************************
function prepForm() {
$(".staffLookup").autocomplete({
source: function (request, response) {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: '/_vti_bin/PeoplePickerDemo/PeopleService.asmx/StaffLookupBasic',
data: "{ 'prefixText' : '" + request.term + "', 'count' : '12'}",
dataType: "json",
async: true,
success: function (data) {
response($.map(data.d, function (item, key) {
return {
label: item,
value: key
}
}))
}
});
},
cache: false,
minLength: 2,
delay: 300,
select: function (event, ui) {
//override the select event, replace text contents with the label (name)
//(default behaviour is to use the key which in our case is the payroll id)
$(this).val(ui.item.label);
//update payroll id textbox
$(this).next(':input').val(ui.item.value);
//invoke postback of our 'fake' button
var callback = $(this).next(':input').next(':input').attr('value');
eval(callback);
//cancel default behaviour
return false;
}
});
$(".staffLookup").keydown(function () {
if (this.value.length < 2) {
$(this).nextAll(':input').first().val("");
}
});
}
TestPage.aspx - Note in our application page, the UserControl is referenced using “~/_CONTROLTEMPLATES” virtual path. I ended up placing the jQuery CSS + images under /Styles, and the JS files under /Layouts. This is only because SharePoint:CssRegistration and SharePoint:ScriptLink are looking for those folders, respectively.
<%@ Register Src="~/_CONTROLTEMPLATES/PeoplePickerDemo/StaffPicker.ascx" TagName="StaffPicker"
TagPrefix="uc1" %>
<asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
<SharePoint:CssRegistration runat="server" Name="PeoplePickerDemo/jquery-ui/css/custom-theme/jquery-ui-1.8.18.custom.css">
</SharePoint:CssRegistration>
<SharePoint:ScriptLink runat="server" Name="PeoplePickerDemo/jquery-ui/js/jquery-1.7.1.min.js"
OnDemand="false" Localizable="false">
</SharePoint:ScriptLink>
<SharePoint:ScriptLink runat="server" Name="PeoplePIckerDemo/jquery-ui/js/jquery-ui-1.8.18.custom.min.js"
OnDemand="false" Localizable="false">
</SharePoint:ScriptLink>
<SharePoint:ScriptLink runat="server" Name="PeoplePickerDemo/app.js" OnDemand="false"
Localizable="false">
</SharePoint:ScriptLink>
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
<asp:UpdatePanel ID="pnl" runat="server">
<ContentTemplate>
<div>
Select employee :
<uc1:StaffPicker id="myStaffPicker" runat="server">
</uc1:StaffPicker>
</div>
<div>
<asp:Label ID="lbl" runat="server"></asp:Label>
</div>
</ContentTemplate>
</asp:UpdatePanel>
</asp:Content>
And finally just an example of responding to the event our UserControl raised.
namespace PeoplePickerDemo.Layouts.PeoplePickerDemo
{
public partial class TestPage : LayoutsPageBase
{
protected global::PeoplePickerDemo.StaffPicker myStaffPicker;
protected void Page_Load(object sender, EventArgs e)
{
myStaffPicker.EmployeeSelected += new StaffPicker.EmployeeSelectedHandler(myStaffPicker_EmployeeSelected);
}
protected void myStaffPicker_EmployeeSelected(object sender, Employee employee)
{
lbl.Text = "You selected " + employee.Fullname;
}
}
}
Emailing contents of an Application Page
Solution : Use a .Net UserControl to hold the common elements between the page and the email, and some cool code here : http://stackoverflow.com/questions/258877/load-a-user-control-programmatically-in-to-a-html-text-writer
Going back to ASP.NET principles to solve SharePoint problems is great, in that :
- it proves that SharePoint is a strong dev platform as it’s built on top of ASP.NET, and
- old ASP.NET people like me still have uses!

Heres the .ascx markup :
<table border="1" style="background-color: Yellow;">
<tr>
<td>
Record id :
</td>
<td>
<asp:Label ID="lblRecordID" runat="server"></asp:Label>
</td>
</tr>
</table>
<asp:Label ID="lblPostBackTest" runat="server"></asp:Label>
And code behind :
using System;
using System.Web.UI;
namespace test.ControlTemplates.test
{
public partial class ViewControl : UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
lblPostBackTest.Text = "Hey the page_load of the UserControl works!";
}
public int MyRecordID
{
get
{
return int.Parse(lblRecordID.Text);
}
set
{
lblRecordID.Text = value.ToString();
//Here we would use this property, to load the form with content from a list or sql db
}
}
}
}
Now we can reference this control in an App Page. Note that unlike a Visual Web Part, an App Page in Visual Studio 2010 does not show the Design View so it’s not possible to drag/drop our UserControl into the form. We have to manually register this .ascx, but watch out for the tilde and underscore in the Src attribute, we’re referencing the control using a Virtual Path.
And the code-behind :<%@ Register Src="~/_ControlTemplates/test/ViewControl.ascx" TagName="ViewControl"
TagPrefix="uc1" %>
<asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
<uc1:ViewControl ID="ViewControl1" runat="server" />
</asp:Content>
<asp:Content ID="PageTitle" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
View Page
</asp:Content>
<asp:Content ID="PageTitleInTitleArea" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server">
View Page
</asp:Content>
Note again, if this was a Visual Web Part, or ASP.NET page, etc, we could have just dragged our UserControl into the design view of the page and Visual Studio would have created a field declaration in our designer.cs file. Manually, we have to do the equivalent thing when we declare ViewControl1.using System;
using Microsoft.SharePoint.WebControls;
namespace test.Layouts.test
{
public partial class ViewPage : LayoutsPageBase
{
protected global::test.ControlTemplates.test.ViewControl ViewControl1;
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
ViewControl1.MyRecordID = 123456;
}
}
}
}
Now lets say in a completely different App Page, we want to generate the same html output this UserControl gives us, for sending in an email. As it’s only used for email, we won’t include our UserControl into this page at design-time, but rather, do so programmatically, when a button is clicked.
Code behind :<asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
A pretend record id :
<asp:TextBox ID="txtSomeInput" runat="server"></asp:TextBox>
<br />
<br />
<asp:Button ID="btnSubmit" Text="Submit" runat="server" OnClick="btnSubmit_Click" />
</asp:Content>
<asp:Content ID="PageTitle" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
Edit Page
</asp:Content>
<asp:Content ID="PageTitleInTitleArea" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea"
runat="server">
Edit Page
</asp:Content>
using System;
using System.Collections.Specialized;
using System.IO;
using System.Web;
using System.Web.UI;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebControls;
namespace test.Layouts.test
{
public partial class EditPage : LayoutsPageBase
{
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
txtSomeInput.Text = "123456";
}
}
protected void btnSubmit_Click(object sender, EventArgs e)
{
var userControlOutput = RenderControl("~/_ControlTemplates/test/ViewControl.ascx", int.Parse(txtSomeInput.Text));
var headers = new StringDictionary();
headers.Add("to", "someone@somewhere.com");
headers.Add("subject","test");
headers.Add("content-type", "text/html");
SPUtility.SendEmail(SPContext.Current.Web, headers, userControlOutput);
}
//http://stackoverflow.com/questions/258877/load-a-user-control-programmatically-in-to-a-html-text-writer
private string RenderControl(string path, int MyRecordID)
{
var pageHolder = new Page();
var viewControl = (UserControl)pageHolder.LoadControl(path);
var viewControlType = viewControl.GetType();
var prop = viewControlType.GetProperty("MyRecordID");
if (prop != null)
prop.SetValue(viewControl, MyRecordID, null);
pageHolder.Controls.Add(viewControl);
var result = new StringWriter();
HttpContext.Current.Server.Execute(pageHolder, result, false);
return result.ToString();
}
}
}
The magic from (Scott Guthrie’s blog) is mentioned here: http://stackoverflow.com/questions/258877/load-a-user-control-programmatically-in-to-a-html-text-writer. I have modified it above, to allow passing of an Int Property called MyRecordID. When we set this Property in the UserControl, we could then use the Setter to populate the Control with say, data from a ListItem, or a record in Sql, etc.
Since the page and email are built with the same UserControl, if you make a change to it, both the page and the email remain in sync. The only problem I guess is if the control references CSS declared in a masterpage, we lose reference to this when rendering programmatically in an ad-hoc page. The workaround for this would be to create a cut-down, inline equivalent of whatever SharePoint CSS we need, for the outgoing email.
Our ‘View-only’ Page (with UserControl) :

Our ‘Edit-Page’, with Button which emails the same (programmatically created) UserControl

And lastly the email output

SharePoint Dev Mail Server
To the rescue is smtp4dev, a handy utility that sits in your in-tray and acts as a dummy SMTP Server. The cool thing about it is that it acts as a catch-all, i.e. you don’t need to setup particular email accounts in your dev environment, smtp4dev will trap them all for you. Double-clicking on incoming emails within the app launches your default mail client, very handy for checking HTML content.
In SharePoint the only thing you have to configure is Central Admin -> System Settings -> Configure outgoing email settings

Or alternatively you can manage the outgoing email for a particular web app, and enter the same settings:

(Note that it expects the fully qualified computer name, which you can obtain from Control Panel -> System).
Incoming email settings ? Sorry, can’t help you there !
Binding heirarchical data to a dropdown using jQuery

To hold the data, we go with the shorthand method below, makes it a bit easier to read.
But how to trawl through the object and work out the indentation for each level? It turns out to be relatively easy, using jQuery’s .each() function to iterate over items in the array, and a recursion to make sure all the sub arrays are inspected. The indentation using 3 dots is a bit of a hack, but I couldn’t seem to use spaces as padding - IE and Safari both ignore them.var terms = [
{
term: 'Ale',
terms: [
{ term: 'Belgian Ale',
terms: [
{ term: 'Abbey' },
{ term: 'Amber' }
]
},
{ term: 'Dark Ale',
terms: [
{ term: 'Stout' },
{ term: 'Porter' }
]
}
]
},
{
term: 'Lager',
terms: [
{ term: 'Bock',
terms: [
{ term: 'Doppelbock' },
{ term: 'Eisbock' }
]
},
{ term: 'Pale Lager',
terms: [
{ term: 'Pilsner' },
{ term: 'Spezial' }
]
}
]
},
];
/* PLUGIN TO BIND THE TERM ARRAY (OR A SUBSET OF IT) TO A GIVEN DROPDOWN
PARAMS :
array : The js array - Must use 'term' and 'terms' to identify the data
options :
recursion : true/false
addPleaseSelect : true/false
*/
(function ($) {
$.fn.extend({
bindComboTerms: function (array, options) {
var defaults =
{
recursion: true,
addPleaseSelect: true
}
var options = $.extend(defaults, options);
return this.each(function () {
var o = options;
populateSelect($(this), array, o.recursion, o.addPleaseSelect);
});
function populateSelect(element, items, recursion, addPleaseSelect) {
var indent = 0;
var el = element.get(0);
el.options.length = 0;
if (addPleaseSelect)
el.options[0] = new Option('Please select...', '');
populateSelectRecurse(el, items, recursion, indent);
}
function populateSelectRecurse(el, items, recursion, indent) {
indent++;
$.each(items, function () {
el.options[el.options.length] = new Option(spaces(indent) + this.term, this.term);
if (recursion && ($.isArray(this.terms))) {
populateSelectRecurse(el, this.terms, recursion, indent);
}
});
}
function spaces(indent) {
return new Array(indent).join('...');
}
}
});
})(jQuery);
$(document).ready(function () {
$('#testcombo1').bindComboTerms(terms);
$('#testcombo2').bindComboTerms(terms[0].terms, { addPleaseSelect: false, recursion: false });
$('#testcombo3').bindComboTerms(terms[0].terms[1].terms);
});
</script>
</head>
<body>
<p>
<select id="testcombo1">
</select>
(All beers)
</p>
<p>
<select id="testcombo2">
</select>
(Only ales, no please select, no recursion)
</p>
<p>
<select id="testcombo3">
</select>
(Only dark ales)
</p>
</body>
</html>
Rollup Calendars in a CQWP
In typical ’trawl google fashion’, we used a variety of really handy posts to come up with a content query web part that can highlight upcoming events on a (jQuery) datepicker, and describe individual events in more detail when a given date is clicked.
Here’s a look at the final result we’re trying to achieve:

In the screenshot above, we have multiple events, coming in from two different calendars of a site. The user has clicked the 14th, resulting in the display of 2 events occurring on that day.

Now a quick test for the 16th, where there is only one event scheduled.

A screenshot of the first calendar, with test events

And finally the second calendar.
For the datepicker control, I used jQueryUI. I simply chose datepicker (which in turn selects the UI core), and the Redmond theme. By downloading a tailored package to suit, you end up with a very compact set of .js and .css to include in your web part.
For the formatting of the Calendar listitems, Steve Ottenad’s post on Sharepoint 2010 XSLT Date Formatting is very handy. Steve’s itemstyle.xsl modifications give a neat layout and CSS for the items, for example:

Now things start to get a bit murky, as it usually does in SharePoint land. If you have an event that spans multiple days, you will get 1 list item returned for the stylesheet to parse. In order to select which days are enabled in the datepicker control, we need to at least know the start and end dates for an event, or alternatively the start date plus the duration (in days). Add to this the SharePoint calendars allowance for all-day events versus start / end times, and we have a few combinations that need taking care of. It starts to become clear that the DataMappings for the Web Part needs to bring back a few other columns, not just the Start Date / Location and Description of the event.
Back to google and Peter Gerritsen’s post on displaying calendar items in an itemstyle gives us a starting point, where he takes into account all day events and multi day events. In the end, the following columns in bold were added to the DataMappings of the web part.
<property name="DataMappings" type="string">Description:{3d303b89-2a5d-48be-95c8-ed510c784a36},ItemRollupDescription,Note;|Title:{fa564e0f-0c70-4ab9-b863-0177e6ddd247},Title,Text;|SomeDate:|Location:{288f5f32-8462-4175-8f09-dd7ba29359a9},Location,Text;|ArticleByLine:|Duration:{4d54445d-1c84-4a6d-b8db-a51ded4e1acc},Duration,Integer|fAllDayEvent:{7d95d1f4-f5fd-4a70-90cd-b35abc9b5bc8},AllDayEvent,Text|EndDate:{2684f9f2-54be-429f-ba06-76754fc056bf},EndDate,DateTime;|EventDate:{64cd368d-2f95-4bfc-a1f9-8d4324ecb007},StartDate,DateTime;|ImageUrl:|LinkUrl:|ItemRollupDescription:{3d303b89-2a5d-48be-95c8-ed510c784a36},ItemRollupDescription,Note;|Ratings:|</property>
<property name="DataMappingViewFields" type="string">{fa564e0f-0c70-4ab9-b863-0177e6ddd247},Text;{3d303b89-2a5d-48be-95c8-ed510c784a36},Note;{4d54445d-1c84-4a6d-b8db-a51ded4e1acc},Integer;{7d95d1f4-f5fd-4a70-90cd-b35abc9b5bc8},AllDayEvent;{2684f9f2-54be-429f-ba06-76754fc056bf},DateTime;{64cd368d-2f95-4bfc-a1f9-8d4324ecb007},DateTime;{288f5f32-8462-4175-8f09-dd7ba29359a9},Text;{3d303b89-2a5d-48be-95c8-ed510c784a36},Note;</property>
Still on the multiple day issue, we need to make the itemstyle.xsl emit 10 divs for a 10 day event, where it would normally only render 1. Here we have to resort to a template that mimics a for loop in xslt, using recursion. Where would any of us business developers be without google !? Because SharePoint 2010 is still using XSLT 1.0, even the simple act of adding 1 day to a datetime to give the next day becomes a hassle. This is done so that each div belonging to an event has an attribute of EventDate=‘MM/dd/yyyy’, which some jQuery code can filter for when deciding which div(s) to show, once the user has selected a date in the datepicker. Here’s the XSL template for adding days from SharePoint Boris. Let me know of any alternate methods for datetime manipulation in XSLT 1.0, I just couldn’t find anything simpler. Or better yet, is there a way to get XSLT 2.0 running instead? (note, this isn’t formatting we’re dealing with here, otherwise ddwrt:FormatDate would have been the way to go).
In order to work out how many days an Event spans, I went with “ceiling(@Duration div 86400)” in XSLT, where (seconds) @Duration is divided by 24hrs x 60 mins x 60 seconds = 86,400 seconds in a day. The ceiling function rounds up, so for something that say started @ 9am Monday and finished @ 5pm Tuesday, we get 2 days.
There’s still a few things that this Content Query Web Part doesn’t handle, namely :
- Recurring events - Haven’t even had a look at this, but I suspect it’s going to be messy
- Perhaps some colour coding based on Calendar - Should be straightforward to do. For example, by adding a custom column to Calendars for colour, which could flow all the way through to the xsl, and onto the datepicker to style accordingly.
Here’s the code, minus references to jQuery, theme CSS, datepicker js, etc. Sorry this whole thing isn’t available as a nice downloadable feature but this should show enough of the idea for tailoring to suit your need).
mainstyle.xsl
<xsl:template name="OuterTemplate.CallItemTemplate">
<xsl:param name="CurPosition" />
<xsl:value-of disable-output-escaping="yes" select="$BeginListItem" />
<xsl:choose>
…
… <!-- important to add the CurPos param below to OuterTemplate.CallItemTemplate, in order to supply our template
… with a way of checking if this is the first item of the render. We use this to inject our jQuery and DatePicker control -->
…
<xsl:otherwise>
<xsl:apply-templates select="." mode="itemstyle">
<xsl:with-param name="CurPos" select="$CurPosition" />
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
<xsl:value-of disable-output-escaping="yes" select="$EndListItem" />
</xsl:template>
itemstyle.xsl
Because we’re manipulating strings, not actual datetime objects, we need a template to return month names :(<xsl:template name="EventForCalendarControl" match="Row[@Style='EventForCalendarControl']" mode="itemstyle">
<xsl:param name="CurPos" />
<xsl:param name="ItemCount" />
<xsl:variable name="preListBlock">
<xsl:if test="$CurPos = 1">
<![CDATA[
<script type='text/javascript'>
// return MM/DD/YYYY string for a date
function getDateMMDDYYYY(date) {
var mm = date.getMonth() + 1;
mm = (mm < 10) ? '0' + mm : mm;
var dd = date.getDate();
dd = (dd < 10) ? '0' + dd : dd;
return mm + '/' + dd + '/' + date.getFullYear();
}
jQuery(document).ready(function() {
//start off with all events hidden
jQuery('div.cal').hide();
jQuery('#datepicker').datepicker({
firstDay: 1,
//first day is monday
beforeShowDay: function(date) {
var hilight = [false, ''];
var dateString = getDateMMDDYYYY(date);
//check every event, if one falls
//on this date then enable the corresponding date cell
jQuery('div.cal').each(function(index, element) {
if (jQuery(this).attr('EventDate') == dateString) {
hilight = [true, 'isActive'];
return false;
}
});
return hilight;
},
onSelect: function(dateText, inst) {
jQuery('div.cal').hide(); //hide all previous events, if any
jQuery('div.cal[EventDate="' + dateText + '"]').show(); //show events with matching date
}
});
//wrap the last day of month, first days of next month onto this calendar
jQuery('#datepicker').datepicker('option', 'showOtherMonths', true);
//show todays events, if any, by default
jQuery('div.cal[EventDate="' + getDateMMDDYYYY(new Date()) + '"]').show();
});
</script>
<div id='datepicker'></div>
]]>
</xsl:if>
</xsl:variable>
<xsl:variable name="daysCounter" select="ceiling(@Duration div 86400)" />
<xsl:value-of select="$preListBlock" disable-output-escaping="yes"/>
<!-- Create N Divs for the given Event, where N is the number of days the Event spans -->
<xsl:call-template name="EventForCalendarControl.Loop">
<xsl:with-param name="count" select="$daysCounter" />
<xsl:with-param name="i" select="1" />
</xsl:call-template>
</xsl:template>
<xsl:template name="EventForCalendarControl.Loop">
<xsl:param name="i" />
<xsl:param name="count" />
<!-- Calculate the date this event is to appear under, for the datepicker -->
<xsl:variable name="calcEventDate">
<xsl:call-template name="calcDate">
<xsl:with-param name="date" select="ddwrt:FormatDateTime(@EventDate, 1033, 'MM/dd/yyyy')" />
<xsl:with-param name="days" select="$i - 1" />
</xsl:call-template>
</xsl:variable>
<!-- ///////////////////////////////////////////////////////// -->
<!-- ///// These are derived from the Event START date!! ///// -->
<xsl:variable name="dateTime" select="ddwrt:FormatDate(string(@EventDate), 1033, 3)" />
<xsl:variable name="dateTimeCondensed" select="ddwrt:FormatDate(string(@EventDate), 1033, 2)" />
<xsl:variable name="date" select="substring-before(substring-after($dateTime, ', '), ', ')" />
<xsl:variable name="time" select="substring-after($dateTimeCondensed, ' ')" />
<!-- ///////////////////////////////////////////////////////// -->
<xsl:variable name="SafeLinkUrl">
<xsl:call-template name="OuterTemplate.GetSafeLink">
<xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
</xsl:call-template>
</xsl:variable>
<xsl:if test="$i <= $count">
<div class="item link-item cal" style="display: none;">
<xsl:attribute name="EventDate">
<xsl:value-of select="$calcEventDate" />
</xsl:attribute>
<div class="description xslCal">
<div class="left">
<span class="month">
<xsl:call-template name="getMonthName">
<xsl:with-param name="monthNum" select="substring($calcEventDate,1,2)"/>
</xsl:call-template>
</span>
<span class="day">
<xsl:value-of select="substring($calcEventDate,4,2)" />
</span>
</div>
<div class="right">
<a href="" onclick="javascript:SP.UI.ModalDialog.ShowPopupDialog('{$SafeLinkUrl}');return false;">
<span class="title">
<xsl:value-of select="@Title" />
</span>
</a>
<span class="location">
Location: <xsl:value-of select="@Location" />
</span>
<span class="desc">
<xsl:value-of disable-output-escaping="yes" select="@ItemRollupDescription" />
</span>
<span class="time">
<xsl:choose>
<xsl:when test="@fAllDayEvent = 0">
Time: <xsl:value-of select="$time" />
</xsl:when>
<xsl:otherwise>
Time: All-day event
</xsl:otherwise>
</xsl:choose>
</span>
</div>
</div>
</div>
</xsl:if>
<xsl:if test="$i <= $count">
<xsl:call-template name="EventForCalendarControl.Loop">
<xsl:with-param name="i">
<xsl:value-of select="$i + 1"/>
</xsl:with-param>
<xsl:with-param name="count">
<xsl:value-of select="$count"/>
</xsl:with-param>
</xsl:call-template>
</xsl:if>
</xsl:template>
Slightly modified version of http://www.sharepointboris.net/2008/11/xsl-template-for-adding-days/ (this one accepts and returns MM/dd/yyyy format)<xsl:template name="getMonthName">
<xsl:param name="monthNum" />
<xsl:param name="monthName">
<xsl:choose>
<xsl:when test="$monthNum = '01'">
<xsl:text>January</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '02'">
<xsl:text>February</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '03'">
<xsl:text>March</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '04'">
<xsl:text>April</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '05'">
<xsl:text>May</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '06'">
<xsl:text>June</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '07'">
<xsl:text>July</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '08'">
<xsl:text>August</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '09'">
<xsl:text>September</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '10'">
<xsl:text>October</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '11'">
<xsl:text>November</xsl:text>
</xsl:when>
<xsl:when test="$monthNum = '12'">
<xsl:text>December</xsl:text>
</xsl:when>
</xsl:choose>
</xsl:param>
<xsl:value-of select="$monthName"/>
</xsl:template>
<xsl:template name="calcDate">
<xsl:param name="date"/>
<xsl:param name="days"/>
<xsl:param name="oldMonth">
<xsl:value-of select="substring($date,1,2)"/>
</xsl:param>
<xsl:param name="oldYear">
<xsl:value-of select="substring($date,7,4)"/>
</xsl:param>
<xsl:param name="oldDay">
<xsl:value-of select="substring($date,4,2)"/>
</xsl:param>
<xsl:param name="newMonth">
<xsl:choose>
<xsl:when test="$oldMonth = '01'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 31">01</xsl:when>
<xsl:otherwise>02</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '02'">
<xsl:choose>
<xsl:when test="($oldYear mod 4 = 0 and number($oldDay) + $days <= 29) or ($oldYear mod 4 != 0 and number($oldDay) + $days <= 28)">02</xsl:when>
<xsl:otherwise>03</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '03'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 31">03</xsl:when>
<xsl:otherwise>04</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '04'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 30">04</xsl:when>
<xsl:otherwise>05</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '05'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 31">05</xsl:when>
<xsl:otherwise>06</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '06'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 30">06</xsl:when>
<xsl:otherwise>07</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '07'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 31">07</xsl:when>
<xsl:otherwise>08</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '08'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 31">08</xsl:when>
<xsl:otherwise>09</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '09'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 30">09</xsl:when>
<xsl:otherwise>10</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '10'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 31">10</xsl:when>
<xsl:otherwise>11</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '11'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 30">11</xsl:when>
<xsl:otherwise>12</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '12'">
<xsl:choose>
<xsl:when test="number($oldDay) + $days <= 31">12</xsl:when>
<xsl:otherwise>01</xsl:otherwise>
</xsl:choose>
</xsl:when>
</xsl:choose>
</xsl:param>
<xsl:param name="newYear">
<xsl:choose>
<xsl:when test="$oldMonth = '12' and (number($oldDay) + $days) >= 31">
<xsl:value-of select="number($oldYear) + 1"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$oldYear"/>
</xsl:otherwise>
</xsl:choose>
</xsl:param>
<xsl:param name="newDay">
<xsl:choose>
<xsl:when test="$oldMonth = '01'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 31"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '02'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="$oldYear mod 4 = 0">
<xsl:value-of select="number($oldDay) + $days - 29"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 28"/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '03'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 31"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '04'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 30"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '05'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 31"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '06'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 30"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '07'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 31"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '08'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 31"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '09'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 30"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '10'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 31"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '11'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 30"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$oldMonth = '12'">
<xsl:choose>
<xsl:when test="$newMonth = $oldMonth">
<xsl:value-of select="number($oldDay) + $days"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="number($oldDay) + $days - 31"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
</xsl:choose>
</xsl:param>
<xsl:if test="$newMonth < 10">0</xsl:if>
<xsl:value-of select="$newMonth" />
<xsl:text>/</xsl:text>
<xsl:if test="$newDay < 10">0</xsl:if>
<xsl:value-of select="$newDay"/>
<xsl:text>/</xsl:text>
<xsl:value-of select="$newYear"/>
</xsl:template>
</xsl:stylesheet>
