Using the Content Query Web Part and jQuery to create a staff desk locator

I recently took it upon myself to provide an alternative to the staff desk locator that was due to go to production in the near future. The back-story is that a requirement existed to locate staff on a floor map to assist with the move to a new office complex. The original concept was created by another developer, but with multiple requirement changes and multiple developers tweaking the original solution, the end result was far from perfect. It only worked completely in IE7 standards browser mode and some of the functionality performed in a less than ideal fashion – think scroll bars to navigate around a zoomed in map amongst other imperfections.

Now the solution I present here is also far from perfect, I have no illusions about that. I debated whether to post about it at all until I had a chance to optimse and refine it seeing that what exists here is essentially a protoype, however I believe it is in a complete enough state to provide some benefit or interest to people out there. If anyone can propose some better ways of achieving some of the functionality demonstrated here I’d be more than interested in hearing it. The first half of the post will explain the journey, so if you’re not interested in what drove the implementation decisions along the way, feel free to skip down to the solution.

The Journey

The concept I was going for was something similar to Google Maps; your standard zoom, move, mouse-wheel and drag functionality. The most attractive option I could find was a paid solution called MapBox which can be seen in their various featured map demos. I didn’t think a paid solution would be an option and besides, I wanted to see if I could create something similar myself. I found a jQuery plugin also called MapBox documented at Mapbox: Zoomable jQuery Map Plugin which definitely appeared to get me half way there. The only remaining challenge was to plot the points on the map and get it driven via SharePoint.

Plotting the points definitely proved challenging. I went through a few iterations as I lead myself down some wrong paths – the joys of experimentation. I went so far as to get everything adjusting correctly from a movement point of view (thinking I was better off starting easy and leaving the complicated functionality for later) only to have it breaking during zoom. It wasn’t until I further investigated the zoom requirements and changed the way in which I was approaching the challenge that I realised the work I had done to adjust for movement was redundant and was handled by the plugin. Lovely.

I eventually got the locations remaining in place during zooming however this exposed its own issues; the pins representing each location didn’t adjust in size as the map did, resulting in a very strange looking experience. It is here where my solution is least elegant – I essentially created multiple CSS classes and multiple sized images and dynamically changed the class of the elements as I zoomed in and out (using the width of the parent div to differentiate). I couldn’t simply scale the image as I was using background images and divs rather than image elements (for reasons based on issues with the previous solution and brief experimentation in this one). This exposed even more gotchyas – it appears different browsers round decimal values in different ways (there were variations around rounding, floor or ceiling). I also noticed that while on most occassions the plugin would zoom to fixed width sizes, occassionally it would result in completely unexpected values which I needed a safety net CSS definition for. I love Internet Explorer. The CSS is hence a pretty ugly beast, but for the most part it works.

The next step was adding some hover over functionality to display the staff members information. This was relatively simple and just leveraged the onMouseOver event and some jQuery for each plotted element. The search functionality was a little more complex. I had witnessed the functionality I desired during my research, present on the Rock ‘n’ Roll Metro Map, which I dug deeper into and discovered it was being achieved via the animate function of the jQuery UI plugin. A couple of equations and experimentation later to determine how I could establish my scroll-to points and it was all humming along nicely.

The final challenge was getting it all into SharePoint. This functionality was to sit on the intranet which had been created as a codeless solution, therefore rather than rendering what I needed via the API in a web part I decided to stick with the theme and leverage the Content Query Web Part (CQWP), drawing out of a custom list. The previous solution had done the same so it wasn’t too difficult with a few tweaks to achieve what I desired. A couple of the gotchyas encountered included ensuring the CommonViewFields were set in the CQWP (exporting and importing the web part was used to achieve this) and rediscovering that for a column to exist to filter on in the web part it needed to be a site column, not just a list column. One last little hurdle that needed to be jumped included how the parts sat on the page with the master page being used – and this differed between browsers (Did I mention how much I love IE?). Seeing this probably had a lot to do with the custom master page being used I’ll leave the specifics out in the solution below, however just know that it is something that may need to be considered.

