Friday, August 15, 2014

SharePoint 2010: Execute custom code when scroll event is triggered

SharePoint 2010 does a fantastic job of handling scrolling. Even so, there are some instances where modifying this behaviour can be beneficial. This blog explores the problem of executing custom code when a scroll event is triggered and uses a function wrapper as a solution. I’ve written an article detailing how SharePoint 2010’s default scrolling behaviour works; I will be referring to ideas and components explained in that article in this blog entry. You can visit that article by clicking this link. It’s not necessary by any means to read it, but it might help. Without further adieu, let’s dive into the world of SharePoint scrolling behaviour.

Jump to Section



1. The Problem

We know that the FixRibbonAndWorkspaceDimensions function is called whenever an element or window is resized. That’s good to know because if we can add our code to the FixRibbonAndWorkspaceDimensions function, then SharePoint will call our code when a resize occurs. That sounds perfect! The problem is we don’t want to modify SharePoint’s JavaScript files in the hive. We want to keep the stock SharePoint files as close to their original release as possible. Why shouldn’t we modify these files? The SharePoint developers aren’t expecting our code in their files, so an update will undoubtedly replace old files with new ones. That would completely erase all our hard work! We can get around this by appending our own JavaScript functions onto the existing functions.

2. The Solution

JavaScript has this nifty ability to reference functions by storing them in objects. When SharePoint sets up its events it is using a reference to the function FixRibbonAndWorkSpaceDimensions as an event handler. This means that SharePoint will execute whatever function reference is in the FixRibbonAndWorkSpaceDimension object when an event fires. So, we can make FixRibbonAndWorkSpaceDimensions reference our custom code instead, right? Yes, we can do that. But you need to consider scenarios where a crucial fix was applied in FixRibbonAndWorkSpaceDimensions. If we replaced the original function with our own, then the crucial fix will never be executed. A better idea is to run SharePoint’s code followed by our own. We can do this by writing a wrapper function that executes both functions one after another. Then we have FixRibbonAndWorkSpaceDimensions reference the wrapper function. The result of this is our wrapper function is called whenever SharePoint fires an event that uses the FixRibbonAndWorkSpaceDimensions object.

Figure 1 provides code that you can use to implement this solution. The first thing the code does is store a reference to the original FixRibbonAndWorkSpaceDimensions function, so that we can call it later in our wrapper function. The next thing we do is set the FixRibbonAndWorkSpaceDimensions object to reference our wrapper function. Our wrapper function first calls the original FixRibbonAndWorkSpaceDimensionsFunction and then calls our custom code. This may seem odd or confusing, but all that’s really happening is we are changing which object points to which function (code). I’ve provided a link at the end of this article with further explanation of how function pointers work to help clear up any confusion.

FixRibbonAndWorkSpaceDimensionsFunction = FixRibbonAndWorkSpaceDimensions;
FixRibbonAndWorkSpaceDimensions = Wrapper;

function Wrapper() {
    FixRibbonAndWorkSpaceDimensionsFunction();
    CustomCode();
}

function CustomCode() {
    // write your own code to do whatever you want
    console.log(‘Our custom code is running!’);
}

Figure 1



3. The End

You can add whatever custom code you want and have it run after the FixRibbonAndWorkSpaceDimensions function without worrying about compatibility issues. The idea of a wrapper function that executes two functions can be applied to any JavaScript problem of this nature. I’m sure you can think of at least a couple more scenarios where you would like to override another default SharePoint behaviour. Good luck in your future SharePoint branding efforts.

4. Additional Resources

Thursday, August 7, 2014

SharePoint 2010: Understanding SharePoint scrolling behaviour

It’s useful to understand how the default scrolling behaviour of a SharePoint 2010 website (site) works. Instances where you may want to change this behaviour become trivial if you already understand the mechanisms that make it work. SharePoint uses JavaScript to set inline style dimensional attributes on the workspace and ribbon bar. SharePoint uses CSS to set scrolling options on the workspace based on the dimensions applied by the JavaScript. We will go over the elements, CSS and JavaScript that SharePoint has setup to provide a seamless scrolling experience.

Jump to Section



1. The HTML

1.1 s4-workspace div (s4-workspace)

