Creating "Smart Help" with Conditional Content

By Dave Gash, HyperTrain dot Com


Contents

Click a link below to jump to a particular section; click any "Top" image following a section heading to jump back here.

Introduction

This article discusses several methods you can use to make Web-based Help systems "smart", by using conditional content to customize the appearance and behavior of your pages to the users' needs.

To get the most from the article, you should have a basic understanding of HTML tags, attributes, and values, and some exposure to JavaScript, although direct coding experience is not required.

You may download the example files at any time, and I encourage you to run them as you follow my rambling drivel... er, I mean, fascinating exposition. When you unzip the file, be sure to check "Use folder names" so the files will be extracted into the proper folders. You should end up with folders called Demo1, Demo2, and Demo3.

Background

Generally speaking, the usefulness of a Web page is inversely proportional to its coolness factor; think of it as "U = –C". The more clever, oddball, esoteric features that are stuffed into a page, the less useful it is likely to be. That's because the designer — almost certainly a programmer rather than a writer–got caught up in the heady rush of gee-whiz code instead of the cerebral satisfaction of accurate and accessible content. But, curiously, the formula doesn't invert well; sites that offer only dry, sterile content have nothing to hold our interest, no way to keep us tuned in for more. Accurate information is important, but without any flash at all, it can zone you out faster than a Josh Groban concert.

Now, programmers have their own vocabulary, much like Hungarians–the primary difference being that if you hang around with Hungarians long enough, you'll eventually figure out what they're saying–and one of our favorite words is "elegant". We use it when we see some code that is particularly sharp and insightful, code that is both graceful and functional, code that combines beauty and brevity. In other words, code that is so freakin' cool it makes us want to jump up and down and squeal like a little girl. But no, that would be undignified; instead, we slowly stroke our beards (especially evocative when done by lady programmers), and mutter, "Hmmm, that's very elegant code. Yas, yas, elegant. Harumph."

So what we need here is not merely elegant code, but elegant code that actually contributes to the content's value — Smart Help! The question is, what kind of gigantic freakazoid nerdnik no-life loser would be geeky enough to get totally wrapped up in a project like that? Well, if your answer is, "Certainly not Dave!," then you don't know me very well. In fact, it's only a pesky concern for providing good content that keeps me from getting completely buried in cool-but-useless code. If it weren't for that annoying pang of conscience, I'd sit around in my underwear all day and write scripts for Dynamic Drive. But again, cool code doesn't have to be useless, and I'm sure you're just dying for me to prove it. You are, right?

Why, When, What, How?

To get a handle on the importance of elegance in the Smart Help equation, let's first remember why users access Help. They go to our Help systems to answer a question, understand a task, overcome a problem, or clarify a concept. Certainly, information is at the heart of each of these goals, but in each case, appropriately sophisticated, interactive pages can help us deliver the information users need without boring them to tears.

We should also be aware of when users access Help. Generally, they call up the Help when they're already having a problem. It doesn't have to be a giant, "omigod" kind of problem; it could be something as small as whether to put dashes in a phone number. In other words, they were trying to accomplish something but couldn't, and so they go to the Help not because they want to, but because they have to. So they're almost certainly already frustrated; a Help page that knows something about the user's habits or preferences can assist in providing frustration relief quickly and efficiently.

And let's not forget what makes Help useful in the first place–not just information, but accurate, accessible, unobtrusive information–information that is relevant to the user's needs at the time it is accessed. A Help system that can sense the user's context or remember its own conditions can save the user time and trouble in navigating to and obtaining the right information at the right time, and let the user get back to what he or she was doing.

We might sum up the why, when, and what of Smart Help in a single word: specificity. Specificity is all about tailoring the response to a request at the time the request is made and conditional to the circumstances in which it was made. So if we can use some cool... er, I mean elegant code to improve the targeting of our Help data to the user's situation, then we have done each user a huge favor, and maybe even had a bit of fun in the process.

That's the why, when, and what of Smart Help; as for how to implement it, there are lots of different techniques. For this article, I've built specific examples in each of three groups: User-Provided Data, System-Provided Data, and Page-Determined Data. The code in each group isn't necessarily wildly different from the others, but the application of the code, specifically the way it obtains its information, varies among sections, so I thought it would be useful to look at them separately.

A Brief Digression

