Finite state machines are an organizing principle for those who design and implement complex behavior in event-driven programs, such as network adapters and compilers. Now, programmable Web browsers have opened a new event-driven environment to a new generation of applications. As browser-based applications, popularized by Ajax, become more complex, you can benefit from the discipline and structure that finite state machines offer.
Part 1 describes a tooltip widget for Web pages that has more elaborate behavior than the built-in implementation provided by popular Web browsers. This FadingTooltip widget fades into view after the cursor hesitates over an HTML element, and then fades from view after the tooltip has displayed for a while. The tooltip follows the cursor as it moves, even as it fades in and out, and the fading will reverse direction when the cursor moves away from the HTML element, or back over it. This behavior requires the FadingTooltip widget to respond to a variety of different events, and in some cases, the desired response to a particular event depends upon previous events.
Developers have used the finite state machine design pattern to organize event-driven programs like this. In Part 1, you applied applied its design discipline to produce a state table that defines the desired behavior, as shown in Figure 1.
Figure 1. State table for FadingTooltip widget
The rows and columns of the state table are labeled with the names of the events that the widget will respond to, and the states the widget will wait in between events. Each cell in the table specifies the actions the widget will take when a particular event occurs in a particular state. Table cells can also specify the next state the widget will transition to, after the actions are taken, or specify that the widget willl remain in the same state. Empty table cells indicate events that should not occur in particular states. Also, in Part 1, you compiled a list of state variables the widget will need to remember between events so that it can execute related actions in different cells.
Part 1 asserted that JavaScript is well-suited as an execution environment for finite state machines, and mentioned some of its capabilities that were relevant to the design phase. In this article, you will work through the details of translating your design into JavaScript, taking advantage of some elegant language features, and accommodating some inelegant details that make the implementation tricky.
Translating the design into JavaScript
Having completed the design of a finite state machine in Part 1, you are ready to implement the FadingTooltip widget in JavaScript. This is where the easy abstractions of the design phase give way to the harsh practicalities of real execution environments.
You'll consider only the most current versions of the most popular browsers: Netscape Navigator, Microsoft® Internet Explorer®, Opera, and Mozilla Firefox. Even this limited set of execution environments will provide headaches enough. You'll struggle with the details of actually hooking mouse and timer events from different browsers to a JavaScript program. An elegant JavaScript language feature called function closures will come to your rescue. You'll employ another elegant JavaScript language feature, associative arrays, to translate your state table directly into code. And, you'll see how to create and style a tooltip using an HTML division element, fill it with text and images, position it near the cursor, fade it into and out of view, and make it follow the cursor's movements.
But first, in the best spirit of object-oriented development, you need an object to contain everything else you'll implement, so we'll start with that.
An object to contain everything
The FadingTooltip widget will require a more sophisticated programming effort than is typical for the short snippets of JavaScript code that Web designers often cut and paste into HTML pages. Software engineers will be comfortable with the idea of grouping the variables and methods of the widget into an object, though the JavaScript object model might seem a bit peculiar to those schooled in Java™ and C++ programming. A JavaScript object is entirely adequate for your needs: it will let you group variables and methods into an object, and then create separate instances of the data for each tooltip. The object instances will share common code and run independently.
In JavaScript, an object constructor is just a function -- the function's name is the object's name. Your widget will need to know which HTML element to hook itself into and what content to display in its tooltip, so you'll specify these as arguments to the constructor and save them in the object. (You will also need a way to set parameters related to the behavior and appearance of the tooltip, so you will specify an argument for that too, and use it later in this article.) Variables are untyped, so the object constructor might begin with code shown in Listing 1.
Listing 1. JavaScript code for FadingTooltip object constructor
function FadingTooltip(htmlElement, tooltipContent, parameters) {
this.htmlElement = htmlElement; // save pointer to HTML element whose mouse events
// are hooked to this object
this.tooltipContent = tooltipContent; // save text and HTML tags for the tooltip's
// HTML Division element
...
|
In JavaScript, you can add object properties, which can be variables or methods, to an object when it is created, or at
any time thereafter, just by assigning a value to them, as the constructor
does for the properties this.htmlElement and
this.tooltipContent.
In JavaScript, an object prototype is a template for creating new instances of an object; it defines an object's initial properties and their initial values. You'll begin your object prototype with the state variables from Part 1 that the widget will need, as shown in Listing 2.
Listing 2. JavaScript code for FadingTooltip object prototype
FadingTooltip.prototype = {
currentState: null, // current state of finite state machine (one of the state
// names in the table below)
currentTimer: null, // returned by setTimeout, non-null if timer is running
currentTicker: null, // returned by setInterval, non-null if ticker is running
currentOpacity: 0.0, // current opacity of tooltip, between 0.0 and 1.0
tooltipDivision: null, // pointer to HTML division element when tooltip is visible
lastCursorX: 0, // cursor x-position at most recent mouse event
lastCursorY: 0, // cursor y-position at most recent mouse event
...
|
The object prototype is a good place to define nearly everything related to the finite state machine: the state table, its actions, and their parameters. You have one little loose end -- hooking cursor events -- to finish up in the object constructor, then the rest of this article is devoted to filling out the object prototype.
As mentioned during the design phase in Part 1, browsers can pass events to JavaScript when the cursor moves onto, moves around within, and moves away from an HTML element. These events include helpful information, such as the event type and the current position of the cursor on the page. Browsers pass events by calling functions that were registered in advance. Unfortunately, the details of how these functions are registered and the arguments passed to them vary by browser. To ensure that oyur finite state machine hooks cursor events in all popular browsers, you'll need to implement three different event models. Fortunately, the code for each event model is quite compact. Unfortunately, the compactness of that code belies its complexity.
Mozilla Firefox, Opera, and recent versions of Netscape Navigator
support a standardized event model proposed by the World Wide Web Consortium (W3C). This is your first choice because
it's easy to register (and unregister) event functions, and chaining
through multiple registered functions is handled by the browsers. If
it is available, you can hook cursor events by calling the
addEventListener method of the HTML element, passing an
event type and a function to be called when that event occurs on that
HTML element, as shown in Listing 3.
Listing 3. JavaScript code for hooking cursor events
function FadingTooltip(htmlElement, tooltipContent, parameters) {
...
htmlElement.fadingTooltip = this;
if (htmlElement.addEventListener) { // for FF and NS and Opera
htmlElement.addEventListener(
'mouseover',
function(event) { this.fadingTooltip.handleEvent(event); },
false);
htmlElement.addEventListener(
'mousemove',
function(event) { this.fadingTooltip.handleEvent(event); },
false);
htmlElement.addEventListener(
'mouseout',
function(event) { this.fadingTooltip.handleEvent(event); },
false);
}
...
|
The second argument for the addEventListener calls are
anonymous functions, which are functions that do not have names.
This is your first opportunity to define functions within other functions in
JavaScript, but it won't be the last, so get used to it now. You can use the
function function anywhere in JavaScript code to
define an anonymous function on-the-fly. It returns a pointer
to the function, which can then be used like any other reference value. In
yur FadingTooltip widget, you will pass function pointers to other functions as arguments, test them for null, assign them to variables, and
declare them as object methods.
The anonymous functions passed to the addEventListener
method don't appear to do much. When cursor events occur, the browser
will call them, passing an event object to them that they will pass on to the handleEvent method of the FadingTooltip
object. The browsers' event objects contain the event type, in
addition to the cursor position, so one handleEvent
method can handle all of the cursor events the widget must respond to.
Those simple anonymous functions also perform another important
but subtle task. In the W3C event model, functions registered with
the addEventListener method of an HTML element become
methods of that element, so when browsers call them, the built-in
this variable points at that HTML element. But the handleEvent method needs a pointer to the FadingTooltip object that contains your state variables. One way
to accomplish this is to add a fadingTooltip property
to the HTML element that points at the FadingTooltip object, and then
follow it to invoke the handleEvent method of your object. This makes this point to the FadingTooltip object when the handleEvent method executes.
Hooking cursor events in Internet Explorer
Microsoft Internet Explorer does not currently support the proposed W3C standard event model, but does offer a similar event model of its own. The differences, as follows, are easy to accommodate:
- Event types are slightly different
- Registered functions do not become methods of the HTML element
- Event objects are left in the global window object
If it is available, you can hook events by
calling the attachEvent method of your HTML element,
passing slightly different event types and functions, as shown in Listing 4.
This is your first opportunity to use function closures to enclose variables in function definitions, but it won't be the last, so get used to this, too.
Listing 4. JavaScript code for hooking cursor events in Internet Explorer
function FadingTooltip(htmlElement, tooltipContent, parameters) {
...
else if (htmlElement.attachEvent) { // for MSIE
htmlElement.attachEvent(
'onmouseover',
function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
htmlElement.attachEvent(
'onmousemove',
function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
htmlElement.attachEvent(
'onmouseout',
function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
}
...
|
The functions registered with the attachEvent method of
an HTML element do not become methods of that element. When cursor
events occur, the browser will call them, but the built-in this
variable will point at the global window object, not
the HTML element, so your functions cannot locate their FadingTooltip
object from the pointer saved in the HTML element.
Fortunately, the anonymous function
definitions are within the lexical scope of the htmlElement
argument of the object constructor. Simply by using the
htmlElement variable in the anonymous function
definitions you can enclose it with those functions. This is
called a function closure: when a function is defined
within another function, and the inner function uses the local variables of the outer
function, JavaScript saves those variables with the definition of the inner function. Later, after
the outer function has returned, when the inner function is called, the outer function's local variables will still be available to it.
Here, JavaScript will retain the value of the htmlElement
variable after the constructor returns so that it is still available
to the anonymous functions when they are called by the browser. This
allows them to locate their HTML elements and follow the pointers to
their FadingTooltip object without help from the browser.
Since function closures are a feature
of the JavaScript language, they work equally well in browsers that
use the W3C event model. You could have used them to enclose the
value of the constructor's htmlElement argument with
the anonymous functions defined in the previous section, instead of
using the built-in this variable.
Hooking cursor events in older browsers
For older browsers that do not support
either the W3C or Internet Explorer event models, you will have to
hook events using the original event model provided by early versions
of Netscape Navigator. It is supported by all popular browsers and
is widely used by Web designers for animating Web pages, but is
the last choice for implementing more complex applications because it
does not chain through multiple event handlers.
To do this yourself, enclose pointers to
previously registered event functions in the definitions of your own
event functions, and then call them after calling your
handleEvent method, as shown in Listing 5.
Listing 5. JavaScript code for hooking cursor events in older browsers
function FadingTooltip(htmlElement, tooltipContent, parameters) {
...
else { // for older browsers
var self = this;
var previousOnmouseover = htmlElement.onmouseover;
htmlElement.onmouseover = function(event) {
self.handleEvent(event ? event : window.event);
if (previousOnmouseover) {
htmlElement.previousHandler = previousOnmouseover;
htmlElement.previousHandler(event ? event : window.event);
}
};
... and similarly for 'onmousemove' and 'onmouseout' ...
}
}
|
Note, though, that this approach is incomplete. It allows the widget to register for the same events that other widgets have already registered to handle, and then chain to them, but it does not allow other event functions to unregister, since the chaining pointers are not accessible to them.
For
variety, the code copied the constructor's this variable, which
points to the FadingTooltip object, into a local variable named
self, and then used the self pointer to locate the FadingTooltip
object in the anonymous function definitions. This encloses a
pointer to your FadingTooltip object in the anonymous function
definitions so that they can locate it directly when any browser
calls them, without depending upon the browser to provide a pointer
to the HTML element, and without the need to save a pointer to the
FadingTooltip object in the HTML element.
You could have enclosed a pointer to your FadingTooltip object in the anonymous functions you defined for the W3C and Microsoft event models as well. This would eliminate the need to save pointers to your objects in the HTML elements, and employ the same technique for locating the HTML element in all event models. The constructor in the source code is coded this way.
Now that you have hooked cursor events in all popular browsers, your object constructor is complete and you can return to the object prototype.
Setting timers and hooking timer events
You've completed the FadingTooltip constructor, and can continue filling out its prototype. In JavaScript, an object prototype can contain methods as well as variables; methods are simply variables that point at functions. You'll start with some general-purpose methods that start and cancel timers.
As you might recall from the design phase in Part 1, JavaScript provides two types of timers, one-shot timeouts and repeating timetick, and the finite state machine needs both. You can start the timers by calling the
setTimeout or setInterval function,
passing a time value (in milliseconds) and a function to be called
when a timeout expires or a timetick occurs, respectively. They
return opaque references that you can later pass to the clearTimeout
or clearInterval function to cancel the timer.
Browsers will call the timer event functions that were passed as
arguments to the setTimeout and setInterval
function, once when the timeout value has elapsed, or
repeatedly each time the timetick interval occurs,
respectively, unless or until their timers are canceled. But these
timeout and timetick functions do not
become methods of any object. When the browser calls them, the this
variable points at the global window object. Nor do browsers
pass any information about the timer event to these functions.
After our struggles with cursor events, hooking timer events is easy. When you set a timer, you will copy the built-in this variable, which points at the
FadingTooltip object containing our state variables, into a local
variable named self that is within the lexical scope of the
setTimeout and setInterval function
calls. You will define anonymous functions that use the self
variable and pass them to the setTimeout and
setInterval functions as arguments. This encloses the self variable with the function definitions, so it is still
available when the browser calls the functions, as shown in Listing 6.
Listing 6. JavaScript code for setting timers, and hooking timer events
FadingTooltip.prototype = {
...
startTimer: function(timeout) {
var self = this;
this.currentTimer =
setTimeout( function() { self.handleEvent( { type: 'timeout' } ); },
timeout);
},
startTicker: function(interval) {
var self = this;
this.currentTicker =
setInterval( function() { self.handleEvent( { type: 'timetick' } ); },
interval);
},
...
|
Your timer event functions don't do much more than your cursor event
functions. They create a trivial timer event object
that contains only an event type -- either timeout
or timetick -- and pass it to the same
handleEvent method that handles cursor events.
Creating the action/transition table
In JavaScript, object prototypes can contain data structures such as arrays and other objects, as well as variables and methods. The elements of ordinary arrays are indexed by integers, whereas the elements of associative arrays are indexed by names instead of numbers. In JavaScript, associative arrays and objects are just different syntax for accessing the same data: object properties can be accessed as associative array elements, as shown in Listing 7.
Listing 7. JavaScript code for accessing object properties as associative array elements
if ( htmlElement.fadingTooltip == htmlElement["fadingTooltip"] ) ... // always true |
You will take advantage of this by implementing your state table as a two-dimensional associative array of
functions. You'll use state names and event names directly as the
array's indexes. The non-empty cells of the array will point to
anonymous functions that take actions for an event by calling utility
methods (such as those that start and cancel timers), and then return the next state. The core of your
handleEvent method will invoke these
action/transition functions using array syntax,
something like the code in Listing 8.
Listing 8. JavaScript code for calling anonymous functions stored in associative arrays
var nextState = this.actionTransitionFunctions[this.currentState][event.type](event); |
The handleEvent method will access the actionTransitionFunctions table as an associative
array, using the current state and the event type as indexes, and
select a function to call. It will pass the event object to that
function as its argument. The function will take whatever actions
are needed, and then return the name of the next state.
Because associative arrays are objects (and vice versa), you can
define the actionTransitionFunctions table using object
syntax, even though the handleEvent method will access
it using array syntax. For example, in the initial state of
Inactive, the only expected event is mouseover,
so you can define a function to handle that situation, as shown in Listing 9.
Listing 9. JavaScript code for storing an anonymous function as an object property
FadingTooltip.prototype = {
...
initialState: 'Inactive',
actionTransitionFunctions: {
Inactive: {
mouseover: function(event) {
this.cancelTimer();
this.saveCursorPosition(event);
this.startTimer(this.pauseTime*1000);
return 'Pause';
}
},
...
|
The prototype for the FadingTooltip object contains the
property actionTransitionFunctions, whose value is
another object. It contains a property named Inactive,
whose value is yet another object. It contains only one property,
named mouseover, whose value is a function. When a
mouseover event occurs in Inactive state,
the handleEvent method will call this function. It
expects an argument named event, takes three actions by
calling three utility functions, and then returns Pause
as the name of the next state. The actions include saving the cursor
position, which browsers store in mouse event objects, and starting
a timer, whose timeout value is a parameter named pauseTime
(specified in seconds, which it converts to milliseconds, as required
by the startTimer method).
Your widget will need to respond to three different
events in the Pause state: the mousemove,
mouseout, and timeout events. You will
define a Pause object in the
actionTransitionFunctions table that has a property for
each of these event types, as shown in Figure 10.
Listing 10. JavaScript code for functions that respond to cursor events in Pause state
FadingTooltip.prototype = {
...
actionTransitionFunctions: {
...
Pause: {
mousemove: function(event) {
return this.doActionTransition('Inactive', 'mouseover', event);
},
mouseout: function(event) {
this.cancelTimer();
return 'Inactive';
},
timeout: function(event) {
this.cancelTimer();
this.createTooltip();
this.startTicker(1000/this.fadeRate);
return 'FadeIn';
}
},
...
|
When a mousemove event occurs in Pause
state, the handleEvent method will call a function that
simply calls the doActionTransition method, passing on
its event argument, and returning whatever it returns.
The doActionTransition method, as you might imagine,
accesses the actionTransitionFunctions table using its
first two arguments as array indexes, similarly to the handleEvent
method, and passes its third argument to the function it finds there.
When a mouseout event occurs, your code will call a function
that cancels the timer started earlier in this section, and then
transitions back to Inactive state.
Or, when a timeout event occurs, you will cancel any timer that might be running, create the tooltip with an initial opacity of zero, start a ticker, and transition to FadeIn state.
As yet
another example of functions in the actionTransitionFunctions
table, you'll define a function to handle timetick
events in the FadeIn state, as shown in Listing 11.
Listing 11. JavaScript code for a function that responds to timer events in FadeIn state
FadingTooltip.prototype = {
...
actionTransitionFunctions: {
...
FadeIn: {
...
timetick: function(event) {
this.fadeTooltip(+this.tooltipOpacity/(this.fadeinTime*this.fadeRate));
if (this.currentOpacity>=this.tooltipOpacity) {
this.cancelTicker();
this.startTimer(this.displayTime*1000);
return 'Display';
}
return this.CurrentState;
}
},
....
|
Each time a timetick event occurs in FadeIn
state, the handleEvent method will call a function that
increases the opacity of the tooltip slightly. The duration of the
fade in time (specified in seconds), the animation rate
at which opacity is increased from zero (specified in steps per
second), and the maximum opacity (specified as a float between 0.0
and 1.0), are all parameters. The function will return the current
state, leaving the finite state machine in FadeIn
state, until the tooltip's opacity reaches the maximum opacity
parameter. Then it will cancel the ticker, start a timer to display the tooltip, and transition to Display State.
The remainder of the functions in the
actionTransitionFunctions table are defined in a similar way.
For details, please consult the complete source code, which is heavily
commented, and compare it to Figure 1.
Implementing the event handler
We have referred so often, and with such portent, to the
handleEvent method that its implementation might seem
anti-climactic, as shown in Listing 12.
Listing 12. JavaScript code for event handler
FadingTooltip.prototype = {
...
handleEvent: function(event) {
var actionTransitionFunction =
this.actionTransitionFunctions[this.currentState][event.type];
if (!actionTransitionFunction)
actionTransitionFunction = this.unexpectedEvent;
var nextState = actionTransitionFunction.call(this, event);
if (!this.actionTransitionFunctions[nextState])
nextState = this.undefinedState(nextState);
this.currentState = nextState;
},
...
|
The actual implementation of accessing the
actionTransitionFunctions table differs from the suggestion in a preceding section. The method does
select a function to call from the actionTransitionFunctions
table using the current state and event type as associative array indexes. However,
the method copies a pointer to the selected function into a local
variable, and then calls the function with the call
method of the function object, rather than calling it directly. You can do this because function objects can be assigned to variables,
just like any other value. You must do this because the built-in
this variable needs to point at a FadingTooltip object
while the function executes. If you actually called the function
directly from the actionTransitionFunctions table using
array indexes, as suggested earlier, the this variable would point into the
table. The call method of the function sets the this
variable to its first argument, and then calls the function, passing
the remainder of its arguments.
Remember that the actionTransitionFunctions table is
sparse; you defined functions for the events you expect in each state,
and left all other cells empty. The handleEvent method
will handle any unexpected events by calling the unexpectedEvent
method. Or, should an action/transition function return some value
that is not a valid state, it will call the undefinedState
method. These methods will cancel any running timers, delete the
tooltip if one has been created, and return the finite state machine
to its initial state. One method is shown in Listing 13; the other is nearly
identical.
Listing 13. JavaScript code for unexpected event handler
FadingTooltip.prototype = {
...
unexpectedEvent: function(event) {
this.cancelTimer();
this.cancelTicker();
this.deleteTooltip();
alert('FadingTooltip received unexpected event ' + event.type +
' in state ' + this.currentState);
return this.initialState;
},
...
|
These methods will display an alert dialog describing the error, in the hope that some helpful user will send a problem description to the code's author.
Displaying the tooltip, finally
With nothing left to implement but the tooltip itself, you can't postpone that any longer.
You want the tooltip to appear near the cursor when the timeout
event occurs in Pause state, but browsers do not pass
the cursor position to timer events. Fortunately, browsers do pass
the cursor position to cursor events, so you will save it in state
variables by calling this saveCursorPosition method
when cursor events occur, as shown in Listing 14.
Listing 14. JavaScript code for saving cursor position
FadingTooltip.prototype = {
...
saveCursorPosition: function(event) {
this.lastCursorX = event.clientX;
this.lastCursorY = event.clientY;
},
...
|
The tooltip is an HTML division element containing whatever text,
images, and markup are passed to the constructor in its
tooltipContent argument. The createTooltip
method is shown in Listing 15.
Listing 15. JavaScript code for creating a tooltip
FadingTooltip.prototype = {
...
createTooltip: function() {
this.tooltipDivision = document.createElement('div');
this.tooltipDivision.innerHTML = this.tooltipContent;
if (this.tooltipClass) {
this.tooltipDivision.className = this.tooltipClass;
} else {
this.tooltipDivision.style.minWidth = '25px';
this.tooltipDivision.style.maxWidth = '350px';
this.tooltipDivision.style.height = 'auto';
this.tooltipDivision.style.border = 'thin solid black';
this.tooltipDivision.style.padding = '5px';
this.tooltipDivision.style.backgroundColor = 'yellow';
}
this.tooltipDivision.style.position = 'absolute';
this.tooltipDivision.style.zIndex = 101;
this.tooltipDivision.style.left = this.lastCursorX + this.tooltipOffsetX;
this.tooltipDivision.style.top = this.lastCursorY + this.tooltipOffsetY;
this.currentOpacity = this.tooltipDivision.style.opacity = 0;
document.body.appendChild(this.tooltipDivision);
},
...
|
If a CSS class name is specified as a parameter, you will apply it to your HTML division element's appearance. Otherwise, you'll apply some basic styling by default. But a few aspects of the tooltip's behavior depend upon its appearance, such as its position and opacity, so you will override anything related to these properties that might be specified in a stylesheet. The HTML division element will be positioned on the page with absolute coordinates, starting near the last saved cursor position, above any other layered elements. Its initial opacity will be zero, which is fully transparent.
Each time a timetick event occurs in FadeIn
or FadeOut state, the fadeTooltip method
will be called to increase or decrease the opacity of the tooltip
slightly, while ensuring that it remains within a range between zero
and the maximum opacity parameter, as shown in Listing 16.
Listing 16. JavaScript code for fading a tooltip
FadingTooltip.prototype = {
...
fadeTooltip: function(opacityDelta) {
this.currentOpacity += opacityDelta;
if (this.currentOpacity<0)
this.currentOpacity = 0;
if (this.currentOpacity>this.tooltipOpacity)
this.currentOpacity = this.tooltipOpacity;
this.tooltipDivision.style.opacity = this.currentOpacity;
},
...
|
The action/transition functions also need utility methods that move and delete the tooltip. Their implementations are straightforward, and fully described in the source file commentary.
As mentioned occasionally in this part of the article, you need to define parameters before the implementation can be considered complete. They are properties of the object prototype, but unlike the state variables, they have default values as shown in Listing 17.
Listing 17. JavaScript code for defining parameters in the object prototype
FadingTooltip.prototype = {
...
tooltipClass: null, // name of a CSS style to apply to the tooltip, or
// 'null' for default style
tooltipOpacity: 0.8, // maximum opacity of tooltip, between 0.0 and 1.0
// (after fade-in, before fade-out)
tooltipOffsetX: 10, // horizontal offset from cursor to upper-left
// corner of tooltip
tooltipOffsetY: 10, // vertical offset from cursor to upper-left
// corner of tooltip
fadeRate: 24, // animation rate for fade-in and fade-out, in
// steps per second
pauseTime: 0.5, // how long the cursor must pause over HTML
// element before fade-in starts, in seconds
displayTime: 10, // how long to display tooltip (after fade-in,
// before fade-out), in seconds
fadeinTime: 1, // how long fade-in animation will take, in seconds
fadeoutTime: 3, // how long fade-out animation will take, in seconds
...
};
|
The optional parameters argument of the object
constructor is an object coded in JavaScript Object Notation
(sometimes called JSON) that can override the
default values for any of these properties, as shown in Listing 18.
Listing 18. JavaScript code for initializing parameters in the object constructor
function FadingTooltip(htmlElement, tooltipContent, parameters) {
...
for (parameter in parameters) {
if (typeof(this[parameter])!='undefined')
this[parameter] = parameters[parameter];
}
...
};
|
The constructor examines each property in its parameters
argument; for each one, if the property exists in the prototype, then
its value overrides the parameter's default value. Remember that the
prototype is an object, so it is also an associative array. Here
again, you used object notation to define the parameters, and array notation to access them.
And with that, the implementation of the FadingTooltip is complete. You can download the actual source code for the constructor and prototype.
Before moving on to test the implementation, a few words about performance are in order.
Browsers execute JavaScript programs synchronously. When an event that is hooked occurs, browsers call its event handler and wait for it to return before continuing on to the next event. If more events occur before the event handler returns, browsers enqueue them until the event handler does return, and then process the queued events in order, one at a time, synchronously. If an event handler takes too long, it might delay the browser's own responses to events that have not been hooked. Users might think the program is sluggish, or that the browser is malfunctioning.
It is always important to keep event handlers as short as
possible, but this is especially important in programs likethis,
which simulate animation with closely spaced timer events. If a
timetick event handler takes longer than the ticker
interval, timetick events will pile up on top of each
other in the browser's event queue, saturating the processor and
making the browser unresponsive.
For example, with an default animation rate of 24 steps per
second, a timetick event handler has at most 40
milliseconds to do everything it needs to before returning to the
browser, assuming that it has the processor all to itself. On modern
workstations, this is enough time to do a lot of processing. Your
goal, though, is not to do as much as you can in the time available,
but to use as little processor time as possible. If your
implementation's processor usage is negligible, its animations will
run smoothly and our program will be responsive even when the
processor is heavily loaded with other activity.
Avoid the temptation to set your animation rate at 60 or 85
steps per second, thinking, perhaps, that matching your monitor's refresh rate will produce smoother animation. This reduces the time between
timetick events to about 12 milliseconds. If your
timetick event handler takes longer than this, or there
is contention for the processor, your animations might become jerky, or
the browser might become unresponsive.
With the implementation complete, you are ready to test your code on some real browsers. That is the subject of Part 3 in this series. Remember, though, that development is an iterative process; you might need to return to the design or implementation phase ...
- Demo: Examples of browser tooltips and FadingTooltip widgets
- Code sample: HTML source code for examples of browser tooltips and FadingTooltip widgets
- Code sample: JavaScript source code for FadingTooltip widget
Learn
- Ajax: A New Approach to Web Applications: Read Jesse James Garrett's article that coined the term Ajax.
- The book
JavaScript: The Definitive Guide (David Flanagan, published repeatedly by O'Reilly Media between 1996 and 2006): Find exhaustive information on how JavaScript works in browsers.
- The Standard ECMA-262: ECMAScript Language Specification (Ecma International, 1999) : Peruse the authoritative definition of the JavaScript language implemented by popular browsers.
- Document Object Model (DOM) Level 2 Events Specification (W3C, 2000): Refer to this spec for the authoritative definition of the DOM Level 2 event model.
- The Gecko DOM Reference (Mozilla): Get the authoritative definition of the object interfaces, including events, implemented by the Firefox browser.
- HTML and DHTML Reference (Microsoft): Refer to the authoritative definition of the object interfaces, including events, implemented by the Internet Explorer browser.
- Chapters 21 "Protocol Representation with Finite State Models" by Andre A. S. Danthine, and 25 "Executable Representation and Validation of SNA" by Gary D. Schultz, et. al. in Computer Network Architectures and Protocols (edited by Paul E. Green, Jr., Plenum Press, 1982):
Read historic examples of finite state machines applied to computer network protocols.
- Chapter 3.5 "Finite Automata" in Compilers: Principles, Techniques, ad Tools (Alfred V. Aho et. al., Addison-Welsley, 1986): Read a description of how finite state machines are applied to computer language compilers.
- Chapter 5 "Behavioral Patterns" in Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma et. al., Addison-Welsley, 1995): Read about the State pattern for a discussion of implementing finite state machines.
- Chapter 13.25 "TCP State Machine" in Internetworking with TCP/IP (Douglas E. Comer, Simon and Schuster Company, 1995): Read to understand the finite state machine that underlies the Internet.
- Chapter 15 "State Machines" in the Unified Modeling Language 2.0 Superstructure Specification (Object Management Group, 2004): Read for a complete graph representation of finite state machines.
- State Chart XML (SCXML): State Machine Notation for Control Abstraction (RJ Auburn et. al., W3C): Read a proposed XML representation of finite state machines.
- developerWorks Web development zone: Expand your site development skills with articles and tutorials that specialize in Web technologies.
- The technology bookstore: Browse for books on these and other technical topics.
- developerWorks technical events and webcasts: Stay current with jam-packed technical sessions that shorten your learning curve, and improve the quality and results of your most difficult software projects.
Get products and technologies
- Download the following browsers to test the FadingTooltip widget:
- Mozilla Firefox browser
- Microsoft Internet Explorer browser
- Netscape Navigator browser
- Opera browser
- developerWorks Web development Downloads and products area: Find more free downloads.
Discuss
- developerWorks blogs: Get involved in the developerWorks community!

Edward Pring holds an M.S. degree in Computer Science from New York University and a B.S. degree in Mathematics from Stanford University. As part of IBM Research, he has contributed to a wide range of IBM products and technologies, including operating systems, publishing applications, terminal emulators for mainframes, virus protection for personal computers, network automation for the Digital Immune System, and visualization and performance analysis for Web Services. His patent portfolio spans all of these fields.



