MediaWiki:Tooltips.js

From Tardis Wiki, the free Doctor Who reference
Clear your cache often around here

After changes are made to this site's CSS or Javascript, you have to bypass your browser's cache to see the changes. You can always do this by going to your browser's preferences panel. But many browsers also offer keyboard shortcuts to save you that trouble. The following shortcuts work in the versions of the browsers that Tardis currently supports. They may not work in earlier versions.

  • Firefox: hold down Shift while performing a page reload.
  • Opera offers no default keyboard shortcut, but you can create a custom keyboard shortcut with the value Clear disk cache
  • Safari users should simultaneously hold down + Option + E. You may need to enable the Develop menu first
  • Chrome: press Ctrl + F5 or Shift + F5 while performing a page reload.
// <syntaxhighlight lang="javascript">

/*
  Cross-browser tooltip support for MediaWiki.
  
  Author: [[User:Lupo]], March 2008
  License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
  
  Choose whichever license of these you like best :-)

  Based on ideas gleaned from prototype.js and prototip.js.
  http://www.prototypejs.org/
  http://www.nickstakenburg.com/projects/prototip/
  However, since prototype is pretty large, and prototip had some
  problems in my tests, this stand-alone version was written.
  
  Note: The fancy effects from scriptaculous have not been rebuilt.
  http://script.aculo.us/
  
  See http://commons.wikimedia.org/wiki/MediaWiki_talk:Tooltips.js for
  more information including documentation and examples.
*/

var EvtHandler = {
  listen_to : function (object, node, evt, f)
  {
    var listener = EvtHandler.make_listener (object, f);
    EvtHandler.attach (node, evt, listener);
  },
  
  attach : function (node, evt, f)
  {
    if (node.attachEvent) node.attachEvent ('on' + evt, f);
    else if (node.addEventListener) node.addEventListener (evt, f, false);
    else node['on' + evt] = f;
  },
  
  remove : function (node, evt, f)
  {
    if (node.detachEvent) node.detachEvent ('on' + evt, f);
    else if (node.removeEventListener) node.removeEventListener (evt, f, false);
    else node['on' + evt] = null;
  },
  
  make_listener : function (obj, listener)
  {
    // Some hacking around to make sure 'this' is set correctly
    var object = obj, f = listener;
    return function (evt) { return f.apply (object, [evt || window.event]); }
  },

  // @todo FIXME: remove fully now that support for ancient IEs is gone (January 2021)
  mouse_offset : function ()
  {
    return null;
  },
  
  killEvt : function (evt)
  {
    if (typeof (evt.preventDefault) == 'function') {
      evt.stopPropagation ();
      evt.preventDefault (); // Don't follow the link
    } else if (typeof (evt.cancelBubble) != 'undefined') { // IE...
      evt.cancelBubble = true;
    }
    return false; // Don't follow the link (IE)
  }

} // end EvtHandler

var Buttons = {
  
  buttonClasses : {},
  
  createCSS : function (imgs, sep, id)
  {
    var width   = imgs[0].getAttribute ('width');
    var height  = imgs[0].getAttribute ('height');
    try {
      // The only way to set the :hover and :active properties through Javascript is by
      // injecting a piece of CSS. There is no direct access within JS to these properties.
      var sel1  = "a" + sep + id;
      var prop1 = "border:0; text-decoration:none; background-color:transparent; "
                + "width:" + width + "px; height:" + height + "px; "
                + "display:inline-block; "
                + "background-position:left; background-repeat:no-repeat; "
                + "background-image:url(" + imgs[0].src + ");";
      var sel2  = null, prop2 = null, sel3  = null, prop3 = null; // For IE...
      var css   = sel1 + ' {' + prop1 + '}\n';                    // For real browsers
      if (imgs.length > 1 && imgs[1]) {
        sel2  = "a" + sep + id + ":hover";
        prop2 = "background-image:url(" + imgs[1].src + ");";
        css   = css + sel2 + ' {' + prop2 + '}\n';
      }
      if (imgs.length > 2 && imgs[2]) {
        sel3  = "a" + sep + id + ":active"
        prop3 = "background-image:url(" + imgs[2].src + ");";
        css   = css + sel3 + ' {' + prop3 + '}\n';
      }
      // Now insert a style sheet with these properties into the document (or rather, its head).
      var styleElem = document.createElement( 'style' );
      styleElem.setAttribute ('type', 'text/css');
      try {
        styleElem.appendChild (document.createTextNode (css));
        document.getElementsByTagName ('head')[0].appendChild (styleElem);
      } catch (ie_bug) {
        // Oh boy, IE has a big problem here
        document.getElementsByTagName ('head')[0].appendChild (styleElem);
//        try {
          styleElem.styleSheet.cssText = css;
/*
        } catch (anything) {
          if (document.styleSheets) {
            var lastSheet = document.styleSheets[document.styleSheets.length - 1];          
            if (lastSheet && typeof (lastSheet.addRule) != 'undefined') {
              lastSheet.addRule (sel1, prop1);
              if (sel2) lastSheet.addRule (sel2, prop2);
              if (sel3) lastSheet.addRule (sel3, prop3);
            }
          }
        }
*/
      }
    } catch (ex) {
      return null;
    }
    if (sep == '.') {
      // It's a class: remember the first image
      Buttons.buttonClasses[id] = imgs[0];
    }
    return id;
  }, // end createCSS
  
  createClass : function (imgs, id)
  {
    return Buttons.createCSS (imgs, '.', id);
  },
  
  makeButton : function (imgs, id, handler, title)
  {
    var success     = false;
    var buttonClass = null;
    var content     = null;
    if (typeof (imgs) == 'string') {
      buttonClass = imgs;
      content     = Buttons.buttonClasses[imgs];
      success     = (content != null);
    } else {
      success     = (Buttons.createCSS (imgs, '#', id) != null);
      content     = imgs[0];
    }
    if (success) {
      var lk = document.createElement ('a');
      lk.setAttribute
        ('title', title || content.getAttribute ('alt') || content.getAttribute ('title') || "");
      lk.id = id;
      if (buttonClass) lk.className = buttonClass;
      if (typeof (handler) == 'string') {
        lk.href = handler;
      } else {
        lk.href = '#'; // Dummy, overridden by the onclick handler below.
        lk.onclick = function (evt)
          {
            var e = evt || window.event; // W3C, IE
            try {handler (e);} catch (ex) {};
            return EvtHandler.killEvt (e);
          };
      }
      content = content.cloneNode (true);
      content.style.visibility = 'hidden';
      lk.appendChild (content);
      return lk;
    } else {
      return null;
    }
  } // end makeButton
  
} // end Button

