Pure JavaScript Internal Windows

This page demonstrates JavaScript internal windows (also known as true or pure JavaScript windows). These are objects that look and behave like normal windows, but are implemented entirely in HTML and JavaScript. The implementation on this page had been tested in the Internet Explorer, Netscape Mozilla browsers. To keep to code relatively simple, it uses a Microsoft-style window — I apologise to Linux users.

This page serves as an example of what can be done with JavaScript, but just because something can be done doesn't mean it should be done — users without JavaScript cannot access the information, and tables are used for layout against the principles of the HTML specification. The demonstration can however be regarded as a masterclass in the implementation of complex JavaScript programs — the code is clear and divided into small functional units, and can cope with the incompatibilities of the different versions of Internet Explorer, Netscape and Mozilla without messy browser detection code. It also shows how to access mouse co-ordinates and scrollbar positions and detect mouse up events outside of the browser window in various browsers — common problems for those new to DHTML.

To see the code in action, look at the Pure JavaScript Internal Windows Demonstration.

The HTML

The HTML used for the window is remarkably concise — it consists almost entirely of tags with no content:

<body onMouseDown="checkSelection();"
      onMouseUp="deselectAll();"
      onMouseMove="mouseMoved(event.pageX,event.pageY,event.clientX,event.clientY);">
  <div id="internalWindow"><table cellspacing="0">
    <tr><td><table cellspacing="0"><tr>
      <td id="title" onMouseDown="beginMove();"></td>
      <td id="top" onMouseDown="beginMove();"></td>
      <td id="close"
          onMouseDown="mouseDownClose();"
          onMouseOver="mouseOverClose();"
          onMouseOut="mouseOutClose();"
          onMouseUp="mouseUpClose();"></td>
    </tr></table></td></tr>
    <tr><td><table cellspacing="0"><tr>
      <td id="left"></td>
      <td><div id="imagePane"><div></div></div></td>
      <td id="right"></td>
    </tr></table></td></tr>
    <tr><td><table cellspacing="0"><tr>
      <td id="bottomLeft"></td>
      <td id="bottom"></td>
      <td id="resize" onMouseDown="beginResize();"></td>
    </tr></table></td></tr>
  </table></div>
  <form><div style="text-align:center;">
    <button type="button" onClick="displayWindow();">
      Load Safalra's JavaScript Picture Viewer demo
    </button>
  </div></form>
</body>

The intrinsic events (onMouseDown, onMouseUp and so on) call various functions to handle interaction with the window. All formatting is done by Cascading Style Sheets through the ‘id’ attributes of each element, which is discribed in more detail below.

The Stylesheet

<style type="text/css">
  td{
    padding:0;
  }

This avoids having to set cellpadding="0" on each table cell.

  #internalWindow{
    display:none;
    background:url('windowgraphics/transparent.gif');
    padding:48px;
    position:absolute;
    left:48px;
    top:48px;
  }

This sets the properties for the <div> containing the window. Note the use of a transparent GIF border — this stops parts of the document's text being selected when you move or resize the window — a transparent GIF is needed as Internet Explorer incorrectly handles transparent backgrounds and transparent PNGs.

  #title{
    background:url('windowgraphics/title.png');
    width:221px;
    height:26px;
  }

  #top{
    background:url('windowgraphics/top.png') repeat-x;
    width:267px;
    height:26px;
  }

  #close{
    background:url('windowgraphics/close.png');
    width:24px;
    height:26px;
  }

This sets the properties of the top section of the window. When the window is resized, the width of the #top element is changed accordingly.

  #left{
    background:url('windowgraphics/left.png') repeat-y;
    width:6px;
    height:206px;
  }

  #imagePane{
    background-color:white;
    width:500px;
    height:206px;
    overflow:scroll;
  }

  #imagePane div{
    background:url('windowgraphics/snow.jpg');
    width:640px;
    height:384px;
  }

  #right{
    background:url('windowgraphics/right.png') repeat-y;
    width:6px;
    height:206px;
  }

This sets the properties of the middle section of the window. When the window is resized, the heights of the #top, #imagePane and #right elements are changed accordingly. Note how the photograph is displayed — as the background to a <div> of the appropriate size, inside a viewing <div> with scrollbars. This avoids the image being treated like a normal HTML image, which causes problems in Mozilla.

  #bottomLeft{
    background:url('windowgraphics/bottomleft.png');
    width:4px;
    height:24px;
  }

  #bottom{
    background:url('windowgraphics/bottom.png') repeat-x;
    width:486px;
    height:24px;
  }

  #resize{
    background:url('windowgraphics/resize.png');
    width:22px;
    height:24px;
    cursor:se-resize;
  }
</style>

This sets the properties of the bottom section of the window. When the window is resized, the width of the #bottom element is changed accordingly. Note the different cursor used on the resize gadget, for extra authenticity.

The JavaScript

The JavaScript consists of 19 global variables, 17 short functions, and one command that starts the window working. Note that there are no local variables, loops, or messy browser detection code.

