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


Advanced JavaScript II

by Howard Feldman
08/16/2007

In the preceding article we introduced two types of JavaScript widgets complete with source: floating text and pop-up menus. In this article, we continue with another handful of useful JavaScript widgets, with an emphasis on how they actually work, so you can easily modify them for your own use. The JavaScript used in this article should work on all major current browsers without modification. So, without further ado...

Image Toggling with the div Tag

Often, you may want to put a bunch of stuff, such as images, on a web page, but they take up too much room. Why not put just one image, and let the user use a switch to toggle between the other ones? It works something like this:

figure

This is actually extremely simple to do. Note that the browser is NOT contacting the server each time you switch images, switching is instantaneous. The trick? Put them all in the HTML, but only show one at a time! This is easily accomplished using the div tag and styles. Quite a number of very impressive effects can be achieved using this technique, as will be seen in the following examples. But first, let us examine how this current example actually works. Only one JavaScript function is used:

function ShowImage(page, tag)
{
    var i = 1;
    var el;
    while (el = document.getElementById(tag + i)) {
        if (i == page)
            el.style.display = 'block';
        else
            el.style.display = 'none';
        i++;
    }
}

The actual images are then placed in the HTML at the location where they should appear, each within its own div tag, like so:

<table>
    <tr valign="top">
        <td>
            <div style="display:block" id="image1">
                <img src="/onlamp/2007/08/23/graphics/pic1.jpg" />
            </div>
            <div style="display:none" id="image2">
                <img src="/onlamp/2007/08/23/graphics/pic2.jpg" />
            </div>
            <div style="display:none" id="image3">
                <img src="/onlamp/2007/08/23/graphics/pic3.jpg" />
            </div>
        </td>
        <td width="100%" align="right">
            <select onchange="ShowImage(parseInt(this.value), 'image');">
                <option selected="selected" value="1">Image 1</option>
                <option value="2">Image 2</option>
                <option value="3">Image 3</option>
            </select>
        </td>
    </tr>
</table>

The table structure above is just for layout and can be ignored for the purposes of this discussion. The important part is the three sets of div tags in the first cell of the table. Note that the first has display set to block while the others have it set to none. Anything with display set to none is hidden from view and not drawn in the browser window, although it is still retrieved from the server and stored locally with the rest of the web page. Thus, initially we see only pic1.jpg and not the other two images.

Then on the selector box we have attached a call to our JavaScript function when the onchange event occurs. This is triggered when the user makes a new selection with the selector. We pass two arguments: a reference to the selector node itself and the base id of the div tags containing the images (note these are image1, image2 and image3, all starting with image).

The function starts by getting the newly selected value indicating which image to display. parseInt is used to ensure it is stored as a number and not a string. Next we fetch a handle to the first div tag, in this case the one with id image1. If this is the image to be shown, we set its display style to block; for all others we set it to none. Then we proceed to the next tag (image2), and so on, until no more tags are found, at which point we exit.

So, whenever a new item is selected in the drop-down box, the corresponding div block is revealed, and all others hidden. They all share the same physical space on the web page because they are right next to each other in the HTML. However, this is certainly not a restriction; we could have spread them across different areas of the page and had pictures appearing in various locations depending on the selection made, if desired. Similarly, we are not limited to images; just about anything can go between the div tags and be hidden and shown with a click of the mouse. This allows you to cram much more information onto a web page than you could normally, without requiring the user to do any scrolling!

Tabs: More Fun with div

Let us continue with another example of creative use of the div tag. Often it is desirable to make a web page with several "tabs" of information on it. Clicking on a tab shows the information on that tab, and hides the rest, much like how tabbed browsing works in the most recent round of web browsers. But, usually, clicking on an HTML tab like this causes the page to reload, which certainly disrupts the slickness of the interface. Wouldn't it be cooler if the information instantly appeared? Well thanks to the little trick we just learned in the previous example, it can. Try it out:

  • Summary
  • Details
  • Known Issues

Introducing the new, improved multi-widget. It slices, it dices, it even does your taxes! Order yours today! Call now: 555-WIDG


Not surprisingly, this example uses the exact same JavaScript function as the previous example, yet accomplishes a completely different effect. The remainder is done with a few simple CSS rules. Let's have a look at the HTML for this example:

