1 // tipsy, facebook style tooltips for jquery
3 // (c) 2008-2010 jason frame [jason@onehackoranother.com]
4 // released under the MIT license
6 // * This installation of tipsy includes several local modifications to both Javascript and CSS.
7 // Please be careful when upgrading.
11 function maybeCall(thing
, ctx
) {
12 return (typeof thing
== 'function') ? (thing
.call(ctx
)) : thing
;
15 function Tipsy(element
, options
) {
16 this.$element
= $(element
);
17 this.options
= options
;
19 this.keyHandler
= $.proxy( this.closeOnEsc
, this );
25 var title
= this.getTitle();
26 if (title
&& this.enabled
) {
27 var $tip
= this.tip();
29 $tip
.find('.tipsy-inner')[this.options
.html
? 'html' : 'text'](title
);
30 $tip
[0].className
= 'tipsy'; // reset classname in case of dynamic gravity
31 if (this.options
.className
) {
32 $tip
.addClass(maybeCall(this.options
.className
, this.$element
[0]));
35 .css({top
: 0, left
: 0, visibility
: 'hidden', display
: 'block'})
36 .attr( 'aria-hidden', 'true' )
37 .appendTo(document
.body
);
39 var pos
= $.extend({}, this.$element
.offset(), {
40 width
: this.$element
[0].offsetWidth
,
41 height
: this.$element
[0].offsetHeight
44 var gravity
= (typeof this.options
.gravity
== 'function')
45 ? this.options
.gravity
.call(this.$element
[0])
46 : this.options
.gravity
;
48 // Attach css classes before checking height/width so they
50 $tip
.addClass('tipsy-' + gravity
);
51 if (this.options
.className
) {
52 $tip
.addClass(maybeCall(this.options
.className
, this.$element
[0]));
55 var actualWidth
= $tip
[0].offsetWidth
, actualHeight
= $tip
[0].offsetHeight
;
57 switch (gravity
.charAt(0)) {
59 tp
= {top
: pos
.top
+ pos
.height
+ this.options
.offset
, left
: pos
.left
+ pos
.width
/ 2 - actualWidth
/ 2};
62 tp
= {top
: pos
.top
- actualHeight
- this.options
.offset
, left
: pos
.left
+ pos
.width
/ 2 - actualWidth
/ 2};
65 tp
= {top
: pos
.top
+ pos
.height
/ 2 - actualHeight
/ 2, left
: pos
.left
- actualWidth
- this.options
.offset
};
68 tp
= {top
: pos
.top
+ pos
.height
/ 2 - actualHeight
/ 2, left
: pos
.left
+ pos
.width
+ this.options
.offset
};
72 if (gravity
.length
== 2) {
73 if (gravity
.charAt(1) == 'w') {
74 if (this.options
.center
) {
75 tp
.left
= pos
.left
+ pos
.width
/ 2 - 15;
80 if (this.options
.center
) {
81 tp
.left
= pos
.left
+ pos
.width
/ 2 - actualWidth
+ 15;
83 tp
.left
= pos
.left
+ pos
.width
;
89 $( document
).on( 'keydown', this.keyHandler
);
90 if (this.options
.fade
) {
92 .css({opacity
: 0, display
: 'block', visibility
: 'visible'})
93 .attr( 'aria-hidden', 'false' )
94 .animate({opacity
: this.options
.opacity
}, 100);
97 .css({visibility
: 'visible', opacity
: this.options
.opacity
})
98 .attr( 'aria-hidden', 'false' );
104 $( document
).off( 'keydown', this.keyHandler
);
105 if (this.options
.fade
) {
106 this.tip().stop().fadeOut(100, function() { $(this).remove(); });
113 fixTitle: function() {
114 var $e
= this.$element
;
115 if ($e
.attr('title') || typeof($e
.attr('original-title')) != 'string') {
116 $e
.attr('original-title', $e
.attr('title') || '').removeAttr('title');
120 getTitle: function() {
121 var title
, $e
= this.$element
, o
= this.options
;
123 if (typeof o
.title
== 'string') {
124 title
= $e
.attr(o
.title
== 'title' ? 'original-title' : o
.title
);
125 } else if (typeof o
.title
== 'function') {
126 title
= o
.title
.call($e
[0]);
128 title
= ('' + title
).replace(/(^\s*|\s*$)/, "");
129 return title
|| o
.fallback
;
134 this.$tip
= $('<div class="tipsy" role="tooltip"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>');
139 validate: function() {
140 if (!this.$element
[0].parentNode
) {
142 this.$element
= null;
147 // $.proxy event handler
148 closeOnEsc: function ( e
) {
149 if ( e
.keyCode
=== 27 ) {
154 enable: function() { this.enabled
= true; },
155 disable: function() { this.enabled
= false; },
156 toggleEnabled: function() { this.enabled
= !this.enabled
; }
159 $.fn
.tipsy = function(options
) {
161 if (options
=== true) {
162 return this.data('tipsy');
163 } else if (typeof options
== 'string') {
164 var tipsy
= this.data('tipsy');
165 if (tipsy
) tipsy
[options
]();
169 options
= $.extend({}, $.fn
.tipsy
.defaults
, options
);
172 var tipsy
= $.data(ele
, 'tipsy');
174 tipsy
= new Tipsy(ele
, $.fn
.tipsy
.elementOptions(ele
, options
));
175 $.data(ele
, 'tipsy', tipsy
);
181 var tipsy
= get(this);
182 tipsy
.hoverState
= 'in';
183 if (options
.delayIn
== 0) {
187 setTimeout(function() { if (tipsy
.hoverState
== 'in') tipsy
.show(); }, options
.delayIn
);
192 var tipsy
= get(this);
193 tipsy
.hoverState
= 'out';
194 if (options
.delayOut
== 0) {
197 setTimeout(function() { if (tipsy
.hoverState
== 'out') tipsy
.hide(); }, options
.delayOut
);
201 if (!options
.live
) this.each(function() { get(this); });
203 if ( options
.trigger
!= 'manual' ) {
204 var eventIn
= options
.trigger
== 'hover' ? 'mouseenter focus' : 'focus',
205 eventOut
= options
.trigger
== 'hover' ? 'mouseleave blur' : 'blur';
206 if ( options
.live
) {
207 mw
.track( 'mw.deprecate', 'tipsy-live' );
208 mw
.log
.warn( 'Use of the "live" option of jquery.tipsy is deprecated.' );
209 // XXX: The official status of 'context' is deprecated, and the official status of
210 // 'selector' is removed, so this really needs to go.
212 .on( eventIn
, this.selector
, enter
)
213 .on( eventOut
, this.selector
, leave
);
216 .on( eventIn
, enter
)
217 .on( eventOut
, leave
);
225 $.fn
.tipsy
.defaults
= {
241 // Overwrite this method to provide options on a per-element basis.
242 // For example, you could store the gravity in a 'tipsy-gravity' attribute:
243 // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' });
244 // (remember - do not modify 'options' in place!)
245 $.fn
.tipsy
.elementOptions = function(ele
, options
) {
246 return $.metadata
? $.extend({}, options
, $(ele
).metadata()) : options
;
249 $.fn
.tipsy
.autoNS = function() {
250 return $(this).offset().top
> ($(document
).scrollTop() + $(window
).height() / 2) ? 's' : 'n';
253 $.fn
.tipsy
.autoWE = function() {
254 return $(this).offset().left
> ($(document
).scrollLeft() + $(window
).width() / 2) ? 'e' : 'w';
258 * yields a closure of the supplied parameters, producing a function that takes
259 * no arguments and is suitable for use as an autogravity function like so:
261 * @param margin (int) - distance from the viewable region edge that an
262 * element should be before setting its tooltip's gravity to be away
264 * @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer
265 * if there are no viewable region edges effecting the tooltip's
266 * gravity. It will try to vary from this minimally, for example,
267 * if 'sw' is preferred and an element is near the right viewable
268 * region edge, but not the top edge, it will set the gravity for
269 * that element's tooltip to be 'se', preserving the southern
272 $.fn
.tipsy
.autoBounds = function(margin
, prefer
) {
274 var dir
= {ns
: prefer
[0], ew
: (prefer
.length
> 1 ? prefer
[1] : false)},
275 boundTop
= $(document
).scrollTop() + margin
,
276 boundLeft
= $(document
).scrollLeft() + margin
,
279 if ($this.offset().top
< boundTop
) dir
.ns
= 'n';
280 if ($this.offset().left
< boundLeft
) dir
.ew
= 'w';
281 if ($(window
).width() + $(document
).scrollLeft() - $this.offset().left
< margin
) dir
.ew
= 'e';
282 if ($(window
).height() + $(document
).scrollTop() - $this.offset().top
< margin
) dir
.ns
= 's';
284 return dir
.ns
+ (dir
.ew
? dir
.ew
: '');
288 }( mediaWiki
, jQuery
) );