ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Writing Advanced JavaScript

by Howard Feldman
07/12/2007

Web Applications

From the dawn of the computer era, software interoperability between different platforms has always been a concern. Wanting to reach as large an audience as possible, publishers would port a popular software program from one machine to another, which often involved months of labor and sometimes a complete rewrite on the new hardware or operating systems. As computers became more powerful and languages like C and C++ became standards available on most platforms, it became easier to just write a program once, and compile it for as many systems as desired. As long as it had a C compiler, the software could be built, and would probably work as intended.

However, when it came to standardization of software with Graphical User Interfaces (GUIs), that was another story. The GUI on Macs looked nothing like that on Windows, or the various flavors of Unix. There are, of course, so-called "widget toolkits" like Tcl/Tk or wxWidgets that have bindings to many popular languages such as C++, Python and Perl, and allow one to create platform-independent GUIs that behave the same (approximately) on Windows, Linux, Max OS X, and others. However, there is a very powerful GUI interpreter on almost every machine today: the web browser.

While web browsers were initially useful for simply displaying markup to the user in a pleasing way, they have evolved now to a point where they can serve as the interface to surprisingly complex pieces of software, rivaling those written in C/C++ and other high-level languages. This is mostly thanks to JavaScript support and Web 2.0 technology. Almost every browser on every platform supports a common subset of this scripting language, making it easier than ever to produce platform-independent applications.

JavaScript Toolkits

Recently, a number of free JavaScript toolkits have become available from places like Yahoo, Google, and Dojo. Much like the widget toolkits mentioned above, they provide JavaScript routines for various widgets such as menus, calendars, or trees, and permit you to incorporate them into your own web site or web application with ease. A nice little summary is provided here.

Toolkits are good if you need to spice up a web page, and have many advantages. They have been tested on many browsers and generally work on all common ones. Some are well documented, and they make writing fancy web pages a snap. However, they can also be bulky, adding several hundred kilobytes to the size of your page, and you may have to wait for support for new browsers to be added (for example, some do not work with IE7 yet). Also, many companies cannot or will not use open source software, due to the lack of accountability associated with it (i.e., you use it at your own risk) and difficulties with debugging.

This series of articles is intended to serve as a tutorial in advanced JavaScript techniques, demonstrating how to create multibrowser compatible widgets (with complete source) much like in these toolkits, but also explaining in detail how they work, so you can create and customize your own. Examples of how to implement things like menus, tabs or trees are abundant on the Web, however many are extremely poorly written, or not cross-browser compatible. One of the better online JavaScript, CSS, DOM, and HTML references is www.w3schools.com, and it is suggested you refer to this site if there are any HTML/CSS tags or JavaScript functions discussed in this article that you are unfamiliar with. A tutorial on HTML DOM can also be found there. Some of the items that will be discussed are pop-up menus, floating messages, drag-and-drop objects, and XML HTTP (dynamically changing page content without reloading the page), just to name a few.

It should be pointed out that the functions to be discussed have been tested on IE7 and Firefox 2.0, under Windows XP, but they should generally work on all modern browsers. Any browser-specific code will be specifically pointed out in the discussion. Also, keep in mind that there are many ways to achieve the same goal with JavaScript and DOM. The following examples provide one such way and are not necessarily the fastest or best way in all cases. The examples are written with simplicity, functionality, and ease of reading in mind.

The Magic of JavaScript

The first example will we look at is how to make floating text (or images) appear, and stay in place even when the browser window scrolls. This could be used to display a message to a user such as "Please wait" in a web application, or to make a persistent watermark on a web page. This can be accomplished with just a few lines of JavaScript:

This is accomplished with the following two JavaScript functions:


var ie = document.all;
var moz = document.getElementById && !document.all; 
var intr;

function Message_UpdatePos(msg, dy) {
    var el = document.getElementById(msg);
    if (ie) {
        el.style.pixelTop = document.body.scrollTop + dy;
    }
    else if (moz) {
        el.style.top = window.pageYOffset + dy + 'px';
    }
}