The Solution

Part 1: The list to hold the data

This was a custom list with some custom columns. Note that the IsAResource column is a site column. The -1000 X and Y values reflect a staff member that does not exist in the building.

Part 2: Header.xsl

The templates in Header.xsl map to the Group Style settings selected in the Content Query Web Parts. They render once to open and close the items which are rendered in between. The below templates were defined.

  <xsl:template name="DeskfinderSelectName" match="*[@GroupStyle='DeskfinderSelectName']" mode="header">
    <script type="text/javascript">document.write('<![CDATA[<div id="SearchByName"><span class="SearchLabel">Search by Name:</span><select onchange="FindUser(this.value);"><option value="">Please select an option...</option>]]>')</script>
  </xsl:template>
  <xsl:template name="DeskfinderSelectNameClose" match="*[@GroupStyle='DeskfinderSelectName']" mode="footer">
    <script type="text/javascript">document.write('<![CDATA[</select></div>]]>')</script>
  </xsl:template>
  <xsl:template name="DeskfinderSelectResource" match="*[@GroupStyle='DeskfinderSelectResource']" mode="header">
    <script type="text/javascript">document.write('<![CDATA[<div id="SearchByResource"><span class="SearchLabel">Search by Resource:</span><select onchange="FindUser(this.value);"><option value="">Please select an option...</option>]]>')</script>
  </xsl:template>
  <xsl:template name="DeskfinderSelectResourceClose" match="*[@GroupStyle='DeskfinderSelectResource']" mode="footer">
    <script type="text/javascript">document.write('<![CDATA[</select></div>]]>')</script>
  </xsl:template>
  <xsl:template name="DeskfinderPin" match="*[@GroupStyle='DeskfinderPin']" mode="header">
    <script type="text/javascript">document.write('<![CDATA[<div class="StaffLocations">]]>')</script>
  </xsl:template>

The most important part to note here is that the DeskfinderPin doesn’t have an associated footer. This was a bit of a gotchya – the browsers closed off the div anyway but if I included the footer, the

<script type="text/javascript">document.write('<![CDATA[</div>]]>')</script>

node was appearing within the div which caused issues when I went to extract the HTML via jQuery and pump it into other locations. It worked fine without the footer so it was removed.

Part 3: ItemStyle.xsl

The templates in ItemStyle.xsl map to the Item Style settings selected in the Content Query Web Parts. They’re rendered for each item in the query. The below templates were defined.

  <xsl:template name="DeskfinderSelectOption" match="Row[@Style='DeskfinderSelectOption']" mode="itemstyle">
    <xsl:variable name="DisplayTitle">
      <xsl:call-template name="OuterTemplate.GetTitle">
        <xsl:with-param name="Title" select="@Title"/>
        <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
      </xsl:call-template>
    </xsl:variable>
    <option value="{@ID}">
      <xsl:value-of select="$DisplayTitle"/>
    </option>
  </xsl:template>
  <xsl:template name="DeskfinderPin" match="Row[@Style='DeskfinderPin']" mode="itemstyle">
    <xsl:variable name="DisplayTitle">
      <xsl:call-template name="OuterTemplate.GetTitle">
        <xsl:with-param name="Title" select="@Title"/>
        <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
      </xsl:call-template>
    </xsl:variable>
    <xsl:choose>
      <xsl:when test="starts-with(@Location,'L2')">
		<div class="pin2" style="left:{@X}px; top:{@Y}px;" onMouseOver="DisplayDetails(this);" data-id="{@ID}" data-xpos="{@X}" data-ypos="{@Y}" data-name="{$DisplayTitle}" data-phone="{@Phone}" data-email="{@Email}"></div>
      </xsl:when>
      <xsl:otherwise>
        <div class="pin3" style="left:{@X}px; top:{@Y}px;" onMouseOver="DisplayDetails(this);" data-id="{@ID}" data-xpos="{@X}" data-ypos="{@Y}" data-name="{$DisplayTitle}" data-phone="{@Phone}" data-email="{@Email}"></div>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

