Let start by admitting that I’m not a javascript expert, so some of this code might be sub-optimal.
And I’ve only tested it with IE (7 to 9).
Any suggestions are off course very welcome.

1. Creating and showing the menu

The menu is a DIV that contains a table, with in each row a menu-item:
DIV id=’popMenu’
       Table
          Tablebody
              Tablerow
                   Table column
                        DIV
              Tablerow
                  Table column
                        DIV
              …
This basic structure is created in the function createMenu:
function createMenu(seq) {

        var menu = document.createElement('div');
        var table = document.createElement('table');
        var tblbody = document.createElement('tbody');
        tblbody.id = 'itemListBody';
        table.id = 'itemList';
        menu.id = 'popMenu' + seq;
        menu.style.display = 'none';
        table.appendChild(tblbody);
        menu.appendChild(table);
        document.body.appendChild(menu);
        thisform = document.getElementsByTagName('form')[0];
        thisform.onclick = getCoordinates;
    }
  

Note that createMenu takes one parameter: seq. This is a number that indicates the level debt of menus.
It's basis is a global variable menuSequence.
Everytime we create a menu we will increase menuSequence, everytime we delete a menu we will delte all menu's with a sequence >= the sequence of the menu (thus deleting all child windows).


Menu items are added with the function addMenuItem
function addMenuItem(itemLabel, actionRef, behavior, seq) {
    var table = document.getElementById('popMenu' + seq).getElementsByTagName("tbody")[0];
    var rowCount = table.childNodes.length;
    var row = document.createElement("tr");
    var col = document.createElement("td");
    var listItem = document.createElement("div");
    listItem.id = "listItem" + seq + '#' + rowCount;
    listItem.name = "listItem" + seq + '#' + rowCount;
    listItem.innerHTML = itemLabel;
		
    if (listItem.addEventListener) {  // all browsers except IE before version 9
      if (behavior.toUpperCase().indexOf('CLICK') > -1) {
	listItem.addEventListener("click", actionRef);
      }
      if (behavior.toUpperCase().indexOf('HOVER') > -1) {
	listItem.addEventListener("mouseover", function() { changeClass(listItem.id, 'myHover'); }); 
	listItem.addEventListener("mouseout", function() { removeClass(listItem.id); }); 
      } else {
	  listItem.className = 'note'; 
        }
    }
    else {
     if (behavior.toUpperCase().indexOf('CLICK') > -1) {
	listItem.attachEvent("onclick", actionRef);
     }
     if (behavior.toUpperCase().indexOf('HOVER') > -1) {
	listItem.attachEvent("onmouseover", function() { changeClass(listItem.id, 'myHover'); });  // hover effect for old IE versions
	listItem.attachEvent("onmouseout", function() { removeClass(listItem.id); });  // hover effect for old IE versions
     } else {
	  listItem.className = 'note';  
	}
   }
   col.style.borderTop="thin none white";
   if (rowCount>0) { 
		   col.style.borderTop="thin solid white" ;
   }
   col.appendChild(listItem);
   row.appendChild(col);
   table.appendChild(row);
}
  

Notes:
  • we simply add a row to the menu
  • the label is added as innerHTML allowing the labels to contain HTML formatting
  • if the behaviour parameter contains the word "click" we add a reference to the actionRef parameter. In other words the actionRef parameter contains a reference to the function that has to be executed when the item is clicked.
  • if the behaviour parameter contains the word "hover" we add the hover functionality.
  • if none of the above, we assume this is a note and we give it a special style.
  • the last part
col.style.borderTop="thin none white";
		if (rowCount>0) { 
		   col.style.borderTop="thin solid white" ;} 

is to put a white separation line between menu items
We show the menu with the following function:
 function showMenu(seq) {

        document.getElementById('popMenu' + seq).style.position = 'absolute';
        document.getElementById('popMenu' + seq).style.display = 'inline';
        document.getElementById('popMenu' + seq).style.left = xMousePos + 'px';
        document.getElementById('popMenu' + seq).style.top = yMousePos + 'px';
  
    }

xMousePos and yMousePos are global variables that contain the mouse position (cfr. 3).

We have a two procedures to delete menu's:
  • delete a specific menu
this will actually delete all menu's that have a higher sequence than the menu that has to be deleted (effectively deleting all submenu's).
  • delete all menu's
