Monday, May 18, 2009

Effective SharePoint UI prototyping using jQuery, FireBug and Greasemonkey

Demo script download available at the end of the post.
Updated May 19th, with clearer code examples.

Background

Customizing SharePoint's user interface, or even updating your own interfaces, can be a tiresome thing to do. Usually you'll have to deal with multiple master pages, many master page overrides, multiple css files and a wide array of dynamic code from XSLTs or web parts. All this makes it quite difficult to maintain a clear understanding and overview of the UIs you're customizing, or what the impact of your changes will be.

To add to the already high stress levels, many changes - especially those where you're deploying changes to your own web parts - will require recompiles, iis resets, feature reactivation or similar time consuming steps.

In this article I want to propose an alternative approach to UI changes, using jQuery prototyping. While jQuery isn't necessarily something you'd want to use to do all SharePoint UI restructuring, it does come in handy if you want to make simple runtime enhancements to the current UI, or even want to extend your custom parts with simple asynchronous behavior, or neat visual effects.

To accomplish this, in a way which will require no recompilation, redelpoyment or other nasty things, and overall keep the time between change and prototype visualization as low as possible; we'll be using the FireFox addons FireBug and Greasemonkey. The former is a brilliant DOM / CSS / Html / Network explorer, built into FireFox' interface, with lightning quick structure inspection and navigation. Greasemonkey, on the other hand, can inject custom javascript into any loaded page, without making any serverside changes.

The combination of these tools will allow us to write advanced javascripts in our favorite javascript editor, and see the results immediately, without having to upload any files to the server. A simple browser refresh will suffice.

Requirements
  • FireFox (3.0.10 is the latest at the time of writing)
  • FireBug
  • Greasemonkey
  • Knowledge of JavaScript and HTML / DOM.
Installing
  1. Download and install FireFox, if you haven't done this already.
  2. Start FireFox, and open the Addons config screen, available from the Tool menu.
  3. Activate "Get Addons" function on the top bar, then search for and install FireBug and Greasemonkey.
  4. Restart FireFox.
  5. Navigate to a SharePoint portal you wish to prototype, and confirm that you've got a set of FireBug / Greasemonkey icons in your browser status bar, such as:
  6. Right click the Greasemonkey icon, make sure it's set to enabled, and click New User script:

  7. Fill in values for the namespace and script title, such as:

    The "Includes" box should be the url to the page you wish to prototype. This can be changed later, so stick with the default value for now. In other words, don't copy this from my example above.
  8. Next you'll be asked to select which editor to use for script editing. This can be any text editor of your choice, such as notepad, or in my case, Visual Studio.
  9. Once the editor selection is completed, it will be opened, you'll see an empty script. To test that you've got it all setup correctly, you can try adding an alert statement, and reload the page.
  10. Next click the FireBug icon, and activate the HTML function:
  11. If you're unfamiliar with FireBug, play around with the Inspect function, to see how various UI elements correspond to the HTML source code. Selecting an element also enables you to immediately see the active CSS styles for said element.

Prototyping something useful

Assignment: Making the Quick Launch on the left collapsible.

"Useful" is relative, I agree, but this is at least example use of how to hotplug jQuery into the mix, and how you can use that to experiment with UI changes you'd later wish to deploy to a master page, or portal page.

From here on we'll concentrate mainly on the script opened in your chosen editor. As of now, all the content should be a commented "UserScript" section - which is good. If you've entered anything else, feel free to remove that now.

So, without further stalling, lets code.

Loading scripts (such as jQuery) at runtime

Greasemonkey lets us inject custom scripts, but rather than messing any more around with it's configuration, we'll do the rest of the script loading with code. Since this is a prototype, and we'll be doing all of the prototyping in FireFox; loading scripts is simple.

The only complicating step, is the fact that Greasemonkey will execute Greasemonkey scripts in a sandbox, isolating certain functionality from the scripts already loaded in the browser. The reasoning here is that Greasemonkey, and its scripts, will execute with greater privileges than those loaded by the browser. To prevent browser-loaded scripts from getting unwanted access from your custom Greasemonkey scripts, and possible do naughty stuff on your system, Greasemonkey forces us to take certain actions to access the stuff outside the sandbox. If you wish to read more about that, search for "unsafeWindow" in your favorite search engine.

