MediaWiki:Tooltips.js

From B.E.R.T. Wiki

(Redirected from Tooltips.js)

// <source 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 is_IE = !!window.ActiveXObject;

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]); }
 },
 mouse_offset : function ()
 {
   // IE does some strange things...
   // This is adapted from dojo 0.9.0, see http://dojotoolkit.org
   if (is_IE) {
     var doc_elem = document.documentElement;
     if (doc_elem) {
       if (typeof (doc_elem.getBoundingClientRect) == 'function') {
         var tmp = doc_elem.getBoundingClientRect ();
         return {x : tmp.left, y : tmp.top};
       } else {
         return {x : doc_elem.clientLeft, y : doc_elem.clientTop};
       }
     }
   }
   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;

// On IE, use the mouseleave/mouseenter events, which fire only when the boundaries of the // element are left (but not when the element if left because the mouse moved over some // contained element)

Tooltip.mouse_in = (is_IE ? 'mouseenter' : 'mouseover'); Tooltip.mouse_out = (is_IE ? 'mouseleave' : 'mouseout');

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';
   // On IE, 'relative' triggers lots of float:right bugs (floats become invisible or are
   // mispositioned).
   //if (!is_IE) wrapper.style.position = 'relative';
   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);
   this.ieFix = null;
   if (is_IE) {
     // Display an invisible IFrame of the same size as the popup beneath it to make popups
     // correctly cover "windowed controls" such as form input fields in IE. For IE >=5.5, but
     // who still uses older IEs?? The technique is also known as a "shim". A good
     // description is at http://dev2dev.bea.com/lpt/a/39
     this.ieFix = document.createElement ('iframe');
     this.ieFix.style.position = 'absolute';
     this.ieFix.style.border   = '0';
     this.ieFix.style.margin   = '0';
     this.ieFix.style.padding  = '0';
     this.ieFix.style.zIndex   = "" + (this.options.z_index - 1); // Below the popup
     this.ieFix.tabIndex       = -1;
     this.ieFix.frameBorder    = '0';
     this.ieFix.style.display  = 'none';
     document.body.appendChild (this.ieFix);
     this.ieFix.style.filter  = 'alpha(Opacity=0)'; // Ensure transparency
   } 
 },
 
 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, Tooltip.mouse_in, 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, Tooltip.mouse_out, this.eventHide);
       op (this.popup, Tooltip.mouse_out, this.eventHide);
       if (this.options.target) op (this.options.target, Tooltip.mouse_out, 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, Tooltip.mouse_in, 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);
   if (this.ieFix) document.body.removeElement (this.ieFix);
 },
 
 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);
   if (elem.ieFix) {
     elem.ieFix.style.top     = elem.popup.style.top;
     elem.ieFix.style.left    = elem.popup.style.left;
     elem.ieFix.style.width   = elem.size.width + "px";
     elem.ieFix.style.height  = elem.size.height + "px";
     elem.ieFix.style.display = "";
   }
   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);
   // Also move the shim!
   if (this.ieFix) {
     this.ieFix.style.top     = this.popup.style.top;
     this.ieFix.style.left    = this.popup.style.left;
     this.ieFix.style.width   = this.size.width + "px";
     this.ieFix.style.height  = this.size.height + "px";
   }
 },
 
 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};
     if (this.ieFix) {
       this.ieFix.style.top     = this.popup.style.top;
       this.ieFix.style.left    = this.popup.style.left;
       this.ieFix.style.width   = this.size.width + "px";
       this.ieFix.style.height  = this.size.height + "px";
     }
   }
 },
   
 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) {
     var mouse_delta = EvtHandler.mouse_offset ();
     if (Tooltips.debug && mouse_delta) {
       alert ("Mouse offsets: x = " + mouse_delta.x + ", y = " + mouse_delta.y);
     }
     x = (evt.pageX || (evt.clientX + off.left - (mouse_delta ? mouse_delta.x : 0)));
     y = (evt.pageY || (evt.clientY + off.top - (mouse_delta ? mouse_delta.y : 0)));
     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 mouse_delta = EvtHandler.mouse_offset ();
   var x = evt.pageX
        || (evt.clientX + this.scroll_offset ('Left') - (mouse_delta ? mouse_delta.x : 0));
   var y = evt.pageY
        || (evt.clientY + this.scroll_offset ('Top') - (mouse_delta ? mouse_delta.y : 0));
   // 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' +
            (mouse_delta ? "m : " + mouse_delta.x + "/" + mouse_delta.y + '\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';
   if (elem.ieFix) elem.ieFix.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
   if (this.ieFix) {
     // Always keep the shim below the actual popup.
     if (z_index > this.options.z_index) {
       this.popup.style.zIndex = z_index;
       this.ieFix.style.zIndex = "" + (z_index - 1);
     } else {
       this.ieFix.style.zIndex = "" + (z_index - 1);
       this.popup.style.zIndex = z_index;
     }
   } else {
     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');
     if (!is_IE) {
       lk.style.cssFloat = 'right';
     } else {
       // IE is incredibly broken on right floats.
       var container = document.createElement ('div');
       container.style.display      = 'inline';
       container.style.styleFloat   = 'right';
       container.appendChild (lk);
       lk = container;
     }
     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 =
'
';
     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

// </source>