Previous Section Next Section

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.

Figure 6-1. Pseudowindows of four operating system designs
figs/jsdc_0601.gif

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.

    Previous Section Next Section