jquery.bt.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. /* @name BeautyTips
  2. * @desc a tooltips/baloon-help plugin for jQuery
  3. * @author Jeff Robbins - Lullabot - http://www.lullabot.com
  4. * @version 0.9.5 release candidate 1 (5/20/2009) */
  5. jQuery.bt = {version: '0.9.5-rc1'};
  6. /* @type jQuery
  7. * @cat Plugins/bt
  8. * @requires jQuery v1.2+
  9. * Dual licensed under the MIT and GPL licenses:
  10. * http://www.opensource.org/licenses/mit-license.php
  11. * http://www.gnu.org/licenses/gpl.html
  12. * Encourage development. If you use BeautyTips for anything cool
  13. * or on a site that people have heard of, please drop me a note.
  14. * - jeff ^at lullabot > com
  15. * No guarantees, warranties, or promises of any kind */
  16. ;(function($) {
  17. /* @credit Inspired by Karl Swedberg's ClueTip
  18. * (http://plugins.learningjquery.com/cluetip/), which in turn was inspired
  19. * by Cody Lindley's jTip (http://www.codylindley.com)
  20. * Usage:
  21. * The function can be called in a number of ways.
  22. * $(selector).bt();
  23. * $(selector).bt('Content text');
  24. * $(selector).bt('Content text', {option1: value, option2: value});
  25. * $(selector).bt({option1: value, option2: value});
  26. * For more/better documentation and lots of examples, visit the demo page included with the distribution */
  27. jQuery.fn.bt = function(content, options) {
  28. if (typeof content != 'string') {
  29. var contentSelect = true;
  30. options = content;
  31. content = false;
  32. }
  33. else {
  34. var contentSelect = false;
  35. }
  36. if (jQuery.fn.hoverIntent && jQuery.bt.defaults.trigger == 'hover') {
  37. jQuery.bt.defaults.trigger = 'hoverIntent';
  38. }
  39. return this.each(function(index) {
  40. var opts = jQuery.extend(false, jQuery.bt.defaults, jQuery.bt.options, options);
  41. opts.overlap = -10;
  42. var ajaxTimeout = false;
  43. if (opts.killTitle) {
  44. $(this).find('[title]').andSelf().each(function() {
  45. if (!$(this).attr('bt-xTitle')) {
  46. $(this).attr('bt-xTitle', $(this).attr('title')).attr('title', '');
  47. }
  48. });
  49. }
  50. if (typeof opts.trigger == 'string') {
  51. opts.trigger = [opts.trigger];
  52. }
  53. if (opts.trigger[0] == 'hoverIntent') {
  54. var hoverOpts = jQuery.extend(opts.hoverIntentOpts, {
  55. over: function() {
  56. this.btOn();
  57. },
  58. out: function() {
  59. this.btOff();
  60. }});
  61. $(this).hoverIntent(hoverOpts);
  62. }
  63. else if (opts.trigger[0] == 'hover') {
  64. $(this).hover(
  65. function() {
  66. this.btOn();
  67. },
  68. function() {
  69. this.btOff();
  70. }
  71. );
  72. }
  73. else if (opts.trigger[0] == 'now') {
  74. if ($(this).hasClass('bt-active')) {
  75. this.btOff();
  76. }
  77. else {
  78. this.btOn();
  79. }
  80. }
  81. else if (opts.trigger[0] == 'none') {
  82. }
  83. else if (opts.trigger.length > 1 && opts.trigger[0] != opts.trigger[1]) {
  84. $(this)
  85. .bind(opts.trigger[0], function() {
  86. this.btOn();
  87. })
  88. .bind(opts.trigger[1], function() {
  89. this.btOff();
  90. });
  91. }
  92. else {
  93. $(this).bind(opts.trigger[0], function() {
  94. if ($(this).hasClass('bt-active')) {
  95. this.btOff();
  96. }
  97. else {
  98. this.btOn();
  99. }
  100. });
  101. }
  102. this.btOn = function () {
  103. if (typeof $(this).data('bt-box') == 'object') {
  104. this.btOff();
  105. }
  106. opts.preBuild.apply(this);
  107. $(jQuery.bt.vars.closeWhenOpenStack).btOff();
  108. $(this).addClass('bt-active ' + opts.activeClass);
  109. if (contentSelect && opts.ajaxPath == null) {
  110. if (opts.killTitle) {
  111. $(this).attr('title', $(this).attr('bt-xTitle'));
  112. }
  113. content = $.isFunction(opts.contentSelector) ? opts.contentSelector.apply(this) : eval(opts.contentSelector);
  114. if (opts.killTitle) {
  115. $(this).attr('title', '');
  116. }
  117. }
  118. if (opts.ajaxPath != null && content == false) {
  119. if (typeof opts.ajaxPath == 'object') {
  120. var url = eval(opts.ajaxPath[0]);
  121. url += opts.ajaxPath[1] ? ' ' + opts.ajaxPath[1] : '';
  122. }
  123. else {
  124. var url = opts.ajaxPath;
  125. }
  126. var off = url.indexOf(" ");
  127. if ( off >= 0 ) {
  128. var selector = url.slice(off, url.length);
  129. url = url.slice(0, off);
  130. }
  131. var cacheData = opts.ajaxCache ? $(document.body).data('btCache-' + url.replace(/\./g, '')) : null;
  132. if (typeof cacheData == 'string') {
  133. content = selector ? $("<div/>").append(cacheData.replace(/<script(.|\s)*?\/script>/g, "")).find(selector) : cacheData;
  134. }
  135. else {
  136. var target = this;
  137. var ajaxOpts = jQuery.extend(false,
  138. {
  139. type: opts.ajaxType,
  140. data: opts.ajaxData,
  141. cache: opts.ajaxCache,
  142. url: url,
  143. complete: function(XMLHttpRequest, textStatus) {
  144. if (textStatus == 'success' || textStatus == 'notmodified') {
  145. if (opts.ajaxCache) {
  146. $(document.body).data('btCache-' + url.replace(/\./g, ''), XMLHttpRequest.responseText);
  147. }
  148. ajaxTimeout = false;
  149. content = selector ?
  150. $("<div/>")
  151. .append(XMLHttpRequest.responseText.replace(/<script(.|\s)*?\/script>/g, ""))
  152. .find(selector) :
  153. XMLHttpRequest.responseText;
  154. }
  155. else {
  156. if (textStatus == 'timeout') {
  157. ajaxTimeout = true;
  158. }
  159. content = opts.ajaxError.replace(/%error/g, XMLHttpRequest.statusText);
  160. }
  161. if ($(target).hasClass('bt-active')) {
  162. target.btOn();
  163. }
  164. }
  165. }, opts.ajaxOpts);
  166. jQuery.ajax(ajaxOpts);
  167. content = opts.ajaxLoading;
  168. }
  169. }
  170. if (opts.offsetParent){
  171. var offsetParent = $(opts.offsetParent);
  172. var offsetParentPos = offsetParent.offset();
  173. var pos = $(this).offset();
  174. var top = numb(pos.top) - numb(offsetParentPos.top) + numb($(this).css('margin-top'));
  175. var left = numb(pos.left) - numb(offsetParentPos.left) + numb($(this).css('margin-left'));
  176. }
  177. else {
  178. var offsetParent = ($(this).css('position') == 'absolute') ? $(this).parents().eq(0).offsetParent() : $(this).offsetParent();
  179. var pos = $(this).btPosition();
  180. var top = numb(pos.top) + numb($(this).css('margin-top'));
  181. var left = numb(pos.left) + numb($(this).css('margin-left'));
  182. }
  183. var width = $(this).btOuterWidth();
  184. var height = $(this).outerHeight();
  185. if (typeof content == 'object') {
  186. var original = content;
  187. var clone = $(original).clone(true).show();
  188. var origClones = $(original).data('bt-clones') || [];
  189. origClones.push(clone);
  190. $(original).data('bt-clones', origClones);
  191. $(clone).data('bt-orig', original);
  192. $(this).data('bt-content-orig', {original: original, clone: clone});
  193. content = clone;
  194. }
  195. if (typeof content == 'null' || content == '') {
  196. return;
  197. }
  198. var $text = $('<div class="bt-content"></div>').append(content).css({position: 'absolute', zIndex: opts.textzIndex, left: 0, top: 0});
  199. var $box = $('<div class="bt-wrapper"></div>').append($text).css({position: 'absolute', zIndex: opts.wrapperzIndex, visibility:'hidden'}).appendTo(offsetParent);
  200. $(this).data('bt-box', $box);
  201. var scrollTop = numb($(document).scrollTop());
  202. var scrollLeft = numb($(document).scrollLeft());
  203. var docWidth = numb($(window).width());
  204. var docHeight = numb($(window).height());
  205. var winRight = scrollLeft + docWidth;
  206. var winBottom = scrollTop + docHeight;
  207. var space = new Object();
  208. var thisOffset = $(this).offset();
  209. space.top = thisOffset.top - scrollTop;
  210. space.bottom = docHeight - ((thisOffset + height) - scrollTop);
  211. space.left = thisOffset.left - scrollLeft;
  212. space.right = docWidth - ((thisOffset.left + width) - scrollLeft);
  213. var textOutHeight = numb($text.outerHeight());
  214. var textOutWidth = numb($text.btOuterWidth());
  215. if (opts.positions.constructor == String) {
  216. opts.positions = opts.positions.replace(/ /, '').split(',');
  217. }
  218. if (opts.positions[0] == 'most') {
  219. var position = 'top';
  220. for (var pig in space) { // <------- pigs in space!
  221. position = space[pig] > space[position] ? pig : position;
  222. }
  223. }
  224. else {
  225. for (var x in opts.positions) {
  226. var position = opts.positions[x];
  227. if ((position == 'left' || position == 'right') && space[position] > textOutWidth) {
  228. break;
  229. }
  230. else if ((position == 'top' || position == 'bottom') && space[position] > textOutHeight) {
  231. break;
  232. }
  233. }
  234. }
  235. // Keep the next two lines intact as backups.
  236. //var horiz = left + ((width - textOutWidth) * .5);
  237. var horiz = left + (width * .5);
  238. var vert = top + ((height - textOutHeight) * .5);
  239. var points = new Array();
  240. var textTop, textLeft, textRight, textBottom, textTopSpace, textBottomSpace, textLeftSpace, textRightSpace, textCenter;
  241. switch(position) {
  242. case 'top':
  243. $text.css('margin-bottom', 0);
  244. $box.css({top: (top - $text.outerHeight(true)), left: horiz});
  245. textRightSpace = (winRight - opts.windowMargin) - ($text.offset().left + $text.btOuterWidth(true));
  246. var xShift = 0;
  247. if (textRightSpace < 0) {
  248. // shift it left
  249. $box.css('left', (numb($box.css('left')) + textRightSpace) + 'px');
  250. xShift -= textRightSpace;
  251. }
  252. // we test left space second to ensure that left of box is visible
  253. textLeftSpace = ($text.offset().left + numb($text.css('margin-left'))) - (scrollLeft + opts.windowMargin);
  254. if (textLeftSpace < 0) {
  255. // shift it right
  256. $box.css('left', (numb($box.css('left')) - textLeftSpace) + 'px');
  257. xShift += textLeftSpace;
  258. }
  259. textTop = $text.btPosition().top + numb($text.css('margin-top'));
  260. textLeft = $text.btPosition().left + numb($text.css('margin-left'));
  261. textRight = textLeft + $text.btOuterWidth();
  262. textBottom = textTop + $text.outerHeight();
  263. textCenter = {x: textLeft + $text.btOuterWidth(), y: textTop + $text.outerHeight()};
  264. break;
  265. case 'bottom':
  266. // spike on top
  267. $text.css('margin-top', 0);
  268. $box.css({top: (top + height), left: horiz});
  269. // move text up/down if extends out of window
  270. textRightSpace = (winRight - opts.windowMargin) - ($text.offset().left + $text.btOuterWidth(true));
  271. var xShift = 0;
  272. if (textRightSpace < 0) {
  273. // shift it left
  274. $box.css('left', (numb($box.css('left')) + textRightSpace) + 'px');
  275. xShift -= textRightSpace;
  276. }
  277. // we ensure left space second to ensure that left of box is visible
  278. textLeftSpace = ($text.offset().left + numb($text.css('margin-left'))) - (scrollLeft + opts.windowMargin);
  279. if (textLeftSpace < 0) {
  280. // shift it right
  281. $box.css('left', (numb($box.css('left')) - textLeftSpace) + 'px');
  282. xShift += textLeftSpace;
  283. }
  284. textTop = $text.btPosition().top + numb($text.css('margin-top'));
  285. textLeft = $text.btPosition().left + numb($text.css('margin-left'));
  286. textRight = textLeft + $text.btOuterWidth();
  287. textBottom = textTop + $text.outerHeight();
  288. textCenter = {x: textLeft + $text.btOuterWidth(), y: textTop + $text.outerHeight()};
  289. break;
  290. case 'left':
  291. $text.css('margin-right', 0);
  292. $box.css({top: vert + 'px', left: (left - $text.btOuterWidth(true)) + 'px'});
  293. textBottomSpace = (winBottom - opts.windowMargin) - ($text.offset().top + $text.outerHeight(true));
  294. var yShift = 0;
  295. if (textBottomSpace < 0) {
  296. $box.css('top', (numb($box.css('top')) + textBottomSpace) + 'px');
  297. yShift -= textBottomSpace;
  298. }
  299. textTopSpace = ($text.offset().top + numb($text.css('margin-top'))) - (scrollTop + opts.windowMargin);
  300. if (textTopSpace < 0) {
  301. $box.css('top', (numb($box.css('top')) - textTopSpace) + 'px');
  302. yShift += textTopSpace;
  303. }
  304. textTop = $text.btPosition().top + numb($text.css('margin-top'));
  305. textLeft = $text.btPosition().left + numb($text.css('margin-left'));
  306. textRight = textLeft + $text.btOuterWidth();
  307. textBottom = textTop + $text.outerHeight();
  308. textCenter = {x: textLeft + $text.btOuterWidth(), y: textTop + $text.outerHeight()};
  309. break;
  310. case 'right':
  311. $text.css('margin-left', 0);
  312. $box.css({top: vert + 'px', left: ((left + width) - opts.overlap) + 'px'});
  313. textBottomSpace = (winBottom - opts.windowMargin) - ($text.offset().top + $text.outerHeight(true));
  314. var yShift = 0;
  315. if (textBottomSpace < 0) {
  316. $box.css('top', (numb($box.css('top')) + textBottomSpace) + 'px');
  317. yShift -= textBottomSpace;
  318. }
  319. textTopSpace = ($text.offset().top + numb($text.css('margin-top'))) - (scrollTop + opts.windowMargin);
  320. if (textTopSpace < 0) {
  321. $box.css('top', (numb($box.css('top')) - textTopSpace) + 'px');
  322. yShift += textTopSpace;
  323. }
  324. textTop = $text.btPosition().top + numb($text.css('margin-top'));
  325. textLeft = $text.btPosition().left + numb($text.css('margin-left'));
  326. textRight = textLeft + $text.btOuterWidth();
  327. textBottom = textTop + $text.outerHeight();
  328. textCenter = {x: textLeft + $text.btOuterWidth(), y: textTop + $text.outerHeight()};
  329. break;
  330. }
  331. opts.preShow.apply(this, [$box[0]]);
  332. $box.css({display:'none', visibility: 'visible'});
  333. opts.showTip.apply(this, [$box[0]]);
  334. if ((opts.ajaxPath != null && opts.ajaxCache == false) || ajaxTimeout) {
  335. content = false;
  336. }
  337. if (opts.clickAnywhereToClose) {
  338. jQuery.bt.vars.clickAnywhereStack.push(this);
  339. $(document).click(jQuery.bt.docClick);
  340. }
  341. if (opts.closeWhenOthersOpen) {
  342. jQuery.bt.vars.closeWhenOpenStack.push(this);
  343. }
  344. opts.postShow.apply(this, [$box[0]]);
  345. };
  346. this.btOff = function() {
  347. var box = $(this).data('bt-box');
  348. opts.preHide.apply(this, [box]);
  349. var i = this;
  350. i.btCleanup = function(){
  351. var box = $(i).data('bt-box');
  352. var contentOrig = $(i).data('bt-content-orig');
  353. var overlay = $(i).data('bt-overlay');
  354. if (typeof box == 'object') {
  355. $(box).remove();
  356. $(i).removeData('bt-box');
  357. }
  358. if (typeof contentOrig == 'object') {
  359. var clones = $(contentOrig.original).data('bt-clones');
  360. $(contentOrig).data('bt-clones', arrayRemove(clones, contentOrig.clone));
  361. }
  362. if (typeof overlay == 'object') {
  363. $(overlay).remove();
  364. $(i).removeData('bt-overlay');
  365. }
  366. jQuery.bt.vars.clickAnywhereStack = arrayRemove(jQuery.bt.vars.clickAnywhereStack, i);
  367. jQuery.bt.vars.closeWhenOpenStack = arrayRemove(jQuery.bt.vars.closeWhenOpenStack, i);
  368. $(i).removeClass('bt-active ' + opts.activeClass);
  369. opts.postHide.apply(i);
  370. }
  371. opts.hideTip.apply(this, [box, i.btCleanup]);
  372. };
  373. var refresh = this.btRefresh = function() {
  374. this.btOff();
  375. this.btOn();
  376. };
  377. });
  378. function numb(num) {
  379. return parseInt(num) || 0;
  380. };
  381. function arrayRemove(arr, elem) {
  382. var x, newArr = new Array();
  383. for (x in arr) {
  384. if (arr[x] != elem) {
  385. newArr.push(arr[x]);
  386. }
  387. }
  388. return newArr;
  389. };
  390. };
  391. jQuery.fn.btPosition = function() {
  392. function num(elem, prop) {
  393. return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0;
  394. };
  395. var left = 0, top = 0, results;
  396. if ( this[0] ) {
  397. var offsetParent = this.offsetParent(),
  398. offset = this.offset(),
  399. parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset();
  400. offset.top -= num( this, 'marginTop' );
  401. offset.left -= num( this, 'marginLeft' );
  402. parentOffset.top += num( offsetParent, 'borderTopWidth' );
  403. parentOffset.left += num( offsetParent, 'borderLeftWidth' );
  404. results = {
  405. top: offset.top - parentOffset.top,
  406. left: offset.left - parentOffset.left
  407. };
  408. }
  409. return results;
  410. };
  411. jQuery.fn.btOuterWidth = function(margin) {
  412. function num(elem, prop) {
  413. return elem[0] && parseInt(jQuery.curCSS(elem[0], prop, true), 10) || 0;
  414. };
  415. return this["innerWidth"]()
  416. + num(this, "borderLeftWidth")
  417. + num(this, "borderRightWidth")
  418. + (margin ? num(this, "marginLeft")
  419. + num(this, "marginRight") : 0);
  420. };
  421. jQuery.fn.btOn = function() {
  422. return this.each(function(index){
  423. if (jQuery.isFunction(this.btOn)) {
  424. this.btOn();
  425. }
  426. });
  427. };
  428. jQuery.fn.btOff = function() {
  429. return this.each(function(index){
  430. if (jQuery.isFunction(this.btOff)) {
  431. this.btOff();
  432. }
  433. });
  434. };
  435. jQuery.bt.vars = {clickAnywhereStack: [], closeWhenOpenStack: []};
  436. jQuery.bt.docClick = function(e) {
  437. if (!e) {
  438. var e = window.event;
  439. };
  440. if (!$(e.target).parents().andSelf().filter('.bt-wrapper, .bt-active').length && jQuery.bt.vars.clickAnywhereStack.length) {
  441. $(jQuery.bt.vars.clickAnywhereStack).btOff();
  442. $(document).unbind('click', jQuery.bt.docClick);
  443. }
  444. };
  445. /* Defaults can be written for an entire page by redefining attributes:
  446. * jQuery.bt.options.width = 400;
  447. * Be sure to use *jQuery.bt.options* and not jQuery.bt.defaults when overriding
  448. * Each of these options may also be overridden globally or at time of call.*/
  449. jQuery.bt.defaults = {
  450. trigger: 'hover', // trigger to show/hide tip - hoverIntent becomes default if available
  451. clickAnywhereToClose:true, // clicking outside of the tip will close it
  452. closeWhenOthersOpen: true, // tip will be closed before another opens
  453. killTitle: true, // kill title tags to avoid double tooltips
  454. textzIndex: 9999, // z-index for the text
  455. boxzIndex: 9998, // z-index for the "talk" box (should always be less than textzIndex)
  456. wrapperzIndex: 9997,
  457. offsetParent: null, // DOM node to append the tooltip into. Must be positioned relative or absolute.
  458. positions: ['top', 'bottom'], // preference of positions for tip (will use first with available space) 'top', 'bottom', 'left', 'right', 'most'
  459. windowMargin: 10, // space (px) to leave between text box and browser edge
  460. activeClass: 'bt-active', // class added to TARGET element when its BeautyTip is active
  461. contentSelector: "$(this).attr('title')", // if there is no content argument, use this selector to retrieve the title
  462. // a function which returns the content may also be passed here
  463. ajaxPath: null, // if using ajax request for content, this contains url and (opt) selector
  464. ajaxError: '<strong>ERROR:</strong> <em>%error</em>',
  465. // error text, use "%error" to insert error from server
  466. ajaxLoading: '<blink>Loading...</blink>', // yes folks, it's the blink tag!
  467. ajaxData: {}, // key/value pairs
  468. ajaxType: 'GET', // 'GET' or 'POST'
  469. ajaxCache: true, // cache ajax results and do not send request to same url multiple times
  470. ajaxOpts: {}, // any other ajax options - timeout, passwords, processing functions, etc...
  471. preBuild: function(){}, // function to run before popup is built
  472. preShow: function(box){}, // function to run before popup is displayed
  473. showTip: function(box){
  474. $(box).show();
  475. },
  476. postShow: function(box){}, // function to run after popup is built and displayed
  477. preHide: function(box){}, // function to run before popup is removed
  478. hideTip: function(box, callback) {
  479. $(box).hide();
  480. callback(); // you MUST call "callback" at the end of your animations
  481. },
  482. postHide: function(){}, // function to run after popup is removed
  483. hoverIntentOpts: { // options for hoverIntent (if installed)
  484. interval: 100, // http://cherne.net/brian/resources/jquery.hoverIntent.html
  485. timeout: 500
  486. }
  487. };
  488. jQuery.bt.options = {};
  489. })(jQuery);