jquery.poshytip.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. /*
  2. * Poshy Tip jQuery plugin v1.1
  3. * http://vadikom.com/tools/poshy-tip-jquery-plugin-for-stylish-tooltips/
  4. * Copyright 2010-2011, Vasil Dinkov, http://vadikom.com/
  5. */
  6. (function($) {
  7. var tips = [],
  8. reBgImage = /^url\(["']?([^"'\)]*)["']?\);?$/i,
  9. rePNG = /\.png$/i,
  10. ie6 = $.browser.msie && $.browser.version == 6;
  11. // make sure the tips' position is updated on resize
  12. function handleWindowResize() {
  13. $.each(tips, function() {
  14. this.refresh(true);
  15. });
  16. }
  17. $(window).resize(handleWindowResize);
  18. $.Poshytip = function(elm, options) {
  19. this.$elm = $(elm);
  20. this.opts = $.extend({}, $.fn.poshytip.defaults, options);
  21. this.$tip = $(['<div class="',this.opts.className,'">',
  22. '<div class="tip-inner tip-bg-image"></div>',
  23. '<div class="tip-arrow tip-arrow-top tip-arrow-right tip-arrow-bottom tip-arrow-left"></div>',
  24. '</div>'].join('')).appendTo(document.body);
  25. this.$arrow = this.$tip.find('div.tip-arrow');
  26. this.$inner = this.$tip.find('div.tip-inner');
  27. this.disabled = false;
  28. this.content = null;
  29. this.init();
  30. };
  31. $.Poshytip.prototype = {
  32. init: function() {
  33. tips.push(this);
  34. // save the original title and a reference to the Poshytip object
  35. var title = this.$elm.attr('title');
  36. this.$elm.data('title.poshytip', title !== undefined ? title : null)
  37. .data('poshytip', this);
  38. // hook element events
  39. if (this.opts.showOn != 'none') {
  40. this.$elm.bind({
  41. 'mouseenter.poshytip': $.proxy(this.mouseenter, this),
  42. 'mouseleave.poshytip': $.proxy(this.mouseleave, this)
  43. });
  44. switch (this.opts.showOn) {
  45. case 'hover':
  46. if (this.opts.alignTo == 'cursor')
  47. this.$elm.bind('mousemove.poshytip', $.proxy(this.mousemove, this));
  48. if (this.opts.allowTipHover)
  49. this.$tip.hover($.proxy(this.clearTimeouts, this), $.proxy(this.mouseleave, this));
  50. break;
  51. case 'focus':
  52. this.$elm.bind({
  53. 'focus.poshytip': $.proxy(this.show, this),
  54. 'blur.poshytip': $.proxy(this.hide, this)
  55. });
  56. break;
  57. }
  58. }
  59. },
  60. mouseenter: function(e) {
  61. if (this.disabled)
  62. return true;
  63. this.$elm.attr('title', '');
  64. if (this.opts.showOn == 'focus')
  65. return true;
  66. this.clearTimeouts();
  67. this.showTimeout = setTimeout($.proxy(this.show, this), this.opts.showTimeout);
  68. },
  69. mouseleave: function(e) {
  70. if (this.disabled || this.asyncAnimating && (this.$tip[0] === e.relatedTarget || jQuery.contains(this.$tip[0], e.relatedTarget)))
  71. return true;
  72. var title = this.$elm.data('title.poshytip');
  73. if (title !== null)
  74. this.$elm.attr('title', title);
  75. if (this.opts.showOn == 'focus')
  76. return true;
  77. this.clearTimeouts();
  78. this.hideTimeout = setTimeout($.proxy(this.hide, this), this.opts.hideTimeout);
  79. },
  80. mousemove: function(e) {
  81. if (this.disabled)
  82. return true;
  83. this.eventX = e.pageX;
  84. this.eventY = e.pageY;
  85. if (this.opts.followCursor && this.$tip.data('active')) {
  86. this.calcPos();
  87. this.$tip.css({left: this.pos.l, top: this.pos.t});
  88. if (this.pos.arrow)
  89. this.$arrow[0].className = 'tip-arrow tip-arrow-' + this.pos.arrow;
  90. }
  91. },
  92. show: function() {
  93. if (this.disabled || this.$tip.data('active'))
  94. return;
  95. this.reset();
  96. this.update();
  97. this.display();
  98. if (this.opts.timeOnScreen)
  99. setTimeout($.proxy(this.hide, this), this.opts.timeOnScreen);
  100. },
  101. hide: function() {
  102. if (this.disabled || !this.$tip.data('active'))
  103. return;
  104. this.display(true);
  105. },
  106. reset: function() {
  107. this.$tip.queue([]).detach().css('visibility', 'hidden').data('active', false);
  108. this.$inner.find('*').poshytip('hide');
  109. if (this.opts.fade)
  110. this.$tip.css('opacity', this.opacity);
  111. this.$arrow[0].className = 'tip-arrow tip-arrow-top tip-arrow-right tip-arrow-bottom tip-arrow-left';
  112. this.asyncAnimating = false;
  113. },
  114. update: function(content, dontOverwriteOption) {
  115. if (this.disabled)
  116. return;
  117. var async = content !== undefined;
  118. if (async) {
  119. if (!dontOverwriteOption)
  120. this.opts.content = content;
  121. if (!this.$tip.data('active'))
  122. return;
  123. } else {
  124. content = this.opts.content;
  125. }
  126. // update content only if it has been changed since last time
  127. var self = this,
  128. newContent = typeof content == 'function' ?
  129. content.call(this.$elm[0], function(newContent) {
  130. self.update(newContent);
  131. }) :
  132. content == '[title]' ? this.$elm.data('title.poshytip') : content;
  133. if (this.content !== newContent) {
  134. this.$inner.empty().append(newContent);
  135. this.content = newContent;
  136. }
  137. this.refresh(async);
  138. },
  139. refresh: function(async) {
  140. if (this.disabled)
  141. return;
  142. if (async) {
  143. if (!this.$tip.data('active'))
  144. return;
  145. // save current position as we will need to animate
  146. var currPos = {left: this.$tip.css('left'), top: this.$tip.css('top')};
  147. }
  148. // reset position to avoid text wrapping, etc.
  149. this.$tip.css({left: 0, top: 0}).appendTo(document.body);
  150. // save default opacity
  151. if (this.opacity === undefined)
  152. this.opacity = this.$tip.css('opacity');
  153. // check for images - this code is here (i.e. executed each time we show the tip and not on init) due to some browser inconsistencies
  154. var bgImage = this.$tip.css('background-image').match(reBgImage),
  155. arrow = this.$arrow.css('background-image').match(reBgImage);
  156. if (bgImage) {
  157. var bgImagePNG = rePNG.test(bgImage[1]);
  158. // fallback to background-color/padding/border in IE6 if a PNG is used
  159. if (ie6 && bgImagePNG) {
  160. this.$tip.css('background-image', 'none');
  161. this.$inner.css({margin: 0, border: 0, padding: 0});
  162. bgImage = bgImagePNG = false;
  163. } else {
  164. this.$tip.prepend('<table border="0" cellpadding="0" cellspacing="0"><tr><td class="tip-top tip-bg-image" colspan="2"><span></span></td><td class="tip-right tip-bg-image" rowspan="2"><span></span></td></tr><tr><td class="tip-left tip-bg-image" rowspan="2"><span></span></td><td></td></tr><tr><td class="tip-bottom tip-bg-image" colspan="2"><span></span></td></tr></table>')
  165. .css({border: 0, padding: 0, 'background-image': 'none', 'background-color': 'transparent'})
  166. .find('.tip-bg-image').css('background-image', 'url("' + bgImage[1] +'")').end()
  167. .find('td').eq(3).append(this.$inner);
  168. }
  169. // disable fade effect in IE due to Alpha filter + translucent PNG issue
  170. if (bgImagePNG && !$.support.opacity)
  171. this.opts.fade = false;
  172. }
  173. // IE arrow fixes
  174. if (arrow && !$.support.opacity) {
  175. // disable arrow in IE6 if using a PNG
  176. if (ie6 && rePNG.test(arrow[1])) {
  177. arrow = false;
  178. this.$arrow.css('background-image', 'none');
  179. }
  180. // disable fade effect in IE due to Alpha filter + translucent PNG issue
  181. this.opts.fade = false;
  182. }
  183. var $table = this.$tip.find('table');
  184. if (ie6) {
  185. // fix min/max-width in IE6
  186. this.$tip[0].style.width = '';
  187. $table.width('auto').find('td').eq(3).width('auto');
  188. var tipW = this.$tip.width(),
  189. minW = parseInt(this.$tip.css('min-width')),
  190. maxW = parseInt(this.$tip.css('max-width'));
  191. if (!isNaN(minW) && tipW < minW)
  192. tipW = minW;
  193. else if (!isNaN(maxW) && tipW > maxW)
  194. tipW = maxW;
  195. this.$tip.add($table).width(tipW).eq(0).find('td').eq(3).width('100%');
  196. } else if ($table[0]) {
  197. // fix the table width if we are using a background image
  198. // IE9, FF4 use float numbers for width/height so use getComputedStyle for them to avoid text wrapping
  199. // for details look at: http://vadikom.com/dailies/offsetwidth-offsetheight-useless-in-ie9-firefox4/
  200. $table.width('auto').find('td').eq(3).width('auto').end().end().width(document.defaultView && document.defaultView.getComputedStyle && parseFloat(document.defaultView.getComputedStyle(this.$tip[0], null).width) || this.$tip.width()).find('td').eq(3).width('100%');
  201. }
  202. this.tipOuterW = this.$tip.outerWidth();
  203. this.tipOuterH = this.$tip.outerHeight();
  204. this.calcPos();
  205. // position and show the arrow image
  206. if (arrow && this.pos.arrow) {
  207. this.$arrow[0].className = 'tip-arrow tip-arrow-' + this.pos.arrow;
  208. this.$arrow.css('visibility', 'inherit');
  209. }
  210. if (async) {
  211. this.asyncAnimating = true;
  212. var self = this;
  213. this.$tip.css(currPos).animate({left: this.pos.l, top: this.pos.t}, 200, function() { self.asyncAnimating = false; });
  214. } else {
  215. this.$tip.css({left: this.pos.l, top: this.pos.t});
  216. }
  217. },
  218. display: function(hide) {
  219. var active = this.$tip.data('active');
  220. if (active && !hide || !active && hide)
  221. return;
  222. this.$tip.stop();
  223. if ((this.opts.slide && this.pos.arrow || this.opts.fade) && (hide && this.opts.hideAniDuration || !hide && this.opts.showAniDuration)) {
  224. var from = {}, to = {};
  225. // this.pos.arrow is only undefined when alignX == alignY == 'center' and we don't need to slide in that rare case
  226. if (this.opts.slide && this.pos.arrow) {
  227. var prop, arr;
  228. if (this.pos.arrow == 'bottom' || this.pos.arrow == 'top') {
  229. prop = 'top';
  230. arr = 'bottom';
  231. } else {
  232. prop = 'left';
  233. arr = 'right';
  234. }
  235. var val = parseInt(this.$tip.css(prop));
  236. from[prop] = val + (hide ? 0 : (this.pos.arrow == arr ? -this.opts.slideOffset : this.opts.slideOffset));
  237. to[prop] = val + (hide ? (this.pos.arrow == arr ? this.opts.slideOffset : -this.opts.slideOffset) : 0) + 'px';
  238. }
  239. if (this.opts.fade) {
  240. from.opacity = hide ? this.$tip.css('opacity') : 0;
  241. to.opacity = hide ? 0 : this.opacity;
  242. }
  243. this.$tip.css(from).animate(to, this.opts[hide ? 'hideAniDuration' : 'showAniDuration']);
  244. }
  245. hide ? this.$tip.queue($.proxy(this.reset, this)) : this.$tip.css('visibility', 'inherit');
  246. this.$tip.data('active', !active);
  247. },
  248. disable: function() {
  249. this.reset();
  250. this.disabled = true;
  251. },
  252. enable: function() {
  253. this.disabled = false;
  254. },
  255. destroy: function() {
  256. this.reset();
  257. this.$tip.remove();
  258. delete this.$tip;
  259. this.content = null;
  260. this.$elm.unbind('.poshytip').removeData('title.poshytip').removeData('poshytip');
  261. tips.splice($.inArray(this, tips), 1);
  262. },
  263. clearTimeouts: function() {
  264. if (this.showTimeout) {
  265. clearTimeout(this.showTimeout);
  266. this.showTimeout = 0;
  267. }
  268. if (this.hideTimeout) {
  269. clearTimeout(this.hideTimeout);
  270. this.hideTimeout = 0;
  271. }
  272. },
  273. calcPos: function() {
  274. var pos = {l: 0, t: 0, arrow: ''},
  275. $win = $(window),
  276. win = {
  277. l: $win.scrollLeft(),
  278. t: $win.scrollTop(),
  279. w: $win.width(),
  280. h: $win.height()
  281. }, xL, xC, xR, yT, yC, yB;
  282. if (this.opts.alignTo == 'cursor') {
  283. xL = xC = xR = this.eventX;
  284. yT = yC = yB = this.eventY;
  285. } else { // this.opts.alignTo == 'target'
  286. var elmOffset = this.$elm.offset(),
  287. elm = {
  288. l: elmOffset.left,
  289. t: elmOffset.top,
  290. w: this.$elm.outerWidth(),
  291. h: this.$elm.outerHeight()
  292. };
  293. xL = elm.l + (this.opts.alignX != 'inner-right' ? 0 : elm.w); // left edge
  294. xC = xL + Math.floor(elm.w / 2); // h center
  295. xR = xL + (this.opts.alignX != 'inner-left' ? elm.w : 0); // right edge
  296. yT = elm.t + (this.opts.alignY != 'inner-bottom' ? 0 : elm.h); // top edge
  297. yC = yT + Math.floor(elm.h / 2); // v center
  298. yB = yT + (this.opts.alignY != 'inner-top' ? elm.h : 0); // bottom edge
  299. }
  300. // keep in viewport and calc arrow position
  301. switch (this.opts.alignX) {
  302. case 'right':
  303. case 'inner-left':
  304. pos.l = xR + this.opts.offsetX;
  305. if (pos.l + this.tipOuterW > win.l + win.w)
  306. pos.l = win.l + win.w - this.tipOuterW;
  307. if (this.opts.alignX == 'right' || this.opts.alignY == 'center')
  308. pos.arrow = 'left';
  309. break;
  310. case 'center':
  311. pos.l = xC - Math.floor(this.tipOuterW / 2);
  312. if (pos.l + this.tipOuterW > win.l + win.w)
  313. pos.l = win.l + win.w - this.tipOuterW;
  314. else if (pos.l < win.l)
  315. pos.l = win.l;
  316. break;
  317. default: // 'left' || 'inner-right'
  318. pos.l = xL - this.tipOuterW - this.opts.offsetX;
  319. if (pos.l < win.l)
  320. pos.l = win.l;
  321. if (this.opts.alignX == 'left' || this.opts.alignY == 'center')
  322. pos.arrow = 'right';
  323. }
  324. switch (this.opts.alignY) {
  325. case 'bottom':
  326. case 'inner-top':
  327. pos.t = yB + this.opts.offsetY;
  328. // 'left' and 'right' need priority for 'target'
  329. if (!pos.arrow || this.opts.alignTo == 'cursor')
  330. pos.arrow = 'top';
  331. if (pos.t + this.tipOuterH > win.t + win.h) {
  332. pos.t = yT - this.tipOuterH - this.opts.offsetY;
  333. if (pos.arrow == 'top')
  334. pos.arrow = 'bottom';
  335. }
  336. break;
  337. case 'center':
  338. pos.t = yC - Math.floor(this.tipOuterH / 2);
  339. if (pos.t + this.tipOuterH > win.t + win.h)
  340. pos.t = win.t + win.h - this.tipOuterH;
  341. else if (pos.t < win.t)
  342. pos.t = win.t;
  343. break;
  344. default: // 'top' || 'inner-bottom'
  345. pos.t = yT - this.tipOuterH - this.opts.offsetY;
  346. // 'left' and 'right' need priority for 'target'
  347. if (!pos.arrow || this.opts.alignTo == 'cursor')
  348. pos.arrow = 'bottom';
  349. if (pos.t < win.t) {
  350. pos.t = yB + this.opts.offsetY;
  351. if (pos.arrow == 'bottom')
  352. pos.arrow = 'top';
  353. }
  354. }
  355. this.pos = pos;
  356. }
  357. };
  358. $.fn.poshytip = function(options) {
  359. if (typeof options == 'string') {
  360. var args = arguments,
  361. method = options;
  362. Array.prototype.shift.call(args);
  363. // unhook live events if 'destroy' is called
  364. if (method == 'destroy')
  365. this.die('mouseenter.poshytip').die('focus.poshytip');
  366. return this.each(function() {
  367. var poshytip = $(this).data('poshytip');
  368. if (poshytip && poshytip[method])
  369. poshytip[method].apply(poshytip, args);
  370. });
  371. }
  372. var opts = $.extend({}, $.fn.poshytip.defaults, options);
  373. // generate CSS for this tip class if not already generated
  374. if (!$('#poshytip-css-' + opts.className)[0])
  375. $(['<style id="poshytip-css-',opts.className,'" type="text/css">',
  376. 'div.',opts.className,'{visibility:hidden;position:absolute;top:0;left:0;}',
  377. 'div.',opts.className,' table, div.',opts.className,' td{margin:0;font-family:inherit;font-size:inherit;font-weight:inherit;font-style:inherit;font-variant:inherit;}',
  378. 'div.',opts.className,' td.tip-bg-image span{display:block;font:1px/1px sans-serif;height:',opts.bgImageFrameSize,'px;width:',opts.bgImageFrameSize,'px;overflow:hidden;}',
  379. 'div.',opts.className,' td.tip-right{background-position:100% 0;}',
  380. 'div.',opts.className,' td.tip-bottom{background-position:100% 100%;}',
  381. 'div.',opts.className,' td.tip-left{background-position:0 100%;}',
  382. 'div.',opts.className,' div.tip-inner{background-position:-',opts.bgImageFrameSize,'px -',opts.bgImageFrameSize,'px;}',
  383. 'div.',opts.className,' div.tip-arrow{visibility:hidden;position:absolute;overflow:hidden;font:1px/1px sans-serif;}',
  384. '</style>'].join('')).appendTo('head');
  385. // check if we need to hook live events
  386. if (opts.liveEvents && opts.showOn != 'none') {
  387. var deadOpts = $.extend({}, opts, { liveEvents: false });
  388. switch (opts.showOn) {
  389. case 'hover':
  390. this.live('mouseenter.poshytip', function() {
  391. var $this = $(this);
  392. if (!$this.data('poshytip'))
  393. $this.poshytip(deadOpts).poshytip('mouseenter');
  394. });
  395. break;
  396. case 'focus':
  397. this.live('focus.poshytip', function() {
  398. var $this = $(this);
  399. if (!$this.data('poshytip'))
  400. $this.poshytip(deadOpts).poshytip('show');
  401. });
  402. break;
  403. }
  404. return this;
  405. }
  406. return this.each(function() {
  407. new $.Poshytip(this, opts);
  408. });
  409. }
  410. // default settings
  411. $.fn.poshytip.defaults = {
  412. content: '[title]', // content to display ('[title]', 'string', element, function(updateCallback){...}, jQuery)
  413. className: 'tip-yellow', // class for the tips
  414. bgImageFrameSize: 10, // size in pixels for the background-image (if set in CSS) frame around the inner content of the tip
  415. showTimeout: 500, // timeout before showing the tip (in milliseconds 1000 == 1 second)
  416. hideTimeout: 100, // timeout before hiding the tip
  417. timeOnScreen: 0, // timeout before automatically hiding the tip after showing it (set to > 0 in order to activate)
  418. showOn: 'hover', // handler for showing the tip ('hover', 'focus', 'none') - use 'none' to trigger it manually
  419. liveEvents: false, // use live events
  420. alignTo: 'cursor', // align/position the tip relative to ('cursor', 'target')
  421. alignX: 'right', // horizontal alignment for the tip relative to the mouse cursor or the target element
  422. // ('right', 'center', 'left', 'inner-left', 'inner-right') - 'inner-*' matter if alignTo:'target'
  423. alignY: 'top', // vertical alignment for the tip relative to the mouse cursor or the target element
  424. // ('bottom', 'center', 'top', 'inner-bottom', 'inner-top') - 'inner-*' matter if alignTo:'target'
  425. offsetX: -22, // offset X pixels from the default position - doesn't matter if alignX:'center'
  426. offsetY: 18, // offset Y pixels from the default position - doesn't matter if alignY:'center'
  427. allowTipHover: true, // allow hovering the tip without hiding it onmouseout of the target - matters only if showOn:'hover'
  428. followCursor: false, // if the tip should follow the cursor - matters only if showOn:'hover' and alignTo:'cursor'
  429. fade: true, // use fade animation
  430. slide: true, // use slide animation
  431. slideOffset: 8, // slide animation offset
  432. showAniDuration: 300, // show animation duration - set to 0 if you don't want show animation
  433. hideAniDuration: 300 // hide animation duration - set to 0 if you don't want hide animation
  434. };
  435. })(jQuery);