Friday 4 November 2011

jQuery UI dynamic drag & drop behaviour

In Coconut, we have a feature where you can drag and drop files in folders. What we wanted to do, was drag a file to a folder. When a file is dragged close to a folder, it should open up the subfolder tree, so you can also drop the file on a subfolder. But this was not possible, because the subfolders didn't become droppable. The code to show subfolders when dragging over a folder is similar to this:
jQuery(".dropTarget").droppable({
  greedy: true,
  tolerance: 'pointer',
  hoverClass: 'dropHover',
  accept: function () {
    //code left out
  },
  drop: function (event, ui) {
    //code left out
  },
  over: function (event, ui) {
    this.folderTimeout = window.setTimeout(function () {
      var dropObject = jQuery(this);
      dropObject.find("ul.folderSubList").show();
    }, 500);  
  },
  out: function () {
    window.clearTimeout(this.folderTimeout);
  }   
});

The over and out events are used to open up the subfolder tree. They use a timeout to make the transition less snappy, more smooth, which works fine. When inspecting the HTML with Firebug (after the code above was run), we could not find anything wrong. The hidden subfolders did get the class "ui-droppable" which droptargets receive when the jQuery ui droppable is initialized. So even though the folders are hidden (by css style "display:none;") they were initialized correctly as droptargets.

I couldn't figure out why, so it was time to pair program. My colleague noticed that it looked like the position of the droptargets were cached when a drag starts. A very sharp observation, because this actually is the case! So what we wanted to was refresh the cached positions once a subfolder tree was opened. The jQuery UI draggable and droppable API does not have a method to force a refresh of the positions. I've found a feature request ticket for the refresh method but it seems the method will probably not be introduced until jQuery UI 2.0 (we're at 1.8.16 at this moment).

Most developers have been at this point: you have to implement certain behaviour into your application, but the plugin you're using does not support that behaviour. At this point you have some options:
  1. use another plugin which does support it
  2. find alternative behaviour and ask customer if that's acceptable
  3. convince the customer it's not possible and abondon it
  4. dig into the plugin's core and find a workaround or hack to enable the support *
Of course, the last option is your last resort. You don't want to do this, because you cannot upgrade the plugin and assume your workaround or hack still works. But if options 1 - 3 are not an option (as it was in my case), here's a solution that works with jQuery 1.6.3 and jQuery UI 1.8.13:
jQuery(".dropTarget").droppable({
  greedy: true,
  tolerance: 'pointer',
  hoverClass: 'dropHover',
  accept: function () {
    //code left out
  },
  drop: function (event, ui) {
    //code left out
  },
  over: function (event, ui) {
    this.folderTimeout = window.setTimeout(function () {
      var dropObject = jQuery(this);
      dropObject.find("ul.folderSubList").show();
      jQuery.ui.ddmanager.prepareOffsets(jQuery.ui.ddmanager.current, null);
    }, 500);  
  },
  out: function () {
    window.clearTimeout(this.folderTimeout);
  }   
});

Trick is to call prepareOffsets on the current jQuery UI drop & dragmanager. Now you'll be able to drop on the dynamically shown elements.

*if the plugin is open source, you can also (or maybe should) try to fix it and do a pull request to get the fix to become a part of the plugin