Pages

Monday, November 29, 2010

Improved Code for Hiding Buttons and MenuItems

[UPDATE 12/2/10: This code no longer uses the display style attribute, which would become undone after certain window operations.  Instead, it uses the removeChild() method.  To make up for the arbitrary destruction of the element, I’ve included instructions and mechanisms at the bottom of this post that will allow you to replace the element to its previous location.]

I’ve often directed people that have inquired about the process of concealing CRM buttons and menu-items to Dave Hawes’ blog.  His blog is the most popular resource for this feature, and I’ve personally contributed to the code for it several times (initially by refactoring the code to accept an array of element titles for concealment).

However, most of the changes I have recommended to Dave through the comments on that blog posting have not appeared in the principle post or in any subsequent post.  So, I thought that I would post the code that has since evolved into the set of functions I use today.

The first function allows you to hide buttons/menu-items anywhere in CRM where the code can run:

function HideTitledButtons(targetElement, buttonTitles) {
  var liElements = targetElement.getElementsByTagName('li');
  var removedButtons = [];

  for (var j = 0; j < buttonTitles.length; j++) {
    for (var i = 0; i < liElements.length; i++) {
      if (liElements[i].getAttribute('title').substr(0, buttonTitles[j].length) == buttonTitles[j]) {
        removedButtons.push({
          title: liElements[i].getAttribute('title'),
          element: liElements[i],
          parent: liElements[i].parentNode,
          sibling: liElements[i].nextSibling
          });
        liElements[i].parentNode.removeChild(liElements[i]);
        break;
      }
    }
  }
  
  return removedButtons;
}

To use the function, here are the parameters:

targetElement:  The HTML DOM element containing the items to hide; document is an acceptable value for buttons within the current form, however a variation of this script is used to target items in the form’s views.  (See below)

buttonTitles:  An array of titles for the buttons to hide.  Each array element is a string containing any portion (from the first character) of the “title” attribute of the labeled button/menu item.  E.g. the “New XXX” button in an associated view has the title “Add a new XXX to this record”; it’s perfectly acceptable to use the value “Add a new” as one element of this array.

This function has been updated to return an array of objects, each element of which contains the following members:

  • title:  The full title attribute of the removed button/menu item element
  • element:  The button/menu item element removed from the DOM
  • parent:  The parent DOM element to the element
  • sibling:  The nextSibling of the element

Now, for a wrapper to this function that works with views contained in Iframes, I’ve developed the following function:

function HideViewButtons(Iframe, buttonTitles, outHiddenButtons) {
  function HideViewButtonsByContext() {
    if (Iframe.readyState == 'complete') {
      var iFrame = frames[window.event.srcElement.id];
      
      var hiddenButtonsReturn = HideTitledButtons(iFrame.document, buttonTitles);
      
      if (typeof outHiddenButtons != "undefined" && outHiddenButtons != null) {
        for (hiddenButtonsReturnIndex in hiddenButtonsReturn) {
          outHiddenButtons.push(hiddenButtonsReturn[hiddenButtonsReturnIndex]);
        }
      }
    }
  }
  
  Iframe.attachEvent("onreadystatechange", HideViewButtonsByContext);
}

With both functions placed in the code, you can call the new wrapper-method with the following parameters:

Iframe:  The Iframe element containing the view.  This can be an “IFRAME” control on the form, or that of an associated view.

buttonTitles:  An array of titles for the buttons to hide.  Each array element is a string containing any portion (from the first character) of the “title” attribute of the labeled button/menu item.  E.g. the “New XXX” button in an associated view has the title “Add a new XXX to this record”; it’s perfectly acceptable to use the value “Add a new” as one element of this array.

outHiddenButtons (Optional):  If you choose to use it, you must pass an array to this parameter.  The referenced array will be loaded with elements from the return of the call to HideTitledButtons().

CAVEAT:  You must call this code before the src attribute of the Iframe is updated.  It’s designed to function by attaching to the “onreadystatechange” event of the Iframe, and executing when the readyState finally changes to “complete”.  If you must perform the hiding afterward, simply use the first function and pass the document element of the Iframe into the first parameter.

For working with naturally navigable associated views, we can use a wrapper to the previous wrapper-method:

function HideAssociatedViewButtons(loadAreaId, buttonTitles, outHiddenButtons){
  function HideViewButtonsByContext() {
    HideViewButtons(document.getElementById(loadAreaId + 'Frame'), buttonTitles, 
      typeof outHiddenButtons != "undefined" ? outHiddenButtons : null);
  }
  
  AttachActionToLoadArea(loadAreaId, HideViewButtonsByContext);
}

Where loadAreaId is the string passed to the loadArea() function by the <A> element’s onclick attribute.  This is discoverable by using the Developer Toolbar in IE8 by pressing [F12] and using the “Select element by click” feature to view the HTML of the navigation link.

outHiddenButtons is an optional parameter for this wrapper function as well, and is passed into the HideViewButtons() method as-is.

This wrapper function, however, requires this bit of utility code:

function AttachActionToLoadArea(loadAreaId, navigationHandler) {
  var navElement = document.getElementById('nav_' + loadAreaId); 

  // if at first you don't succeed...
  if (navElement == null) {
   navElement = document.getElementById('nav' + loadAreaId);
  }

  if (navElement != null)  {    
    navElement.attachEvent("onclick", navigationHandler);
  }
}

(The nifty thing about the AttachActionToLoadArea function is its ability to use the loadAreaId above to discover and attach any function as a handler to the “onclick” event of the targeted navigation link.)

As you can see, I’ve isolated functionality into separate functions that can be circumstantially used as the situation requires.  However, there is much more that could be done to unify the code in this post with the sister “Renaming Buttons and MenuItems” post to make a common, single point of interface for all Button and MenuItem modification.

Restoring Hidden Buttons and MenuItems

So, you’ve used the code above, and now you want to dynamically restore a button that was hidden.  How you go about this depends on how the element was removed in the first place:

Hidden with the HideTitledButtons() method:

// First, capture the return of the HideTitledButtons function that the element is
// saved in the variable hiddenButtons
var hiddenButtons = HideTitledButtons(document, "Some Button Title");

// To restore it, I can use a contextual reference from an array item of hiddenButtons
// to the parent of a hidden element so that I can call it's insertBefore() method.
// When calling that method, I'll pass two members from the array item: element and sibling
hiddenButtons[0].parent.insertBefore(hiddenButtons[0].element, hiddenButtons[0].sibling);

Hidden with either HideViewButtons() or HideAssociatedViewButtons():

// In order to capture the return from the underlying HideTitledButtons function,
// which executes asynchronously, we need a static recipient to pass into the
// outHiddenButtons parameter; using a singleton for this is ideal if you want
// to avoid having to pass the reference of the recipient into various script scopes.
// Whichever you decide, initialize it as an empty array:
HiddenButtons = [];

// Passing it into HideViewButtons:
HideViewButtons(crmForm.all.IFRAME_Something, ["Add a new", "Add existing"], HiddenButtons);

// Passing it into HideAssociatedViewButtons:
HideAssociatedViewButtons("areaSomething", ["Add a new", "Add existing"], HiddenButtons);

Restoring elements from HiddenButtons is identical to the process for hiddenButtons in the HideTitledButtons() example.

You can pass the same singleton/variable (in this example: HiddenButtons) into all calls of HideViewButtons() or HideAssociatedViewButtons(), and you’ll have a single array that holds every removed button/menu item element.  That may be confusing, however, when trying to restore specific elements (though you can probe the title member of each array item to determine if you have the correct one).  Ideally, you’ll want a 1:1 ratio of outHiddenButtons recipients to views.