function deleteMenu(seq) {
        for(i = menuSequence; i >= Math.max(seq,1); i--){
		menu = document.getElementById('popMenu' + i);
		if (menu) menu.parentElement.removeChild(menu);
	};
	menuSequence = Math.max(seq-1,0);
		
}
	
function deleteAllMenus() {
	for(i = menuSequence; i > 0; i--){
		menu = document.getElementById('popMenu' + i);
		if (menu) menu.parentElement.removeChild(menu);
	};
	menuSequence = 0;
}



2. One function to rule them all

To make things easy I’ve added one function that allows you to create menu’s with 1 to 7 menu items. This function takes three parameters for each menu-item (cfr user guide).

function menuPopper(
            label1, actionType1, action1,
            label2, actionType2, action2,
            label3, actionType3, action3,
            label4, actionType4, action4,
            label5, actionType5, action5,
            label6, actionType6, action6,
            label7, actionType7, action7 ) {
           
            menuSequence = menuSequence + 1;
            createMenu(menuSequence);
            if (label1) {addMenuItem(label1, menuActionRef(actionType1, action1, menuSequence),  menuBehavior(actionType1, action1), menuSequence);}
            if (label2) {addMenuItem(label2, menuActionRef(actionType2, action2, menuSequence),  menuBehavior(actionType2, action2), menuSequence);}
            if (label3) {addMenuItem(label3, menuActionRef(actionType3, action3, menuSequence),  menuBehavior(actionType3, action3), menuSequence);}
            if (label4) {addMenuItem(label4, menuActionRef(actionType4, action4, menuSequence),  menuBehavior(actionType4, action4), menuSequence);}
            if (label5) {addMenuItem(label5, menuActionRef(actionType5, action5, menuSequence),  menuBehavior(actionType5, action5), menuSequence);}
            if (label6) {addMenuItem(label6, menuActionRef(actionType6, action6, menuSequence),  menuBehavior(actionType6, action6), menuSequence);}
            if (label7) {addMenuItem(label7, menuActionRef(actionType7, action7, menuSequence),  menuBehavior(actionType7, action7), menuSequence);}
            showMenu(menuSequence);
    }

 

This simply creates the menu, adds the menu items en shows it.
It relies on to functions that define the behaviour of the menu item and the function that is executes when the menu item is clicked.

function menuBehavior(actionType, action) {
        switch(actionType.toUpperCase())
    {
        case 'NOTE':
            return ''
        default:
            return 'ClickHover'
    }
}

menuBehaviour is the easiest, by default it returns ‘ClickHover’ which will be translated (in the addMenuItem function) in adding click and hover functionality. When the menu item type is a note, none of those will be activated.

The second is menuActionRef which will define the functionality when the menu item is clickes. It does that by returning a reference to a javascript function, this references is add to the click-event in the addMenuitem function.
menuActionRef has it’s own two helper functions: menuTarget and menuOptions, which are self-explanatory .

function menuTarget(actionType) {
    switch(actionType.toUpperCase())
    {
        case 'URL':
        case 'REPORT':
            return '_self'
        default:
            return '_blank'
    }
}

function menuOptions(actionType, action) {
            switch(actionType.toUpperCase())
        {
            case 'POPREPORTMODAL':
            case 'POPURLMODAL':
                return 'Status=0, Menubar = 0, resizable=1, scrollbars=1, location=0'
            default:
                return 'Status=1, Menubar = 1, resizable=1, scrollbars=1, location=1'
        }
    }


function menuActionRef(actionType, action, seq) {
        switch(actionType.toUpperCase())
    {
        case 'POPURLMODAL':
        case 'URL':
        case 'POPURL':
            return function() { window.open('http://' + action.replace( "&[", "%2526["), menuTarget(actionType), menuOptions(actionType, action)); deleteAllMenus(); }
        case 'SUBNOTE':
            return function() { getCoordinatesLeaveMenus(); deleteMenu(seq+1); menuPopper( action , 'Note' , '' ); }
        case 'SUBMENU':
            return function() { getCoordinatesLeaveMenus(); deleteMenu(seq+1); eval(action) ; }
        default:
            return function() { window.open('http://BI02RS/Reportserver?' + action.replace( "&[", "%2526["), menuTarget(actionType), menuOptions(actionType, action)); deleteAllMenus(); }

    }
}

For the POPURLMODAL, URL and POPURL menu item types we just return a function that opens a new window with the target URL and then deletes all menu’s. We do a little formatting on the URL, we add “http://” and we replace “&” with “%2526[“. The last one will make sure that SSAS-members (unique names) are treated correctly (SSAS-members use the & sign in the member unique name, we have to change it so that the browser won’t mistake it for an URL parameter).