It is important to note that all of the custom columns referenced in the template (X, Y, Email, Phone) need to appear in the CommonViewFields element of the Content Query Web Part to be recognised. This can be done by exporting the web part, manually editing the .webpart file to add those columns and importing it back onto the page.

Part 4: The resources

CSS – the CSS used was extended from that used for the MapBox demo with some slight tweaks to those definitions. A little more was added to style the drop down menus and staff information. The majority of CSS was to style the pins mapping to the locations of staff members. As mentioned before, this was a very bloated solution and far too much to include in this post. I’ll try and summarise.

There were essentially 4 groups of styles – level 2 pins, level 3 pins, level 2 ‘located’ pins and level 3 ‘located’ pins. These were seperated into whether they appeared in the firstLayer, secondLayer, thirdLayer or fourthLayer. They were further seperated into sizes mapping to the width of the parent div (the reason for 2 sizes 1 pixel apart is due to the rounding issue I mentioned earlier). Here are a few randomly selected examples:

#firstLayer .pin2
{
	position: absolute;
	width: 10px;
	height: 9px;
	background: url(../PublishingImages/pin2-size709.gif);
}

#secondLayer .pin2.found.size974, #secondLayer .pin2.found.size975
{
	width: 16px;
	height: 16px;
	background: url(../PublishingImages/found2-size975.gif);
}

#fourthLayer .pin3 {
	position: absolute;
	width: 30px;
	height: 27px;
	background: url(../PublishingImages/pin3-size2126.gif);
}

#fourthLayer .pin3.size1505, #fourthLayer .pin3.size1506 {
	width: 21px;
	height: 19px;
	background: url(../PublishingImages/pin3-size1506.gif);
}

You get the idea. Perhaps look into an alternative solution!

Images – The images included the 4 different sized floor map images, the map control image and 100 different pin images to match the CSS styles.

JavaScript – The javascript leveraged that provided with the MapBox demo with the following adjustments.

Functionality to populate the staff information:

function DisplayDetails(person)
{
	$("#staffName").text($(person).attr("data-name"));
	$("#staffPhone").text($(person).attr("data-phone"));
	$("#staffEmail").html('<a href="mailto:' + $(person).attr("data-email") + '">' + $(person).attr("data-email") + '</a>');
}

Functionality to find a given user or resource:

function FindUser(id)
{
	var limitX = 0, limitY = 0, mapWidth = $(".mapwrapper").width(), mapHeight = $(".mapwrapper").height(),
	nodeWidth = $(".current-map-layer").width(), nodeHeight = $(".current-map-layer").height();

	if(mapWidth < nodeWidth) limitX = mapWidth - nodeWidth;
	if(mapHeight < nodeHeight) limitY = mapHeight - nodeHeight;

	var element = $(".current-map-layer div[data-id='" + id + "']");
	var left = (parseInt($(element).css("left")) * -1) + (parseInt($(".mapwrapper").css("width")) / 2);
	var top = (parseInt($(element).css("top")) * -1) + (parseInt($(".mapwrapper").css("height")) / 2);

	left = (left > 0) ? 0 : left;
	left = (left < limitX) ? limitX : left;
	top = (top > 0) ? 0 : top;
	top = (top < limitY) ? limitY : top;

	var width = parseInt($(".current-map-layer").css("width"));
	if ($("div[data-id='" + id + "']").attr("class").indexOf("pin2") == -1)
		$("div[data-id='" + id + "']").attr("class", "pin3 found size" + width);
	else
		$("div[data-id='" + id + "']").attr("class", "pin2 found size" + width);

	$(".current-map-layer").animate({top: top,left: left}, {easing: "easeOutQuart"});
	DisplayDetails(element);
}