<script type="text/javascript">
  // Window x co-ordinate (relative to the viewport)
  windowX=48;

  // Window y co-ordinate (relative to the viewport)
  windowY=48;

  // Window width
  windowWidth=512;

  // Window height
  windowHeight=256;

  // Window padding
  windowPadding=48;

  // Window offset
  offset=0;

  // Speed of change in window offset
  offsetSpeed=0;

  // Vertical scrollbar position
  scrollbarY=0;

  // Mouse x co-ordinate (relative to the viewport)
  mouseX=0;

  // Mouse y co-ordinate (relative to the viewport)
  mouseY=0;

These variables store various properties of the window and user interface. The value of ‘offset’ is changed by the ‘offsetSpeed’ variable in order to make its value smoothly approach the value of ‘scrollbarY’ — this allows the window to move smoothly when the user scrolls. Note that the values of position variables are relative to the viewport — this greatly simplifies the code.

  // Whether the window is currently visible
  windowVisible=false;

  // Whether the window is currently selected
  windowSelected=true;

  // Whether the window is currently being moved
  beingMoved=false;

  // Mouse x co-ordinate (relative to left of window) when move began
  moveMouseX=0;

  // Mouse y co-ordinate (relative to top of window) when move began
  moveMouseY=0;

  // Whether the window is currently being resized
  beingResized=false;

  // Mouse x co-ordinate (relative to right of window) when resize began
  resizeMouseX=0;

  // Mouse y co-ordinate (relative to bottom of window) when resize began
  resizeMouseY=0;

  // Whether close has been pressed
  closePressed=false;

These variables store various properties relating to the user’s current interaction with the window.

  // Begins the move process, by finding the mouse position
  function beginMove(){
    beingMoved=true;
    moveMouseX=mouseX-windowX;
    moveMouseY=mouseY-windowY;
  }

  // Begins the resize process, by finding the mouse position
  function beginResize(){
    beingResized=true;
    resizeMouseX=windowX+windowWidth-mouseX;
    resizeMouseY=windowY+windowHeight-mouseY;
  }

These two functions are called when the user starts to move or resize the window, respectively. To function correctly, the program needs to know where the mouse was relative to the appropriate corner of the window when the action was started.

    // Called when the mouse is pressed down on the close button
  function mouseDownClose(){
    closePressed=true;
    document.getElementById('close').style.background=
        'url(\'windowgraphics/close_down.png\')';
  }

  // Called when the mouse moves over the close button
  function mouseOverClose(){
    if (closePressed){
      document.getElementById('close').style.background=
          'url(\'windowgraphics/close_down.png\')';
    }
  }

  // Called when the mouse moves off the close button
  function mouseOutClose(){
    if (closePressed){
      document.getElementById('close').style.background=
          'url(\'windowgraphics/close.png\')';
    }
  }

  // Called when the mouse is released on the close button
  function mouseUpClose(){
    if (closePressed){
      document.getElementById('internalWindow').style.display='none';
      windowVisible=false;
      closePressed=false;
      document.getElementById('close').style.background=
          'url(\'windowgraphics/close.png\')';
    }
  }

These four functions handle the close button. The close button should only been shown in the ‘down’ position if the mouse is over it, the mouse button is down, and the mouse button went down when over the close button. The window should only be closed if the mouse is released on the close button after the mouse had been pressed down on the close button. The ‘closePressed’ global variable stores the necessary information. Note that the status of ‘closePressed’ must be checked when the mouse moves off the close button, as the window may not currently be selected (if ‘closePressed’ is true, the window must be selected).

  // Displays the window
  function displayWindow(){
    document.getElementById('internalWindow').style.display='block';
    windowVisible=true;
  }

This function displays the window. Note that the CSS ‘display’ property is used rather than ‘visibility’, as this has wider support.

  // Checks whether the window should be selected or deselected
  function checkSelection(){
    if ((mouseX>=windowX+windowPadding)&&
        (mouseX<windowX+windowWidth+windowPadding)&&
        (mouseY>=windowY+windowPadding)&&
        (mouseY<windowY+windowHeight+windowPadding)){
      if (!windowSelected) selectWindow();
    }else{
      if (windowVisible) deselectWindow();
    }
  }

  // Selects the window
  function selectWindow(){
    windowSelected=true;
    document.getElementById('title').style.background=
        'url(\'windowgraphics/title.png\')';
    document.getElementById('top').style.background=
        'url(\'windowgraphics/top.png\')';
    if (!closePressed){
      document.getElementById('close').style.background=
          'url(\'windowgraphics/close.png\')';
    }
  }

  // Deselects the window
  function deselectWindow(){
    windowSelected=false;
    document.getElementById('title').style.background=
        'url(\'windowgraphics/title_unselected.png\')';
    document.getElementById('top').style.background=
        'url(\'windowgraphics/top_unselected.png\')';
    document.getElementById('close').style.background=
        'url(\'windowgraphics/close_unselected.png\')';
  }