var Tooltips = {
  // Helper object to force quick closure of a tooltip if another one shows up.
  debug    : false,  
  top_tip  : null,
  nof_tips : 0,

  new_id : function ()
  {
    Tooltips.nof_tips++;
    return 'tooltip_' + Tooltips.nof_tips;
  },

  register : function (new_tip)
  {
    if (Tooltips.top_tip && Tooltips.top_tip != new_tip) Tooltips.top_tip.hide_now ();
    Tooltips.top_tip = new_tip;
  },
  
  deregister : function (tip)
  {
    if (Tooltips.top_tip == tip) Tooltips.top_tip = null;
  },

  close : function ()
  {
    if (Tooltips.top_tip) {
      Tooltips.top_tip.hide_now ();
      Tooltips.top_tip = null;
    }
  }
}

var Tooltip = function () {this.initialize.apply (this, arguments);}
// This is the Javascript way of creating a class. Methods are added below to Tooltip.prototype;
// one such method is 'initialize', and that will be called when a new instance is created.
// To create instances of this class, use var t = new Tooltip (...);

// Location constants
Tooltip.MOUSE             = 0; // Near the mouse pointer
Tooltip.TRACK             = 1; // Move tooltip when mouse pointer moves
Tooltip.FIXED             = 2; // Always use a fixed poition (anchor) relative to an element

// Anchors
Tooltip.TOP_LEFT          = 1;
Tooltip.TOP_RIGHT         = 2;
Tooltip.BOTTOM_LEFT       = 3;
Tooltip.BOTTOM_RIGHT      = 4;

// Activation constants
Tooltip.NONE              = -1; // You must show the tooltip explicitly in this case.
Tooltip.HOVER             =  1;
Tooltip.FOCUS             =  2; // always uses the FIXED position
Tooltip.CLICK             =  4;
Tooltip.ALL_ACTIVATIONS   =  7;

// Deactivation constants

Tooltip.MOUSE_LEAVE       =  1; // Mouse leaves target, alternate target, and tooltip
Tooltip.LOSE_FOCUS        =  2; // Focus changes away from target
Tooltip.CLICK_ELEM        =  4; // Target is clicked
Tooltip.CLICK_TIP         =  8; // Makes only sense if not tracked
Tooltip.ESCAPE            = 16;
Tooltip.ALL_DEACTIVATIONS = 31;
Tooltip.LEAVE             = Tooltip.MOUSE_LEAVE | Tooltip.LOSE_FOCUS;


