Article

Script Smarter: Quality JavaScript from Scratch

Page: 1 2 3 4 5 6 7 8 Next

Chapter 7. Working with Windows and Frames

This chapter is about simple window and frame manipulation, including tasks like opening popups, communicating between frames, (The techniques involved in reading data from an iframe will be covered in Chapter 18, Building Web Applications with JavaScript.) and finding out the page's scrolling position.

Plenty of people feel that window manipulation is akin to the Dark Side. They believe that a window is part of the user's GUI, not the document, and since JavaScript is a document scripting language, it has no business manipulating windows.

I'm generally inclined to agree, yet I know that opinion is sometimes a luxury. If your clients ask for something specific, you can't necessarily change their minds, or have the freedom to turn down work on the basis of such a principle. In this chapter, we'll cover a range of practical window and frame manipulation tasks while remaining sensitive to the usability and accessibility issues that can arise from their use.

Note, though, that there are limits, and some varieties of window scripting are particularly unfriendly. We won't be dealing with aggressive tactics like closing or modifying the user's primary window, moving windows around the screen, or opening full-screen or "chromeless" windows. These are exactly the kinds of abuses that have given JavaScript a bad name.

Through most of this chapter we'll be looking closely at the properties and methods of the window object. These are implemented by different browsers in a variety of ways, most of which have been in use since the days before JavaScript was standardized.

We'll have quite a few code branches to deal with, but we'll avoid the dreaded browser sniffing by careful use of object detection, the process of detecting an object or feature to test for compatibility, rather than detecting specific browsers.

Using Popup Windows

Should you use popup windows? The most considered answer I have is this: not if you can help it. Popup windows have gained a bad reputation from marketers' aggressive use of them, but even requested popups can be barriers to good usability.

I won't say that popups are never appropriate, but I will say that they're seldom so. Nevertheless, there are situations where popping open a new window is arguably the most appropriate solution: an online survey might be one example, as the format may make the content more approachable; DHTML games are another, as the viewport may need to be of a known size.

I'll qualify my opinion by discussing the problems that popups create, then providing a pragmatic method for using them that mitigates these problems as much as possible.

What's Wrong with Popups?

The main problem with most popup window scripts is that they don't consider the needs of the user?they address only the needs of the designer. The results? We've all seen them:

  • popups that are generated from links, though those links do nothing when scripting is not available
  • popup windows that don't have a status bar, so you can't necessarily tell whether the document has loaded or stalled, is still loading, etc.
  • popups that don't give users the ability to resize the window, and popups that fail to generate scrollbars for content that might scale outside the window
  • windows that are "chromeless," or open to the full size of the user's screen

These issues are not just questions of usability, but of accessibility as well. For example, screen-reader users may not be notified by their devices that a new window has opened. This could obviously cause confusion if they then attempted to go back in the browser history (they can't). The same thing might happen for a sighted user if a window opens at full-size: you and I may be familiar with using the taskbar to monitor open windows, but not all computer users are -- they may not even realize that a new window has popped up.

If you're going to use popups, looking out for issues like these, and being generally sensitive to their impacts, will make your popups friendlier to users, and less of a strain on your conscience.

Also, bear in mind that, from a developer's perspective, popup windows are not guaranteed to work: most browsers now include options to suppress popup windows, and in some cases, suppression occurs even if the popup is generated in response to a user event.

You may be able to allow for this as you would for situations in which scripting was not supported: by ensuring that the underlying trigger for the popup still does something useful if the popup fails. Or you might have your code open a window and then check its own closed property, to see if it's actually displayed (we'll look at this technique in the next solution).

But neither of these approaches is guaranteed to work with every browser and popup blocker out there, so for this as much as the usability reasons, it's simpler and better to avoid using popups whenever you can.

How Do I Minimize the Problems?