<div style="display:block" id="tab1">
    <ul class="tab">
        <li class="tab_selected" onclick="ShowImage(1, 'tab');">
            Summary
        </li>
        <li onclick="ShowImage(2, 'tab');">
            Details
        </li>
        <li onclick="ShowImage(3, 'tab');">
            Known Issues
        </li>
    </ul>
    <p>
        Introducing the new, improved multi-widget.  It slices, it dices, it even does
        your taxes!  Order yours today!  Call now: 555-WIDG
    </p>
</div>

<div style="display:none" id="tab2">
    <ul class="tab">
        <li onclick="ShowImage(1, 'tab');">
            Summary
        </li>
        <li class="tab_selected" onclick="ShowImage(2, 'tab');">
            Details
        </li>
        <li onclick="ShowImage(3, 'tab');">
            Known Issues
        </li>
    </ul>
    <p>
        The multi-widget is a sophisticated piece of complex machinery designed by the
        country's leading nuclear physicists.  Order yours today and you will quickly
        learn how easy it is to do just about anything in no time, thanks to our patented
        EZ-Widge technology.
    </p>
    <p>
        Motor: 5HP<br />
        Dimensions: 8" x 5" x 2"<br />
        Weight: 212 g<br />
        Radioactivity: negligible
    </p>
</div>

<div style="display:none" id="tab3">
    <ul class="tab">
        <li onclick="ShowImage(1, 'tab');">
            Summary
        </li>
        <li onclick="ShowImage(2, 'tab');">
            Details
        </li>
        <li class="tab_selected" onclick="ShowImage(3, 'tab');">
            Known Issues
        </li>
    </ul>

    <ul>
        <li>Do not use multi-widget near open flames</li>
        <li>Do not run while holding multi-widget</li>
        <li>Do not taunt multi-widget</li>
        <li>
            Multi-widget may, under certain as yet undetermined circumstances,
            spontaneously explode.  We hereby disclaim any libaility for personal injury
            caused as a result of multi-widget; for your safety, we recommend wearing
            body armor while handling multi-widget.
        </li>
    </ul>
</div>

Note that we have three div sections (one for each tab) just as we had one for each image in the previous example. And again, the first block is given display:block while the others are display:none, so only the first will be visible initially. Each of these sections begins by drawing the three tabs, with the selected one a different color than the others. So, we actually draw all three tabs over again on each tab! The contents of the tab follow, and can be arbitrary HTML. Note that the contents only appear once, only the actual HTML for the tabs themselves is repeated multiple times, so the "wasted" space is minimal.

Note that each item calls the ShowImage JavaScript function when the onclick event is triggered. As we saw in the first example, this hides all the div sections except the one given as the argument to ShowImage. Thus we use it here to show the tab you have just clicked on and hide the rest. CSS or styles are used to actually draw the tabs properly. We have defined two classes: tab, and tab_selected. The former is general and applies to the whole tab bar, while the latter is applied only to the currently selected tab. The CSS looks like this:

ul.tab {
    margin: 0;
    padding: 3px 0;
    border-bottom: 1px solid #778;
    font-weight: bold;
}

ul.tab li {
    display: inline;
    padding: 3px 0.5em;
    margin-left: 3px;
    border-top: 1px solid #778;
    border-left: 1px solid #778;
    border-right: 1px solid #778;
    border-bottom: none;
    background: top repeat-x #89aac7;
    white-space: nowrap;
    color: white;
    cursor:pointer;
}

ul.tab li.tab_selected {
    background: #fff;
    border-bottom: 1px solid #fff;
    color: black;
}

The first section applies to the ul tag and thus the whole tab bar. It sets the margins and padding so it is forced all the way over to the left and a border is drawn along the bottom of the tab bar. The padding is needed for everything to line up properly. We also bold any text in the tab bar.

The next section is applied to all the li tags in the tab bar. display:inline is perhaps the most important, causing the list to go horizontally instead of vertically. Again padding and margins are used to make things line up and leave a pleasing space between the tabs. The border properties draw the top and sides of the tabs themselves (the bottom comes from the ul tag rememeber). Finally, we set the background and foreground colors and ensure text wrapping is disabled.

The final section is applied only to the currently selected tab, overriding any settings in the second section. This changes the background and foreground colors of the tab to make it stand out, as well as drawing a white bottom border to cover up the black bottom border from the ul tag. This gives the illusion that the tab has popped forward in front of the other tabs.

