MediaWiki:Tooltips.js
Appearance
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>