Making a click-for-help window with jQuery’s tooltip widget

This post was written by eli on February 6, 2013
Posted Under: Drupal,Internet,JavaScript,Server admin,Software

Intro

I needed some neat overlay to appear when the user clicks those question-mark icons on a specific, Drupal-based page. So I went for using jQuery’s tooltip widget, with due adaptations. It turned out to be a wise choice.

The idea is simple: Click on the question mark, an overlay window appears right underneath it, and stays there. Click on the “X” at the window’s top right, the window closes. Simple and clean.

These are my notes as I set this up. Most of the work is around the fact that tooltips usually appear and disappear on mouse hovering, not clicks.

Getting a bundle for a tooltip

Build your own set and download on jQuery’s official site. For a plain tooltip activation, pick the following options: All UI Core, no Interactions. Widgets: Only Tooltip. Effects: Only Effects Core and Fade Effect.

From the downloaded bundle, the following inclusions (in the <head> section) suffice (version numbers may vary):

<style>@import url("jquery-ui-1.10.0.custom.css");</style>

and

<script type="text/javascript" src="jquery-1.9.0.js"></script>
<script type="text/javascript" src="jquery-ui-1.10.0.custom.min.js"></script>

The “.min” version is just a condensed version of the non-min JavaScript file. The rest of the bundle can be ignored for a minimal tooltip implementation.

In a Drupal environment

Since Drupal uses jQuery as well, the initial idea was to rely on the script loaded anyhow, but I got the “$ is not a function” error in the error console. To solve this, the script inclusions shown above should appear after Drupal’s. In theory, the jQuery.noConflict() call should be used to save the previous “$” function, but since Drupal doesn’t inject jQuery calls after its last declaration in <head>, there is no need for this. This is OK for me, since I work on one specific page, but it’s theoretically possible that other Drupal pages put JavaScript in the <body>. So keep this in mind.

Maybe the best way is to load your own jQuery script bundle before Drupal’s, run noConflict() and then load Drupal’s. Just to be safe.

IE6 note

Internet Explorer 6 is an ancient thing, but since I always check against antique browsers, I noted that if there’s a selection box under the tooltip, it will appear above the tooltip, despite the latter’s z-index set to 9999. This is a bug specific to IE6, and therefore nothing to worry about too much. Anyone with IE6 is used to pages appearing ugly anyhow.

Declaring the jQuery handlers and hooks

This is a general note on how to inject jQuery definitions into the HTML.

Following jQuery’s recommendation, the application-specific definitions are made in the <head> section, in an anonymous function which is triggered by the “ready” event. This makes the jQuery hooks up and running as soon as the DOM hierarchy is set up, but doesn’t requires the onload event, which is triggered only when all assets (e.g. images) have been loaded.

So to run a tooltip on all elements marked “myclass” (they need to have a title= assignment),

<script>
$(function() {
 $( ".myclass" ).tooltip();
});
</script>

The hooking to the “ready” event is implicit by the $(function() { … }) declaration.

Appear on click, don’t disappear

To make a “Hello world” tooltip appear on clicking an image of class “helpimg”, this goes into the <head> section (partly taken from here):

<script>
$(function() {
 $( ".helpimg" ).tooltip({
   content: "<i>Hello, world</i>",
   items: 'img'
 })
 .off( "mouseover" )
 .on( "click", function(){
    var clicked = this;
    $( ".helpimg" ).each(function (i) {
      if (this != clicked) {
         $( this ).tooltip( "close" );
      }
    });

    $( this ).tooltip( "open" );
    $( this ).unbind( "mouseleave" );
    return false;
 });

 $( "body" ).on( "click", ".helpclose", function(){
   $( ".helpimg" ).each(function (i) { $( this ).tooltip( "close" ); });
   return false;
 });
});
</script>

This little script does two things: Assign a tooltip object for each DOM element of class “helpimg” (for opening the tooltip) and an event handler to each “helpclose” element (for closing it).

See the tooltip API reference for the meanings of the attributes (content, item etc.).

Open the tooltip (.helpimg assignment)