Before we go charging off into the code, we need to dispense with one little item. For some of the examples, we need a temporary storage method, a way to tuck away bits of data on the user's machine and then retrieve or delete them later. Hmmm, if that doesn't scream "COOKIES!" louder than a four-year-old at Grandma's, I don't know what does. Cookies are the perfect mechanism for this requirement: They are innocuous, easy to manage, can be named to associate them with their data elements, and can be set to expire after a given period.

There are literally thousands of prewritten cookie routines available; I have a simple and stable set that I've used for some time, so we're covered on that point. The three functions, setCookie, getCookie, and delCookie, live in the JavaScript file cookiefuncs.js, which is included by reference in the appropriate pages. The code is well documented, but unless you're just a glutton for punishment, there's no need to wade through it; just assume the functions work (they do, trust me) and use them as you would any pre-defined JavaScript language elements.

Ready? Let's boogie.

User-Provided Data

This example is fairly straightforward, and consists of three main pages: a Starter page, a Welcome page, and a Welcome Back page. The Welcome page is kind of a one-timer; it provides a form where the user can define some preferences, tucks the data away in some cookies, and calls the Welcome Back page. The Welcome Back page represents the body of the Help system; it uses the information stored in the cookies to set viewing preferences for itself and subsequent pages to which it links. The Starter page is the controller; it always runs first (invisibly) and decides which of the other two pages to call based on the presence or absence of the user information cookies.

Now would be a good time to run the first demo. In the Demo1 folder, double-click Starter.htm, which will take you to the Welcome Page. Enter your name into the field and set some radio buttons, then click Continue. When you reach the Welcome Back page, you'll see that it's using the preferences you just set. Close the browser and double-click Starter.htm again; this time, notice that it takes you directly to the Welcome Back page, with your preferences still set as before. Any time you're in the Welcome Back page, you can change your preferences by clicking the designated link. Or, you can click the other link to go to yet another page, which is itself determined by your information type preference, and of course also uses your color and font settings, and includes a return link to the Welcome Back page. Whew! To see how all this is done, let's begin with the Starter page code.

<html>
<head>
<title>Smart Help–Starter</title>
<script src="cookiefuncs.js"></script>
<script language="JavaScript">
function redirect()
{
if (getCookie("username") == null)
  {
  window.location="welcome.htm";
  }
else
  {
  window.location="welcomeback.htm";
  }
}
</script>
</head>
<body onload="redirect()">
</body>
</html>

That's the entire HTML page. No, seriously. In the words of Diamond David Lee Roth, the Starter page "ain't got no body." Note the inclusion of the cookiefuncs.js file in the head, and note that the redirect() function is attached to the body's onload event handler, so that as soon as the page loads, the function executes. All it does is look for a cookie, then jump to one page if it's there and another if it's not. Specifically, the if/else statement tries to retrieve the username cookie's value. If the cookie is not there (or has expired) the return value is equal to the JavaScript special value null, which is exactly what you think it is: nothing. In plain English, that means the user preferences haven't been set, so you have to go set them. If the if statement succeeds, the function executes a programmatic jump by setting the window's location property to a new target, welcome.htm, where you're asked to enter your preferences. However, if the cookie is there, its return value is not null (it's the user's name, but we don't care what the name is right now, only whether it's there at all). When that's the case, the if statement fails, the else clause takes over, sets the location property to the Welcome Back page name, and you can almost hear John Sebastian singing the theme song! (That's an old-guy joke; if you don't get it, that just means you're too young to remember most of the stuff old guys make jokes about, which we find really annoying.)

Anyway, assuming we haven't set our preferences yet, we're now at the Welcome page. Let's see the pertinent code from that one. As a precaution, the body's onload event executes a little cleanup() function, which simply deletes any leftover cookies. It looks like this.

function cleanup ()
{
// delete any old cookies
delCookie("username");
delCookie("userbgnd");
delCookie("userfsiz");
delCookie("userhtyp");
}

The only important bit about this code is the cookie names: username for (duh) the user's name, userbgnd for the user's preferred background color, userfsiz for the user's preferred font size, and userhtyp for the user's preferred help information type. Remember these; you'll see them again later. The form then presents itself, and much like the cleanup function, it's not very exciting, but there is one thing you should note: the values assigned to the radio buttons through their (duh squared) value properties.

