Chapter 13. Working with Web Page Spaces

13.0. Introduction

The web page space is all of the area contained within the browser’s chrome: the outer edge of the browser, status, and menu bars. If the contents of the page are larger than the window area, vertical and horizontal scrollbars are added so you can scroll to see all of the page contents.

The web page size is determined by the page elements, in combination with the default and explicit styling for each. If there are no elements, or the elements don’t participate in the page flow (if they’re absolutely positioned, or in some other way removed from the flow), the area of the web page space is the size of the window minus the chrome.

Element sizes vary, based on their contents, but they can be resized or even clipped. If they are resized, setting the element’s overflow alters what happens to their content if it is larger than the element’s size. If the overflow is set to scroll, vertical and horizontal scrollbars are added to the element.

How the elements impact page flow is based on a number of factors. For instance, if the element is a block-level element, such as a div, header (h1), or paragraph (p), there’s a new line before and after the element. An inline element, though, such as a span, is not surrounded by line breaks. The display for both types of elements can be changed by setting the CSS display or float property.

An element’s positioning can also impact page flow. By default, elements have static positioning, where properties such as top and left have no impact. An element’s position can be changed through the CSS position property, and through the use of positional properties such as left and top—either changing the element’s position in the page, or removing it entirely out of the page flow.

All of the element properties just mentioned can be set in a stylesheet:

<style>
div#test
{
   position: absolute;
   left: 10px;
   top: 10px;
}

In this chapter, we’re more interested in the many ways of using JavaScript to dynamically manage the web page’s space.

13.1. Determining the Area of the Web Page

Problem

You want to measure the width and height of the current web page window.

Solution

You’ll need to use a cross-browser technique that ensures consistent results regardless of the browser used. The following is an example of a function that returns an object with the web page viewport’s width and height:

function size()
{
  var wdth = 0;
  var hth = 0;

  if(!window.innerWidth)
  {
     wdth = (document.documentElement.clientWidth ?
document.documentElement.clientWidth :
document.body.clientWidth);
     hth = (document.documentElement.clientHeight ?
document.documentElement.clientWidth :
document.body.clientHeight);
  }
  else
  {
    wdth = window.innerWidth;
    hth = window.innerHeight;
  }
  return {width:wdth, height:hth};
}

Discussion

There is currently no standard approach to accessing the window viewport information, which is why we have to use a series of case statements.

Most major browsers, including Opera, Firefox, Chrome, and Safari, support window object properties called innerWidth and innerHeight, which return the window’s viewport area, minus any scrollbar dimensions. However, Internet Explorer doesn’t support innerWidth and innerHeight, which accounts for the first test case in the solution.

If the innerWidth property is supported, the width and height are accessed from innerWidth and innerHeight, respectively. If it isn’t supported, the ternary operator is used to test for a document property, documentView, and its property, clientWidth. If there is no documentView object, the clientWidth property is accessed on the document body property:

wdth = (document.documentElement.clientWidth ?
              document.documentElement.clientWidth :
              document.body.clientWidth);

IE provides viewport information through the documentView property except in IE6, when the application is run in quirks mode. Though support for IE6 is rapidly diminishing—major websites such as Google, YouTube, and Amazon no longer support it, and it isn’t one of the supported browsers in this book—I’ve included the test to be comprehensive.

Note

Quirks mode is a way for browsers to provide backward version support. It’s typically triggered with a special DOCTYPE, such as: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">.

After the tests, both the width and height values are assigned to appropriately named properties in a returned object, using JSON notation:

return {width:wdth, height: hth}

Once the viewport size object is returned, you can access the width and height using the following code:

var viewPort = size();
var w = viewPort.width;
var h = viewPort.height;

See Also

There is a draft for a CSS3 standards effort to define a CSSOM View module, in order to provide a standard set of CSS specifications for window viewports and other dimensional and view information. The current draft is at http://www.w3.org/TR/cssom-view/. However, the priority for this specification is low, and the cross-browser approach just demonstrated is widely adopted, so a standard approach is off in the distant future.

See Recipe 19.4 for more on the JSON notation.

13.2. Measuring Elements

Problem

You want to determine the current height of a web page element.

Solution

Call getBoundingClientRect for the element. If the returned TextRectangle/ClientRect object has a height property, use that. Otherwise, subtract the rectangle’s bottom value from its top to derive the height:

var height = 0;
var rect = document.getElementById("it").getBoundingClientRect();
if (rect.height) {
    height = rect.height;
} else {
    height = rect.bottom - rect.height; // derive height
}
alert(rect.height);

Discussion

The getBoundingClientRect method is based on a method that Microsoft implemented for IE5, and is now being standardized in the W3C CSSOM View module. The W3C CSSOM View module specification provides a standardized way of getting information about the web page viewport, and the element’s spatial arrangement within the viewport.

The element.getBoundingClientRect method returns a ClientRect object (TextRectangle in implementations) that contains information about the bounding rectangle for the element. Most implementations support four properties on the object: top, bottom, right, and left. Firefox also includes width and height, though both can be derived from the other values.

When I mention the bounding rectangle for the element, the dimensions returned include any padding and border values. If an element has the following stylesheet setting:

#elem
{
   height: 400px;
 }

and you access the bounding rectangle’s height, it is 400 pixels. However, if the element has the following stylesheet:

#elem
{
  height: 400px;
  padding: 10px;
  border: 5px solid red;
  margin: 20px;
}

the bounding rectangle’s height would be 430 pixels: 400 for the height setting, plus 10 pixels each side for the padding, and 5 pixels each side for the border. The margin doesn’t figure into the calculation.

If you don’t provide any stylesheet setting, the height depends on the viewport size; changing the viewport will also change the element’s dimensions, including its height.

The values for each of the ClientRect/TextRectangle dimensions are floating-point numbers.

Note

Another impact on the element’s bounding rectangle position is if it’s in a foreignObject element in embedded SVG, in which case the top-left is relative to foreignObject’s container and other contents.

13.3. Locating Elements in the Page

Problem

You want to know the exact position of an element in the page.

Solution

Use getBoundingClientRect to get the dimensions and position of the bounding rectangle for the element, and then access its top and left values to find position:

function positionObject(obj) {
   var rect = obj.getBoundingClientRect();
   return [rect.left,rect.top];
}

Discussion

Element positioning is based on the position, or offset, of the element’s top-left corner relative to its viewport and ancestor elements. The position of the element is relative to other elements, and is dependent on whether its position is static (by default), relative, fixed, or absolute. The margin also affects the element’s position.

The element.getBoundingClientRect method returns the rectangle for the element, including the top, left, right, and bottom positions, regardless of stylesheet setting.

To demonstrate how the method can be used to find element positions, Example 13-1 contains a web page with three nested div elements and a separate div element acting as cursor. Each nested div element is labeled and outlined, and the separate element is a solid color. When the page loads, the user is prompted for a box label, and if the label matches one of the div elements, the solid cursor div element is positioned over the specified div element. The prompt/move continues until canceled.

Example 13-1. Moving elements over others based on location position and absolute positioning
<!DOCTYPE html>
<head>
<title>Locating Elements</title>
<style type="text/css">
div#a
{
   width: 500px;
}
div
{
  border: 1px solid #000;
  padding: 10px;
}
#cursor
{
  position: absolute;
  background-color: #ffff00;
  width: 20px;
  height: 20px;
  left: 50px;
  top: 300px;
}
</style>
<script type="text/javascript">

function positionObject(obj) {
   var rect = obj.getBoundingClientRect();
   return [rect.left,rect.top];
}
window.onload=function() {
   var tst = document.documentElement.getBoundingClientRect();
   alert(tst.top);
   var cont = "A";
   var cursor = document.getElementById("cursor");
   while (cont) {
      cont = prompt("Where do you want to move the cursor block?",
                         "A");
      if (cont) {
         cont=cont.toLowerCase();
         if (cont == "a" || cont == "b" || cont == "c") {
            var elem = document.getElementById(cont);
            var pos = positionObject(elem);
            cursor.setAttribute("style","top: " + pos[1] +
                                     "px; left : " + pos[0] + "px");
          }
       }
    }
}
</script>
</head>
<body>
  <div id="a">
    <p>A</p>
    <div id="b">
      <p>B</p>
      <div id="c">
         <p>C</p>
      </div>
     </div>
  </div>
  <div id="cursor"></div>
</body>

Figure 13-1 shows the page after I selected the “B” div element. The application works with all of this book’s target browsers.

Showing element position in action
Figure 13-1. Showing element position in action

