Flip your Tip: Keeping the Event-delegation Tooltip in View

Before we begin, please accept my apologies for not posting this tutorial sooner. I know at least two or three people were beginning to wonder if I'd ever finish what I started with this tooltip series. Please also forgive me if the phrase "flip your tip" has a double meaning in some ultra-hip corner of the universe. If it does, I can assure you that I am unaware of it—ignorant and unhip, to be sure, but more important, innocent. Now, on with the show.

Quick Review

In my last three tutorials, I discussed how to put together a very simple tooltip, and I introduced a different feature or concept in each one. In an effort to continue in the spirit of simplicity, I will refrain from repeating the explanations of previous posts and instead simply direct your attention to them before we begin:


Now I'd like to round off this series with a little exploration of one way to keep the tooltip within view when it might otherwise get clipped to the right or the bottom of the viewable area.

As with the previous tooltip tutorials, I start by setting a few variables:

[js]var $liveTip = $('
').hide().appendTo('body'), $win = $(window), showTip; [/js]

The first line creates the tooltip container, hides it, appends it to the body, and stores a reference to it for later use. The next line is more for convenience than anything else (though it might provide nominal performance benefit), since I'll be calling jQuery methods on the window object a few times. The third line will be used as a reference to a setTimeout function for delaying the tooltip's visibility.

Position the Tip

Okay. So far, so boring. But here is where things start getting fun. I have a few more pieces I want to store and retrieve, but since they're all related, I figured it would be nice to have them be properties of a single tip object:

[js]var tip = { title: '', offset: 12, delay: 300, position: function(event) { var positions = {x: event.pageX, y: event.pageY}; var dimensions = { x: [ $win.width(), $liveTip.outerWidth() ], y: [ $win.scrollTop() + $win.height(), $liveTip.outerHeight() ] }; for ( var axis in dimensions ) { if (dimensions[axis][0] < dimensions[axis][1] + positions[axis] + this.offset) { positions[axis] -= dimensions[axis][1] + this.offset; } else { positions[axis] += this.offset; } } $liveTip.css({ top: positions.y, left: positions.x }); } }; [/js]

Since the position needs to be calculated each time the tooltip is displayed, I've made position a function—in other words, a method of the tip object. This is the part that does the hard work of figuring out where exactly to put the tooltip and whether or not to "flip" it above or to the left of the mouse position. Because this method is going to be called repeatedly as the mouse moves over a link, I wanted to make it as bare-bones as possible. I defined two objects. The first one contains the x and y mouse coordinates. The other contains two sets of numbers: (x) the inner width of the browser window and outer width of the tooltip, and (y) the distance from the top of the document to the bottom edge of the viewable area and the outer height of the tooltip.

Notice that the properties of the dimensions object have the same names as the properties of the positions object. I did that so I could easily loop through the properties of one and use the same loop variable for both, as shown in lines 18-26 above. Within that little loop, I'm testing to see if the tooltip, plus the mouse position, plus our user-defined "offset," is greater than the window dimension—on the x axis and then on the y. If it is, I subtract the tooltip's dimension and its offset from the mouse position; if not, I just add the offset to the mouse position.

Finally, I set the top and left style properties of the tooltip with the appropriate coordinates. I suppose it would have been cool to name the properties of my two objects "left" and "top". That way, I could have written $liveTip.css(positions). But, x and y make more sense to me as I read them, so I figured it would be worth a few extra bytes to help me remember what I was doing here when I look back at the script later on.

Wire it up

Now that the tooltip positioning is taken care of, it's time to hook it up to some events. If you read Binding Multiple Events to Reduce Redundancy with Event Delegation Tooltips, you may remember that I used the .bind() method and passed in three events: mouseover, mouseout, and mousemove. That seemed to work fine, but now that jQuery 1.4.2 has provided us with the more convenient .delegate() method, we can use that one instead. Jordan Boesch recently wrote a nice introductory article on Using Delegate and Undelegate in jQuery, so check that out if you're uncertain about what the method does. One thing I'd like to note about it, though, is that it's currently the only jQuery method (of its kind) that doesn't map the this keyword in the callback function to the current element within the matched set. In the code below, for example, the callback function for other methods would map this to the "#mytable" element; for .delegate(), the function maps to its first argument, "a" — when it is within "#mytable" and when it or one of its ancestors is the event.target element. I hope this little .delegate() excursion didn't confuse matters. Suffice it to say that the method allows us to use this for the link we want to act on, instead of going through the rigamarole of checking for the event target ourselves as I did in my previous tooltip articles.

[js]$('#mytable').delegate('a', 'mouseover mouseout mousemove', function(event) { var link = this, $link = $(this); if (event.type == 'mouseover') { tip.title = link.title; link.title = ''; showTip = setTimeout(function() { $link.data('tipActive', true); tip.position(event); $liveTip .html('
' + tip.title + '
' + link.href + '
') .fadeOut(0) .fadeIn(200); }, tip.delay); } if (event.type == 'mouseout') { link.title = tip.title || link.title; if ($link.data('tipActive')) { $link.removeData('tipActive'); $liveTip.hide(); } else { clearTimeout(showTip); } } if (event.type == 'mousemove' && $link.data('tipActive')) { tip.position(event); } }); [/js]

Since a lot of what's going on in this code snippet has already been discussed in my previous posts, I'll just mention a couple things.

The tip positioning occurs in line 13, triggered by the mouseover event, and in line 34, triggered by mousemove. It only gets triggered in the mousemove, however, if the mouse has been over the link for the time specified in tip.delay for the setTimeout. That's where the link's "tipActive" data attribute is set.

This time around, I also added a little fade effect to show the tooltip. As a safeguard, I hide the tooltip and reset its opacity to 0 (using .fadeout() with a 0ms duration) before fading it in, because jQuery will only fade in elements if they are hidden to begin with. This extra step is only necessary if the fade-in duration is greater than tip.delay.


So that wraps up this tooltip series. As always, if you see something that I could do better, please leave a comment. I always appreciate learning better ways of plying the craft. And if you have any questions about what's going on in the code, please point out what you'd like me to clarify.

To see this one in action, visit the Flip Tip demo page. You can also look at the complete JavaScript file or download a zip of both the HTML and JavaScript.

Responsive Menu
Add more content here...