Performance Optimising SharePoint Sites – Part 2

In Performance Optimising SharePoint Sites – Part 1 of this series I focussed on some of the first steps you should undertake or consider when embarking on performance optimising your SharePoint site. This part of the series will explore some of the platform-independent techniques available at your disposal while Performance Optimising SharePoint Sites – Part 3 will identify some of the SharePoint-specific techniques able to be leveraged.

Before I delve into some of the techniques available I should point out that the information available on this subject is extensive. It may seem a bit redundant even including this section in my series due to the wealth of information available elsewhere however I’ve done so for completeness. Both Google and Yahoo offer terrific sources of information on this subject matter if you’d prefer to go directly to the source – i’ll try to differentiate my contribution by linking to alternate tools and resources.

Minimise HTTP Requests

Reducing the number of HTTP requests on your page is one of the best ways to reduce the load time, particularly for first time visitors. As pointed out on Yahoo’s performance rules page via Tenni Theurer’s Browser Cache Usage – Exposed! 40-60% of daily visitors to your site come in with an empty cache. Every request requires a round trip to the server so it’s easy to see how this component of page optimisation could provide a significant improvement to the overall load time of the page. The majority of techniques that can be used to achieve this will be identified in the following 3 sections.

Optimise your JavaScript

There are a number of ways in which the JavaScript on your site can be performance optimised. Firstly, you should Move Scripts to the Bottom as Steve Sauders explains. This enables progressive rendering and helps to achieve parallel downloads. This should only be done where it doesn’t impact the visual rendering of the page. You should ensure that JavaScript isn’t duplicated within your site – either in various libraries or particularly in terms of referencing the same file multiple times (which can happen if you’ve included the reference in multiple web parts or controls). Your JavaScript should exist in external files rather than inline so that it can be cached and hence not add to the size of the HTML document. You should combine your JavaScript into as few external files as possible to minimise the number of HTTP requests and finally you should minify those external JavaScript files. There are a number of resources available to assist you in automating the minification process, both for Visual Studio described in Dave Ward’s post Automatically minify and combine JavaScript in Visual Studio or if you prefer it with a SharePoint flavour via Waldek Mastykarz’s Minifying JavaScript and CSS files made easy with Mavention SharePoint Assets Minifier.

Optimise your CSS

Similarly to optimising JavaScript, you can perform a number of optimisations to your CSS. Whereas you should load your JavaScript at the bottom of the page to enable progressive rendering, with CSS you should Put Stylesheets at the Top. You should always refactor your CSS similar to how you would your code to ensure it is optimal and doesn’t repeat definitions. Take advantage of the fact you can assign multiple classes to your elements – for instance where appropriate include a base CSS class which has the shared definitions and another which has the unique ones. You should externalise your CSS for the same reasons as you should your JavaScript and combine your CSS into as few external files as appropriate (find the balance between reducing HTTP requests and combining a large amount of CSS which is only used on one or two pages). Finally, you are also able to minify your CSS much in the same fashion as you can your JavaScript.

Optimise your Images

Along with HTTP requests, the size of images is one of the biggest factors in determining page load times. There are 3 major areas of focus when it comes to optimising your images. Firstly, it is important that the images are as optimised as possible to begin with. This means achieving smaller file sizes without reducing the quality of the image itself. A number of tools exist to assist with this highlighted in Jacob Gube’s post 8 Excellent Tools for Optimizing Your Images – the one I’m most familiar with it Yahoo’s Smush.it. Secondly, it’s important to ensure that you’re not relying on the browser to resize your images. It is common to find large images reduced in size by the size of the containing element which is definitely not optimal. Finally, CSS sprites can be used to reduce the number of HTTP requests required to retrieve the images explained via Chris Coyier’s CSS Sprites: What They Are, Why They’re Cool, and How To Use Them. One thing you’ll want to consider is that you may be able to optimise the images for the site initially, but if you have content being added by end users and they upload and reference unoptimised and HTML-resized images to a page you’re at risk of losing the optimised nature of that page. One final thing to note is that if you’re displaying a page with a known transition path you can pre-load the images that will appear on the next screen/page to further optimise that page’s load time using a method such as CSS Ninja’s Even better image preloading with CSS2.

Utilise GZip Compression

GZip compression is another way in which you can reduce the size of the payload coming back from the server to the user accessing your site. The majority of browsers support GZip compression and hence this should not be ignored. For a better explanation of why it is beneficial and what it actually does have a read of Kalid Azad’s How To Optimize Your Site With GZIP Compression. For implementation details with IIS6 see Bill Baer’s HTTP Compression, Internet Information Services 6.0, and SharePoint Products and Technologies or Todd Sharp’s Enabling GZip Encoding On IIS7.

