Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery droppable - receiving events during drag over (not just on initial drag over)

Tags:

I am using jQuery droppable (in conjunction with jQuery draggable) to allow the user to add rows to an HTML table by dragging items from a list and dropping them on the table.

This works well, however at present the logic is that when the user drag-drops on a table row the new row gets added below the row they dropped on.

It would be better if the new row's add position was based on whether the user dropped in the upper or lower half of an existing row.

This is easy enough to calculate in the drop event, but I need to give UI feedback as the user drags (which I would do by means of two CSS classes droppable-above and droppable-below for example).

This doesn't seem to be possible, as the over event only fires once; when the user initially drags over the droppable element.

Is it possible to get the over event to fire for every mouse move while the user is over a droppable element?

If so, then I'd be able to do this:

$("tr.droppable").droppable({     over: function(event, ui) {         if (/* mouse is in top half of row */) {             $(this).addClass("droppable-above").removeClass("droppable-below");         }         else {             $(this).removeClass("droppable-above").addClass("droppable-below");         }     },      out: function(event, ui) {         $(this).removeClass("droppable-above").removeClass("droppable-below");     },      drop: function(event, ui) {         $(this).removeClass("droppable-above").removeClass("droppable-below");         if (/* mouse is in top half of row */) {             // Add new row above the dropped row         }         else {             // Add new row below the dropped row         }     } }); 

The CSS styles would be something like...

droppable-above { border-top: solid 3px Blue; } droppable-below { border-bottom: solid 3px Blue; } 
like image 940
Richard Ev Avatar asked Jun 01 '11 10:06

Richard Ev


2 Answers

As you said, over (like its counterpart out) is only raised once on the droppable. On the other hand, the drag event of the draggable is raised every time the mouse moves, and seems appropriate for the task. There are, however, two problems with this strategy:

  • drag is raised whether or not the draggable actually lies over a droppable,
  • even in that case, the droppable is not passed to the event handler.

One way to solve both problems is to associate the droppable and the draggable in the over handler, using jQuery's data() facility, and disassociate them in the out and drop handlers:

$("tr.droppable").droppable({     over: function(event, ui) {         if (/* mouse is in top half of row */) {             $(this).removeClass("droppable-below")                    .addClass("droppable-above");         }         else {             $(this).removeClass("droppable-above")                    .addClass("droppable-below");         }         ui.draggable.data("current-droppable", $(this));  // Associate.     },      out: function(event, ui) {         ui.draggable.removeData("current-droppable");     // Break association.         $(this).removeClass("droppable-above droppable-below");     },      drop: function(event, ui) {         ui.draggable.removeData("current-droppable");     // Break association.         $(this).removeClass("droppable-above droppable-below");         if (/* mouse is in top half of row */) {             // Add new row above the dropped row.         }         else {             // Add new row below the dropped row.         }     } }); 

Now that the draggable knows the droppable it's lying over, we can update the element's appearance in a drag event handler:

$(".draggable").draggable({     drag: function(event, ui) {         var $droppable = $(this).data("current-droppable");         if ($droppable) {             if (/* mouse is in top half of row */) {                 $droppable.removeClass("droppable-below")                           .addClass("droppable-above");             } else {                 $droppable.removeClass("droppable-above")                           .addClass("droppable-below");             }         }     } }); 

The code that follows is a simple test case that demonstrates this solution (it basically fills the commented gaps above and refactors common patterns into helper functions). The droppable setup is a little more intricate than in the previous example, mainly because the newly created table rows have to be made droppable like their siblings.

You can see the results in this fiddle.

HTML:

<div class="draggable">New item 1</div> <div class="draggable">New item 2</div> <div class="draggable">New item 3</div> <div class="draggable">New item 4</div> <div class="draggable">New item 5</div> <p>Drag the items above into the table below.</p> <table>     <tr class="droppable"><td>Item 1</td></tr>     <tr class="droppable"><td>Item 2</td></tr>     <tr class="droppable"><td>Item 3</td></tr>     <tr class="droppable"><td>Item 4</td></tr>     <tr class="droppable"><td>Item 5</td></tr> </table> 

CSS:

p {     line-height: 32px; }  table {     width: 100%; }  .draggable {     background-color: #d0ffff;     border: 1px solid black;     cursor: pointer;     padding: 6px; }  .droppable {     background-color: #ffffd0;     border: 1px solid black; }  .droppable td {     padding: 10px; }  .droppable-above {     border-top: 3px solid blue; }  .droppable-below {     border-bottom: 3px solid blue; } 

Javascript:

$(document).ready(function() {     $(".draggable").draggable({         helper: "clone",         drag: function(event, ui) {             var $droppable = $(this).data("current-droppable");             if ($droppable) {                 updateHighlight(ui, $droppable);             }         }     });      initDroppable($(".droppable"));      function initDroppable($elements)     {         $elements.droppable({             over: function(event, ui) {                 var $this = $(this);                 updateHighlight(ui, $this);                 ui.draggable.data("current-droppable", $this);             },             out: function(event, ui) {                 cleanupHighlight(ui, $(this));             },             drop: function(event, ui) {                 var $this = $(this);                 cleanupHighlight(ui, $this);                 var $new = $this.clone().children("td:first")                                 .html(ui.draggable.html()).end();                 if (isInUpperHalf(ui, $this)) {                     $new.insertBefore(this);                 } else {                     $new.insertAfter(this);                 }                 initDroppable($new);             }         });     }      function isInUpperHalf(ui, $droppable)     {         var $draggable = ui.draggable || ui.helper;         return (ui.offset.top + $draggable.outerHeight() / 2                 <= $droppable.offset().top + $droppable.outerHeight() / 2);     }      function updateHighlight(ui, $droppable)     {         if (isInUpperHalf(ui, $droppable)) {             $droppable.removeClass("droppable-below")                       .addClass("droppable-above");         } else {             $droppable.removeClass("droppable-above")                       .addClass("droppable-below");         }     }      function cleanupHighlight(ui, $droppable)     {         ui.draggable.removeData("current-droppable");         $droppable.removeClass("droppable-above droppable-below");     } }); 
like image 151
Frédéric Hamidi Avatar answered Sep 27 '22 19:09

Frédéric Hamidi


I am hitting the same issue and have been thinking about two solutions which I will share in case they give direction to others who find this relatively rare need.

  • Two div solution: Add two divs into each cell of the row, positioned to be stacked and each 50% high and full width with z-index set to -1 to protect from UI interference. Now make these droppables and use their 'over' and 'out' events to toggle the classes of the parent cell or row.

  • Abandon droppable and roll your own collision detection: Write your own collision detection to mimic the droppable effect. This which would give more freedom but would lead to some serious coding and so is not for the casual requirement. Would also be susceptible to performance issues. That said, there should be some obvious case-based shortcuts that would work in your favour.

I would be interested to hear of any other approaches to low-code solution.

like image 27
James Evason Avatar answered Sep 27 '22 19:09

James Evason