Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

jQuery Sortable Plug-in and unwanted Clicks


:P
On this page:

Here’s a problem I’ve hit on a few occasions with the very cool jQuery.ui.sortable plug-in. When you’re sorting elements that are clickable it often happens that when you drop your sortable item in a new sort location that the click event of the item fires.

For example check out this page:

http://www.west-wind.com/WestwindWebToolkit/samples/Ajax/AmazonBooks/BooksAdmin.aspx

Notice that each of the items are clickable to bring up in-place editing for each item via a popup window. Now click on the Sort List button. Once you do the list becomes sortable and you can drag items up and down the list based on the sortable plug-in:

Draggable

(Note: it works correctly now, but just *imagine* that when you drop the item the item display pops up. :-})

The sortable plug-in is super easy to use. In this case the sortable is applied to a <div> container element that contains additional <div> tags for each of the items. Here’s the .sortable applied against the outer container:

$("#divBookListWrapper").sortable(
    {
        opacity: 0.7,
        revert: true,
        scroll: true,
        containment: "parent",
        stop: function(e) {
            $(ctl).data("Sort", "1");
            $(ctl).html($(ctl).html().replace("Sort List", "Update Sort"));            
        }
    });    

The code as written would cause a problem however, because when sort items are dropped into place when a single sort operation is done the mouse up event from the drop also triggers a click event on the item dropped which produces rather unexpected results. In the example above it would show the detail window at the end of each drop operation – hardly the desired result. (note the sample doesn’t exhibit this behavior)

Unfortunately there’s no native switch for the .sortable plug-in that allows you to override this behavior although I think there definitely should be because I’ve had this particular behavior kick in for me in a variety of situations.

I experimented around with a few generic solutions trying to find a way to surpress the click event from firing by attempting to preventDefault, stopPropagation on the original source events fired, but unfortunately this had no effect. In the end the only thing that did work was to remove the click events in the start event handler for .sortable and then hook the events back up in the stop handler. The code that actually works correctly looks like this:

[Note: Updated based on comments – problem with e.originalTarget in WebKit and Opera requires unbinding all sortable elements]

$("#divBookListWrapper").sortable(
{
    opacity: 0.7,
    revert: true,
    scroll: true,
    containment: "parent",
    start: function(e) {
        // have to remvoe click handler off item so drop doesn't click
        //$(e.originalTarget).unbind("click");
        $(".bookitem").unbind("click");
},
    stop: function(e) {
        $(ctl).data("Sort", "1");
        $(ctl).html($(ctl).html().replace("Sort List", "Update Sort"));

        // reattach the item click handler
        //$(e.originalTarget).click(itemClickHandler);
        $(".bookitem").click(itemClickHandler);
    }
}); 

The start and stop events fire when you start and stop sorting a single item and so is perfect for this situation. Start and stop are called in the context of the parent element, not the actual item, so in order to get the current dragged item e.originalTarget can be used which is the element the mouseDown event fires on. In start, the click handler is unbound and then hooked back up in stop. I’m using an explicit function itemClickHandler here so the event code is only defined in one place for loading/updating of items and reattaching here. If you have more than one click handler in the particular item you might also want to look into hooking up your events using event namespaces (ie. .bind("click.bookitem") and .unbind("click.bookitem")).

[Note: Updated based on comments – problem with e.originalTarget in WebKit and Opera requires unbinding all sortable elements]

The original code is commented out  because it caused problems with Safari, Chrome and Opera. The issue is the e.originalTarget apparently is not getting set by the start/stop handlers so the click handler is actually NOT unset. The workaround here is to remove the click handler from ALL child items (since we don’t know which one is being dragged) and the reattaching them all in the stop handler. It appears that this is a bug in sortable as e.originalTarget certainly should be set as it is in FireFox and IE. <sigh>

Thanks to claya for the nudge in the right direction via Twitter.

FWIW, I’ve run into situations like this on a few occasions with various plug-ins including my own where overlapping events can cause errant behavior like this.The idea of unhooking events at the start of an operation and then re-attaching them is not an uncommon task and if I wouldn’t have been thinking that “there should be an easier way” from the start I would have solved this problem much quicker :-}. jQuery’s event management makes it very easy to unattach and reattach events so as long as you have your event logic isolated it’s just a couple of lines of code.

Posted in jQuery  

The Voices of Reason


 

Ira
May 12, 2009

# re: jQuery Sortable Plug-in and unwanted Clicks

The problem you're encountering seems to only be happening for me in IE. Good catch though!

jonx
May 12, 2009

# re: jQuery Sortable Plug-in and unwanted Clicks

hello, this is not working in chrome, it still show a strange working: when you drop the item, the panel dialog shows up and can't be closed...

Rick Strahl
May 12, 2009

# re: jQuery Sortable Plug-in and unwanted Clicks

Ok, so it looks like there's definitely a problem with the code above in WebKit (Chrome, Safari) and also in Opera. After some quick checking it appears the problem is that e.originalTarget is not set by these browsers - the value is undefined and so the events are never unhooked.