In Performance Optimising SharePoint Sites – Part 3 of this series i’ll identify some of the SharePoint-specific techniques able to be leveraged.

Anti-aliased fonts, Rounded Corners in IE and the Implications for SharePoint

That’s one mouthful of a title. I’ve lately been revisiting the Career Centre website and thought this one would be worthy of a post. It works on two fronts; firstly as an introduction to a couple of useful libraries which can add that extra bit of flavour to your websites and secondly as a warning or solution to a strange issue they cause in SharePoint. I haven’t tested them in SP2010 as yet so for now I can only describe the issue as I’ve seen it, in MOSS 2007.

Anti-aliased fonts

One of my pet hates when creating public facing sites from image files provided by a design company has always been fonts. It has always seemed a case of having to revert to an uglier looking font on the page or using an image sliced from the design. The problem is to do with the anti-aliasing effect image software such as Photoshop applies to the fonts in the image which isn’t replicated when using the font in a web browser. The image below outlines the difference.

Another limitation of browser-based fonts is being limited to that which is installed on the client machine. It’s all good and well defining your font to be a custom masterpiece and viewing it on your own desktop, however your regular punter who encounters your site will not witness the same experience.

Enter Cufon – a font generator and JavaScript rendering engine which will allow you to display the text within the browser how it was meant to be seen. Below is an example of the navigation on the Career Centre site; before and after Cufon.

The code to achieve this effect is pretty easy, something along the lines of:

<script type="text/javascript" src="cufon-yui.js"></script>
<script type="text/javascript" src="my.font.js"></script>

<script type="text/javascript">
    Cufon.replace('h1');
    Cufon.replace('.class');
    Cufon.replace('#id');
    Cufon.now();
</script>

I’m undecided if I really like the concept. I’m more of a purist and not sure if I like the performance hit and reliance on JavaScript – however you can’t argue with the results. Visually, it’s impressive.

Rounded corners in IE

Another feature which is harnessed across the Career Centre website is rounded corners driven by CSS. Previously if this had needed to be achieved I would have expected to have to use rounded corner images or an image background. These days with the advancement in browsers it can largely be achieved via CSS.

The newer versions of the major browsers all implement the border-radius tag to achieve this. Note however that IE9 needs the following meta tag to be included:

<meta http-equiv="X-UA-Compatible" content="IE=edge">

Older versions of FireFox, Chrome, Safari and Opera used proprietary prefixes like the ones below to achieve the same effect.

-moz-border-radius: 9px;
-webkit-border-radius: 9px;
-o-border-radius: 9px;

As usual however, older versions of IE fail to play nicely. This is where PIE comes in to play. PIE stands for Progressive Internet Explorer and assists in implementing CSS3 features for older versions of IE. There is a JavaScript version, however the version used for the Career Centre is an .htc file and CSS definitions which look like that represented below:

behavior: url(/_layouts/myfolder/PIE.htc);

This is another feature that i’m yet to be sold on – again there is an obvious trade-off between performance and visual excellence. To read a little further on this topic check out Jamy Golden’s post on IE and rounded corners.

Implications for SharePoint

So where does SharePoint come into this you ask? I’ve recently been investigating an issue related to a popup message that comes up every time you click into the editing zones in IE – “One or more URLs were not valid and have been reformatted. Review the links before leaving this page.” as seen below.

It was even more strange considering there was no content in the editing zone. My first cynical instinct was to blame the new foreign features that had been introduced to the site and ironically enough that was precisely the case. Turns out that if I removed the offending JavaScript calls to Cufon and behavior attributes in the CSS file, the message wouldn’t appear.

So is the choice a visually impressive public facing website or an interrupted editing experience? Not exactly. The trick here is to make use of SharePoint’s EditModePanel control.

<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Edit">
</PublishingWebControls:EditModePanel>

By enclosing the offending JavaScript calls in the EditModePanel and seperating the behavior attributes into a seperate CSS file and referencing that in the control, you can manage to get the best of both worlds.

One last thing that needs to be considered regarding SharePoint and these features is that if you’re using

<meta http-equiv="X-UA-Compatible" content="IE=edge">

as pointed out earlier and want to edit content in IE8 standards mode, it’s quite possible you might run into problems – we did anyway. You can read a little bit about it via Randy Drisgill’s post Problems with IE8 Standards Mode, SharePoint Menus, and DocTypes. The solution here made use of the EditModePanel as well and went something along the lines of:

<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Display">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
</PublishingWebControls:EditModePanel>
<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Edit">
    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
</PublishingWebControls:EditModePanel>

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.

Follow

Get every new post delivered to your Inbox.