eads@18: /* eads@18: * @name BeautyTips eads@18: * @desc a tooltips/baloon-help plugin for jQuery eads@18: * eads@18: * @author Jeff Robbins - Lullabot - http://www.lullabot.com eads@18: * @version 0.9.1 (2/15/2009) eads@18: * eads@18: * @type jQuery eads@18: * @cat Plugins/bt eads@18: * @requires jQuery v1.2+ (not tested on versions prior to 1.2.6) eads@18: * eads@18: * Dual licensed under the MIT and GPL licenses: eads@18: * http://www.opensource.org/licenses/mit-license.php eads@18: * http://www.gnu.org/licenses/gpl.html eads@18: * eads@18: * Encourage development. If you use BeautyTips for anything cool eads@18: * or on a site that people have heard of, please drop me a note. eads@18: * - jeff ^at lullabot > com eads@18: * eads@18: * No guarantees, warranties, or promises of any kind eads@18: * eads@18: */ eads@18: eads@18: /** eads@18: * @credit Inspired by Karl Swedberg's ClueTip eads@18: * (http://plugins.learningjquery.com/cluetip/), which in turn was inspired eads@18: * by Cody Lindley's jTip (http://www.codylindley.com) eads@18: * eads@18: * @fileoverview eads@18: * Beauty Tips is a jQuery tooltips plugin which uses the canvas drawing element eads@18: * in the HTML5 spec in order to dynamically draw tooltip "talk bubbles" around eads@18: * the descriptive help text associated with an item. This is in many ways eads@18: * similar to Google Maps which both provides similar talk-bubbles and uses the eads@18: * canvas element to draw them. eads@18: * eads@18: * The canvas element is supported in modern versions of FireFox, Safari, and eads@18: * Opera. However, Internet Explorer needs a separate library called ExplorerCanvas eads@18: * included on the page in order to support canvas drawing functions. ExplorerCanvas eads@18: * was created by Google for use with their web apps and you can find it here: eads@18: * http://excanvas.sourceforge.net/ eads@18: * eads@18: * Beauty Tips was written to be simple to use and pretty. All of its options eads@18: * are documented at the bottom of this file and defaults can be overwritten eads@18: * globally for the entire page, or individually on each call. eads@18: * eads@18: * By default each tooltip will be positioned on the side of the target element eads@18: * which has the most free space. This is affected by the scroll position and eads@18: * size of the current window, so each Beauty Tip is redrawn each time it is eads@18: * displayed. It may appear above an element at the bottom of the page, but when eads@18: * the page is scrolled down (and the element is at the top of the page) it will eads@18: * then appear below it. Additionally, positions can be forced or a preferred eads@18: * order can be defined. See examples below. eads@18: * eads@18: * To fix z-index problems in IE6, include the bgiframe plugin on your page eads@18: * http://plugins.jquery.com/project/bgiframe - BeautyTips will automatically eads@18: * recognize it and use it. eads@18: * eads@18: * BeautyTips also works with the hoverIntent plugin eads@18: * http://cherne.net/brian/resources/jquery.hoverIntent.html eads@18: * see hoverIntent example below for usage eads@18: * eads@18: * Usage eads@18: * The function can be called in a number of ways. eads@18: * $(selector).bt(); eads@18: * $(selector).bt('Content text'); eads@18: * $(selector).bt('Content text', {option1: value, option2: value}); eads@18: * $(selector).bt({option1: value, option2: value}); eads@18: * eads@18: * For more/better documentation and lots of examples, visit the demo page included with the distribution eads@18: * eads@18: */ eads@18: jQuery.fn.bt = function(content, options) { eads@18: eads@18: if (typeof content != 'string') { eads@18: var contentSelect = true; eads@18: options = content; eads@18: content = false; eads@18: } eads@18: else { eads@18: var contentSelect = false; eads@18: } eads@18: eads@18: // if hoverIntent is installed, use that as default instead of hover eads@18: if (jQuery.fn.hoverIntent && jQuery.bt.defaults.trigger == 'hover') { eads@18: jQuery.bt.defaults.trigger = 'hoverIntent'; eads@18: } eads@18: eads@18: return this.each(function(index) { eads@18: eads@18: var opts = jQuery.extend(false, jQuery.bt.defaults, options); eads@18: eads@18: // clean up the options eads@18: opts.spikeLength = numb(opts.spikeLength); eads@18: opts.spikeGirth = numb(opts.spikeGirth); eads@18: opts.overlap = numb(opts.overlap); eads@18: eads@18: var ajaxTimeout = false; eads@18: eads@18: /** eads@18: * This is sort of the "starting spot" for the this.each() eads@18: * These are sort of the init functions to handle the call eads@18: */ eads@18: eads@18: if (opts.killTitle) { eads@18: $(this).find('[title]').andSelf().each(function() { eads@18: if (!$(this).attr('bt-xTitle')) { eads@18: $(this).attr('bt-xTitle', $(this).attr('title')).attr('title', ''); eads@18: } eads@18: }); eads@18: } eads@18: eads@18: if (typeof opts.trigger == 'string') { eads@18: opts.trigger = [opts.trigger]; eads@18: } eads@18: if (opts.trigger[0] == 'hoverIntent') { eads@18: var hoverOpts = $.extend(opts.hoverIntentOpts, { eads@18: over: function() { eads@18: this.btOn(); eads@18: }, eads@18: out: function() { eads@18: this.btOff(); eads@18: }}); eads@18: $(this).hoverIntent(hoverOpts); eads@18: eads@18: } eads@18: else if (opts.trigger[0] == 'hover') { eads@18: $(this).hover( eads@18: function() { eads@18: this.btOn(); eads@18: }, eads@18: function() { eads@18: this.btOff(); eads@18: } eads@18: ); eads@18: } eads@18: else if (opts.trigger[0] == 'now') { eads@18: // toggle the on/off right now eads@18: // note that 'none' gives more control (see below) eads@18: if ($(this).hasClass('bt-active')) { eads@18: this.btOff(); eads@18: } eads@18: else { eads@18: this.btOn(); eads@18: } eads@18: } eads@18: else if (opts.trigger[0] == 'none') { eads@18: // initialize the tip with no event trigger eads@18: // use javascript to turn on/off tip as follows: eads@18: // $('#selector').btOn(); eads@18: // $('#selector').btOff(); eads@18: } eads@18: else if (opts.trigger.length > 1 && opts.trigger[0] != opts.trigger[1]) { eads@18: $(this) eads@18: .bind(opts.trigger[0], function() { eads@18: this.btOn(); eads@18: }) eads@18: .bind(opts.trigger[1], function() { eads@18: this.btOff(); eads@18: }); eads@18: } eads@18: else { eads@18: // toggle using the same event eads@18: $(this).bind(opts.trigger[0], function() { eads@18: if ($(this).hasClass('bt-active')) { eads@18: this.btOff(); eads@18: } eads@18: else { eads@18: this.btOn(); eads@18: } eads@18: }); eads@18: } eads@18: eads@18: eads@18: /** eads@18: * The BIG TURN ON eads@18: * Any element that has been initiated eads@18: */ eads@18: this.btOn = function () { eads@18: if (typeof $(this).data('bt-box') == 'object') { eads@18: // if there's already a popup, remove it before creating a new one. eads@18: this.btOff(); eads@18: } eads@18: eads@18: // trigger preShow function eads@18: opts.preShow.apply(this); eads@18: eads@18: // turn off other tips eads@18: $(jQuery.bt.vars.closeWhenOpenStack).btOff(); eads@18: eads@18: // add the class to the target element (for hilighting, for example) eads@18: // bt-active is always applied to all, but activeClass can apply another eads@18: $(this).addClass('bt-active ' + opts.activeClass); eads@18: eads@18: if (contentSelect && opts.ajaxPath == null) { eads@18: // bizarre, I know eads@18: if (opts.killTitle) { eads@18: // if we've killed the title attribute, it's been stored in 'bt-xTitle' so get it.. eads@18: $(this).attr('title', $(this).attr('bt-xTitle')); eads@18: } eads@18: // then evaluate the selector... title is now in place eads@18: content = eval(opts.contentSelector); eads@18: if (opts.killTitle) { eads@18: // now remove the title again, so we don't get double tips eads@18: $(this).attr('title', ''); eads@18: } eads@18: } eads@18: eads@18: // ---------------------------------------------- eads@18: // All the Ajax(ish) stuff is in this next bit... eads@18: // ---------------------------------------------- eads@18: if (opts.ajaxPath != null && content == false) { eads@18: if (typeof opts.ajaxPath == 'object') { eads@18: var url = eval(opts.ajaxPath[0]); eads@18: url += opts.ajaxPath[1] ? ' ' + opts.ajaxPath[1] : ''; eads@18: } eads@18: else { eads@18: var url = opts.ajaxPath; eads@18: } eads@18: var off = url.indexOf(" "); eads@18: if ( off >= 0 ) { eads@18: var selector = url.slice(off, url.length); eads@18: url = url.slice(0, off); eads@18: } eads@18: eads@18: // load any data cached for the given ajax path eads@18: var cacheData = opts.ajaxCache ? $(document.body).data('btCache-' + url.replace(/\./g, '')) : null; eads@18: if (typeof cacheData == 'string') { eads@18: content = selector ? jQuery("
").append(cacheData.replace(//g, "")).find(selector) : cacheData; eads@18: } eads@18: else { eads@18: var target = this; eads@18: eads@18: // set up the options eads@18: var ajaxOpts = jQuery.extend(false, eads@18: { eads@18: type: opts.ajaxType, eads@18: data: opts.ajaxData, eads@18: cache: opts.ajaxCache, eads@18: url: url, eads@18: complete: function(XMLHttpRequest, textStatus) { eads@18: if (textStatus == 'success' || textStatus == 'notmodified') { eads@18: if (opts.ajaxCache) { eads@18: $(document.body).data('btCache-' + url.replace(/\./g, ''), XMLHttpRequest.responseText); eads@18: } eads@18: ajaxTimeout = false; eads@18: content = selector ? eads@18: // Create a dummy div to hold the results eads@18: jQuery("
") eads@18: // inject the contents of the document in, removing the scripts eads@18: // to avoid any 'Permission Denied' errors in IE eads@18: .append(XMLHttpRequest.responseText.replace(//g, "")) eads@18: eads@18: // Locate the specified elements eads@18: .find(selector) : eads@18: eads@18: // If not, just inject the full result eads@18: XMLHttpRequest.responseText; eads@18: eads@18: } eads@18: else { eads@18: if (textStatus == 'timeout') { eads@18: // if there was a timeout, we don't cache the result eads@18: ajaxTimeout = true; eads@18: } eads@18: content = opts.ajaxError.replace(/%error/g, XMLHttpRequest.statusText); eads@18: } eads@18: // if the user rolls out of the target element before the ajax request comes back, don't show it eads@18: if ($(target).hasClass('bt-active')) { eads@18: target.btOn(); eads@18: } eads@18: } eads@18: }, opts.ajaxData); eads@18: // do the ajax request eads@18: $.ajax(ajaxOpts); eads@18: // load the throbber while the magic happens eads@18: content = opts.ajaxLoading; eads@18: } eads@18: } eads@18: // eads@18: eads@18: eads@18: // now we start actually figuring out where to place the tip eads@18: eads@18: var offsetParent = $(this).offsetParent(); eads@18: var pos = $(this).btPosition(); eads@18: // top, left, width, and height values of the target element eads@18: var top = numb(pos.top) + numb($(this).css('margin-top')); // IE can return 'auto' for margins eads@18: var left = numb(pos.left) + numb($(this).css('margin-left')); eads@18: var width = $(this).outerWidth(); eads@18: var height = $(this).outerHeight(); eads@18: eads@18: if (typeof content == 'object') { eads@18: // if content is a DOM object (as opposed to text) eads@18: // use a clone, rather than removing the original element eads@18: // and ensure that it's visible eads@18: content = $(content).clone(true).show(); eads@18: eads@18: } eads@18: eads@18: // create the tip content div, populate it, and style it eads@18: var $text = $('
').append(content).css({padding: opts.padding, position: 'absolute', width: opts.width, zIndex: opts.textzIndex}).css(opts.cssStyles); eads@18: // create the wrapping box which contains text and canvas eads@18: // put the content in it, style it, and append it to the same offset parent as the target eads@18: var $box = $('
').append($text).addClass(opts.cssClass).css({position: 'absolute', width: opts.width, zIndex: opts.wrapperzIndex}).appendTo(offsetParent); eads@18: eads@18: // use bgiframe to get around z-index problems in IE6 eads@18: // http://plugins.jquery.com/project/bgiframe eads@18: if ($.fn.bgiframe) { eads@18: $text.bgiframe(); eads@18: $box.bgiframe(); eads@18: } eads@18: eads@18: $(this).data('bt-box', $box); eads@18: eads@18: // see if the text box will fit in the various positions eads@18: var scrollTop = numb($(document).scrollTop()); eads@18: var scrollLeft = numb($(document).scrollLeft()); eads@18: var docWidth = numb($(window).width()); eads@18: var docHeight = numb($(window).height()); eads@18: var winRight = scrollLeft + docWidth; eads@18: var winBottom = scrollTop + docHeight; eads@18: var space = new Object(); eads@18: space.top = $(this).offset().top - scrollTop; eads@18: space.bottom = docHeight - (($(this).offset().top + height) - scrollTop); eads@18: space.left = $(this).offset().left - scrollLeft; eads@18: space.right = docWidth - (($(this).offset().left + width) - scrollLeft); eads@18: var textOutHeight = numb($text.outerHeight()); eads@18: var textOutWidth = numb($text.outerWidth()); eads@18: if (opts.positions.constructor == String) { eads@18: opts.positions = opts.positions.replace(/ /, '').split(','); eads@18: } eads@18: if (opts.positions[0] == 'most') { eads@18: // figure out which is the largest eads@18: var position = 'top'; // prime the pump eads@18: for (var pig in space) { // pigs in space! eads@18: position = space[pig] > space[position] ? pig : position; eads@18: } eads@18: } eads@18: else { eads@18: for (var x in opts.positions) { eads@18: var position = opts.positions[x]; eads@18: if ((position == 'left' || position == 'right') && space[position] > textOutWidth + opts.spikeLength) { eads@18: break; eads@18: } eads@18: else if ((position == 'top' || position == 'bottom') && space[position] > textOutHeight + opts.spikeLength) { eads@18: break; eads@18: } eads@18: } eads@18: } eads@18: eads@18: // horizontal (left) offset for the box eads@18: var horiz = left + ((width - textOutWidth) * .5); eads@18: // vertical (top) offset for the box eads@18: var vert = top + ((height - textOutHeight) * .5); eads@18: var animDist = opts.animate ? numb(opts.distance) : 0; eads@18: var points = new Array(); eads@18: var textTop, textLeft, textRight, textBottom, textTopSpace, textBottomSpace, textLeftSpace, textRightSpace, crossPoint, textCenter, spikePoint; eads@18: eads@18: // Yes, yes, this next bit really could use to be condensed eads@18: // each switch case is basically doing the same thing in slightly different ways eads@18: switch(position) { eads@18: case 'top': eads@18: // spike on bottom eads@18: $text.css('margin-bottom', opts.spikeLength + 'px'); eads@18: $box.css({top: (top - $text.outerHeight(true) - animDist) + opts.overlap, left: horiz}); eads@18: // move text left/right if extends out of window eads@18: textRightSpace = (winRight - opts.windowMargin) - ($text.offset().left + $text.outerWidth(true)); eads@18: var xShift = 0; eads@18: if (textRightSpace < 0) { eads@18: // shift it left eads@18: $box.css('left', (numb($box.css('left')) + textRightSpace) + 'px'); eads@18: xShift -= textRightSpace; eads@18: } eads@18: // we test left space second to ensure that left of box is visible eads@18: textLeftSpace = ($text.offset().left + numb($text.css('margin-left'))) - (scrollLeft + opts.windowMargin); eads@18: if (textLeftSpace < 0) { eads@18: // shift it right eads@18: $box.css('left', (numb($box.css('left')) - textLeftSpace) + 'px'); eads@18: xShift += textLeftSpace; eads@18: } eads@18: textTop = $text.btPosition().top + numb($text.css('margin-top')); eads@18: textLeft = $text.btPosition().left + numb($text.css('margin-left')); eads@18: textRight = textLeft + $text.outerWidth(); eads@18: textBottom = textTop + $text.outerHeight(); eads@18: textCenter = {x: textLeft + ($text.outerWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)}; eads@18: // points[points.length] = {x: x, y: y}; eads@18: points[points.length] = spikePoint = {y: textBottom + opts.spikeLength, x: ((textRight-textLeft) * .5) + xShift, type: 'spike'}; eads@18: crossPoint = findIntersectX(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textBottom); eads@18: // make sure that the crossPoint is not outside of text box boundaries eads@18: crossPoint.x = crossPoint.x < textLeft + opts.spikeGirth/2 + opts.cornerRadius ? textLeft + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.x; eads@18: crossPoint.x = crossPoint.x > (textRight - opts.spikeGirth/2) - opts.cornerRadius ? (textRight - opts.spikeGirth/2) - opts.CornerRadius : crossPoint.x; eads@18: points[points.length] = {x: crossPoint.x - (opts.spikeGirth/2), y: textBottom, type: 'join'}; eads@18: points[points.length] = {x: textLeft, y: textBottom, type: 'corner'}; // left bottom corner eads@18: points[points.length] = {x: textLeft, y: textTop, type: 'corner'}; // left top corner eads@18: points[points.length] = {x: textRight, y: textTop, type: 'corner'}; // right top corner eads@18: points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner eads@18: points[points.length] = {x: crossPoint.x + (opts.spikeGirth/2), y: textBottom, type: 'join'}; eads@18: points[points.length] = spikePoint; eads@18: break; eads@18: case 'left': eads@18: // spike on right eads@18: $text.css('margin-right', opts.spikeLength + 'px'); eads@18: $box.css({top: vert + 'px', left: ((left - $text.outerWidth(true) - animDist) + opts.overlap) + 'px'}); eads@18: // move text up/down if extends out of window eads@18: textBottomSpace = (winBottom - opts.windowMargin) - ($text.offset().top + $text.outerHeight(true)); eads@18: var yShift = 0; eads@18: if (textBottomSpace < 0) { eads@18: // shift it up eads@18: $box.css('top', (numb($box.css('top')) + textBottomSpace) + 'px'); eads@18: yShift -= textBottomSpace; eads@18: } eads@18: // we ensure top space second to ensure that top of box is visible eads@18: textTopSpace = ($text.offset().top + numb($text.css('margin-top'))) - (scrollTop + opts.windowMargin); eads@18: if (textTopSpace < 0) { eads@18: // shift it down eads@18: $box.css('top', (numb($box.css('top')) - textTopSpace) + 'px'); eads@18: yShift += textTopSpace; eads@18: } eads@18: textTop = $text.btPosition().top + numb($text.css('margin-top')); eads@18: textLeft = $text.btPosition().left + numb($text.css('margin-left')); eads@18: textRight = textLeft + $text.outerWidth(); eads@18: textBottom = textTop + $text.outerHeight(); eads@18: textCenter = {x: textLeft + ($text.outerWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)}; eads@18: points[points.length] = spikePoint = {x: textRight + opts.spikeLength, y: ((textBottom-textTop) * .5) + yShift, type: 'spike'}; eads@18: crossPoint = findIntersectY(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textRight); eads@18: // make sure that the crossPoint is not outside of text box boundaries eads@18: crossPoint.y = crossPoint.y < textTop + opts.spikeGirth/2 + opts.cornerRadius ? textTop + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.y; eads@18: crossPoint.y = crossPoint.y > (textBottom - opts.spikeGirth/2) - opts.cornerRadius ? (textBottom - opts.spikeGirth/2) - opts.cornerRadius : crossPoint.y; eads@18: points[points.length] = {x: textRight, y: crossPoint.y + opts.spikeGirth/2, type: 'join'}; eads@18: points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner eads@18: points[points.length] = {x: textLeft, y: textBottom, type: 'corner'}; // left bottom corner eads@18: points[points.length] = {x: textLeft, y: textTop, type: 'corner'}; // left top corner eads@18: points[points.length] = {x: textRight, y: textTop, type: 'corner'}; // right top corner eads@18: points[points.length] = {x: textRight, y: crossPoint.y - opts.spikeGirth/2, type: 'join'}; eads@18: points[points.length] = spikePoint; eads@18: break; eads@18: case 'bottom': eads@18: // spike on top eads@18: $text.css('margin-top', opts.spikeLength + 'px'); eads@18: $box.css({top: (top + height + animDist) - opts.overlap, left: horiz}); eads@18: // move text up/down if extends out of window eads@18: textRightSpace = (winRight - opts.windowMargin) - ($text.offset().left + $text.outerWidth(true)); eads@18: var xShift = 0; eads@18: if (textRightSpace < 0) { eads@18: // shift it left eads@18: $box.css('left', (numb($box.css('left')) + textRightSpace) + 'px'); eads@18: xShift -= textRightSpace; eads@18: } eads@18: // we ensure left space second to ensure that left of box is visible eads@18: textLeftSpace = ($text.offset().left + numb($text.css('margin-left'))) - (scrollLeft + opts.windowMargin); eads@18: if (textLeftSpace < 0) { eads@18: // shift it right eads@18: $box.css('left', (numb($box.css('left')) - textLeftSpace) + 'px'); eads@18: xShift += textLeftSpace; eads@18: } eads@18: textTop = $text.btPosition().top + numb($text.css('margin-top')); eads@18: textLeft = $text.btPosition().left + numb($text.css('margin-left')); eads@18: textRight = textLeft + $text.outerWidth(); eads@18: textBottom = textTop + $text.outerHeight(); eads@18: textCenter = {x: textLeft + ($text.outerWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)}; eads@18: points[points.length] = spikePoint = {x: ((textRight-textLeft) * .5) + xShift, y: 0, type: 'spike'}; eads@18: crossPoint = findIntersectX(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textTop); eads@18: // make sure that the crossPoint is not outside of text box boundaries eads@18: crossPoint.x = crossPoint.x < textLeft + opts.spikeGirth/2 + opts.cornerRadius ? textLeft + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.x; eads@18: crossPoint.x = crossPoint.x > (textRight - opts.spikeGirth/2) - opts.cornerRadius ? (textRight - opts.spikeGirth/2) - opts.cornerRadius : crossPoint.x; eads@18: points[points.length] = {x: crossPoint.x + opts.spikeGirth/2, y: textTop, type: 'join'}; eads@18: points[points.length] = {x: textRight, y: textTop, type: 'corner'}; // right top corner eads@18: points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner eads@18: points[points.length] = {x: textLeft, y: textBottom, type: 'corner'}; // left bottom corner eads@18: points[points.length] = {x: textLeft, y: textTop, type: 'corner'}; // left top corner eads@18: points[points.length] = {x: crossPoint.x - (opts.spikeGirth/2), y: textTop, type: 'join'}; eads@18: points[points.length] = spikePoint; eads@18: break; eads@18: case 'right': eads@18: // spike on left eads@18: $text.css('margin-left', (opts.spikeLength + 'px')); eads@18: $box.css({top: vert + 'px', left: ((left + width + animDist) - opts.overlap) + 'px'}); eads@18: // move text up/down if extends out of window eads@18: textBottomSpace = (winBottom - opts.windowMargin) - ($text.offset().top + $text.outerHeight(true)); eads@18: var yShift = 0; eads@18: if (textBottomSpace < 0) { eads@18: // shift it up eads@18: $box.css('top', (numb($box.css('top')) + textBottomSpace) + 'px'); eads@18: yShift -= textBottomSpace; eads@18: } eads@18: // we ensure top space second to ensure that top of box is visible eads@18: textTopSpace = ($text.offset().top + numb($text.css('margin-top'))) - (scrollTop + opts.windowMargin); eads@18: if (textTopSpace < 0) { eads@18: // shift it down eads@18: $box.css('top', (numb($box.css('top')) - textTopSpace) + 'px'); eads@18: yShift += textTopSpace; eads@18: } eads@18: textTop = $text.btPosition().top + numb($text.css('margin-top')); eads@18: textLeft = $text.btPosition().left + numb($text.css('margin-left')); eads@18: textRight = textLeft + $text.outerWidth(); eads@18: textBottom = textTop + $text.outerHeight(); eads@18: textCenter = {x: textLeft + ($text.outerWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)}; eads@18: points[points.length] = spikePoint = {x: 0, y: ((textBottom-textTop) * .5) + yShift, type: 'spike'}; eads@18: crossPoint = findIntersectY(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textLeft); eads@18: // make sure that the crossPoint is not outside of text box boundaries eads@18: crossPoint.y = crossPoint.y < textTop + opts.spikeGirth/2 + opts.cornerRadius ? textTop + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.y; eads@18: crossPoint.y = crossPoint.y > (textBottom - opts.spikeGirth/2) - opts.cornerRadius ? (textBottom - opts.spikeGirth/2) - opts.cornerRadius : crossPoint.y; eads@18: points[points.length] = {x: textLeft, y: crossPoint.y - opts.spikeGirth/2, type: 'join'}; eads@18: points[points.length] = {x: textLeft, y: textTop, type: 'corner'}; // left top corner eads@18: points[points.length] = {x: textRight, y: textTop, type: 'corner'}; // right top corner eads@18: points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner eads@18: points[points.length] = {x: textLeft, y: textBottom, type: 'corner'}; // left bottom corner eads@18: points[points.length] = {x: textLeft, y: crossPoint.y + opts.spikeGirth/2, type: 'join'}; eads@18: points[points.length] = spikePoint; eads@18: break; eads@18: } // eads@18: eads@18: var canvas = $('').appendTo($box).css({position: 'absolute', top: $text.btPosition().top, left: $text.btPosition().left, zIndex: opts.boxzIndex}).get(0); eads@18: eads@18: // if excanvas is set up, we need to initialize the new canvas element eads@18: if (typeof G_vmlCanvasManager != 'undefined') { eads@18: canvas = G_vmlCanvasManager.initElement(canvas); eads@18: } eads@18: eads@18: if (opts.cornerRadius > 0) { eads@18: // round the corners! eads@18: var newPoints = new Array(); eads@18: var newPoint; eads@18: for (var i=0; i 0) { eads@18: ctx.shadowColor = 'rgba(0, 0, 0, 0)'; eads@18: ctx.lineWidth = opts.strokeWidth; eads@18: ctx.strokeStyle = opts.strokeStyle; eads@18: ctx.beginPath(); eads@18: drawIt.apply(ctx, [points], opts.strokeWidth); eads@18: ctx.closePath(); eads@18: ctx.stroke(); eads@18: } eads@18: eads@18: if (opts.animate) { eads@18: $box.css({opacity: 0.1}); eads@18: } eads@18: eads@18: $box.css({visibility: 'visible'}); eads@18: eads@18: if (opts.overlay) { eads@18: // EXPERIMENTAL!!!! eads@18: var overlay = $('
').css({ eads@18: position: 'absolute', eads@18: backgroundColor: 'blue', eads@18: top: top, eads@18: left: left, eads@18: width: width, eads@18: height: height, eads@18: opacity: '.2' eads@18: }).appendTo(offsetParent); eads@18: $(this).data('overlay', overlay); eads@18: } eads@18: eads@18: var animParams = {opacity: 1}; eads@18: if (opts.animate) { eads@18: switch (position) { eads@18: case 'top': eads@18: animParams.top = $box.btPosition().top + opts.distance; eads@18: break; eads@18: case 'left': eads@18: animParams.left = $box.btPosition().left + opts.distance; eads@18: break; eads@18: case 'bottom': eads@18: animParams.top = $box.btPosition().top - opts.distance; eads@18: break; eads@18: case 'right': eads@18: animParams.left = $box.btPosition().left - opts.distance; eads@18: break; eads@18: } eads@18: $box.animate(animParams, {duration: opts.speed, easing: opts.easing}); eads@18: } eads@18: eads@18: if ((opts.ajaxPath != null && opts.ajaxCache == false) || ajaxTimeout) { eads@18: // if ajaxCache is not enabled or if there was a server timeout, eads@18: // remove the content variable so it will be loaded again from server eads@18: content = false; eads@18: } eads@18: eads@18: // stick this element into the clickAnywhereToClose stack eads@18: if (opts.clickAnywhereToClose) { eads@18: jQuery.bt.vars.clickAnywhereStack.push(this); eads@18: $(document).click(jQuery.bt.docClick); eads@18: } eads@18: eads@18: // stick this element into the closeWhenOthersOpen stack eads@18: if (opts.closeWhenOthersOpen) { eads@18: jQuery.bt.vars.closeWhenOpenStack.push(this); eads@18: } eads@18: eads@18: // trigger postShow function eads@18: opts.postShow.apply(this); eads@18: eads@18: eads@18: }; // eads@18: eads@18: this.btOff = function() { eads@18: eads@18: // trigger preHide function eads@18: opts.preHide.apply(this); eads@18: eads@18: var box = $(this).data('bt-box'); eads@18: var overlay = $(this).data('bt-overlay'); eads@18: if (typeof box == 'object') { eads@18: $(box).remove(); eads@18: $(this).removeData('bt-box'); eads@18: } eads@18: if (typeof overlay == 'object') { eads@18: $(overlay).remove(); eads@18: $(this).removeData('bt-overlay'); eads@18: } eads@18: eads@18: // remove this from the stacks eads@18: jQuery.bt.vars.clickAnywhereStack = arrayRemove(jQuery.bt.vars.clickAnywhereStack, this); eads@18: jQuery.bt.vars.closeWhenOpenStack = arrayRemove(jQuery.bt.vars.closeWhenOpenStack, this); eads@18: eads@18: // trigger postHide function eads@18: opts.postHide.apply(this); eads@18: eads@18: // remove the 'bt-active' and activeClass classes from target eads@18: $(this).removeClass('bt-active ' + opts.activeClass); eads@18: eads@18: }; // eads@18: eads@18: var refresh = this.btRefresh = function() { eads@18: this.btOff(); eads@18: this.btOn(); eads@18: }; eads@18: eads@18: eads@18: }); // eads@18: eads@18: eads@18: function drawIt(points, strokeWidth) { eads@18: this.moveTo(points[0].x, points[0].y); eads@18: for (i=1;i eads@18: eads@18: /** eads@18: * For odd stroke widths, round to the nearest .5 pixel to avoid antialiasing eads@18: * http://developer.mozilla.org/en/Canvas_tutorial/Applying_styles_and_colors eads@18: */ eads@18: function round5(num, strokeWidth) { eads@18: var ret; eads@18: strokeWidth = numb(strokeWidth); eads@18: if (strokeWidth%2) { eads@18: ret = num; eads@18: } eads@18: else { eads@18: ret = Math.round(num - .5) + .5; eads@18: } eads@18: return ret; eads@18: }; // eads@18: eads@18: /** eads@18: * Ensure that a number is a number... or zero eads@18: */ eads@18: function numb(num) { eads@18: return parseInt(num) || 0; eads@18: }; // eads@18: eads@18: /** eads@18: * Remove an element from an array eads@18: */ eads@18: function arrayRemove(arr, elem) { eads@18: var x, newArr = new Array(); eads@18: for (x in arr) { eads@18: if (arr[x] != elem) { eads@18: newArr.push(arr[x]); eads@18: } eads@18: } eads@18: return newArr; eads@18: }; // eads@18: eads@18: /** eads@18: * Given two points, find a point which is dist pixels from point1 on a line to point2 eads@18: */ eads@18: function betweenPoint(point1, point2, dist) { eads@18: // figure out if we're horizontal or vertical eads@18: var y, x; eads@18: if (point1.x == point2.x) { eads@18: // vertical eads@18: y = point1.y < point2.y ? point1.y + dist : point1.y - dist; eads@18: return {x: point1.x, y: y}; eads@18: } eads@18: else if (point1.y == point2.y) { eads@18: // horizontal eads@18: x = point1.x < point2.x ? point1.x + dist : point1.x - dist; eads@18: return {x:x, y: point1.y}; eads@18: } eads@18: }; // eads@18: eads@18: function centerPoint(arcStart, corner, arcEnd) { eads@18: var x = corner.x == arcStart.x ? arcEnd.x : arcStart.x; eads@18: var y = corner.y == arcStart.y ? arcEnd.y : arcStart.y; eads@18: var startAngle, endAngle; eads@18: if (arcStart.x < arcEnd.x) { eads@18: if (arcStart.y > arcEnd.y) { eads@18: // arc is on upper left eads@18: startAngle = (Math.PI/180)*180; eads@18: endAngle = (Math.PI/180)*90; eads@18: } eads@18: else { eads@18: // arc is on upper right eads@18: startAngle = (Math.PI/180)*90; eads@18: endAngle = 0; eads@18: } eads@18: } eads@18: else { eads@18: if (arcStart.y > arcEnd.y) { eads@18: // arc is on lower left eads@18: startAngle = (Math.PI/180)*270; eads@18: endAngle = (Math.PI/180)*180; eads@18: } eads@18: else { eads@18: // arc is on lower right eads@18: startAngle = 0; eads@18: endAngle = (Math.PI/180)*270; eads@18: } eads@18: } eads@18: return {x: x, y: y, type: 'center', startAngle: startAngle, endAngle: endAngle}; eads@18: }; // eads@18: eads@18: /** eads@18: * Find the intersection point of two lines, each defined by two points eads@18: * arguments are x1, y1 and x2, y2 for r1 (line 1) and r2 (line 2) eads@18: * It's like an algebra party!!! eads@18: */ eads@18: function findIntersect(r1x1, r1y1, r1x2, r1y2, r2x1, r2y1, r2x2, r2y2) { eads@18: eads@18: if (r2x1 == r2x2) { eads@18: return findIntersectY(r1x1, r1y1, r1x2, r1y2, r2x1); eads@18: } eads@18: if (r2y1 == r2y2) { eads@18: return findIntersectX(r1x1, r1y1, r1x2, r1y2, r2y1); eads@18: } eads@18: eads@18: // m = (y1 - y2) / (x1 - x2) // <-- how to find the slope eads@18: // y = mx + b // the 'classic' linear equation eads@18: // b = y - mx // how to find b (the y-intersect) eads@18: // x = (y - b)/m // how to find x eads@18: var r1m = (r1y1 - r1y2) / (r1x1 - r1x2); eads@18: var r1b = r1y1 - (r1m * r1x1); eads@18: var r2m = (r2y1 - r2y2) / (r2x1 - r2x2); eads@18: var r2b = r2y1 - (r2m * r2x1); eads@18: eads@18: var x = (r2b - r1b) / (r1m - r2m); eads@18: var y = r1m * x + r1b; eads@18: eads@18: return {x: x, y: y}; eads@18: }; // eads@18: eads@18: /** eads@18: * Find the y intersection point of a line and given x vertical eads@18: */ eads@18: function findIntersectY(r1x1, r1y1, r1x2, r1y2, x) { eads@18: if (r1y1 == r1y2) { eads@18: return {x: x, y: r1y1}; eads@18: } eads@18: var r1m = (r1y1 - r1y2) / (r1x1 - r1x2); eads@18: var r1b = r1y1 - (r1m * r1x1); eads@18: eads@18: var y = r1m * x + r1b; eads@18: eads@18: return {x: x, y: y}; eads@18: }; // eads@18: eads@18: /** eads@18: * Find the x intersection point of a line and given y horizontal eads@18: */ eads@18: function findIntersectX(r1x1, r1y1, r1x2, r1y2, y) { eads@18: if (r1x1 == r1x2) { eads@18: return {x: r1x1, y: y}; eads@18: } eads@18: var r1m = (r1y1 - r1y2) / (r1x1 - r1x2); eads@18: var r1b = r1y1 - (r1m * r1x1); eads@18: eads@18: // y = mx + b // your old friend, linear equation eads@18: // x = (y - b)/m // linear equation solved for x eads@18: var x = (y - r1b) / r1m; eads@18: eads@18: return {x: x, y: y}; eads@18: eads@18: }; // eads@18: eads@18: }; // eads@18: eads@18: /** eads@18: * jQuery's compat.js (used in Drupal's jQuery upgrade module, overrides the $().position() function eads@18: * this is a copy of that function to allow the plugin to work when compat.js is present eads@18: * once compat.js is fixed to not override existing functions, this function can be removed eads@18: * and .btPosion() can be replaced with .position() above... eads@18: */ eads@18: jQuery.fn.btPosition = function() { eads@18: eads@18: function num(elem, prop) { eads@18: return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0; eads@18: }; eads@18: eads@18: var left = 0, top = 0, results; eads@18: eads@18: if ( this[0] ) { eads@18: // Get *real* offsetParent eads@18: var offsetParent = this.offsetParent(), eads@18: eads@18: // Get correct offsets eads@18: offset = this.offset(), eads@18: parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset(); eads@18: eads@18: // Subtract element margins eads@18: // note: when an element has margin: auto the offsetLeft and marginLeft eads@18: // are the same in Safari causing offset.left to incorrectly be 0 eads@18: offset.top -= num( this, 'marginTop' ); eads@18: offset.left -= num( this, 'marginLeft' ); eads@18: eads@18: // Add offsetParent borders eads@18: parentOffset.top += num( offsetParent, 'borderTopWidth' ); eads@18: parentOffset.left += num( offsetParent, 'borderLeftWidth' ); eads@18: eads@18: // Subtract the two offsets eads@18: results = { eads@18: top: offset.top - parentOffset.top, eads@18: left: offset.left - parentOffset.left eads@18: }; eads@18: } eads@18: eads@18: return results; eads@18: }; // eads@18: eads@18: eads@18: /** eads@18: * A convenience function to run btOn() (if available) eads@18: * for each selected item eads@18: */ eads@18: jQuery.fn.btOn = function() { eads@18: return this.each(function(index){ eads@18: if ($.isFunction(this.btOn)) { eads@18: this.btOn(); eads@18: } eads@18: }); eads@18: }; // eads@18: eads@18: /** eads@18: * eads@18: * A convenience function to run btOff() (if available) eads@18: * for each selected item eads@18: */ eads@18: jQuery.fn.btOff = function() { eads@18: return this.each(function(index){ eads@18: if ($.isFunction(this.btOff)) { eads@18: this.btOff(); eads@18: } eads@18: }); eads@18: }; // eads@18: eads@18: jQuery.bt = {}; eads@18: jQuery.bt.vars = {clickAnywhereStack: [], closeWhenOpenStack: []}; eads@18: eads@18: /** eads@18: * This function gets bound to the document's click event eads@18: * It turns off all of the tips in the click-anywhere-to-close stack eads@18: */ eads@18: jQuery.bt.docClick = function(e) { eads@18: if (!e) { eads@18: var e = window.event; eads@18: }; eads@18: if (!$(e.target).parents().andSelf().filter('.bt-wrapper, .bt-active').length) { eads@18: // if clicked element isn't inside tip, close tips in stack eads@18: $(jQuery.bt.vars.clickAnywhereStack).btOff(); eads@18: $(document).unbind('click', jQuery.bt.docClick); eads@18: } eads@18: }; // eads@18: eads@18: /** eads@18: * Defaults for the beauty tips eads@18: * eads@18: * Note this is a variable definition and not a function. So defaults can be eads@18: * written for an entire page by simply redefining attributes like so: eads@18: * eads@18: * jQuery.bt.defaults.width = 400; eads@18: * eads@18: * This would make all Beauty Tips boxes 400px wide. eads@18: * eads@18: * Each of these options may also be overridden during eads@18: * eads@18: * Can be overriden globally or at time of call. eads@18: * eads@18: */ eads@18: jQuery.bt.defaults = { eads@18: trigger: 'hover', // trigger to show/hide tip eads@18: // use [on, off] to define separate on/off triggers eads@18: // also use space character to allow multiple to trigger eads@18: // examples: eads@18: // ['focus', 'blur'] // focus displays, blur hides eads@18: // 'dblclick' // dblclick toggles on/off eads@18: // ['focus mouseover', 'blur mouseout'] // multiple triggers eads@18: // 'now' // shows/hides tip without event eads@18: // 'none' // use $('#selector').btOn(); and ...btOff(); eads@18: // 'hoverIntent' // hover using hoverIntent plugin (settings below) eads@18: // note: eads@18: // hoverIntent becomes default if available eads@18: eads@18: clickAnywhereToClose: true, // clicking anywhere outside of the tip will close it eads@18: closeWhenOthersOpen: false, // tip will be closed before another opens - stop >= 2 tips being on eads@18: eads@18: width: '200px', // width of tooltip box eads@18: // when combined with cssStyles: {width: 'auto'}, this becomes a max-width for the text eads@18: padding: '10px', // padding for content (get more fine grained with cssStyles) eads@18: spikeGirth: 10, // width of spike eads@18: spikeLength: 15, // length of spike eads@18: overlap: 0, // spike overlap (px) onto target (can cause problems with 'hover' trigger) eads@18: overlay: false, // display overlay on target (use CSS to style) -- BUGGY! eads@18: killTitle: true, // kill title tags to avoid double tooltips eads@18: eads@18: textzIndex: 9999, // z-index for the text eads@18: boxzIndex: 9998, // z-index for the "talk" box (should always be less than textzIndex) eads@18: wrapperzIndex: 9997, eads@18: positions: ['most'], // preference of positions for tip (will use first with available space) eads@18: // possible values 'top', 'bottom', 'left', 'right' as an array in order of eads@18: // preference. Last value will be used if others don't have enough space. eads@18: // or use 'most' to use the area with the most space eads@18: fill: "rgb(255, 255, 102)", // fill color for the tooltip box eads@18: eads@18: windowMargin: 10, // space (px) to leave between text box and browser edge eads@18: eads@18: strokeWidth: 1, // width of stroke around box, **set to 0 for no stroke** eads@18: strokeStyle: "#000", // color/alpha of stroke eads@18: eads@18: cornerRadius: 5, // radius of corners (px), set to 0 for square corners eads@18: eads@18: // following values are on a scale of 0 to 1 with .5 being centered eads@18: eads@18: centerPointX: .5, // the spike extends from center of the target edge to this point eads@18: centerPointY: .5, // defined by percentage horizontal (x) and vertical (y) eads@18: eads@18: shadow: false, // use drop shadow? (only displays in Safari and FF 3.1) - experimental eads@18: shadowOffsetX: 2, // shadow offset x (px) eads@18: shadowOffsetY: 2, // shadow offset y (px) eads@18: shadowBlur: 3, // shadow blur (px) eads@18: shadowColor: "#000", // shadow color/alpha eads@18: eads@18: animate: false, // animate show/hide of box - EXPERIMENTAL (buggy in IE) eads@18: distance: 15, // distance of animation movement (px) eads@18: easing: 'swing', // animation easing eads@18: speed: 200, // speed (ms) of animation eads@18: eads@18: cssClass: '', // CSS class to add to the box wrapper div (of the TIP) eads@18: cssStyles: {}, // styles to add the text box eads@18: // example: {fontFamily: 'Georgia, Times, serif', fontWeight: 'bold'} eads@18: eads@18: activeClass: 'bt-active', // class added to TARGET element when its BeautyTip is active eads@18: eads@18: contentSelector: "$(this).attr('title')", // if there is no content argument, use this selector to retrieve the title eads@18: eads@18: ajaxPath: null, // if using ajax request for content, this contains url and (opt) selector eads@18: // this will override content and contentSelector eads@18: // examples (see jQuery load() function): eads@18: // '/demo.html' eads@18: // '/help/ajax/snip' eads@18: // '/help/existing/full div#content' eads@18: eads@18: // ajaxPath can also be defined as an array eads@18: // in which case, the first value will be parsed as a jQuery selector eads@18: // the result of which will be used as the ajaxPath eads@18: // the second (optional) value is the content selector as above eads@18: // examples: eads@18: // ["$(this).attr('href')", 'div#content'] eads@18: // ["$(this).parents('.wrapper').find('.title').attr('href')"] eads@18: // ["$('#some-element').val()"] eads@18: eads@18: ajaxError: 'ERROR: %error', eads@18: // error text, use "%error" to insert error from server eads@18: ajaxLoading: 'Loading...', // yes folks, it's the blink tag! eads@18: ajaxData: {}, // key/value pairs eads@18: ajaxType: 'GET', // 'GET' or 'POST' eads@18: ajaxCache: true, // cache ajax results and do not send request to same url multiple times eads@18: ajaxOpts: {}, // any other ajax options - timeout, passwords, processing functions, etc... eads@18: // see http://docs.jquery.com/Ajax/jQuery.ajax#options eads@18: eads@18: preShow: function(){return;}, // function to run before popup is built and displayed eads@18: postShow: function(){return;}, // function to run after popup is built and displayed eads@18: preHide: function(){return;}, // function to run before popup is removed eads@18: postHide: function(){return;}, // function to run after popup is removed eads@18: eads@18: hoverIntentOpts: { // options for hoverIntent (if installed) eads@18: interval: 300, // http://cherne.net/brian/resources/jquery.hoverIntent.html eads@18: timeout: 500 eads@18: } eads@18: eads@18: }; // eads@18: eads@18: eads@18: // @todo eads@18: // use larger canvas (extend to edge of page when windowMargin is active) eads@18: // add options to shift position of tip vert/horiz and position of spike tip eads@18: // create drawn (canvas) shadows eads@18: // use overlay to allow overlap with hover eads@18: // experiment with making tooltip a subelement of the target eads@18: // rework animation system