[ Team LiB ] Previous Section Next Section

6.9 Simulating a Cross-Browser Modal Dialog Window

NN 4, IE 4

6.9.1 Problem

You want to present a consistent modal dialog on multiple browsers.

6.9.2 Solution

Although IE provides the showModalDialog( ) method, no other browser supports it. This recipe uses a browser subwindow to simulate the behavior of a modal dialog box. It operates in IE 4 or later, Navigator 4 or later, and Opera 6 or later. Note that this is a simulation of true modality. Due to some odd behavior in IE for Windows with respect to disabling hyperlinks in the main window, a determined user can bypass the modality of this solution. For casual users, however, the window behaves much like a modal dialog box.

Assemble your main HTML page around the simModal.js script library described in the Discussion. This library works by disabling form controls and links in the main page after the modal dialog is displayed and making sure the dialog keeps the focus, so that the user is forced to deal with the dialog. After the dialog is dismissed, the form controls and links are enabled again.

The following skeletal HTML main page shows the event handler additions that the simModal.js library relies upon, and a demonstration of how to invoke the function that displays a simulated modal window (in this example, a Preferences window):

<html>
<head>
<title>Main Application Page</title>
<script type="text/javascript" src="simModal.js"></script>
<script language="JavaScript" type="text/javascript">
// function to run upon closing the dialog with "OK".
function setPrefs( ) {
    // Statements here to apply choices from the dialog window
}
</script>
</head>
<body onclick="checkModal( )" onfocus="return checkModal( )">
<!-- Page Content Here -->
<a href="noPrefs.html" onmouseover="status='Set preferences...';return true"
   onmouseout="status='';return true"
   onclick="openSimDialog('dialog_main.html', 400, 300, setPrefs);return false">
Preferences
</a>
<!-- More Page Content Here -->
</body>
</html>

Add the onclick and onfocus event handlers to the <body> tag as shown. Those event handlers invoke the checkModal( ) event handler function defined in the external library to make sure the dialog window keeps the focus. Call the openSimDialog( ) function to display the window, passing the URL of the page to load into the dialog window, the window's width and height (in pixels), and a reference to a function in the main page that the modal window invokes when the window closes (setPrefs( ) in this case).

In the dialog window's page, add the closeme( ), handleOK( ), and handleCancel( ) functions shown in the following extract to take care of the actions from the dialog window's Cancel and OK buttons. The onload and onunload event handlers of the <body> tag trigger essential event-blocking services controlled by the blockEvents( ) and unblockEvents( ) event handlers in the simModal.js library.

<html>
<head>
<title>Preferences</title>
<script language="JavaScript" type="text/javascript">
// close the dialog
function closeme( ) {
    window.close( );
}
   
// handle click of OK button
function handleOK( ) {
    if (opener && !opener.closed && opener.dialogWin) {
        opener.dialogWin.returnFunc( );
    } else {
        alert("You have closed the main window.\n\nNo action will be taken on the " +
              "choices in this dialog box.");
    }
    closeme( );
    return false;
}
   
// handle click of Cancel button
function handleCancel( ) {
    closeme( );
    return false;
}
</script>
</head>
<body onload="if (opener && opener.blockEvents) opener.blockEvents( )" onunload="if 
(opener && opener.unblockEvents) opener.unblockEvents( )">
<!--- Dialog Window Page Content Here -->
<form>
<input type="button" value="Cancel" onclick="handleCancel( )">
<input type="button" value="   OK   " onclick="handleOK( )">
</form>
   
</body>
</html>

If the dialog window contains a frameset (where the Cancel and OK buttons are in one of the frames), locate the onload and onunload event handlers in the <frameset> tag. Keep the three functions in the framesetting document, and have the onclick event handlers of the buttons reference parent.handleCancel( ) and parent.handleOK( ).

6.9.3 Discussion

Example 6-1 shows the entire simModal.js library, which you link into the main HTML page, as shown in the Solution.