An interesting little quirk with getBoundingClientRect and positioning is that Microsoft originally added a two-pixel margin around the document element, which affected the getBoundingClientRect values. If you test the method with the document element:

var tst = document.documentElement.getBoundingClientRect();
alert(tst.top);

You get a value of 0 for Firefox, Safari, Chrome, and Opera, but you get a value of 2 for IE7, and –2 for IE8. If you run the application in IE7 compatibility mode and re-position the cursor element using code that’s compatible for IE7 (IE7 doesn’t like the setAttribute technique when used with the style attribute):

var pos = positionObject(elem);
cursor.style.top = pos[1] + "px";
cursor.style.left = pos[0] + "px";

The cursor is exactly 2 pixels off—both from the top and the left. With IE8, Microsoft “regularized” the margin for the document element, offsetting it –2 pixels. Now, when you use getBoundingClientRect with any element in the page, IE8 returns the same results as the other browsers.

If you need to support IE7, you’ll need to adjust the getBoundingClientRect values accordingly, and also use the older approach of setting style values. The downloadable example code contains a workaround for this older browser.

See Also

See Recipe 12.15 for more on using setAttribute to change style settings.

13.4. Hiding Page Sections

Problem

You want to hide an existing page element and its children until needed.

Solution

You can set the CSS visibility property to hide and show the message:

msg.style.hidden="visible"; // to display
msg.style.hidden="hidden"; // to hide

or you can use the CSS display property:

msg.style.display="block"; // to display
msg.style.display="none"; // to remove from display

Discussion

Both the CSS visibility and display properties can be used to hide and show elements. There is one major difference between the two that impacts on which you’ll use.

The visibility property controls the element’s visual rendering, but its physical presence still affects other elements. When an element is hidden, it still takes up page space. The display property, on the other hand, removes the element completely from the page layout. The property can take several values, but there are four of interest to us:

none

When display is set to none, the element is removed completely from display.

block

When display is set to block, the element is treated like a block element, with a line break before and after.

inline-block

When display is set to inline-block, the contents are formatted like a block element, which is then flowed like inline content.

inherit

This is the default display, and specifies that the display property is inherited from the element’s parent.

There are other values, but these are the ones we’re most likely to use within JavaScript applications.

Unless you’re using absolute positioning with the hidden element, you’ll want to use the CSS display property. Otherwise, the element will affect the page layout, pushing any elements that follow down and to the right, depending on the type of hidden element.

There is another approach to removing an element out of page view, and that is to move it totally offscreen using a negative left value. This could work, especially if you’re creating a slider element that will slide in from the left. It’s also an approach that the accessibility community has suggested using when you have content that you want rendered by Assistive Technology (AT) devices, but not visually rendered.

To just hide an element, I generally use the hidden attribute, and to remove the element from the page display, I use the display attribute.

See Also

See Recipe 14.1 on accessible approaches to hiding and displaying page elements. See Recipe 13.4 for a demonstration of the display attribute.

13.5. Creating Collapsible Form Sections

Problem

You have a large form that takes up a lot of space. You only want to display sections of the form as they are needed.

Solution

Split the form into display blocks using div elements, and then change the block’s styling to control the display of the form section. When the page is loaded, hide all of the form blocks by changing the display value to none using JavaScript:

theformblock.setAttribute("style","display: none");

or:

theformblock.style.display="none";

To expand the section, change the display setting to block using setAttribute:

theformblock.setAttribute("style","block");

or set the value directly:

theformblock.style.display="block";

Discussion

There are multiple ways you can prevent form elements from taking up page space. For one, you can clip the element by setting the clipping area. Another approach is to resize the element to zero height. The best approach, though, and the one most applications use, is to employ a collapsible section.

A collapsible section is a form of widget—a set of elements, CSS, and JavaScript packaged together and generally considered one object. The typical implementation consists of one element that acts as a label that is always displayed, another element that holds the content, and all contained within a third, parent element.

The collapsible section may or may not be used with other collapsible sections to form a higher level widget, the accordion. The accordion widget is a grouping of collapsible sections with an additional behavior: depending on preference, any number of collapsible sections can be expanded, or only one section can be expanded at a time.

To demonstrate how collapsible sections can be used with forms, Example 13-2 shows a form that’s split into two sections. Notice that each form block has an associated label that expands the collapsed form section when clicked. When the label is clicked again, the form section is collapsed again.