<form name="infoform">
<p>First name: <input type="text" name="fname" value="(name)">
<p>Preferred background color:
<input type="radio" name="bgc" value="#3CB371">Greenish
<input type="radio" name="bgc" checked value="#87CEFA">Bluish
<input type="radio" name="bgc" value="#FFC0CB">Pinkish
<p>Preferred font size:
<input type="radio" name="fnt" value="3">10ish
<input type="radio" name="fnt" checked value="4">12ish
<input type="radio" name="fnt" value="5">14ish
<p>Preferred help information type:
<input type="radio" name="hlp" value="conceptual">Conceptual
<input type="radio" name="hlp" checked value="procedural">Procedural
<input type="radio" name="hlp" value="technical">Technical
<p><input type="button" value="Continue" onclick="proceed(infoform)">
</form>

Of course, as forms will, it just sits there and looks at you while you fill in the fields, click check boxes or radio buttons, etc. It finally wakes up when you click the Continue button, and executes the proceed() function, shown below.

function proceed (theForm)
{
var firstnm, backcol, fontsiz, helptyp;
// gather form info
firstnm = theForm.fname.value;
if (theForm.bgc[0].checked) backcol = theForm.bgc[0].value;
if (theForm.bgc[1].checked) backcol = theForm.bgc[1].value;
if (theForm.bgc[2].checked) backcol = theForm.bgc[2].value;
if (theForm.fnt[0].checked) fontsiz = theForm.fnt[0].value;
if (theForm.fnt[1].checked) fontsiz = theForm.fnt[1].value;
if (theForm.fnt[2].checked) fontsiz = theForm.fnt[2].value;
if (theForm.hlp[0].checked) helptyp = theForm.hlp[0].value;
if (theForm.hlp[1].checked) helptyp = theForm.hlp[1].value;
if (theForm.hlp[2].checked) helptyp = theForm.hlp[2].value;
// set cookies
setCookie("username", firstnm, 365);
setCookie("userbgnd", backcol, 365);
setCookie("userfsiz", fontsiz, 365);
setCookie("userhtyp", helptyp, 365);
// jump to Welcome Back page
window.location="welcomeback.htm";
}

This function uses a simple brute-force approach to check each radio button group, retrieve the value from the one that's checked (only one of each three if statements will succeed), and set those values into four variables: firstnm, backcol, fontsiz, and helptyp. As you can see, the variable names roughly correspond to our cookie names; in fact, the next thing the function does is use the variables to set the values of the cookies. Finally, it forces a jump to Welcome Back, and it's done. Now, I could have coded this bit more elegantly, sure; in fact, I did so on the first pass, but while the resulting loop-based process was smaller and more efficient, it was also less readable. I had to remind myself that I was trying to teach you something here, not impress you with deliberately convoluted code, and wound up with the above less elegant, but far simpler, version. A maxim for aspiring coders, then, might be: In function there is beauty. (Translation: If it works, it's pretty!)

Moving on, the Welcome Back page is where the cool stuff actually gets done. Let's look at its main block of code.

<script language="JavaScript">
// get document color from cookie
document.bgColor=getCookie("userbgnd");
// get font size from cookie
document.write("<font size=" + getCookie('userfsiz') + ">");
// get help preference from cookie
helppref=getCookie("userhtyp");
// greet user by name from cookie
document.write("<p><b>Hello again, " + getCookie('username') + "!</b></p>");
</script>

A subtle but significant point here is that this script is not a function, and is not called from the body's onload event. Instead, it is what's called immediate code, embedded directly in the body so that it executes as the page loads, not after it loads. It needs to do that because it actually writes text into the page, and the simplest way to accomplish that is to put the script exactly where its generated text needs to go. This script gets the four cookies in turn; the first is used to set document's bgColor (background color) property; the second document.writes a font tag (I know it's been deprecated in favor of styles, but humor me here) into the page to set the font size; the third stores the user's help information type preference in a variable for later use; and the fourth document.writes a welcome-back greeting to the user. Remember, this all happens as the page loads, before the user actually sees anything in the window; so when the document first appears, it calls the user by the previously stored name, and reflects the previously stored preferences. So far, so spiffy, eh?!?

Okay, that takes care of the user name, background color, and font size cookies, but what about that funky help information type cookie? What's that all about? Let's look at the link at the bottom of the page to find out.

<p>
<a href="javascript:jump()">Click here</a> for more information on computers.
</p>

