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; }
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,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"); } });
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With