6.10 Simulating a Window with Layers
NN 6, IE 5
6.10.1 Problem
You want to create the
impression of a separate draggable window but without actually
creating a new window.
6.10.2 Solution
This solution consists of many individual files, including three
.js JavaScript library files and several
.css style sheet files. One of the library
files,
DHTML2API.js, is applied directly from Recipe 13.3;
the other libraries,
layerDialog.js and
layerDialogDrag.js, which take care of creating the window
and making it draggable, respectively, are shown in the Discussion.
The .css files provide different looks for the
window on different operating systems; they are also shown in the
Discussion.
The following skeletal HTML main page shows the JavaScript libraries
as they are linked into the page, the makeup of the required elements
(some div, span, and
iframe elements) that comprise the pseudowindow,
and a sample hyperlink element that invokes the
openLayerDialog(
) function to display the modeless,
draggable pseudowindow (in this example, a Preferences window):
<html>
<head>
<title>Main Application Page</title>
<script type="text/javascript" src="DHTML2API.js"></script>
<script type="text/javascript" src="layerDialog.js"></script>
<script type="text/javascript" src="layerDialogDrag.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 onload="initDHTMLAPI( ); initDrag( ); initLayerDialog( )">
<!-- PAGE CONTENT HERE -->
<a href="noPrefs.html" onmouseover="status='Set preferences...';return true"
onmouseout="status='';return true"
onclick="openLayerDialog('dialog_main.html', 'User Preferences', setPrefs,
null);return false">
Preferences
</a>
<!-- More Page Content Here -->
<div id="pseudoWindow">
<div id="titlebar" class="draggable"><img id="closebox"
src="closeBox_win9x.jpg" onclick="closeLayerDialog( )" />
<span id="barTitle">Titlebar</span></div>
<iframe id="contentFrame" src="" frameborder="0" vspace="0" hspace="0"
marginwidth="14" marginHeight="14" width="100%" height="480" scrolling="auto">
</iframe>
</div>
</body>
</html>
Three key event initializations routines, initDHTMLAPI(
) (from DHTML2API.js),
initDrag( ) (from
layerDialogDrag.js), and
initLayerDialog( ) (from
layerDialog.js) are invoked by the
onload event handler of the
<body> tag.
6.10.3 Discussion
Specifying the functional characteristics of a positioned element
acting as a simulated window gets a bit complicated if you wish to
take into account the wide variety of looks and feels of popular
operating systems. A window design tailored for Windows 98
won't look anything like a Windows XP window. Even
in the Mac world, there is a traditional look to Mac OS 9 and the
radically different Mac OS X. This recipe makes the following
assumptions about the pseudowindow constructed from positioned
elements:
The window is a fixed size, governed by style sheets (the example
shows 800x600)
Titlebars and close boxes are customized for Windows 9x, Windows XP,
Mac OS, and Mac OS X
Other operating systems default to the Windows 9x look
The content of the window can be loaded from any URL
The window is draggable, behaving like a modeless dialog window
The element-based pseudowindow is a draggable composite object. An
outer wrapper div
(pseudoWindow) is the overall container for the
pseudowindow. Nested inside are two child elements. The first
represents the pseudowindow's titlebar. Two child
nodes of the titlebar div element are an image for
the window's close box and a span
for the window's text title. The second is an
iframe element, which holds the content.
At the core of the interactivity built into the pseudowindow are
three JavaScript libraries: DHTML2API.js,
layerDialog.js, and
layerDialogDrag.js. Because the
DHTML2API.js library (Recipe 13.3) contains
numerous cross-browser functions for element positioning, the current
recipe loads this library to support the positioning tasks required
of the pseudowindow.
Example 6-2 shows the code for the
layerDialog.js library, which has two important jobs:
linking in the OS-specific style sheets for the pseudowindow, and
controlling the creation, initial position, and display of the
pseudowindow.
Example 6-2. layerDialog.js library
// Help choose from four UI pseudo-window flavors
function getCurrOSUI( ) {
var ua = navigator.userAgent;
if (ua.indexOf("Mac") != -1) {
if (ua.indexOf("OS X") != -1 || ua.indexOf("MSIE 5.2") != -1) {
return "macosX";
} else {
return "macos9";
}
} else if (ua.indexOf("Windows XP") != -1 || ua.indexOf("NT 5.1") != -1) {
return "winxp";
} else if ((document.compatMode && document.compatMode != "BackComp") ||
(navigator.product && navigator.product = = "Gecko")) {
// Win9x and CSS-compatible
return "win9x";
} else {
// default for Windows 9x in quirks mode, Unix/Linux, & unknowns
return "win9xQ";
}
}
var currOS = getCurrOSUI( );
// Load OS-specific style sheet for pseudo dialog layer
document.write("<link rel='stylesheet' type='text/css' href='dialogLayer_" + currOS +
".css'>");
//******************************
// Begin Layer Dialog Code
//******************************/
// One object tracks the current pseudo-window layer.
var dialogLayer = {layer:null, visible:false};
// Center a positionable element whose name is passed as
// a parameter in the current window/frame, and show it
function centerOnWindow(elemID) {
// 'obj' is the positionable object
var obj = getRawObject(elemID);
// window scroll factors
var scrollX = 0, scrollY = 0;
if (document.body && typeof document.body.scrollTop != "undefined") {
scrollX += document.body.scrollLeft;
scrollY += document.body.scrollTop;
if (document.body.parentNode &&
typeof document.body.parentNode.scrollTop != "undefined") {
scrollX += document.body.parentNode.scrollLeft;
scrollY += document.body.parentNode.scrollTop
}
} else if (typeof window.pageXOffset != "undefined") {
scrollX += window.pageXOffset;
scrollY += window.pageYOffset;
}
var x = Math.round((getInsideWindowWidth( )/2) -
(getObjectWidth(obj)/2)) + scrollX;
var y = Math.round((getInsideWindowHeight( )/2) -
(getObjectHeight(obj)/2)) + scrollY;
shiftTo(obj, x, y);
}
function initLayerDialog( ) {
document.getElementById("closebox").src="closeBox_" + currOS + ".jpg";
dialogLayer.layer = document.getElementById("pseudoWindow");
}
// Set up and display pseudo-window.
// Parameters:
// url -- URL of the page/frameset to be loaded into iframe
// 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 openLayerDialog(url, title, returnFunc, args) {
if (!dialogLayer.visible) {
// Initialize properties of the modal dialog object.
dialogLayer.url = url;
dialogLayer.title = title;
dialogLayer.returnFunc = returnFunc;
dialogLayer.args = args;
dialogLayer.returnedValue = "";
// Load URL
document.getElementById("contentFrame").src = url;
// Set title of "window"
document.getElementById("barTitle").firstChild.nodeValue = title;
// Center "window" in browser window or frame
dialogLayer.layer.style.visibility = "hidden";
dialogLayer.layer.style.display = "block"
centerOnWindow("pseudoWindow");
// Show it and set visibility flag
dialogLayer.layer.style.visibility = "visible"
dialogLayer.visible = true;
}
}
function closeLayerDialog( ) {
dialogLayer.layer.style.display = "none"
dialogLayer.visible = false;
}
//**************************
// End Layer Dialog Code
//**************************/
The layerDialog.js library begins by loading the
external style sheet file matching the user's
operating system. The getCurrOSUI( ) function uses
browser operating system detection to determine which of the five
supported styles applies to the current browser. Then the function
dynamically writes the <link> tag with the
desired URL. The code for the five .css files
can be found in Examples 6-4 through 6-8 later in this recipe.
The library continues by defining a global variable that acts as an
abstract object holding various pieces of information about the
actual pseudowindow. A utility function adapted from Recipe 13.7
(centerOnWindow( )) centers the layer in the
current browser window when asked to by the function that prepares
the window for display. This application of the recipe, however,
removes the final show( ) function call (to the
DHTML API) because another function of the library controls the
pseudowindow's visibility.
A brief initialization routine in initLayerDialog(
) runs just after the page loads so that the correct close
box art is downloaded at the outset. The
dialogLayer global object also is assigned a
reference to the layer for a shortcut reference in functions that
operate on the layer.
The main function of this library, openLayerDialog(
), is invoked by your scripts when they need to display the
pseudowindow. This function shares many arguments with the functions
in Recipe 6.9 that are used for simulating a modal dialog. However,
the dimensions of the window are not needed here, since the
pseudowindow is a fixed size. One final function,
closeLayerDialog( ), is invoked when the user
clicks on the pseudowindow's close box.
If you're wondering why the
openLayerDialog( ) function includes code that
modifies both the style.display and
style.visibility properties to show the layer, it
is a result of a rendering bug in IE 6 for Windows. Using only the
style.visibility property to hide the pseudowindow
can leave the rectangular area of the iframe
completely blank (white) after the layer hides itself. Using the
style.display settings takes care of the bug. But
this introduces a different problem with positioning the layer prior
to showing it. The odd combination of turning the
layer's visibility to hidden and
the display to block before centering the window
does the trick. Then it's safe to use the
visibility property to present the layer to the
user.
Because this pseudowindow does not pick up the title from the
document loaded into the iframe (security
restrictions prevent such actions if the pseudowindow content is
served from a different domain and server), you must pass the desired
window titlebar text as the parameter to the
openLayerDialog( ) function. The third parameter
is a reference to a function that you want to invoke when the user
clicks on an OK or Apply button in the window—very much like
the function passed to the simulated dialog window in Recipe 6.9.
This reference is also available as a property of the global
dialogLayer object. The same is true for the
fourth parameter of openLayerDialog( ), which you
can use to pass data to the pseudowindow. Be aware, however, that the
document in the pseudowindow's
iframe won't be able to access
the dialogLayer object if the content arrives from
another domain and server. To display a new document in the
pseudowindow without passing any function reference or arguments,
supply a null parameter, as in:
openLayerDialog("prefs.html", "User Preferences", null, null);
Example 6-3 shows the
layerDialogDrag.js library, which provides support for
dragging the pseudowindow around by the element standing in for the
titlebar.
Example 6-3. layerDialogDrag.js library
// Global holds reference to selected element
var selectedObj;
// Globals hold location of click relative to element
var offsetX, offsetY;
// Set global reference to element being engaged and dragged
function setSelectedElem(evt) {
var target = (evt.target) ? evt.target : evt.srcElement;
var divID = (target.id = = "titlebar") ? "pseudoWindow" : "";
if (divID) {
if (document.layers) {
selectedObj = document.layers[divID];
} else if (document.all) {
selectedObj = document.all(divID);
} else if (document.getElementById) {
selectedObj = document.getElementById(divID);
}
setZIndex(selectedObj, 100);
return;
}
selectedObj = null;
return;
}
// Turn selected element on
function engage(evt) {
evt = (evt) ? evt : event;
setSelectedElem(evt);
if (selectedObj) {
if (document.body && document.body.setCapture) {
// engage event capture in IE/Win
document.body.setCapture();
}
if (evt.pageX) {
offsetX = evt.pageX - ((typeof selectedObj.offsetLeft != "undefined") ?
selectedObj.offsetLeft : selectedObj.left);
offsetY = evt.pageY - ((selectedObj.offsetTop) ?
selectedObj.offsetTop : selectedObj.top);
} else if (typeof evt.clientX != "undefined") {
offsetX = evt.clientX - ((selectedObj.offsetLeft) ?
selectedObj.offsetLeft : 0);
offsetY = evt.clientY - ((selectedObj.offsetTop) ?
selectedObj.offsetTop : 0);
} else if (evt.offsetX || evt/offsetY) {
offsetX = evt.offsetX - ((evt.offsetX < -2) ?
0 : document.body.scrollLeft);
offsetX -= (document.body.parentElement &&
document.body.parentElement.scrollLeft) ?
document.body.parentElement.scrollLeft : 0
offsetY = evt.offsetY - ((evt.offsetY < -2) ?
0 : document.body.scrollTop);
offsetY -= (document.body.parentElement &&
document.body.parentElement.scrollTop) ?
document.body.parentElement.scrollTop : 0
}
evt.cancelBubble = true;
return false;
}
}
// Drag an element
function dragIt(evt) {
evt = (evt) ? evt : event;
if (selectedObj) {
if (evt.pageX) {
shiftTo(selectedObj, (evt.pageX - offsetX), (evt.pageY - offsetY));
} else if (evt.clientX || evt.clientY) {
shiftTo(selectedObj, (evt.clientX - offsetX), (evt.clientY - offsetY));
}
evt.cancelBubble = true;
return false;
}
}
// Turn selected element off
function release(evt) {
if (selectedObj) {
setZIndex(selectedObj, 0)
if (document.body && document.body.releaseCapture) {
// stop event capture in IE/Win
document.body.releaseCapture();
}
selectedObj = null;
}
}
function blockEvents(evt) {
evt = (evt) ? evt : event;
evt.cancelBubble = true;
return false;
}
// Assign event handlers used by both Navigator and IE
function initDrag( ) {
if (document.layers) {
// turn on event capture for these events in NN4 event model
document.captureEvents(Event.MOUSEDOWN | Event.MOUSEMOVE | Event.MOUSEUP);
return;
} else if (document.body & document.body.addEventListener) {
// turn on event capture for these events in W3C DOM event model
document.addEventListener("mousedown", engage, true);
document.addEventListener("mousemove", dragIt, true);
document.addEventListener("mouseup", release, true);
return;
}
document.onmousedown = engage;
document.onmousemove = dragIt;
document.onmouseup = release;
return;
}
The layerDialogDrag.js library is identical to
the element-dragging library from Recipe 13.11, with one small
modification. The setSelectedElement( ) function
needs to acknowledge events only from the titlebar
layer, but it must set the draggable layer to be the outer
pseudoWindow layer. Thus, when the user drags the
titlebar, the entire pseudowindow div element
moves. The rest of Recipe 13.11 works as-is in this library,
including the event handler definitions.
You may experience a cosmetic annoyance in some circumstances,
however. If the content of the pseudowindow has form controls,
browsers running on slow computers don't refresh the
screen promptly, leading to temporary ghosts while the pseudowindow
is dragged. You'll have to evaluate how troubling
this might be to your users based on the types of pages you load into
the iframe.
Let's now come back to the issue of style sheets and
their influence on the pseudo-window elements. When style sheets are
applied to the basic HTML of the pseudoWindow div
element, you get a range of looks, as shown in Figure 6-1.
One of the stated goals of this application was to have the
pseudowindow blend into the operating system look and feel as much as
possible. It's no longer a simple task, thanks to
radically revamped user interfaces for Windows XP and Mac OS X. The
example shown in Figure 6-1 tries to match the look
and feel of four different operating system versions. To that end, a
set of five separate external style sheets are created to handle the
specific art files and sizes to come close to simulating the native
look and feel of the host operating system. This assumes, of course,
that you have available to you the art files for titlebar backgrounds
(just small vertical slices needed because the backgrounds repeat to
fit the space) as well as close boxes. So you can compare the subtle
differences needed between the versions, we'll begin
with
dialogLayer_win9xQ.css
(shown in Example 6-4), which is the style sheet
used for backward-compatible IE versions (IE 5, 5.5, and 6 running in
quirks mode).
Example 6-4. dialogLayer_win9xQ.css for backward-compatible IE/Windows versions
#pseudoWindow {position:absolute;
top:0px;
left:0px;
width:600px;
height:502px;
border:2px solid black;
background-color:#ffffff;
border-top:3px solid #cccccc;
border-left:3px solid #cccccc;
border-right:3px solid #666666;
border-bottom:3px solid #666666;
display:none
}
#titlebar {position:absolute;
top:0px;
left:0px;
height:16px;
width:596px;
background-image:url(titlebar_win9x.jpg);
color:#ffffff;
border-bottom:2px solid #666666;
font-family:Tahoma;
font-size:8pt;
font-weight:bold;
padding:2px;
text-align:left
}
#closebox {position:absolute;
right:0px;
top:1px
}
#barTitle {padding-left:3px}
#contentFrame{position:absolute;
top:19px;
left:0px;
height:477px;
width:594px;
background-color:#ffffff;
margin-left:0px;
margin-top:0px;
overflow:visible
}
Only minor differences in dimensions accrue to the CSS and Windows
9x-compatible version of the style sheet:
dialogLayer_win9x.css
(shown in Example 6-5). This variation is needed to
account for the different ways that CSS-compatible browsers measure
element widths when borders, margins, and padding are involved. The
titlebar background art file is the same for both Windows
9x versions, as is the entire look of the
pseudowindow.
Example 6-5. dialogLayer_win9x.css CSS-compatible stylesheet for Windows 9x
#pseudoWindow {position:absolute;
top:0px;
left:0px;
width:600px;
height:502px;
border:2px solid black;
background-color:#ffffff;
border-top:3px solid #cccccc;
border-left:3px solid #cccccc;
border-right:3px solid #666666;
border-bottom:3px solid #666666;
display:none
}
#titlebar {position:absolute;
top:0px;
left:0px;
height:16px;
width:596px;
background-image:url(titlebar_win9x.jpg);
color:#ffffff;
border-bottom:2px solid #666666;
font-family:Tahoma;
font-size:8pt;
font-weight:bold;
padding:2px;
text-align:left
}
#closebox {position:absolute;
right:0px;
top:1px
}
#barTitle {padding-left:3px}
#contentFrame{position:absolute;
top:22px;
left:0px;
height:480px;
width:600px;
background-color:#ffffff;
margin-left:0px;
margin-top:0px;
overflow:visible
}
The Windows XP version,
dialogLayer_winxp.css
(shown in Example 6-6), adjusts its dimensions to
accommodate a thicker titlebar.
Example 6-6. dialogLayer_winxp.css for browsers running in Windows XP
#pseudoWindow {position:absolute;
top:0px;
left:0px;
width:600px;
height:502px;
border:2px solid black;
background-color:#ffffff;
border-top:3px solid #cccccc;
border-left:3px solid #cccccc;
border-right:3px solid #666666;
border-bottom:3px solid #666666;
display:none
}
#titlebar {position:absolute;
top:0px;
left:0px;
height:26px;
width:596px;
background-image:url(titlebar_winxp.jpg);
color:#ffffff;
border-bottom:2px solid #666666;
font-family:Tahoma;
font-size:10pt;
font-weight:bold;
padding:2px;
text-align:left
}
#closebox {position:absolute;
right:1px;
top:1px
}
#barTitle {padding-left:3px}
#contentFrame{position:absolute;
top:30px;
left:0px;
height:472px;
width:600px;
background-color:#ffffff;
margin-left:0px;
margin-top:0px;
overflow:visible
}
For the Macintosh version prior to Mac OS X,
dialogLayer_macos9.css
(shown in Example 6-7), the close box is on the left
side of the titlebar, and minor dimensional differences are needed
for the user interface elements.
Example 6-7. dialogLayer_macos9.css for Macintosh browsers in OS 9 and earlier
#pseudoWindow {position:absolute;
top:0px;
left:0px;
width:600px;
height:502px;
border:2px solid black;
background-color:#ffffff;
border-top:3px solid #cccccc;
border-left:3px solid #cccccc;
border-right:3px solid #666666;
border-bottom:3px solid #666666;
display:none
}
#titlebar {position:absolute;
top:0px;
left:0px;
height:16px;
width:596px;
background-image:url(titlebar_macos9.jpg);
color:#000000;
border-bottom:2px solid #666666;
font-family:Charcoal;
font-size:9pt;
font-weight:normal;
padding:2px;
text-align:center
}
#closebox {position:absolute;
left:0px;
top:1px;
padding-right:3px
}
#barTitle {padding-right:6px;
background-color:#cccccc;
padding-left:6px
}
#contentFrame{position:absolute;
top:22px;
left:0px;
height:480px;
width:600px;
background-color:#ffffff;
margin-left:0px;
margin-top:0px;
overflow:visible
}
The Mac OS X version,
dialogLayer_macosX.css
(shown in Example 6-8), has its own dimensional
details.
Example 6-8. dialogLayer_macosX.css for Macintosh browsers in Mac OS X
#pseudoWindow {position:absolute;
top:0px;
left:0px;
width:600px;
height:502px;
border:2px solid black;
background-color:#ffffff;
border-top:3px solid #cccccc;
border-left:3px solid #cccccc;
border-right:3px solid #666666;
border-bottom:3px solid #666666;
display:none
}
#titlebar {position:absolute;
top:0px;
left:0px;
height:16px;
width:596px;
background-image:url(titlebar_macosX.jpg);
color:#000000;
border-bottom:2px solid #666666;
font-family:Charcoal;
font-size:9pt;
font-weight:normal;
padding:2px;
text-align:center
}
#closebox {position:absolute;
left:0px;
top:0px;
padding-left:5px
}
#barTitle {padding-right:6px
background-color:transparent
padding-left:6px
}
#contentFrame{position:absolute;
top:22px;
left:0px;
height:480px;
width:600px;
background-color:#ffffff;
margin-left:0px;
margin-top:0px;
overflow:visible
}
Perhaps the most significant advantage of using a positioned element
as a pseudowindow is that no extraneous pop-up windows are spawned.
Not only do some pop-up ad blockers prevent window.open(
) from working for some users, but multiple windows can get
lost behind others, causing confusion among casual web surfers.
Another, more philosophical, advantage is that the W3C DOM Level 2
pays only slight recognition to the window as an object. XHTML
specifications recommend against the usage of multiple frames or
windows (target attributes of
form or a elements are not
valid in strict XHTML 1.0, for example). Keeping everything in one
browser window appeals to one-window designers.
At the same time, however, using a layer to simulate a window
presents disadvantages over and above the dragging issue noted
earlier. The foremost concern is that a layer is confined to the
window or frame boundaries in which it resides. Unlike a true window,
the user cannot drag the layer to a position such that it extends
beyond the main window or frame edges. If the main
window's content is scrollable, the layer moves with
the page when the user scrolls the main window while the pseudowindow
is visible.
Although the example shown here is for a fixed-size pseudowindow, you
could expand upon the existing code to let the
openLayerDialog(
) function receive and operate on two
more parameters that specify the layer size. This gets more complex
in a version that needs to support the same range of operating-system
user interfaces, as this example does. If you compare the style sheet
settings that control div element widths and some
div heights and top measures, you can see that you
must account for a wide variety of tweaks for a number of UIs. For
example, the height, width, and
top style sheet properties for the
iframe vary between the CSS- and
backward-compatible versions of the Windows 9x
specifications. If you are prepared to uncover the factors affecting
various elements in this pseudowindow, as well as apply them to a
fresh window size passed as parameters to openLayerDialog(
), you can make the function more malleable than shown
here. The ideal scenario is deployment on an intranet where the
browsers and operating systems that need supporting are strictly
limited to a tolerable handful (or one!).
Another expansion on the pseudowindow is to fashion the equivalent of
a modal window, where the user is blocked from clicking on underlying
links or form controls. You can accomplish this by wrapping the
current pseudoWindow div inside
yet another div whose background is a transparent
image. The tricky part is sizing the outer wrapper to the dimensions
of the document so that it doesn't extend to such an
arbitrarily large size that the browser window's
scrollbars let users scroll the page into blank space. You must then
position the pseudoWindow div
within the outer wrapper, taking the page scrolling into account.
Trap all events in the outer div, and assign the
dragging event handlers to that div as well
(instead of the base document). The more browser types you want to
support for this kind of feature, the greater the challenge. But
it's doable if you are persistent and patient.
6.10.4 See Also
Recipe 6.8 for proprietary IE modal and modeless dialog windows;
Recipe 6.9 for a cross-browser simulated modal window using a
subwindow; Recipe 11.5 for importing OS-specific style sheets; Recipe 13.3 for details of the DHTML API library; Recipe 13.7 for centering
an element in a window; Recipe 13.11 for creating a draggable
element; Recipe 14.14 for dynamically replacing a portion of body
content.
|