Friday, June 17, 2011

Using jQuery UI Position With Mouse Events

I've been playing around with the position utility for jQuery UI. Its very handy giving page elements a fixed position, especially if you're trying to assemble visual elements of a widget. An alternative method to tell your widget where individual elements should be positioned is by using the CSS you create for your widget. In fact, this is the preferred method as it make for easy editing in themes. Theme authors can't aren't expected to edit Javascript files and change position() call values. There are certain uses for the position utility inside widgets when we know the theme isn't necessary going to change the alignment. If we can use position(), its syntax is intuitive enough that it makes for easy programming. Easy and simple, I like it. Sometimes combining the position utility with mouse events can give unexpected results. I'll give you an idea here of what I encountered and how I was able to get around it.

When do we want to use position()? When we have a subject element we want to position against a target element. For example, I have a paragraph of text, and a sidebar div. I want the div positioned to the left of the paragraph. Simple enough, I do sidebar.position({my: 'right', at:'left', of: paragraph}). Which means “take my right side and move it to the left this paragraph”. Instead of specifying pixel or percentage values, we're able to give the utility simple positions. More importantly, we can read and make sense of them. Sometimes the maze of floats and margins are difficult to grasp.

Let's get a little more elaborate with the position utility by creating a simple image widget. The widget will display a control to manipulate the image when the user hovers over it. We'll use the position utility to place the control. Here is the HTML markup:

<html>

 <head>
 
        <title>Using Position With Mouse Events</title>
        
        <link type="text/css" href="jqueryuitheme.css" rel="stylesheet"/>
        <script type="text/javascript" src="jqueryui.min.js"></script>
        <script type="text/javascript" src="jqueryui.min.js"></script>
        <script type="text/javascript" src="progresseffects.js"></script>
        <script type="text/javascript">
  
        $(document).ready(function(){
            $('img').image();
        });
  
        </script>
 
    </head>
 
    <body style="font-size: 10px;">
 
        <img src="myimg.png"/>
 
    </body>

</html>

And here is our widget that displays the control when the user hovers over the image:

$.widget('ui.image', {
    
    _init: function() {
        
        var image = this.element.addClass('ui-image'),
            control = $('<button></button>');
            
        control.text('Hide')
               .addClass('ui-image-button')
               .button()
               .insertAfter(image)
               .position({my:'center', at:'center', of:image})
               .hide();
               
        control.click(function(event) {
            $(event.currentTarget).fadeOut();
            $(event.currentTarget).siblings('img.ui-image').fadeOut();
        });
               
        image.mouseenter(function(event) {
            control.stop(true, true).fadeIn();
        });
        
        image.mouseleave(function(event) {
            control.stop(true, true).fadeOut();
        });

    }
 
});

That's all there is to it – you can open the example and try moving your mouse pointer over the image. You'll see the hide button in the middle of the image. We used the position utility to place the control in the center. Since we're dealing with an image element, we can't exactly insert the button as a nested image element, so it gets inserted after the image. This makes moving the button to the middle of our image painful if not for the position utility.

The hide button, when clicked, will fade the image and the control out of view. I know, not exactly practical, but I want to keep the irrelevant stuff miniscule. If you try clicking the hide button, you'll notice the problem with our widget. If you move the mouse pointer over the button, it fades back out again. Why does this happen? Moving the mouse pointer over our image control shouldn't cause this behaviour.

It turns out, that the mouseleave event handler we've defined for the image is triggered when hovering over the control. Remember, the control isn't a nested element of the image, so triggering the mouseleave event is actually correct behaviour. Is there any way we can salvage our implementation?

The way I got around this was by using the relatedTarget event property. This will tell us why the mouseleave event was triggered to begin with – the new element being entered. With our widget, we can use this property to cancel the mouseleave event handler if the mouse is entering the control.

$.widget('ui.image', {
    
    _init: function() {
        
        var image = this.element.addClass('ui-image'),
            control = $('<button></button>');
            
        control.text('Hide')
               .addClass('ui-image-button')
               .button()
               .insertAfter(image)
               .position({my:'center', at:'center', of:image})
               .hide();
               
        control.click(function(event) {
            $(event.currentTarget).fadeOut();
            $(event.currentTarget).siblings('img.ui-image').fadeOut();
        });
               
        image.mouseenter(function(event) {
            control.stop(true, true).fadeIn();
        });
        
        image.mouseleave(function(event) {
            if(!$(event.relatedTarget).is('button.ui-image-button')){
                control.stop(true, true).fadeOut();
            }
        });

    }
 
});