1  ////////////////////////////////////////////////////////////////////////////////
  2  //
  3  // This file contains the JavaScript source for the FadingTooltip widget 
  4  // described in this IBM developerWorks article:
  5  // 
  6  //    Finite state machines in JavaScript, Part 1: Designing a widget
  7  //    http://www.ibm.com/developerworks/library/wa-finitemach1/
  8  //
  9  // (c) Copyright IBM Corporation 2006. All rights reserved.
 10  // 
 11  // U.S. Government Users Restricted Rights - Use, duplication or disclosure 
 12  // restricted by GSA ADP Schedule Contract with IBM Corp.
 13  // 
 14  //                                            -- Edward Pring <pring@us.ibm.com>
 15  // 
 16  ////////////////////////////////////////////////////////////////////////////////
 17  
 18  
 19  // The constructor for the FadingTooltip object creates and initializes an object 
 20  // instance.  The constructor"s arguments are:
 21  //
 22  //     "htmlElement" is a required pointer to an HTML element.  A tooltip will be
 23  //     displayed when the cursor pauses over this HTML element.
 24  //
 25  //     "tooltipContent" is a required string containing the text and HTML tags to
 26  //     be displayed in the tooltip.
 27  //
 28  //     "parameters" is an optional JSON object containing zero or more parameter
 29  //     values that determine how the tooltip will behave.  The parameter names 
 30  //     and default values are listed below.
 31  //
 32  //     "this" points to the new FadingTooltip object instance
 33  //
 34  // The constructor throws an exception if any of these arguments are invalid.  It
 35  // implicitly returns a pointer to the new object instance.
 36  
 37  function FadingTooltip(htmlElement, tooltipContent, parameters) { 
 38  
 39      // Do some basic validation of the constructor"s required arguments, and 
 40      // throw an exception if any are obviously invalid.  There is obviously 
 41      // room for more rigorous validation here.
 42      
 43      if (!htmlElement || typeof(htmlElement)!="object") throw "Sorry, "htmlElement" argument of FadingTooltip should be an HTML element";
 44      if (!tooltipContent || typeof(tooltipContent)!="string") throw "Sorry, "tooltipContent" argument of FadingTooltip should be a string containing text and HTML tags";
 45  
 46      // If the constructor"s optional argument is specified, make sure that 
 47      // all of its properties are defined in the object prototype, or throw 
 48      // an exception.  Again, this could certainly be more rigorous.
 49  
 50      if (parameters && typeof(parameters)!="object") { throw "Sorry, "parameters" argument of FadingTooltip should be a JSON object containing parameter values"; }
 51      for (var parameter in parameters) { 
 52          if (typeof(this[parameter])=="undefined") throw "Sorry, "parameters{" + parameter + "} passed to FadingTooltip is not recognized";
 53      }
 54  
 55      // Save the constructor"s argument values in the new object instance.
 56      
 57      this.htmlElement = htmlElement;
 58      this.tooltipContent = tooltipContent;
 59      for (parameter in parameters) this[parameter] = parameters[parameter];
 60  
 61      //if (this.trace) { 
 62      //    trace("creating FadingToolip for HTML element id=" + htmlElement.id); 
 63      //    for (parameter in parameters) trace("... " + parameter + "=" + parameters[parameter]);
 64      //}
 65      
 66      // Copy a pointer to the new object into the "self" variable.  This variable
 67      // will be referenced in the functions defined below that hook cursor events,
 68      // so its value will be enclosed with those function definitions.  This will
 69      // enable the functions to locate this object later when they are called.
 70      // Note that the constructor"s "htmlElement" argument is also referenced in
 71      // some of the functions defined below, so its value will also be enclosed 
 72      // with them as well.
 73      
 74      var self = this;
 75      
 76      // If the browser provides the W3C DOM Level 2 event model (for example, 
 77      // recent versions of Mozilla Firefox and Netscape Navigator), use it to  
 78      // hook mouse events for the HTML element.  For each event type, provide 
 79      // an anonymous function as the mouse event handler.   When the cursor
 80      // moves over, within, or off the HTML element, the browser will call the
 81      // corresponding mouse event handler: "this" will point at the HTML element, 
 82      // and the browser will pass an "event" object as an argument.  The "event"
 83      // object will specify the type of the event and the position of the cursor.
 84      // The mouse event handlers will locate this FadingTooltip object via the 
 85      // "self" variable defined above and pass the "event" object to the finite 
 86      // state machine"s event handler (the "handleEvent" method defined below).  
 87      // When the finite state machine"s event handler executes, "this" will point 
 88      // at the FadingTooltip object, and the HTML element will be accessible via 
 89      // the "htmlElement" property of the object.
 90      
 91      if (htmlElement.addEventListener) { // for FF and NS and Opera
 92          //if (this.trace) trace("FadingTooltip hooking HTML element id=" + htmlElement.id + " using DOM Level 2 event model");
 93          htmlElement.addEventListener("mouseover", function(event) { self.handleEvent(event); }, false);
 94          htmlElement.addEventListener("mousemove", function(event) { self.handleEvent(event); }, false);
 95          htmlElement.addEventListener("mouseout",  function(event) { self.handleEvent(event); }, false);
 96      }
 97  
 98      // Otherwise, if the browser provides the Microsoft event model (recent versions  
 99      // of Internet Explorer only), use it to hook mouse events for the HTML element.
100      // For each event type, provide an anonymous function as the mouse event
101      // handler.  When the cursor moves over, within, or off the HTML element, 
102      // the browser will call the corresponding mouse event handler, but unlike
103      // DOM Level 2 event handlers defined above, "this" will point at the 
104      // global window object, and the browser will not pass any arguments. An
105      // "event" object will be available in the global window object that will
106      // specify the type of the event and the position of the cursor, but there is 
107      // no obvious way to locate the HTML element from the global window object. 
108      // The mouse event handlers will locate this FadingTooltip object via the 
109      // "self" variable defined above and pass the "event" object to the finite 
110      // state machine"s event handler (the "handleEvent" method defined below).  
111      // When the finite state machine"s event handler executes, "this" will point 
112      // at the FadingTooltip object, and the HTML element will be accessible via 
113      // the "htmlElement" property of the object.
114  
115      else if (htmlElement.attachEvent) { // for MSIE 
116          //if (this.trace) trace("FadingTooltip hooking HTML element id=" + htmlElement.id + " using Microsoft event model");
117          htmlElement.attachEvent("onmouseover", function() { self.handleEvent(window.event); } );
118          htmlElement.attachEvent("onmousemove", function() { self.handleEvent(window.event); } );
119          htmlElement.attachEvent("onmouseout",  function() { self.handleEvent(window.event); } );
120      } 
121      
122      // If the browser does not provide either of the more modern event models, use 
123      // the original W3C "DOM Level 0" event model to hook mouse events for the HTML 
124      // element. This event model does not support multiple event handlers, so we
125      // will save a pointer to any previous handler in a local variable, and chain to
126      // it from our own handler after handling the event.  For each event type, provide 
127      // an anonymous function as the mouse event handler.  When the cursor moves over, 
128      // within, or off the HTML element, the browser will call the corresponding 
129      // mouse event handler: like the DOM Level 2 event handlers defined above, 
130      // "this" will point at the HTML element, but the browser may (Firefox and
131      // Netscape) or may not (Internet Explorer) pass an "event" object as an argument.
132      // The mouse event handlers will locate this FadingTooltip object via the 
133      // "self" variable defined above and pass the "event" object to the finite 
134      // state machine"s event handler (the "handleEvent" method defined below).  
135      // When the finite state machine"s event handler executes, "this" will point 
136      // at the FadingTooltip object, and the HTML element will be accessible via 
137      // the "htmlElement" property of the object.  Pointers to any previously
138      // hooked mouse event handlers are saved in local variables that will be 
139      // enclosed with the function defintions, so that their values will be available
140      // when our functions are called.  After our functions have called the 
141      // "handleEvent" method, they will chain to previously hooked handlers, passing
142      // (Firefox and Netscape) or not passing (Internet Explorer) the "event" object
143      // as an argument, setting "this" to point at the HTML element.
144      
145      else { // for older browsers
146          //if (this.trace) trace("FadingTooltip hooking HTML element id=" + htmlElement.id + " using DOM level 0 event model");
147          var previousOnmouseover = htmlElement.onmouseover;
148          htmlElement.onmouseover = function(event) { 
149              self.handleEvent(event ? event : window.event); 
150              if (previousOnmouseover) { 
151                  htmlElement.previousHandler = previousOnmouseover;
152                  htmlElement.previousHandler(event ? event : window.event); 
153              }
154          };
155          var previousOnmousemove = htmlElement.onmousemove;
156          htmlElement.onmousemove = function(event) { 
157              self.handleEvent(event ? event : window.event); 
158              if (previousOnmousemove) { 
159                  htmlElement.previousHandler = previousOnmousemove;
160                  htmlElement.previousHandler(event ? event : window.event); 
161              }
162          };
163          var previousOnmouseout = htmlElement.onmouseout;
164          htmlElement.onmouseout  = function(event) { 
165              self.handleEvent(event ? event : window.event); 
166              if (previousOnmouseout) { 
167                  htmlElement.previousHandler = previousOnmouseout;
168                  htmlElement.previousHandler(event ? event : window.event); 
169              }
170          };       
171      }
172  
173      // Set the initial state of the finite state machine.
174      
175      this.currentState = this.initialState;    
176  } 
177  
178  // The prototype for the FadingTooltip object defines the properties of 
179  // object instances, that is, the variables and methods of the object.  This
180  // includes the optional parameters of the object constructor (and their 
181  // default values), the object"s state variables, the table of 
182  // action/transition functions, and a collection of private methods.
183  
184  FadingTooltip.prototype = { 
185  
186      // Any optional parameters of the constructor will be saved in these properties
187      // of the object, otherwise the default values defined in the prototype will be 
188      // used.
189  
190      tooltipClass: null, // name of a CSS style for rendering the tooltip, or "null" for default style below
191      tooltipOpacity: 0.8, // maximum opacity of tooltip, between 0.0 and 1.0 (after fade-in finishes, before fade-out begins)
192      tooltipOffsetX: 10, // horizontal offset from cursor to upper-left corner of tooltip
193      tooltipOffsetY: 10, // vertical offset from cursor to upper-left corner of tooltip
194      fadeRate: 24, // animation rate for fade-in and fade-out, in steps per second
195      pauseTime: 0.5, // how long the cursor must pause over HTML element before fade-in starts, in seconds
196      displayTime: 10, // how long the tooltip will be displayed (after fade-in finishes, before fade-out begins), in seconds
197      fadeinTime: 1, // how long fade-in animation will take, in seconds
198      fadeoutTime: 3, // how long fade-out animation will take, in seconds
199  
200      // These are state variables used by the finite state machine"s 
201      // action/transition functions (see the "actionTransitionTable" and
202      // utility functions defined below).
203      
204      currentState: null, // current state of finite state machine (one of "actionTransitionFunctions" properties)
205      currentTimer: null, // returned by setTimeout, if a timer is currently running
206      currentTicker: null, // returned by setInterval, if a ticker is currently running
207      currentOpacity: 0, // current opacity of tooltip, between 0.0 and "tooltipOpacity"
208      tooltipDivision: null, // pointer to HTML Division element, if tooltip is currently visible
209      lastCursorX: 0, // cursor x-position at most recent mouse event
210      lastCursorY: 0, // cursor y-position at most recent mouse event
211      trace: false, // trace execution points that may be helpful for debugging, if set to "true"
212      
213      // The "handleEvent" method handles mouse and timer events as appropriate for 
214      // the current state of the finite state machine.  The required "event" argument 
215      // is an object that has (at least) a "type" property whose value corresponds to 
216      // one of the event types in the current state"s column of the 
217      // "actionTransitionFunctions" table.  For mouse events, it must also have 
218      // "clientX" and "clientY" properties that specify the location of the cursor.
219      // This method will select the appropriate action/transition function from the 
220      // table and call it, passing on the "event" argument. Note that the
221      // action/transition function is invoked via the "call" method of its Function
222      // object, which allows us to set the context for the function so that the 
223      // built-in variable "this" will point at the FadingTooltip object.  If we
224      // were to call the function directly from the "actionTransitionFunctions" table, 
225      // the "this" variable would point into the table.  The action/transition function 
226      // returns a new state, which this method will store as current state of the finite 
227      // state machine.  This method does not return a value.
228      
229      handleEvent: function(event) { 
230          var actionTransitionFunction = this.actionTransitionFunctions[this.currentState][event.type];
231          if (!actionTransitionFunction) actionTransitionFunction = this.unexpectedEvent;
232          var nextState = actionTransitionFunction.call(this, event);
233          if (!nextState) nextState = this.currentState;
234          //if (this.trace) trace(""" + event.type + "" event caused transition from "" + this.currentState + "" state to "" + nextState + "" state");
235          if (!this.actionTransitionFunctions[nextState]) nextState = this.undefinedState(event, nextState);
236          this.currentState = nextState;
237      },
238  
239      // The "unexpectedEvent" method is called by the "handleEvent" method when the
240      // "actionTransitionFunctions" table does not contain a function for the current
241      // event and state.  The required "event" argument is an object, but only its 
242      // "type" property is required.  The method cancels any active timers, deletes 
243      // the tooltip, if one has been created, and returns the finite state machine"s 
244      // initial state.  The unexpected event and state are shown in an "alert" dialog 
245      // to the user, who will hopefully send a problem report to the author of this code.
246  
247      unexpectedEvent: function(event) { 
248          this.cancelTimer();
249          this.cancelTicker();
250          this.deleteTooltip();
251          alert("FadingTooltip handled unexpected event "" + event.type + "" in state "" + this.currentState + "" for id="" + this.htmlElement.id + "" running browser " + window.navigator.userAgent);
252          return this.initialState; 
253      },  
254      
255      // The "undefinedState" method is called by the "handleEvent" method when the
256      // "actionTransitionFunctions" table does not contain a column for the next 
257      // state returned by the selected function.  The required "state" argument is 
258      // the name of the undefined state.  The method cancels any active timers, deletes 
259      // the tooltip, if one has been created, and returns the finite state machine"s 
260      // initial state.  The undefind state is shown in an "alert" dialog to the user, 
261      // who will hopefully send a problem report to the author of this code.
262      
263      undefinedState: function(event, state) {
264          this.cancelTimer();
265          this.cancelTicker();
266          this.deleteTooltip();
267          alert("FadingTooltip transitioned to undefined state "" + state + "" from state "" + this.currentState + "" due to event "" + event.type + "" from HTML element id="" + this.htmlElement.id + "" running browser " + window.navigator.userAgent);
268          return this.initialState; 
269      },  
270  
271      // The "initialState" constant specifies the initial state of the finite state 
272      // machine, which must match one of the state names in the 
273      // "actionTransitionFunctions" table below.
274  
275      initialState: "Inactive",
276      
277      // The "actionTransitionFunctions" table is a two-dimensional associative array
278      // of anonymous functions, or, if you prefer, an object containing more objects
279      // containing anonymous functions.  The first dimension of the array (the outer
280      // object) is indexed by state names; the second dimension of the array (the
281      // inner objects) is indexed by event types.  When a mouse or timer event hander 
282      // calls the "handleEvent" method, it calls the appropriate function from the table, 
283      // passing an "event" object as an argument, ensuring that "this" points at the 
284      // FadingTooltip object.  The selected function takes whatever actions are 
285      // required for that event in the current state, and returns either the name of 
286      // a new state, if a state transition is needed, or "null" if not.  See the design
287      // documentation, in particular the state diagram and table, for details.  Note that
288      // the array is sparse: state/event combinations that "should not occur" are 
289      // empty.  If an event does occur in a state that does not expect it, the 
290      // "unexpectedEvent" method will be called.
291  
292      actionTransitionFunctions: { 
293      
294          // The "Inactive" column of the "actionTransitionFunctions" table contains a 
295          // function for each mouse and timer event that is expected in this state.
296          Inactive: {
297              // When a "mouseover" event occurs in "Inactive" state, save the current
298              // location of the cursor, [re-]start the pause timer, and transition to
299              // "Pause" state.  Note that this function is also executed for "mouseover" 
300              // events in "Pause" state, and "mousemove" events in "Inactive" state, 
301              // since all of the same actions, and the same transition, are appropriate 
302              // for them. 
303              mouseover: function(event) { 
304                  this.cancelTimer();
305                  this.saveCursorPosition(event.clientX, event.clientY);
306                  this.startTimer(this.pauseTime*1000);
307                  return "Pause";
308              },
309              // When a "mousemove" event occurs in "Inactive" state, take the same 
310              // actions, and make the same state transition, as for "mouseover" 
311              // events in "Inactive" state.  Note that this state/event situation
312              // was not anticipated in the initial design of the finite state machine; 
313              // this function was added when it occurred unexpectedly during testing.  
314              // With MSIE, this often happens as soon as the mouse moves over the HTML element, 
315              // before any "mouseover" event occurs, presumably because of a bug in the 
316              // browser.  With FF and NN, this can also happen if the mouse remains over
317              // the HTML element after the tooltip has been displayed and faded out, and
318              // the mouse then moves within the HTML element.
319              mousemove: function(event) { 
320                  return this.doActionTransition("Inactive", "mouseover", event);
321              },
322              // When a "mouseout" event occurs in "Inactive" state, just ignore the event:
323              // take no action and make no state transition.  Note that this state/event 
324              // situation was not anticipated in the initial design; this function was added 
325              // when it occurred unexpectedly during testing.  With MSIE, this may happen
326              // when the mouse crosses the HTML element, without any "mouseover" event, 
327              // presumably because of a bug in the browser.  With FF and NN, this can also 
328              // happen if the mouse remains over the HTML element after the tooltip has been 
329              // displayed and faded out, and the mouse then moves off the HTML element.
330              mouseout: function(event) {
331                  return this.currentState; // do nothing
332              }
333          }, // end of FadingTooltip.prototype.actionTransitionFunctions.Inactive
334          
335          // The "Pause" column of the "actionTransitionFunctions" table contains a 
336          // function for each mouse and timer event that is expected in this state.
337          Pause: {
338              // When a "mousemove" event occurs in "Pause" state, take the same 
339              // actions, and make the same state transition, as for "mouseover" 
340              // events in "Inactive" state.
341              mousemove: function(event) { 
342                  return this.doActionTransition("Inactive", "mouseover", event);
343              },
344              // When a "mouseout" event occurs in "Pause" state, just cancel the
345              // timer and return to "Inactive" state.  Since tooltip has been created
346              // yet, there is nothing more to do.
347              mouseout: function(event) { 
348                  this.cancelTimer();
349                  return "Inactive";
350              },
351              // When a "timeout" event occurs in "Pause" state, create the 
352              // tooltip (with an initial opacity of zero).  In the normal case, 
353              // when the fade-in time is non-zero, start the animation ticker and 
354              // transition to "FadeIn" state.  But when the fade-in time is zero, 
355              // skip the fade animation (to avoid dividing by zero when "timetick"
356              // events occur in "FadeIn" state), and transition directly to "Display"
357              // state (after increasing the tooltip opacity to its maximum value and
358              // setting the display timer).
359              timeout: function(event) { 
360                  this.cancelTimer();
361                  this.createTooltip();
362                  if (this.fadeinTime>0) { 
363                      this.startTicker(1000/this.fadeRate);
364                      return "FadeIn";
365                  } else {
366                      this.fadeTooltip(+this.tooltipOpacity);
367                      this.startTimer(this.displayTime*1000);
368                      return "Display";
369                  }
370              }
371          }, // end of FadingTooltip.prototype.actionTransitionFunctions.Pause
372          
373          // The "FadeIn" column of the "actionTransitionFunctions" table contains a 
374          // function for each mouse and timer event that is expected in this state.
375          FadeIn: {
376              // When a "mousemove" event occurs in "FadeIn" state, take the same 
377              // actions as for "mousemove" events in "Display" state.  Note that no
378              // state transition occurs; the finite state machine remains in its 
379              // current state.
380              mousemove: function(event) { 
381                  return this.doActionTransition("Display", "mousemove", event);
382              },
383              // When a "mouseout" event occurs in "FadeIn" state, just transition
384              // to "FadeOut" state.  Leave the animation ticker running; subsequent
385              // "timetick" events in "FadeOut" state will cause the fade animation to
386              // reverse direction at the current tooltip opacity.
387              mouseout: function(event) { 
388                  return "FadeOut";
389              },
390              // When a "timetick" event occurs in "FadeIn" state, increase the
391              // opacity of the tooltip slightly (such that opacity increases from zero 
392              // to the specified maximum in equal increments over the specified fade-in 
393              // time at the specified animation rate).  When tooltip opacity reaches the
394              // specified maximum, cancel the ticker, start the display timer, and 
395              // transition to "Display" state.
396              timetick: function(event) {
397                  this.fadeTooltip(+this.tooltipOpacity/(this.fadeinTime*this.fadeRate));
398                  if (this.currentOpacity>=this.tooltipOpacity) {
399                      this.cancelTicker();
400                      this.startTimer(this.displayTime*1000);
401                      return "Display";
402                  }
403                  return this.CurrentState;
404              }
405          }, // end of FadingTooltip.prototype.actionTransitionFunctions.FadeIn
406          
407          // The "Display" column of the "actionTransitionFunctions" table contains a 
408          // function for each mouse and timer event that is expected in this state.
409          Display: {
410              // When a "mousemove" event occurs in "Display" state, move the tooltip
411              // to the current cursor location, and leave the finite state machine in
412              // its current state.
413              mousemove: function(event) { 
414                  this.moveTooltip(event.clientX, event.clientY);
415                  return this.currentState;
416              },
417              // When a "mouseout" event occurs in "Display" state, take the same 
418              // actions, and make the same state transitions, as for "timeout" 
419              // events in "Display" state.
420              mouseout: function(event) { 
421                  return this.doActionTransition("Display", "timeout", event);
422              },
423              // When a "timeout" event occurs in "Display" state, in the normal case, 
424              // (when the fade-out time is non-zero), start the animation ticker and 
425              // transition to "FadeOut" state.  But when the fade-out time is zero, 
426              // skip the fade animation (to avoid dividing by zero when "timetick"
427              // events occur in "FadeOut" state), and transition directly to "Inactive"
428              // state (after deleting the tooltip).
429              timeout: function(event) { 
430                  this.cancelTimer();
431                  if (this.fadeoutTime>0) { 
432                      this.startTicker(1000/this.fadeRate);
433                      return "FadeOut";
434                  } else {
435                      this.deleteTooltip();
436                      return "Inactive";
437                  }
438              }
439          }, // end of FadingTooltip.prototype.actionTransitionFunctions.Display
440          
441          // The "FadeOut" column of the "actionTransitionFunctions" table contains a 
442          // function for each mouse and timer event that is expected in this state.
443          FadeOut: {
444              // When a "mouseover" event occurs in "FadeOut" state, move the tooltip
445              // to the current cursor location, and transition back to "FadeIn" state.
446              // Leave the animation ticker running; subsequent "timetick" events in 
447              // "FadeIn" state will cause the fade animation to reverse direction at 
448              // the current tooltip opacity.
449              mouseover: function(event) { 
450                  this.moveTooltip(event.clientX, event.clientY);
451                  return "FadeIn";
452              },
453              // When a "mousemove" event occurs in "FadeOut" state, take the same 
454              // actions as for "mousemove" events in "Display" state.  Note that no
455              // state transition occurs; the finite state machine remains in the 
456              // current state.
457              mousemove: function(event) { 
458                  return this.doActionTransition("Display", "mousemove", event);
459              },
460              mouseout: function(event) { 
461                  return this.currentState; // do nothing
462              }, 
463              // When a "timetick" event occurs in "FadeOut" state, decrease the
464              // opacity of the tooltip slightly (such that opacity decreases from the 
465              // specified maximum to zero in equal increments over the specified fade-out
466              // time at the specified animation rate).  When tooltip opacity reaches zero,
467              // cancel the ticker, delete the tooltip, and transition to "Inactive" state.
468              timetick: function(event) { 
469                  this.fadeTooltip(-this.tooltipOpacity/(this.fadeoutTime*this.fadeRate));
470                  if (this.currentOpacity<=0) {
471                      this.cancelTicker();
472                      this.deleteTooltip();
473                      return "Inactive";
474                  }
475                  return this.currentState;
476              }
477          } // end of FadingTooltip.prototype.actionTransitionFunctions.FadeOut
478      }, // end of FadingTooltip.prototype.actionTransitionFunctions 
479  
480      // The "doActionTransition" method is used in the "actionTransitionFunctions" 
481      // table when one function takes exactly the same actions as another function 
482      // in the table.  It selects another function from the table, using the required
483      // "anotherState" and "anotherEventType" arguments, and calls that function, passing
484      // on the required "event" argument, and then returning its return value.  As with
485      // the "handleEvent" method, the function is called via the "call" method of its 
486      // Function object, which allows us to set its context so that the build-in "this"
487      // variable will point to the FadingTooltip object while the function executes. 
488  
489      doActionTransition: function(anotherState, anotherEventType, event) {
490           return this.actionTransitionFunctions[anotherState][anotherEventType].call(this,event);
491      },
492      
493      // The "startTimer" method starts a one-shot timer.  The required "timeout"
494      // argument specifies the duration of the timer in milliseconds.  The
495      // method defines an anonymous function for the timeout event handler.
496      // When the browser calls timer event handlers, "this" points at the 
497      // global window object.  Therefore, a pointer to the FadeTooltip object
498      // is copied to the "self" local variable and enclosed with the anonymous
499      // function definition so that the timeout event handler can locate the 
500      // object when it is called.  The browser does not pass any arguments to
501      // timer event handlers, so the timeout event handler creates a simple
502      // "timer event" object containing only a "type" property, and passes
503      // it to the "handleEvent" method (defined above).  So, when the 
504      // "handleEvent" method executes, "this" will point at the FadingTooltip 
505      // object, and the "type" property of its "event" argument will identify 
506      // it as a "timeout" event.  The opaque reference to a timer object (returned
507      // by the browser when any timer is started) is saved as a state variable 
508      // so that the timer can be cancelled prematurely, if necessary.  This method
509      // does not return a value.
510      
511      startTimer: function(timeout) { 
512          var self = this;
513          this.currentTimer = setTimeout(function() { self.handleEvent( { type: "timeout" } ); }, timeout);
514      },
515      
516      // The "cancelTimer" method cancels any one-shot timer that may be
517      // running (or recently expired) and then removes the opaque reference to 
518      // to the timer object saved in the "startTimer" method (defined above).
519      // This method does not return a value.
520      
521      cancelTimer: function() { 
522          if (this.currentTimer) clearTimeout(this.currentTimer);
523          this.currentTimer = null;
524      },
525      
526      // The "startTicker" method starts a repeating ticker.  The required 
527      // "interval" argument specifies the period of the ticker in milliseconds.  
528      // The method defines an anonymous function for the ticker event handler.
529      // When the browser calls timer event handlers, "this" points at the 
530      // global window object.  Therefore, a pointer to the FadeTooltip object
531      // is copied to the "self" local variable and enclosed with the anonymous
532      // function definition so that the ticker event handler can locate the 
533      // object when it is called.  The browser does not pass any arguments to
534      // timer event handlers, so the ticker event handler creates a simple
535      // "timer event" object containing only a "type" property, and passes
536      // it to the "handleEvent" method (defined above).  So, when the 
537      // "handleEvent" method executes, "this" will point at the FadingTooltip 
538      // object, and the "type" property of its "event" argument will identify 
539      // it as a "timetick" event.  The opaque reference to a timer object (returned
540      // by the browser when any timer is started) is saved as a state variable 
541      // so that the ticker can be cancelled when it is no longer needed.
542      // This method does not return a value.
543      
544      startTicker: function(interval) { 
545          var self = this;
546          this.currentTicker = setInterval(function() { self.handleEvent( { type: "timetick" } ); }, interval);
547      },
548      
549      // The "cancelTicker" method cancels any repeating ticker that may be
550      // running, and then removes the opaque reference to the timer object 
551      // saved in the "startTicker" method (defined above).  This method does 
552      // not return a value.
553      
554      cancelTicker: function() {
555          if (this.currentTicker) clearInterval(this.currentTicker);
556          this.currentTicker = null; 
557      },
558      
559      // The "saveCursorPosition" method is called when the cursor position
560      // changes while waiting for the cursor to pause over the HTML element.  
561      // The required arguments "x" and "y" are the current cursor 
562      // coordinates, which the method  It saves the so that the tooltip 
563      // can be positioned near it, after the cursor pauses, when the 
564      // "Pause" state timer expires. This method does not return a value.
565      
566      saveCursorPosition: function(x, y) {
567          this.lastCursorX = x;
568          this.lastCursorY = y;
569      },
570      
571      // The createTooltip" method is called when the fade-in animation is
572      // about to start.  It creates a "floating" HTML Division element for 
573      // the tooltip.  The tooltip is styled with a named CSS style, if one 
574      // is defined, or a default style if not.  In either case, the initial
575      // opacity is set to zero for FF, NN, and MSIE.  This method does not 
576      // return a value.
577      
578      createTooltip: function() { 
579      
580          // create an HTML Division element for the tooltip and load the 
581          // tooltip"s text and HTML tags into it
582          this.tooltipDivision = document.createElement("div");
583          this.tooltipDivision.innerHTML = this.tooltipContent;
584          
585          // if a named CSS style has been defined, apply it to the tooltip,
586          // otherwise apply some default styling 
587          if (this.tooltipClass) {
588              this.tooltipDivision.className = this.tooltipClass;
589          } else {
590              this.tooltipDivision.style.minWidth = "25px";
591              this.tooltipDivision.style.maxWidth = "350px";
592              this.tooltipDivision.style.height = "auto";
593              this.tooltipDivision.style.border = "thin solid black";
594              this.tooltipDivision.style.padding = "5px";
595              this.tooltipDivision.style.backgroundColor = "yellow";
596          }
597          
598          // make sure that the tooltip floats over the rest of the HTML 
599          // elements on the page
600          this.tooltipDivision.style.position = "absolute";
601          this.tooltipDivision.style.zIndex = 101;
602          
603          // position the tooltip near the last known cursor coordinates
604          this.tooltipDivision.style.left = this.lastCursorX + this.tooltipOffsetX;
605          this.tooltipDivision.style.top = this.lastCursorY + this.tooltipOffsetY;
606          
607          // set the initial opacity of the tooltip to zero, using the proposed W3C 
608          // CSS3 "style" property, and, if we are running MSIE, also create an 
609          // "alpha" filter with an "opacity" property whose initial value is zero
610          this.currentOpacity = 0; 
611          this.tooltipDivision.style.opacity = 0;
612          if (this.tooltipDivision.filters) this.tooltipDivision.style.filter = "alpha(opacity=0)"; // for MSIE only
613          
614          // display the tooltip on the page
615          document.body.appendChild(this.tooltipDivision);                
616      },
617      
618      // The "fadeTooltip" method increases or decreases the opacity of the 
619      // tooltip.  The required "opacityDelta" argument specifies the size
620      // of the increase (positive values) or decrease (negative values).
621      // The increase is limited to the specified maximum value; the decrease
622      // is limited to zero. This method does not return a value.
623      
624      fadeTooltip: function(opacityDelta) { 
625          
626          // calculate the new opacity value as a decimal fraction, rounded 
627          // to the nearest 0.000001 (that is, the nearest one-millionth), 
628          // to avoid exponential representation of very small values, which 
629          // are not recognized as valid values of the "opacity" style property
630          this.currentOpacity = Math.round((this.currentOpacity + opacityDelta)*1000000)/1000000; 
631                  
632          // make sure the new opacity value is between 0.0 and the specified
633          // maximum tooltip opacity
634          if (this.currentOpacity<0) this.currentOpacity = 0;
635          if (this.currentOpacity>this.tooltipOpacity) this.currentOpacity = this.tooltipOpacity;
636          
637          // change the "opacity" style property of the HTML Division element that
638          // contains the tooltip text, and, if we are running MSIE, find the "alpha"
639          // filter created in "createTooltip" (defined above) and change its "opacity"
640          // property to match, remembering that its range is 0 to 100, not 0 to 1
641          this.tooltipDivision.style.opacity = this.currentOpacity;
642          if (this.tooltipDivision.filters) this.tooltipDivision.filters.item("alpha").opacity = 100*this.currentOpacity; // for MSIE only
643      },
644      
645      // The "moveTooltip" method is called when the cursor position
646      // changes while the tooltip is visible, whether it is fading in, 
647      // fully displayed, or fading out.  It moves the tooltip so that
648      // it follows the movement of the cursor.  This method does not 
649      // return a value.
650      
651      moveTooltip: function(x, y) { 
652          this.tooltipDivision.style.left = x + this.tooltipOffsetX;
653          this.tooltipDivision.style.top = y + this.tooltipOffsetY;
654      },
655      
656      // The "deleteTooltip" method is called after the tooltip has faded out
657      // completely.  It deletes the HTML Division element.  This method does 
658      // not return a value.  
659      
660      deleteTooltip: function() { 
661          if (this.tooltipDivision) document.body.removeChild(this.tooltipDivision);
662          this.tooltipDivision = null;
663      }   
664          
665  }; // end of FadingTooltip.prototype
666  
667  
668  // With MSIE and Opera, an extra "mouseover" event sometimes occurs in "Pause" state, 
669  // after a previous "mouseover" event has made the finite state machine transition 
670  // from "Inactive" state to "Pause" state.  Presumably this is due to a bug in 
671  // the browser.  For MSIE and Opera only, add an extra anonymous function to the 
672  // "actionTransitionTable" for this situation: when it occurs, take the same actions, 
673  // and return the same state transition, as for a "mouseover" event in "Inactive" state.
674  
675  if ( (window.navigator.userAgent).indexOf("MSIE")!=-1 || (window.navigator.userAgent).indexOf("Opera")!=-1 ) {
676      //if (this.trace) trace("Pause/mouseover hack added to state table");
677      FadingTooltip.prototype.actionTransitionFunctions.Pause.mouseover = function(event) { 
678          return this.doActionTransition("Inactive", "mouseover", event);
679      };
680  }