The workaround for this is to unbind ALL of the child items and then hook them all back up, which works on all browsers, but is obviously a bit less efficient:

$("#divBookListWrapper").sortable(
{
    opacity: 0.7,
    revert: true,
    scroll: true,
    containment: "parent",
    start: function(e) {
        // have to remvoe click handler off item so drop doesn't click
        //$(e.originalTarget).unbind("click");
        $(".bookitem").unbind("click");
},
    stop: function(e) {
        $(ctl).data("Sort", "1");
        $(ctl).html($(ctl).html().replace("Sort List", "Update Sort"));

        // reattach the item click handler
        //$(e.originalTarget).click(itemClickHandler);
        $(".bookitem").click(itemClickHandler);
    }
});


Not really a problem for smallish lists like the one in the example, but cna be an issues for larger ones.

Looks like this is a bug in sortable.

I'm going to update the code above along with a quick note. Thanks for the feedback!

# re: jQuery Sortable Plug-in and unwanted Clicks

I've always loved Netflix's UI where you can drag your rental choices around, thank you for this article.

Now only if governments, banks, and rest of the world operated as efficiently as Netflix...

Viren
May 25, 2010

# re: jQuery Sortable Plug-in and unwanted Clicks

Hi,
I'm creating a simple page showing 3-4 tabs. Implemented jquery Sortable to sort the tabs and jEditable for edit the tab name in place.

The script works but acts a little strangely in Firefox. If I double click on my editable span it will be replaced with the input field. It will then disappear if I focus on some other element EXCEPT another editable span.

Here, is the page script

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TabExample1.aspx.cs" Inherits="TabStrip.TabExample1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<style type="text/css">
.tab_container
{
border: 1px solid #999;
border-top: none;
overflow: hidden;
clear: both;
float: left;
width: 100%;
background: #fff;
}
.tab_content
{
padding: 20px;
font-size: 1.2em;
}
ul.tabs
{
margin: 0;
padding: 0;
float: left;
list-style: none;
height: 32px; /*--Set height of tabs--*/
border-bottom: 1px solid #999;
border-left: 1px solid #999;
width: 100%;
}
ul.tabs li
{
float: left;
margin: 0;
padding: 0;
height: 31px; /*--Subtract 1px from the height of the unordered list--*/
line-height: 31px; /*--Vertically aligns the text within the tab--*/
border: 1px solid #999;
border-left: none;
margin-bottom: -1px; /*--Pull the list item down 1px--*/
overflow: hidden;
position: relative;
background: #e0e0e0;
}
ul.tabs li a
{
text-decoration: none;
color: #000;
display: block;
font-size: 1.2em;
padding: 0 20px;
border: 1px solid #fff; /*--Gives the bevel look with a 1px white border inside the list item--*/
outline: none;
}
ul.tabs li a:hover
{
background: #ccc;
}
html ul.tabs li.active, html ul.tabs li.active a:hover
{
/*--Makes sure that the active tab does not listen to the hover properties--*/
background: #fff;
border-bottom: 1px solid #fff; /*--Makes the active tab look like it's connected with its content--*/
}
</style>

<link type="text/css" href="jquery-ui-1.8.1.custom.css" rel="stylesheet" />
<script type="text/javascript" src="jquery-1.4.2.min.js"></script>
<script type="text/javascript" src="jquery-ui-1.8.custom.min.js"></script>
<script src="jquery.jeditable.js" type="text/javascript" charset="utf-8"></script>

<script type="text/javascript" language="javascript">
$(document).ready(function() {

$('.edit').editable(function(value, settings) {
return value;
}, {
type: 'text',
event: 'dblclick',
width: '70px',
onblur: 'cancel'
});

$("#sortable").sortable({
items: 'li:not(.ui-add-tab)'
});

//When page loads...
$(".tab_content").hide(); //Hide all content
$("ul.tabs li:first").addClass("active").show(); //Activate first tab
$(".tab_content:first").show(); //Show first tab content

//On Click Event
$("ul.tabs li").click(function() {
$("ul.tabs li").removeClass("active"); //Remove any "active" class
$(this).addClass("active"); //Add "active" class to selected tab
$(".tab_content").hide(); //Hide all tab content

var activeTab = $(this).find("a").attr("href"); //Find the href attribute value to identify the active tab + content
$(activeTab).fadeIn(); //Fade in the active ID content
return false;
});

});
</script>

</head>
<body>
<ul id="sortable" class="tabs">
<li class="ui-state-default"><a class="edit" href="#tab1">Tab1</a>a</li>
<li class="ui-state-default"><a class="edit" href="#tab2">Tab2</a>a</li>
<li class="ui-state-default"><a class="edit" href="#tab3">Tab3</a>a</li>
<li class="ui-state-default"><a class="edit" href="#tab4">Tab4</a>a</li>
<li class="ui-add-tab"><a href="#addtab">Add Tab</a></li>
</ul>
</body>
</html>

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024