In this example, all the contents of all the tabs are loaded and stored with the page, so clicking on a tab produces the new information almost instantly and without any distracting reloading of the page. Using a very simple JavaScript function and a bit of CSS, we have achieved a very impressive effect indeed.

And Even More div Tricks

As a last example of a useful application of the tricks we have learned above, try the following example that can be used as a way of summarizing large sections of text or lists and allowing the user to open and look at only the lists that he/she is interested in:

Click to Expand Choice of four widget colors

And the source:

<div style="display:block;" id="colors1">
    <table style="background:#eeeebb">
        <tr>
            <td>
                <img src="/onlamp/2007/08/23/graphics/expand.jpg" style="cursor:pointer;"
                alt="Click to Expand" title="Click to Expand" onclick="ShowImage(2, 'colors');" />
            </td>
            <td>
                Choice of four widget colors
            </td>
        </tr>
    </table>
</div>
<div style="display:none;" id="colors2">
    <table style="background:#eeeebb">
        <tr valign="top">
            <td>
                <img src="/onlamp/2007/08/23/graphics/collapse.jpg" style="cursor:pointer;"
                alt="Click to Collapse" title="Click to Collapse" onclick="ShowImage(1, 'colors');" />
            </td>
            <td>
                <ul>
                    <li>blue</li>
                    <li>green</li>
                    <li>red</li>
                    <li>brown</li>
                </ul>
            </td>
        </tr>
    </table>
</div>

By now it should be pretty clear how this is accomplished. Again we have two div blocks, one with the collapsed version of the text and the expand button and one with the fully expanded version of the text and the collapse button. Initially, we show only the collapsed version (display:block) and we use the onclick callback of the image to trigger the swap between div blocks. The blocks are given id of colors1 and colors2 so that they can be easily switched by the ShowImage JavaScript function we wrote earlier.

Drag and Drop and Swap

Now that we have examined div tags ad nauseum, let us look at something a little different. In this example, we reveal a method to drag and drop text (or images) with the mouse. This could be used in making a little JavaScript game or have a more serious purpose, for example, allowing you to swap the order of images in a page of images. In the following example, try to drag and drop the three names on top of each other to swap their positions. Note how you can swap John and Jane with no trouble, but Bill will not swap with either one of them, as he is always the troublemaker.

John
Jane
Bill

This example requires more code than the others shown so far, but is still reasonably straightforward. Unlike the others, it uses global mouse events. Specifically, it depends on three callback functions that are triggered: whenever the mouse button is depressed; when the mouse moves; and when the mouse button is released. The disadvantage of using these sorts of global events is that, if you have multiple things going on at once, it can get pretty hairy quickly. So, you should minimize your use of these and try to have at most one or two per page. For this particular example, global events are pretty much necessary. Let's go through the code one section at a time:

// Set the callbacks
document.onmousedown = mousedown;
document.onmousemove = movemouse;
document.onmouseup   = mouseup;

var lastobj;  // Last draggable object we hovered over
var isdrag;   // True if dragging an object

Here we initialize the three callback functions. mousedown will be called whenever the user clicks a mouse button, movemouse will be called whenever the mouse moves, and mouseup will be triggered whenever the left mouse button is released. We need two global variables as well, the first to track the most recent object we hovered over and the second is a flag, true if we are currently dragging an object.

// This prevents browsers from highlighting the draggable text
// when you click on it.  The table containing all the draggable
// text has id drag_drop.

window.onload = function()
{
    var e = document.getElementById('drag_drop');
    if (e) {
        if (moz)
            e.onmousedown = function () { return false; } // mozilla
        else
            e.onselectstart = function () { return false; } // ie
    }
}

This little function is executed as soon as the page is loaded and ensures clicking on the draggable objects does not highlight the text, as would normally happen when clicking on text. This is achieved by overwriting the normally called function responsible for highlighting the text with an empty function.

// Checks to see if a swap is allowed between two objects based on their ids.
// Change this as you see fit to permit or forbid swapping each possible pair
// of draggable items.
function allowswap(a,b)
{
    if (a.id == "dragdropa" && b.id == "dragdropb" || a.id == "dragdropb" && b.id == "dragdropa")
        return true;
    return false;
}