Example 6-1. The simulated modal dialog window script library (simModal.js)
// Global flag for Navigator 4-only event handling branches.
var Nav4 = ((navigator.appName =  = "Netscape") && (parseInt(navigator.appVersion) =  = 4))
   
// One object tracks the current modal dialog opened from this window.
var dialogWin = new Object( );
   
// Event handler to inhibit Navigator 4 form element 
// and IE link activity when dialog window is active.
function deadend( ) {
    if (dialogWin.win && !dialogWin.win.closed) {
        dialogWin.win.focus( );
        return false;
    }
}
   
// Since links in some browsers cannot be truly disabled, preserve 
// link onclick & onmouseout event handlers while they're "disabled."
// Restore when re-enabling the main window.
var linkClicks;
   
// Disable form elements and links in all frames.
function disableForms( ) {
    linkClicks = new Array( );
    for (var i = 0; i < document.forms.length; i++) {
        for (var j = 0; j < document.forms[i].elements.length; j++) {
            document.forms[i].elements[j].disabled = true;
        }
    }
    for (i = 0; i < document.links.length; i++) {
        linkClicks[i] = {click:document.links[i].onclick, up:null};
        linkClicks[i].up = document.links[i].onmouseup;
        document.links[i].onclick = deadend;
        document.links[i].onmouseup = deadend;
        document.links[i].disabled = true;
    }
    window.onfocus = checkModal;
    document.onclick = checkModal;
}
   
// Restore form elements and links to normal behavior.
function enableForms( ) {
    for (var i = 0; i < document.forms.length; i++) {
        for (var j = 0; j < document.forms[i].elements.length; j++) {
            document.forms[i].elements[j].disabled = false;
        }
    }
    for (i = 0; i < document.links.length; i++) {
        document.links[i].onclick = linkClicks[i].click;
        document.links[i].onmouseup = linkClicks[i].up;
        document.links[i].disabled = false;
    }
}
   
// Grab all Navigator events that might get through to form
// elements while dialog is open. For IE, disable form elements.
function blockEvents( ) {
    if (Nav4) {
        window.captureEvents(Event.CLICK | Event.MOUSEDOWN | Event.MOUSEUP | Event.FOCUS);
        window.onclick = deadend;
    } else {
        disableForms( );
    }
    window.onfocus = checkModal;
}

// As dialog closes, restore the main window's original
// event mechanisms.
function unblockEvents( ) {
    if (Nav4) {
        window.releaseEvents(Event.CLICK | Event.MOUSEDOWN | Event.MOUSEUP | Event.FOCUS);
        window.onclick = null;
        window.onfocus = null;
    } else {
        enableForms( );
    }
}
   
