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>
- JQuery for Everyone: Accordion Left Nav
- JQuery for Everyone: Print (Any) Web Part
- JQuery for Everyone: HTML Calculated Column
- JQuery for Everyone: Dressing-up Links Pt1
- JQuery for Everyone: Dressing-up Links Pt2
- JQuery for Everyone: Dressing-up Links Pt3
- JQuery for Everyone: Cleaning Windows Pt1
- JQuery for Everyone: Cleaning Windows Pt2
- JQuery for Everyone: Fixing the Gantt View
- JQuery for Everyone: Dynamically Sizing Excel Web Parts
- JQuery for Everyone: Manually Resizing Web Parts
- JQuery for Everyone: Total Calculated Columns
- JQuery for Everyone: Total of Time Differences
- JQuery for Everyone: Fixing Configured Web Part Height
- JQuery for Everyone: Expand/Collapse All Groups
- JQuery for Everyone: Preview Pane for Multiple Lists
- JQuery for Everyone: Preview Pane for Calendar View
- JQuery for Everyone: Degrading Dynamic Script Loader
- JQuery for Everyone: Force Checkout
- JQuery for Everyone: Replacing [Today]
- JQuery for Everyone: Whether They Want It Or Not
- JQuery for Everyone: Linking the Attachment Icon
- JQuery for Everyone: Aspect-Oriented Programming with jQuery
- JQuery for Everyone: AOP in Action - loadTip Gone Wild
- JQuery for Everyone: Wiki Outbound Links
- JQuery for Everyone: Collapse Text in List View
- JQuery for Everyone: AOP in Action - Clone List Header
- JQuery for Everyone: $.grep and calcHTML Revisited
- JQuery for Everyone: Evolution of the Preview
- JQuery for Everyone: Create a Client-Side Object Model
- JQuery for Everyone: Print (Any) Web Part(s) Plugin
- JQuery for Everyone: Minimal AOP and Elegant Modularity
- JQuery for Everyone: Cookies and Plugins
- JQuery for Everyone: Live Events vs. AOP
- JQuery for Everyone: Live Preview Pane
- JQuery for Everyone: Pre-populate Form Fields
- JQuery for Everyone: Get XML List Data with OWSSVR.DLL (RPC)
- Use Firebug in IE
- JQuery for Everyone: Extending OWS API for Calculated Columns
- JQuery for Everyone: Accordion Left-nav with Cookies Speed Test
- JQuery for Everyone: Email a List of People with OWS
- JQuery for Everyone: Faster than Document.Ready
- jQuery for Everyone: Collapse or Prepopulate Form Fields
- jQuery for Everyone: Hourly Summary Web Part
- jQuery for Everyone: "Read More..." On a Blog Site
- jQuery for Everyone: Slick Speed Test
- jQuery for Everyone: The SharePoint Game Changer
- JQuery For Everyone: Live LoadTip
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.
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!
@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.
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?
@Jorge,
You can override the preview defaults (or replace them) with a selector that drops most of the header, like this:
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.
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.
Hi,
How can i use the above script with Calendar control?
Thanks
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!
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:
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??