Example 13-2. Collapsed form element
<!DOCTYPE html>
<head>
<title>Collapsed Form Elements</title>
<style>
.label
{
  width: 400px;
  margin: 10px 0 0 0;
  padding: 10px;
  background-color: #ccccff;
  text-align: center;
  border: 1px solid #ccccff;
}
.elements
{
  border: 1px solid #ccccff;
  padding: 10px;
  border: 1px solid #ccccff;
  width: 400px;
}
button
{
   margin: 20px;
}
</style>
</head>
<body>
<form>
  <div>
    <div id="section1" class="label">
      <p>Checkboxes</p>
    </div>
    <div id="section1b" class="elements">
      <input type="checkbox" name="box1" /> - box one<br />
      <input type="checkbox" name="box1" /> - box one<br />
      <input type="checkbox" name="box1" /> - box one<br />
      <input type="checkbox" name="box1" /> - box one<br />
      <input type="checkbox" name="box1" /> - box one<br />
     </div>
    </div>
  <div>

  <div id="section2" class="label">
    <p>Buttons</p>
  </div>
  <div class="elements">
    <input type="radio" name="button1" /> - button one<br />
    <input type="radio" name="button1" /> - button one<br />
    <input type="radio" name="button1" /> - button one<br />
    <input type="radio" name="button1" /> - button one<br />
    <input type="radio" name="button1" /> - button one<br />
    <button>Submit</button>
  </div>
</div>
</form>
<script type="text/javascript">

var elements = document.getElementsByTagName("div");

// collapse all sections
for (var i = 0; i < elements.length; i++) {
  if (elements[i].className == "elements") {
    elements[i].style.display="none";
  } else if (elements[i].className == "label") {
    elements[i].onclick=switchDisplay;
  }
}

//collapse or expand depending on state
function switchDisplay() {

  var parent = this.parentNode;
  var target = parent.getElementsByTagName("div")[1];

  if (target.style.display == "none") {
    target.style.display="block";
  } else {
    target.style.display="none";
  }
  return false;
}
</script>
</body>

There are numerous ways you can map the click activity in one element by changing the display in another. In Example 13-2, I wrapped both the label and the content elements in a parent element. When you click on a label, the parent to the label element is accessed in JavaScript and its children returned as an HTML collection. The second element’s display toggles—if the element’s display is none, it’s changed to block; if block, changed to none. Figure 13-2 shows the page, with one form section expanded.

Form split over collapsible accordion sections, with one section expanded
Figure 13-2. Form split over collapsible accordion sections, with one section expanded

In the example, notice that the form elements are displayed when the page loads, and only collapsed after the elements are loaded. The reason for this is if JavaScript is turned off, the form elements are displayed by default.

See Also

See Recipe 14.5 for how to make a collapsible section/accordion widget accessible with ARIA attributes.

13.6. Adding a Page Overlay

Problem

You want to overlay the web page in order to display a message, or provide an expanded photo.

Solution

Provide a stylesheet setting for a div element that is sized and positioned to cover the entire web page:

.overlay
{
   background-color: #000;
   opacity: .7;
   filter: alpha(opacity=70);
   position: absolute; top: 0; left: 0;
   width: 100%; height: 100%;
   z-index: 10;
}

Create the div element on demand, adding whatever other content is to be displayed to the div element:

function expandPhoto() {

   var overlay = document.createElement("div");
   overlay.setAttribute("id","overlay");
   overlay.setAttribute("class", "overlay");
   document.body.appendChild(overlay);
}

When the overlay is no longer needed, remove it from the page:

function restore() {
   document.body.removeChild(document.getElementById("overlay"));
}

Discussion

Creating an overlay in a web page consists of creating a div element set to a z-index higher than anything else in the page, absolutely positioned at the upper left of the page, and sized 100%.

In the solution, this is achieved more easily by created a CSS style setting for the overlay class that manages the appearance of the element, and then using document.createElement and appendChild to add it to the page. To restore the page, the overlay element is removed.

Page overlays are popular for displaying ads, logins, or providing important site messages. They are also useful with photos. Example 13-3 contains a web page with four photo thumbnails. Clicking any of the thumbnails opens an overlay, and displays a larger size photo.

Example 13-3. Creating an overlay for displaying a larger photo
<!DOCTYPE html>
<head>
<title>Overlay</title>
<style>
img
{
  padding: 5px;
}