I've put together a set of functions, which will load custom scripts in order, and map the objects from these into the namespace available from the Greasemonkey sandbox. The functions looks as follows, and you can go ahead and input these in your prototype script. Reading and understanding them are optional steps -- we'll soon be getting to the interesting stuff.

  1: function mapUnsafeObjects(objectArray) {
  2:     if (objectArray == null) {
  3:         return;
  4:     }
  5: 
  6:     for (var i in objectArray) {
  7:         var objectName = objectArray[i];
  8:         window[objectName] = unsafeWindow[objectName];
  9:     }
 10: }
 11: 
 12: function loadScript(url, objectArray, loadCallback) {
 13:     var js = document.createElement('script');
 14:     js.src = url;
 15:     js.type = 'text/javascript';
 16:     js.wrappedJSObject.onload = function() {
 17:         mapUnsafeObjects(objectArray);
 18:         loadCallback();
 19:     };
 20:     document.getElementsByTagName('head')[0].appendChild(js);
 21: }
 22: 
 23: function loadScripts(scriptArray, completeCallback) {
 24:     if (scriptArray.length == 0) {
 25:         completeCallback();
 26:     }
 27:     var scriptEntry = scriptArray.shift();
 28:     loadScript(scriptEntry.url, scriptEntry.objects, function() { loadScripts(scriptArray, completeCallback); });
 29: }


So these are the only utility functions you need to load external scripts. You can reuse them in any prototyping scripts you feed into Greasemonkey, to load whatever external content you need.

Here's an example on how to load the jQuery, jQuery UI and JSON2. We won't be needing more than the first in the following prototype, but if you wish to play around with jQuery UI (which has some cool drag and drop features, among other things), or the ajax + JSON functionality in jQuery, the others will be handy.

  1: loadScripts([
  2:              {url: "http://jquery.com/src/jquery-latest.js", objects: ["jQuery""$"]},
  3:              {url: "http://jquery-ui.googlecode.com/svn/tags/latest/ui/minified/jquery-ui.min.js", objects: []},
  4:              {url: "http://www.json.org/json2.js", objects: ["JSON"]}
  5:             ], onLoadComplete);


What this will do, is load one script at a time, making sure it's completely loaded before skipping to the next in line. For each script loaded, an array of objects is mapped into the sandbox namespace, for easy access in the prototype script. Once all scripts are loaded, the callback function named at the end will be called.

First jQuery action

Thus, the onLoadComplete function will be where our action will take place. Once this is called, jQuery will be fully loaded, and available for use. To demonstrate this, create a callback such as follows:

  1: function onLoadComplete() 
  2: {
  3:     $("a").css({ fontSize: '18pt' });
  4: }


Once you save this, along with the previous code snippets, in the Greasemonkey script opened in your chosen browser, and reload the portal, all html anchor elements will get a ridiculous font size. Not too useful, but it at least confirms everything is working properly. If the jQuery syntax is unfamiliar to you, now would be a good time to read up on some basic examples at jQuery.com.

Collapsible Quick Launch

This is when FireBug will come in handy. Writing the code is simple, and Greasemonkey is great, but without a proper tool to navigate the html, we'd be banging heads into walls before long.

With the FireBug window opened, and pointed to "HTML" mode, hit Inspect and select the section just below a main quick launch link, as shown in this screen shot:



Doing this tells us that the quick launch main menu items have IDs such as "zz2_QuickLaunchMenunN", where N is the menu item index. Tapping into this with jQuery is pretty simple, with a selector such as $("tr[id^=zz2_QuickLaunchMenun]"). What we'd like to do with this, is to attach a click handler to collapse / expand the table row below it, if the following table row isn't another main menu item.

Adding such a handler, in a pretty straight-forward manner, would look like:

  1: function onLoadComplete() {
  2:     $(function() {
  3:         $("tr[id^=zz2_QuickLaunchMenun]")
  4:             .next("[id=]").toggle()
  5:             .prev().click(function(evt) {
  6:                 $(this).next().toggle();
  7:                 evt.preventDefault();
  8:             });
  9:     });
 10: }


This function will install a handler to be run once the DOM tree is loaded and ready for manipulation: the "$(function(){})" syntax. This handler will, noted by line number:
3
Select all tr nodes with an id beginning with "zz2_QuickLaunchMenun". Hence forth called "main items".
4
For each of these main items, select the next sibling, if it has an empty id attribute. The empty id is an important point, as we don't want one main item to collapse the following main item, in the case of there being no child menu between the two. If there's no child menu, lines 5-8 will do nothing more for this main item. In this case, the selector from line 3 will bring us to the next main item instead.
4
Toggle (collapse) the child menu.
5
Select the sibling's previous item, which brings the selection state back to the main item
5
Install a click handler to the main item.
6
Use the toggle() function from jQuery to collapse or expand the sibling (child menu).
7
Simply prevents the click handler from bubbling further on to other nodes down the tree.
If jQuery is still new to you, you may just have noticed that jQuery selectors are cascading. Selecting one node, then doing another selector expression on that, will make a relative selection. That's how we, in the previous example, can move from the parent to the child, then back to the parent.

Saving and reloading the page, should confirm that the above code is working. When you click the background of the main quick launch items, the section will collapse or expand.

