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:
(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.
Other Posts you might also like