function Message_Display(msg, vis, dx, dy) {
    var el = document.getElementById(msg);

    // Position Message

    if (ie) {
        el.style.pixelTop = document.body.scrollTop + dy;
        el.style.pixelLeft = document.body.clientWidth - dx;
    }
    else if (moz) {
        el.style.top = window.pageYOffset + dy + 'px';
        el.style.left = window.innerWidth - dx + 'px';
    }
    if (vis) {  // and display it
        el.style.visibility = "visible";
        intr = setInterval("Message_UpdatePos('" + msg + "', " + dy + ")", 1);
    }
    else {  // or hide it
        el.style.visibility = "hidden";
        if (intr)
            clearInterval(intr);
    }
}

The message itself can be instantiated anywhere on the page that a span tag is permitted:

<span id="testmsg" style="position: absolute;
visibility: hidden; background: red;">This is a testů</span>

The buttons are then implemented simply with:

onclick="Message_Display('testmsg', 1, 700, 50); return false;"

to turn on the message and

onclick="Message_Display('testmsg', 0, 700, 50); return false;"

to turn it off again.

The arguments to Message_Display are: the id of the span tag containing the message text, a Boolean to indicate whether the message should appear or disappear, and the x and y offsets from the upper-right corner of the browser window where the message should be placed (in this case 700px from the right, 50px from the top). The key here is the style of the span tag. By using absolute positioning, we cause the message to appear to float above the rest of the web page, and not be fixed into the flow of the rest of the page. We should also briefly discuss why a span tag is used. Both span and div tags are essentially "do nothing" tags, used to delineate blocks horizontally and vertically, respectively, in a document. They normally have no visible effect on a document, and so can be used to surround a block with a particular style without changing the overall appearance. Thus they are quite handy when making JavaScript widgets.

The function works as follows. First, a handle to the message span tag is retrieved using getElementById, the easiest way to retrieve a pointer to a tag. The next step differs between Internet Explorer (ie) and Firefox/Mozilla (moz), but both achieve the same goal of repositioning the message to the desired location (dx, dy). The message is turned on or off using the visibility style. The last line requires some further explanation about JavaScript Timers.

Timers are a useful way in JavaScript to cause a function to be executed at some later time, either once using setTimeout or repeatedly using setInterval. These are particularly useful because there is no equivalent of a sleep command in JavaScript, and so these provide the best alternative. They do not consume any CPU time until their specified interval of time passes. One must be careful not to overuse these powerful tools, but they can allow one to achieve some impressive effects when used properly. In this case, we set the timer to execute Message_UpdatePos(msg, dy) based on the argument vis. If vis is zero, the timer is cleared (and the message hidden). If vis is non-zero, the timer is set to execute every 1 ms. Note we must save the timer pointer in a global variable intr, since it must persist between several calls to Message_Display.

Lastly, looking at Message_UpdatePos, note it essentially does the same thing as the beginning part of Message_Display--it gets a handle to the message and updates the y offset from the top of the page. Thus as the page scrolls, the message position gets re-adjusted accordingly within 1 ms of the scroll event, appearing to always float at the same position in the upper right corner. Note that the message does not move if the browser is resized horizontally to make it wider or narrower. As an exercise, see if you can modify the code to do this as well.

Pop-up Menus

The next, and last, widget we will discuss in this article is more complex than the previous example: pop-up menus. First, the widget in action:

For menu click here: X

This example is more complex as it involves both a JavaScript function and some complex event handling. Here is the JavaScript code and HTML used:


function getElementAbsPosX(el)
{
    var dx = 0;
    if (el.offsetParent) {
        dx = el.offsetLeft + 8;
        while (el = el.offsetParent) {
            dx += el.offsetLeft;
        }
    }
    return dx;
}