What we need to do is establish some golden rules for the ethical use of popups:

  • Make sure any triggering link degrades properly when scripting is not available.
  • Always include the status bar.
  • Always include a mechanism to overflow the content: either allow window resizing, or allow scrollbars to appear, or both.
  • Don't open windows that are larger than 640x480 pixels. By limiting the size of popups, you ensure that they're smaller than users' primary windows on the vast majority of monitors. This increases the likelihood that the user will realize that the popup is a new window.

Solution

Here's a generic popup function that's based on the guidelines above:

Example 7.1. make-popup.js (excerpt)    
   
function makePopup(url, width, height, overflow)    
{    
 if (width > 640) { width = 640; }    
 if (height > 480) { height = 480; }    
   
 if (overflow == '' || !/^(scroll|resize|both)$/.test(overflow))    
 {    
   overflow = 'both';    
 }    
   
 var win = window.open(url, '',    
     'width=' + width + ',height=' + height    
     + ',scrollbars=' + (/^(scroll|both)$/.test(overflow) ?    
     'yes' : 'no')    
     + ',resizable=' + (/^(resize|both)$/.test(overflow) ?    
     'yes' : 'no')    
     + ',status=yes,toolbar=no,menubar=no,location=no'    
 );    
   
 return win;    
}

As well as limiting the window size, this script refuses to create a popup that doesn't have an overflow, so if you don't specify "scroll", "resize", or "both" for the overflow argument, the default setting of "both" will be used.

The Ternary Operator

This script uses a shortcut expression called a ternary operator to evaluate each of the overflow options. The ternary operator uses ? and : characters to divide the two possible outcomes of an evaluation, and is equivalent to a single pair of if..else conditions. Consider this code:

if (outlook == 'optimistic') { glass = 'half-full'; }    
else { glass = 'half-empty'; }

That code is equivalent to the markup below:

glass = (outlook == 'optimistic' ? 'half-full' :    
   'half-empty');

The parentheses are not required, but you may find they make the expression easier to read. For more about this and other useful shortcuts, see Chapter 20, Keeping up the Pace.

Once you have the popup function in place, you can call it in a variety of ways. For example, you could use a regular link:

Example 7.2. make-popup.html (excerpt)    
   
<a href="survey.html" id="survey_link">Online survey</a>

If scripting is not available, this will work just like any other link, but if scripting is available, the script can trigger a click event handler that passes its href to the makePopup function, along with the other settings. The return value of the handler depends on whether or not the window is actually opened; browsers that block the popup will follow the link as normal:

Example 7.3. make-popup.js (excerpt)    
   
document.getElementById('survey_link').onclick = function()    
{    
 var survey = makePopup(this.href, 640, 480, 'scroll');    
   
 return survey.closed;    
};

In general, if you have a script that requires that a window be generated, you can call the makePopup function directly with a URL:

var cpanel = makePopup('cpanel.html', 480, 240, 'resize');

If you need to close that window later in your script, you can do so by using the close method on the stored window reference:

cpanel.close();

Discussion.

The window.open method can take a number of arguments -- in addition to the URL and window name -- which specify whether the window should have particular decorations, such as the menu bar, tool bar, or address (location) bar. These arguments are passed as a comma-delimited string to the third argument of window.open:

var win = window.open('page.html', 'winName',    
   'width=640,height=480,'    
   + 'scrollbars=yes,resizable=yes,status=yes,'    
   + 'toolbar=no,menubar=no,location=no');

In our makePopup function, the menubar, toolbar, and location arguments are all preset to no because these elements are rarely useful for popup windows -- they're navigational tools, after all. Popups are mostly used for one-page interfaces, or those in which history navigation is discouraged, such as our survey example, or the logon procedure for a bank's web site.