// Returns true if an object is draggable - change this to suit your needs.
function isdraggable(obj)
{
    if (obj.id.substr(0,8) == "dragdrop")
        return true;
    return false;
}

There are two other utility functions we need; given two objects, allowswap returns true if the objects can be swapped, and false otherwise. Obviously, with some more sophisticated logic here, you can do some interesting things. isdraggable returns true if a given object is draggable. For the purposes of this example, we base this decision on the id of the object beginning with dragdrop, but you may, of course, change this to suit your needs. Draggability could be indicated in other ways, such as with a particular class, but this will do for the purposes of our example.

// Callback when mouse button is pressed.  This checks if an item is draggable, and
// if so initiates the process.
function mousedown(e) 
{
    var obj = moz ? e.target : event.srcElement;
    // Trace up DOM tree to see if item clicked on is draggable.  This allows
    // for the fact that you may click, for example, on a TD while the enclosing
    // TR is the draggable object.
    while (obj.tagName != "HTML" && obj.tagName != "BODY" && !isdraggable(obj)) {
        obj = moz ? obj.parentNode : obj.parentElement;
    }
    if (isdraggable(obj)) {
        // If draggable, set a global flag to track this, and save a pointer
        // to the object in a global variable as well (dragobj).
        isdrag = true;
        dragobj = obj;

        // origx, origy is original starting location of dragged object
        origx = dragobj.style.left;
        origy = dragobj.style.top;

        // x,y is absolute co-ordinates within the window
        x = moz ? e.clientX : event.clientX;
        y = moz ? e.clientY : event.clientY;

        // While offsetX, offSetY depend on where exactly you clicked on the object.
        // Thus if you click in the middle of the object, it will be 'attached' to
        // the mouse at that point, and not the upper left corner, for example.
        offsetX = moz ? e.layerX + 2: event.x + 2;
        offsetY = moz ? e.layerY + 2: event.y + 2;
    }
}

mousedown, called when a mouse button is pressed, is the first main piece of code. So, when something is clicked on, we check if it is draggable. But if not, we also need to check if its parent is draggable, and so on. This is because when you click on something, the browser will usually return you a handle to the lowest, deepest item in the DOM tree. So, for example, if you had a span tag marked as draggable surrounding a p tag and clicked on the text in between, you would get back a handle to the p tag, which is not itself marked draggable. Checking its parent however, we would see that it is indeed draggable as a result of being a child of a draggable item.

Assuming we have clicked on something draggable, we set the global flag to true and save the object in dragobj. We also save several sets of coordinates: the original location of dragobj, the relative position of the mouse when we clicked, and the absolute position where we clicked.

// Callback when mouse is moved.  It will change the cursor when you move over an object
// you can - or cannot - swap with.

function movemouse(e)
{
    // Check if we are dragging an object
    if (isdrag) {
        // If so, set the dragged object's position relative to how much the mouse has moved
        // since first clicked.
        dragobj.style.left = moz ? origx + e.clientX - x + offsetX + 'px' : origx + event.clientX - x + offsetX;
        dragobj.style.top  = moz ? origy + e.clientY - y + offsetY + 'px' : origy + event.clientY - y + offsetY;
        var obj = moz ? e.target : event.srcElement;
    
        // If we are over an element that we cannot swap with, change its cursor style
        // to show that a swap is forbidden
        if (obj != dragobj && isdraggable(obj) && !allowswap(dragobj,obj)) {
            obj.style.cursor = 'wait';
            // save in a handle to the object in a global so we can reset the cursor later
            lastobj = obj;
        }
        else if (lastobj)  // reset the cursor as soon as we move off a non-swappable object
            lastobj.style.cursor = 'pointer';
        return false;
    }
    else {
        // Sometimes can get stuck with no drop icon, so restore cursor just to be safe,
        // when not dragging but passing over a draggable item
        var obj = moz ? e.target : event.srcElement;
        if (isdraggable(obj))
            obj.style.cursor = 'pointer';
    }
}

Whenever the mouse moves, we first check if something is being dragged. If so, we compute where the dragged object should be (after all it does NOT magically follow the mouse around by itself!) and move the object accordingly. Basically the formulas ensure the object moves the same amount the mouse has moved, since the object was clicked on. We also look to see if we are hovering over another draggable object. If so, we use allowswap to determine if a swap can take place. If not, the cursor turns into a "wait" shape (varies from computer to computer but this is the standard busy cursor). If a swap can take place or we are not over a draggable object, we leave the cursor alone, and restore it if we had changed it previously.

