Apple Developer Connection
Advanced Search
Member Login Log In | Not a Member? Contact ADC

Hide/Show Layer

It’s easy to forget it now, but around the time when the original Mac OS was being developed, new graphical user interfaces (GUIs) were coming out right and left. And everybody did things slightly differently. The Macintosh design team got a lot of things right, largely because they put an incredible amount of thought into what they were doing. While brainstorming ideas for the script I would write for Internet Developer, I thought it would be worthwhile to look at some of the original Macintosh Human Interface Guidelines and see how they might apply to Web interfaces. I was looking less for specific widgets to copy and more for the design principles behind this famously friendly interface.

One of the principles, modelessness, struck me as particularly pertinent to the Web. As the Mac designers wrote, a modal interface (where what you can do depends on what mode you’re in) "locks the user into one operation and doesn’t allow the user to work on anything else until that operation is completed." Using plain old HTML, everything is modal in the sense that to change anything on the page you have to load a new page. So for example, if you’re filling out a form and you need some help with it, you have to go to a new page with the help info on it and then go back to finish filling in the form.

In other words, you’re either in "help" mode or "form-filling" mode. This highlights two major limitations of the Web: statelessness (when you click on the help link, whatever you had already typed in the form gets lost) and latency (you have to wait around for the pages to load). So I decided to write a little script to help deal with these issues. The script uses dynamic HTML (DHTML) to pop open a box with info in it when you click on a link. In my demo I’ve used this to pop up contextual help about filling in a form. It could also be used, for example, to give pop-up definitions for terms in an article. In both cases, it makes sense to give the information in context, modelessly. Likewise, this solution avoids the problems of statelessness and latency.

In fact, the basic functions I’ve written could be used for any case where you want to move or change the visibility of an object on the page. I did a quick pulldown menu example just to show another way you might use the same code.

For those of you worried about people with older browsers, the script can fairly easily be made to degrade gracefully, so that those people will simply get taken to a separate page for the information. I’ve explained how to do that in the How to use the Script section below.

The Script’s Purpose

This script will create dynamic menus and popups. It includes a couple of basic cross-browser functions for moving and changing the visibility of DHTML objects. A DHTML object is an absolutely-positioned DIV in Netscape 4.x, or any HTML element in Safari, Internet Explorer 4 and 5 or Netscape 6. These functions can be used for a variety of DHTML applications; the two examples given here show how to create a popup tip and a drop-down menu. The main functions are:

  • changeObjectVisibility, which toggles a DHTML object’s visibility.
  • moveObject, which moves a DHTML object to a specific location in the browser window
  • getStyleObject, which simplifies cross-browser DTHML by getting a single reference to the style object from which we can read or set properties for the object, including its position, visibility, color, size, and so on.

Coding Challenges

Unfortunately, due to a long history of proprietary browser implementations, cross-browser and cross-platform DHTML is an often-Byzantine exercise in conditionalization. Writing these scripts for any one browser would have been a piece of cake. Making them work on Netscape 4 and later and Internet Explorer 4 and later was a bit more complicated, and making them degrade for older browsers adds yet more complexity. The problem is that, while similar, each browser has many idiosyncracies for how you find and manipulate objects on the Web page. To make life easier, I’ve written the code to handle the conditionalization.

There are a few items in these functions that will break on older browsers such as Netscape 3. It’s not too hard to make it degrade gracefully, though. You’ll just need to:

  • Include the scripts directly on the page rather than using a linked .js files
  • Use JavaScript to only write out the popup DIVs for those browsers that can manipulate them. The code to do so would look something like this:
    if(document.getElementById || document.all
      || document.layers) {
      // write out div tag with document.write
    }

The biggest challenge I found has to do with the ways the browsers handle events. The cursor position at the time an event occurs (such as a click or a mouseover) gets stored on an event object, which is handled slightly differently by the different browsers. When an event occurs, both Netscape 4 and 6 generate a new event object which you can pass to functions explicitly as an argument, while Internet Explorer uses a single global window.event object. After discarding several harebrained-in-retrospect schemes for dealing with this, I realized that if you explicitly pass the event object into a function it’ll work for both browsers:

<a href="#" onclick="showPopup('popupName',
  event);">click</a>

Note the lack of quote marks around event. That’s because it’s an object, not text. Now, in your function you can use the event object you passed in like so:

function showPopup(nameOfPopup, eventObject) {
  alert(eventObject.clientX);
}

Once you’ve passed the object into your function, you can get the cursor position by reading either the pageX and pageY properties (Netscape 4 and 6) or the clientX and clientY properties (IE 4+). Note, though, that the clientX and clientY properties fail to take into account that the page may have been scrolled, since they measure from the top-left corner of the window, rather than the document. To deal with this, we add the IE properties document.body.scrollLeft and document.body.scrollTop. In case you’re interested, there are a bunch of other useful properties of the event object, including a reference to the element which triggered it (srcElement for IE and target for Netscape).

The only trouble with passing in the event object is that it will break older browsers in which the object does not exist. To get around this, there’s a function included in the popup.js file which creates a fake event object for those browsers that don’t have a real one. This function runs when the document loads:

function createFakeEventObj() {
// create a fake event object for older browsers
//to avoid errors in function call when we 
//need to pass the event object
    if (!window.event) {
        window.event = false;
    }
}

The function sets window.event to be false so that, later on, we can test to see if we have a real event or not before we try to use it.

In Internet Explorer 5 on the Mac, the popup layer was only partially displayed when it appeared on top of text. The problem cleared up mysteriously when I moved the DIV tags so that they were the first elements in the document’s body.

Also in IE 5 on the Mac, for some reason the document.onclick event will only fire where there is actual text on the page. To work around this (so that you can click anywhere in the window to close the popup), I added an absolutely-positioned DIV to the page with nothing inside it and then usee JavaScript to size it to cover the entire window. The code looks like this:

function resizeBlankDiv() {
// resize blank placeholder div so IE 5
// on mac will get all clicks in window
if ((navigator.appVersion.indexOf('MSIE 5') != -1)
  && (navigator.platform.indexOf('Mac') != -1)
  && getStyleObject('blankDiv')) {
    getStyleObject('blankDiv').width =
      document.body.clientWidth - 20;
    getStyleObject('blankDiv').height = 
      document.body.clientHeight - 20;
  }
}

Unfortunately if the browser gets resized, the only way to get the DIV to the correct size is to reload the document. (You’d think you could just use the window.onresize event, but that happens before the window is actually resized so you end up with unnecessary scrollbars). To deal with this, there’s another function which simply reloads the page from the cache for IE 5 on the Mac whenever the window is resized.

On Internet Explorer 5 on the Mac, when you click on a link, an absolutely huge outline is added, which overlays the popup. To suppress this, I’ve added a style rule to the links:

.popupLink { outline: none }

Netscape 4 has some weird problems with DIV names. Names that begin with a number (such as "1div") and sometimes names with underscores in them (such as "my_div") don’t get converted to layers, so I generally avoid both of these cases and name my DIVs things like myDiv or div1.

Netscape 4 also has a nasty bug that causes it to lose all style rules when the window is resized. I haven’t included the fix for this because there are numerous ones out there already, such as this one on Webmonkey.

Finally, in Netscape 4 if you put javascript: within hrefs; it causes the page to reload, leaving only the function’s return value as the page’s only contents. So instead of:

<a href="javascript:myFunction();">clickme</a>

you have to use this:

<a href="#" onclick="myFunction();
  return false;">clickme</a>

In fact, this is a good way to make sure your script degrades for browsers that can’t run these functions. Notice the bit that says return false. That stops the link from loading the URL specified in the href. That way if JavaScript is turned off or the browser doesn’t process JavaScript, you can supply a different page, but if the function runs, the link won’t be followed.

In the demo, I’ve gone one step further:

<a href="#" onclick="return 
  !showPopup('nameFieldPopup', event);">
  clickme</a>

Without getting into all the details, this line says “run the function showPopup and then return the opposite of the value it returns. That way, if showPopup returns true (meaning it successfully showed the popup), then we return false to the link so it doesn’t change the page. On the other hand, if showPopup returns false (meaning it couldn’t show the popup), then we go ahead and follow the link to a separate page which could have the same information that would have been on the popup. This may seem confusing, but just remember that if you return false, the link won’t be followed.

Using the Script

To make use of the script for popups, follow these instructions:

  • To make use of popup layers, include both the layer utilities and the popup script files by putting the following in the head of your document:
     <script src="utility.js"></script>
    <script src="popup.js"></script>
  • Make sure there are DIVs to be popped up. They must be absolutely positioned and should be hidden to begin with. For example:
    <DIV onclick="event.cancelBubble = true;"
    class=popup id=nameOfPopup>
      Popup text goes here.<br> 
      <a href="#" onclick="hideCurrentPopup();
        return false;">
       You can include a link like this to 
       close the DIV if you like
      </a>
    </DIV>
    Be sure to include the onclick=’event.cancelBubble = true;’ part in the DIV. That tells JavaScript that, if you click on the DIV, it shouldn’t pass the click event to other objects in the hierarchy. If you omit this and someone clicks on the popup, it will close (for most browsers) because we’ve set an event handler which closes it. By including this line of code, we’re basically saying "close the popup whenever the person clicks anywhere but on the popup itself (or the original link to open it).
  • To change what the popup looks like, edit the .popup style rule in the stylesheet.
  • For each word that should trigger a popup, call the showPopup function, changing ’nameOfPopup’ to whatever you’ve called the popup you want to show (but keep it in the single quotes):
    <a href="http://url.for.older.browsers" 
      onclick="return !showPopup
        ('nameOfPopup', event);">
      clickme</a>
    If you wanted to make the popup appear when you roll the mouse over a link, you’d just change the event:
    <a href="http://url.for.older.browsers" 
    onmouseover="showPopup('nameOfPopup', event);"
    onmouseout="hideCurrentPopup();">clickme
    </a>
  • (Optional) Change the two variables in popup.js which control where the popup will appear relative to the cursor position:
    var xOffset = 30;
    var yOffset = -5;

Here’s a breakdown of the individual functions:

  • changeObjectVisibility(objectId, newVisibility): To call this function, objectId should be the name of the object you want to show or hide. The function expects this to be text, so you’ll need to enclose it in quotation marks. newVisibility is either visible or hidden. Again, this is a text string, so enclose it in quotes. Here’s an example which would hide the object called myBigLayer:
    changeObjectVisibility('myBigLayer', 'hidden')
  • moveObject(objectId, newXCoordinate, newYCoordinate): Again, objectId should be the name of the object you want to move. It’s text, so it should be in quote marks. newXCoordinate and newYCoordinate are numbers (so no quote marks) which describe where you want to move the object. So, to move myBigLayer to a position 300 pixels from the left of the window and 10 pixels from the top, you’d do this:
    moveObject('myBigLayer', 300, 10)
  • getStyleObject(objectId): Both of the previous functions use this function to change the name of the object into a reference to the style object that belongs to that object. This function returns the correct reference for Netscape 4+ and IE 4+, so you don’t have to worry about the different ways the different browsers do things. (Note: the one thing it won’t handle is nested layers in Netscape 4, so you’ll have to avoid putting layers inside each other).

    You could use this function outside of the context presented here any time you needed in order to change any CSS properties of an object. For example, to give myBigLayer a green background, you could do this:

    ar myBigLayerStyleObject =
      getStyleObject('myBigLayer');
    myBigLayerStyleObject.backgroundColor =
      'green';

    Or, for shorthand, you could just do this:

    getStyleObject('myBigLayer').backgroundColor
      = 'green';

View Examples:

Download Scripts