To wrap things up, we're going to add another piece of UI code, to animate the font size of the quick launch menu items, as they are mouse hovered. Expand / replace the function to resemble the following:

  1: function onLoadComplete() {
  2:     $(function() {
  3:         $("tr[id^=zz2_QuickLaunchMenun]")
  4:             .mouseenter(function(evt) {
  5:                 $(this).find("a").animate({ fontSize: '14px' });
  6:             })
  7:             .mouseleave(function(evt) {
  8:                 $(this).find("a").animate({ fontSize: '11px' });
  9:             })
 10:             .next("[id=]").toggle()
 11:             .prev().click(function(evt) {
 12:                 $(this).next().toggle();
 13:                 evt.preventDefault();
 14:             });
 15:     });
 16: }


Saving the script and reloading the page will actually uncover a bug in our previous selector, which wrongfully assumed that only the main items of the quick launch menu have the previously mentioned IDs. As it happens, sub items also employ this ID. While the click handler won't be affected (due to the sibling check), the mouseenter and mouseleave hover effect will be. Hovering the menu will now animate everything, while we want only the main items to be affected.

Pulling FireBug back up real quick will show that the items we actually want to attach to, have a parent table with an ID of "zz2_QuickLaunchMenu". To fix the issue we can thus take advantage of a more specific jQuery selector syntax: $("#zz2_QuickLaunchMenu > tbody > tr[id^=zz2_QuickLaunchMenun]").

This tells jQuery to look for a tr node with an id which starts with "zz2_QuickLaunchMenun", that has a direct parent element of type tbody, which in turn has a direct parent element with id "zz2_QuickLaunchMenu". Saving and reloading the page will confirm that the hover effect now only affects the main menu items.

Where to go from here

What I've demonstrated is a way to install and setup a hotpluggable SharePoint UI development environment. jQuery, and various other scripts, have vast possibilities for expanding SharePoints UI, and essential to exploring these, is a way to doing so quickly, without redeploying a bunch of stuff to the development environment (possibly polluting that, with nonsensical experiments).

Once you're done with the prototyping, you would obviously have to deploy jQuery, or whatever scripts you use, to the portal. Either in shape of a master page change, a new aspx page, web parts page template or similar. How you should deploy it depends largely on what you're installing, and what you want it to override.

There's no doubt in my mind that SharePoint development can benefit from such hotplug prototyping, however. Both the previously blogged about CodeConsole web part, and this simple prototyping environment, can - if used with care - simplify your life as a SharePoint developer or designer. Any opinions on how it could be done differently, are much obliged of course!

Downloads



MyProtoType.js

The final example code, with a decent looking expander icon next to the collapsible rows.

4 comments:

rocky said...

I simply want to style (change the background to transparent) the subnav items on my QuickLaunch menu. I have changed ms-navitem like this:
table.ms-navitem td,span.ms-navitem{
background:transparent;
}
but it doesn't work, the backgroundcolor of the subnav items changed to White.
Do I need to change "zz2_QuickLaunchMenu"?

Einar Otto Stangvik said...

Hi Rocky

Sorry for taking so long to reply - I just got back from vactation :-)

I haven't attempted to do exactly what you're describing, but it shouldn't be too time consuming to figure out, if you're using FireBug. Bring up the html inspector view, and select one of the quick launch items - from there you can navigate all the active CSS styles for the specific cell. You can even modify the styles directly through Firebug, and thus test drive changes without reloading the page.

Hope this helps.

Jeroen said...

Not sure why I have never come across this blog, it has some excellent content.

With regards to injecting code at run-time, this technique you describe works realy well in combinatin with our (Free) SharePoint Infuser, which allows you to easily inject content (JavaScript, JQuery etc) on every page.

For details see http://www.muhimbi.com/blog/2009/07/massage-sharepoint-into-submission.html

willhlaw said...

Excellent post. Great ideas and well articulated. I have not used Greasemonkey before, so I'll have to take a deeper look. I also like what you've done with the loadScripts. I've seen Paul Grenier (Autosponge) do something similiar I believe with using a JSON object to determine which dependencies to load. I've done something similiar with the jPointLoader.js, which is part of the jPoint project.

I also wanted to see if you have had time to look at jPoint's script deployment methodology. I think that scripts can be wrapped into .dwp files and added the Web Part Gallery and deployed by end users just like any other web part. jPoint refers to scripts in content editor web parts as "jParts". When the page is in design mode, the jPart turns into a config mode (yellow background) where the user can change the parameters used to execute the script. Look at around the 3 minute mark at this screencast http://www.youtube.com/watch?v=HwCMzibnrLU. Also, see my latest blog entry to see my deployment model - http://willhlaw.wordpress.com/2009/12/02/a-slide-from-my-spsdc-jpoint-presentation/. I'll be interested to hear your opinion on the pros and cons for this model.

@Willhlaw