#outer
{
  width: 100%; height: 100%;
}
.overlay
{
   background-color: #000;
   opacity: .7;
   filter: alpha(opacity=70);
   position: fixed; top: 0; left: 0;
   width: 100%; height: 100%;
   z-index: 10;
}
.overlayimg
{
  position: absolute;
  z-index: 11;
  left: 50px;
  top: 50px;
}
</style>
<script>

function expandPhoto() {

   // create overlay and append to page
   var overlay = document.createElement("div");
   overlay.setAttribute("id","overlay");
   overlay.setAttribute("class", "overlay");
   document.body.appendChild(overlay);

   // create image and append to page
   var img = document.createElement("img");
   img.setAttribute("id","img");
   img.src = this.getAttribute("data-larger");
   img.setAttribute("class","overlayimg");

   // click to restore page
   img.onclick=restore;

   document.body.appendChild(img);

}
// restore page to normal
function restore() {

 document.body.removeChild(document.getElementById("overlay"));
 document.body.removeChild(document.getElementById("img"));
}


window.onload=function() {
   var imgs = document.getElementsByTagName("img");
   imgs[0].focus();
   for (var i = 0; i < imgs.length; i++) {
     imgs[i].onclick=expandPhoto;
     imgs[i].onkeydown=expandPhoto;
   }
}

</script>

</head>
<body>
<div id="outer">
  <p>Mouse click on image to expand the photo. To close expanded
photo, mouse click on image.</p>
  <img src="dragonfly2.thumbnail.jpg" data-larger="dragonfly2.jpg"
alt="image of common dragonfly on bright green and pink flowers" />
  <img src="dragonfly4.thumbnail.jpg" data-larger="dragonfly4.jpg"
alt="Dark orange dragonfly on water lily" />
  <img src="dragonfly6.thumbnail.jpg" data-larger="dragonfly6.jpg"
alt="Dark orange dragonfly on purple water lily" />
  <img src="dragonfly8.thumbnail.jpg" data-larger="dragonfly8.jpg"
alt="Dragonfly on bright pink water lily" />
</div>
</body>

Example 13-3 creates an overlay that fits the size of the page as it’s currently opened. Note the CSS setting for the overlay, in particular the fixed positioning. This ensures that the overlay fits the window even if the contents require you to scroll to the right, or down, to see all of the contents.

Figure 13-3 shows the page with the overlay and one of the photos displayed.

Demonstration of an overlap and photo
Figure 13-3. Demonstration of an overlap and photo

The application works with Firefox, Opera, Chrome, Safari, and IE8. IE7 doesn’t like the use of setAttribute with the class attribute. If you need to support IE7, set the className attribute directly:

overlay.className = "overlay";

See Also

See Recipe 12.15 for more on using setAttribute with CSS styles.

13.7. Creating Tab Pages

Problem

You have divided content that you want to hide or display based on the web page reader’s actions.

Solution

Create a tabbed page effect and hide or display tab pages based on clicking the tab label:

// click on tab
function displayPage() {
  var current = this.parentNode.getAttribute("data-current");
  document.getElementById("tabnav_" + current).setAttribute("style",
                          "background-color: #fff");
  document.getElementById("tabpage_" + current).style.display="none";

  var ident = this.id.split("_")[1];
  this.setAttribute("style","background-color: #f00");
  document.getElementById("tabpage_" + ident).style.display="block";
  this.parentNode.setAttribute("data-current",ident);
}

Discussion

Tabbed pages have been popular for some time, and rightfully so. They’re a great way to make use of limited web page space in a manner that’s intuitive—we’re all familiar with tabs from other applications, including browsers.

The tabbed page concept is simple: display a list of tabs to click on the top, and pages underneath. All tabs are shown, but only one page is shown at a time. Clicking any of the tabs resets the page:

  • The highlighted tab is changed to the one just clicked.

  • The currently displayed page is hidden or set to nondisplay.

  • The clicked tab’s style is changed (so it’s highlighted).

  • The associated content page is displayed.

In the solution, part of a tabbed page application is shown: the part that demonstrates what happens when you click the tab. In this case, the tab is associated by an identifier number attached to the end of the tab identifier for the associated tab page, so there doesn’t have to be any form of container relation between the two. You want to avoid a container relationship between tab and page as much as possible, because it makes it difficult to ensure the page displays well when JavaScript is turned off.