Note that, as an extra precaution, if nothing is being dragged we still restore the cursor to normal if hovering over a draggable object. This is because sometimes the cursor can get stuck in the "wait" state, depending how quickly the user moves the mouse, releases the button, etc. That is, the logic mentioned above for restoring the cursor is not failproof.

// Callback when mouse is released - checks if a swap should occur and
// returns dragged object to its starting position if not.

function mouseup(e)
{
    if (isdrag) {  // If something is being dragged

        // Get the object over which the mouse button was just released
        var obj = moz ? e.target : event.srcElement;

        // Check if mouse was release over an object we can swap with
        if (obj != dragobj && isdraggable(obj) && allowswap(dragobj, obj)) {

            // A swap is allowed - swap color, tooltip and contents of the
            // dragged object with that it was released over
            var htm = obj.innerHTML;
            obj.innerHTML = dragobj.innerHTML;
            dragobj.innerHTML = htm;

            var col = obj.style.color;
            obj.style.color = dragobj.style.color;
            dragobj.style.color = col;

            var titl = obj.title;
            obj.title = dragobj.title;
            dragobj.title = titl;

            // Set the position of the object we were dragging (dragobj) where the
            // other object (obj) is located and move obj to the original location
            // of dragobj before it was dragged (origx, origy).
            dragobj.style.left = obj.style.left;
            dragobj.style.top = obj.style.top;
            obj.style.left = origx;
            obj.style.top = origy;
        }
        else {
            // No swap can occur so return dragged object to its starting position
            dragobj.style.left = origx;
            dragobj.style.top = origy;
        }

        // Restore cursor to pointer if it was changed in movemouse above
        if (lastobj) {
            lastobj.style.cursor = 'pointer';
        }
    }
    isdrag = false;  // Nothing is being dragged now
}

mouseup does the most legwork if the mouse is released while dragging an object. If released over a draggable object, we check with allowswap if a swap can take place. If it can, we swap the textual contents, color, and tooltips (title attribute) of the two objects. You may conceivably prefer to just swap ALL their attributes at this point, depending on your goals. Then we move the dragged object to the location of the object we released the mouse over, and move that object in turn to the original starting place of the dragged object. Thus the swap is complete. On the other hand, if a swap is forbidden, we merely return the dragged object to its starting location. Either way, we restore the mouse cursor in case it had been changed to a "wait" cursor in movemouse above, and we clear the isdrag flag to indicate we are no longer dragging an object.

Finally, to instantiate the objects is quite easy. Just remember that draggable objects have ids that start with dragdrop:

<table id="drag_drop" align="center" style="font-size:150%; color:green; background-color:#88ccff; white-space: nowrap;">
    <tr>
        <td>
            <span id="dragdropa" style="cursor: pointer; position: relative; color: #ff0000" title="John Doe">John</span>
        </td>
    </tr>
    <tr>
        <td>
            <span id="dragdropb" style="cursor: pointer; position: relative; color: #a0522d" title="Jane Smith">Jane</span>
        </td>
    </tr>
    <tr>
        <td>
            <span id="dragdropc" style="cursor: pointer; position: relative; color: #00aa00" title="Bill Schwartz">Bill</span>
        </td>
    </tr>
</table>

The objects were placed in a table, as well as given different colors, for convenience only, but the only important things here are the ids of the draggable items, which are also referenced in allowswap, and the fact that the draggable objects have position:relative in their style attribute. The formulas used to compute positions in the JavaScript assume relative positioning for these objects. Also note that the surrounding table has id drag_drop, which is referenced at the very beginning in the window.onload function.

Summary

Hopefully, the preceding examples have shown you some of the creative ways in which div tags can be used to section off and selectively display portions of a web page. The drag-and-drop example is just a very basic example of an extremely powerful concept in some of the most advanced web pages available today. With just a bit more work, it can be used, for example, to make a fully customizable home page, where you can drag around and swap various news feeds, images, and more to your heart's content. The only limits are your imagination!

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


Return to ONLamp.

Copyright © 2009 O'Reilly Media, Inc.