S4-workspace is where all the page content for your SharePoint site is rendered; this includes items such as your site logo, web parts, s4-bodycontainer, s4-titlerow, navigation items, etc... Figure 1 shows the location of and all the content contained in the s4-workspace div.

1.2 s4-ribbonrow div (s4-ribbonrow)

s4-ribbonrow is located at the top of your screen. It contains the Site Actions menu, the breadcrumb pop-up, ribbon tabs (e.g. browse and page) and user menu. Figure 1 shows the location of and all the content contained in the s4-ribbonrow.

Figure 1


2. The CSS

SharePoint sets the CSS used for scrolling in corev4.css. Figure 2 displays the CSS for the s4-workspace; it has the properties overflow-y: scroll and overflow-x: auto. These overflow properties signify that if the current content rendered into the s4-workspace doesn’t fit within the height (overflow-y) or width (overflow-x) of the div, then a scrollbar will be provided. For example, s4-workspace has a height of 200 pixels and we render content with a height of 400 pixels. In this situation, a vertical scroll bar will appear in the s4-workspace that allows us to view the 200 pixels of content that are outside our current viewing area (viewport). Figure 3 shows an example of content being rendered that has a greater height and width than s4-workspace.
Body #s4-workspace {
 Overflow-y: scroll;
 Overflow-x: auto;
 Position: relative;
 Left: 0px;
}

Figure 2


Figure 3


3. The JavaScript

All the JavaScript that we need to look at is located in init.js. Init.js gets called when a SharePoint site is loaded. This script sets up all the event handlers required for scrolling to function properly. The default scrolling behaviour uses events that get fired when the window or s4-ribbonrow containers resize. The purpose of capturing these events is to resize s4-workspace and s4-ribbonrow, so that they fit within the viewport.

3.1 FixRibbonAndWorkspaceDimensionsForResize

Figure 4 displays the code for this function that is called whenever the browser window resizes. The purpose of it is to set the global variables g_viewportHeight and g_viewportWidth to the height and width of the viewport. The GetViewportHeight and GetViewportWidth functions are shown on lines 18 and 26 in Figure 4 and work exactly as you would expect. After setting up the global variables we call the FixRibbonandWorkspaceDimensions function to perform the resizing.
function FixRibbonAndWorkspaceDimensionsForResize()
{  ULSxSy:;
 if(g_frl)
  return;
 var vph=GetViewportHeight();
 var vpw=GetViewportWidth();
 if(g_viewportHeight==vph
  && g_viewportWidth==vpw)
 {
  return;
 }
 g_viewportHeight=vph;
 g_viewportWidth=vpw;
 window.setTimeout(FixRibbonAndWorkspaceDimensions, 0);
}

function GetViewportHeight()
{  ULSxSy:;
 if(typeof(window.innerHeight) !='undefined')
  viewportHeight=window.innerHeight;
 else
  viewportHeight=document.documentElement.clientHeight;
 return viewportHeight;
}
function GetViewportWidth()
{  ULSxSy:;
 if(typeof(window.innerWidth) !='undefined')
  viewportWidth=window.innerWidth;
 else
  viewportWidth=document.documentElement.clientWidth;
}

Figure 4


3.2 FixRibbonAndWorkspaceDimensions

This function adds inline styles for the width and height dimensions of s4-ribbonrow and s4-workspace. The FixRibbonAndWorkspaceDimensions function gets called whenever the s4-ribbonrow or window resizes.

3.2.1 s4-ribbonrow height calculation

The height of the s4-ribbonrow is based on whether the ribbon is minimized or maximized. A maximized ribbon bar means that that the browse ribbon tab is not selected. A minimized ribbon bar means that we currently have the browse ribbon tab selected. Line 1 in Figure 5 show the logic for determining the ribbon bar height based on its minimized / maximized status and line 2 calculates factors in the amount of padding applied to the s4-ribbonrow element; the actual height is set as an inline style on line 7 by combining the results of the two calculations.
var baseRibbonHeight=RibbonIsMinimized() ? 44 : 135;  // if the ribbon bar is minimized, then the height is 44.  If the ribbon bar is maximized, then the height is 135.
var ribbonHeight=baseRibbonHeight+g_wpadderHeight;  // g_wpadderHeight is set to 0 by default
if (GetCurrentEltStyle(elmRibbon, "visibility")=="hidden")
{
    ribbonHeight=0;
}
    elmRibbon.style.height=ribbonHeight+"px";  // set the ribbon bar height as an inline style element