// Generate a modal dialog.
// Parameters:
//    url -- URL of the page/frameset to be loaded into dialog
//    width -- pixel width of the dialog window
//    height -- pixel height of the dialog window
//    returnFunc -- reference to the function (on this page)
//                  that is to act on the data returned from the dialog
//    args -- [optional] any data you need to pass to the dialog
function openSimDialog(url, width, height, returnFunc, args) {
    if (!dialogWin.win || (dialogWin.win && dialogWin.win.closed)) {
        // Initialize properties of the modal dialog object.
        dialogWin.url = url;
        dialogWin.width = width;
        dialogWin.height = height;
        dialogWin.returnFunc = returnFunc;
        dialogWin.args = args;
        dialogWin.returnedValue = "";
        // Keep name unique.
        dialogWin.name = (new Date( )).getSeconds( ).toString( );
        // Assemble window attributes and try to center the dialog.
        if (window.screenX) {              // Navigator 4+
            // Center on the main window.
            dialogWin.left = window.screenX + 
               ((window.outerWidth - dialogWin.width) / 2);
            dialogWin.top = window.screenY + 
               ((window.outerHeight - dialogWin.height) / 2);
            var attr = "screenX=" + dialogWin.left + 
               ",screenY=" + dialogWin.top + ",resizable=no,width=" + 
               dialogWin.width + ",height=" + dialogWin.height;
        } else if (window.screenLeft) {    // IE 5+/Windows 
            // Center (more or less) on the IE main window.
            // Start by estimating window size, 
            // taking IE6+ CSS compatibility mode into account
            var CSSCompat = (document.compatMode && document.compatMode != "BackCompat");
            window.outerWidth = (CSSCompat) ? document.body.parentElement.clientWidth : 
                document.body.clientWidth;
            window.outerHeight = (CSSCompat) ? document.body.parentElement.clientHeight :  
                document.body.clientHeight;
            window.outerHeight -= 80;
            dialogWin.left = parseInt(window.screenLeft+ 
               ((window.outerWidth - dialogWin.width) / 2));
            dialogWin.top = parseInt(window.screenTop + 
               ((window.outerHeight - dialogWin.height) / 2));
            var attr = "left=" + dialogWin.left + 
               ",top=" + dialogWin.top + ",resizable=no,width=" + 
               dialogWin.width + ",height=" + dialogWin.height;
        } else {                           // all the rest
            // The best we can do is center in screen.
            dialogWin.left = (screen.width - dialogWin.width) / 2;
            dialogWin.top = (screen.height - dialogWin.height) / 2;
            var attr = "left=" + dialogWin.left + ",top=" + 
               dialogWin.top + ",resizable=no,width=" + dialogWin.width + 
               ",height=" + dialogWin.height;
        }
        // Generate the dialog and make sure it has focus.
        dialogWin.win=window.open(dialogWin.url, dialogWin.name, attr);
        dialogWin.win.focus( );
    } else {
        dialogWin.win.focus( );
    }
}
   
// Invoked by onfocus event handler of EVERY frame,
// return focus to dialog window if it's open.
function checkModal( ) {
    setTimeout("finishChecking( )", 50);
    return true;
}
   
function finishChecking( ) {
    if (dialogWin.win && !dialogWin.win.closed) {
        dialogWin.win.focus( );
    }
}

The library begins with a couple of global variable declarations that ripple through the entire application. One, Nav4, is a flag for Navigator 4 only; the other, dialogWin, holds the reference to the dialog window.

The deadend( ) function is an event handler function that the simModal.js library assigns to all main page hyperlinks whenever the dialog box is visible. The function does its best to block the default action of clicking on a hyperlink, as well as block all Navigator 4 mouse-related events.

