Mercurial > defr > drupal > scald > dnd
comparison js/bt/jquery.bt.js @ 18:0d557e6e73f7
Added beautytips and some additional event handling code to the library.
author | David Eads <eads@chicagotech.org> |
---|---|
date | Fri, 06 Mar 2009 14:11:46 -0600 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
17:1a77f87927dd | 18:0d557e6e73f7 |
---|---|
1 /* | |
2 * @name BeautyTips | |
3 * @desc a tooltips/baloon-help plugin for jQuery | |
4 * | |
5 * @author Jeff Robbins - Lullabot - http://www.lullabot.com | |
6 * @version 0.9.1 (2/15/2009) | |
7 * | |
8 * @type jQuery | |
9 * @cat Plugins/bt | |
10 * @requires jQuery v1.2+ (not tested on versions prior to 1.2.6) | |
11 * | |
12 * Dual licensed under the MIT and GPL licenses: | |
13 * http://www.opensource.org/licenses/mit-license.php | |
14 * http://www.gnu.org/licenses/gpl.html | |
15 * | |
16 * Encourage development. If you use BeautyTips for anything cool | |
17 * or on a site that people have heard of, please drop me a note. | |
18 * - jeff ^at lullabot > com | |
19 * | |
20 * No guarantees, warranties, or promises of any kind | |
21 * | |
22 */ | |
23 | |
24 /** | |
25 * @credit Inspired by Karl Swedberg's ClueTip | |
26 * (http://plugins.learningjquery.com/cluetip/), which in turn was inspired | |
27 * by Cody Lindley's jTip (http://www.codylindley.com) | |
28 * | |
29 * @fileoverview | |
30 * Beauty Tips is a jQuery tooltips plugin which uses the canvas drawing element | |
31 * in the HTML5 spec in order to dynamically draw tooltip "talk bubbles" around | |
32 * the descriptive help text associated with an item. This is in many ways | |
33 * similar to Google Maps which both provides similar talk-bubbles and uses the | |
34 * canvas element to draw them. | |
35 * | |
36 * The canvas element is supported in modern versions of FireFox, Safari, and | |
37 * Opera. However, Internet Explorer needs a separate library called ExplorerCanvas | |
38 * included on the page in order to support canvas drawing functions. ExplorerCanvas | |
39 * was created by Google for use with their web apps and you can find it here: | |
40 * http://excanvas.sourceforge.net/ | |
41 * | |
42 * Beauty Tips was written to be simple to use and pretty. All of its options | |
43 * are documented at the bottom of this file and defaults can be overwritten | |
44 * globally for the entire page, or individually on each call. | |
45 * | |
46 * By default each tooltip will be positioned on the side of the target element | |
47 * which has the most free space. This is affected by the scroll position and | |
48 * size of the current window, so each Beauty Tip is redrawn each time it is | |
49 * displayed. It may appear above an element at the bottom of the page, but when | |
50 * the page is scrolled down (and the element is at the top of the page) it will | |
51 * then appear below it. Additionally, positions can be forced or a preferred | |
52 * order can be defined. See examples below. | |
53 * | |
54 * To fix z-index problems in IE6, include the bgiframe plugin on your page | |
55 * http://plugins.jquery.com/project/bgiframe - BeautyTips will automatically | |
56 * recognize it and use it. | |
57 * | |
58 * BeautyTips also works with the hoverIntent plugin | |
59 * http://cherne.net/brian/resources/jquery.hoverIntent.html | |
60 * see hoverIntent example below for usage | |
61 * | |
62 * Usage | |
63 * The function can be called in a number of ways. | |
64 * $(selector).bt(); | |
65 * $(selector).bt('Content text'); | |
66 * $(selector).bt('Content text', {option1: value, option2: value}); | |
67 * $(selector).bt({option1: value, option2: value}); | |
68 * | |
69 * For more/better documentation and lots of examples, visit the demo page included with the distribution | |
70 * | |
71 */ | |
72 jQuery.fn.bt = function(content, options) { | |
73 | |
74 if (typeof content != 'string') { | |
75 var contentSelect = true; | |
76 options = content; | |
77 content = false; | |
78 } | |
79 else { | |
80 var contentSelect = false; | |
81 } | |
82 | |
83 // if hoverIntent is installed, use that as default instead of hover | |
84 if (jQuery.fn.hoverIntent && jQuery.bt.defaults.trigger == 'hover') { | |
85 jQuery.bt.defaults.trigger = 'hoverIntent'; | |
86 } | |
87 | |
88 return this.each(function(index) { | |
89 | |
90 var opts = jQuery.extend(false, jQuery.bt.defaults, options); | |
91 | |
92 // clean up the options | |
93 opts.spikeLength = numb(opts.spikeLength); | |
94 opts.spikeGirth = numb(opts.spikeGirth); | |
95 opts.overlap = numb(opts.overlap); | |
96 | |
97 var ajaxTimeout = false; | |
98 | |
99 /** | |
100 * This is sort of the "starting spot" for the this.each() | |
101 * These are sort of the init functions to handle the call | |
102 */ | |
103 | |
104 if (opts.killTitle) { | |
105 $(this).find('[title]').andSelf().each(function() { | |
106 if (!$(this).attr('bt-xTitle')) { | |
107 $(this).attr('bt-xTitle', $(this).attr('title')).attr('title', ''); | |
108 } | |
109 }); | |
110 } | |
111 | |
112 if (typeof opts.trigger == 'string') { | |
113 opts.trigger = [opts.trigger]; | |
114 } | |
115 if (opts.trigger[0] == 'hoverIntent') { | |
116 var hoverOpts = $.extend(opts.hoverIntentOpts, { | |
117 over: function() { | |
118 this.btOn(); | |
119 }, | |
120 out: function() { | |
121 this.btOff(); | |
122 }}); | |
123 $(this).hoverIntent(hoverOpts); | |
124 | |
125 } | |
126 else if (opts.trigger[0] == 'hover') { | |
127 $(this).hover( | |
128 function() { | |
129 this.btOn(); | |
130 }, | |
131 function() { | |
132 this.btOff(); | |
133 } | |
134 ); | |
135 } | |
136 else if (opts.trigger[0] == 'now') { | |
137 // toggle the on/off right now | |
138 // note that 'none' gives more control (see below) | |
139 if ($(this).hasClass('bt-active')) { | |
140 this.btOff(); | |
141 } | |
142 else { | |
143 this.btOn(); | |
144 } | |
145 } | |
146 else if (opts.trigger[0] == 'none') { | |
147 // initialize the tip with no event trigger | |
148 // use javascript to turn on/off tip as follows: | |
149 // $('#selector').btOn(); | |
150 // $('#selector').btOff(); | |
151 } | |
152 else if (opts.trigger.length > 1 && opts.trigger[0] != opts.trigger[1]) { | |
153 $(this) | |
154 .bind(opts.trigger[0], function() { | |
155 this.btOn(); | |
156 }) | |
157 .bind(opts.trigger[1], function() { | |
158 this.btOff(); | |
159 }); | |
160 } | |
161 else { | |
162 // toggle using the same event | |
163 $(this).bind(opts.trigger[0], function() { | |
164 if ($(this).hasClass('bt-active')) { | |
165 this.btOff(); | |
166 } | |
167 else { | |
168 this.btOn(); | |
169 } | |
170 }); | |
171 } | |
172 | |
173 | |
174 /** | |
175 * The BIG TURN ON | |
176 * Any element that has been initiated | |
177 */ | |
178 this.btOn = function () { | |
179 if (typeof $(this).data('bt-box') == 'object') { | |
180 // if there's already a popup, remove it before creating a new one. | |
181 this.btOff(); | |
182 } | |
183 | |
184 // trigger preShow function | |
185 opts.preShow.apply(this); | |
186 | |
187 // turn off other tips | |
188 $(jQuery.bt.vars.closeWhenOpenStack).btOff(); | |
189 | |
190 // add the class to the target element (for hilighting, for example) | |
191 // bt-active is always applied to all, but activeClass can apply another | |
192 $(this).addClass('bt-active ' + opts.activeClass); | |
193 | |
194 if (contentSelect && opts.ajaxPath == null) { | |
195 // bizarre, I know | |
196 if (opts.killTitle) { | |
197 // if we've killed the title attribute, it's been stored in 'bt-xTitle' so get it.. | |
198 $(this).attr('title', $(this).attr('bt-xTitle')); | |
199 } | |
200 // then evaluate the selector... title is now in place | |
201 content = eval(opts.contentSelector); | |
202 if (opts.killTitle) { | |
203 // now remove the title again, so we don't get double tips | |
204 $(this).attr('title', ''); | |
205 } | |
206 } | |
207 | |
208 // ---------------------------------------------- | |
209 // All the Ajax(ish) stuff is in this next bit... | |
210 // ---------------------------------------------- | |
211 if (opts.ajaxPath != null && content == false) { | |
212 if (typeof opts.ajaxPath == 'object') { | |
213 var url = eval(opts.ajaxPath[0]); | |
214 url += opts.ajaxPath[1] ? ' ' + opts.ajaxPath[1] : ''; | |
215 } | |
216 else { | |
217 var url = opts.ajaxPath; | |
218 } | |
219 var off = url.indexOf(" "); | |
220 if ( off >= 0 ) { | |
221 var selector = url.slice(off, url.length); | |
222 url = url.slice(0, off); | |
223 } | |
224 | |
225 // load any data cached for the given ajax path | |
226 var cacheData = opts.ajaxCache ? $(document.body).data('btCache-' + url.replace(/\./g, '')) : null; | |
227 if (typeof cacheData == 'string') { | |
228 content = selector ? jQuery("<div/>").append(cacheData.replace(/<script(.|\s)*?\/script>/g, "")).find(selector) : cacheData; | |
229 } | |
230 else { | |
231 var target = this; | |
232 | |
233 // set up the options | |
234 var ajaxOpts = jQuery.extend(false, | |
235 { | |
236 type: opts.ajaxType, | |
237 data: opts.ajaxData, | |
238 cache: opts.ajaxCache, | |
239 url: url, | |
240 complete: function(XMLHttpRequest, textStatus) { | |
241 if (textStatus == 'success' || textStatus == 'notmodified') { | |
242 if (opts.ajaxCache) { | |
243 $(document.body).data('btCache-' + url.replace(/\./g, ''), XMLHttpRequest.responseText); | |
244 } | |
245 ajaxTimeout = false; | |
246 content = selector ? | |
247 // Create a dummy div to hold the results | |
248 jQuery("<div/>") | |
249 // inject the contents of the document in, removing the scripts | |
250 // to avoid any 'Permission Denied' errors in IE | |
251 .append(XMLHttpRequest.responseText.replace(/<script(.|\s)*?\/script>/g, "")) | |
252 | |
253 // Locate the specified elements | |
254 .find(selector) : | |
255 | |
256 // If not, just inject the full result | |
257 XMLHttpRequest.responseText; | |
258 | |
259 } | |
260 else { | |
261 if (textStatus == 'timeout') { | |
262 // if there was a timeout, we don't cache the result | |
263 ajaxTimeout = true; | |
264 } | |
265 content = opts.ajaxError.replace(/%error/g, XMLHttpRequest.statusText); | |
266 } | |
267 // if the user rolls out of the target element before the ajax request comes back, don't show it | |
268 if ($(target).hasClass('bt-active')) { | |
269 target.btOn(); | |
270 } | |
271 } | |
272 }, opts.ajaxData); | |
273 // do the ajax request | |
274 $.ajax(ajaxOpts); | |
275 // load the throbber while the magic happens | |
276 content = opts.ajaxLoading; | |
277 } | |
278 } | |
279 // </ ajax stuff > | |
280 | |
281 | |
282 // now we start actually figuring out where to place the tip | |
283 | |
284 var offsetParent = $(this).offsetParent(); | |
285 var pos = $(this).btPosition(); | |
286 // top, left, width, and height values of the target element | |
287 var top = numb(pos.top) + numb($(this).css('margin-top')); // IE can return 'auto' for margins | |
288 var left = numb(pos.left) + numb($(this).css('margin-left')); | |
289 var width = $(this).outerWidth(); | |
290 var height = $(this).outerHeight(); | |
291 | |
292 if (typeof content == 'object') { | |
293 // if content is a DOM object (as opposed to text) | |
294 // use a clone, rather than removing the original element | |
295 // and ensure that it's visible | |
296 content = $(content).clone(true).show(); | |
297 | |
298 } | |
299 | |
300 // create the tip content div, populate it, and style it | |
301 var $text = $('<div class="bt-content"></div>').append(content).css({padding: opts.padding, position: 'absolute', width: opts.width, zIndex: opts.textzIndex}).css(opts.cssStyles); | |
302 // create the wrapping box which contains text and canvas | |
303 // put the content in it, style it, and append it to the same offset parent as the target | |
304 var $box = $('<div class="bt-wrapper"></div>').append($text).addClass(opts.cssClass).css({position: 'absolute', width: opts.width, zIndex: opts.wrapperzIndex}).appendTo(offsetParent); | |
305 | |
306 // use bgiframe to get around z-index problems in IE6 | |
307 // http://plugins.jquery.com/project/bgiframe | |
308 if ($.fn.bgiframe) { | |
309 $text.bgiframe(); | |
310 $box.bgiframe(); | |
311 } | |
312 | |
313 $(this).data('bt-box', $box); | |
314 | |
315 // see if the text box will fit in the various positions | |
316 var scrollTop = numb($(document).scrollTop()); | |
317 var scrollLeft = numb($(document).scrollLeft()); | |
318 var docWidth = numb($(window).width()); | |
319 var docHeight = numb($(window).height()); | |
320 var winRight = scrollLeft + docWidth; | |
321 var winBottom = scrollTop + docHeight; | |
322 var space = new Object(); | |
323 space.top = $(this).offset().top - scrollTop; | |
324 space.bottom = docHeight - (($(this).offset().top + height) - scrollTop); | |
325 space.left = $(this).offset().left - scrollLeft; | |
326 space.right = docWidth - (($(this).offset().left + width) - scrollLeft); | |
327 var textOutHeight = numb($text.outerHeight()); | |
328 var textOutWidth = numb($text.outerWidth()); | |
329 if (opts.positions.constructor == String) { | |
330 opts.positions = opts.positions.replace(/ /, '').split(','); | |
331 } | |
332 if (opts.positions[0] == 'most') { | |
333 // figure out which is the largest | |
334 var position = 'top'; // prime the pump | |
335 for (var pig in space) { // pigs in space! | |
336 position = space[pig] > space[position] ? pig : position; | |
337 } | |
338 } | |
339 else { | |
340 for (var x in opts.positions) { | |
341 var position = opts.positions[x]; | |
342 if ((position == 'left' || position == 'right') && space[position] > textOutWidth + opts.spikeLength) { | |
343 break; | |
344 } | |
345 else if ((position == 'top' || position == 'bottom') && space[position] > textOutHeight + opts.spikeLength) { | |
346 break; | |
347 } | |
348 } | |
349 } | |
350 | |
351 // horizontal (left) offset for the box | |
352 var horiz = left + ((width - textOutWidth) * .5); | |
353 // vertical (top) offset for the box | |
354 var vert = top + ((height - textOutHeight) * .5); | |
355 var animDist = opts.animate ? numb(opts.distance) : 0; | |
356 var points = new Array(); | |
357 var textTop, textLeft, textRight, textBottom, textTopSpace, textBottomSpace, textLeftSpace, textRightSpace, crossPoint, textCenter, spikePoint; | |
358 | |
359 // Yes, yes, this next bit really could use to be condensed | |
360 // each switch case is basically doing the same thing in slightly different ways | |
361 switch(position) { | |
362 case 'top': | |
363 // spike on bottom | |
364 $text.css('margin-bottom', opts.spikeLength + 'px'); | |
365 $box.css({top: (top - $text.outerHeight(true) - animDist) + opts.overlap, left: horiz}); | |
366 // move text left/right if extends out of window | |
367 textRightSpace = (winRight - opts.windowMargin) - ($text.offset().left + $text.outerWidth(true)); | |
368 var xShift = 0; | |
369 if (textRightSpace < 0) { | |
370 // shift it left | |
371 $box.css('left', (numb($box.css('left')) + textRightSpace) + 'px'); | |
372 xShift -= textRightSpace; | |
373 } | |
374 // we test left space second to ensure that left of box is visible | |
375 textLeftSpace = ($text.offset().left + numb($text.css('margin-left'))) - (scrollLeft + opts.windowMargin); | |
376 if (textLeftSpace < 0) { | |
377 // shift it right | |
378 $box.css('left', (numb($box.css('left')) - textLeftSpace) + 'px'); | |
379 xShift += textLeftSpace; | |
380 } | |
381 textTop = $text.btPosition().top + numb($text.css('margin-top')); | |
382 textLeft = $text.btPosition().left + numb($text.css('margin-left')); | |
383 textRight = textLeft + $text.outerWidth(); | |
384 textBottom = textTop + $text.outerHeight(); | |
385 textCenter = {x: textLeft + ($text.outerWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)}; | |
386 // points[points.length] = {x: x, y: y}; | |
387 points[points.length] = spikePoint = {y: textBottom + opts.spikeLength, x: ((textRight-textLeft) * .5) + xShift, type: 'spike'}; | |
388 crossPoint = findIntersectX(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textBottom); | |
389 // make sure that the crossPoint is not outside of text box boundaries | |
390 crossPoint.x = crossPoint.x < textLeft + opts.spikeGirth/2 + opts.cornerRadius ? textLeft + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.x; | |
391 crossPoint.x = crossPoint.x > (textRight - opts.spikeGirth/2) - opts.cornerRadius ? (textRight - opts.spikeGirth/2) - opts.CornerRadius : crossPoint.x; | |
392 points[points.length] = {x: crossPoint.x - (opts.spikeGirth/2), y: textBottom, type: 'join'}; | |
393 points[points.length] = {x: textLeft, y: textBottom, type: 'corner'}; // left bottom corner | |
394 points[points.length] = {x: textLeft, y: textTop, type: 'corner'}; // left top corner | |
395 points[points.length] = {x: textRight, y: textTop, type: 'corner'}; // right top corner | |
396 points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner | |
397 points[points.length] = {x: crossPoint.x + (opts.spikeGirth/2), y: textBottom, type: 'join'}; | |
398 points[points.length] = spikePoint; | |
399 break; | |
400 case 'left': | |
401 // spike on right | |
402 $text.css('margin-right', opts.spikeLength + 'px'); | |
403 $box.css({top: vert + 'px', left: ((left - $text.outerWidth(true) - animDist) + opts.overlap) + 'px'}); | |
404 // move text up/down if extends out of window | |
405 textBottomSpace = (winBottom - opts.windowMargin) - ($text.offset().top + $text.outerHeight(true)); | |
406 var yShift = 0; | |
407 if (textBottomSpace < 0) { | |
408 // shift it up | |
409 $box.css('top', (numb($box.css('top')) + textBottomSpace) + 'px'); | |
410 yShift -= textBottomSpace; | |
411 } | |
412 // we ensure top space second to ensure that top of box is visible | |
413 textTopSpace = ($text.offset().top + numb($text.css('margin-top'))) - (scrollTop + opts.windowMargin); | |
414 if (textTopSpace < 0) { | |
415 // shift it down | |
416 $box.css('top', (numb($box.css('top')) - textTopSpace) + 'px'); | |
417 yShift += textTopSpace; | |
418 } | |
419 textTop = $text.btPosition().top + numb($text.css('margin-top')); | |
420 textLeft = $text.btPosition().left + numb($text.css('margin-left')); | |
421 textRight = textLeft + $text.outerWidth(); | |
422 textBottom = textTop + $text.outerHeight(); | |
423 textCenter = {x: textLeft + ($text.outerWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)}; | |
424 points[points.length] = spikePoint = {x: textRight + opts.spikeLength, y: ((textBottom-textTop) * .5) + yShift, type: 'spike'}; | |
425 crossPoint = findIntersectY(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textRight); | |
426 // make sure that the crossPoint is not outside of text box boundaries | |
427 crossPoint.y = crossPoint.y < textTop + opts.spikeGirth/2 + opts.cornerRadius ? textTop + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.y; | |
428 crossPoint.y = crossPoint.y > (textBottom - opts.spikeGirth/2) - opts.cornerRadius ? (textBottom - opts.spikeGirth/2) - opts.cornerRadius : crossPoint.y; | |
429 points[points.length] = {x: textRight, y: crossPoint.y + opts.spikeGirth/2, type: 'join'}; | |
430 points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner | |
431 points[points.length] = {x: textLeft, y: textBottom, type: 'corner'}; // left bottom corner | |
432 points[points.length] = {x: textLeft, y: textTop, type: 'corner'}; // left top corner | |
433 points[points.length] = {x: textRight, y: textTop, type: 'corner'}; // right top corner | |
434 points[points.length] = {x: textRight, y: crossPoint.y - opts.spikeGirth/2, type: 'join'}; | |
435 points[points.length] = spikePoint; | |
436 break; | |
437 case 'bottom': | |
438 // spike on top | |
439 $text.css('margin-top', opts.spikeLength + 'px'); | |
440 $box.css({top: (top + height + animDist) - opts.overlap, left: horiz}); | |
441 // move text up/down if extends out of window | |
442 textRightSpace = (winRight - opts.windowMargin) - ($text.offset().left + $text.outerWidth(true)); | |
443 var xShift = 0; | |
444 if (textRightSpace < 0) { | |
445 // shift it left | |
446 $box.css('left', (numb($box.css('left')) + textRightSpace) + 'px'); | |
447 xShift -= textRightSpace; | |
448 } | |
449 // we ensure left space second to ensure that left of box is visible | |
450 textLeftSpace = ($text.offset().left + numb($text.css('margin-left'))) - (scrollLeft + opts.windowMargin); | |
451 if (textLeftSpace < 0) { | |
452 // shift it right | |
453 $box.css('left', (numb($box.css('left')) - textLeftSpace) + 'px'); | |
454 xShift += textLeftSpace; | |
455 } | |
456 textTop = $text.btPosition().top + numb($text.css('margin-top')); | |
457 textLeft = $text.btPosition().left + numb($text.css('margin-left')); | |
458 textRight = textLeft + $text.outerWidth(); | |
459 textBottom = textTop + $text.outerHeight(); | |
460 textCenter = {x: textLeft + ($text.outerWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)}; | |
461 points[points.length] = spikePoint = {x: ((textRight-textLeft) * .5) + xShift, y: 0, type: 'spike'}; | |
462 crossPoint = findIntersectX(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textTop); | |
463 // make sure that the crossPoint is not outside of text box boundaries | |
464 crossPoint.x = crossPoint.x < textLeft + opts.spikeGirth/2 + opts.cornerRadius ? textLeft + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.x; | |
465 crossPoint.x = crossPoint.x > (textRight - opts.spikeGirth/2) - opts.cornerRadius ? (textRight - opts.spikeGirth/2) - opts.cornerRadius : crossPoint.x; | |
466 points[points.length] = {x: crossPoint.x + opts.spikeGirth/2, y: textTop, type: 'join'}; | |
467 points[points.length] = {x: textRight, y: textTop, type: 'corner'}; // right top corner | |
468 points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner | |
469 points[points.length] = {x: textLeft, y: textBottom, type: 'corner'}; // left bottom corner | |
470 points[points.length] = {x: textLeft, y: textTop, type: 'corner'}; // left top corner | |
471 points[points.length] = {x: crossPoint.x - (opts.spikeGirth/2), y: textTop, type: 'join'}; | |
472 points[points.length] = spikePoint; | |
473 break; | |
474 case 'right': | |
475 // spike on left | |
476 $text.css('margin-left', (opts.spikeLength + 'px')); | |
477 $box.css({top: vert + 'px', left: ((left + width + animDist) - opts.overlap) + 'px'}); | |
478 // move text up/down if extends out of window | |
479 textBottomSpace = (winBottom - opts.windowMargin) - ($text.offset().top + $text.outerHeight(true)); | |
480 var yShift = 0; | |
481 if (textBottomSpace < 0) { | |
482 // shift it up | |
483 $box.css('top', (numb($box.css('top')) + textBottomSpace) + 'px'); | |
484 yShift -= textBottomSpace; | |
485 } | |
486 // we ensure top space second to ensure that top of box is visible | |
487 textTopSpace = ($text.offset().top + numb($text.css('margin-top'))) - (scrollTop + opts.windowMargin); | |
488 if (textTopSpace < 0) { | |
489 // shift it down | |
490 $box.css('top', (numb($box.css('top')) - textTopSpace) + 'px'); | |
491 yShift += textTopSpace; | |
492 } | |
493 textTop = $text.btPosition().top + numb($text.css('margin-top')); | |
494 textLeft = $text.btPosition().left + numb($text.css('margin-left')); | |
495 textRight = textLeft + $text.outerWidth(); | |
496 textBottom = textTop + $text.outerHeight(); | |
497 textCenter = {x: textLeft + ($text.outerWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)}; | |
498 points[points.length] = spikePoint = {x: 0, y: ((textBottom-textTop) * .5) + yShift, type: 'spike'}; | |
499 crossPoint = findIntersectY(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textLeft); | |
500 // make sure that the crossPoint is not outside of text box boundaries | |
501 crossPoint.y = crossPoint.y < textTop + opts.spikeGirth/2 + opts.cornerRadius ? textTop + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.y; | |
502 crossPoint.y = crossPoint.y > (textBottom - opts.spikeGirth/2) - opts.cornerRadius ? (textBottom - opts.spikeGirth/2) - opts.cornerRadius : crossPoint.y; | |
503 points[points.length] = {x: textLeft, y: crossPoint.y - opts.spikeGirth/2, type: 'join'}; | |
504 points[points.length] = {x: textLeft, y: textTop, type: 'corner'}; // left top corner | |
505 points[points.length] = {x: textRight, y: textTop, type: 'corner'}; // right top corner | |
506 points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner | |
507 points[points.length] = {x: textLeft, y: textBottom, type: 'corner'}; // left bottom corner | |
508 points[points.length] = {x: textLeft, y: crossPoint.y + opts.spikeGirth/2, type: 'join'}; | |
509 points[points.length] = spikePoint; | |
510 break; | |
511 } // </ switch > | |
512 | |
513 var canvas = $('<canvas width="'+ (numb($text.outerWidth(true)) + opts.strokeWidth*2) +'" height="'+ (numb($text.outerHeight(true)) + opts.strokeWidth*2) +'"></canvas>').appendTo($box).css({position: 'absolute', top: $text.btPosition().top, left: $text.btPosition().left, zIndex: opts.boxzIndex}).get(0); | |
514 | |
515 // if excanvas is set up, we need to initialize the new canvas element | |
516 if (typeof G_vmlCanvasManager != 'undefined') { | |
517 canvas = G_vmlCanvasManager.initElement(canvas); | |
518 } | |
519 | |
520 if (opts.cornerRadius > 0) { | |
521 // round the corners! | |
522 var newPoints = new Array(); | |
523 var newPoint; | |
524 for (var i=0; i<points.length; i++) { | |
525 if (points[i].type == 'corner') { | |
526 // create two new arc points | |
527 // find point between this and previous (using modulo in case of ending) | |
528 newPoint = betweenPoint(points[i], points[(i-1)%points.length], opts.cornerRadius); | |
529 newPoint.type = 'arcStart'; | |
530 newPoints[newPoints.length] = newPoint; | |
531 // the original corner point | |
532 newPoints[newPoints.length] = points[i]; | |
533 // find point between this and next | |
534 newPoint = betweenPoint(points[i], points[(i+1)%points.length], opts.cornerRadius); | |
535 newPoint.type = 'arcEnd'; | |
536 newPoints[newPoints.length] = newPoint; | |
537 } | |
538 else { | |
539 newPoints[newPoints.length] = points[i]; | |
540 } | |
541 } | |
542 // overwrite points with new version | |
543 points = newPoints; | |
544 | |
545 } | |
546 | |
547 var ctx = canvas.getContext("2d"); | |
548 drawIt.apply(ctx, [points], opts.strokeWidth); | |
549 ctx.fillStyle = opts.fill; | |
550 if (opts.shadow) { | |
551 ctx.shadowOffsetX = 2; | |
552 ctx.shadowOffsetY = 2; | |
553 ctx.shadowBlur = 5; | |
554 ctx.shadowColor = opts.shadowColor; | |
555 } | |
556 ctx.closePath(); | |
557 ctx.fill(); | |
558 if (opts.strokeWidth > 0) { | |
559 ctx.shadowColor = 'rgba(0, 0, 0, 0)'; | |
560 ctx.lineWidth = opts.strokeWidth; | |
561 ctx.strokeStyle = opts.strokeStyle; | |
562 ctx.beginPath(); | |
563 drawIt.apply(ctx, [points], opts.strokeWidth); | |
564 ctx.closePath(); | |
565 ctx.stroke(); | |
566 } | |
567 | |
568 if (opts.animate) { | |
569 $box.css({opacity: 0.1}); | |
570 } | |
571 | |
572 $box.css({visibility: 'visible'}); | |
573 | |
574 if (opts.overlay) { | |
575 // EXPERIMENTAL!!!! | |
576 var overlay = $('<div class="bt-overlay"></div>').css({ | |
577 position: 'absolute', | |
578 backgroundColor: 'blue', | |
579 top: top, | |
580 left: left, | |
581 width: width, | |
582 height: height, | |
583 opacity: '.2' | |
584 }).appendTo(offsetParent); | |
585 $(this).data('overlay', overlay); | |
586 } | |
587 | |
588 var animParams = {opacity: 1}; | |
589 if (opts.animate) { | |
590 switch (position) { | |
591 case 'top': | |
592 animParams.top = $box.btPosition().top + opts.distance; | |
593 break; | |
594 case 'left': | |
595 animParams.left = $box.btPosition().left + opts.distance; | |
596 break; | |
597 case 'bottom': | |
598 animParams.top = $box.btPosition().top - opts.distance; | |
599 break; | |
600 case 'right': | |
601 animParams.left = $box.btPosition().left - opts.distance; | |
602 break; | |
603 } | |
604 $box.animate(animParams, {duration: opts.speed, easing: opts.easing}); | |
605 } | |
606 | |
607 if ((opts.ajaxPath != null && opts.ajaxCache == false) || ajaxTimeout) { | |
608 // if ajaxCache is not enabled or if there was a server timeout, | |
609 // remove the content variable so it will be loaded again from server | |
610 content = false; | |
611 } | |
612 | |
613 // stick this element into the clickAnywhereToClose stack | |
614 if (opts.clickAnywhereToClose) { | |
615 jQuery.bt.vars.clickAnywhereStack.push(this); | |
616 $(document).click(jQuery.bt.docClick); | |
617 } | |
618 | |
619 // stick this element into the closeWhenOthersOpen stack | |
620 if (opts.closeWhenOthersOpen) { | |
621 jQuery.bt.vars.closeWhenOpenStack.push(this); | |
622 } | |
623 | |
624 // trigger postShow function | |
625 opts.postShow.apply(this); | |
626 | |
627 | |
628 }; // </ turnOn() > | |
629 | |
630 this.btOff = function() { | |
631 | |
632 // trigger preHide function | |
633 opts.preHide.apply(this); | |
634 | |
635 var box = $(this).data('bt-box'); | |
636 var overlay = $(this).data('bt-overlay'); | |
637 if (typeof box == 'object') { | |
638 $(box).remove(); | |
639 $(this).removeData('bt-box'); | |
640 } | |
641 if (typeof overlay == 'object') { | |
642 $(overlay).remove(); | |
643 $(this).removeData('bt-overlay'); | |
644 } | |
645 | |
646 // remove this from the stacks | |
647 jQuery.bt.vars.clickAnywhereStack = arrayRemove(jQuery.bt.vars.clickAnywhereStack, this); | |
648 jQuery.bt.vars.closeWhenOpenStack = arrayRemove(jQuery.bt.vars.closeWhenOpenStack, this); | |
649 | |
650 // trigger postHide function | |
651 opts.postHide.apply(this); | |
652 | |
653 // remove the 'bt-active' and activeClass classes from target | |
654 $(this).removeClass('bt-active ' + opts.activeClass); | |
655 | |
656 }; // </ turnOff() > | |
657 | |
658 var refresh = this.btRefresh = function() { | |
659 this.btOff(); | |
660 this.btOn(); | |
661 }; | |
662 | |
663 | |
664 }); // </ this.each() > | |
665 | |
666 | |
667 function drawIt(points, strokeWidth) { | |
668 this.moveTo(points[0].x, points[0].y); | |
669 for (i=1;i<points.length;i++) { | |
670 if (points[i-1].type == 'arcStart') { | |
671 // if we're creating a rounded corner | |
672 //ctx.arc(round5(points[i].x), round5(points[i].y), points[i].startAngle, points[i].endAngle, opts.cornerRadius, false); | |
673 this.quadraticCurveTo(round5(points[i].x, strokeWidth), round5(points[i].y, strokeWidth), round5(points[(i+1)%points.length].x, strokeWidth), round5(points[(i+1)%points.length].y, strokeWidth)); | |
674 i++; | |
675 //ctx.moveTo(round5(points[i].x), round5(points[i].y)); | |
676 } | |
677 else { | |
678 this.lineTo(round5(points[i].x, strokeWidth), round5(points[i].y, strokeWidth)); | |
679 } | |
680 } | |
681 }; // </ drawIt() > | |
682 | |
683 /** | |
684 * For odd stroke widths, round to the nearest .5 pixel to avoid antialiasing | |
685 * http://developer.mozilla.org/en/Canvas_tutorial/Applying_styles_and_colors | |
686 */ | |
687 function round5(num, strokeWidth) { | |
688 var ret; | |
689 strokeWidth = numb(strokeWidth); | |
690 if (strokeWidth%2) { | |
691 ret = num; | |
692 } | |
693 else { | |
694 ret = Math.round(num - .5) + .5; | |
695 } | |
696 return ret; | |
697 }; // </ round5() > | |
698 | |
699 /** | |
700 * Ensure that a number is a number... or zero | |
701 */ | |
702 function numb(num) { | |
703 return parseInt(num) || 0; | |
704 }; // </ numb() > | |
705 | |
706 /** | |
707 * Remove an element from an array | |
708 */ | |
709 function arrayRemove(arr, elem) { | |
710 var x, newArr = new Array(); | |
711 for (x in arr) { | |
712 if (arr[x] != elem) { | |
713 newArr.push(arr[x]); | |
714 } | |
715 } | |
716 return newArr; | |
717 }; // </ arrayRemove() > | |
718 | |
719 /** | |
720 * Given two points, find a point which is dist pixels from point1 on a line to point2 | |
721 */ | |
722 function betweenPoint(point1, point2, dist) { | |
723 // figure out if we're horizontal or vertical | |
724 var y, x; | |
725 if (point1.x == point2.x) { | |
726 // vertical | |
727 y = point1.y < point2.y ? point1.y + dist : point1.y - dist; | |
728 return {x: point1.x, y: y}; | |
729 } | |
730 else if (point1.y == point2.y) { | |
731 // horizontal | |
732 x = point1.x < point2.x ? point1.x + dist : point1.x - dist; | |
733 return {x:x, y: point1.y}; | |
734 } | |
735 }; // </ betweenPoint() > | |
736 | |
737 function centerPoint(arcStart, corner, arcEnd) { | |
738 var x = corner.x == arcStart.x ? arcEnd.x : arcStart.x; | |
739 var y = corner.y == arcStart.y ? arcEnd.y : arcStart.y; | |
740 var startAngle, endAngle; | |
741 if (arcStart.x < arcEnd.x) { | |
742 if (arcStart.y > arcEnd.y) { | |
743 // arc is on upper left | |
744 startAngle = (Math.PI/180)*180; | |
745 endAngle = (Math.PI/180)*90; | |
746 } | |
747 else { | |
748 // arc is on upper right | |
749 startAngle = (Math.PI/180)*90; | |
750 endAngle = 0; | |
751 } | |
752 } | |
753 else { | |
754 if (arcStart.y > arcEnd.y) { | |
755 // arc is on lower left | |
756 startAngle = (Math.PI/180)*270; | |
757 endAngle = (Math.PI/180)*180; | |
758 } | |
759 else { | |
760 // arc is on lower right | |
761 startAngle = 0; | |
762 endAngle = (Math.PI/180)*270; | |
763 } | |
764 } | |
765 return {x: x, y: y, type: 'center', startAngle: startAngle, endAngle: endAngle}; | |
766 }; // </ centerPoint() > | |
767 | |
768 /** | |
769 * Find the intersection point of two lines, each defined by two points | |
770 * arguments are x1, y1 and x2, y2 for r1 (line 1) and r2 (line 2) | |
771 * It's like an algebra party!!! | |
772 */ | |
773 function findIntersect(r1x1, r1y1, r1x2, r1y2, r2x1, r2y1, r2x2, r2y2) { | |
774 | |
775 if (r2x1 == r2x2) { | |
776 return findIntersectY(r1x1, r1y1, r1x2, r1y2, r2x1); | |
777 } | |
778 if (r2y1 == r2y2) { | |
779 return findIntersectX(r1x1, r1y1, r1x2, r1y2, r2y1); | |
780 } | |
781 | |
782 // m = (y1 - y2) / (x1 - x2) // <-- how to find the slope | |
783 // y = mx + b // the 'classic' linear equation | |
784 // b = y - mx // how to find b (the y-intersect) | |
785 // x = (y - b)/m // how to find x | |
786 var r1m = (r1y1 - r1y2) / (r1x1 - r1x2); | |
787 var r1b = r1y1 - (r1m * r1x1); | |
788 var r2m = (r2y1 - r2y2) / (r2x1 - r2x2); | |
789 var r2b = r2y1 - (r2m * r2x1); | |
790 | |
791 var x = (r2b - r1b) / (r1m - r2m); | |
792 var y = r1m * x + r1b; | |
793 | |
794 return {x: x, y: y}; | |
795 }; // </ findIntersect() > | |
796 | |
797 /** | |
798 * Find the y intersection point of a line and given x vertical | |
799 */ | |
800 function findIntersectY(r1x1, r1y1, r1x2, r1y2, x) { | |
801 if (r1y1 == r1y2) { | |
802 return {x: x, y: r1y1}; | |
803 } | |
804 var r1m = (r1y1 - r1y2) / (r1x1 - r1x2); | |
805 var r1b = r1y1 - (r1m * r1x1); | |
806 | |
807 var y = r1m * x + r1b; | |
808 | |
809 return {x: x, y: y}; | |
810 }; // </ findIntersectY() > | |
811 | |
812 /** | |
813 * Find the x intersection point of a line and given y horizontal | |
814 */ | |
815 function findIntersectX(r1x1, r1y1, r1x2, r1y2, y) { | |
816 if (r1x1 == r1x2) { | |
817 return {x: r1x1, y: y}; | |
818 } | |
819 var r1m = (r1y1 - r1y2) / (r1x1 - r1x2); | |
820 var r1b = r1y1 - (r1m * r1x1); | |
821 | |
822 // y = mx + b // your old friend, linear equation | |
823 // x = (y - b)/m // linear equation solved for x | |
824 var x = (y - r1b) / r1m; | |
825 | |
826 return {x: x, y: y}; | |
827 | |
828 }; // </ findIntersectX() > | |
829 | |
830 }; // </ jQuery.fn.bt() > | |
831 | |
832 /** | |
833 * jQuery's compat.js (used in Drupal's jQuery upgrade module, overrides the $().position() function | |
834 * this is a copy of that function to allow the plugin to work when compat.js is present | |
835 * once compat.js is fixed to not override existing functions, this function can be removed | |
836 * and .btPosion() can be replaced with .position() above... | |
837 */ | |
838 jQuery.fn.btPosition = function() { | |
839 | |
840 function num(elem, prop) { | |
841 return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0; | |
842 }; | |
843 | |
844 var left = 0, top = 0, results; | |
845 | |
846 if ( this[0] ) { | |
847 // Get *real* offsetParent | |
848 var offsetParent = this.offsetParent(), | |
849 | |
850 // Get correct offsets | |
851 offset = this.offset(), | |
852 parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset(); | |
853 | |
854 // Subtract element margins | |
855 // note: when an element has margin: auto the offsetLeft and marginLeft | |
856 // are the same in Safari causing offset.left to incorrectly be 0 | |
857 offset.top -= num( this, 'marginTop' ); | |
858 offset.left -= num( this, 'marginLeft' ); | |
859 | |
860 // Add offsetParent borders | |
861 parentOffset.top += num( offsetParent, 'borderTopWidth' ); | |
862 parentOffset.left += num( offsetParent, 'borderLeftWidth' ); | |
863 | |
864 // Subtract the two offsets | |
865 results = { | |
866 top: offset.top - parentOffset.top, | |
867 left: offset.left - parentOffset.left | |
868 }; | |
869 } | |
870 | |
871 return results; | |
872 }; // </ jQuery.fn.btPosition() > | |
873 | |
874 | |
875 /** | |
876 * A convenience function to run btOn() (if available) | |
877 * for each selected item | |
878 */ | |
879 jQuery.fn.btOn = function() { | |
880 return this.each(function(index){ | |
881 if ($.isFunction(this.btOn)) { | |
882 this.btOn(); | |
883 } | |
884 }); | |
885 }; // </ $().btOn() > | |
886 | |
887 /** | |
888 * | |
889 * A convenience function to run btOff() (if available) | |
890 * for each selected item | |
891 */ | |
892 jQuery.fn.btOff = function() { | |
893 return this.each(function(index){ | |
894 if ($.isFunction(this.btOff)) { | |
895 this.btOff(); | |
896 } | |
897 }); | |
898 }; // </ $().btOff() > | |
899 | |
900 jQuery.bt = {}; | |
901 jQuery.bt.vars = {clickAnywhereStack: [], closeWhenOpenStack: []}; | |
902 | |
903 /** | |
904 * This function gets bound to the document's click event | |
905 * It turns off all of the tips in the click-anywhere-to-close stack | |
906 */ | |
907 jQuery.bt.docClick = function(e) { | |
908 if (!e) { | |
909 var e = window.event; | |
910 }; | |
911 if (!$(e.target).parents().andSelf().filter('.bt-wrapper, .bt-active').length) { | |
912 // if clicked element isn't inside tip, close tips in stack | |
913 $(jQuery.bt.vars.clickAnywhereStack).btOff(); | |
914 $(document).unbind('click', jQuery.bt.docClick); | |
915 } | |
916 }; // </ docClick() > | |
917 | |
918 /** | |
919 * Defaults for the beauty tips | |
920 * | |
921 * Note this is a variable definition and not a function. So defaults can be | |
922 * written for an entire page by simply redefining attributes like so: | |
923 * | |
924 * jQuery.bt.defaults.width = 400; | |
925 * | |
926 * This would make all Beauty Tips boxes 400px wide. | |
927 * | |
928 * Each of these options may also be overridden during | |
929 * | |
930 * Can be overriden globally or at time of call. | |
931 * | |
932 */ | |
933 jQuery.bt.defaults = { | |
934 trigger: 'hover', // trigger to show/hide tip | |
935 // use [on, off] to define separate on/off triggers | |
936 // also use space character to allow multiple to trigger | |
937 // examples: | |
938 // ['focus', 'blur'] // focus displays, blur hides | |
939 // 'dblclick' // dblclick toggles on/off | |
940 // ['focus mouseover', 'blur mouseout'] // multiple triggers | |
941 // 'now' // shows/hides tip without event | |
942 // 'none' // use $('#selector').btOn(); and ...btOff(); | |
943 // 'hoverIntent' // hover using hoverIntent plugin (settings below) | |
944 // note: | |
945 // hoverIntent becomes default if available | |
946 | |
947 clickAnywhereToClose: true, // clicking anywhere outside of the tip will close it | |
948 closeWhenOthersOpen: false, // tip will be closed before another opens - stop >= 2 tips being on | |
949 | |
950 width: '200px', // width of tooltip box | |
951 // when combined with cssStyles: {width: 'auto'}, this becomes a max-width for the text | |
952 padding: '10px', // padding for content (get more fine grained with cssStyles) | |
953 spikeGirth: 10, // width of spike | |
954 spikeLength: 15, // length of spike | |
955 overlap: 0, // spike overlap (px) onto target (can cause problems with 'hover' trigger) | |
956 overlay: false, // display overlay on target (use CSS to style) -- BUGGY! | |
957 killTitle: true, // kill title tags to avoid double tooltips | |
958 | |
959 textzIndex: 9999, // z-index for the text | |
960 boxzIndex: 9998, // z-index for the "talk" box (should always be less than textzIndex) | |
961 wrapperzIndex: 9997, | |
962 positions: ['most'], // preference of positions for tip (will use first with available space) | |
963 // possible values 'top', 'bottom', 'left', 'right' as an array in order of | |
964 // preference. Last value will be used if others don't have enough space. | |
965 // or use 'most' to use the area with the most space | |
966 fill: "rgb(255, 255, 102)", // fill color for the tooltip box | |
967 | |
968 windowMargin: 10, // space (px) to leave between text box and browser edge | |
969 | |
970 strokeWidth: 1, // width of stroke around box, **set to 0 for no stroke** | |
971 strokeStyle: "#000", // color/alpha of stroke | |
972 | |
973 cornerRadius: 5, // radius of corners (px), set to 0 for square corners | |
974 | |
975 // following values are on a scale of 0 to 1 with .5 being centered | |
976 | |
977 centerPointX: .5, // the spike extends from center of the target edge to this point | |
978 centerPointY: .5, // defined by percentage horizontal (x) and vertical (y) | |
979 | |
980 shadow: false, // use drop shadow? (only displays in Safari and FF 3.1) - experimental | |
981 shadowOffsetX: 2, // shadow offset x (px) | |
982 shadowOffsetY: 2, // shadow offset y (px) | |
983 shadowBlur: 3, // shadow blur (px) | |
984 shadowColor: "#000", // shadow color/alpha | |
985 | |
986 animate: false, // animate show/hide of box - EXPERIMENTAL (buggy in IE) | |
987 distance: 15, // distance of animation movement (px) | |
988 easing: 'swing', // animation easing | |
989 speed: 200, // speed (ms) of animation | |
990 | |
991 cssClass: '', // CSS class to add to the box wrapper div (of the TIP) | |
992 cssStyles: {}, // styles to add the text box | |
993 // example: {fontFamily: 'Georgia, Times, serif', fontWeight: 'bold'} | |
994 | |
995 activeClass: 'bt-active', // class added to TARGET element when its BeautyTip is active | |
996 | |
997 contentSelector: "$(this).attr('title')", // if there is no content argument, use this selector to retrieve the title | |
998 | |
999 ajaxPath: null, // if using ajax request for content, this contains url and (opt) selector | |
1000 // this will override content and contentSelector | |
1001 // examples (see jQuery load() function): | |
1002 // '/demo.html' | |
1003 // '/help/ajax/snip' | |
1004 // '/help/existing/full div#content' | |
1005 | |
1006 // ajaxPath can also be defined as an array | |
1007 // in which case, the first value will be parsed as a jQuery selector | |
1008 // the result of which will be used as the ajaxPath | |
1009 // the second (optional) value is the content selector as above | |
1010 // examples: | |
1011 // ["$(this).attr('href')", 'div#content'] | |
1012 // ["$(this).parents('.wrapper').find('.title').attr('href')"] | |
1013 // ["$('#some-element').val()"] | |
1014 | |
1015 ajaxError: '<strong>ERROR:</strong> <em>%error</em>', | |
1016 // error text, use "%error" to insert error from server | |
1017 ajaxLoading: '<blink>Loading...</blink>', // yes folks, it's the blink tag! | |
1018 ajaxData: {}, // key/value pairs | |
1019 ajaxType: 'GET', // 'GET' or 'POST' | |
1020 ajaxCache: true, // cache ajax results and do not send request to same url multiple times | |
1021 ajaxOpts: {}, // any other ajax options - timeout, passwords, processing functions, etc... | |
1022 // see http://docs.jquery.com/Ajax/jQuery.ajax#options | |
1023 | |
1024 preShow: function(){return;}, // function to run before popup is built and displayed | |
1025 postShow: function(){return;}, // function to run after popup is built and displayed | |
1026 preHide: function(){return;}, // function to run before popup is removed | |
1027 postHide: function(){return;}, // function to run after popup is removed | |
1028 | |
1029 hoverIntentOpts: { // options for hoverIntent (if installed) | |
1030 interval: 300, // http://cherne.net/brian/resources/jquery.hoverIntent.html | |
1031 timeout: 500 | |
1032 } | |
1033 | |
1034 }; // </ jQuery.bt.defaults > | |
1035 | |
1036 | |
1037 // @todo | |
1038 // use larger canvas (extend to edge of page when windowMargin is active) | |
1039 // add options to shift position of tip vert/horiz and position of spike tip | |
1040 // create drawn (canvas) shadows | |
1041 // use overlay to allow overlap with hover | |
1042 // experiment with making tooltip a subelement of the target | |
1043 // rework animation system |