These three functions control selection and deselection of the window — here the advantage of using co-ordinates relative to the viewport is clear. It would be possible to combine the latter two functions by use of the trinary operator (condition?result1:result2), but the code would be less readable. Note the different conditions in the ‘checkSelection()’ function — the window should be reselected if it already is selected, as this would reset the status of the close button, and should not be deselected if it is not currently visible.

  // Deselectes all icons when the mouse button is released
  function deselectAll(){
    beingMoved=false;
    beingResized=false;
    closePressed=false;
  }

When the mouse button is released, all actions should stop, even if the mouse is released when off the window. This is achieved by captured mouse up events on the body element.

  // Updates the variables storing the mouse co-ordinates
  function mouseMoved(pageX,pageY,clientX,clientY){
    if (pageX){
      mouseX=pageX;
      mouseY=pageY-scrollbarY;
    }else if (clientX){
      mouseX=clientX;
      mouseY=clientY;
    }
  }

This function, called whenever the mouse moves, maintains the current mouse position. Using an event is the only way to capture the mouse co-ordinates. Some browsers implement pageX and pageY, others clientX and clientY — Mozilla implements both. Page co-ordinates are relative to the top of the page, not the viewport, so as we are using relative co-ordinates we need to correct for this by subtracting the scrollbar position.

  // Updates the variable storing the vertical scrollbar co-ordinate
  function updateVerticalScrollbar(){
    if (window.pageYOffset){
      scrollbarY=window.pageYOffset;
    }else if (document.documentElement && document.documentElement.scrollTop){
      scrollbarY=document.documentElement.scrollTop;
    }else if (document.body){
      scrollbarY=document.body.scrollTop;
    }
  }

This function calculates the vertical scrollbar position. Browsers store this in one of three places — Internet Explorer even uses a different place depending on whether it is in standards mode or quirks mode.

  // Updates the window offset and moves the window accordingly
  function updateWindowOffset(){
    if (offset>scrollbarY){
      if (offset-scrollbarY<-offsetSpeed*(1-offsetSpeed)/2) offsetSpeed++;
      if (offset-scrollbarY>(1-offsetSpeed)*(2-offsetSpeed)/2) offsetSpeed--;
    }
    if (offset<scrollbarY){
      if (scrollbarY-offset<offsetSpeed*(offsetSpeed+1)/2) offsetSpeed--;
      if (scrollbarY-offset>(offsetSpeed+1)*(offsetSpeed+2)/2) offsetSpeed++;
    }
    if (offset==scrollbarY) offsetSpeed=0;
    offset+=offsetSpeed;
    document.getElementById('internalWindow').style.top=windowY+offset+'px';
  }

This functions controls the smooth movement of the window when the user scrolls. The mathematics here is a little tricky — it helps to introduce the concept of a minimum stopping distance (MSD). This is the distance that the window would travel if it decelerated to a stop as quickly as possible. If the distance to the target is less than what the MSD would be if the window didn’t decelerate now, it decelerates. If the distance to the target is greater than what the MSD would be if the window accelerated now, it accelerates. Remember that signs have to be reversed if the window is travelling upwards.

  // Moves the window
  function updateWindowPosition(){
    windowX=mouseX-moveMouseX;
    windowY=mouseY-moveMouseY;
    document.getElementById('internalWindow').style.left=windowX+'px';
    document.getElementById('internalWindow').style.top=windowY+offset+'px';
  }

  // Resizes the window
  function updateWindowSize(){
    windowWidth=Math.min(668,Math.max(256,mouseX-windowX+resizeMouseX));
    windowHeight=Math.min(450,Math.max(128,mouseY-windowY+resizeMouseY));
    document.getElementById('top').style.width=windowWidth-245+'px';
    document.getElementById('left').style.height=windowHeight-50+'px';
    document.getElementById('imagePane').style.width=windowWidth-12+'px';
    document.getElementById('imagePane').style.height=windowHeight-50+'px';
    document.getElementById('right').style.height=windowHeight-50+'px';
    document.getElementById('bottom').style.width=windowWidth-26+'px';
  }

These two functions move and resize the window, respectively. Note the use of the ‘Math.min’ and ‘Math.max’ in the ‘updateWindowSize()’ function — these set minimum and maximum sizes for the window, without requiring extra ‘if’ tests.

  // Updates the position and size of the window
  function updateWindow() {
    updateVerticalScrollbar();
    updateWindowOffset();
    if (beingMoved) updateWindowPosition();
    if (beingResized) updateWindowSize();
  }

  // Interval to update the window
  window.setInterval('updateWindow();',20);

This simple function and command control the update of the window. Fifty times a second the position of the scrollbar is recorded, and the window is moved and resized if necessary.

Writing DHTML for many browsers

Writing DHTML that works with a range of browsers is not easy, as their implementations of the standard (ECMA-262) differ. The JavaScript Developer’s Dictionary details how different browsers respond to JavaScript, and is very helpful in writing cross-browser DHTML.

This article was last edited on 15th March 2008. The author can be contacted using the form below.