The current tab picked is stored in the parent node’s custom data attribute, data-current. Using a custom data-* attribute, as these values are called, means we can avoid global variables. The next time a tab is clicked, it’s easy to find the current selection in order to reset the page.

Note

The custom data-* attributes were introduced with HTML5. You can use any name that begins with data-, and the page is still considered conforming to HTML5.

There are probably dozens of different approaches you can use to create tabbed pages, including making your own library, or using another that can be dropped into a page to automatically build the pages based on class settings.

The approach in Example 13-4 incorporates the approach outlined in the solution. It also makes use of the fact that elements form their own document trees, and that we can query an element tree the same as we can query the document tree. Using this approach, we can add as many tabbed page containers to the page as we wish. For each container, the application displays the navigation bar, turns off the display for all of the pages except the first, and highlights the first tab, and then adds the onclick event handler to each tab.

Example 13-4. Creating a reusable, multiple-count, tabbed page application
<!DOCTYPE html>
<head>
<title>Tabbed Pages</title>
<style>
  .tabcontainer
  {
    padding: 5px; width: 500px;
    margin: 20px;
  }
  .tabnavigation ul
  {
    padding: 0; margin: 0; display: none;
  }
  .tabnavigation ul li
  {
    padding: 3px; display: inline;
    border: 1px solid #000; background-color: #fff;
  }
  .tabnavigation ul li:hover
  {
    cursor: pointer;
  }
  .tabpages
  {
    position: relative; z-index: 2;
    border: 1px solid #000; background-color: #fff;
  }
  .tabpage
  {
    margin: 0 10px;
  }
</style>
<script>

// set up display
// for each container display navigation
// hide all but first page, highlight first tab
window.onload=function() {

  // for each container
  var containers = document.querySelectorAll(".tabcontainer");
  for (var j = 0; j < containers.length; j++) {

    // display and hide elements
    var nav = containers[j].querySelector(".tabnavigation ul");
    nav.style.display="block";

    // set current tab
    var navitem = containers[j].querySelector(".tabnavigation ul li");
    var ident = navitem.id.split("_")[1];
    navitem.parentNode.setAttribute("data-current",ident);
    navitem.setAttribute("style","background-color: #f00");

    var pages = containers[j].querySelectorAll(".tabpage");
    for (var i = 1; i < pages.length; i++) {
      pages[i].style.display="none";
    }

    var tabs = containers[j].querySelectorAll(".tabnavigation ul li");
    for (var i = 0; i < tabs.length; i++) {
      tabs[i].onclick=displayPage;
    }
  }
}

// click on tab
function displayPage() {
  var current = this.parentNode.getAttribute("data-current");
  document.getElementById("tabnav_" + current).setAttribute("style",
"background-color: #fff");
  document.getElementById("tabpage_" + current).style.display="none";

  var ident = this.id.split("_")[1];
  this.setAttribute("style","background-color: #f00");
  document.getElementById("tabpage_" + ident).style.display="block";
  this.parentNode.setAttribute("data-current",ident);
}
</script>
</head>
<body>
<div class="tabcontainer">
   <div class="tabnavigation">
      <ul>
         <li id="tabnav_1">Page One</li>
         <li id="tabnav_2">Page Two</li>
         <li id="tabnav_3">Page Three</li>
      </ul>
   </div>

   <div class="tabpages">
      <div class="tabpage" id="tabpage_1">
         <p>page 1</p>
      </div>
      <div class="tabpage" id="tabpage_2">
         <p>page 2</p>
      </div>
      <div class="tabpage" id="tabpage_3">
         <p>page 3</p>
      </div>
   </div>
</div>
<div class="tabcontainer">
   <div class="tabnavigation">
      <ul>
         <li id="tabnav_4">Page Two One</li>
         <li id="tabnav_5">Page Two Two</li>
       </ul>
    <div>

    <div class="tabpages">
       <div class="tabpage" id="tabpage_4">
          <p>Page 4</p>
       </div>
       <div class="tabpage" id="tabpage_5">
          <p>Page 5</p>
       </div>
    </div>
</div>
</body>

Figure 13-4 shows the application with two containers, different tabbed pages open in each. The application works with Chrome, Firefox, Opera, Safari, and IE8. It doesn’t work with IE7 because of the use of querySelectorAll.

