Yet Another Attempt at Modal Dialogs in CSS/jQuery.

Introduction.

I was noodling with CSS and Javascript while our Internet connection happened to be down trying to come up with a scheme that would improve the pseudo-modal dialogs that I was using in my current project. The result seems quite robust, and looks decent on IE8, and the current (or close to) versions of Firefox, Chrome, and Opera. So as usual I thought I had better make some notes so I would continue to understand how it works.

The CSS Basis

I had never used the CSS position:fixed attribute before - can't think why, because I know I have attempted this sort of thing in the past. Reading up on it, it seemed that it should do the job. I settled on two styles, both using fixed:

#shield {
   position:fixed;
   display:none;
   background: black;
   opacity: 0.5; filter: alpha(opacity=50);  /* Cover IE as well as standards conforming browsers */
   width:3000px; height:10000px;
   top:0px; left:0px;
   z-index:9999;
}
.dialog {
   position:fixed;
   display:none;
   border:solid 1px black; border-radius:10px;
   text-align:left;
   background: white;
   z-index:10000;
}
A single 'shield' DIV sits immediately under any dialogs in the z-order. The width and height of this should be sufficient to cover any area that your application might reasonably occupy. It is set up to bounce off mouse events, and to provide a dimming of the page content behind any dialog that is up. The 'dialog' class is applied to container DIVs that typically will contain FORM type elements. The beautyof 'fixed', is that you can scroll the page underneath around to remind yourself of what you might need to be entering in the dialog, while the dialog remains at a fixed position relative to your browser window. Note that the semi-opaque layer is required in IE, otherwise clicks and such will penetrate it (although that layer can actually have an opacity value that makes it completely transparent.)

The Javscript

At document-ready time, any dialogs that you have defined must be positioned so they are more or less centered in the window. With the simple approach I'm taking here I assume that they are roughly 300px by 300px. A dialog which is that size will be centered - others will probably be close enough. I'm sure it would not be a big deal to do more sophisticated centering.

At the same time we need to bind the elements that will invoke the dialogs to their dialog, and to bind the elements within the dialog that will close it. The code is surprisingly compact:


function fixupDialogs(posOnly)
{
   // The posOnly argument allows us to use this function to reposition the dialogs when the window size changes
   // or to do the whole job at document-ready.

   // Nested functions here mirror the structure of elements that we use.
   function positionDialog()
   {
      // 'this' here is the DOM element that was labelled with class 'dialog'
      var cd = $(this);    // Get a jQuery object representing the current dialog

      // This function fixes up the elements within the dialog that can close it
      function fixupClose()
      {
         var elem = $(this);
         elem.click(function() { cd.hide(); $('#shield').hide(); });
      }

      // Work out the positioning relative to the window
      var ww = $(window).width();
      var x = ww/2-150;    // Like I said, I've assumed that they are of the order of 300x300
      var wh = $(window).height();
      var y = wh/2-150;

      // Position the dialog
      cd.css("left", ""+x+"px"). css("top", ""+y+"px");

      // Fixup any elements within the dialog that are marked as class 'dlgclose'
      if (!posOnly)
         cd.find('.dlgclose').each(fixupClose);
   }

   // This function fixes up the elements outside the dialog that can invoke it.
   function fixupOpener()
   {
      var cb = $(this);
      // Pick up the ID of the associated dialog - you could use 'alt' instead of 'name'
      var assoc = cb.attr('name');
      cb.click(function() { $('#shield').show(); $('#'+assoc).show(); });
   }

   // Now we do each for classes 'dlgopen' and 'dialog'
   // While we're at it - if we're at document-ready, we can set up the shield and
   // the 'dlgopen' elements
   $('.dialog').each(positionDialog);
   if (!posOnly)
   {
      $('#shield').click(function(e) { e.stopPropagation(); return false; });
      $('#shield').mousemove(function(e) { e.stopPropagation(); return false; });
      $('.dlgopen').each(fixupOpener);
   }
}

Relatively painless I think you will agree. I like nested functions for this sort of thing since they keep the operations well encapsulated.

A Demo HTML Page

You should be able to fathom out what's happening here from what has been described above.

<!DOCTYPE html>
<html>
<head>
<style>
   /* We've already covered this */
</style>
<script src="/script/jquery.js" type="text/javascript"/></script>
<script>
function fixupDialogs(posOnly)
{
   // We've already covered this
}

$(function() {
   fixupDialogs(false);
   $(window).resize(function() { fixupDialogs(true); });
   $('#clickme').click(function() { alert("clicked"); });
   $('#tb').mouseenter(function() { $('#tb').css("color", "red"); });
   $('#tb').mouseleave(function() { $('#tb').css("color", "black"); });
});
</script>
</head>
<body style="background-color:#eeeeee; text-align:center;">
<div id="shield"></div>
<div class="dialog" id="dlg1" style="width:300px; height:300px">
<button class="dlgclose" id="close1" name="dlg1">Close 1</button>
</div>
<div class="dialog" id="dlg2" style="width:300px; height:300px">
<button class="dlgclose" id="close2" name="dlg2">Close 2</button>
</div>
<div style="width:800px; height:3000px; margin: 0 auto; text-align:left; padding-top:80px;
       position:relative; background-color:#ffffff;">
<button class="dlgopen" id="open1" name="dlg1">Open 1</button><p>
<button class = "dlgopen" id="open2" name="dlg2">Open 2</button><p>
<input id="tb" type="text" value=""> <button id="clickme">Click Me</button><p>
Whatever else .....
</div>
</body>
</html>
There's a live demo here. When you find ways of breaking it let me know via the Discussion item in the top menu of this page.

I found it remarkably easy to plug this into the target page, though there were a fair number of tiying up deletions. Compared to the approach I had used before, this was much simpler.