Figure 5


3.2.2 s4-workspace height calculation

The height of s4-workspace is calculated based on the viewport height, s4-ribbonrow height and the amount of pixels applied to the margin-top css property of the s4-ribbonrow. The viewport height is obtained from the variable g_viewportHeight that was set by FixRibbonAndWorkspaceDimensionsForResize. Line 7 performs the logic to calculate the height of s4-workspace. The calculation takes the size of the viewport and subtracts the full height of the ribbon bar (includes any margins and padding). For example, s4-ribbonrow has a height of 300 pixels and margin-top property of 10 pixels. The viewport height is 800 pixels. The calculation would be 800 – 300 – 10 = 490 pixels. This means that the maximum height of the s4-workspace element can be 490 pixels. The actual height of s4-workspace is set as an inline style on line 10.
var vph=g_viewportHeight;
if (null===vph)
{
 vph=GetViewportHeight();
 g_viewportHeight=vph;
}
var newWorkspaceHeight=vph - elmRibbon.offsetHeight - AbsTop(elmRibbon);
if(newWorkspaceHeight < 0)
 newWorkspaceHeight=0;
elmWorkspace.style.height=newWorkspaceHeight+"px";

Figure 6


3.2.3 s4-workspace width calculation

SharePoint offers the option of setting the width as an inline style on s4-workspace. If s4-workspace has the class “s4-nosetwidth”, then the width will not be set as an inline style. The code that performs this check is on line 4 in Figure 7. Line 10 to 20 in Figure 7 sets the width of the s4-workspace, s4-bodycontainer and s4-titlerow elements. The width of s4-workspace is set to the viewport width on line 12.
if(!g_setWidthInited)
{
    var setWidth=true;
    if(elmWorkspace.className.indexOf("s4-nosetwidth") > -1)
        setWidth=false;
    g_setWidth=setWidth;
    g_setWidthInited=true;
}

if(setWidth)
{
    elmWorkspace.style.width=document.documentElement.clientWidth+"px";
    if(elmBodyTable.offsetWidth < elmWorkspace.clientWidth)
        elmBodyTable.style.width=elmWorkspace.clientWidth+"px";
    if(elmTitleArea)
    {
 elmTitleArea.style.width=Math.max(elmBodyTable.offsetWidth - 1, 0)+"px";
 elmTitleArea.className+=" ms-titlerowborder";
    }
}

Figure 7


3.2.4 s4-workspace scrollbar specifics

When the browser loads a page we want the slider in the scrollbar to start at the top of the element; this means that we need to give it a scroll value of zero. The code in Figure 8 sets the slider to reside at the top of the scrollbar and it modifies the CSS property overflow-y based on whether or not the current window is a dialog. I’ve added comments to the code in Figure 8 to help explain what is happening and when.
var isIE7=browseris.ie && browseris.iever==7 && !browseris.ie8standard;
if (!g_setScrollPos) // only executes once
{
    if (browseris.firefox && browseris.firefox36up) // if firefox, then scroll to the top of the page
        window.scroll(0,0); // set slider to the top of the scrollbar
    if (window.location.search.match("[?&]IsDlg=1"))
// if this is a pop-up window and we aren’t using IE7, then the overflow-y property is set to auto
// if this is a pop-up window and the scrollbar is bigger than the s4-workspace div, then set the overflow-y property auto
    {
        if (!isIE7  || elmWorkspace.scrollHeight < elmWorkspace.clientHeight)
            elmWorkspace.style.overflowY="auto";
    }
    var scrollElem=document.getElementById("_maintainWorkspaceScrollPosition");
    if (scrollElem !=null && scrollElem.value !=null)
    {
        elmWorkspace.scrollTop=scrollElem.value; // set slider to the top of the scrollbar
    }
g_setScrollPos=true;
}

Figure 8


3.2.5 Executing Additional event handlers

The last thing the FixWorkspaceAndRibbonDimensions function does is call all of the event handlers that are associated with resizing the s4-workspace. Figure 9 shows the code in the FixRibbonAndWorkspaceDimensions function that executes all of the event handlers in the g_workspaceResizedHandlers array. You can add your own function to the stack of event handlers by calling SP.UI.Workspace.add_resized(yourEventHandler) shown in Figure 10.
var handlers=[].concat(g_workspaceResizedHandlers);
for (var i=0, wLen=handlers.length; i < wLen; i++)
{
    handlers[i]();
}