A tabbed page application with two containers, and different tabbed pages open in each
Figure 13-4. A tabbed page application with two containers, and different tabbed pages open in each

See Also

See Recipe 14.8 for how to make tabbed pages accessible using ARIA roles and attributes.

13.8. Creating Hover-Based Pop-up Info Windows

Problem

You like the Netflix web site’s pop-up window that displays when the mouse cursor is over a movie thumbnail, and you want to incorporate this functionality into your own application.

Solution

The Netflix-style of pop-up info window is based on four different functionalities.

First, you need to capture the mouseover and mouseout events for each image thumbnail, in order to display or remove the pop-up window, respectively. Using a function that manages cross-browser event handling for every object that has an info bubble, assign a function to both the onmouseover and onmouseout event handlers. In the following code, the event handlers are attached to all images in the page:

function manageEvent(eventObj, event, eventHandler) {
   if (eventObj.addEventListener) {
      eventObj.addEventListener(event, eventHandler,false);
   } else if (eventObj.attachEvent) {
      event = "on" + event;
      eventObj.attachEvent(event, eventHandler);
   }
}

window.onload=function() {
  var imgs = document.getElementsByTagName("img");
  for (var i = 0; i < imgs.length; i++) {
     manageEvent(imgs[i],"mouseover",getInfo);
     manageEvent(imgs[i],"mouseout",removeWindow);
  }
}

Second, you need to access something about the item you’re hovering over in order to know what to use to populate the pop-up bubble. The information can be in the page, or you can use Ajax to get the information:

function getInfo() {

  // prepare request
  if (!xmlhttp) {
    xmlhttp = new XMLHttpRequest();
  }
  var value = this.getAttribute("id");
  var url = "photos.php?photo=" + value;
  xmlhttp.open('GET', url, true);
  xmlhttp.onreadystatechange = showWindow;
  xmlhttp.send(null);

  return false;
}

Third, you need to either show the pop-up window, if it already exists and is not displayed, or create the window. In the following code, the pop-up window is created just below the object, and just to the right when the Ajax call returns with the information about the item. The element.getBoundingClientRect method is used to determine the location where the pop up should be placed, and the DOM methods document.createElement and document.createTextNode are used to create the pop up:

// compute position for pop up
function compPos(obj) {
    var rect = obj.getBoundingClientRect();
    var height;
    if (rect.height) {
      height = rect.height;
    } else {
      height = rect.bottom - rect.top;
    }
    var top = rect.top + height + 10;
    return [rect.left, top];
}

// process return
function showWindow() {
   if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
     var response = xmlhttp.responseText.split("#");
     var img = document.getElementById(response[0]);

     if (!img) return;

     // derive location for pop up
     var loc = compPos(img);
     var left = loc[0] + "px";
     var top = loc[1] + "px";

     // create pop up
     var div = document.createElement("popup");
     div.id = "popup";
     var txt = document.createTextNode(response[1]);
     div.appendChild(txt);

     // style pop up
     div.setAttribute("class","popup");
     div.setAttribute("style","left: " + left + "; top: " + top);
     document.body.appendChild(div);
   }
}

Lastly, when the mouseover event fires, you need to either hide the pop-up window or remove it—whichever makes sense in your setup. Since I created a new pop-up window in the mouseover event, I’ll remove it in the mouseout event handler:

function removeWindow() {
   var popup = document.getElementById("popup");
   if (popup)
     popup.parentNode.removeChild(popup);

   return false;
}

Discussion

Creating a pop-up information or help window doesn’t have to be complicated, if you keep the action simple and follow the four steps outlined in the solution. If the pop up is help for form elements, then you might want to cache the information within the page, and just show and hide pop-up elements as needed. However, if you have pages like the ones at Netflix, which can have hundreds of items, you’ll have better performance if you get the pop-up window information on demand using Ajax. The solution demonstrates that using Ajax doesn’t add significant additional complexity to the application.

When I positioned the pop up in the example, I didn’t place it directly over the object, as shown in Figure 13-5. The reason is that I’m not capturing the mouse position to have the pop up follow the cursor around, ensuring that I don’t move the cursor directly over the pop up. But if I statically position the pop up partially over the object, the web page readers could move their mouse over the pop up, which triggers the event to hide the pop up...which then triggers the event to show the pop up, and so on. This creates a flicker effect, not to mention a lot of network activity.