You can change those arguments if you need to, but the status argument should always be set to yes, because turning it off undermines good usability. (I know -- I've mentioned it already, but I'm saying it again because it's important!)

The resizable argument may not have any effect -- in some browsers, either by design or as a result of user preferences, it's not possible to create non-resizable windows, even if you set this value to no. In fact, in Opera 8 for Mac OS X, it's not possible to create custom-sized windows at all -- a created window will appear as a new tab in the current window. That specific exception might not be significant in itself, but it serves to illustrate the general point that control over the properties of a created window is not absolutely guaranteed.

Once a new window is open, you can bring it into focus using the object's focus method. This isn't usually necessary -- generally, it happens by default -- but the technique may be useful when you're scripting with multiple windows:

var cpanel = makePopup('cpanel.html', 480, 240, 'resize');    
cpanel.focus();

Alternatively, you may want to open a popup but keep the focus in the primary window (thereby creating a so-called "popunder"). You can take the focus away from a window using its blur method:

var cpanel = makePopup('cpanel.html', 480, 240, 'resize');    
cpanel.blur();

However, in that case you can't predict where the focus will go to next, so it's more reliable to refocus the primary window:

var cpanel = makePopup('cpanel.html', 480, 240, 'resize');    
self.focus();

Opening Off-site Links in a New Window

In the strict versions of HTML 4 and XHTML 1, the target attribute for links no longer exists. One interpretation of this is that web pages simply shouldn't open links in new windows; another is that targeting doesn't have universal semantics and therefore shouldn't be defined in HTML. (The CSS 3 working draft includes a set of target properties for link presentation, which could eventually see this mechanism handed to CSS instead. Personally, I hope this never gets past the draft stage, because it's nothing to do with CSS: interface control is no more appropriate in a design language than it is in a semantic markup language!)

There are other interpretations, and the arguments are long (and sometimes tedious), but suffice it to say that you may find yourself needing a solution to this problem. Whatever your personal views may be, it's a common request of web development clients.

Solution

This script identifies links by the rel attribute value external. The rel attribute is a way of describing the relationship between a link and its target, so its use for identifying links that point to another site is semantically non-dubious:

Example 7.4. offsite-links.html (excerpt)    
   
<a href="http://www.google.com/" rel="external">Google    
 (offsite)</a>

If each external link is identified like that, a single document.onclick event handler can process clicks on all such links:

Example 7.5. offsite-links.js    
   
document.onclick = function(e)    
{    
 var target = e ? e.target : window.event.srcElement;    
   
 while (target && !/^(a|body)$/i.test(target.nodeName))    
 {    
   target = target.parentNode;    
 }    
   
 if (target && target.getAttribute('rel')    
     && target.rel == 'external')    
 {    
   var external = window.open(target.href);    
   
   return external.closed;    
 }    
}

Discussion

Using a single, document-wide event handler is the most efficient approach -- it's much better than iterating through all the links and binding a handler to each one individually. We can find out which element was actually clicked by referencing the event target property. For more about events and event properties, see Chapter 13, Basic Dynamic HTML, but here's a brief summary of the situation.

Two completely different event models are employed by current browsers. The script establishes which one should be used by looking for e -- the event argument that's used by Mozilla browsers, and has been adopted by most other browsers -- as opposed to the window.event object used by Internet Explorer. It then saves the object property that's appropriate to the model in use: either target for Mozilla and like browsers, or srcElement for IE.

The target object (if it's not null) can be one of three things: a link element node, an element or text node inside a link, or some other node. We want the first two cases to be handled by our script, but clicks arising from the last situation may be safely ignored. What we do is follow the trail of parent nodes from the event target until we either find a link, or get to the body element.

Once we have a unified target link, we need simply to check for a rel attribute with the correct value; if it exists, we can open a window with the link's href, and if all of that is successful (as judged by the new window object's closed property), the handler will return false, preventing the original link from being followed.

Passing a link to window.open without defining arguments will create a window with default decorations -- as will a link with target="_blank".

The First Test

We use getAttribute as the first test for rel because attribute-specific properties are only reliable if you know for certain that the attribute in question has been assigned a value. We can't go straight to testing target.rel against a string, because it might be null or undefined. This was discussed in more detail in the section called "Reading and Writing the Attributes of an Element".

If you liked this article, share the love:
Print-Friendly Version Suggest an Article

Sponsored Links