Jump to content

MediaWiki:Tooltips.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by commons>Lupo at 17:07, 8 March 2008 (Tested at home on Win XP Pro Sp 2: FF 2.0.0.12, IE6, Opera 9.26, Opera 9.50beta, Safari 3.0.4beta. Now let's see if this works here, too.). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
(diff) ← Previous revision | Latest revision (diff) | Newer revision → (diff)
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <source lang="javascript">

/*
  Cross-browser tooltip support for MediaWiki.
  
  Author: [[User:Lupo]], March 2008
  
  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 Class = {
  create: function () {
    return function () {
      this.initialize.apply (this, arguments);
    }
  }
}

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]); }
  }
}

var Tooltips = {
  // Helper object to force quick closure of a tooltip if another one shows up.
  
  top_tip : null,
  
  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;
  }
}

var Tooltip = Class.create ();

// 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.HOVER             = 1;
Tooltip.FOCUS             = 2; // always uses the FIXED position
Tooltip.CLICK             = 4;
Tooltip.ALL_ACTIVATIONS   = 7;

// Deactivation constants

Tooltip.LEAVE             = 1;
Tooltip.CLICK_ELEM        = 2;
Tooltip.CLICK_TIP         = 4; // Makes only sense if not tracked
Tooltip.ALL_DEACTIVATIONS = 7;

// Of course IE does things differently than all the rest.
Tooltip.mouse_in    = (window.ActiveXObject ? 'mouseenter' : 'mouseover');
Tooltip.mouse_out   = (window.ActiveXObject ? 'mouseleave' : 'mouseout');

Tooltip.prototype =
{
  initialize : function (on_element, tt_content, opt, css)
  {
    // 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 abosulte positioning
    // (We always position in global document coordinates.)
    // In my tests, it appeared as if Nick Stakenburg's "prototip" has
    // this problem...
    if (tt_content.parentNode) {
      if (tt_content.ownerDocument != document)
        tt_content = document.importNode (tt_content, true);
      else
        tt_content = tt_content.cloneNode (true);
    }
    if (css) {
      for (var styledef in css) tt_content.style[styledef] = css[styledef];
    }
    if (tt_content.id && tt_content.id.length > 0) tt_content.id = tt_content.id + '_tooltip';
    if (is_opera_95) tt_content.style.opacity = "1.0"; // Bug workaround.
    if (tt_content.style.display == 'none') tt_content.style.display = "";
    this.content = tt_content;
    // Wrap it
    var wrapper = document.createElement ('div');
    wrapper.style.position = 'relative';
    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.MOUSE              // Where to display the tooltip.
      ,activate     : Tooltip.HOVER              // When to activate
      ,deactivate   : Tooltip.LEAVE | Tooltip.CLICK_ELEM // 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%)
      ,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
    };
    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];
      }
    }
    if ((this.options.activate & Tooltip.ALL_ACTIVATIONS) == 0)
      this.options.activate = Tooltip.HOVER;
    if ((this.options.deactivate & Tooltip.ALL_DEACTIVATIONS) == 0)
      this.options.deactivate = Tooltip.LEAVE | Tooltip.CLICK_ELEM;    
    document.body.appendChild (this.popup);
    this.has_links = this.content.getElementsByTagName ('a').length > 0;
    if (this.has_links && (this.options.mode == Tooltip.TRACK))
      this.options.mode = Tooltip.MOUSE; // If you track a tooltip with links, you'll never be able to click the links
    // 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};
    // Set up event handlers as appropriate
    this.eventShow  = EvtHandler.make_listener (this, this.show);
    this.eventFocus = EvtHandler.make_listener (this, this.show_focus);
    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.setupEvents (EvtHandler.attach);
  },
  
  setupEvents : function (op)
  {
    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) op (this.target, 'click', this.eventShow);
    if (this.options.deactivate & Tooltip.LEAVE) {
    	op (this.target, Tooltip.mouse_out, this.eventHide);
      op (this.target, 'blur', this.eventHide);
      op (this.content, Tooltip.mouse_out, this.eventHide);
      if (this.options.target) op (this.options.target, Tooltip.mouse_out, this.eventHide);
    }     
    if (this.options.deactivate & Tooltip.CLICK_ELEM) {
    	op (this.target, 'click', this.eventClose);
    }
    if (   (this.options.deactivate & Tooltip.CLICK_TIP)
        && (this.options.mode != Tooltip.TRACK))
      op (this.content, 'click', this.eventClose);
    if (this.options.mode == Tooltip.TRACK)
      op (this.target, 'mousemove', this.eventTrack);
    
    // Some more event handling
    if (this.hide_delay > 0) {
      op (this.content, Tooltip.mouse_in, this.eventShow);
      op (this.content, 'mousemove', this.eventShow);
    }
  },
  
  remove: function ()
  {
  	this.hide_now ();
    this.setupEvents (EvtHandler.remove);
  },
  
  show : function (evt)
  {
  	this.show_tip (evt, true);
  },
  
  show_focus : function (evt)
  {
  	this.show_tip (evt, 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;
    // Position it now. It must be positioned before the timeout below!
    this.position_tip (evt, is_mouse_evt);

    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
    elem.open_timeout_id = null;
  },
  
  track : function (evt)
  {
  	this.position_tip (evt, true);
  },
  
  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 pos = this.position (this.options.target || this.target);
    	switch (this.options.anchor) {
    		default:
    		case Tooltip.BOTTOM_LEFT:
    		  x = pos.x; y = pos.y + this.target.offsetHeight;
    		  break;
    		case Tooltip.BOTTOM_RIGHT:
    		  x = pos.x + this.target.offsetWidth; y = pos.y + this.target.offsetHeight;
    		  break;
    		case Tooltip.TOP_LEFT:
    		  x = pos.x; y = pos.y;
    		  break;
    		case Tooltip.TOP_RIGHT:
    		  x = pos.x + this.target.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.open_timeout_id != null) window.clearTimeout (this.open_timeout_id);
    this.open_timeout_id = null;
    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
    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.options.hide_delay > 0) {
        var obj = this;
        this.hide_timeout_id = window.setTimeout (function () {obj.hide_popup (obj);}, this.options.hide_delay);
      } else
        this.hide_popup (this);
    }
  },
  
  hide_popup : function (elem)
  {
    elem.popup.style.display = 'none';
    elem.hide_timeout_id = null;
    Tooltips.deregister (elem);
  },
  
  hide_now : function (evt)
  {
    this.hide_popup (this);
  },
  
  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 (node)
  {
    var t = 0, l = 0;
    do {
      t = t + (node.offsetTop  || 0);
      l = l + (node.offsetLeft || 0);
      node = node.offsetParent;
    } while (node);
    return {x : l, y : t};
  },
  
  scroll_offset : function (what)
  {
  	var s = 'scroll' + what;
  	return (document.documentElement ? document.documentElement[s] : 0)
           || document.body[s] || 0;
  },

  viewport : function (what)
  {
  	if (is_opera_95 || is_safari && !document.evaluate) return window['inner' + what];
  	var s = 'client' + what;
  	if (is_opera) return document.body[s];
  	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.top = "0px";
    this.popup.style.overflow = 'hidden';
    this.popup.style.visibility = 'hidden';
    this.popup.style.display = "";   // Display it. Now we should have a width
    var w = this.popup.offsetWidth;
    if (w > view_width * this.options.max_width) {
      this.popup.style.maxWidth = Math.round (view_width * this.options.max_width) + "px";
      this.popup.style.width    = Math.round (view_width * this.options.max_width) + "px";
    }
    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>