This link (anchor tag) uses its href attribute not to jump directly to a new page, but to execute a JavaScript function. One small point, and then we'll look at the function. Notice the value of the href attribute includes the keyword "javascript:" before the function name, while other attribute values that are clearly JavaScript have only the function name; the body tag's onload="redirect()" attribute, for example. The special class of HTML attributes called event handlers–onload, onclick, onmouseover, onmouseout, ondonder, onblitzen, etc.–expect their values to be JavaScript code, and so don't require the keyword. The href attribute of an anchor tag, however, expects its value to be a page name, not JavaScript; therefore, the addition of the "javascript:" keyword alerts the tag to process the remaining text as JavaScript, not as a page name. Not a big deal, but I thought you'd want to know.

What does the jump() function do, then? Well, here it is.

function jump ()
{
window.location = "demo1" + helppref + ".htm";
}

Doesn't look very impressive, does it? It only does one thing, one little thing, but one very cool little thing. It grabs the value stored in the helppref variable, retrieved by the immediate script explained above, and uses it to construct a file name on the fly. And not just any old file name, mind you, but a file name that reflects the user's help type preference! Then it uses the by-now familiar window.location trick to jump to the page. How cool is that? (Your correct answer is, "Way.") Let's see how that one little line of code works.

First, look at the files you extracted from the zip archive into the Demo1 folder. Notice the three secondary pages, called demo1conceptual.htm, demo1procedural.htm, and demo1technical.htm. Now, remember that the value attributes of the help information type radio buttons in the Welcome page form were conceptual, procedural, and technical. Also remember that those values were stored in the userhtyp cookie by the Welcome page and retrieved into the helppref variable by the Welcome Back page. Finally, look at the jump() function again; it takes the string "demo1", concatenates the user's help type preference onto it, and then tacks on ".htm" to make it a file name. For example, if the user's preference was "procedural", the resulting file name is "demo1procedural.htm"; if it was "conceptual", the file name is "demo1conceptual.htm". Then in the very same statement, it shoehorns the generated file name into the window's location property and aw jeez, willya lookit dis, Edith, ya got yer, whaddayacallit, Smart Help here.

Next!

System–provided Data

I admit that this example is one of my faves, not just because of the goofy subject matter — "Mom's Roadkill Cafe" — but because of the elegant (there's that word again) way its code does certain jobs. Browsers will gladly expose themselves faster than Janet Jackson, if a script just knows what to look for. Date, time, browser name and version, available Document Object Model features, page elements, object attribute values, and much more is easily accessible through JavaScript, and can be used to provide conditional content to users without their having to take any action or provide any information. The data we need is all freely given to us by the system; all we have to do is use it. In this demo, we're going to use a simple piece of data to produce some pretty specific conditional content.

To run this example, go to the Demo2 folder and double-click the page MomsCafe.htm. Look at it carefully; you should notice three things. First, the picture and text for today's daily special is sick, disgusting, and revolting (that doesn't have anything to do with the example, I just enjoy pointing it out); second, the third line on the page shows a friendly greeting that's appropriate to the general mealtime (breakfast, lunch, dinner, or late-night snack); and third, in the group of radio buttons at the bottom, the current day of the week is already selected (determining which stomach-turning daily special you see). And to cause the page to display that timely greeting and daily special, you had to do exactly... nothing. Ah, she's a clever rascal, that Mom. Let's figure out how she always knows which meal is coming up.

var timeofday = new Date().getHours();
document.write("<center><h4>*** ");
switch (true)
{
  case timeofday < 10:
    document.write("Good morning! You're just in time for breakfast!");
    break;
  case timeofday < 15:
    document.write("Howdy! Care for a bite of lunch?");
    break;
  case timeofday < 20:
    document.write("Hi there! You must be about ready for dinner!");
    break;
  default:
    document.write("Hey, night owl! How about a late snack?");
}
document.write(" ***</h4></center>");

As you'd expect, one of the common things that browsers expose to running scripts is date information. And not just the day of the month, but also the month, year, hour, minute, second, day of the week, and phase of the moon. (Okay, I just made up that last one.) While we can extract a wealth of information from the Date() object, this script retrieves only the hour of the day, by executing the Date() object's getHours() method and assigning the result to a variable called, not surprisingly, timeofday. Notice that like the Welcome Back code in the previous example, this isn't a deferred function, but a block of immediate script that writes some text into the page as it executes. So, it document.writes a bit of HTML and text, beginning the greeting; that's a start.

