Thursday, May 29, 2014

Add preview for pages with friendly urls in search results in Sharepoint 2013

Search preview is one of the nice features which was introduced in Sharepoint 2013. It gives possibility to see the target page without redirecting on that page. Preview may work for basic html pages or for documents (e.g. content of Word, Excel, Powerpoint documents can be shown in preview window). Some time ago we faced with one strange problem, which is platform bug from my point of view: for pages with friendly urls search preview doesn’t work. I.e. if for pages which have regular structural urls ( preview panel looked like this:


then for pages with friendly urls defined in managed metadata term store (e.g. it looked like this:


The key for solving this problem is in display template used for the page. Or to be more accurate, display template for hover panel used for the item. As you probably know for most items there are 2 display templates:

  1. for rendering item in search results;
  2. for hover panel for rendered item.

First “basic” display template has reference on second “supporting” display template in its code. By default Sharepoint uses standard Item_CommonItem_Body.html display template for items in search result (it may be overridden for different content types by search result types). Some time ago I wrote about how to modify this display template for fixing another Sharepoint problem, see Problem with cut titles in search results in Sharepoint 2013. In this article I will continue to work with the same display template, however you may use this approach with any other display template.

First of all let’s see from where problem with missing search preview for pages with friendly urls comes from. In order to do this we will need to check the code of Item_CommonItem_Body.html. First of all here is how javascript callback function is defined (this callback function is called when you click on the item in search result, see below):

   2: var showHoverPanelCallback = ctx.currentItem_ShowHoverPanelCallback;
   3: if (Srch.U.n(showHoverPanelCallback)) {
   4:     var itemId = id + Srch.U.Ids.item;
   5:     var hoverId = id + Srch.U.Ids.hover;
   6:     var hoverUrl = "~sitecollection/_catalogs/masterpage/" +
   7:         "Display Templates/Search/Item_Default_HoverPanel.js";
   8:     showHoverPanelCallback = Srch.U.getShowHoverPanelCallback(itemId, hoverId,
   9:         hoverUrl);
  10: }

Later in the code it is used like this:

   1: <div id="_#= $htmlEncode(id + Srch.U.Ids.body) =#_" class="ms-srch-item-body" onclick="_#= showHoverPanelCallback =#_">
   2:     ...
   3: </div>

i.e. in onclick attrbiute of the item’s div. Second place where it is used is a link with item’s title:

   1: var titleHtml = String.format('<a clicktype="{0}" id="{1}" href="{2}"' +
   2:     'class="ms-srch-item-link" title="{3}" onfocus="{4}" {5}>{6}</a>',
   3:     $htmlEncode(clickType), $htmlEncode(id + Srch.U.Ids.titleLink),
   4:     $urlHtmlEncode(url), $htmlEncode(ctx.CurrentItem.Title), 
   5:     showHoverPanelCallback, appAttribs,
   6:     Srch.U.trimTitle(title, maxTitleLengthInChars, termsToUse));

Here it is used in onfocus attribute for the link which points to the target page.

Let’s check javascript code which gets callback function again. At first it retrieves it from ctx.currentItem_ShowHoverPanelCallback property and stores it to showHoverPanelCallback variable. Then it passes showHoverPanelCallback to Srch.U.n() function which is defined in Search.ClientControls.js like this:

   1: Srch.U.n = function Srch_U$n(obj) {
   2:     return SP.ScriptUtility.isNullOrUndefined(obj);
   3: }

and if result is true (i.e. if callback in ctx.currentItem_ShowHoverPanelCallback is null or undefined), it creates callback by itself. And it uses the following display template for the hover panel: ~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_Default_HoverPanel.js.

Item_Default_HoverPanel.js is automatically generated javascript file from OTB display template Item_Default_HoverPanel.html. The problem is that this display template doesn’t support preview, i.e. it doesn’t have necessary code which loads the page into iframe and shows it. So if it is used, it technically can’t show preview. In order to show preview for pages another display template should be used: Item_WebPage_HoverPanel.html. It includes all necessary code and iframe for displaying the page like shown on the picture in the beginning of the article.

My first thought was just to replace Item_Default_HoverPanel.js on Item_WebPage_HoverPanel.js in the code above. However as it turned out code, which constructs callback function, is not called for most of the items (moreover it has a bug, because itemId variable defined as itemId = id + Srch.U.Ids.item will be incorrect. It will end with “_item_item” and will point to not-existent html element). It means that Srch.U.n() function returned false, i.e. callback was defined in ctx.currentItem_ShowHoverPanelCallback for most of the items. I added tracing with console.log() for ctx.currentItem_ShowHoverPanelCallback and found the following:

for pages with regular structural urls it contained something like this:

   2: EnsureScriptParams('SearchUI.js', 'HP.Show',
   3: 'ctl00_SPWebPartManager1_g_2dd8e1bb_d0f9_486a_8daa_0e775d6c05cc_csr2_item', 
   4: 'ctl00_SPWebPartManager1_g_2dd8e1bb_d0f9_486a_8daa_0e775d6c05cc_csr2_hover',
   5: '~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002f' +
   6:     'Search\u002fItem_WebPage_HoverPanel.js', false);

while for pages with friendly urls it shows this:

   1: EnsureScriptParams('SearchUI.js', 'HP.Show',
   2: 'ctl00_SPWebPartManager1_g_2dd8e1bb_d0f9_486a_8daa_0e775d6c05cc_csr2_item',
   3: 'ctl00_SPWebPartManager1_g_2dd8e1bb_d0f9_486a_8daa_0e775d6c05cc_csr2_hover',
   4: '~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002f' +
   5:     'Search\u002fItem_Default_HoverPanel.js', false);

The key difference is in display templates used for hover panel: in first case Item_WebPage_HoverPanel.js is used which supports previews, while in second case Item_Default_HoverPanel.js is used where previews are not supported. After that another solution came to my mind: we need to construct callback function by ourselves for all items using Item_WebPage_HoverPanel.js display template. But how to do it? As callback is defined in ctx.currentItem_ShowHoverPanelCallback (which technically may be done on server side or in earlier steps in call stack on the client side), it may be hard to intercept the code where it is created. Much simpler to do it directly in display template.

Here is how it can be done. Instead of code shown above we need to use another code:

   1: var showHoverPanelCallback = ctx.currentItem_ShowHoverPanelCallback;
   3: // change hover callback to WebPage for all items for showing preview
   4: try {
   5:     var idPrefix = id;
   6:     var suffix = "_item";
   7:     var idx = idPrefix.indexOf(suffix, idPrefix.length - suffix.length);
   8:     if (idx != -1) {
   9:         idPrefix = idPrefix.substring(0, idx);
  10:     }
  11:     var itemId = idPrefix + Srch.U.Ids.item;
  12:     var hoverId = idPrefix + Srch.U.Ids.hover;
  13:     var hoverUrl = "~sitecollection/_catalogs/masterpage/Display Templates/" +
  14:         "Search/Item_WebPage_HoverPanel.js";
  15:     showHoverPanelCallback = Srch.U.getShowHoverPanelCallback(itemId, hoverId,
  16:         hoverUrl);
  18:     // we can access top-level div only asynchronously
  19:     AddPostRenderCallback(ctx, function(){
  20:         var itemContainer = document.getElementById(itemId);
  21:         itemContainer.setAttribute("onmouseover", showHoverPanelCallback)
  22:     });
  23: } catch (ex) {
  24:     console.log("Error occured in common item display template:" + ex);
  25: }

First of all we create correct ids of the divs for item itself and hover panel (lines 5-12). As I wrote above, code in OTB display template is incorrect: id variable which is used as a base already contains “_item” prefix, so we don’t need to add “_item” and “_hover” to it directly. After that for hoverUrl we use Item_WebPage_HoverPanel.js display template which supports previews. The last interesting part is how we attach created callback to the parent div, which is used as container for the item in search results. We can’t do it synchronously in the code, because this DOM element is not ready on the moment when the code is called. So we add it asynchronously by adding custom post render callback (lines 19-21).

After that you need to reupload Item_CommonItem_Body.html display template to /_catalogs/masterpage/Display templates/Search folder and publish it with major version. If you have several site collections you need to do it on all site collections separately. And the last thing: if you don’t want to change OTB display template, but instead want to create custom based on it and change this custom template, then in order to ensure that this display template is used on your search results page, you need to assign it to the ItemBodyTemplateId property of the ResultScriptWebPart on the target page for search requests:

   1: <property name="ItemBodyTemplateId" type="string">
   2:     ~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_CustomCommonItem_Body.js
   3: </property>

After that it should use your custom display template for the search items.

That is all which I wanted to write about fixing problem with search previews for pages with friendly urls in Sharepoint 2013. In future articles I will also show how to enable search preview for list items in search results, which is also not available by default.

No comments:

Post a Comment