The important thing to note here is that extra code was required to ensure the map wasn’t moved ‘out of viewport’. Thankfully, the MapBox JavaScript already included that logic for its drag functionality so I just leveraged that.

Document Ready functionality:

$(document).ready(function() {
	$("#firstLayer,.mapcontent").html($(".StaffLocations").html());

	$("#viewport").mapbox({
		mousewheel: true,
		layerSplit: 8//smoother transition for mousewheel
	});
	$(".map-control a").click(function() {//control panel
		var viewport = $("#viewport");
		//this.className is same as method to be called
		if(this.className == "zoom" || this.className == "back") {
			viewport.mapbox(this.className, 2);//step twice
		}
		else {
			viewport.mapbox(this.className);
		}
		return false;
	});
})

The first line is the most important – this is where the HTML is taken from the hidden div rendered by our Content Query Web Part and injected into the necessary locations. The rest of the code is the same that existed for the MapBox demo.

Custom changes to the _zoom function (insert above return movement; line)

/* Custom changes */
var xChange = (totalWidth == undefined) ? 1 : totalWidth / $("#firstLayer").width();
var yChange = (totalHeight == undefined) ? 1 : totalHeight / $("#firstLayer").height();

$(".pin2,.pin3").each(function(index, value) {
	$(this).css("left", ($(this).attr("data-xpos") * xChange));
	$(this).css("top", ($(this).attr("data-ypos") * yChange));

	var width = parseInt($(".current-map-layer").css("width"));
	if ($(this).attr("class").indexOf("pin2") == -1)
	{
		if ($(this).attr("class").indexOf("found") == -1)
			$(this).attr("class", "pin3 size" + width);
		else
			$(this).attr("class", "pin3 found size" + width);
	}
	else
	{
		if ($(this).attr("class").indexOf("found") == -1)
			$(this).attr("class", "pin2 size" + width);
		else
			$(this).attr("class", "pin2 found size" + width);
	}
});
/* End Custom changes */

Part 5: Referencing the resources

This was a chromeless Content Editor Web Part referencing the required resources:

<link href="../Documents/master.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.3/jquery.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script>
<script type="text/javascript" src="../Documents/mousewheel.js"></script>
<script type="text/javascript" src="../Documents/mapbox.js"></script>

Take note the version of jQuery referenced – it’s not the latest, but was required for MapBox to function correctly.

Part 6: The search box

This was a chromeless Content Query Web Part pointing to the list in Part 1. It was set to show items of type ‘Custom List’ and filtered where ‘IsAResource is equal to 0’. The Presentation was grouped by <Site> (to ensure the group element was rendered) and sorted by ‘Title ascending’. The ‘Limit the number of items to display’ was turned off. The Group Style was set to ‘DeskfinderSelectName’ and Item Style set to ‘DeskfinderSelectOption’.

To include a Search by Resource box as well as a Search by Name box, the steps above can be followed, however the filter value should be 1 and the Group Style should be set to ‘DeskfinderSelectResource’.

Part 7: The map, associated controls and staff information

This was a chromeless Content Editor Web Part which essentially rendered the HTML which existed in the MapBox demo with a few minor tweaks.