Now comes the interesting bit: figuring out what time it is. Most coders would approach this task with a series of nested if/else statements, but try it (go ahead, I double-dog dare ya) and you'll find that it quickly becomes ugly and hard to read. Although nested if/elses are sometimes necessary, most programmers will admit that dealing with them is a lot like using a loofah: It feels best when you quit. The solution to the multiple-level if/else issue is a nifty little feature called a switch statement. A switch evaluates a single expression or value, then compares the result to a list of possible values and executes a block of code associated with the first one that matches. Now, that works fine when you're testing for individual values, like "10" or "Otto", but doesn't work at all when you're testing for ranges or inequalities. For instance, you'd think this would work...

switch (timeofday)
{
  case "< 10":
    document.write("Good morning! You're just in time for breakfast!");
    break;
(etc.)

...but you'd be wrong. "< 10" doesn't mean anything to the case statement because it's not an individual value. In other words, case statements can only test if their value is exactly equal to the switch value, not if it's unequal or more or less. Now, if you think this tends to limit the usefulness of the switch statement, you'd be right this time; but there's a coding trick we can use to fool the switch into doing what we want.

Notice that in the working example two boxes up, instead of testing the value of timeofday, we test the value of the JavaScript reserved keyword true, which is always, well, true. Sounds a bit odd, until you look at the subsequent case statements. They differ from the standard syntax in that they don't look at single values, they look at expressions that evaluate to a value. The first one evaluates the expression "timeofday < 10", which is always, always, always true or false, period. That result can now easily be compared to the forced true in the switch expression. If they're both true, the case code executes; if either is false, it doesn't. Since the forced true is always true, the case statement works the way we want, whether it likes it or not! Ah, very cool, very slick, very... that's right, elegant. Each case block ends with a break statement, so that only the first true/true match executes. And if the hour of the day is 20 or greater and all the cases fail, the default block does CYA duty and writes out the late-night message. Finally, one more simple document.write closes the greeting's text and HTML tags and you have your mealtime greeting, appropriately customized to the time of day. Hey, Mom knows what you need, when you need it.

The other bit of processing is a bit more sophisticated, and no less elegant. The whole issue of how to display the right daily special requires various chunks of HTML and JavaScript to make nice with each other, so let's see how that comes about. As I'm sure you've already discovered, clicking the other radio buttons causes each daily special to appear by itself, and clicking the ALL radio button displays all seven in order. Clearly, there must be seven separate daily special text blocks; that's a pretty good place to start, so let's look at the first one, for day 0, or Sunday.

<div id=grp0 style="display:none">
<p><b>Sunday: Sorry!</b> <img src="sun.jpg" width="110" height="66" border=0 alt=""><br>
Hon, you know we're closed on Sundays. Please visit us another day!
</p>
</div>

There's nothing, er, special about the content itself; it's a paragraph with some text and an image in it. Okay fine. But notice that it's wrapped in an HTML <div> container, with an id of "grp0" so we can identify it as the day 0 special, and with a display style of "none", meaning "don't display this in the page." It's still in the page, it's just not visible in the page. All seven specials are coded exactly the same way, except of course each id is unique and matches its day of the week, 0 through 6, for Sunday through Saturday.

Since the specials all have a display style of "none", that means when the page first loads, no special is displayed at all. So it's up to the page to get information provided by the system and act on it to give us today's conditional content. I know you're all jazzed up to see the Big Finish, but it will have more meaning (not to mention gee-whiz value) if we finish up the "regular" code first. Let's assume for a moment that today's special is already displayed, but we want to see a different one. So we click one of the radio buttons, whose code looks like this.

<form name="specform">
<h4>Click a day (or ALL) below to see our other specials!</h4>
<input type="radio" name="spec" onclick="setAll('none');setOne(0,'')">Sun.
&nbsp;&nbsp;
<input type="radio" name="spec" onclick="setAll('none');setOne(1,'')">Mon.
&nbsp;&nbsp;
<input type="radio" name="spec" onclick="setAll('none');setOne(2,'')">Tue.
(etc.)

Notice that we've named the form so we can refer to its elements later, and that all radio buttons in a group have the same name so we can refer to them by numeric subscripts (just as you'd refer to array elements). In particular, look at each button's onclick event, which executes not one but two JavaScript functions. The only difference from button to button is the number passed to the setOne() function; as you can see, that number again corresponds to the <div> group ids, which in turn correspond to the day of the week value. Let's take a quick look at those two functions.

function setAll (what)
{
// loop through days 0-6 to show or hide all specials
for (x=0; x<=6; x++)
  {
  setOne(x,what);
  }
}

