Uncle Dave’s Workshop: Adding User Annotations to Help Topics
By Dave Gash, HyperTrain dot Com
Contents
Click a link below to jump to a particular section; click any "CONTENTS" image following a section heading to jump back here.
Introduction 
This article discusses a feature called User Annotations, whereby users can add their own custom notes to Help topics, and presents a method for implementing it in web pages or in HTML Help topics.
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 the narrative below. For the browser-based Help example, start by running "helptopicone.htm" or "helptopictwo.htm" in either Internet Explorer 5+ or Netscape 6+ (do not run "annotate.htm" by itself). For the HTML Help example, run "userann.chm". In both examples, there are two topics that can each have their own annotations, and both examples require the user's default browser to have cookies enabled.
Also please bear in mind that the term "browser" as used here also refers to the HTML Help viewer, because the content of its topic pane is, after all, rendered by IE's display engine. Everyone okay with that? Great; here we go.
Background 
Ah, annotations. I'll bet a lot of you younger writers out there don't even remember them. The ability for users to add their own topic annotations was a feature of "old style," RTF-based WinHelp. It was built right into the viewer's interface, way back in the days when bad haircuts were still unintentional. The idea was that while viewing any Help topic, users could open a separate annotation window and add their own remarksprerequisites, cautionary reminders, job-specific commentswhatever they thought was important. When saved, the annotation was associated with that topic, and was persistent across executions. On subsequent viewings of the Help system, users not only could see the original topic information, but also could access their own customized notes. Quite the elegant little feature, it was.
Unfortunately, this terrific idea suffered from poor implementation. The details aren't really important any more, but the upshot is this: If the users ever received an updated version of the Help systemindeed, if the Help file was changed by so much as one byteall annotations became disconnected from their topics, physically present but no longer accessible, lost forever, swirling about somewhere in the bit bucket. As you might imagine, this little quirk was considered by most users to be an unacceptable risk, and forced many to adopt less elegant but more stable annotation methods, such as carving notes into their forearms with a penknife. Hey, just ask your senior writers to roll up their sleeves. You'll see.
With the passing of WinHelp 4, the whole concept of annotations sort of fell by the wayside. The feature is not present in the current version of the Microsoft HTML Help viewer, HH.EXE, nor of course is it supported by any browser. And I expect that on Microsoft's list of "Top One Thousand Things To Do Next," user annotations are somewhere down in the eight- to nine-hundred range. Still, it was a great idea, and judging from the comments I get from my clients and students, one that would be welcomed back into the HTML world with open arms... if only someone would show you how to do it!!!
And that's my job.
The Plan 
Before we go charging off to write a bunch of code, let's get a mental picture of the feature. Here's how it might look in action, with the annotation window open on top of a browser window.

And here's how it might look when called from an HTML Help file.

