1,575 articles and 11,600 comments as of Sunday, June 6th, 2010

Saturday, April 11, 2009

JQuery for Everyone: Live Preview Pane

I recently discussed several improvements to the Preview Pane but Live Events, using event delegation, holds the greatest benefit. It also presents the greatest challenge to someone new to scripting. But first, I will “walk the script” and point out which parts do what so you can make your customizations as needed.

Click “Read more…” for the code.

<script type="text/javascript">
if(typeof jQuery=="undefined"){
	var jQPath="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/";
	document.write("<script src='",jQPath,"jquery.min.js' type='text/javascript'><\/script>");
}
</script>
<script type="text/javascript">
/*
 * Copyright (c) 2008 Paul Grenier (endusersharepoint.com)
 * Licensed under the MIT (MIT-LICENSE.txt)
 */

To this point, the script displays my standard template for getting jQuery on the page through the CEWP. Of course, if you already have jQuery in your .master, you just need the script tag to start the next bit.

(function(){

This starts a closure. It will help keep our variables private so we don’t clutter the global namespace. That affords us flexibility to name our functions what we want.

	var defaults = {
			links: 	"a[href*='DispForm.aspx']",	//tags that trigger a preview
			type: 	"link",				//descriptive name sometimes used in cleanupCSS()
			key: 	"a", 				//find the target url by this key element's attribute
			target: "", 				//specify the target
			suffix: "",				//appended to the target url
			delay: 	800,				//delay in ms
			selector: "span#part1",			//selector used on target page
			remove: ":button, table.ms-formtoolbar:first",	//selector(s) of removed elements from target page
			source: "",				//source parameter appended to urls in preview
			html: 	"td[id='SPFieldCalculated']"	//fields needing re-rendering
		},

Those who write jQuery plugins often use this pattern to offer options with defaults. The defaults will allow the main methods to work against DispForm.aspx links however any or all of the options may be overridden during invocation (see examples at the end).

I picked the 800ms delay as the default because it seemed to be about the time it takes for the new animated .gif to “cycle.” Of course, if you have impatient users, you can reduce the timer delay. I recommend against going less than 400ms because you will see a lot more unintended previews which increases the load on the server.

		cleanupCSS = function(p,o){
			switch (o.type){
			case "discussion":
				$("td.ms-disc-bordered-noleft",p).css({"font-size":"1em",width:"100%"});
				break;
			default:
				$("table.ms-formtable",p).css({"border-bottom":"1px solid #D8D8D8",width:"100%"});
				$("h3.ms-standardheader",p).css({"font-size":"1em"});
				$("td:not(.ms-toolbar)",p).removeAttr("width");
				$("td.ms-formbody[cellIndex='1']",p).css({width:"100%"});
			}
		},

This section may need customization depending on your particular needs. Although I could have used an if-then pattern, I used the switch-case pattern instead so you can add your own cases with minimal changes to the code.

		callback = function(p,o){
			var u = (!o.source) ? window.location.pathname : o.source,
				t = o.target,
				s = o.selector,
				r = o.remove,
				x = o.suffix,
				h = o.html;
			if (r && r.length>0){$(r,p).remove();}
			if (h && h.length>0){$(h,p).calcHTML();}
			$("a[href*='Source']",p).each(function(i,e){
				var replaceSource = e.href.replace(/(\?|&|\\u0026)Source[^&|']+/,"$1Source="+u);
				e.href=replaceSource;
				e.setAttribute("onclick",replaceSource);
			});
			$("#previewLink").unbind("click").click(function(e){window.location=t+x+"&Source="+u;return false;});
			$("#previewLink img").attr("src","/_layouts/images/lg_ICONGO.gif");
		},

The callback function replaces the Source (which tells SP where to redirect the user when submit or cancel functions are used) parameter in links and JavaScript onClick events that appear in the Preview Pane after the load completes (usually in the toolbar). This was Mark’s idea, and it’s a great one for really driving a “dashboard” feel.

We also add an onClick event to the “go” icon that points to the previewed item. We can’t use a generic href because our Live Event will fire on that just like it does for our other links. We also can’t use onclick=”this.href…” because of a bug in IE7.

We also perform the operations of “remove” and “calcHTML.” As you will see, this modular design puts more choices in your hands at runtime. This means you have more control, but you also need to do more testing with selectors. With both features, the selector provided by the options will determine which, if any, fields get “re-rendered” as HTML or get removed. By default, we remove the input buttons from the previewed item as they serve no purpose in the preview pane.

		handleError = function(){
			return true;
		},
		getOptions = function(str){
			if (str.indexOf("{")===0){
				return eval('('+str+')');
			}
		},
		stringObj = function(obj){
			var str = "{";
			var len=0;
			for (var name in obj){
				if (obj.hasOwnProperty(name)){
					len++;
					str += (len>1)?',':"";
					str += name+':"'+ obj[name]+'"';
				}
			}
			str += "}";
			return(str);
		},

I call the above functions utility functions. We use them in other areas of the script but each can be useful on its own. If used elsewhere, we can move them out of the closure and make them available globally.

		fire_preview = function(p,o){
			p.load(o.target+o.suffix+" "+o.selector, function(){
				p.data("preview",o.target);
				callback(p,o);
				cleanupCSS(p,o);
				SetCookie("preview",stringObj(o).replace(/\=/g,"~"), "/");
			});
		},

This function performs the actual AJAX .load(). I kept this separate to support the cookie functionality and skip the delay timer. As you can see, this function actually calls our other private functions keeping it nice and readable.

		bind = function(p,options){
			window.onerror = handleError;  //needed for IE
			var o = $.extend({}, defaults, options);
			$(o.links).live("mouseover", function(event){
				var elm = $(event.target);
				switch (o.key){
				case "a":
					o.target = elm.attr("href");
					break;
				case "table":
					elm = elm.parents("table[ctxname]");
					o.target = "/" + elm.attr("dref").replace(/\s/g,"%20")+
								"/Forms/DispForm.aspx?ID="+elm.attr("id");
					break;
				case "img":
					elm = elm.parents("a");
					o.target = elm.attr("href");
					break;
				default:
					//fall through to pass target override
				}
				if (p.data("preview")==o.target){return;}
				$("#previewLink img").attr("src","/_layouts/images/GEARS_AN.GIF");
				if (jQuery.fn.preview.timer){clearTimeout(jQuery.fn.preview.timer);}
				jQuery.fn.preview.timer = setTimeout(function(){
					fire_preview(p,o);
				},o.delay);
				event.stopImmediatePropagation();
			});
			$(o.links).live("mouseout", function(event){
				clearTimeout(jQuery.fn.preview.timer);
				$("#previewLink img").attr("src","/_layouts/images/lg_ICONGO.gif");
				event.stopImmediatePropagation();
			});
		},

Bind is a busy function. Here, we create the Live Events that will listen for events matching our links option. Because we use Live Events, we no longer need to worry about SP’s ExpGroupRenderData function adding more DOM elements. Live Event listeners work for current and future targets.

Because the links to Picture lists, Document libraries, and Item lists all render differently, we need a switch-case to find the item ID and path attributes. If you come across a case where none of these keys work, you may need to write your own case. The cases build the target option (url to the item to be previewed). You can effectively hard-code the target if you leave key blank (or a value with no case) and populate target with a valid url.

		cookie_read = function(p){
			var g = GetCookie("preview");
			var d = p.data("preview");
			if (!d && g){
				var cookie_opts = getOptions(g.replace(/\~/g,"=").replace(/([^,|"|'])\s/g,"$1%20"));
				fire_preview(p,cookie_opts);
				return true;
			}
		};

While cookie_read appears as a utility function, it has a very specific purpose to the Preview Pane. The cookie name is hard-coded into this function.

Notice that all of the above functions are stored as variables and between each variable I used a comma to avoid typing var each time. The list of variables ends with a semi-colon.

	jQuery.fn.preview = function(options){
		bind(this,options);
		if (!jQuery.fn.preview.preload){
			jQuery.fn.preview.preload = cookie_read(this);
		}
		return this;
	};

In this section I define methods, prototyped extensions of the jQuery object. Here the .preview() method takes this, the object(s) passed by jQuery, and binds the Live Events to them using our bind function. It also checks for the existence of a preload value in the preview object. Notice that the cookie_read function returns “true” which sets the preload value and stops further attempts to load a preview based on the cookie value.

Another important aspect of this method is that we return this. Returning this allows us to chain our methods jQuery-style (see the examples below).

	jQuery.fn.calcHTML = function(){
		return $(this).each(function(i,e) {
			$(e).html($(e).text());
		});
	};

This version of calcHTML, as a method, keeps the script very simple. The difficult part is passing the right objects to it by way of a selector. However, it’s usually less complicated in a DispForm than a List Form web part (the default selector should work for all situations except where you use a plain text field for HTML).

	jQuery.fn.print = function(options){
		var html = 	"<HTML>\n<HEAD>\n<style type='text/css'>"+
					options.css+"</style>"+
					$("head").html()+"\n</HEAD>\n<BODY>\n";
		this.each(function(i,e){
			html += "<DIV style='font-family:verdana,arial,helvetica,"+
					"sans-serif;font-size:8pt;margin:20px;'>\n"+
					$(e).html()+"</DIV>\n";
		});
			html += "</BODY>\n</HTML>";
		var printWP = window.open("","printWebPart");
		printWP.document.open();
		printWP.document.write(html);
		printWP.document.close();
		printWP.print();
		return this;
	};

This familiar code was originally a function. As a jQuery-style method, it can stand alone and be used anywhere with a selector to pass one or more web parts. If you use my new object model script, you can create links to print multiple things like this: $(”#”+wp.Tasks2.id).add(”#”+wp.Calendar.id).print();

})();

Here we complete our closure which keeps our private stuff private. This will help ensure that if we use other scripts on the page, the namespaces will not collide and overwrite each other’s functions.

$(function(){
	$("#previewContent").preview()
	.preview({type:"wiki",links:"a[href*='.aspx']:has(img.ms-hidden)",key:"table",selector:"div#WebPartWPQ1"})
	.preview({type:"thumbnail",links:"a[href*='Forms/DispForm.aspx']",key:"img",selector:"div#WebPartWPQ1"})
	.preview({type:"person",links:"a[href*='userdisp.aspx']",suffix:"&Force=1"})
	.preview({type:"discussion",links:"a[href*='Flat.aspx']",selector:"table.ms-disc",remove:"[id^='ToggleQuotedText'],tr.ms-viewheadertr"})
	.preview({type:"excel document",links:"a[href*='.xls']:has(img.ms-hidden)",key:"table",selector:"div#WebPartWPQ1"});
});
</script>

This area fires on the document.ready event. Here we assign our preview area with a jQuery selector and start calling our .preview() method with, or without, various options. Because we return this from .preview(), we only have to write this selector once.

In each invocation, I used a type option to describe what that particular invocation is responsible for. Only in the case of discussions does the type option have any meaning to the code (it is used in the cleanupCSS function).

If two links options would overlap, the more general one comes first. For instance, we use our defaults first to bind our Live Events to all anchors with an href pointing to DispForm.aspx. We overwrite that default logic when we bind the thumbnails with the Forms/DispForm.aspx selector. Both selectors are valid for thumbnails, so it’s important for the correct one to come last. This does not affect other links that are not thumbnails since they do not meet the criteria of the thumbnail selector.

Once again, while this is a better development technique and has much better performance than previous versions, the use of it takes a little more practice and understanding of how it works.

<div id='previewWrapper' style='background-color:#fff;'>
	<a href='javascript:print preview pane' onClick='$("#previewContent").print({css:".ms-toolbar{display:none;}"}); return false;'>
		<img src='/_layouts/images/itdl.gif' style='border:none;padding-top:5px;' alt='printer friendly' />
	</a>
	<a href="#" id="previewLink">
		<img src="/_layouts/images/lg_ICONGO.gif" alt="" height="16" style="border:none;-ms-interpolation-mode:bicubic;" />
	</a>
	<div id='previewContent' class='content'></div>
</div>

Lastly, our block of HTML has changed a little. I now change the image in previewLink to an animated .gif when the preview timer starts. We also call the .print() method instead of a print function. Otherwise, feel free to style this any way you see fit.

All together now: (to test this script, copy and paste the following code into the Source Editor of a Content Editor Web Part).

<script type="text/javascript">
if(typeof jQuery=="undefined"){
	var jQPath="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/";
	document.write("<script src='",jQPath,"jquery.min.js' type='text/javascript'><\/script>");
}
</script>
<script type="text/javascript">
/*
 * Copyright (c) 2008 Paul Grenier (endusersharepoint.com)
 * Licensed under the MIT (MIT-LICENSE.txt)
 */
(function(){
	var defaults = {
			links: 	"a[href*='DispForm.aspx']",				//tags that trigger a preview
			type: 	"link",									//descriptive name sometimes used in cleanupCSS()
			key: 	"a", 									//find the target url by this key element's attribute
			target: "", 									//specify the target
			suffix: "",										//appended to the target url
			delay: 	800,									//delay in ms
			selector: "span#part1",							//selector used on target page
			remove: ":button, table.ms-formtoolbar:first",	//selector(s) of removed elements from target page
			source: "",										//source parameter appended to urls in preview
			html: 	"td[id='SPFieldCalculated']"			//fields needing re-rendering
		},
		cleanupCSS = function(p,o){
			switch (o.type){
			case "discussion":
				$("td.ms-disc-bordered-noleft",p).css({"font-size":"1em",width:"100%"});
				break;
			default:
				$("table.ms-formtable",p).css({"border-bottom":"1px solid #D8D8D8",width:"100%"});
				$("h3.ms-standardheader",p).css({"font-size":"1em"});
				$("td:not(.ms-toolbar)",p).removeAttr("width");
				$("td.ms-formbody[cellIndex='1']",p).css({width:"100%"});
			}
		},
		callback = function(p,o){
			var u = (!o.source) ? window.location.pathname : o.source,
				t = o.target,
				s = o.selector,
				r = o.remove,
				x = o.suffix,
				h = o.html;
			if (r && r.length>0){$(r,p).remove();}
			if (h && h.length>0){$(h,p).calcHTML();}
			$("a[href*='Source']",p).each(function(i,e){
				var replaceSource = e.href.replace(/(\?|&|\\u0026)Source[^&|']+/,"$1Source="+u);
				e.href=replaceSource;
				e.setAttribute("onclick",replaceSource);
			});
			$("#previewLink").unbind("click").click(function(e){window.location=t+x+"&Source="+u;return false;});
			$("#previewLink img").attr("src","/_layouts/images/lg_ICONGO.gif");
		},
		handleError = function(){
			return true;
		},
		getOptions = function(str){
			if (str.indexOf("{")===0){
				return eval('('+str+')');
			}
		},
		stringObj = function(obj){
			var str = "{";
			var len=0;
			for (var name in obj){
				if (obj.hasOwnProperty(name)){
					len++;
					str += (len>1)?',':"";
					str += name+':"'+ obj[name]+'"';
				}
			}
			str += "}";
			return(str);
		},
		fire_preview = function(p,o){
			p.load(o.target+o.suffix+" "+o.selector, function(){
				p.data("preview",o.target);
				callback(p,o);
				cleanupCSS(p,o);
				SetCookie("preview",stringObj(o).replace(/\=/g,"~"), "/");
			});
		},
		bind = function(p,options){
			window.onerror = handleError;
			var o = $.extend({}, defaults, options);
			$(o.links).live("mouseover", function(event){
				var elm = $(event.target);
				switch (o.key){
				case "a":
					o.target = elm.attr("href");
					break;
				case "table":
					elm = elm.parents("table[ctxname]");
					o.target = "/" + elm.attr("dref").replace(/\s/g,"%20")+
								"/Forms/DispForm.aspx?ID="+elm.attr("id");
					break;
				case "img":
					elm = elm.parents("a");
					o.target = elm.attr("href");
					break;
				default:
					//fall through to pass target override
				}
				if (p.data("preview")==o.target){return;}
				$("#previewLink img").attr("src","/_layouts/images/GEARS_AN.GIF");
				if (jQuery.fn.preview.timer){clearTimeout(jQuery.fn.preview.timer);}
				jQuery.fn.preview.timer = setTimeout(function(){
					fire_preview(p,o);
				},o.delay);
				event.stopImmediatePropagation();
			});
			$(o.links).live("mouseout", function(event){
				clearTimeout(jQuery.fn.preview.timer);
				$("#previewLink img").attr("src","/_layouts/images/lg_ICONGO.gif");
				event.stopImmediatePropagation();
			});
		},
		cookie_read = function(p){
			var g = GetCookie("preview");
			var d = p.data("preview");
			if (!d && g){
				var cookie_opts = getOptions(g.replace(/\~/g,"=").replace(/([^,|"|'])\s/g,"$1%20"));
				fire_preview(p,cookie_opts);
				return true;
			}
		};
	jQuery.fn.preview = function(options){
		bind(this,options);
		if (!jQuery.fn.preview.preload){
			jQuery.fn.preview.preload = cookie_read(this);
		}
		return this;
	};
	jQuery.fn.calcHTML = function(){
		return $(this).each(function(i,e) {
			$(e).html($(e).text());
		});
	};
	jQuery.fn.print = function(options){
		var html = 	"<HTML>\n<HEAD>\n<style type='text/css'>"+
					options.css+"</style>"+
					$("head").html()+"\n</HEAD>\n<BODY>\n";
		this.each(function(i,e){
			html += "<DIV style='font-family:verdana,arial,helvetica,"+
					"sans-serif;font-size:8pt;margin:20px;'>\n"+
					$(e).html()+"</DIV>\n";
		});
			html += "</BODY>\n</HTML>";
		var printWP = window.open("","printWebPart");
		printWP.document.open();
		printWP.document.write(html);
		printWP.document.close();
		printWP.print();
		return this;
	};
})();
$(function(){
	$("#previewContent").preview()
	.preview({type:"wiki",links:"a[href*='.aspx']:has(img.ms-hidden)",key:"table",selector:"div#WebPartWPQ1"})
	.preview({type:"thumbnail",links:"a[href*='Forms/DispForm.aspx']",key:"img",selector:"div#WebPartWPQ1"})
	.preview({type:"person",links:"a[href*='userdisp.aspx']",suffix:"&Force=1"})
	.preview({type:"discussion",links:"a[href*='Flat.aspx']",selector:"table.ms-disc",remove:"[id^='ToggleQuotedText'],tr.ms-viewheadertr"})
	.preview({type:"excel document",links:"a[href*='.xls']:has(img.ms-hidden)",key:"table",selector:"div#WebPartWPQ1"});
});
</script>

<div id='previewWrapper' style='background-color:#fff;'>
	<a href='javascript:print preview pane' onClick='$("#previewContent").print({css:".ms-toolbar{display:none;}"}); return false;'>
		<img src='/_layouts/images/itdl.gif' style='border:none;padding-top:5px;' alt='printer friendly' />
	</a>
	<a href="#" id="previewLink">
		<img src="/_layouts/images/lg_ICONGO.gif" alt="" height="16" style="border:none;-ms-interpolation-mode:bicubic;" />
	</a>
	<div id='previewContent' class='content'></div>
</div>

Paul Grenier

View all entries in this series: PaulGrenier-JQuery for Everyone»
Entries in this series:
  1. JQuery for Everyone: Accordion Left Nav
  2. JQuery for Everyone: Print (Any) Web Part
  3. JQuery for Everyone: HTML Calculated Column
  4. JQuery for Everyone: Dressing-up Links Pt1
  5. JQuery for Everyone: Dressing-up Links Pt2
  6. JQuery for Everyone: Dressing-up Links Pt3
  7. JQuery for Everyone: Cleaning Windows Pt1
  8. JQuery for Everyone: Cleaning Windows Pt2
  9. JQuery for Everyone: Fixing the Gantt View
  10. JQuery for Everyone: Dynamically Sizing Excel Web Parts
  11. JQuery for Everyone: Manually Resizing Web Parts
  12. JQuery for Everyone: Total Calculated Columns
  13. JQuery for Everyone: Total of Time Differences
  14. JQuery for Everyone: Fixing Configured Web Part Height
  15. JQuery for Everyone: Expand/Collapse All Groups
  16. JQuery for Everyone: Preview Pane for Multiple Lists
  17. JQuery for Everyone: Preview Pane for Calendar View
  18. JQuery for Everyone: Degrading Dynamic Script Loader
  19. JQuery for Everyone: Force Checkout
  20. JQuery for Everyone: Replacing [Today]
  21. JQuery for Everyone: Whether They Want It Or Not
  22. JQuery for Everyone: Linking the Attachment Icon
  23. JQuery for Everyone: Aspect-Oriented Programming with jQuery
  24. JQuery for Everyone: AOP in Action - loadTip Gone Wild
  25. JQuery for Everyone: Wiki Outbound Links
  26. JQuery for Everyone: Collapse Text in List View
  27. JQuery for Everyone: AOP in Action - Clone List Header
  28. JQuery for Everyone: $.grep and calcHTML Revisited
  29. JQuery for Everyone: Evolution of the Preview
  30. JQuery for Everyone: Create a Client-Side Object Model
  31. JQuery for Everyone: Print (Any) Web Part(s) Plugin
  32. JQuery for Everyone: Minimal AOP and Elegant Modularity
  33. JQuery for Everyone: Cookies and Plugins
  34. JQuery for Everyone: Live Events vs. AOP
  35. JQuery for Everyone: Live Preview Pane
  36. JQuery for Everyone: Pre-populate Form Fields
  37. JQuery for Everyone: Get XML List Data with OWSSVR.DLL (RPC)
  38. Use Firebug in IE
  39. JQuery for Everyone: Extending OWS API for Calculated Columns
  40. JQuery for Everyone: Accordion Left-nav with Cookies Speed Test
  41. JQuery for Everyone: Email a List of People with OWS
  42. JQuery for Everyone: Faster than Document.Ready
  43. jQuery for Everyone: Collapse or Prepopulate Form Fields
  44. jQuery for Everyone: Hourly Summary Web Part
  45. jQuery for Everyone: "Read More..." On a Blog Site
  46. jQuery for Everyone: Slick Speed Test
  47. jQuery for Everyone: The SharePoint Game Changer
  48. JQuery For Everyone: Live LoadTip
 

Please Join the Discussion

61 Responses to “JQuery for Everyone: Live Preview Pane”
  1. Chris says:

    I got it!

    I added this to my preview call
    .preview({type:”link”,links:”a[href*='DispForm.aspx']“,key:”a”,selector:”div#WebPartWPQ2″})

    thank you again for your help.

  2. Adam says:

    Hi AutoSponge this is a great script! My question is do I have to use the preview in a webpart or can it be displayed using your loadtip?

    Thanks!

  3. AutoSponge says:

    @Adam,

    The preview was designed to run in a specific area of the screen and accommodates basically whatever data the display form holds. It can display the toolbar and all of the “audit” data.

    Loadtip was designed for small bits (like Event data). It can’t reasonably hold the toolbar. Otherwise, they’re basically the same. They both pull in the HTML from the display form.

  4. Jorge says:

    Again the toolbar… So, is it possible to display only some os the item from the toolbar. I just want to display the “Alert Me” item?

  5. AutoSponge says:

    @Jorge,

    You can override the preview defaults (or replace them) with a selector that drops most of the header, like this:

    $("#previewContent").preview(remove: ":button, table.ms-formtoolbar:first, td.ms-toolbar:not(:contains('Alert Me')), td.ms-separator")
    
  6. Jason says:

    Did anyone ever figure out the double hover issue? I hover over an item and the gif fires but not results, mouseout and hover over the item again, it fires and then the list display item loads.

  7. Jason says:

    FOUND IT.

    jQuery.fn.preview = function(options){
    bind(this,options);
    if (!jQuery.fn.preview.preload){
    jQuery.fn.preview.preload = cookie_read(this);
    }
    //return this;
    return;
    };

    Changed it to just “return” and it works 1st time everytime.

  8. Bharat Sharma says:

    Hi,

    How can i use the above script with Calendar control?

    Thanks

  9. SharePoint says:

    It keeps on asking me – To display the web page again, IE need to resent the information you’ve previously submitted.

    If you were making a purchase, you should cancel to avoid a duplicate transaction. Otherwise, click retry to display the webpage again!

  10. AutoSponge says:

    There’s a problem with some lists and the “wiki” configuration. If you have issues where certain links seem to need two mouse-overs to display, comment that line out like this:

    //.preview({type:"wiki",links:"a[href*='.aspx']:has(img.ms-hidden)",key:"table",selector:"div#WebPartWPQ1"})
    
  11. Parth says:

    I have implemented ur solution on SharePoint Calendar. Works just fine.
    Currently the tool tip would display all default columns for specific item.
    What if i just want to display “Title” and “Start Date” when an item has been mouse hover??


Notify me of comments to this article:


Speak and you will be heard.

We check comments hourly.
If you want a pic to show with your comment, go get a gravatar!