Hmmm, looks like setAll() simply calls setOne() once for each day, passing along whatever display setting was passed to itself in the parameter what. Referring back to the radio button code, you can see that all the setAll() calls pass "none" as the parameter, so we can infer that the very first thing each radio button does is turn all the <div>s off. But to see how that's done we have to look at the other function, setOne().

function setOne (dow,onoff)
{
// turn display style for today's group on ("") or off ("none")
switch (dow)
  {
  case 0 : grp0.style.display = onoff; break;
  case 1 : grp1.style.display = onoff; break;
  case 2 : grp2.style.display = onoff; break;
  case 3 : grp3.style.display = onoff; break;
  case 4 : grp4.style.display = onoff; break;
  case 5 : grp5.style.display = onoff; break;
  case 6 : grp6.style.display = onoff;
  }
}

Here's our old buddy switch again, only this time he's coded with the standard (read: soooo inelegant) syntax, where the passed parameter dow (day of week) is tested against a series of fixed values. When a match is found, the display style for the appropriate group is changed to the setting passed to it in the parameter onoff. Since the only possible group values are 0 through 6, no default case is needed. The setAll() function, then, calls this function seven times, but that's just the "turn everything off" loop. Again referring to the radio button code, each makes a final call to setOne(), passing its associated <div>/day number and a null display style (""), which causes the individual <div> to appear.

All righty then, let's summarize what we know so far, then move on to the clincher. When the user clicks a radio button, it calls setAll() with a display parameter of "none". Next, setAll() calls setOne() seven times, once for each <div> number, but also passing the display parameter of "none". This results in all seven <div>s disappearing, regardless of what was or was not already displayed. Finally, the same radio button calls setOne() individually, passing its own <div> number and a null display style, which makes only that day's <div> appear. That's the manual version, and it's reasonably efficient, but still manual. This brings us back to our original question: How does the page use the system-provided data to produce the correct conditional content without the user's intervention?

Ah, saved the best for last, we have. The body's onload event takes care of that bit of magic by calling the showspecial() function, seen below.

function showspecial()
{
// get day of week, 0 (Sun) through 6 (Sat), then
// click correct radio button & let it do the work
var dayofweek = new Date().getDay();
specform.spec[dayofweek].click();
}

The first line just grabs the day of the week via the Date() object's getDay() method and stuffs it into a variable. But the second line is the elegant bit–it uses the variable as a subscript into the form's radio button array and executes the radio button's click event. In other words, the script programmatically clicks a radio button, just as if the user had done it! Think about that for a sec: The script isn't repeating code that's elsewhere in the page; it isn't even calling nested functions, like setAll() calls setOne()–it's actually using system-provided data to cause a form element to take an action normally executed only by direct user interaction. Excuse me for sounding like an eHelp product manual, but that's incredible! Amazing! Revolutionary! State-of-the-art! Okay, okay, it may not excite you as much as it does me, but then, you probably have a life. Eh, don't rub it in, okay?

The bottom line here is that with no interaction whatsoever from the user, this page retrieves some data from the system and dynamically changes the appearance of the page, providing content that's appropriately conditional to the circumstances — in this case, time of day and day of week. But, you hit Mom's page at a different time, or on a different day, and bang, you get significantly different content. That, kids, is Smart Help any way you cut it.

Page-Determined Data

In this example, we'll look at a technique that uses neither user-provided data nor system-provided data, but data that the page determines, accumulates, and saves for its own use. As in the first demo, we'll use cookies for data storage, but that's where the similarity ends. This demo presents a Help system for the Windows game Minesweeper, with a Table of Contents and a few topics. Now, this isn't a real Help system, it's just make-believe, like Ashley Simpson's singing. The idea of this example is "redirection by usage"; each time you open the Help system, it automatically goes to the page you use the most. That's important, so I'll say it again: The system opens not to the last page you visited, but to the one you visit the most. Imagine that, a Help system that knows the topics you use most often. Sweet.

In the Demo3 folder, start MinesweeperHelp.htm to see the TOC and default topic. Even though this is the first execution, go ahead and click the "Reset page counts" link just to make sure we start off with a clean slate; you'll get a confirming alert. Now before you charge off clicking links, let me remind you that this is a small, faux Help system, and there aren't that many unique navigation paths. In a real, substantial Help system, there would be enough topics to work with that testing sequence wouldn't be a problem. But to make this miniature Help system demo play out correctly, you have to select a specific series of topics in just the right order, and close/reopen the Help system as instructed. So please stick with me here for a minute, and then you can run along and play, okay?