The “content” attribute obviously sets the tooltip’s content. Here it’s given as a string itself, which is OK if the data is known when the tooltip is generated. Another possibility is to put a function here. Unlike what some documentation implies, there is no bodyHandler attribute (on jquery-ui-1.10.0). To provide a callback, a function is given instead of the string. For example, to put the ID of the element clicked on in the tooltip box, it should say (replacing the content: assignment above):

content: function() {
  return $( this ).attr( "id" );
},

The “item” attribute defines the item type(s) involved. If this is absent, the click operation works only once: After the tooltip fades away, another click on the same item do nothing. Frankly, I don’t understand why this is.

The anonymous function defined in the .on() call is called when the element is clicked. Its first part (the definition of “clicked” and the “each” call) scans all elements of class “helpimg” and sends them a “close” command. This part is optional, to maintain an exclusive tooltip open. The if-statement in the each-function prevents closing the tooltip that is just about to be opened, to prevent flickering. Maybe it’s better to let the flickering happen, so that the user gets some feedback on the click. A matter of taste, I suppose.

Note that the function defined in the “each” call has a variable, which is the index. It’s not used in this case.

The call to unbind() (marked in red) kills the “mouseleave” event handler(s) that are related to the tooltip, so it doesn’t listen to this event. Normally, the “open” method sets an event handler for either “mouseleave” or “focusout”, depending on whether the triggering event was “mouseover” or “focusin”, respectively.

There is a non-specific call to unbind(), which makes the tooltip deaf to any event, but that made the tooltip appear only on the first click and then not again. So obviously there are some events handlers that should be left (maybe timers?).

Closing the tooltip

Since the tooltip won’t close by itself, this must be done by clicking on some element. The dedicated elements (actually, top-right “X” boxes) are given the “helpclose” class. When one of those elements get the “onclick” event, all tooltips are closed, with a function identical to the one used before opening a tooltip.

The important difference is the selection of these elements. Since they are hidden when the script is run, selecting them with $( “.xillyclose”).on( “click”, …) will catch nothing. The solution is to delegate the event to the “body” object, which is of course visible, and push the selector to within the .on() call. This way, the to-be visible elements are selected as they appear.

Closing on ESC keystroke

I found myself hitting ESC to close a tooltip, so why not support it? The idea is to catch keyup events, check if the key code happens to be 27 (ESC), and act as necessary.

$( document ).on( "keyup", function(event){
 if (event.keyCode == 27) {
 $( ".helpimg" ).each(function (i) { $( this ).tooltip( "close" ); });
 return false;     
}

I found the key code for ESC by running the demo on the .keyup() API page. Note that the .keyup() command is just a shorthand for the .on() call used above.

Position the tooltip

To get the tooltip properly positioned, the position property is set up during initialization. The declaration above should therefore start as

$(function() {
  $( ".helpimg" ).tooltip({
    content: "<i>Hello, world</i>",
     items: 'img'
     position: {
       my: "center top",
       at: "center bottom+5",
       collision: "fit",
       within: "#content"
     }
[ ... ]

To avoid confusion, let’s think about the tooltip as a piece of paper that we want to nail on a billboard.

As described in the docs, the “my” property says where the on the tooltip we should place that nail (on the horizontal center and top of the tooltip), and the “at” property says where we should stick that nail (the horizontal center and bottom of the object is relates to, just 5 pixels lower).

The “collision property” tells what to do when the positioning would make the tooltip exceed the given area (“fit” means push it away from the border as necessary, but don’t throw it to the opposite side). “within” defines the framework in which the tooltip should be placed (the id=”content”, which is the main context box, defined by Drupal).

Conclusion

It took some time to get the hang of it, but all in all, jQuery is worth it, in particular by making the application code compact and therefore less bug prone.

It also handles those corner cases gracefully and takes care of browser compatibility. After all, what’s good for Wikipedia is good for me.

Reader Comments

This is a great tutorial, I’ve spend 3 days to find the solution to make the tooltip to appear and close on the same element.
The only suggestion is to create a working demo http://jsfiddle.net/vScw2/12/

#1 
Written By Manicko on December 16th, 2013 @ 10:25

Super, thanks for the click to view Tooltip code!

#2 
Written By Nob on December 3rd, 2014 @ 00:12

Add a Comment

required, use real name
required, will not be published
optional, your blog address