function getElementAbsPosY(el)
{
    var dy = 0;
    if (el.offsetParent) {
        dy = el.offsetTop + el.offsetHeight / 2;
        while (el = el.offsetParent) {
            dy += el.offsetTop;
        }
    }
    return dy;
}

function GetAbsWindowBottom()
{
    // Compute the bottom of the popup window and the bottom of
    // the browser window, in absolute co-ordinates - different
    // on all browsers but the below should be accurate usually!
 
    var abswindowbottom = 0;
    if (typeof(window.innerHeight) == 'number')
        abswindowbottom = window.innerHeight;
    else if (document.documentElement && document.documentElement.clientHeight)
        abswindowbottom = document.documentElement.clientHeight;
    else if (document.body && document.body.clientHeight)
        abswindowbottom = document.body.clientHeight;
 
    if (typeof(window.pageYOffset) == 'number')
        abswindowbottom = abswindowbottom + window.pageYOffset;
    else if (document.body && document.body.scrollTop)
        abswindowbottom = abswindowbottom + document.body.scrollTop;
    else if (document.documentElement && document.documentElement.scrollTop)
        abswindowbottom = abswindowbottom + document.documentElement.scrollTop;
    return abswindowbottom;
}

function PopupMenu(name, vis)
{
    var el = name + 'menu';
    var tag = name + 'menuroot';
    if (!document.getElementById(el))  // menu object not found
        return;
    if (vis == 0) {  // hide the menu
        document.getElementById(el).style.visibility = 'hidden';
        return;
    }

    // Get menuroot position
    var pos = document.getElementById(tag);
    var dx = getElementAbsPosX(pos);
    var dy = getElementAbsPosY(pos);

    // Compare bottom of menu to bottom of window
    var abspopupbottom = dy + document.getElementById(el).clientHeight + 10;
    var abswindowbottom = GetAbsWindowBottom();

    // If menu goes below bottom of window, move it up!
    if (abspopupbottom > abswindowbottom)
        dy = dy - (abspopupbottom - abswindowbottom);

    // Set final menu position and make it appear
    document.getElementById(el).style.left = dx + 'px';
    document.getElementById(el).style.top = dy + 'px';
    if (vis > 0)
        document.getElementById(el).style.visibility = 'visible';
}

The actual HTML for the menu, as in the previous example, can be placed anywhere a div tag is permitted:

<div id="testmenu" style="position: absolute; visibility: hidden; color: #aaaaaa; font-style: italic; border: solid thin #888;
 background-color: #afafaf; padding: 4px;" onmouseover="clearTimeout(tout);" onmouseout="tout=setTimeout('PopupMenu(\'test\', 0);', 1500);">
 <table>
  <tr>
   <td><span style="cursor:pointer; color:blue;" onclick="PopupMenu('test', 0);">Menu Item #1</span></td>
  </tr>
  <tr>
   <td><span style="cursor:pointer; color:blue;" onclick="PopupMenu('test', 0);">Menu Item #2</span></td>
  </tr>
  <tr>
   <td><span style="cursor:pointer; color:blue;" onclick="PopupMenu('test', 0);">Menu Item #3</span></td>
  </tr>
 </table>
</div>

And to instantiate the menu:

<span id="testmenuroot" style="cursor:pointer; color:blue;" 
onclick="PopupMenu('test', 1); tout=setTimeout('PopupMenu
(\'test\', 0);', 1500);" >X</span>

First, let us examine the menu. We could put anything we like in between the div tags, the magic is all in the div tag attributes. Specifically, note that, as in our previous example, we use absolute positioning, so we can control the exact position of the menu, regardless of where other text and images appear. We also ensure the menu starts with visibility: hidden and assign it an ID of testmenu, so we can easily get a handle to it later.