Demonstrating a pop-up mouseover information window
Figure 13-5. Demonstrating a pop-up mouseover information window

If, instead, I allowed the mouse events to continue by returning true from either event handler function, when the web page readers move their mouse over the pop up, the pop up won’t go away. However, if they move the mouse from the image to the pop up, and then to the rest of the page, the event to trigger the pop-up event removal won’t fire, and the pop up is left on the page.

The best approach is to place the pop up directly under (or to the side, or a specific location in the page) rather than directly over the object. This is the approach Netflix uses on its site.

IE7 doesn’t like the use of setAttribute with the class or style attributes. To modify the code so it also works with IE7, replace the setAttribute with:

// IE7
div.className="popup";
div.style.left=left;
div.style.top = top;

See Also

See Recipes 13.2 and 13.3 for more information about using element.getBoundingClientRect. Chapter 12 provides coverage of creating new page elements, and Chapter 18 of using Ajax. Recipe 12.15 covers using setAttribute with CSS style settings.

13.9. Collapsing or Resizing the Sidebar

Problem

You have a website that has a main column and one or more side columns, as shown in Figure 13-6. You want to provide a way for your web page readers to control the width of the main column, without having to resize the browser.

Solution

Use a collapsible sidebar.

Add an X in a span element at the top of the sidebar. Include a custom data-expand attribute set to "true", to track whether the column is expanded or not. Also include tabindex="0", so that the element can receive keypress events:

 <div>
   <p id="x" data-expand="true" tabindex="0">
<span style="text-decoration: underline">X</span>
 Collapse sidebar</p>
 </div>
Collapsible sidebar page before sidebar is collapsed
Figure 13-6. Collapsible sidebar page before sidebar is collapsed

Add a method handler to the element’s click and keypress events that checks the data-expand attribute to see if the column is currently expanded or not. If it is expanded, the column is collapsed, and the main column expanded; if not, the sidebar and main column are returned to their regular dimensions:

window.onload=function() {

 var x = document.getElementById("x");
 x.setAttribute("style","display: block");
 x.onclick=expandOrShrink;
 x.onkeypress=expandOrShrink;
}

function expandOrShrink() {
   if (this.getAttribute("data-expand") == "true") {
      document.getElementById("sidebar").setAttribute("style",
             "width: 50px");
      document.getElementById("main").setAttribute("style",
             "width: 700px");
      this.setAttribute("data-expand", "false");
   } else {
      document.getElementById("sidebar").setAttribute("style",
              "width: 240px");
      document.getElementById("main").setAttribute("style",
              "width: 500px");
      this.setAttribute("data-expand", "true");
   }
}

Discussion

Working with page space doesn’t mean that the JavaScript has to be complex or involved. The collapsible sidebar is very simple. The key to making it work is to ensure that the sidebar contents are clipped when the sidebar is shrunk (as shown in Figure 13-7), rather than overflowing the sidebar dimensions.

Collapsible sidebar page after sidebar is collapsed
Figure 13-7. Collapsible sidebar page after sidebar is collapsed

To ensure the page works if JavaScript is disabled, the default CSS for the X element is set to display none. When the page loads, JavaScript changes the X to display.

Since IE7 doesn’t like the use of setAttribute with the style attribute, you can modify the application to support this older browser by using direct attribute assignment:

function expandOrShrink() {
   if (this.getAttribute("data-expand") == "true") {
      document.getElementById("sidebar").setAttribute("style",
                "width: 50px");
      document.getElementById("main").setAttribute("style",
                "width: 700px");

      // IE7
      // document.getElementById("sidebar").style.width="50px";
      // document.getElementById("main").style.width="700px";

      this.setAttribute("data-expand", "false");
   } else {
      document.getElementById("sidebar").setAttribute("style",
                 "width: 240px");
      document.getElementById("main").setAttribute("style",
                 "width: 500px");

      // IE7
      // document.getElementById("sidebar").style.width="240px";
      // document.getElementById("main").style.width="500px";

      this.setAttribute("data-expand", "true");
   }
}

IE7 does support the use of getAttribute and setAttribute with custom data-* attributes.

Get JavaScript Cookbook now with O’Reilly online learning.

O’Reilly members experience live online training, plus books, videos, and digital content from 200+ publishers.