Now let's make a list of items we'll need to implement the feature and discuss each one briefly.
Storage Method
First, we need a way to store the annotations; without that, everything else is moot. We must be able to save a block of text on the user's computer and retrieve it later through some sort of identifier. 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 feature: They are easy to manage, can be named to associate them with their parent pages, 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.
Calling Mechanism
Of course, we need to call the annotation window from a topic page, and for this we have our pick of methods. A plain text link, a button, or an image with a hotspot will all work equally well. All the link has to do is open a new window, position it on the screen, and load the annotation page into it.
For this example, I chose an image (two images, actually) of Clippy, the Jar Jar Binks of user assistance. Clippy will perform two functions for us: He will open the annotation window from the topic page, and he will indicate whether the topic page has an annotation (eyes open, as in the first screen shot above) or not (eyes closed, as in the second).
Annotation Form
The page loaded into the annotation window must contain a text box for the annotation, and buttons to save, delete, or cancel the annotation. No problem; that just requires a simple form with a few elements that execute the cookie handling functions.
That's about it for the straightforward items, but there are a few behind-the-scenes things we also need to take care of, such as...
Topic Identification
Each topic page must have its own annotation cookie, so we need a way to associate a cookie with its topic. A good identification method would be to use the calling page's name as part of the cookie name. To accomplish that, each instance of the annotation window must know which topic page it belongs to; this in turn requires that a topic know its own name and make it available to the annotation window. The most obvious method for this is another cookie, but that just complicates things; why write more information to the user's disk than we have to? There is an easier way for one page to access a bit of information on another page, by using window handles (more about this later). We'll use that method instead, and eliminate the extra cookie.
Annotation Status
As mentioned above, we want Clippy to let us know if an annotation exists without having to open the annotation window. To accomplish this, we'll have the topic page check for its own annotation cookie and set the Clippy image accordingly. In fact, to make sure the indicator is current, the page will set the image not only when it loads, but every time it receives the focusfor example, when returning from the annotation window.
Cancel Confirmation
No self-respecting form would let the user cancel changes without the ubiquitous "Are you sure?" alert box. Our form, however, will only ask the question if the annotation actually has changed. Wow, what a concept.
Enough talk, already. Let's write some code!
The Code 
The Cookie Functions
Let's begin with the cookies that contain the user annotations. The cookie handling functions are not the focus of this article, so instead of going through these line by line, let's just assume that they work (they do) and see how to use them. They are in the external JavaScript file "cookiefuncs.js", which is included in both the topic pages and in the annotation page. You may of course examine this file at your leisure for more details. As you might guess, there are three functions, one to write a cookie, one to read a cookie, and one to delete a cookie.
The write function, setCookie, takes three parameters: The name of the cookie to be written, the value to be written into it, and the number of days before it expires. As you'll see later, the cookie name is constructed from the word "annotation-" plus the original topic page name; the value is of course the text from the annotation box; and the expiration date in our example is set to 365 days from the date the cookie is written.
setCookie(NameOfCookie, value, expiredays)
The read function, getCookie, takes only one parameter: The name of the cookie to be read. When used in an assignment statementfor example, cooktext = getCookie("usernotes")it returns the value of the cookie into a variable, which then may be manipulated by the script.
getCookie(NameOfCookie)
Finally, the delCookie function also takes only one parameter: The name of the cookie to be deleted. Cookies aren't physically removed from the user's disk in a delete operation; instead, they are forced to expire, which basically does the same thing. This function sets the named cookie's expiration date to January 1, 1970, which has the same effect on the cookie as the phrase "...starring John Goodman" has on a network sitcom.
delCookie(NameOfCookie)
The Topic Page
Since each topic that uses the annotation feature will require some modifications, I've made an effort to minimize that by putting as much code as possible into external files that may easily be included into the topics. In a nutshell, the topic must reference the cookie functions file and the annotation functions file in its <head> section, thus:
<script src="cookiefuncs.js"></script>
<script src="annotate.js"></script>
Then, in the <body> section, the topic must call the routine (present in the "annotate.js" file) that writes the code to insert the image and wraps the link (the <a> tag that calls the annotation window) around it. This code is placed wherever the image is to appear, thus:
<script>placeimage();</script>
That's it for the annotation-specific code; the remainder of the topic is coded as desired.
The Annotation Script File
Although the "annotate.js" file isn't large at all, it does most of the work for the topic page. First, it declares two global variables: whoami, which will hold the calling (topic) page's name; and annwin, which will hold the annotation window's identifier, called its handle.
var whoami = "";
var annwin = null;
Next, it declares two new image objects and preloads the "on" and "off" images into them. This is less important for small images than for large ones, but always improves the image-swapping speed, and is simply good coding practice.
var pclipoff = new Image();
pclipoff.src = "clippy0.gif";
var pclipon = new Image();
pclipon.src = "clippy1.gif";
Now it sets up some event handlers for the window. Normally, this would be done in each page's <body> tag<body onfocus="seticon()" onload="getmyname()" onunload="autosave()">but that would require additional changes to every page, which, as mentioned above, we're trying to avoid. So, we can force those specific window events to execute our own functions by directly scripting them in the include file. Note the absence of quotes and parentheses on the function references, coded like this:
window.onload=getmyname;
window.onfocus=seticon;
window.onunload=autosave;
Let's look at those three event handler functions. Remember that each topic must know its own name, in order to tell the annotation page what to call the cookie. This is something we could hard-code in each pagemyname="helptopicone.htm"but that's just one more change we'd have to make to every topic. Instead, we can let the page figure out its own name by looking at its complete URL, finding the last slash character, and stripping out whatever is left over. Presto, the page name is revealed: "helptopicone.htm", "index.html", whatever. The last thing it does is force the window to receive the focus, so the icon will be set properly (see below). This function is called as soon as the page is loaded, from its onload event handler.
function getmyname()
{
whoami = " " + document.location;
slash = whoami.lastIndexOf("/");
whoami = whoami.substring(slash + 1);
window.focus();
}
The next function is called whenever the topic page gains the focus, and updates the annotation icon to indicate whether an annotation exists. As we'll see later, this is forced to occur when the annotation window closes, and as we saw above, it is also forced when the document loads so the page displays the correct icon immediately. Note how the cookie name is built from the word "annotation-" plus the previously derived page name.
function seticon()
{
var cookietext = getCookie("annotation-" + whoami);
if (cookietext == null) document.annicon.src = pclipoff.src;
else document.annicon.src = pclipon.src;
}
Ah, but there's a fly in the ointment: What if the user were to edit the annotation, then jump to another topic while the first topic's annotation is still open and unsaved? Why, the annotation window would remain up, but it wouldn't contain the annotation for the displayed topic! Um, okay, that is, like, sooooo not good. Our confused-user-prevention choices then become: (a) Ask whether to save the changes, then try to continue the jump they wanted to take in the first place; (b) cancel their changes without asking; or (c) save their changes, close the annotation window, and take the jump. To me, the latter choice is the least bothersome and most reasonable; so, when the topic unloads, it executes this little function, which quietly saves anything in the annotation box and lets the page go about its business. Note the syntax "annwin.saveann()", which allows the topic page to execute a function in the other window by referencing it through its handle. This is a very important concept, and we'll come back to it in a minute.
function autosave()
{
if (annwin != null) annwin.saveann();
}
It's worth noting here that the annotation window is not modal; that is, users may activate either it or the main window at any time, flipping back and forth all they like. This is a good argument for placing the annotation window near the top right of the screen, rather than on top of the main window where it could be obscured easily. But this tactic by no means prevents users from moving either window, or from resizing the main window, or even from closing the main window while the annotation window is open! Our only safeguard is the little "autosave()" function discussed above, which saves and closes the annotation if the main topic is unloaded for any reason.
The only case we absolutely can't cover is if users click the button in the annotation window's title bar to close it instead of clicking one of the provided form buttons. If they do that, then tough noogies; their changes are gone, adios, sayonara, gotta go, see ya later. It's a shame, but it can't be helped; remember, if you make your system foolproof, they'll just make smarter fools.
However, the seemingly simple operation shown aboveclosing one window from anotherbegs a bit of discussion about multiple windows. Forgive the digression, but it's important. Many JavaScripters are under the impression that there's a hierarchical association, a parent-child relationship, between a calling window and a called window. Actually, that's not the case at all; once the second window is opened, it's on its own, a true instance of the browser, separate and apart from its caller or any other window on the screen. How, then, can these windows "talk" to each other?
Such inter-window communication is accomplished through the use of window handles. Handles are object identifiers assigned by the browser when a window is created. We carbon-based units aren't privy to the actual contents of handle objects, but we can be assured that each window has a unique handle, and that's all we need to know to use them. In the case of an opened window, its handle is returned by the window.open statement that created itthat's why we assign the statement's result to a variable, so we can capture it. When the new window is successfully opened, the variable contains its handle, and can be used to reference objects in it, such as functions (as done above), variables, form elements, etc.
That's all well and good for the calling window, but what about the called window? Communication is a two-way street, and the called window may need to access some of its caller's objects as well. Since the calling window is of course open, and therefore already has a handle, how can the newly called window know what that handle is? There is a special object called window.opener, which every opened window has access to, and which contains the handle of the window that opened it. The opened window can use this handle to reference objects in its caller, just like its caller uses the opened window's handle. In this way, each window can "reach into" the other and get to virtually any object, even though the object is in a completely different window! To me, the fact that this is even possible is more amazing than a rap song with a melody.
Okay, digression over; let's forge ahead.
Next, there's the function that puts the image and its hotspot onto the page. This is accomplished with a couple of document.write statements that shoehorn the anchor and image tags into the document. Yet again, we could "simply" code this into each page, but then if we ever want to change it... you know the rest. By putting the function in the include file, we only have to maintain it in one place. Note (a) the JavaScript call in the href attribute, which opens the annotation window, (b) the "this.blur()" trick, which keeps the browser from putting that ugly little dotted box around the image when you click it, and (c) the image's name attribute, which allows it to be referenced by the "seticon()" function. And while you're about it, note the carefully nested single- and double-quotes, and the title attribute, which gives you that annoying little manila yellow text box when you rest the mouse over the graphic.
function placeimage()
{
document.write("<a href='javascript:callannotate()' onfocus='this.blur()'>");
document.write("<img name='annicon' src='clippy0.gif' width=41 height=82 border=0 title='Click to edit annotation'></a>");
}
Finally, we have the function that actually opens the annotation window. Its first job is to not do anything (return) if the annotation window is already open, indicated by a non-null value for the annotation window's handle, annwin. If there's no annotation window up, it calculates an offset for the left edge of the window (arbitrarily placing it near the top right corner of the screen), creates a string for the window options (with everything turned off, to make the window as plain as possible), and executes a window.open statement, providing the called window's handle. But remember, the called window must also figure out the name of its caller to know which cookie to use, and there's no parameter passing of any kind going on here. In a moment, we'll see how the called page locates and uses that information.
function callannotate()
{
if (annwin != null) return;
var winleft = screen.width-325-40;
var windowopts = "location=no, toolbar=no, menubar=no, status=no, scrollbars=no, resizable=no, "
windowopts += "width=325, height=240, top=20, left="+winleft;
annwin = window.open("annotate.htm", "annwin", windowopts);
}
That's the entire annotation script file; next, let's look at the annotation page itself.
The Annotation Page
First, a general note about this page. Whereas each Help topic is a separate HTML page, a single pagethis oneis used for all topic annotations. Therefore, there's no need to externalize the code; we'll just put it all right here, in "annotate.htm".
As the page loads, it displays a simple form with a 10-row by 35-column text area called "anntext" as well as Save, Delete, and Cancel buttons, which execute the functions "saveann()", "deleteann()", and "cancelann()", respectively. More about these functions later; here's the form.
<form name="annform">
<textarea name="anntext" rows=10 cols=35 onchange="changed=true"></textarea>
<p align=center>
<input type=button value=" Save " onclick="saveann()">
<input type=button value="Delete" onclick="deleteann()">
<input type=button value="Cancel" onclick="cancelann()">
</p>
</form>
Next, a silly little thing. In the <title> tag is the word "Annotation" plus a whole boatload of non-breaking spaces (coded in HTML as ). This has the effect of pushing the browser name off the right edge of the title bar so you don't see it. Why? Because it just bugs me, that's why.
<title>
Annotation ...
</title>
Of course, like the topic pages, this page also includes the cookie functions by reference.
<script src="cookiefuncs.js"></script>
Next, we declare two global variables; a string for the annotation cookie name and a boolean to indicate whether the annotation text has changed, called by programmers a "dirty flag" (honest).
var anncookie = "";
var changed = false;
The first thing the page does, via its onload event handler, is execute the "checkann()" function.
You know that the calling page's name is used as part of the cookie name, but so far the annotation window has no idea what its caller's name is. To get it, this page identifies its caller via the special window.opener handle, and retrieves the value of the other window's whoami variable into its own callername variable. Voilá, it now has the name of its calling page. How cool is that? (The correct answer is "Way.")
Then it builds the cookie name, "annotation-" plus the caller's name, and does a getCookie, retrieving any cookie text into another variable called, not surprisingly, cookietext. If the variable is not null, it is placed into the "anntext" text area; otherwise, the text area is already empty, so no action is needed. In either case, the focus is then set to the text area, which places the insertion point in the box, and we're up and running.
function checkann()
{
var callername = window.opener.whoami;
anncookie = "annotation-" + callername;
var cookietext = getCookie(anncookie);
if (cookietext != null) document.annform.anntext.value = cookietext;
document.annform.anntext.focus();
}
Edit, edit, edit, add, change, backspace, cut, paste, typeity type type, the user creates or modifies the annotation. The text area's onchange event handler (see the form code, above) sets the dirty flag to true at the slightest change, so the page can tell if the annotation actually has been edited. When the user is through editing, they click one of the three buttons, and here's where it gets interesting. Let's look at each one individually.
The Save button executes the "saveann()" function. This function quietly truncates the annotation text to 2400 characters (the limit for cookie size) if necessary, then writes the cookie with the "setCookie()" function, passing it the previously derived name, the cookie text, and an arbitrary one-year expiration. Then, it sets the focus to its calling window using the built-in window.opener handle. Why bother? Because regardless of where on the desktop the user clicks next, even momentarily sending the focus to the caller forces it to update its annotation icon. For an edited annotation, there will be no visible change, but for a new annotation, the icon will change from closed-eyes Clippy to open-eyes Clippy (or for a deleted annotation, the other way around). And this without reloading the page, I might add! It then closes itself by executing a "window.close()" statement.
function saveann()
{
var thetext = document.annform.anntext.value;
if (thetext.length > 2400) thetext = thetext.substring(0, 2400);
setCookie(anncookie, thetext, 365);
window.opener.focus();
window.close();
}
The Delete button executes the "deleteann()" function, which asks the user to confirm the deletion and bails out, returning to the annotation window, if the user clicks Cancel on the confirmation alert box. If they click OK, however, the function proceeds, killing the cookie with the "delCookie()" function. It then sets the focus to the calling window and closes itself, as above.
function deleteann()
{
if (!confirm ("Are you sure you want to delete this annotation?")) return;
delCookie(anncookie);
window.opener.focus();
window.close();
}
The Cancel button executes the "cancelann()" function, which performs a compound if statement (one with multiple conditions), returning to the annotation window only if the annotation text has changed and the user clicks Cancel on the confirmation alert box. This statement actually uses a JavaScript idiosyncrasy to its advantage. (Stay with me on this, now.) In a compound if that uses the "logical and" operator &&, both operands must be true for the entire if to be true. Therefore, JavaScript saves itself a few nanoseconds of processing time and only tests the second operand if the first operand is true, reasoning that if the first operand is false, the entire condition fails anyway, so there's no need to check the second operand. So in this case, if the dirty flag is false, meaning the annotation was not changed, the user never even sees the confirmation alert. Spiffy, eh?
This processing rule can also cause problems, though. It's easy to miss errors such as if (first == "Bob" && last == Smith) (missing quotes around Smith) or if (a < b && c = 5) (the = should be ==) because the second operands will never be tested if the first one is false. A word to the wise: Code compound ifs verrrrry carefully. This kind of error frequently survives repeated testing, and can be harder to pin down than Madonna's accent.
Anyway, following the confirmation question (or not), the function then sets the focus to the calling window and closes itself, as always.
function cancelann()
{
if (changed && !confirm("Are you sure you want to cancel your changes?")) return;
window.opener.focus();
window.close();
}
One more thing about the annotation page, and then we'll talk customization. As you've seen, each button's function does a "window.close()" as its last statement, but it's not over yet; the document does one other little thing on its way out. Its onunload event handler, fired when the document is purged from memory, uses the special window.opener handle once more to set the annwin variable in the calling page (which contains the annotation window's handle) to null. That is, it not only closes itself, but it tells the caller it's closed. Remember that the calling routine ("callannotate()" in the "annotate.js" file) checks the handle to make sure no annotation window is already open before opening one, so setting its own handle to null as the window closes clears the way for it to be reopened. Part of the annotation page's <body> tag showing that statement is below.
<body ... onunload="window.opener.annwin=null">
So that's about it. An extremely useful feature resurrected from our dearly departed WinHelp. Seems fitting somehow, don't you think?
But waitthere's more!
Implementation 
If you followed along with this article and ran the example files, you should have a pretty good idea of how the annotation feature works, and about now you're probably asking yourself one of two questions: "How can I actually apply this technique in my own Help systems?" or "Has Dave been smoking old socks again?" Although I'm not at liberty to answer the second question, the first one is fully covered below. Please read on.
Here are specific instructions for implementing this feature in your own Help topics. There are two sets of instructions, one for uncompiled web sites, and one for compiled HTML Help (CHM) files. Customization notes are included as appropriate.
Adding User Annotations to a Web Site
Note: These files have been tested in Internet Explorer 5+ and Netscape 6+, and the exact same code appears to work fine in either environment. Netscape 4 is a different matter, though, as its window opening, sizing, and positioning methods are... 'ow you say... "special," oui? Certainly the techniques could be made to work in Netscape 4, but not without adding a significant amount of browser-specific code, which for simplicity's sake I elected not to do. If you really must have these routines work in Netscape 4, contact me and we'll talk.
Follow these steps to add user annotations to a web site.
- Put the activation images "clippy0.gif" and "clippy1.gif" in your output folder (the one that gets FTP'd to your server).
Customization You may replace these images with any others of your choosing. They can be GIFs or JPGs, but should be visually distinguishable as "yes" and "no" annotation markers.
- Put the files "cookiefuncs.js", "annotate.js", and "annotate.htm" in your output folder.
Customization If you replace the images, edit the file "annotate.js" and replace the values assigned to "pclipoff.src" and "pclipon.src" in the "preload images" section, and the values of the <img> tag's src, width, and height attributes in the second document.write statement in the "placeimage()" function.
- In every topic page that is to have annotation capability, place the two script include lines for "cookiefuncs.js" and "annotate.js" in the <head> section. In the <body> section, put the script call to "placeimage()" exactly where you want the activation image to appear.
That's it for cross-browser Help. FTP the files to your server and start annotating.
Adding User Annotations to an HTML Help File
Note: You will almost certainly use a Help Authoring Tool (AuthorIT, ForeHelp, HDK, RoboHelp, Sevensteps, etc.) to create your compiled HTML Help (CHM) file. Since I don't know which one is your fave, these instructions are "genericized." They should work with whatever tool you use, as long as you have access to the actual HTML code for your topics.
Follow these steps to add user annotations to an HTML Help project.
- Put the activation images "clippy0.gif" and "clippy1.gif" in your project folder (the one your tool uses to store your project source).
Customization You may replace these images with any others of your choosing. They can be GIFs or JPGs, but should be visually distinguishable as "yes" and "no" annotation markers.
- Put the files "cookiefuncs.js", "annotate.js", and "annotate.htm" in your project folder.
Customization If you replace the images, edit the file "annotate.js" and replace the values assigned to "pclipoff.src" and "pclipon.src" in the "preload images" section, and the values of the <img> tag's src, width, and height attributes in the second document.write statement in the "placeimage()" function.
- Add the files "clippy0.gif" and "clippy1.gif" (or your own images), as well as the files "cookiefuncs.js", "annotate.js", and "annotate.htm" to your project's Baggage section.
Important: Do not import the file "annotate.htm" into your project as a topic. It needs to be accessed as an "external" HTML page.
- In every topic that is to have annotation capability, place the two script include lines for "cookiefuncs.js" and "annotate.js" in the <head> section. In the <body> section, put the script call to "placeimage()" exactly where you want the activation image to appear.
After your project successfully compiles, you need only distribute the CHM file, as all the support files are included as baggage.
And we're done!
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!

Copyright 2002, Dave Gash
Dave is the owner of HyperTrain dot Com, a Dallas firm specializing in hypertext training for Help system developers. A veteran software professional with over twenty years of development, documentation, and training experience, Dave is well known in the Tech Pubs community as an interesting and animated technical instructor. When he's not on the road doing training or chained to his PC developing new courses, Dave is a frequent speaker and presenter at Help-related conferences and seminars in the US and abroad.

|