qtip2
Version:
Introducing... qTip2. The second generation of the advanced qTip plugin for the ever popular jQuery framework.
405 lines (341 loc) • 13.3 kB
JavaScript
function delay(callback, duration) {
// If tooltip has displayed, start hide timer
if(duration > 0) {
return setTimeout(
$.proxy(callback, this), duration
);
}
else{ callback.call(this); }
}
function showMethod(event) {
if(this.tooltip.hasClass(CLASS_DISABLED)) { return; }
// Clear hide timers
clearTimeout(this.timers.show);
clearTimeout(this.timers.hide);
// Start show timer
this.timers.show = delay.call(this,
function() { this.toggle(TRUE, event); },
this.options.show.delay
);
}
function hideMethod(event) {
if(this.tooltip.hasClass(CLASS_DISABLED) || this.destroyed) { return; }
// Check if new target was actually the tooltip element
var relatedTarget = $(event.relatedTarget),
ontoTooltip = relatedTarget.closest(SELECTOR)[0] === this.tooltip[0],
ontoTarget = relatedTarget[0] === this.options.show.target[0];
// Clear timers and stop animation queue
clearTimeout(this.timers.show);
clearTimeout(this.timers.hide);
// Prevent hiding if tooltip is fixed and event target is the tooltip.
// Or if mouse positioning is enabled and cursor momentarily overlaps
if(this !== relatedTarget[0] &&
(this.options.position.target === 'mouse' && ontoTooltip) ||
this.options.hide.fixed && (
(/mouse(out|leave|move)/).test(event.type) && (ontoTooltip || ontoTarget))
)
{
/* eslint-disable no-empty */
try {
event.preventDefault();
event.stopImmediatePropagation();
} catch(e) {}
/* eslint-enable no-empty */
return;
}
// If tooltip has displayed, start hide timer
this.timers.hide = delay.call(this,
function() { this.toggle(FALSE, event); },
this.options.hide.delay,
this
);
}
function inactiveMethod(event) {
if(this.tooltip.hasClass(CLASS_DISABLED) || !this.options.hide.inactive) { return; }
// Clear timer
clearTimeout(this.timers.inactive);
this.timers.inactive = delay.call(this,
function(){ this.hide(event); },
this.options.hide.inactive
);
}
function repositionMethod(event) {
if(this.rendered && this.tooltip[0].offsetWidth > 0) { this.reposition(event); }
}
// Store mouse coordinates
PROTOTYPE._storeMouse = function(event) {
(this.mouse = $.event.fix(event)).type = 'mousemove';
return this;
};
// Bind events
PROTOTYPE._bind = function(targets, events, method, suffix, context) {
if(!targets || !method || !events.length) { return; }
var ns = '.' + this._id + (suffix ? '-'+suffix : '');
$(targets).bind(
(events.split ? events : events.join(ns + ' ')) + ns,
$.proxy(method, context || this)
);
return this;
};
PROTOTYPE._unbind = function(targets, suffix) {
targets && $(targets).unbind('.' + this._id + (suffix ? '-'+suffix : ''));
return this;
};
// Global delegation helper
function delegate(selector, events, method) {
$(document.body).delegate(selector,
(events.split ? events : events.join('.'+NAMESPACE + ' ')) + '.'+NAMESPACE,
function() {
var api = QTIP.api[ $.attr(this, ATTR_ID) ];
api && !api.disabled && method.apply(api, arguments);
}
);
}
// Event trigger
PROTOTYPE._trigger = function(type, args, event) {
var callback = new $.Event('tooltip'+type);
callback.originalEvent = event && $.extend({}, event) || this.cache.event || NULL;
this.triggering = type;
this.tooltip.trigger(callback, [this].concat(args || []));
this.triggering = FALSE;
return !callback.isDefaultPrevented();
};
PROTOTYPE._bindEvents = function(showEvents, hideEvents, showTargets, hideTargets, showCallback, hideCallback) {
// Get tasrgets that lye within both
var similarTargets = showTargets.filter( hideTargets ).add( hideTargets.filter(showTargets) ),
toggleEvents = [];
// If hide and show targets are the same...
if(similarTargets.length) {
// Filter identical show/hide events
$.each(hideEvents, function(i, type) {
var showIndex = $.inArray(type, showEvents);
// Both events are identical, remove from both hide and show events
// and append to toggleEvents
showIndex > -1 && toggleEvents.push( showEvents.splice( showIndex, 1 )[0] );
});
// Toggle events are special case of identical show/hide events, which happen in sequence
if(toggleEvents.length) {
// Bind toggle events to the similar targets
this._bind(similarTargets, toggleEvents, function(event) {
var state = this.rendered ? this.tooltip[0].offsetWidth > 0 : false;
(state ? hideCallback : showCallback).call(this, event);
});
// Remove the similar targets from the regular show/hide bindings
showTargets = showTargets.not(similarTargets);
hideTargets = hideTargets.not(similarTargets);
}
}
// Apply show/hide/toggle events
this._bind(showTargets, showEvents, showCallback);
this._bind(hideTargets, hideEvents, hideCallback);
};
PROTOTYPE._assignInitialEvents = function(event) {
var options = this.options,
showTarget = options.show.target,
hideTarget = options.hide.target,
showEvents = options.show.event ? $.trim('' + options.show.event).split(' ') : [],
hideEvents = options.hide.event ? $.trim('' + options.hide.event).split(' ') : [];
// Catch remove/removeqtip events on target element to destroy redundant tooltips
this._bind(this.elements.target, ['remove', 'removeqtip'], function() {
this.destroy(true);
}, 'destroy');
/*
* Make sure hoverIntent functions properly by using mouseleave as a hide event if
* mouseenter/mouseout is used for show.event, even if it isn't in the users options.
*/
if(/mouse(over|enter)/i.test(options.show.event) && !/mouse(out|leave)/i.test(options.hide.event)) {
hideEvents.push('mouseleave');
}
/*
* Also make sure initial mouse targetting works correctly by caching mousemove coords
* on show targets before the tooltip has rendered. Also set onTarget when triggered to
* keep mouse tracking working.
*/
this._bind(showTarget, 'mousemove', function(moveEvent) {
this._storeMouse(moveEvent);
this.cache.onTarget = TRUE;
});
// Define hoverIntent function
function hoverIntent(hoverEvent) {
// Only continue if tooltip isn't disabled
if(this.disabled || this.destroyed) { return FALSE; }
// Cache the event data
this.cache.event = hoverEvent && $.event.fix(hoverEvent);
this.cache.target = hoverEvent && $(hoverEvent.target);
// Start the event sequence
clearTimeout(this.timers.show);
this.timers.show = delay.call(this,
function() { this.render(typeof hoverEvent === 'object' || options.show.ready); },
options.prerender ? 0 : options.show.delay
);
}
// Filter and bind events
this._bindEvents(showEvents, hideEvents, showTarget, hideTarget, hoverIntent, function() {
if(!this.timers) { return FALSE; }
clearTimeout(this.timers.show);
});
// Prerendering is enabled, create tooltip now
if(options.show.ready || options.prerender) { hoverIntent.call(this, event); }
};
// Event assignment method
PROTOTYPE._assignEvents = function() {
var self = this,
options = this.options,
posOptions = options.position,
tooltip = this.tooltip,
showTarget = options.show.target,
hideTarget = options.hide.target,
containerTarget = posOptions.container,
viewportTarget = posOptions.viewport,
documentTarget = $(document),
windowTarget = $(window),
showEvents = options.show.event ? $.trim('' + options.show.event).split(' ') : [],
hideEvents = options.hide.event ? $.trim('' + options.hide.event).split(' ') : [];
// Assign passed event callbacks
$.each(options.events, function(name, callback) {
self._bind(tooltip, name === 'toggle' ? ['tooltipshow','tooltiphide'] : ['tooltip'+name], callback, null, tooltip);
});
// Hide tooltips when leaving current window/frame (but not select/option elements)
if(/mouse(out|leave)/i.test(options.hide.event) && options.hide.leave === 'window') {
this._bind(documentTarget, ['mouseout', 'blur'], function(event) {
if(!/select|option/.test(event.target.nodeName) && !event.relatedTarget) {
this.hide(event);
}
});
}
// Enable hide.fixed by adding appropriate class
if(options.hide.fixed) {
hideTarget = hideTarget.add( tooltip.addClass(CLASS_FIXED) );
}
/*
* Make sure hoverIntent functions properly by using mouseleave to clear show timer if
* mouseenter/mouseout is used for show.event, even if it isn't in the users options.
*/
else if(/mouse(over|enter)/i.test(options.show.event)) {
this._bind(hideTarget, 'mouseleave', function() {
clearTimeout(this.timers.show);
});
}
// Hide tooltip on document mousedown if unfocus events are enabled
if(('' + options.hide.event).indexOf('unfocus') > -1) {
this._bind(containerTarget.closest('html'), ['mousedown', 'touchstart'], function(event) {
var elem = $(event.target),
enabled = this.rendered && !this.tooltip.hasClass(CLASS_DISABLED) && this.tooltip[0].offsetWidth > 0,
isAncestor = elem.parents(SELECTOR).filter(this.tooltip[0]).length > 0;
if(elem[0] !== this.target[0] && elem[0] !== this.tooltip[0] && !isAncestor &&
!this.target.has(elem[0]).length && enabled
) {
this.hide(event);
}
});
}
// Check if the tooltip hides when inactive
if('number' === typeof options.hide.inactive) {
// Bind inactive method to show target(s) as a custom event
this._bind(showTarget, 'qtip-'+this.id+'-inactive', inactiveMethod, 'inactive');
// Define events which reset the 'inactive' event handler
this._bind(hideTarget.add(tooltip), QTIP.inactiveEvents, inactiveMethod);
}
// Filter and bind events
this._bindEvents(showEvents, hideEvents, showTarget, hideTarget, showMethod, hideMethod);
// Mouse movement bindings
this._bind(showTarget.add(tooltip), 'mousemove', function(event) {
// Check if the tooltip hides when mouse is moved a certain distance
if('number' === typeof options.hide.distance) {
var origin = this.cache.origin || {},
limit = this.options.hide.distance,
abs = Math.abs;
// Check if the movement has gone beyond the limit, and hide it if so
if(abs(event.pageX - origin.pageX) >= limit || abs(event.pageY - origin.pageY) >= limit) {
this.hide(event);
}
}
// Cache mousemove coords on show targets
this._storeMouse(event);
});
// Mouse positioning events
if(posOptions.target === 'mouse') {
// If mouse adjustment is on...
if(posOptions.adjust.mouse) {
// Apply a mouseleave event so we don't get problems with overlapping
if(options.hide.event) {
// Track if we're on the target or not
this._bind(showTarget, ['mouseenter', 'mouseleave'], function(event) {
if(!this.cache) {return FALSE; }
this.cache.onTarget = event.type === 'mouseenter';
});
}
// Update tooltip position on mousemove
this._bind(documentTarget, 'mousemove', function(event) {
// Update the tooltip position only if the tooltip is visible and adjustment is enabled
if(this.rendered && this.cache.onTarget && !this.tooltip.hasClass(CLASS_DISABLED) && this.tooltip[0].offsetWidth > 0) {
this.reposition(event);
}
});
}
}
// Adjust positions of the tooltip on window resize if enabled
if(posOptions.adjust.resize || viewportTarget.length) {
this._bind( $.event.special.resize ? viewportTarget : windowTarget, 'resize', repositionMethod );
}
// Adjust tooltip position on scroll of the window or viewport element if present
if(posOptions.adjust.scroll) {
this._bind( windowTarget.add(posOptions.container), 'scroll', repositionMethod );
}
};
// Un-assignment method
PROTOTYPE._unassignEvents = function() {
var options = this.options,
showTargets = options.show.target,
hideTargets = options.hide.target,
targets = $.grep([
this.elements.target[0],
this.rendered && this.tooltip[0],
options.position.container[0],
options.position.viewport[0],
options.position.container.closest('html')[0], // unfocus
window,
document
], function(i) {
return typeof i === 'object';
});
// Add show and hide targets if they're valid
if(showTargets && showTargets.toArray) {
targets = targets.concat(showTargets.toArray());
}
if(hideTargets && hideTargets.toArray) {
targets = targets.concat(hideTargets.toArray());
}
// Unbind the events
this._unbind(targets)
._unbind(targets, 'destroy')
._unbind(targets, 'inactive');
};
// Apply common event handlers using delegate (avoids excessive .bind calls!)
$(function() {
delegate(SELECTOR, ['mouseenter', 'mouseleave'], function(event) {
var state = event.type === 'mouseenter',
tooltip = $(event.currentTarget),
target = $(event.relatedTarget || event.target),
options = this.options;
// On mouseenter...
if(state) {
// Focus the tooltip on mouseenter (z-index stacking)
this.focus(event);
// Clear hide timer on tooltip hover to prevent it from closing
tooltip.hasClass(CLASS_FIXED) && !tooltip.hasClass(CLASS_DISABLED) && clearTimeout(this.timers.hide);
}
// On mouseleave...
else {
// When mouse tracking is enabled, hide when we leave the tooltip and not onto the show target (if a hide event is set)
if(options.position.target === 'mouse' && options.position.adjust.mouse &&
options.hide.event && options.show.target && !target.closest(options.show.target[0]).length) {
this.hide(event);
}
}
// Add hover class
tooltip.toggleClass(CLASS_HOVER, state);
});
// Define events which reset the 'inactive' event handler
delegate('['+ATTR_ID+']', INACTIVE_EVENTS, inactiveMethod);
});