In the div tag we also encounter our first use of the setTimeout and clearTimeout functions. These behave similarly to the setInterval function in the previous example, except they execute a command only one time after the specified duration. In this example, they are used to close the menu 1,500 ms after moving the mouse out of the menu area, and to cancel this effect upon moving the mouse back into the menu area (while it is still open). tout is a global variable defined in our JavaScript file and keeps a pointer to the timer object. When the onmouseout event occurs (moving the mouse outside the area spanned by the div tag--the menu) the PopupMenu('test', 0) is set to be called in 1,500 ms (note how we must escape the quotation marks in this case since it appears inside quotes already). Assuming this is not cancelled before the 1,500 ms elapses, the function is called, and the menu disappears (as we will see below). However if the onmouseover event should occur (moving the mouse over the menu), clearTimeout(tout) is executed, which cancels the timer and prevents the menu from disappearing, until the next time the user triggers a mouse event.

Within the menu itself, we would normally have links to perform tasks or go to other pages. For this example they are just dummy links, of course. However do note the onclick event attached to each menu option. As with the div tag above, these cause the menu to disappear as soon as one of the menu items is clicked (try it!). Note that no timeout is used here, as we want it to disappear immediately in this case.

The next piece of HTML enclosed in a span tag with id testmenuroot surrounds the clickable area at which the menu should actually appear. In the example we simply place an X in between the span tag, but you could, of course, put whatever you like here: a button, an image, and so on. The only other thing to note here is the onclick event, which causes the menu to appear, and then sets our timer to turn off the menu again in 1,500 ms. However, we already know that as long as the user moves his mouse over the menu area, this timer will be cancelled and the menu will stay up until the mouse moves away again.

Lastly, let us examine the JavaScript function itself, PopupMenu. It depends on a few other functions: GetAbsWindowBottom, which simply gets the absolute position of the bottom of the browser window in a browser-independent fashion, and getElementAbsPosX and getElementAbsPosY, which get the x and y position of an element in absolute coordinates. These last two work by recursively adding up the offsets of parent elements until the DocumentRoot is reached. These functions may seem overly complicated, but they are most likely the only way to reliably get these values that works on almost every modern browser. Unfortunately, each browser stores this information in very different ways.

The PopupMenu function itself takes two arguments: a name and a flag whether the menu should be turned on or off. Regarding the name, the menu is assumed to be surrounded by a tag with id being the name argument followed by 'menu' (so 'testmenu' in our example since the argument is 'test'). Similarly, the position at which it is to appear is assumed to be with its top left corner situated at a tag of the name plus 'menuroot' ('testmenuroot' in the example). If the vis argument is zero, we simply set the visibility style of the menu tag to hidden, and we are done. Otherwise, we need to make the menu appear in the right spot. First, we get the absolute position of the 'menuroot' tag using the GetElementAbsPos functions. We can then just set the menu position to these values which would make the upper-left corner of the menu appear near where we clicked to make the menu pop up. However, we do not really want the menu to scroll off the bottom of the page either; that would be annoying. Note that most widget toolkits that have pop-up menus do just that, so we will try to go one better. We can compute the bottom of the browser window, and of the pop-up menu, in absolute coordinates, as done on the next two lines. Note the height of the menu is given by the clientHeight property, and we add 10 just to give a bit of padding. Then we can easily check if the pop-up would go down beyond the bottom of the browser window, and if so, adjust its y coordinate accordingly, so its bottom just touches the bottom of the window. Lastly, we go ahead and position the pop-up menu and make it visible.

Summary

Hopefully, this discussion and these two examples have given you a taste of some of the things you can do with just a few lines of JavaScript, and reveal that there is not really a whole lot of magic going on inside JavaScript toolkits. Although sometimes tricky, it is usually just a bit more work to ensure your functions work on all major browsers, and almost always worth that little bit of extra effort. More detailed examples of JavaScript widgets will be explored in future articles.

Howard Feldman is a research scientist at the Chemical Computing Group in Montreal, Quebec.


Return to ONLamp.com.

Copyright © 2009 O'Reilly Media, Inc.