<div class="mapwrapper">
	<div id="viewport" style="overflow: hidden; position: relative;">
		<div id="firstLayer" class="current-map-layer" style="background: url('Images/FloorImage1.png') no-repeat scroll 0% 0% transparent; width: 709px; height: 631px; position: absolute; display: block; left: 0px; top: 0px;">
		</div>
		<div id="secondLayer" style="height: 947px; width: 1063px; position: absolute; display: none;">
			<img style="width: 100%; position: absolute; left: 0pt; top: 0pt;" src="Images/FloorImage.png" alt="">
			<div style="position: absolute; left: 0pt; top: 0pt; background: none repeat scroll 0% 0% white; opacity: 0;" class="map-layer-mask"></div>
			<div style="position: absolute; top: 0pt; left: 0pt;" class="mapcontent">
			</div>
		</div>
		<div id="thirdLayer" style="height: 1262px; width: 1417px; position: absolute; display: none;">
			<img style="width: 100%; position: absolute; left: 0pt; top: 0pt;" src="Images/FloorImage.png" alt="">
			<div style="position: absolute; left: 0pt; top: 0pt; background: none repeat scroll 0% 0% white; opacity: 0;" class="map-layer-mask"></div>
			<div style="position: absolute; top: 0pt; left: 0pt;" class="mapcontent">
			</div>
		</div>
		<div id="fourthLayer" style="height: 1893px; width: 2126px; position: absolute; display: none;">
			<img style="width: 100%; position: absolute; left: 0pt; top: 0pt;" src="Images/FloorImage.png" alt="">
			<div style="position: absolute; left: 0pt; top: 0pt; background: none repeat scroll 0% 0% white; opacity: 0;" class="map-layer-mask"></div>
			<div style="position: absolute; top: 0pt; left: 0pt;" class="mapcontent">
			</div>
		</div>
	</div>
	<div class="map-control">
		<a href="#left" class="left">Left</a>
		<a href="#right" class="right">Right</a>
		<a href="#up" class="up">Up</a>
		<a href="#down" class="down">Down</a>
		<a href="#zoom" class="zoom">Zoom</a>
		<a href="#zoom_out" class="back">Back</a>
	</div>
	<div class="staff-information">
		<div><span class="staff-info-label">Name: </span><span id="staffName"></span></div>
		<div><span class="staff-info-label">Phone: </span><span id="staffPhone"></span></div>
		<div><span class="staff-info-label">Email: </span><span id="staffEmail"></span></div>
	</div>
</div>

Note that the images and sizes were adjusted respectively and each layer div was given an id so I could reference them via both jQuery and CSS. The staff-information div was also included to present the staff information.

Part 8: The staff locations

This was a chromeless Content Query Web Part pointing to the list in Part 1. It was set to show items of type ‘Custom List’, grouped by <Site> and the ‘Limit the number of items to display’ was turned off. The Group Style was set to ‘DeskfinderPin’ and Item Style set to ‘DeskfinderPin’.

Note that the contents rendered here were within a div hidden by the CSS – it’s main purpose was to render the data once, then jQuery injected it into the locations required.

The End Result

An image doesn’t really do it justice, but for the purposes of completing the article, the end result looks something like that depicted below.

Either that or you can watch a video below on how the functionality works.

How the Career Centre mobile site was built using SharePoint

I’ve recently had the pleasure of working on a large public facing portal solution for the Career Centre. I came onto the project midway through so inherited a lot of branding and functionality, however early enough to create a number of components from scratch, having an input into the design and implementation decisions along the way. One such piece of functionality was the mobile site which can be accessed on your phones by clicking the link above, or in your browsers by accessing the mobile site directly.

The reason for this post is that it hasn’t been done exactly the way one would imagine. I thought I’d take the chance to explain the decision process behind the implementation and potentially open the lines of discussion on whether the decisions made were valid, based on false logic or whether there was a better solution available. For the purpose of context the Career Centre portal was developed on MOSS 2007 Enterprise Edition.

We’ll start with the requirements. The mobile site design had been provided to us so barring some minor tweaks, was not something that required a large amount of attention. The content of the site was to be a sub-set of the live site; a combination of landing pages which were to differ from the live site, content pages which mirrored the live site exactly, content pages which either differed from or didn’t exist in the live site, ‘multi-part’ content pages (based on a custom page layout) and functional aspects of the site (including but not limited to various search capabilities).

