
Its 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 youre in) "locks the user into one operation and doesnt 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 youre 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, youre 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 Ive 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 Ive 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. Ive explained how to do that in the How to use the Script section below.
The Scripts 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 objects 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, Ive 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. Its not too hard to make it degrade gracefully, though. Youll just need to:
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 itll work for both browsers:
<a href="#" onclick="showPopup('popupName',
event);">click</a>
Note the lack of quote marks around event. Thats because its 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 youve 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 youre 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, theres a function included in the popup.js file which creates a fake event object for those browsers that dont 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 documents 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. (Youd 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, theres 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, Ive 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") dont 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 havent 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 functions return value as the pages 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 cant 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 doesnt process JavaScript, you can supply a different page, but if the function runs, the link wont be followed.
In the demo, Ive 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 doesnt change the page. On the other hand, if showPopup returns false (meaning it couldnt 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 wont 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 shouldnt 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 weve set an event handler which closes it. By including this line of code, were 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 youve 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, youd 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;
Heres 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 youll need to enclose it in quotation marks. newVisibility is either visible or hidden. Again, this is a text string, so enclose it in quotes. Heres 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. Its 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, youd 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 dont have to worry about the different ways the different browsers do things. (Note: the one thing it wont handle is nested layers in Netscape 4, so youll 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
|