source: trunk/template-common/lib/plugins/jquery.cluetip.js @ 4067

Last change on this file since 4067 was 3282, checked in by plg, 15 years ago

change: according to topic:15067, svn:keywords property was removed

  • Property svn:eol-style set to LF
File size: 23.5 KB
Line 
1/*
2 * jQuery clueTip plugin
3 * Version 0.9.8  (05/22/2008)
4 * @requires jQuery v1.1.4+
5 * @requires Dimensions plugin (for jQuery versions < 1.2.5)
6 *
7 * Dual licensed under the MIT and GPL licenses:
8 * http://www.opensource.org/licenses/mit-license.php
9 * http://www.gnu.org/licenses/gpl.html
10 *
11 */
12;(function($) { 
13/*
14 * @name clueTip
15 * @type jQuery
16 * @cat Plugins/tooltip
17 * @return jQuery
18 * @author Karl Swedberg
19 *
20 * @credit Inspired by Cody Lindley's jTip (http://www.codylindley.com)
21 * @credit Thanks to the following people for their many and varied contributions:
22      Shelane Enos, Glen Lipka, Hector Santos, Torben Schreiter, Dan G. Switzer, Jörn Zaefferer
23 * @credit Thanks to Jonathan Chaffer, as always, for help with the hard parts. :-)
24 */
25
26 /**
27 *
28 * Displays a highly customizable tooltip when the user hovers (default) or clicks (optional) the matched element.
29 * By default, the clueTip plugin loads a page indicated by the "rel" attribute via ajax and displays its contents.
30 * If a "title" attribute is specified, its value is used as the clueTip's heading.
31 * The attribute to be used for both the body and the heading of the clueTip is user-configurable.
32 * Optionally, the clueTip's body can display content from an element on the same page.
33 * * Just indicate the element's id (e.g. "#some-id") in the rel attribute.
34 * Optionally, the clueTip's body can display content from the title attribute, when a delimiter is indicated.
35 * * The string before the first instance of the delimiter is set as the clueTip's heading.
36 * * All subsequent strings are wrapped in separate DIVs and placed in the clueTip's body.
37 * The clueTip plugin allows for many, many more options. Pleasee see the examples and the option descriptions below...
38 *
39 *
40 * @example $('#tip).cluetip();
41 * @desc This is the most basic clueTip. It displays a 275px-wide clueTip on mouseover of the element with an ID of "tip." On mouseout of the element, the clueTip is hidden.
42 *
43 *
44 * @example $('a.clue').cluetip({
45 *  hoverClass: 'highlight',
46 *  sticky: true,
47 *  closePosition: 'bottom',
48 *  closeText: '<img src="cross.png" alt="close" />',
49 *  truncate: 60,
50 *  ajaxSettings: {
51 *    type: 'POST'
52 *  }
53 * });
54 * @desc Displays a clueTip on mouseover of all <a> elements with class="clue". The hovered element gets a class of "highlight" added to it (so that it can be styled appropriately. This is esp. useful for non-anchor elements.). The clueTip is "sticky," which means that it will not be hidden until the user either clicks on its "close" text/graphic or displays another clueTip. The "close" text/graphic is set to diplay at the bottom of the clueTip (default is top) and display an image rather than the default "Close" text. Moreover, the body of the clueTip is truncated to the first 60 characters, which are followed by an ellipsis (...). Finally, the clueTip retrieves the content using POST rather than the $.ajax method's default "GET."
55 *
56 * More examples can be found at http://plugins.learningjquery.com/cluetip/demo/
57 *
58 * Full list of options/settings can be found at the bottom of this file and at http://plugins.learningjquery.com/cluetip/
59 */
60
61  var $cluetip, $cluetipInner, $cluetipOuter, $cluetipTitle, $cluetipArrows, $dropShadow, imgCount;
62  $.fn.cluetip = function(js, options) {
63    if (typeof js == 'object') {
64      options = js;
65      js = null;
66    }
67    return this.each(function(index) {
68      var $this = $(this);     
69     
70      // support metadata plugin (v1.0 and 2.0)
71      var opts = $.extend(false, {}, $.fn.cluetip.defaults, options || {}, $.metadata ? $this.metadata() : $.meta ? $this.data() : {});
72
73      // start out with no contents (for ajax activation)
74      var cluetipContents = false;
75      var cluezIndex = parseInt(opts.cluezIndex, 10)-1;
76      var isActive = false, closeOnDelay = 0;
77
78      // create the cluetip divs
79      if (!$('#cluetip').length) {
80        $cluetipInner = $('<div id="cluetip-inner"></div>');
81        $cluetipTitle = $('<h3 id="cluetip-title"></h3>');       
82        $cluetipOuter = $('<div id="cluetip-outer"></div>').append($cluetipInner).prepend($cluetipTitle);
83        $cluetip = $('<div id="cluetip"></div>').css({zIndex: opts.cluezIndex})
84        .append($cluetipOuter).append('<div id="cluetip-extra"></div>')[insertionType](insertionElement).hide();
85        $('<div id="cluetip-waitimage"></div>').css({position: 'absolute', zIndex: cluezIndex-1})
86        .insertBefore('#cluetip').hide();
87        $cluetip.css({position: 'absolute', zIndex: cluezIndex});
88        $cluetipOuter.css({position: 'relative', zIndex: cluezIndex+1});
89        $cluetipArrows = $('<div id="cluetip-arrows" class="cluetip-arrows"></div>').css({zIndex: cluezIndex+1}).appendTo('#cluetip');
90      }
91      var dropShadowSteps = (opts.dropShadow) ? +opts.dropShadowSteps : 0;
92      if (!$dropShadow) {
93        $dropShadow = $([]);
94        for (var i=0; i < dropShadowSteps; i++) {
95          $dropShadow = $dropShadow.add($('<div></div>').css({zIndex: cluezIndex-i-1, opacity:.1, top: 1+i, left: 1+i}));
96        };
97        $dropShadow.css({position: 'absolute', backgroundColor: '#000'})
98        .prependTo($cluetip);
99      }
100      var tipAttribute = $this.attr(opts.attribute), ctClass = opts.cluetipClass;
101      if (!tipAttribute && !opts.splitTitle && !js) return true;
102      // if hideLocal is set to true, on DOM ready hide the local content that will be displayed in the clueTip     
103      if (opts.local && opts.hideLocal) { $(tipAttribute + ':first').hide(); }
104      var tOffset = parseInt(opts.topOffset, 10), lOffset = parseInt(opts.leftOffset, 10);
105      // vertical measurement variables
106      var tipHeight, wHeight;
107      var defHeight = isNaN(parseInt(opts.height, 10)) ? 'auto' : (/\D/g).test(opts.height) ? opts.height : opts.height + 'px';
108      var sTop, linkTop, posY, tipY, mouseY, baseline;
109      // horizontal measurement variables
110      var tipInnerWidth = isNaN(parseInt(opts.width, 10)) ? 275 : parseInt(opts.width, 10);
111      var tipWidth = tipInnerWidth + (parseInt($cluetip.css('paddingLeft'))||0) + (parseInt($cluetip.css('paddingRight'))||0) + dropShadowSteps;
112      var linkWidth = this.offsetWidth;
113      var linkLeft, posX, tipX, mouseX, winWidth;
114           
115      // parse the title
116      var tipParts;
117      var tipTitle = (opts.attribute != 'title') ? $this.attr(opts.titleAttribute) : '';
118      if (opts.splitTitle) {
119        if(tipTitle == undefined) {tipTitle = '';}
120        tipParts = tipTitle.split(opts.splitTitle);
121        tipTitle = tipParts.shift();
122      }
123      var localContent;
124
125/***************************************     
126* ACTIVATION
127****************************************/
128   
129//activate clueTip
130    var activate = function(event) {
131      if (!opts.onActivate($this)) {
132        return false;
133      }
134      isActive = true;
135      $cluetip.removeClass().css({width: tipInnerWidth});
136      if (tipAttribute == $this.attr('href')) {
137        $this.css('cursor', opts.cursor);
138      }
139      $this.attr('title','');
140      if (opts.hoverClass) {
141        $this.addClass(opts.hoverClass);
142      }
143      linkTop = posY = $this.offset().top;
144      linkLeft = $this.offset().left;
145      mouseX = event.pageX;
146      mouseY = event.pageY;
147      if ($this[0].tagName.toLowerCase() != 'area') {
148        sTop = $(document).scrollTop();
149        winWidth = $(window).width();
150      }
151// position clueTip horizontally
152      if (opts.positionBy == 'fixed') {
153        posX = linkWidth + linkLeft + lOffset;
154        $cluetip.css({left: posX});
155      } else {
156        posX = (linkWidth > linkLeft && linkLeft > tipWidth)
157          || linkLeft + linkWidth + tipWidth + lOffset > winWidth 
158          ? linkLeft - tipWidth - lOffset 
159          : linkWidth + linkLeft + lOffset;
160        if ($this[0].tagName.toLowerCase() == 'area' || opts.positionBy == 'mouse' || linkWidth + tipWidth > winWidth) { // position by mouse
161          if (mouseX + 20 + tipWidth > winWidth) { 
162            $cluetip.addClass(' cluetip-' + ctClass);
163            posX = (mouseX - tipWidth - lOffset) >= 0 ? mouseX - tipWidth - lOffset - parseInt($cluetip.css('marginLeft'),10) + parseInt($cluetipInner.css('marginRight'),10) :  mouseX - (tipWidth/2);
164          } else {
165            posX = mouseX + lOffset;
166          }
167        }
168        var pY = posX < 0 ? event.pageY + tOffset : event.pageY;
169        $cluetip.css({left: (posX > 0 && opts.positionBy != 'bottomTop') ? posX : (mouseX + (tipWidth/2) > winWidth) ? winWidth/2 - tipWidth/2 : Math.max(mouseX - (tipWidth/2),0)});
170      }
171        wHeight = $(window).height();
172
173/***************************************
174* load a string from cluetip method's first argument
175***************************************/
176      if (js) {
177        $cluetipInner.html(js);
178        cluetipShow(pY);
179      }
180/***************************************
181* load the title attribute only (or user-selected attribute).
182* clueTip title is the string before the first delimiter
183* subsequent delimiters place clueTip body text on separate lines
184***************************************/
185
186      else if (tipParts) {
187        var tpl = tipParts.length;
188        for (var i=0; i < tpl; i++){
189          if (i == 0) {
190            $cluetipInner.html(tipParts[i]);
191          } else { 
192            $cluetipInner.append('<div class="split-body">' + tipParts[i] + '</div>');
193          }           
194        };
195        cluetipShow(pY);
196      }
197/***************************************
198* load external file via ajax         
199***************************************/
200
201      else if (!opts.local && tipAttribute.indexOf('#') != 0) {
202        if (cluetipContents && opts.ajaxCache) {
203          $cluetipInner.html(cluetipContents);
204          cluetipShow(pY);
205        }
206        else {
207          var ajaxSettings = opts.ajaxSettings;
208          ajaxSettings.url = tipAttribute;
209          ajaxSettings.beforeSend = function() {
210            $cluetipOuter.children().empty();
211            if (opts.waitImage) {
212              $('#cluetip-waitimage')
213              .css({top: mouseY+20, left: mouseX+20})
214              .show();
215            }
216          };
217         ajaxSettings.error = function() {
218            if (isActive) {
219              $cluetipInner.html('<i>sorry, the contents could not be loaded</i>');
220            }
221          };
222          ajaxSettings.success = function(data) {
223            cluetipContents = opts.ajaxProcess(data);
224            if (isActive) {
225              $cluetipInner.html(cluetipContents);
226            }
227          };
228          ajaxSettings.complete = function() {
229                imgCount = $('#cluetip-inner img').length;
230                        if (imgCount && !$.browser.opera) {
231                          $('#cluetip-inner img').load(function() {
232                                imgCount--;
233                                if (imgCount<1) {
234                                        $('#cluetip-waitimage').hide();
235                                  if (isActive) cluetipShow(pY);
236                                }
237                          }); 
238                        } else {
239                                $('#cluetip-waitimage').hide();
240                          if (isActive) cluetipShow(pY);   
241                        } 
242          };
243          $.ajax(ajaxSettings);
244        }
245
246/***************************************
247* load an element from the same page
248***************************************/
249      } else if (opts.local){
250        var $localContent = $(tipAttribute + ':first');
251        var localCluetip = $.fn.wrapInner ? $localContent.wrapInner('<div></div>').children().clone(true) : $localContent.html();
252        $.fn.wrapInner ? $cluetipInner.empty().append(localCluetip) : $cluetipInner.html(localCluetip);
253        cluetipShow(pY);
254      }
255    };
256
257// get dimensions and options for cluetip and prepare it to be shown
258    var cluetipShow = function(bpY) {
259      $cluetip.addClass('cluetip-' + ctClass);
260     
261      if (opts.truncate) { 
262        var $truncloaded = $cluetipInner.text().slice(0,opts.truncate) + '...';
263        $cluetipInner.html($truncloaded);
264      }
265      function doNothing() {}; //empty function
266      tipTitle ? $cluetipTitle.show().html(tipTitle) : (opts.showTitle) ? $cluetipTitle.show().html('&nbsp;') : $cluetipTitle.hide();
267      if (opts.sticky) {
268        var $closeLink = $('<div id="cluetip-close"><a href="#">' + opts.closeText + '</a></div>');
269        (opts.closePosition == 'bottom') ? $closeLink.appendTo($cluetipInner) : (opts.closePosition == 'title') ? $closeLink.prependTo($cluetipTitle) : $closeLink.prependTo($cluetipInner);
270        $closeLink.click(function() {
271          cluetipClose();
272          return false;
273        });
274        if (opts.mouseOutClose) {
275          if ($.fn.hoverIntent && opts.hoverIntent) { 
276            $cluetip.hoverIntent({
277              over: doNothing, 
278              timeout: opts.hoverIntent.timeout, 
279              out: function() { $closeLink.trigger('click'); }
280            });
281          } else {
282            $cluetip.hover(doNothing, 
283            function() {$closeLink.trigger('click'); });
284          }
285        } else {
286          $cluetip.unbind('mouseout');
287        }
288      }
289// now that content is loaded, finish the positioning
290      var direction = '';
291      $cluetipOuter.css({overflow: defHeight == 'auto' ? 'visible' : 'auto', height: defHeight});
292      tipHeight = defHeight == 'auto' ? Math.max($cluetip.outerHeight(),$cluetip.height()) : parseInt(defHeight,10);   
293      tipY = posY;
294      baseline = sTop + wHeight;
295      if (opts.positionBy == 'fixed') {
296        tipY = posY - opts.dropShadowSteps + tOffset;
297      } else if ( (posX < mouseX && Math.max(posX, 0) + tipWidth > mouseX) || opts.positionBy == 'bottomTop') {
298        if (posY + tipHeight + tOffset > baseline && mouseY - sTop > tipHeight + tOffset) { 
299          tipY = mouseY - tipHeight - tOffset;
300          direction = 'top';
301        } else { 
302          tipY = mouseY + tOffset;
303          direction = 'bottom';
304        }
305      } else if ( posY + tipHeight + tOffset > baseline ) {
306        tipY = (tipHeight >= wHeight) ? sTop : baseline - tipHeight - tOffset;
307      } else if ($this.css('display') == 'block' || $this[0].tagName.toLowerCase() == 'area' || opts.positionBy == "mouse") {
308        tipY = bpY - tOffset;
309      } else {
310        tipY = posY - opts.dropShadowSteps;
311      }
312      if (direction == '') {
313        posX < linkLeft ? direction = 'left' : direction = 'right';
314      }
315      $cluetip.css({top: tipY + 'px'}).removeClass().addClass('clue-' + direction + '-' + ctClass).addClass(' cluetip-' + ctClass);
316      if (opts.arrows) { // set up arrow positioning to align with element
317        var bgY = (posY - tipY - opts.dropShadowSteps);
318        $cluetipArrows.css({top: (/(left|right)/.test(direction) && posX >=0 && bgY > 0) ? bgY + 'px' : /(left|right)/.test(direction) ? 0 : ''}).show();
319      } else {
320        $cluetipArrows.hide();
321      }
322
323// (first hide, then) ***SHOW THE CLUETIP***
324      $dropShadow.hide();
325      $cluetip.hide()[opts.fx.open](opts.fx.open != 'show' && opts.fx.openSpeed);
326      if (opts.dropShadow) $dropShadow.css({height: tipHeight, width: tipInnerWidth}).show();
327      if ($.fn.bgiframe) { $cluetip.bgiframe(); }
328      // trigger the optional onShow function
329      if (opts.delayedClose > 0) {
330        closeOnDelay = setTimeout(cluetipClose, opts.delayedClose);
331      }
332      opts.onShow($cluetip, $cluetipInner);
333     
334    };
335
336/***************************************
337   =INACTIVATION
338-------------------------------------- */
339    var inactivate = function() {
340      isActive = false;
341      $('#cluetip-waitimage').hide();
342      if (!opts.sticky || (/click|toggle/).test(opts.activation) ) {
343        cluetipClose();
344clearTimeout(closeOnDelay);       
345      };
346      if (opts.hoverClass) {
347        $this.removeClass(opts.hoverClass);
348      }
349      $('.cluetip-clicked').removeClass('cluetip-clicked');
350    };
351// close cluetip and reset some things
352    var cluetipClose = function() {
353      $cluetipOuter 
354      .parent().hide().removeClass().end()
355      .children().empty();
356      if (tipTitle) {
357        $this.attr(opts.titleAttribute, tipTitle);
358      }
359      $this.css('cursor','');
360      if (opts.arrows) $cluetipArrows.css({top: ''});
361    };
362
363/***************************************
364   =BIND EVENTS
365-------------------------------------- */
366  // activate by click
367      if ( (/click|toggle/).test(opts.activation) ) {
368        $this.click(function(event) {
369          if ($cluetip.is(':hidden') || !$this.is('.cluetip-clicked')) {
370            activate(event);
371            $('.cluetip-clicked').removeClass('cluetip-clicked');
372            $this.addClass('cluetip-clicked');
373
374          } else {
375            inactivate(event);
376
377          }
378          this.blur();
379          return false;
380        });
381  // activate by focus; inactivate by blur   
382      } else if (opts.activation == 'focus') {
383        $this.focus(function(event) {
384          activate(event);
385        });
386        $this.blur(function(event) {
387          inactivate(event);
388        });
389  // activate by hover
390    // clicking is returned false if cluetip url is same as href url
391      } else {
392        $this.click(function() {
393          if ($this.attr('href') && $this.attr('href') == tipAttribute && !opts.clickThrough) {
394            return false;
395          }
396        });
397        //set up mouse tracking
398        var mouseTracks = function(evt) {
399          if (opts.tracking == true) {
400            var trackX = posX - evt.pageX;
401            var trackY = tipY ? tipY - evt.pageY : posY - evt.pageY;
402            $this.mousemove(function(evt) {
403              $cluetip.css({left: evt.pageX + trackX, top: evt.pageY + trackY });
404            });
405          }
406        };
407        if ($.fn.hoverIntent && opts.hoverIntent) {
408          $this.mouseover(function() {$this.attr('title',''); })
409          .hoverIntent({
410            sensitivity: opts.hoverIntent.sensitivity,
411            interval: opts.hoverIntent.interval, 
412            over: function(event) {
413              activate(event);
414              mouseTracks(event);
415            }, 
416            timeout: opts.hoverIntent.timeout, 
417            out: function(event) {inactivate(event); $this.unbind('mousemove');}
418          });           
419        } else {
420          $this.hover(function(event) {
421            activate(event);
422            mouseTracks(event);
423          }, function(event) {
424            inactivate(event);
425            $this.unbind('mousemove');
426          });
427        }
428      }
429    });
430  };
431 
432/*
433 * options for clueTip
434 *
435 * each one can be explicitly overridden by changing its value.
436 * for example: $.fn.cluetip.defaults.width = 200;
437 * would change the default width for all clueTips to 200.
438 *
439 * each one can also be overridden by passing an options map to the cluetip method.
440 * for example: $('a.example').cluetip({width: 200});
441 * would change the default width to 200 for clueTips invoked by a link with class of "example"
442 *
443 */
444 
445  $.fn.cluetip.defaults = {  // set up default options
446    width:            275,      // The width of the clueTip
447    height:           'auto',   // The height of the clueTip
448    cluezIndex:       97,       // Sets the z-index style property of the clueTip
449    positionBy:       'auto',   // Sets the type of positioning: 'auto', 'mouse','bottomTop', 'fixed'
450    topOffset:        15,       // Number of px to offset clueTip from top of invoking element
451    leftOffset:       15,       // Number of px to offset clueTip from left of invoking element
452    local:            false,    // Whether to use content from the same page for the clueTip's body
453    hideLocal:        true,     // If local option is set to true, this determines whether local content
454                                // to be shown in clueTip should be hidden at its original location
455    attribute:        'rel',    // the attribute to be used for fetching the clueTip's body content
456    titleAttribute:   'title',  // the attribute to be used for fetching the clueTip's title
457    splitTitle:       '',       // A character used to split the title attribute into the clueTip title and divs
458                                // within the clueTip body. more info below [6]
459    showTitle:        true,     // show title bar of the clueTip, even if title attribute not set
460    cluetipClass:     'default',// class added to outermost clueTip div in the form of 'cluetip-' + clueTipClass.
461    hoverClass:       '',       // class applied to the invoking element onmouseover and removed onmouseout
462    waitImage:        true,     // whether to show a "loading" img, which is set in jquery.cluetip.css
463    cursor:           'help',
464    arrows:           false,    // if true, displays arrow on appropriate side of clueTip
465    dropShadow:       true,     // set to false if you don't want the drop-shadow effect on the clueTip
466    dropShadowSteps:  6,        // adjusts the size of the drop shadow
467    sticky:           false,    // keep visible until manually closed
468    mouseOutClose:    false,    // close when clueTip is moused out
469    activation:       'hover',  // set to 'click' to force user to click to show clueTip
470                                // set to 'focus' to show on focus of a form element and hide on blur
471    clickThrough:     false,    // if true, and activation is not 'click', then clicking on link will take user to the link's href,
472                                // even if href and tipAttribute are equal
473    tracking:         false,    // if true, clueTip will track mouse movement (experimental)
474    delayedClose:     0,        // close clueTip on a timed delay (experimental)
475    closePosition:    'top',    // location of close text for sticky cluetips; can be 'top' or 'bottom' or 'title'
476    closeText:        'Close',  // text (or HTML) to to be clicked to close sticky clueTips
477    truncate:         0,        // number of characters to truncate clueTip's contents. if 0, no truncation occurs
478
479    // effect and speed for opening clueTips
480    fx: {             
481                      open:       'show', // can be 'show' or 'slideDown' or 'fadeIn'
482                      openSpeed:  ''
483    },     
484
485    // settings for when hoverIntent plugin is used             
486    hoverIntent: {   
487                      sensitivity:  3,
488                                  interval:     50,
489                                  timeout:      0
490    },
491
492    // function to run just before clueTip is shown.           
493    onActivate:       function(e) {return true;},
494
495    // function to run just after clueTip is shown.
496    onShow:           function(ct, c){},
497   
498    // whether to cache results of ajax request to avoid unnecessary hits to server   
499    ajaxCache:        true, 
500
501    // process data retrieved via xhr before it's displayed
502    ajaxProcess:      function(data) {
503                        data = data.replace(/<s(cript|tyle)(.|\s)*?\/s(cript|tyle)>/g, '').replace(/<(link|title)(.|\s)*?\/(link|title)>/g,'');
504                        return data;
505    },               
506
507    // can pass in standard $.ajax() parameters, not including error, complete, success, and url
508    ajaxSettings: {   
509                      dataType: 'html'
510    },
511    debug: false
512  };
513
514
515/*
516 * Global defaults for clueTips. Apply to all calls to the clueTip plugin.
517 *
518 * @example $.cluetip.setup({
519 *   insertionType: 'prependTo',
520 *   insertionElement: '#container'
521 * });
522 *
523 * @property
524 * @name $.cluetip.setup
525 * @type Map
526 * @cat Plugins/tooltip
527 * @option String insertionType: Default is 'appendTo'. Determines the method to be used for inserting the clueTip into the DOM. Permitted values are 'appendTo', 'prependTo', 'insertBefore', and 'insertAfter'
528 * @option String insertionElement: Default is 'body'. Determines which element in the DOM the plugin will reference when inserting the clueTip.
529 *
530 */
531   
532  var insertionType = 'appendTo', insertionElement = 'body';
533  $.cluetip = {};
534  $.cluetip.setup = function(options) {
535    if (options && options.insertionType && (options.insertionType).match(/appendTo|prependTo|insertBefore|insertAfter/)) {
536      insertionType = options.insertionType;
537    }
538    if (options && options.insertionElement) {
539      insertionElement = options.insertionElement;
540    }
541  };
542 
543})(jQuery);
Note: See TracBrowser for help on using the repository browser.