My thoughts were that with a custom master page, mobile-specific page layouts and mobile-specific controls and web parts, the majority of requirements would be easy enough to satisfy, and that largely turned out to be the case. My main concern was with content and ensuring the most efficient solution without resorting to content duplication, and the thought process behind that is what will form the basis of this post.

I had a fair understanding already that the ‘best practice’ way of implementing a mobile site in SharePoint was to leverage the variation capabilities of the product. Some initial research I performed backed up that presumption; the most impressive post I personally found for the topic was Jamie McAllister’s discussion on Exposing your MOSS WCM to Mobile Devices. I’d had some brief exposure to SharePoint variations from my time at TourismWA on westernaustralia.com. There, variations were used for the purposes of multi-lingual sites (the complexity of that implementation is definitely worthy of a post on its own!). I was therefore already hesitant to embrace this functionality for my purpose.

Some extra research I did around variations did little to calm my nerves. Some of the more relevant posts I read include Jamie McAllister’s Do MOSS Variations really SUCK, Alex’s Common Problems with MOSS Variations and Jeremy Jameson’s Dumping MOSS 2007 variations Part 1, 2 and 3. There were a bunch of other negative posts I read that escape me now. For the sake of balance, Stefan Goßner’s series on the Variation Feature should also be read.

My main concern however was that changing a page’s layout from the initial site’s to the mobile-specific layout would cause a variation and hence no longer receive content updates; funnily enough, this proved a non-issue. Through my experimentation however I did come up with a raft of other concerns including;

  1. A variation required the source to be a sub-web of the root – which means we couldn’t have careercentre.dtwd.wa.gov.au, it would have to be careercentre.dtwd.wa.gov.au/careercentre or something to that effect. The varied site would then also be a sub-site like careercentre.dtwd.wa.gov.au/mobile. This may seem like a small issue – but the URL of the site definitely was no small issue for the client.
  1. You can set up variations initially so that they auto-populate the varied site when new sites/pages are created, or you can set them up so they do not. We would have needed to turn that off considering the mobile site was to be a small sub-section of the main site. Problem being, once that feature was turned off we would then have to manually build up any new site structure to match. Once the variation hierarchy has been created automatically it can’t be done again.
  1. When I automatically created this hierarchy, it seemed to pick and choose which pages it recreated. I noticed only 2nd level sub-site pages where being brought across. I also noticed that for each site brought across, instead of recreating the default page it created a new default welcome page. This was obviously a huge concern, and would amount to a fair degree of manual labour to get it all up and running initially.
  1. Lists don’t get brought across and can’t be considered linked-up varied content. That means all the lists (that we needed for the mobile site) would need to be created in the mobile site manually and a process set up to copy content across in the event it was added or changed. This was more programmatic work that we were trying to avoid due to time constraints. (If this is a concern of yours have a read of another of Jamie’s posts)

For these reasons along with the research I had conducted and my own prior bias, rightly or wrongly I decided to turf the idea of using variations and proceeded to investigate an alternate solution.

Thanks to a huge degree of good fortune I had only days before been playing around with the Content Query Web Part along with CommonViewFields and styling with ItemStyle.xsl. I decided to leverage this functionality and the result was instant. I exported the Content Query Web Part and redeployed it under a different name to include the CommonViewFields of PublishingPageContent,HTML; and all fields associated with my ‘multi-part’ page layout. Important to note is that when used on the page and after pointing it to the relevant Pages library the page to copy exists in, an additional filter of Title equal to the current page’s title needed to be set – in hindsight I could have created a new web part overriding the content query web part to set this filter and the CommonViewFields automatically.

I then edited the ItemStyle.xsl file to include templates for all the fields listed in CommonViewFields:

<xsl:template name="PageHTMLOnly" match="Row[@Style='PageHTMLOnly']" mode="itemstyle">
    <xsl:variable name="DisplayTitle">
        <xsl:call-template name="OuterTemplate.GetTitle">
            <xsl:with-param name="Title" select="@Title"/>
            <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
        </xsl:call-template>
    </xsl:variable>
    <xsl:value-of disable-output-escaping="yes" select="@PublishingPageContent"/>
</xsl:template>

Only 2 issues remained; firstly, all in-content links pointed to the non-optimised pages regardless if a mobile-optimised page existed and secondly, users weren’t being warned if they were being taken to an un-optimised page within the site. jQuery to the rescue.

To ensure mobile-optimised pages were linked to appropriately, I included the attribute class=”mobilelink” on all hyperlinks that needed it. The following jQuery took care of the rest:

$('.mobilelink').attr('href', function (index, val) {
  var returnVal = val;

  if (val.toString().toUpperCase() == '/CAREERPLANNING/KNOWINGYOURSELF/PAGES/KNOWINGYOURSELF.ASPX')
    returnVal = '/mobile/careerplanning/pages/knowingyourself.aspx';
  else if (val.toString().toUpperCase() == '/PAGES/CONTACTUS.ASPX')
    returnVal = '/mobile/contactus/pages/contactus.aspx';
  else if (val.toString().toUpperCase() == '/HOWWECANHELP/PAGES/HOWWECANHELP.ASPX')
    returnVal = '/mobile/pages/howwecanhelp.aspx';
  else
    returnVal = '/mobile' + val;

  return returnVal;
});

It would have been a little neater had the mobile site structure mapped more accurately to the live site structure, but never mind.

To ensure users were warned if they were about to enter a non-optimised page, jQuery Dialog was harnessed:

<div id="dialog" title="Confirmation Required">
  You are about to leave the mobile-optimised site.<br /><br />
  Would you like to continue?
</div>
$("#dialog").dialog({
  modal: true,
  bgiframe: true,
  width: 280,
  height: 185,
  autoOpen: false
});

$("a").filter(function (index) {
  var theHREF = $(this).attr("href");
  return theHREF != undefined &&
  theHREF != null &&
  theHREF != "" &&
  theHREF.toUpperCase().indexOf("JAVASCRIPT:") == -1 &&
  theHREF.toUpperCase().indexOf("/MOBILE/") == -1 &&
  theHREF.toUpperCase().indexOf("TEL:") == -1 &&
  theHREF.toUpperCase().indexOf("MAILTO:") == -1 &&
  theHREF != "#" &&
  theHREF.charAt(0) != "#";
}).click(function(e) {
  var theHREF = $(this).attr("href");
  var target = $(this).attr("target");
  e.preventDefault();

  $("#dialog").dialog('option', 'buttons', {
    "Confirm" : function() {
      if (target != undefined && target == "_blank")
      {
        window.open(theHREF, target);
      }
      else
      {
        window.location.href = theHREF;
      }

      $(this).dialog("close");
    },
    "Cancel" : function() {
      $(this).dialog("close");
    }
  });

  $("#dialog").dialog("open");
});

The end result was a mobile site that needed to be created manually initially (but with little effort – my experimentation with variations concluded that I would have had to do the same if not more to get that working initially) but would largely be set and forget assuming the mobile site structure didn’t need to change. Content updates would be instant rather than relying on the synchronisation process between the source and varied page. We were also able to maintain the ‘root’ URL of careercentre.dtwd.wa.gov.au by not requiring the source variation site to be created.

The last remaining problem was getting there. Seeing we hadn’t harnessed variations, we didn’t have the VariationRoot page to direct browsers to the mobile version of the site. Instead I ensured the default page of the root site had a web part on it that contained the logic to perform the redirect courtesy of Detect Mobile Browsers.

So there we have it. A result not particularly conforming with stated best practices however one the client is stoked with and should prove easily maintainable and effective. I’d be particularly interested to hear about other ways in which mobile sites have been created in SharePoint and anyone’s experience using SharePoint variations for that purpose.