JavaScript – Clean Up Your Popups

Keeping code easy to maintain requires a little foresight but can easily
pay big dividends in the end. I was reminded of this recently by a
consulting assignment that had me ‘cleaning up’ the JavaScript in a
somewhat elaborate PDF document that relied heavily on popup menus.

By now, I’m sure you’re aware that in version 4.0 (and 4.05) of Acrobat,
there lurks an undocumented JavaScript method called popUpMenu(), which is
a method of the App class that lets you attach custom popup menus to
mouse-down events in form fields. To use this method, supply an argument
list consisting either of the strings you want to use for menu choices, or
an array of strings. Each array will be treated as a nested item. For
example:


var reply = app.popUpMenu(
'Jody Foster',
'Julia Roberts',
'-',
['Arquette','Patricia','Rosanna'],
['Olsen','May-Kate','Ashley'],
'-',
'Demi Moore'
);

I suggest, by the way, that you get used to formatting your popup code this
way (with line breaks separating the choices) so that you can more easily
read and replace items later.

It’s important to note that this code should be attached to the mouse-down
(not the mouse-up) event for whatever field uses it, because when it
executes, it will make a menu appear until the next mouse-up. If the code
is attached to a mouse-up handler, the menu will just flash by and the user
won’t have a chance to select anything. Here is what the popup created by
the above code looks like at runtime:

Note that wherever a hyphen has been used in the code, a separator occurs
in the popup menu. Likewise, wherever an array was used, a nested submenu
occurs.

Return Values

The return value for app.popUpMenu() is the string for the item chosen by
the user. If the user lets go of the mouse without selecting anything, the
return value is null. It’s important to check for this before executing any
code that depends on a non-null value. Do something like this:


if (reply != null) {
// ...do something...
}
else {
// ...do something else
}

Cleaner Popup Code

One problem you can quickly get into with popup code is that you find
yourself writing a lot of specialized, one-off, inline code to handle
possible user selections. Here’s a quick example. Suppose you have a long
document with lots of section headings but you don’t want to waste screen
space by making the user keep a Bookmarks frame open, so you’ve decided to
provide a jump menu on page one in the form of a popup:


var choices = ['Go to:',
'Introduction', ?
'Business Model',
'Key Personnel',
'Projections',
'For Further Info' ];

var reply = app.popUpMenu(choices);

// now process user's selection:
if (reply != null) {

if (reply == 'Introduction')
this.pageNum = 3;

if (reply == 'Business Model')
this.pageNum = 6;

if (reply == 'Key Personnel')
this.pageNum = 19;

// etc.
}

The idea is to let the user select a section heading, then take him there
by setting pageNum to the proper page number. But it takes a lot of
repetitive code to do it. Most programmers would try to rewrite the
selection handling code as a ‘switch’ or ‘case’ statement block, but that
doesn’t really solve the problem. It’s still a lot of code.

Associative Arrays

A cleaner way to write this kind of code is using an associative array: an
array that associates ‘Introduction’ with 3, ‘Business Model’ with 6, etc.
In JavaScript, you can do this by creating a custom object and attaching
properties to it as follows:


var myArray = new Object();

myArray['Introduction'] = 3;
myArray['Business Model'] = 6;

... etc.

The square brackets (array-index notation) let you create new properties
based on string values. Usually, of course, you use dot notation to specify
object properties. Brackets are interchangeable with dot notation. The
advantage of brackets is that they allow the use of strings containing
spaces.

The above code will work, but how do you go from this to constructing the
argument list for popUpMenu()? The answer is, you can use JavaScript’s
for-in loop, which enumerates all the properties of an object:


var choices = new Array();

for (var i in myArray) // enumerate properties
choices.push(i); // add them to choices

// now choices == ['Introduction','Business Model', ...]

var reply = app.popUpMenu(choices);

When you check the return value of popUpMenu(), you can process it by just
finding the associated lookup value. In other words, we can now ‘case out’
the user’s selection by doing:


if (reply != null)
this.pageNum = myArray[reply]; // done!

This replaces tons of ‘if’ statements, collapsing a huge block of code to
one expression.

So far, so good. But it’s possible to do even better.

Code Organization

For purposes of easier code maintenance, it’s important to separate code
from data. Hence, our associative array should really be in its own
(top-level) area, outside the function that accesses it, so that it can be
maintained or updated separately from the code that uses it. Likewise, the
action code should be in its own top-level function. The mouse-down handler
should just refer to this function.

Let’s declare our associative array as follows, putting it in a top-level
area called top_level_stuff (using Tools:Forms:Document JavaScripts… in
Acrobat):


myArray = {
'Go to:' : null,
'Introduction' : 3,
'Business Model' : 6,
'Key Personnel' : 19,
'Projections' : 25,
'For Further Info' : 40
};

This notation, using curly braces to set off a list of choice/value pairs
separated by colons, is called object-literal notation. It makes ‘myArray’
an object with properties of ‘Go to’, ‘Introduction’, etc., and with those
properties initialized to the values shown on the right side of each colon.

Notice very carefully that I have NOT used the ‘var’ declarator in front of
myArray! This was deliberate. Leaving the ‘var’ keyword off makes myArray a
global, but it is not parented to the Global object. Rather, it will be
parented to the Doc object (‘this’). That means the scope of myArray will
be document-wide but not app-wide. Any routine anywhere in the document can
access myArray. You can access it either by writing this.myArray or just
myArray.

Also, remember that we’re putting myArray in a top-level area (using
Tools:Forms:Document JavaScripts), but not inside a function. Just enter
the declaration ‘out in the open,’ by itself. Whenever the PDF document
opens, the variable will be available, immediately, to any routine that
needs it, document-wide.

Also at the top level, let’s declare a function called Do_Jump() that looks
like this:


function Do_Jump() {

var choices = new Array();

for (var i in myArray) // enumerate properties
choices.push(i); // build a list

var reply = app.popUpMenu(choices);

if (reply) this.pageNum = myArray[reply];

} // end function

Now in the JavaScript action for our jump button, where we used to have all
our code, we have just one statement: Do_Jump().

Was It Worth It?

Separating code from data greatly enhances the readability and
maintainability of this kind of code. And putting the action code in its
own top-level function encourages code reuse. This may not be obvious to
you from the above example. But in a recent consulting assignment, my
client had a jump button (using the same huge tangle of inline code) at the
bottom of EVERY page in the document! Replacing 25 lines of code with one
Do_Jump() statement, 100 times for a 100-page document, saved this client a
lot of space. Plus, I showed him how to clone the jump button using
template spawning, for even better storage efficiency (and ease of document
creation).

In the future, as this client’s document grows, he will be able to add or
change jump headings and jump targets very easily, just by changing the
values in one easy-to-read object-literal declaration.

Is code reorganization worth the trouble? If you write enough JavaScript,
you’ll learn the only possible answer.

You May Also Like

About the Author: Kas Thomas

Leave a Reply