A SUBNOTE (like a SUBMENU) will first record the mouseposition (cfr. below), so that the note (or submenu) is placed on the menu item. Then we delete all other menus that have a higher sequence (f.e. you opened the menu than you opened a submenu, changed your mind, and clicked on the subnote. The submenu has to disappear before we show the note). And finally we create a new menu that has only one menu item: the note.

SUBMENU is basically like a subnote, only now the definition of the submenu is in fact the action parameter. We convert the action parameter to a javascript function by using the “eval” funtion.

Finally all the other menu item types (t.i. reports) are handled exactly like URL’s only now we add “http://BI02RS/Reportserver?” to the action (off course you’ve changed the BI02RS into the name of your own Reportserver cfr. installing)

 

3. Keeping track of the mouse position & dropping the menu when you click outside it

Because we’re acting on click’s in reporting services, the javascript event object is not available when SSRS fires the action (and the window.event object is empty).

To cover this we provide a function that records the mouse position every time the page is clicked (actually when the form is clicked).

function getCoordinatesLeaveMenus(e) {
        if (!e) var e = window.event;
        var screenX = event.screenX;
        var screenY = event.screenY;
        var clientX = e.clientX;
        var clientY = e.clientY;
        xMousePos = clientX + (document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft);
        yMousePos = clientY + (document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop);

    }
   
    function getCoordinates(e) {
        getCoordinatesLeaveMenus(e);
       
        // if the menu isvisible and you click (anywhere) all the open menues should dissapear (note that the menu-action will be called
        // via the event attachted to the menu-items)
        deleteAllMenus();
    }

It’s the second function that will be added to the click-event of the form.
We use the first one in the click-event of some of the menu item types (cfr. higher). What’s the difference? The second function will delete all menu’s, this is what you want when a user clicks outside of the menus.

To attach it to the form we use:

(function(callback) {
        if (document.readyState === "complete") {
            // if the document already has finished loading, execute immediately
            callback();
        } else {
            // if it hasn't finished yet, attach the callback to the load event
            if (document.addEventListener) {  /* Opera, Firefox, Webkit */
                window.addEventListener("load", callback, false);
            } else if (document.attachEvent) {  /* IE */
                window.attachEvent("onload", callback);
            }
        }
    }

  // immediately call that anonymous function, passing your callback
  function() {
      // record mouseclicks
      thisform = document.getElementsByTagName('form')[0];
      thisform.onclick = getCoordinates;

    
  }
  );

Don’t ask me how it works, picked it of the internet.

 

4. Layout and hover effect

Most of the layout is handled by a CSS part.

<Style>
          #itemList {
            border-radius: 2px;
            font: 13px calibri;
            background: lightgrey;
            border-color: black;
            width: auto;
            z-index: 999;
            opacity: 0.8;
            filter: alpha(opacity=80);
           
        }

   
            #itemList div {
                display: block;
                margin: 2px;
                padding: 1px;
                width: auto;
                color: black;
                font-weight: 600;
                text-align: left;
                text-decoration: none;
               
            }
           
                       
                #itemList div.myHover {
                    color: white;
                    background: black;
                    cursor: pointer;
                }
               
                #itemList div.note {
                    color: black;
                    background: lightyellow;
                    cursor: default;
                }
</Style>   

 

The simulation of the hover effect is achieved by adding the function changeClass to the mouseover event of the menu-items, and removeClass to the mouseout event.

 

function changeClass(elmid, cl) {
            document.getElementById(elmid).className = cl;
        }

function removeClass(elmid) {
            document.getElementById(elmid).className = "";
        }

The css section is included in the js file (to keep things simple).

 

5. Adding the javascript and CSS file to SSRS

Now that we have a js and css file that provide all the necessary functionality we need to add this to SSRS.

The least intrusive way is to inject both form the report (ref. http://blogs.infosupport.com/reporting-services-javascript-injection/), this works, but it implies that the user first hast to click a button in the report to activate the menu functionality (injecting the js and css files) before it will be activated.
We can’t add the injection together with showing the menu because we need to inject before we know the mouse position.

So I chose a bit more intrusive way by adding the functionality to the ReportViewer.aspx and Report.aspx pages.

Last edited Feb 19, 2015 at 7:10 PM by antoon1, version 21