Start by clicking the fourth link, "Game Background". You read the story, blah blah blah, yada yada yada, and decide you need some real info. So you click "Playing Minesweeper" and read that topic, then go to "Strategy and Tips", then back to "Playing Minesweeper", and finally "Object of the Game." Go ahead and click the links, in that order; I'll wait for you. (Dum de dum dum...) Okay, now close the browser completely. Now let's say you play a couple of games, blow yourself up immediately, and decide to take another look at the Help system. Start MinesweeperHelp.htm again, and notice that it jumps directly to "Playing Minesweeper". That's because in your previous navigation sequence, you visited that topic more than any other. (True, just twice to the other topics' once, but still more.) Hmmm, interesting. Okay, you're good for now, so again, close the browser.

But let's pretend that you play a while, keep losing, and so you feel the need to re-read some of those topics. You know, maybe your short-term memory isn't what it used to be, or you're taking some heavy flu medication, or you've started smoking old socks again. Whatever the reason, you open the system again (it still opens at "Playing Minesweeper"), and go to "Strategy and Tips", then "Game Background", then back to "Strategy and Tips", and finally to "Object of the Game". Satisfied that you know everything there is to know about Minesweeper, you again close the browser, with "Object of the Game" showing, and return to the game.

After a few more frustrating rounds of Please Blow Me Up Again Thank You Very Much, you once more start the Help system, and note that it opens at your currently most-accessed topic, "Strategy and Tips" (which doesn't seem to be doing you that much good, if you don't mind my saying so). The point is, the Help system remembers how often you access which topics, and uses that information to guide you to your most frequently-used selections. And believe it or not, all this is done by two little scripts; one to keep the page counts, and one to see which page count is highest.

Each time you start MinesweeperHelp.htm, the frameset places wmtoc.htm in the navigation frame, and default.htm in the topics frame. Except for the first run, default.htm doesn't really display itself; it just does some quick math and decides which page to load in its place, much like Starter.htm in the first demo. To see how this works, let's look at some code from default.htm.

var pages = new Array();
pages[0] = "mine_object";
pages[1] = "mine_play";
pages[2] = "mine_tips";
pages[3] = "mine_story";
var jumpto = -1;

A tiny but interesting factoid about this script is that it's in the page head rather than the body, but is not a deferred function; in fact, it's an immediate script that runs when it's encountered in the head section, before the page ever displays. All it does is set up an array containing the page names, and a global variable called jumpto, which is initially set to -1. In most language environments, where a value of 0 could actually mean something, programmers often use -1 as a "this variable doesn't have any value yet" indicator. When the page loads, then, the body's onload event calls the getmost() function.

function getmost()
{
var most = 0;
for (x=0; x<4; x++)
  {
  var times = getCookie(pages[x]);
  if (eval(times) > most)
    {
    most = eval(times);
    jumpto = x;
    }
  }
if (jumpto != -1)
  {
  window.location = pages[jumpto] + ".htm";
  }
}

This function first declares a variable called most to hold the biggest page count number it finds, and sets it to 0. It then loops through the four possible page values (0 through 3) looking for a cookie that matches the page name from the array. (The page counts, as you'll see below, are kept in cookies that correspond to the page names.) If it finds a matching page-count cookie, it retrieves the value, and here's where the big deal goes down: If the cookie's value is greater than the current high count stored in the variable most, the script replaces most with the new high count and sets jumpto to the subscript number of the new high-scoring page. If no page count wins over the others (meaning they're all 0 or all equal), the value of most remains unchanged and jumpto stays at -1. The final if statement tests jumpto for a page number; if it's not -1, then the loop found a page with a new high count, so our trusty old window.location statement gets that page name plus ".htm" stuffed into it, and the first thing you see is the page with the highest hit count. Man, that is slicker than Vaseline on a stripper's pole, just not as funny.

So far so good; default.htm has decided which page to jump to and forced the jump. There's only one more thing to do: Each page must keep track of its own hit count somehow; let's see how that's done by looking at this function, found in all four topic pages.

function setmycount()
{
var mycount = 0;
mycount = eval(getCookie("mine_object")) + 1;
setCookie("mine_object",mycount,365);
}

