Project

General

Profile

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

    
7
(function($) {
8

    
9
	var tips = [],
10
		reBgImage = /^url\(["']?([^"'\)]*)["']?\);?$/i,
11
		rePNG = /\.png$/i,
12
		ie6 = !!window.createPopup && document.documentElement.currentStyle.minWidth == 'undefined';
13

    
14
	// make sure the tips' position is updated on resize
15
	function handleWindowResize() {
16
		$.each(tips, function() {
17
			this.refresh(true);
18
		});
19
	}
20
	$(window).resize(handleWindowResize);
21

    
22
	$.Poshytip = function(elm, options) {
23
		this.$elm = $(elm);
24
		this.opts = $.extend({}, $.fn.poshytip.defaults, options);
25
		this.$tip = $(['<div class="',this.opts.className,'">',
26
				'<div class="tip-inner tip-bg-image"></div>',
27
				'<div class="tip-arrow tip-arrow-top tip-arrow-right tip-arrow-bottom tip-arrow-left"></div>',
28
			'</div>'].join('')).appendTo(document.body);
29
		this.$arrow = this.$tip.find('div.tip-arrow');
30
		this.$inner = this.$tip.find('div.tip-inner');
31
		this.disabled = false;
32
		this.content = null;
33
		this.init();
34
	};
35

    
36
	$.Poshytip.prototype = {
37
		init: function() {
38
			tips.push(this);
39

    
40
			// save the original title and a reference to the Poshytip object
41
			var title = this.$elm.attr('title');
42
			this.$elm.data('title.poshytip', title !== undefined ? title : null)
43
				.data('poshytip', this);
44

    
45
			// hook element events
46
			if (this.opts.showOn != 'none') {
47
				this.$elm.bind({
48
					'mouseenter.poshytip': $.proxy(this.mouseenter, this),
49
					'mouseleave.poshytip': $.proxy(this.mouseleave, this)
50
				});
51
				switch (this.opts.showOn) {
52
					case 'hover':
53
						if (this.opts.alignTo == 'cursor')
54
							this.$elm.bind('mousemove.poshytip', $.proxy(this.mousemove, this));
55
						if (this.opts.allowTipHover)
56
							this.$tip.hover($.proxy(this.clearTimeouts, this), $.proxy(this.mouseleave, this));
57
						break;
58
					case 'focus':
59
						this.$elm.bind({
60
							'focus.poshytip': $.proxy(this.show, this),
61
							'blur.poshytip': $.proxy(this.hide, this)
62
						});
63
						break;
64
				}
65
			}
66
		},
67
		mouseenter: function(e) {
68
			if (this.disabled)
69
				return true;
70

    
71
			this.$elm.attr('title', '');
72
			if (this.opts.showOn == 'focus')
73
				return true;
74

    
75
			this.clearTimeouts();
76
			this.showTimeout = setTimeout($.proxy(this.show, this), this.opts.showTimeout);
77
		},
78
		mouseleave: function(e) {
79
			if (this.disabled || this.asyncAnimating && (this.$tip[0] === e.relatedTarget || jQuery.contains(this.$tip[0], e.relatedTarget)))
80
				return true;
81

    
82
			var title = this.$elm.data('title.poshytip');
83
			if (title !== null)
84
				this.$elm.attr('title', title);
85
			if (this.opts.showOn == 'focus')
86
				return true;
87

    
88
			this.clearTimeouts();
89
			this.hideTimeout = setTimeout($.proxy(this.hide, this), this.opts.hideTimeout);
90
		},
91
		mousemove: function(e) {
92
			if (this.disabled)
93
				return true;
94

    
95
			this.eventX = e.pageX;
96
			this.eventY = e.pageY;
97
			if (this.opts.followCursor && this.$tip.data('active')) {
98
				this.calcPos();
99
				this.$tip.css({left: this.pos.l, top: this.pos.t});
100
				if (this.pos.arrow)
101
					this.$arrow[0].className = 'tip-arrow tip-arrow-' + this.pos.arrow;
102
			}
103
		},
104
		show: function() {
105
			if (this.disabled || this.$tip.data('active'))
106
				return;
107

    
108
			this.reset();
109
			this.update();
110
			this.display();
111
			if (this.opts.timeOnScreen) {
112
				this.clearTimeouts();
113
				this.hideTimeout = setTimeout($.proxy(this.hide, this), this.opts.timeOnScreen);
114
			}
115
		},
116
		hide: function() {
117
			if (this.disabled || !this.$tip.data('active'))
118
				return;
119

    
120
			this.display(true);
121
		},
122
		reset: function() {
123
			this.$tip.queue([]).detach().css('visibility', 'hidden').data('active', false);
124
			this.$inner.find('*').poshytip('hide');
125
			if (this.opts.fade)
126
				this.$tip.css('opacity', this.opacity);
127
			this.$arrow[0].className = 'tip-arrow tip-arrow-top tip-arrow-right tip-arrow-bottom tip-arrow-left';
128
			this.asyncAnimating = false;
129
		},
130
		update: function(content, dontOverwriteOption) {
131
			if (this.disabled)
132
				return;
133

    
134
			var async = content !== undefined;
135
			if (async) {
136
				if (!dontOverwriteOption)
137
					this.opts.content = content;
138
				if (!this.$tip.data('active'))
139
					return;
140
			} else {
141
				content = this.opts.content;
142
			}
143

    
144
			// update content only if it has been changed since last time
145
			var self = this,
146
				newContent = typeof content == 'function' ?
147
					content.call(this.$elm[0], function(newContent) {
148
						self.update(newContent);
149
					}) :
150
					content == '[title]' ? this.$elm.data('title.poshytip') : content;
151
			if (this.content !== newContent) {
152
				this.$inner.empty().append(newContent);
153
				this.content = newContent;
154
			}
155

    
156
			this.refresh(async);
157
		},
158
		refresh: function(async) {
159
			if (this.disabled)
160
				return;
161

    
162
			if (async) {
163
				if (!this.$tip.data('active'))
164
					return;
165
				// save current position as we will need to animate
166
				var currPos = {left: this.$tip.css('left'), top: this.$tip.css('top')};
167
			}
168

    
169
			// reset position to avoid text wrapping, etc.
170
			this.$tip.css({left: 0, top: 0}).appendTo(document.body);
171

    
172
			// save default opacity
173
			if (this.opacity === undefined)
174
				this.opacity = this.$tip.css('opacity');
175

    
176
			// 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
177
			var bgImage = this.$tip.css('background-image').match(reBgImage),
178
				arrow = this.$arrow.css('background-image').match(reBgImage);
179

    
180
			if (bgImage) {
181
				var bgImagePNG = rePNG.test(bgImage[1]);
182
				// fallback to background-color/padding/border in IE6 if a PNG is used
183
				if (ie6 && bgImagePNG) {
184
					this.$tip.css('background-image', 'none');
185
					this.$inner.css({margin: 0, border: 0, padding: 0});
186
					bgImage = bgImagePNG = false;
187
				} else {
188
					this.$tip.prepend('<table class="tip-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>')
189
						.css({border: 0, padding: 0, 'background-image': 'none', 'background-color': 'transparent'})
190
						.find('.tip-bg-image').css('background-image', 'url("' + bgImage[1] +'")').end()
191
						.find('td').eq(3).append(this.$inner);
192
				}
193
				// disable fade effect in IE due to Alpha filter + translucent PNG issue
194
				if (bgImagePNG && !$.support.opacity)
195
					this.opts.fade = false;
196
			}
197
			// IE arrow fixes
198
			if (arrow && !$.support.opacity) {
199
				// disable arrow in IE6 if using a PNG
200
				if (ie6 && rePNG.test(arrow[1])) {
201
					arrow = false;
202
					this.$arrow.css('background-image', 'none');
203
				}
204
				// disable fade effect in IE due to Alpha filter + translucent PNG issue
205
				this.opts.fade = false;
206
			}
207

    
208
			var $table = this.$tip.find('> table.tip-table');
209
			if (ie6) {
210
				// fix min/max-width in IE6
211
				this.$tip[0].style.width = '';
212
				$table.width('auto').find('td').eq(3).width('auto');
213
				var tipW = this.$tip.width(),
214
					minW = parseInt(this.$tip.css('min-width')),
215
					maxW = parseInt(this.$tip.css('max-width'));
216
				if (!isNaN(minW) && tipW < minW)
217
					tipW = minW;
218
				else if (!isNaN(maxW) && tipW > maxW)
219
					tipW = maxW;
220
				this.$tip.add($table).width(tipW).eq(0).find('td').eq(3).width('100%');
221
			} else if ($table[0]) {
222
				// fix the table width if we are using a background image
223
				// IE9, FF4 use float numbers for width/height so use getComputedStyle for them to avoid text wrapping
224
				// for details look at: http://vadikom.com/dailies/offsetwidth-offsetheight-useless-in-ie9-firefox4/
225
				$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%');
226
			}
227
			this.tipOuterW = this.$tip.outerWidth();
228
			this.tipOuterH = this.$tip.outerHeight();
229

    
230
			this.calcPos();
231

    
232
			// position and show the arrow image
233
			if (arrow && this.pos.arrow) {
234
				this.$arrow[0].className = 'tip-arrow tip-arrow-' + this.pos.arrow;
235
				this.$arrow.css('visibility', 'inherit');
236
			}
237

    
238
			if (async && this.opts.refreshAniDuration) {
239
				this.asyncAnimating = true;
240
				var self = this;
241
				this.$tip.css(currPos).animate({left: this.pos.l, top: this.pos.t}, this.opts.refreshAniDuration, function() { self.asyncAnimating = false; });
242
			} else {
243
				this.$tip.css({left: this.pos.l, top: this.pos.t});
244
			}
245
		},
246
		display: function(hide) {
247
			var active = this.$tip.data('active');
248
			if (active && !hide || !active && hide)
249
				return;
250

    
251
			this.$tip.stop();
252
			if ((this.opts.slide && this.pos.arrow || this.opts.fade) && (hide && this.opts.hideAniDuration || !hide && this.opts.showAniDuration)) {
253
				var from = {}, to = {};
254
				// this.pos.arrow is only undefined when alignX == alignY == 'center' and we don't need to slide in that rare case
255
				if (this.opts.slide && this.pos.arrow) {
256
					var prop, arr;
257
					if (this.pos.arrow == 'bottom' || this.pos.arrow == 'top') {
258
						prop = 'top';
259
						arr = 'bottom';
260
					} else {
261
						prop = 'left';
262
						arr = 'right';
263
					}
264
					var val = parseInt(this.$tip.css(prop));
265
					from[prop] = val + (hide ? 0 : (this.pos.arrow == arr ? -this.opts.slideOffset : this.opts.slideOffset));
266
					to[prop] = val + (hide ? (this.pos.arrow == arr ? this.opts.slideOffset : -this.opts.slideOffset) : 0) + 'px';
267
				}
268
				if (this.opts.fade) {
269
					from.opacity = hide ? this.$tip.css('opacity') : 0;
270
					to.opacity = hide ? 0 : this.opacity;
271
				}
272
				this.$tip.css(from).animate(to, this.opts[hide ? 'hideAniDuration' : 'showAniDuration']);
273
			}
274
			hide ? this.$tip.queue($.proxy(this.reset, this)) : this.$tip.css('visibility', 'inherit');
275
			this.$tip.data('active', !active);
276
		},
277
		disable: function() {
278
			this.reset();
279
			this.disabled = true;
280
		},
281
		enable: function() {
282
			this.disabled = false;
283
		},
284
		destroy: function() {
285
			this.reset();
286
			this.$tip.remove();
287
			delete this.$tip;
288
			this.content = null;
289
			this.$elm.unbind('.poshytip').removeData('title.poshytip').removeData('poshytip');
290
			tips.splice($.inArray(this, tips), 1);
291
		},
292
		clearTimeouts: function() {
293
			if (this.showTimeout) {
294
				clearTimeout(this.showTimeout);
295
				this.showTimeout = 0;
296
			}
297
			if (this.hideTimeout) {
298
				clearTimeout(this.hideTimeout);
299
				this.hideTimeout = 0;
300
			}
301
		},
302
		calcPos: function() {
303
			var pos = {l: 0, t: 0, arrow: ''},
304
				$win = $(window),
305
				win = {
306
					l: $win.scrollLeft(),
307
					t: $win.scrollTop(),
308
					w: $win.width(),
309
					h: $win.height()
310
				}, xL, xC, xR, yT, yC, yB;
311
			if (this.opts.alignTo == 'cursor') {
312
				xL = xC = xR = this.eventX;
313
				yT = yC = yB = this.eventY;
314
			} else { // this.opts.alignTo == 'target'
315
				var elmOffset = this.$elm.offset(),
316
					elm = {
317
						l: elmOffset.left,
318
						t: elmOffset.top,
319
						w: this.$elm.outerWidth(),
320
						h: this.$elm.outerHeight()
321
					};
322
				xL = elm.l + (this.opts.alignX != 'inner-right' ? 0 : elm.w);	// left edge
323
				xC = xL + Math.floor(elm.w / 2);				// h center
324
				xR = xL + (this.opts.alignX != 'inner-left' ? elm.w : 0);	// right edge
325
				yT = elm.t + (this.opts.alignY != 'inner-bottom' ? 0 : elm.h);	// top edge
326
				yC = yT + Math.floor(elm.h / 2);				// v center
327
				yB = yT + (this.opts.alignY != 'inner-top' ? elm.h : 0);	// bottom edge
328
			}
329

    
330
			// keep in viewport and calc arrow position
331
			switch (this.opts.alignX) {
332
				case 'right':
333
				case 'inner-left':
334
					pos.l = xR + this.opts.offsetX;
335
					if (pos.l + this.tipOuterW > win.l + win.w)
336
						pos.l = win.l + win.w - this.tipOuterW;
337
					if (this.opts.alignX == 'right' || this.opts.alignY == 'center')
338
						pos.arrow = 'left';
339
					break;
340
				case 'center':
341
					pos.l = xC - Math.floor(this.tipOuterW / 2);
342
					if (pos.l + this.tipOuterW > win.l + win.w)
343
						pos.l = win.l + win.w - this.tipOuterW;
344
					else if (pos.l < win.l)
345
						pos.l = win.l;
346
					break;
347
				default: // 'left' || 'inner-right'
348
					pos.l = xL - this.tipOuterW - this.opts.offsetX;
349
					if (pos.l < win.l)
350
						pos.l = win.l;
351
					if (this.opts.alignX == 'left' || this.opts.alignY == 'center')
352
						pos.arrow = 'right';
353
			}
354
			switch (this.opts.alignY) {
355
				case 'bottom':
356
				case 'inner-top':
357
					pos.t = yB + this.opts.offsetY;
358
					// 'left' and 'right' need priority for 'target'
359
					if (!pos.arrow || this.opts.alignTo == 'cursor')
360
						pos.arrow = 'top';
361
					if (pos.t + this.tipOuterH > win.t + win.h) {
362
						pos.t = yT - this.tipOuterH - this.opts.offsetY;
363
						if (pos.arrow == 'top')
364
							pos.arrow = 'bottom';
365
					}
366
					break;
367
				case 'center':
368
					pos.t = yC - Math.floor(this.tipOuterH / 2);
369
					if (pos.t + this.tipOuterH > win.t + win.h)
370
						pos.t = win.t + win.h - this.tipOuterH;
371
					else if (pos.t < win.t)
372
						pos.t = win.t;
373
					break;
374
				default: // 'top' || 'inner-bottom'
375
					pos.t = yT - this.tipOuterH - this.opts.offsetY;
376
					// 'left' and 'right' need priority for 'target'
377
					if (!pos.arrow || this.opts.alignTo == 'cursor')
378
						pos.arrow = 'bottom';
379
					if (pos.t < win.t) {
380
						pos.t = yB + this.opts.offsetY;
381
						if (pos.arrow == 'bottom')
382
							pos.arrow = 'top';
383
					}
384
			}
385
			this.pos = pos;
386
		}
387
	};
388

    
389
	$.fn.poshytip = function(options) {
390
		if (typeof options == 'string') {
391
			var args = arguments,
392
				method = options;
393
			Array.prototype.shift.call(args);
394
			// unhook live events if 'destroy' is called
395
			if (method == 'destroy') {
396
				this.die ?
397
					this.die('mouseenter.poshytip').die('focus.poshytip') :
398
					$(document).undelegate(this.selector, 'mouseenter.poshytip').undelegate(this.selector, 'focus.poshytip');
399
			}
400
			return this.each(function() {
401
				var poshytip = $(this).data('poshytip');
402
				if (poshytip && poshytip[method])
403
					poshytip[method].apply(poshytip, args);
404
			});
405
		}
406

    
407
		var opts = $.extend({}, $.fn.poshytip.defaults, options);
408

    
409
		// generate CSS for this tip class if not already generated
410
		if (!$('#poshytip-css-' + opts.className)[0])
411
			$(['<style id="poshytip-css-',opts.className,'" type="text/css">',
412
				'div.',opts.className,'{visibility:hidden;position:absolute;top:0;left:0;}',
413
				'div.',opts.className,' table.tip-table, div.',opts.className,' table.tip-table td{margin:0;font-family:inherit;font-size:inherit;font-weight:inherit;font-style:inherit;font-variant:inherit;}',
414
				'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;}',
415
				'div.',opts.className,' td.tip-right{background-position:100% 0;}',
416
				'div.',opts.className,' td.tip-bottom{background-position:100% 100%;}',
417
				'div.',opts.className,' td.tip-left{background-position:0 100%;}',
418
				'div.',opts.className,' div.tip-inner{background-position:-',opts.bgImageFrameSize,'px -',opts.bgImageFrameSize,'px;}',
419
				'div.',opts.className,' div.tip-arrow{visibility:hidden;position:absolute;overflow:hidden;font:1px/1px sans-serif;}',
420
			'</style>'].join('')).appendTo('head');
421

    
422
		// check if we need to hook live events
423
		if (opts.liveEvents && opts.showOn != 'none') {
424
			var handler,
425
				deadOpts = $.extend({}, opts, { liveEvents: false });
426
			switch (opts.showOn) {
427
				case 'hover':
428
					handler = function() {
429
						var $this = $(this);
430
						if (!$this.data('poshytip'))
431
							$this.poshytip(deadOpts).poshytip('mouseenter');
432
					};
433
					// support 1.4.2+ & 1.9+
434
					this.live ?
435
						this.live('mouseenter.poshytip', handler) :
436
						$(document).delegate(this.selector, 'mouseenter.poshytip', handler);
437
					break;
438
				case 'focus':
439
					handler = function() {
440
						var $this = $(this);
441
						if (!$this.data('poshytip'))
442
							$this.poshytip(deadOpts).poshytip('show');
443
					};
444
					this.live ?
445
						this.live('focus.poshytip', handler) :
446
						$(document).delegate(this.selector, 'focus.poshytip', handler);
447
					break;
448
			}
449
			return this;
450
		}
451

    
452
		return this.each(function() {
453
			new $.Poshytip(this, opts);
454
		});
455
	}
456

    
457
	// default settings
458
	$.fn.poshytip.defaults = {
459
		content: 		'[title]',	// content to display ('[title]', 'string', element, function(updateCallback){...}, jQuery)
460
		className:		'tip-yellow',	// class for the tips
461
		bgImageFrameSize:	10,		// size in pixels for the background-image (if set in CSS) frame around the inner content of the tip
462
		showTimeout:		500,		// timeout before showing the tip (in milliseconds 1000 == 1 second)
463
		hideTimeout:		100,		// timeout before hiding the tip
464
		timeOnScreen:		0,		// timeout before automatically hiding the tip after showing it (set to > 0 in order to activate)
465
		showOn:			'hover',	// handler for showing the tip ('hover', 'focus', 'none') - use 'none' to trigger it manually
466
		liveEvents:		false,		// use live events
467
		alignTo:		'cursor',	// align/position the tip relative to ('cursor', 'target')
468
		alignX:			'right',	// horizontal alignment for the tip relative to the mouse cursor or the target element
469
							// ('right', 'center', 'left', 'inner-left', 'inner-right') - 'inner-*' matter if alignTo:'target'
470
		alignY:			'top',		// vertical alignment for the tip relative to the mouse cursor or the target element
471
							// ('bottom', 'center', 'top', 'inner-bottom', 'inner-top') - 'inner-*' matter if alignTo:'target'
472
		offsetX:		-22,		// offset X pixels from the default position - doesn't matter if alignX:'center'
473
		offsetY:		18,		// offset Y pixels from the default position - doesn't matter if alignY:'center'
474
		allowTipHover:		true,		// allow hovering the tip without hiding it onmouseout of the target - matters only if showOn:'hover'
475
		followCursor:		false,		// if the tip should follow the cursor - matters only if showOn:'hover' and alignTo:'cursor'
476
		fade: 			true,		// use fade animation
477
		slide: 			true,		// use slide animation
478
		slideOffset: 		8,		// slide animation offset
479
		showAniDuration: 	300,		// show animation duration - set to 0 if you don't want show animation
480
		hideAniDuration: 	300,		// hide animation duration - set to 0 if you don't want hide animation
481
		refreshAniDuration:	200		// refresh animation duration - set to 0 if you don't want animation when updating the tooltip asynchronously
482
	};
483

    
484
})(jQuery);
(1-1/2)