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.
|