Figure 9


SP.UI.Workspace.add_resized=function (handler)
{   ULSxSy:;
    g_workspaceResizedHandlers.push(handler);
};

Figure 10


4. The End

That was a lot of information to process, so let’s do a quick re-cap.
  • Whenever the browser window or an element on the page resizes the FixWorkspaceAndRibbonDimensionsForResize or FixWorkspaceAndRibbonDimensions functions are called.
  • The FixWorkspaceAndRibbonDimensionsForResize or FixWorkspaceAndRibbonDimensions functions work together to set the height and width dimensions as inline styles of the s4-workspace and s4-ribbonrow elements.
  • The CSS properties overflow-y is set as scroll and overflow-x is set as auto on s4-workspace; these properties will create a scrollbar on an element when the content exceeds the width and/or height of the containing element.
  • You can add your own function to the g_workspaceResizedHandler stack to be executed at the end of the FixWorkspaceAndRibbonDimensions function.
  • The slider on the scrollbar is programmatically set to the top of the scrollbar.
  • The s4-ribbonrow height is 44 when minimized and 135 when maximized.
  • The s4-workspace height is based on the height of s4-ribbonrow and the viewport.
  • In most situations, if our current window is a dialog, then the overflow-y property of s4-workspace will be set to auto.

You should now have a better understanding of how SharePoint creates a seamless scrolling experience for its users. The next step is to take this information and manipulate the scrolling behaviour to suit your own needs. Hopefully this information will help you in your future SharePoint 2010 branding projects.

Friday, July 25, 2014

SharePoint 2010: Fix jQuery error: “undefined is not a function” in Safari

SharePoint 2010 can be a funny animal sometimes. If we throw Safari, web parts and jQuery into the mix, then we are in for a real barn burner. This blog entry discusses a problem I ran into where jQuery wasn’t initializing properly and a solution to the problem. Specifically, jQuery version 1.11.1 wasn’t initializing when I was using Safari 5.1 and was on a page that included an Excel Web Access web part. The solution uses conditional comments in the master page to select the correct version of jQuery based on the user’s browser.

Jump to Section



1. The Problem

I was testing my SharePoint 2010 branding efforts in Safari 5.1 when I came across the bug shown in Figure 1. The error reads “TypeError: ‘undefined’ is not a function (evaluating ‘b.cloneNode(!0).click()’). This signifies that there is some code in jquery.min.js that isn’t working. The error occurs in Safari 5.1 when I import the jquery.min.js library on SharePoint 2010 pages with an Excel Web Access web part. I was using jQuery 1.11.1 because it was the newest version of jQuery that supported older versions of Internet Explorer. I later determined that jQuery 1.11.1 doesn’t play nicely with SharePoint in this specific scenario.

Figure 1



2. The Solution

I waged war with jQuery versions until I discovered that this bug doesn’t occur when using jQuery 1.8.0. I could simply use jQuery 1.8.0 for all browsers, but that didn’t sit well with me. I want to use the most up to date working version of jQuery possible for each browser. That meant using 1.11.1 for Internet Explorer and 1.8.0 for all other browsers. I accomplished this by using conditional comments to reference my jQuery scripts. Figure 2 shows the original code that I had to reference jQuery script. Figure 3 shows the code containing conditional comments that I replaced it with.

  

Figure 2



In Figure 3 the first line tests to see if our browser is Internet Explorer. If it is, then we import the jQuery library 1.11.1 from Google’s API library. The second line in Figure 3 says that if our browser is not Internet Explorer, then we import the jQuery library version 1.8.0 from Google’s API library. The result of these conditional comments is that we use a working version of jQuery based on the browser we are using.

  
    

Figure 3



3. The End

You probably won’t see jQuery breaking often unless it’s an extreme edge case like the one detailed in this blog. Filtering scripts and cascading style sheets using conditional comments helps a lot with supporting different browsers. We took that idea and applied it by ensuring that our SharePoint 2010 site supports the most up-to-date working version of jQuery possible based on the users browser.