/* * Poshy Tip jQuery plugin v1.2 * http://vadikom.com/tools/poshy-tip-jquery-plugin-for-stylish-tooltips/ * Copyright 2010-2013, Vasil Dinkov, http://vadikom.com/ */ (function($) { var tips = [], reBgImage = /^url\(["']?([^"'\)]*)["']?\);?$/i, rePNG = /\.png$/i, ie6 = !!window.createPopup && document.documentElement.currentStyle.minWidth == 'undefined'; // make sure the tips' position is updated on resize function handleWindowResize() { $.each(tips, function() { this.refresh(true); }); } $(window).resize(handleWindowResize); $.Poshytip = function(elm, options) { this.$elm = $(elm); this.opts = $.extend({}, $.fn.poshytip.defaults, options); this.$tip = $(['
', '
', '
', '
'].join('')).appendTo(document.body); this.$arrow = this.$tip.find('div.tip-arrow'); this.$inner = this.$tip.find('div.tip-inner'); this.disabled = false; this.content = null; this.init(); }; $.Poshytip.prototype = { init: function() { tips.push(this); // save the original title and a reference to the Poshytip object var title = this.$elm.attr('title'); this.$elm.data('title.poshytip', title !== undefined ? title : null) .data('poshytip', this); // hook element events if (this.opts.showOn != 'none') { this.$elm.bind({ 'mouseenter.poshytip': $.proxy(this.mouseenter, this), 'mouseleave.poshytip': $.proxy(this.mouseleave, this) }); switch (this.opts.showOn) { case 'hover': if (this.opts.alignTo == 'cursor') this.$elm.bind('mousemove.poshytip', $.proxy(this.mousemove, this)); if (this.opts.allowTipHover) this.$tip.hover($.proxy(this.clearTimeouts, this), $.proxy(this.mouseleave, this)); break; case 'focus': this.$elm.bind({ 'focus.poshytip': $.proxy(this.showDelayed, this), 'blur.poshytip': $.proxy(this.hideDelayed, this) }); break; } } }, mouseenter: function(e) { if (this.disabled) return true; this.$elm.attr('title', ''); if (this.opts.showOn == 'focus') return true; this.showDelayed(); }, mouseleave: function(e) { if (this.disabled || this.asyncAnimating && (this.$tip[0] === e.relatedTarget || jQuery.contains(this.$tip[0], e.relatedTarget))) return true; if (!this.$tip.data('active')) { var title = this.$elm.data('title.poshytip'); if (title !== null) this.$elm.attr('title', title); } if (this.opts.showOn == 'focus') return true; this.hideDelayed(); }, mousemove: function(e) { if (this.disabled) return true; this.eventX = e.pageX; this.eventY = e.pageY; if (this.opts.followCursor && this.$tip.data('active')) { this.calcPos(); this.$tip.css({left: this.pos.l, top: this.pos.t}); if (this.pos.arrow) this.$arrow[0].className = 'tip-arrow tip-arrow-' + this.pos.arrow; } }, show: function() { if (this.disabled || this.$tip.data('active')) return; this.reset(); this.update(); // don't proceed if we didn't get any content in update() (e.g. the element has an empty title attribute) if (!this.content) return; this.display(); if (this.opts.timeOnScreen) this.hideDelayed(this.opts.timeOnScreen); }, showDelayed: function(timeout) { this.clearTimeouts(); this.showTimeout = setTimeout($.proxy(this.show, this), typeof timeout == 'number' ? timeout : this.opts.showTimeout); }, hide: function() { if (this.disabled || !this.$tip.data('active')) return; this.display(true); }, hideDelayed: function(timeout) { this.clearTimeouts(); this.hideTimeout = setTimeout($.proxy(this.hide, this), typeof timeout == 'number' ? timeout : this.opts.hideTimeout); }, reset: function() { this.$tip.queue([]).detach().css('visibility', 'hidden').data('active', false); this.$inner.find('*').poshytip('hide'); if (this.opts.fade) this.$tip.css('opacity', this.opacity); this.$arrow[0].className = 'tip-arrow tip-arrow-top tip-arrow-right tip-arrow-bottom tip-arrow-left'; this.asyncAnimating = false; }, update: function(content, dontOverwriteOption) { if (this.disabled) return; var async = content !== undefined; if (async) { if (!dontOverwriteOption) this.opts.content = content; if (!this.$tip.data('active')) return; } else { content = this.opts.content; } // update content only if it has been changed since last time var self = this, newContent = typeof content == 'function' ? content.call(this.$elm[0], function(newContent) { self.update(newContent); }) : content == '[title]' ? this.$elm.data('title.poshytip') : content; if (this.content !== newContent) { this.$inner.empty().append(newContent); this.content = newContent; } this.refresh(async); }, refresh: function(async) { if (this.disabled) return; if (async) { if (!this.$tip.data('active')) return; // save current position as we will need to animate var currPos = {left: this.$tip.css('left'), top: this.$tip.css('top')}; } // reset position to avoid text wrapping, etc. this.$tip.css({left: 0, top: 0}).appendTo(document.body); // save default opacity if (this.opacity === undefined) this.opacity = this.$tip.css('opacity'); // check for images - this code is here (i.e. executed each time we show the tip and not on init) due to some browser inconsistencies var bgImage = this.$tip.css('background-image').match(reBgImage), arrow = this.$arrow.css('background-image').match(reBgImage); if (bgImage) { var bgImagePNG = rePNG.test(bgImage[1]); // fallback to background-color/padding/border in IE6 if a PNG is used if (ie6 && bgImagePNG) { this.$tip.css('background-image', 'none'); this.$inner.css({margin: 0, border: 0, padding: 0}); bgImage = bgImagePNG = false; } else { this.$tip.prepend('
') .css({border: 0, padding: 0, 'background-image': 'none', 'background-color': 'transparent'}) .find('.tip-bg-image').css('background-image', 'url("' + bgImage[1] +'")').end() .find('td').eq(3).append(this.$inner); } // disable fade effect in IE due to Alpha filter + translucent PNG issue if (bgImagePNG && !$.support.opacity) this.opts.fade = false; } // IE arrow fixes if (arrow && !$.support.opacity) { // disable arrow in IE6 if using a PNG if (ie6 && rePNG.test(arrow[1])) { arrow = false; this.$arrow.css('background-image', 'none'); } // disable fade effect in IE due to Alpha filter + translucent PNG issue this.opts.fade = false; } var $table = this.$tip.find('> table.tip-table'); if (ie6) { // fix min/max-width in IE6 this.$tip[0].style.width = ''; $table.width('auto').find('td').eq(3).width('auto'); var tipW = this.$tip.width(), minW = parseInt(this.$tip.css('min-width')), maxW = parseInt(this.$tip.css('max-width')); if (!isNaN(minW) && tipW < minW) tipW = minW; else if (!isNaN(maxW) && tipW > maxW) tipW = maxW; this.$tip.add($table).width(tipW).eq(0).find('td').eq(3).width('100%'); } else if ($table[0]) { // fix the table width if we are using a background image // IE9, FF4 use float numbers for width/height so use getComputedStyle for them to avoid text wrapping // for details look at: http://vadikom.com/dailies/offsetwidth-offsetheight-useless-in-ie9-firefox4/ $table.width('auto').find('td').eq(3).width('auto').end().end().width(document.defaultView && document.defaultView.getComputedStyle && parseFloat(document.defaultView.getComputedStyle(this.$tip[0], null).width) || this.$tip.width()).find('td').eq(3).width('100%'); } this.tipOuterW = this.$tip.outerWidth(); this.tipOuterH = this.$tip.outerHeight(); this.calcPos(); // position and show the arrow image if (arrow && this.pos.arrow) { this.$arrow[0].className = 'tip-arrow tip-arrow-' + this.pos.arrow; this.$arrow.css('visibility', 'inherit'); } if (async && this.opts.refreshAniDuration) { this.asyncAnimating = true; var self = this; this.$tip.css(currPos).animate({left: this.pos.l, top: this.pos.t}, this.opts.refreshAniDuration, function() { self.asyncAnimating = false; }); } else { this.$tip.css({left: this.pos.l, top: this.pos.t}); } }, display: function(hide) { var active = this.$tip.data('active'); if (active && !hide || !active && hide) return; this.$tip.stop(); if ((this.opts.slide && this.pos.arrow || this.opts.fade) && (hide && this.opts.hideAniDuration || !hide && this.opts.showAniDuration)) { var from = {}, to = {}; // this.pos.arrow is only undefined when alignX == alignY == 'center' and we don't need to slide in that rare case if (this.opts.slide && this.pos.arrow) { var prop, arr; if (this.pos.arrow == 'bottom' || this.pos.arrow == 'top') { prop = 'top'; arr = 'bottom'; } else { prop = 'left'; arr = 'right'; } var val = parseInt(this.$tip.css(prop)); from[prop] = val + (hide ? 0 : (this.pos.arrow == arr ? -this.opts.slideOffset : this.opts.slideOffset)); to[prop] = val + (hide ? (this.pos.arrow == arr ? this.opts.slideOffset : -this.opts.slideOffset) : 0) + 'px'; } if (this.opts.fade) { from.opacity = hide ? this.$tip.css('opacity') : 0; to.opacity = hide ? 0 : this.opacity; } this.$tip.css(from).animate(to, this.opts[hide ? 'hideAniDuration' : 'showAniDuration']); } hide ? this.$tip.queue($.proxy(this.reset, this)) : this.$tip.css('visibility', 'inherit'); if (active) { var title = this.$elm.data('title.poshytip'); if (title !== null) this.$elm.attr('title', title); } this.$tip.data('active', !active); }, disable: function() { this.reset(); this.disabled = true; }, enable: function() { this.disabled = false; }, destroy: function() { this.reset(); this.$tip.remove(); delete this.$tip; this.content = null; this.$elm.unbind('.poshytip').removeData('title.poshytip').removeData('poshytip'); tips.splice($.inArray(this, tips), 1); }, clearTimeouts: function() { if (this.showTimeout) { clearTimeout(this.showTimeout); this.showTimeout = 0; } if (this.hideTimeout) { clearTimeout(this.hideTimeout); this.hideTimeout = 0; } }, calcPos: function() { var pos = {l: 0, t: 0, arrow: ''}, $win = $(window), win = { l: $win.scrollLeft(), t: $win.scrollTop(), w: $win.width(), h: $win.height() }, xL, xC, xR, yT, yC, yB; if (this.opts.alignTo == 'cursor') { xL = xC = xR = this.eventX; yT = yC = yB = this.eventY; } else { // this.opts.alignTo == 'target' var elmOffset = this.$elm.offset(), elm = { l: elmOffset.left, t: elmOffset.top, w: this.$elm.outerWidth(), h: this.$elm.outerHeight() }; xL = elm.l + (this.opts.alignX != 'inner-right' ? 0 : elm.w); // left edge xC = xL + Math.floor(elm.w / 2); // h center xR = xL + (this.opts.alignX != 'inner-left' ? elm.w : 0); // right edge yT = elm.t + (this.opts.alignY != 'inner-bottom' ? 0 : elm.h); // top edge yC = yT + Math.floor(elm.h / 2); // v center yB = yT + (this.opts.alignY != 'inner-top' ? elm.h : 0); // bottom edge } // keep in viewport and calc arrow position switch (this.opts.alignX) { case 'right': case 'inner-left': pos.l = xR + this.opts.offsetX; if (this.opts.keepInViewport && pos.l + this.tipOuterW > win.l + win.w) pos.l = win.l + win.w - this.tipOuterW; if (this.opts.alignX == 'right' || this.opts.alignY == 'center') pos.arrow = 'left'; break; case 'center': pos.l = xC - Math.floor(this.tipOuterW / 2); if (this.opts.keepInViewport) { if (pos.l + this.tipOuterW > win.l + win.w) pos.l = win.l + win.w - this.tipOuterW; else if (pos.l < win.l) pos.l = win.l; } break; default: // 'left' || 'inner-right' pos.l = xL - this.tipOuterW - this.opts.offsetX; if (this.opts.keepInViewport && pos.l < win.l) pos.l = win.l; if (this.opts.alignX == 'left' || this.opts.alignY == 'center') pos.arrow = 'right'; } switch (this.opts.alignY) { case 'bottom': case 'inner-top': pos.t = yB + this.opts.offsetY; // 'left' and 'right' need priority for 'target' if (!pos.arrow || this.opts.alignTo == 'cursor') pos.arrow = 'top'; if (this.opts.keepInViewport && pos.t + this.tipOuterH > win.t + win.h) { pos.t = yT - this.tipOuterH - this.opts.offsetY; if (pos.arrow == 'top') pos.arrow = 'bottom'; } break; case 'center': pos.t = yC - Math.floor(this.tipOuterH / 2); if (this.opts.keepInViewport) { if (pos.t + this.tipOuterH > win.t + win.h) pos.t = win.t + win.h - this.tipOuterH; else if (pos.t < win.t) pos.t = win.t; } break; default: // 'top' || 'inner-bottom' pos.t = yT - this.tipOuterH - this.opts.offsetY; // 'left' and 'right' need priority for 'target' if (!pos.arrow || this.opts.alignTo == 'cursor') pos.arrow = 'bottom'; if (this.opts.keepInViewport && pos.t < win.t) { pos.t = yB + this.opts.offsetY; if (pos.arrow == 'bottom') pos.arrow = 'top'; } } this.pos = pos; } }; $.fn.poshytip = function(options) { if (typeof options == 'string') { var args = arguments, method = options; Array.prototype.shift.call(args); // unhook live events if 'destroy' is called if (method == 'destroy') { this.die ? this.die('mouseenter.poshytip').die('focus.poshytip') : $(document).undelegate(this.selector, 'mouseenter.poshytip').undelegate(this.selector, 'focus.poshytip'); } return this.each(function() { var poshytip = $(this).data('poshytip'); if (poshytip && poshytip[method]) poshytip[method].apply(poshytip, args); }); } var opts = $.extend({}, $.fn.poshytip.defaults, options); // generate CSS for this tip class if not already generated if (!$('#poshytip-css-' + opts.className)[0]) $([''].join('')).appendTo('head'); // check if we need to hook live events if (opts.liveEvents && opts.showOn != 'none') { var handler, deadOpts = $.extend({}, opts, { liveEvents: false }); switch (opts.showOn) { case 'hover': handler = function() { var $this = $(this); if (!$this.data('poshytip')) $this.poshytip(deadOpts).poshytip('mouseenter'); }; // support 1.4.2+ & 1.9+ this.live ? this.live('mouseenter.poshytip', handler) : $(document).delegate(this.selector, 'mouseenter.poshytip', handler); break; case 'focus': handler = function() { var $this = $(this); if (!$this.data('poshytip')) $this.poshytip(deadOpts).poshytip('showDelayed'); }; this.live ? this.live('focus.poshytip', handler) : $(document).delegate(this.selector, 'focus.poshytip', handler); break; } return this; } return this.each(function() { new $.Poshytip(this, opts); }); } // default settings $.fn.poshytip.defaults = { content: '[title]', // content to display ('[title]', 'string', element, function(updateCallback){...}, jQuery) className: 'tip-yellow', // class for the tips bgImageFrameSize: 10, // size in pixels for the background-image (if set in CSS) frame around the inner content of the tip showTimeout: 500, // timeout before showing the tip (in milliseconds 1000 == 1 second) hideTimeout: 100, // timeout before hiding the tip timeOnScreen: 0, // timeout before automatically hiding the tip after showing it (set to > 0 in order to activate) showOn: 'hover', // handler for showing the tip ('hover', 'focus', 'none') - use 'none' to trigger it manually liveEvents: false, // use live events alignTo: 'cursor', // align/position the tip relative to ('cursor', 'target') alignX: 'right', // horizontal alignment for the tip relative to the mouse cursor or the target element // ('right', 'center', 'left', 'inner-left', 'inner-right') - 'inner-*' matter if alignTo:'target' alignY: 'top', // vertical alignment for the tip relative to the mouse cursor or the target element // ('bottom', 'center', 'top', 'inner-bottom', 'inner-top') - 'inner-*' matter if alignTo:'target' offsetX: -22, // offset X pixels from the default position - doesn't matter if alignX:'center' offsetY: 18, // offset Y pixels from the default position - doesn't matter if alignY:'center' keepInViewport: true, // reposition the tooltip if needed to make sure it always appears inside the viewport allowTipHover: true, // allow hovering the tip without hiding it onmouseout of the target - matters only if showOn:'hover' followCursor: false, // if the tip should follow the cursor - matters only if showOn:'hover' and alignTo:'cursor' fade: true, // use fade animation slide: true, // use slide animation slideOffset: 8, // slide animation offset showAniDuration: 300, // show animation duration - set to 0 if you don't want show animation hideAniDuration: 300, // hide animation duration - set to 0 if you don't want hide animation refreshAniDuration: 200 // refresh animation duration - set to 0 if you don't want animation when updating the tooltip asynchronously }; })(jQuery);