In each page, the body's onload event calls setmycount(), which simply gets the current value of its own counter cookie (in this case, the "mine_object" page), adds 1 to the value, and rewrites it. That's it. As the topics are accessed, this lets each page increment its own counter whenever it's loaded, which builds the values used by default.htm's getmost() function. And we're done!

Almost.

There's one little "gotcha" in this plan. Who can tell me what it is? Anyone? You there, looking the other way — no? Anyone? Anyone? No? Sigh.

All right then, here's the problem. Each time you start the Help system, default.htm decides which page currently has the highest count and jumps to that page, right? Right. But what's the first thing each page does? If you said "increments its own counter", you're right! You win First Prize, two tickets to "Jerry Springer: The Opera". (In case you're wondering, second prize is four tickets.) In other words, if the idea is for each page to get a point when the user jumps to it, then won't the current high-scoring page get false points every time default.htm jumps to it? The answer, of course, is "$*%#!"... er, I mean, "yes".

Think about that for a moment; it's a serious problem, and in fact throws the validity of the user hit count right out the window. If a page gets a point for every jump, including the ones from default.htm, then its count is going to get artificially high pretty fast, and it might actually become unbeatable no matter what other pages the user accesses. As ugly a problem as it seems, the fix is relatively straightforward; we'll add another cookie to tell the pages who called them, and it only takes a few lines of code in the two main functions. Here's the corrected version of default.htm's getmost() function.

function getmost()
{
var most = 0;
for (x=0; x<4; x++)
  {
  var times = getCookie(pages[x]);
    if (eval(times) > most)
      {
      most = eval(times);
      jumpto = x;
      }
  }
if (jumpto != -1)
  {
  setCookie("redirect","yes",1);
  window.location = pages[jumpto] + ".htm";
  }
}

The new line, in bold above, tells getmost() to write out a separate cookie called "redirect" before jumping to the current high-scoring page. This places a flag or indicator out there for the other pages to look for. Now let's revisit the pages' setmycount() function.

function setmycount()
{
if (!getCookie("redirect"))
  {
  var mycount = 0;
  mycount = eval(getCookie("mine_object")) + 1;
  setCookie("mine_object",mycount,365);
  }
delCookie("redirect");
}

Again, the bold lines indicate the changes. The new if simply tells the page to increment its counter only if there's no redirect cookie; to put it the other way around, if there's a redirect cookie, the page was called from default.htm and therefore doesn't get a point. The delCookie() call is just a cleanup statement to get rid of the redirect cookie if it exists, and is called outside the if block so it executes no matter what.

Check it out: With no input from the user, no retrieval of browser-exposed data, no interaction of any kind with any external entity, carbon- or silicon-based, this Help system watches how the user uses it, keeps track of favorite pages, and modifies its own navigation to return to them automatically when restarted. Now I ask you: Is this Smart Help? Hey, is Elvis dead? Does the Pope wear a funny hat? Is a frog's butt watertight? Does Victoria have a secret? Is Pauly Shore's career over? (Answers: Yes, yes, yes, yes, yes, and oh hell yes.)

And now we're really done.

Summary

In this article, we looked at a number of ways to improve the specificity, or targeting, of Help information to make it more accurate, more accessible, more relevant to users' situations. We've learned, then, that providing conditional content by using Smart Help techniques can benefit users in a number of ways.

  • By simplifying the location of pertinent information. If Help systems are more aware of the user's circumstances, they can provide information appropriate to those circumstances and more quickly connect users with the information they want.

  • By reducing work flow interruption. Remember that the users' goals are to get the information they need and get back to work, so Help systems that are quick to provide the right data and therefore easy to get out of are the best of all worlds.

  • By avoiding repetitive navigation. When Help systems retain information about their own states and conditions, they can assist users in navigating to the parts of the system they most likely need.

Thank You...

...for reading this article! I hope you enjoyed the techniques presented here, and that you find them practical and useful in your own Help systems. Don't forget to download the example files if you have not already done so, and please feel free to contact me any time with questions or comments. I'm always glad to hear from you!


Dave Gash is the owner of HyperTrain dot Com, a Southern California firm specializing in training and consulting for hypertext developers. A veteran software professional with over thirty years of programming, documentation, and training experience, Dave holds degrees in Business and Computer Science, and is well known in the tech pubs community as an interesting and animated technical instructor. When he's not developing new scripts, software, Web sites, and documentation materials for his clients, Dave is a frequent speaker at Help-related seminars and conferences in the US and around the world.



up