Tooltip.prototype =
{
  initialize : function (on_element, tt_content, opt, css)
  {
    if (!on_element || !tt_content) return;
    this.tip_id      = Tooltips.new_id ();
    // Registering event handlers via attacheEvent on IE is apparently a time-consuming
    // operation. When you add many tooltips to a page, this can add up to a noticeable delay.
    // We try to mitigate that by only adding those handlers we absolutely need when the tooltip
    // is created: those for showing the tooltip. The ones for hiding it again are added the
    // first time the tooltip is actually shown. We thus record which handlers are installed to
    // avoid installing them multiple times:
    //   event_state: -1 : nothing set, 0: activation set, 1: all set
    //   tracks:      true iff there is a mousemove handler for tracking installed.
    // This change bought us about half a second on IE (for 13 tooltips on one page). On FF, it
    // doesn't matter at all; in Firefoy, addEventListener is fast anyway.
    this.event_state = -1;
    this.tracks      = false;
    // We clone the node, wrap it, and re-add it at the very end of the
    // document to make sure we're not within some nested container with
    // position='relative', as this screws up all absolute positioning
    // (We always position in global document coordinates.)
    // In my tests, it appeared as if Nick Stakenburg's "prototip" has
    // this problem...
    if (typeof (tt_content) == 'function') {
      this.tip_creator = tt_content;
      this.css         = css;
      this.content     = null;
    } else {
      this.tip_creator = null;
      this.css         = null;
      if (tt_content.parentNode) {
        if (tt_content.ownerDocument != document)
          tt_content = document.importNode (tt_content, true);
        else
          tt_content = tt_content.cloneNode (true);
      }
      tt_content.id = this.tip_id;
      this.content  = tt_content;
    }
    // Wrap it
    var wrapper = document.createElement ('div');
    wrapper.className = 'tooltipContent';
    if (this.content) wrapper.appendChild (this.content);
    this.popup = document.createElement ('div');
    this.popup.style.display = 'none';
    this.popup.style.position = 'absolute';
    this.popup.style.top = "0px";
    this.popup.style.left = "0px";
    this.popup.appendChild (wrapper);
    // Set the options
    this.options = {
       mode         : Tooltip.TRACK              // Where to display the tooltip.
      ,activate     : Tooltip.HOVER              // When to activate
      ,deactivate   : Tooltip.LEAVE | Tooltip.CLICK_ELEM | Tooltip.ESCAPE // When to deactivate
      ,mouse_offset : {x: 5, y: 5, dx: 1, dy: 1} // Pixel offsets and direction from mouse pointer
      ,fixed_offset : {x:10, y: 5, dx: 1, dy: 1} // Pixel offsets from anchor position
      ,anchor       : Tooltip.BOTTOM_LEFT        // Anchor for fixed position
      ,target       : null                       // Optional alternate target for fixed display.
      ,max_width    :    0.6         // Percent of window width (1.0 == 100%)
      ,max_pixels   :    0           // If > 0, maximum width in pixels
      ,z_index      : 1000           // On top of everything
      ,open_delay   :  500           // Millisecs, set to zero to open immediately
      ,hide_delay   : 1000           // Millisecs, set to zero to close immediately
      ,close_button : null           // Either a single image, or an array of up to three images
                                     // for the normal, hover, and active states, in that order
      ,onclose      : null           // Callback to be called when the tooltip is hidden. Should be
                                     // a function taking a single argument 'this' (this Tooltip)
                                     // an an optional second argument, the event.
      ,onopen       : null           // Ditto, called after opening.
    };
    // The lower of max_width and max_pixels limits the tooltip's width.
    if (opt) { // Merge in the options
      for (var option in opt) {
        if (option == 'mouse_offset' || option == 'fixed_offset') {
          try {
            for (var attr in opt[option]) {
              this.options[option][attr] = opt[option][attr];
            }
          } catch (ex) {
          }
        } else
          this.options[option] = opt[option];
      }
    }
    // Set up event handlers as appropriate
    this.eventShow   = EvtHandler.make_listener (this, this.show);
    this.eventToggle = EvtHandler.make_listener (this, this.toggle);
    this.eventFocus  = EvtHandler.make_listener (this, this.show_focus);
    this.eventClick  = EvtHandler.make_listener (this, this.show_click);
    this.eventHide   = EvtHandler.make_listener (this, this.hide);
    this.eventTrack  = EvtHandler.make_listener (this, this.track);
    this.eventClose  = EvtHandler.make_listener (this, this.hide_now);
    this.eventKey    = EvtHandler.make_listener (this, this.key_handler);

    this.close_button       = null;
    this.close_button_width = 0;
    if (this.options.close_button) {
      this.makeCloseButton ();
      if (this.close_button) {
        // Only a click on the close button will close the tip.
        this.options.deactivate = this.options.deactivate & ~Tooltip.CLICK_TIP;
        // And escape is always active if we have a close button
        this.options.deactivate = this.options.deactivate | Tooltip.ESCAPE;
        // Don't track, you'd have troubles ever getting to the close button.
        if (this.options.mode == Tooltip.TRACK) this.options.mode = Tooltip.MOUSE;
        this.has_links = true;
      }
    }
    if (this.options.activate == Tooltip.NONE) {
      this.options.activate = 0;
    } else {
      if ((this.options.activate & Tooltip.ALL_ACTIVATIONS) == 0) {
        if (on_element.nodeName.toLowerCase () == 'a')
          this.options.activate = Tooltip.CLICK;
        else
          this.options.activate = Tooltip.HOVER;
      }
    }
    if ((this.options.deactivate & Tooltip.ALL_DEACTIVATIONS) == 0 && !this.close_button)
      this.options.deactivate = Tooltip.LEAVE | Tooltip.CLICK_ELEM | Tooltip.ESCAPE;
    document.body.appendChild (this.popup);
    if (this.content) this.apply_styles (this.content, css); // After adding it to the document
    // Clickable links?
    if (this.content && this.options.mode == Tooltip.TRACK) {
      this.setHasLinks ();
      if (this.has_links) {
        // If you track a tooltip with links, you'll never be able to click the links
        this.options.mode = Tooltip.MOUSE;
      }
    }
    // No further option checks. If nonsense is passed, you'll get nonsense or an exception.
    this.popup.style.zIndex = "" + this.options.z_index;
    this.target             = on_element;
    this.open_timeout_id    = null;
    this.hide_timeout_id    = null;
    this.size               = {width : 0, height : 0};
    this.setupEvents (EvtHandler.attach, 0);
  },
  
  apply_styles : function (node, css)
  {
    if (css) {
      for (var styledef in css) node.style[styledef] = css[styledef];
    }
    if (this.close_button) node.style.opacity = "1.0"; // Bug workaround.
    // FF doesn't handle the close button at all if it is (partially) transparent...
    if (node.style.display == 'none') node.style.display = "";
  },

  setHasLinks : function ()
  {
    if (this.close_button) { this.has_links = true; return; }
    var lks = this.content.getElementsByTagName ('a');
    this.has_links = false;
    for (var i=0; i < lks.length; i++) {
      var href = lks[i].getAttribute ('href');
      if (href && href.length > 0) { this.has_links = true; return; }
    }
    // Check for form elements
    function check_for (within, names)
    {
      if (names) {
        for (var i=0; i < names.length; i++) {
          var elems = within.getElementsByTagName (names[i]);
          if (elems && elems.length > 0) return true;
        }
      }
      return false;
    }
    this.has_links = check_for (this.content, ['form', 'textarea', 'input', 'button', 'select']);
  },

  setupEvents : function (op, state)
  {
    if (state < 0 || state == 0 && this.event_state < state) {
      if (this.options.activate & Tooltip.HOVER)
        op (this.target, 'mouseover', this.eventShow);
      if (this.options.activate & Tooltip.FOCUS)
        op (this.target, 'focus', this.eventFocus);
      if (   (this.options.activate & Tooltip.CLICK)
          && (this.options.deactivate & Tooltip.CLICK_ELEM)) {
        op (this.target, 'click', this.eventToggle);
      } else {
        if (this.options.activate & Tooltip.CLICK)
          op (this.target, 'click', this.eventClick);
        if (this.options.deactivate & Tooltip.CLICK_ELEM)
          op (this.target, 'click', this.eventClose);
      }
      this.event_state = state;
    }
    if (state < 0 || state == 1 && this.event_state < state) {
      if (this.options.deactivate & Tooltip.MOUSE_LEAVE) {
        op (this.target, 'mouseout', this.eventHide);
        op (this.popup, 'mouseout', this.eventHide);
        if (this.options.target) op (this.options.target, 'mouseout', this.eventHide);
      }
      if (this.options.deactivate & Tooltip.LOSE_FOCUS)
        op (this.target, 'blur', this.eventHide);
      if (   (this.options.deactivate & Tooltip.CLICK_TIP)
          && (this.options.mode != Tooltip.TRACK))
        op (this.popup, 'click', this.eventClose);        
      
      // Some more event handling
      if (this.hide_delay > 0) {
        if (!(this.options.activate & Tooltip.HOVER))
          op (this.popup, 'mouseover', this.eventShow);
        op (this.popup, 'mousemove', this.eventShow);
      }
      this.event_state = state;
    }
    if (state < 0 && this.tracks)
      op (this.target, 'mousemove', this.eventTrack);
  },
  
  remove: function ()
  {
    this.hide_now ();
    this.setupEvents (EvtHandler.remove, -1);
    this.tip_creator = null;
    document.body.removeElement (this.popup);
  },
  
  show : function (evt)
  {
    this.show_tip (evt, true);
  },
  
  show_focus : function (evt) // Show on focus
  {
    this.show_tip (evt, false);
  },
  
  show_click : function (evt)
  {
    this.show_tip (evt, false);
    if (this.target.nodeName.toLowerCase () == 'a') return EvtHandler.killEvt (evt); else return false;
  },
  
  toggle : function (evt)
  {
    if (this.popup.style.display != 'none' && this.popup.style.display != null) {
      this.hide_now (evt);
    } else {
      this.show_tip (evt, true);
    }
    if (this.target.nodeName.toLowerCase () == 'a') return EvtHandler.killEvt (evt); else return false;
  },

  show_tip : function (evt, is_mouse_evt)
  {
    if (this.hide_timeout_id != null) window.clearTimeout (this.hide_timeout_id);
    this.hide_timeout_id = null;
    if (this.popup.style.display != 'none' && this.popup.style.display != null) return;
    if (this.tip_creator) {
      // Dynamically created tooltip.
      try {
        this.content = this.tip_creator (evt);
      } catch (ex) {
        // Oops! Indicate that something went wrong!
        var error_msg = document.createElement ('div');
        error_msg.appendChild (
          document.createElement ('b').appendChild (
            document.createTextNode ('Exception: ' + ex.name)));
        error_msg.appendChild(document.createElement ('br'));
        error_msg.appendChild (document.createTextNode (ex.message));
        if (typeof (ex.fileName) != 'undefined' &&
            typeof (ex.lineNumber) != 'undefined') {
          error_msg.appendChild(document.createElement ('br'));
          error_msg.appendChild (document.createTextNode ('File ' + ex.fileName));
          error_msg.appendChild(document.createElement ('br'));
          error_msg.appendChild (document.createTextNode ('Line ' + ex.lineNumber));
        }
        this.content = error_msg;
      }
      // Our wrapper has at most two children: the close button, and the content. Don't remove
      // the close button, if any.
      if (this.popup.firstChild.lastChild && this.popup.firstChild.lastChild != this.close_button)
        this.popup.firstChild.removeChild (this.popup.firstChild.lastChild);
      this.popup.firstChild.appendChild (this.content);
      this.apply_styles (this.content, this.css);
      if (this.options.mode == Tooltip.TRACK) this.setHasLinks ();
    }
    // Position it now. It must be positioned before the timeout below!
    this.position_tip (evt, is_mouse_evt);
    if (Tooltips.debug) {
      alert ('Position: x = ' + this.popup.style.left + ' y = ' + this.popup.style.top);
    }
    this.setupEvents (EvtHandler.attach, 1);
    if (this.options.mode == Tooltip.TRACK) {
      if (this.has_links) {
        if (this.tracks) EvtHandler.remove (this.target, 'mousemove', this.eventTrack);
        this.tracks = false;
      } else {
        if (!this.tracks) EvtHandler.attach (this.target, 'mousemove', this.eventTrack);
        this.tracks = true;
      }
    }
    if (this.options.open_delay > 0) {
      var obj = this;
      this.open_timout_id = 
        window.setTimeout (function () {obj.show_now (obj);}, this.options.open_delay);
    } else
      this.show_now (this);
  },
  
  show_now : function (elem)
  {
    if (elem.popup.style.display != 'none' && elem.popup.style.display != null) return;
    Tooltips.register (elem);
    elem.popup.style.display = ""; // Finally show it
    if (   (elem.options.deactivate & Tooltip.ESCAPE)
        && typeof (elem.popup.focus) == 'function') {
      // We need to attach this event globally.
      EvtHandler.attach (document, 'keydown', elem.eventKey);
    }
    elem.open_timeout_id = null;
    // Callback
    if (typeof (elem.options.onopen) == 'function') elem.options.onopen (elem);
  },
  
  track : function (evt)
  {
    this.position_tip (evt, true);
  },
  
  size_change : function ()
  {
    // If your content is such that it changes, make sure this is called after each size change.
    // Unfortunately, I have found no way of monitoring size changes of this.popup and then doing
    // this automatically. See for instance the "toggle" example (the 12th) on the example page at
    // http://commons.wikimedia.org/wiki/MediaWiki:Tooltips.js/Documentation/Examples
    if (this.popup.style.display != 'none' && this.popup.style.display != null) {
      // We're visible. Make sure the shim gets resized, too!
      this.size = {width : this.popup.offsetWidth, height: this.popup.offsetHeight};
    }
  },
    
  position_tip : function (evt, is_mouse_evt)
  {
    var view = {width  : this.viewport ('Width'),
                height : this.viewport ('Height')};
    var off  = {left   : this.scroll_offset ('Left'),
                top    : this.scroll_offset ('Top')};
    var x = 0, y = 0;
    var offset = null;
    // Calculate the position
    if (is_mouse_evt && this.options.mode != Tooltip.FIXED) {
      x = (evt.pageX || (evt.clientX + off.left));
      y = (evt.pageY || (evt.clientY + off.top));
      offset = 'mouse_offset';
    } else {
      var tgt = this.options.target || this.target;
      var pos = this.position (tgt);
      switch (this.options.anchor) {
        default:
        case Tooltip.BOTTOM_LEFT:
          x = pos.x; y = pos.y + tgt.offsetHeight;
          break;
        case Tooltip.BOTTOM_RIGHT:
          x = pos.x + tgt.offsetWidth; y = pos.y + tgt.offsetHeight;
          break;
        case Tooltip.TOP_LEFT:
          x = pos.x; y = pos.y;
          break;
        case Tooltip.TOP_RIGHT:
          x = pos.x + tgt.offsetWidth; y = pos.y;
          break;
      }
      offset = 'fixed_offset';
    }
    
    x = x + this.options[offset].x * this.options[offset].dx;
    y = y + this.options[offset].y * this.options[offset].dy;

    this.size = this.calculate_dimension ();
    if (this.options[offset].dx < 0) x = x - this.size.width;
    if (this.options[offset].dy < 0) y = y - this.size.height;
    
    // Now make sure we're within the view.
    if (x + this.size.width > off.left + view.width) x = off.left + view.width - this.size.width;
    if (x < off.left) x = off.left;
    if (y + this.size.height > off.top + view.height) y = off.top + view.height - this.size.height;
    if (y < off.top) y = off.top;
    
    this.popup.style.top  = y + "px";
    this.popup.style.left = x + "px";
  },
  
  hide : function (evt)
  {
    if (this.popup.style.display == 'none') return;
    // Get mouse position
    var x = evt.pageX
         || (evt.clientX + this.scroll_offset ('Left'));
    var y = evt.pageY
         || (evt.clientY + this.scroll_offset ('Top'));
    // We hide it if we're neither within this.target nor within this.content nor within the
    // alternate target, if one was given.
    if (Tooltips.debug) {
      var tp = this.position (this.target);
      var pp = this.position (this.popup);
      alert ("x = " + x + " y = " + y + '\n' +
             "t: " + tp.x + "/" + tp.y + "/" +
               this.target.offsetWidth + "/" + this.target.offsetHeight + '\n' +
             (tp.n ? "t.m = " + tp.n.nodeName + "/" + tp.n.getAttribute ('margin') + "/"
                     + tp.n.getAttribute ('marginTop')
                     + "/" + tp.n.getAttribute ('border') + '\n'
                   : "") +
             "p: " + pp.x + "/" + pp.y + "/" +
               this.popup.offsetWidth + "/" + this.popup.offsetHeight + '\n' +
             (pp.n ? "p.m = " + pp.n.nodeName + "/" + pp.n.getAttribute ('margin') + "/"
                     + pp.n.getAttribute ('marginTop')
                     + "/" + pp.n.getAttribute ('border') + '\n'
                   : "") +
             "e: " + evt.pageX + "/" + evt.pageY + " "
               + evt.clientX + "/" + this.scroll_offset ('Left') + " "
               + evt.clientY + "/" + this.scroll_offset ('Top') + '\n'
             );
    }
    if (   !this.within (this.target, x, y)
        && !this.within (this.popup, x, y)
        && (!this.options.target || !this.within (this.options.target, x, y))) {
      if (this.open_timeout_id != null) window.clearTimeout (this.open_timeout_id);
      this.open_timeout_id = null;
      var event_copy = evt;
      if (this.options.hide_delay > 0) {
        var obj = this;
        this.hide_timeout_id =
          window.setTimeout (
              function () {obj.hide_popup (obj, event_copy);}
            , this.options.hide_delay
          );
      } else
        this.hide_popup (this, event_copy);
    }
  },
  
  hide_popup : function (elem, event)
  {
    if (elem.popup.style.display == 'none') return; // Already hidden, recursion from onclose?
    elem.popup.style.display = 'none';
    elem.hide_timeout_id = null;
    Tooltips.deregister (elem);
    if (elem.options.deactivate & Tooltip.ESCAPE)
      EvtHandler.remove (document, 'keydown', elem.eventKey);
    // Callback
    if (typeof (elem.options.onclose) == 'function') elem.options.onclose (elem, event);
  },
  
  hide_now : function (evt)
  {
    if (this.open_timeout_id != null) window.clearTimeout (this.open_timeout_id);
    this.open_timeout_id = null;
    var event_copy = evt || null;
    this.hide_popup (this, event_copy);
    if (evt && this.target.nodeName.toLowerCase == 'a') return EvtHandler.killEvt (evt); else return false;
  },
  
  key_handler : function (evt)
  {
    if (Tooltips.debug) alert ('key evt ' + evt.keyCode);
    if (evt.DOM_VK_ESCAPE && evt.keyCode == evt.DOM_VK_ESCAPE || evt.keyCode == 27)
      this.hide_now (evt);
    return true;
  },

  setZIndex : function (z_index)
  {
    if (z_index === null || isNaN (z_index) || z_index < 2) return;
    z_index = Math.floor (z_index);
    if (z_index == this.options.z_index) return; // No change
    this.popup.style.zIndex = z_index;
    this.options.z_index = z_index;
  },

  makeCloseButton : function ()
  {
    this.close_button = null;
    if (!this.options.close_button) return;
    var imgs = null;
    if (typeof (this.options.close_button.length) != 'undefined')
      imgs = this.options.close_button; // Also if it's a string (name of previously created class)
    else
      imgs = [this.options.close_button];
    if (!imgs || imgs.length == 0) return; // Paranoia
    var lk = Buttons.makeButton (imgs, this.tip_id + '_button', this.eventClose); 

    if (lk) {
      var width = lk.firstChild.getAttribute ('width');
      lk.style.cssFloat = 'right';
      lk.style.paddingTop   = '2px';
      lk.style.paddingRight = '2px';
      this.popup.firstChild.insertBefore (lk, this.popup.firstChild.firstChild);
      this.close_button = lk;
      this.close_button_width = parseInt ("" + width, 10);
    }
  },

  within : function (node, x, y)
  {
    if (!node) return false;
    var pos = this.position (node);
    return    (x == null || x > pos.x && x < pos.x + node.offsetWidth)
           && (y == null || y > pos.y && y < pos.y + node.offsetHeight);
  },
  
  position : (function ()
  {
    // The following is the jQuery.offset implementation. We cannot use jQuery yet in globally
    // activated scripts (it has strange side effects for Opera 8 users who can't log in anymore,
    // and it breaks the search box for some users). Note that jQuery does not support Opera 8.
    // Until the WMF servers serve jQuery by default, this copy from the jQuery 1.3.2 sources is
    // needed here. If and when we have jQuery available officially, the whole thing here can be
    // replaced by "var tmp = jQuery (node).offset(); return {x:tmp.left, y:tmp.top};"
    // Kudos to the jQuery development team. Any errors in this adaptation are my own. (Lupo,
    // 2009-08-24).
    //   Note: I have virtually the same code also in LAPI.js, but I cannot import that here
    // because I know that at least one gadget at the French Wikipedia includes this script here
    // directly from here. I'd have to use importScriptURI instead of importScript to keep that
    // working, but I'd run the risk that including LAPI at the French Wikipedia might break
    // something there. I *hate* it when people hotlink scripts across projects!

    var data = null;

    function jQuery_init ()
    {
      data = {};
      // Capability check from jQuery.
      var body = document.body;
      var container = document.createElement('div');
      var html =
          '<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;'
        + 'padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;'
        + 'top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" '
        + 'cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';
      var rules = { position: 'absolute', visibility: 'hidden'
                   ,top: 0, left: 0
                   ,margin: 0, border: 0
                   ,width: '1px', height: '1px'
                  };
      Object.merge (rules, container.style);

      container.innerHTML = html;
      body.insertBefore(container, body.firstChild);
      var innerDiv = container.firstChild;
      var checkDiv = innerDiv.firstChild;
      var td = innerDiv.nextSibling.firstChild.firstChild;

      data.doesNotAddBorder = (checkDiv.offsetTop !== 5);
      data.doesAddBorderForTableAndCells = (td.offsetTop === 5);

      innerDiv.style.overflow = 'hidden', innerDiv.style.position = 'relative';
      data.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);

      var bodyMarginTop    = body.style.marginTop;
      body.style.marginTop = '1px';
      data.doesNotIncludeMarginInBodyOffset = (body.offsetTop === 0);
      body.style.marginTop = bodyMarginTop;

      body.removeChild(container);
    };

    function jQuery_offset (node)
    {
      if (node === node.ownerDocument.body) return jQuery_bodyOffset (node);
      if (node.getBoundingClientRect) {
        var box    = node.getBoundingClientRect ();
        var scroll = {x : this.scroll_offset ('Left'), y: this.scroll_offset ('Top')};
        return {x : (box.left + scroll.x), y : (box.top + scroll.y)};
      }
      if (!data) jQuery_init ();
      var elem              = node;
      var offsetParent      = elem.offsetParent;
      var prevOffsetParent  = elem;
      var doc               = elem.ownerDocument;
      var prevComputedStyle = doc.defaultView.getComputedStyle(elem, null);
      var computedStyle;

      var top  = elem.offsetTop;
      var left = elem.offsetLeft;

      while ( (elem = elem.parentNode) && elem !== doc.body && elem !== doc.documentElement ) {
        computedStyle = doc.defaultView.getComputedStyle(elem, null);
        top -= elem.scrollTop, left -= elem.scrollLeft;
        if ( elem === offsetParent ) {
          top += elem.offsetTop, left += elem.offsetLeft;
          if (   data.doesNotAddBorder
              && !(data.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.tagName))
             )
          {
            top  += parseInt (computedStyle.borderTopWidth,  10) || 0;
            left += parseInt (computedStyle.borderLeftWidth, 10) || 0;
          }
          prevOffsetParent = offsetParent; offsetParent = elem.offsetParent;
        }
        if (data.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== 'visible')
        {
          top  += parseInt (computedStyle.borderTopWidth,  10) || 0;
          left += parseInt (computedStyle.borderLeftWidth, 10) || 0;
        }
        prevComputedStyle = computedStyle;
      }

      if (prevComputedStyle.position === 'relative' || prevComputedStyle.position === 'static') {
        top  += doc.body.offsetTop;
        left += doc.body.offsetLeft;
      }
      if (prevComputedStyle.position === 'fixed') {
        top  += Math.max (doc.documentElement.scrollTop, doc.body.scrollTop);
        left += Math.max (doc.documentElement.scrollLeft, doc.body.scrollLeft);
      }
      return {x: left, y: top};            
    }

    function jQuery_bodyOffset (body)
    {
      if (!data) jQuery_init();
      var top = body.offsetTop, left = body.offsetLeft;
      if (data.doesNotIncludeMarginInBodyOffset) {
        var styles;
        if (   body.ownerDocument.defaultView
            && body.ownerDocument.defaultView.getComputedStyle)
        { // Gecko etc.
          styles = body.ownerDocument.defaultView.getComputedStyle (body, null);
          top  += parseInt (style.getPropertyValue ('margin-top' ), 10) || 0;
          left += parseInt (style.getPropertyValue ('margin-left'), 10) || 0;
        } else {
          function to_px (element, val) {
            // Convert em etc. to pixels. Kudos to Dean Edwards; see
            // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
            if (!/^\d+(px)?$/i.test (val) && /^\d/.test (val) && body.runtimeStyle) {
              var style                 = element.style.left;
              var runtimeStyle          = element.runtimeStyle.left;
              element.runtimeStyle.left = element.currentStyle.left;
              element.style.left        = result || 0;
              val = elem.style.pixelLeft + "px";
              element.style.left        = style;
              element.runtimeStyle.left = runtimeStyle;
            }
            return val;
          }
          style = body.currentStyle || body.style;
          top  += parseInt (to_px (body, style.marginTop ), 10) || 0;
          left += parseInt (to_px (body, style.marginleft), 10) || 0;
        }
      }
      return {x: left, y: top};
    }

    return jQuery_offset;
  })(),

  scroll_offset : function (what)
  {
    var s = 'scroll' + what;
    return (document.documentElement ? document.documentElement[s] : 0)
           || document.body[s] || 0;
  },

  viewport : function (what)
  {
    var s = 'client' + what;
    return (document.documentElement ? document.documentElement[s] : 0) || document.body[s] || 0;
  },


  calculate_dimension : function ()
  {
    if (this.popup.style.display != 'none' && this.popup.style.display != null) {
      return {width : this.popup.offsetWidth, height : this.popup.offsetHeight};
    }
    // Must display it... but position = 'absolute' and visibility = 'hidden' means
    // the user won't notice it.
    var view_width = this.viewport ('Width');
    this.popup.style.top        = "0px";
    this.popup.style.left       = "0px";
    // Remove previous width as it may change with dynamic tooltips
    this.popup.style.width      = "";
    this.popup.style.maxWidth   = "";
    this.popup.style.overflow   = 'hidden';
    this.popup.style.visibility = 'hidden';
    // Remove the close button, otherwise the float will always extend the box to
    // the right edge.
    if (this.close_button)
      this.popup.firstChild.removeChild (this.close_button);
    this.popup.style.display = "";   // Display it. Now we should have a width
    var w = this.popup.offsetWidth;
    var h = this.popup.offsetHeight;
    var limit = Math.round (view_width * this.options.max_width);
    if (this.options.max_pixels > 0 && this.options.max_pixels < limit)
      limit = this.options.max_pixels;
    if (w > limit) {
      w = limit;
      this.popup.style.width    = "" + w + "px";
      this.popup.style.maxWidth = this.popup.style.width;
      if (this.close_button) {
        this.popup.firstChild.insertBefore
          (this.close_button, this.popup.firstChild.firstChild);
      }
    } else {
      this.popup.style.width    = "" + w + "px";
      this.popup.style.maxWidth = this.popup.style.width;
      if (this.close_button) {
        this.popup.firstChild.insertBefore
          (this.close_button, this.popup.firstChild.firstChild);
      }
      if (h != this.popup.offsetHeight) {
        w =  w + this.close_button_width;    
        this.popup.style.width    = "" + w + "px";
        this.popup.style.maxWidth = this.popup.style.width;
      }
    }
    var size = {width : this.popup.offsetWidth, height : this.popup.offsetHeight};
    this.popup.style.display = 'none';       // Hide it again
    this.popup.style.visibility = "";
    return size;
  }
    
} // end Tooltip

// </syntaxhighlight>