Next are a pair of functions that disable or enable form controls and links. The disableForms( ) method is ultimately invoked when the modal window appears (the dialog window's onload event handler invokes blockEvents( ), which, in turn, calls disableForms( )). Default event handler assignments for hyperlinks are preserved in a global variable called linkClicks before the links are temporarily assigned the deadend( ) function. When the modal window closes, enableForms( ) restores default states.

The goal of the blockEvents( ) function varies slightly with browser. Navigator 4's event capture mechanism takes care of a lot of ills, whereas other browsers need to go through the disableForms( ) function. When it's time to bring everything back to normal, the unblockEvents( ) function, invoked by the onunload event handler of the dialog window, reverses the process

The heart of the dialog creation function is openSimDialog( ). This function takes several parameters that let you specify the URL of the document to occupy the dialog box, the size of the window, the name of the function from the main document that can be invoked easily from the dialog, and optional values to be passed directly to the dialog window (although the traditional subwindow relationships are in force if you want to communicate between windows that way, as described in Recipe 6.6 and Recipe 6.7). Most of the code here is devoted to calculating the (sometimes approximate) center of the browser window to place the dialog window, but the function also populates the global dialogWin object, which maintains important values that the dialog window's scripts access (described shortly).

After all this setup code, the final two functions, checkModal( ) and the chained finishChecking( ), force the subwindow to act like a modal window by giving the subwindow focus whenever the main window tries to come forward. A time-out takes care of the usual window synchronizing stuff that particularly affects IE for Windows.

The simulated modal dialog window library is a fairly complex application of JavaScript. It came into being not so much to get modality for Netscape Navigator, but to work around a problem in earlier IE versions for Windows that prevented scripts in showModalDialog( ) windows from working with framesets in the modal window. By employing regular browser windows, the problem was solved; with only a little tweaking, the solution worked for Netscape and, now, Opera. An earlier version of this solution appeared in an article for the Netscape developer web site.

One significant way that this simulated modal dialog differs from the IE showModalDialog( ) approach is that script execution in the main window does not halt while the simulated window is open. Instead, the simulated version operates more like IE's showModelessDialog( ). Notice in the large openSimDialog( ) function that several arguments to the function are assigned to properties of the dialogWin global object. This object acts as a warehouse for key data about the window, including a reference to the dialog window itself (the dialogWin.win property). One property, returnFunc, is a reference to a main window function that the subwindow can invoke easily. Although the syntax, modeled after showModelessDialog( ), is intended to be invoked when the dialog window closes (perhaps the result of a click of an OK button), a script in the dialog window can reach out to the main window function at any time. It's just that handling it in batch mode as the dialog closes reinforces the modality you're trying to convey to the user. Invoking the function from the subwindow is as easy as:

opener.dialogWin.returnFunc( );

If the function takes parameters, you can included them in the call as well:

opener.dialogWin.returnFunc(document.myForm.myTextBox.value);

Going in the direction of passing data to the dialog window, the optional fifth parameter to openSimDialog( ) is a value of any JavaScript data type that you want scripts in the dialog to access easily. You can pack a bunch of values together as an array or custom object. Access the value via the dialogWin.args property. Thus, a script in the dialog window can read the value as follows:

var passedValue = opener.dialogWin.args;

A typical modal dialog window asks the user to make some settings or entries that affect the main window and its document or data. Good user interface design suggests that you always include a way for the user to back out of the dialog box without making any changes to the main document. As shown in the Solution, a pair of buttons (or button equivalents) that connote Cancel and OK should let users choose between aborting the dialog or committing the data to the application. Notice that the code watches out for the possibility that the user has closed the main window (because scripts cannot block access to the main browser window's close button).

Applying the simulated modal dialog window to a main window that holds a frameset gets a little more complicated, but it is entirely possible. The key to successful implementation begins by moving the disableForms( ) and enableForms( ) functions (and their supporting functions) to the frameset's scripts. Modify both functions so that they loop through all frames to disable and enable the form controls and hyperlinks. You can continue to use the linkClicks global variable, but only as an array of arrays: the outer array corresponds to each frame; the inner array corresponds to the links in the frame. Here is an example of how disableForms( ) could be modified:

// Disable form elements and links in all frames.
function disableForms( ) {
    linkClicks = new Array( );
    for (var h = 0; h < frames.length; h++) {
        for (var i = 0; i < frames[h].document.forms.length; i++) {
            for (var j = 0; j < frames[h].document.forms[i].elements.length; j++) {
                frames[h].document.forms[i].elements[j].disabled = true;
            }
        }
        linkClicks[h] = new Array( );
        for (i = 0; i < frames[h].document.links.length; i++) {
            linkClicks[h][i] = {click:frames[h].document.links[i].onclick, up:null};
            linkClicks[h][i].up = frames[h].document.links[i].onmouseup;
            frames[h].document.links[i].onclick = deadend;
            frames[h].document.links[i].onmouseup = deadend;
            frames[h].document.links[i].disabled = true;
        }
        frames[h].window.onfocus = checkModal;
        frames[h].document.onclick = checkModal;
    }
}

6.9.4 See Also

Recipe 6.8 for the IE proprietary (and more robust) modal and modeless window methods; Recipe 6.10 for using layers to simulate an overlaid window; Recipe 3.1 and Recipe 3.7 for creating an array or custom object as a chunk of data to be passed as arguments to the modal window.

    [ Team LiB ] Previous Section Next Section