Wednesday, November 23, 2005

Extreme SharePoint Design: Custom User Menus, Part 2

In a previous post, I described at length the process for creating custom user menus in SharePoint. In the associated code samples, I neglected to account for windowed controls that might obscure the menu drop-downs on a page. As anyone who has designed extensively for IE knows, windowed controls (such as ActiveX or Select boxes) ignore the 'z-index' property assigned to DOM elements and take precedence in the display; that is, they are drawn on top of all other controls. If you use the hidden DIV method I outlined there are some instances, such as opening a document library in datasheet view, where the menu options disappear behind the web part.

There is no direct solution for this problem but there is a workaround using IFRAMEs and javascript. First, let's re-familiarize ourselves with the code that draws that the hidden div and the script that activates it when the user clicks the menu button:
Page Code (in DEFAULT.ASPX or a custom user control):

<td valign="top" align="right" nowrap onclick="SwitchMenu('ActionsMenu')" class="spsWPZ_ActionsMenu_Header">Actions<img src="/_layouts/images/menudark.gif" align="absbottom" > <div id="ActionsMenu" onmouseover="this.style.display='inline'" onmouseout="this.style.display='none'" class="spsWPZ_PageHeaderMenu_Div" > <table width="100%" cellpadding="0" cellspacing="0" border="0" class="spsWPZ_PageHeaderMenu"> <!-- Toolbar Elements Here --> </table> </div></td>

Javascript (in the page header or OWS.JS):

function SwitchMenu(obj){var el = document.getElementById(obj);var parent = el.parentElement;var x = getPageOffsetLeft(parent);var y = getPageOffsetTop(parent) + parent.offsetHeight;var p = parent.offsetWidth; if(el.style.display == "inline") { el.style.left = (x - (175 - p)) + "px"; el.style.top = (y - 7) + "px"; el.style.display = "none"; } else { el.style.left = (x - (175 - p)) + "px"; el.style.top = (y - 7) + "px"; el.style.display = "inline"; }}

The code above displays a table cell ('Actions') that acts as a menu button and creates a DIV element ('ActionsMenu') that is hidden when the page loads. When the user clicks within the table cell the SwitchMenu function is called, which activates the ActionsMenu DIV, positions it below the cell and displays it on the page. The onMouseOver and onMouseOut events cause the DIV to disappear when the user moves the mouse outside the DIV boundary, mimicking the behavior of a regular Windows menu.

This works just fine until a windowed control is placed on the page that intersects with the ActionsMenu DIV, at which time the control will obscure the DIV, making it impossible to access the menu functions. In some cases, the page becomes unusable because all the operations ('Modify Settings and Columns', 'Edit Page', etc.) are hidden. To resolve this issue an IFRAME must be placed immediately below the ActionsMenu DIV that is the same width and height and exposed in response to the user click on the table cell.

First, create a hidden IFRAME within the ActionsMenu DIV and place it immediately before the content table as follows:

<div id="ActionsMenu" onmouseover="this.style.display='inline'" onmouseout="this.style.display='none'" class="spsWPZ_PageHeaderMenu_Div" > <IFRAME style="display: none;" id="ActionsMenuFrame" name="ActionsMenuFrame" src="javascript:false;" frameBorder="0" scrolling="no" ></IFRAME> <table width="100%" cellpadding="0" cellspacing="0" border="0" class="spsWPZ_PageHeaderMenu"> <!-- Toolbar Elements Here --> </table></div>

The style of the IFRAME is set to "none" to keep it hidden until the DIV is exposed. The NAME and ID settings are important as these will be used in the SwitchMenu function later. The SRC property calls a dummy javascript instead of referencing a blank HTML file, which can lead to client-side security warnings in an HTTPS environment.

Next, update the SwitchMenu function with methods to display and position the IFRAME:

function SwitchMenu(obj){var el = document.getElementById(obj);var parent = el.parentElement;var x = getPageOffsetLeft(parent);var y = getPageOffsetTop(parent) + parent.offsetHeight;var p = parent.offsetWidth;var aFrame = document.getElementById('ActionsMenuFrame'); if(el.style.display == "inline") { el.style.left = (x - (175 - p)) + "px"; el.style.top = (y - 1) + "px"; el.style.display = "none"; } else { el.style.left = (x - (175 - p)) + "px"; el.style.top = (y - 1) + "px"; el.style.display = "inline"; aFrame.style.zIndex = el.style.zIndex - 1; aFrame.style.display = "inline"; aFrame.style.position = "absolute"; aFrame.style.top = "0px"; aFrame.style.left = "0px"; aFrame.style.height = el.offsetHeight; aFrame.style.width = "175px"; }}

The function now uses a variable called 'aFrame' to access the properties of the IFRAME('ActionsMenuFrame'). The IFRAME is then positioned one level below the DIV by setting the zIndex property. The display type is then changed to 'inline' and the IFRAME is set to the top and left position of the parent DIV. The most difficult part of positioning the IFRAME is matching the height of the parent object; because the ActionsMenu DIV does not have an explicit height setting (it expands or contracts based on the content in the child table), the el.style.Height value cannot be used; instead, the function uses the offsetHeight property, which is accessible once the control is drawn (which happens in the preceding IF...ELSE blocks).

The menu will now overlay all page controls, as IFRAME is a unique IE element that is aware of the zIndex values of both browser elements and windowed controls (the zIndex for the ActionsMenu DIV is set in a CSS file). The page will now render with the windowed controls on the bottom, then the IFRAME, then the DIV on top. This will prevent any ActiveX controls, select boxes, or other page elements from obscuring the menus and limiting usability.