Search code examples
jquery-uidraggablejquery-ui-draggabledroppablejquery-ui-droppable

Revert draggable after a different draggable is dropped


I have a 2x2 grid of droppable areas [[A,B][C,D]] and under the grid is a 1x4 grid of draggables. I only want certain draggables next to each other. So for example, if there is a draggable in B, and I drag a different draggable into A, is there a way to make the draggable in B revert? The draggables have data-row and data-col so that I can grab the draggable in the prev/next column if I need to.

$(".draggable").draggable({
            scroll: false,
            snap: ".snaptarget",
            snapMode: "inner",
            stack: ".draggable",
            revert: function (event, ui) {
                var $draggable = $(this);
                $draggable.data("uiDraggable").originalPosition = {
                    top: 0,
                    left: 0
                };

                return !event;
            }
});

$(".snaptarget").droppable({
            accept: ".draggable",
            drop: function (event, ui) {
                var $draggable = $(ui.draggable);
                var $droppable = $(this);

                // This droppable is taken, so don't allow other draggables
                $droppable.droppable('option', 'accept', ui.draggable);

                ui.draggable.position({
                    my: "center",
                    at: "center",
                    of: $droppable,
                    using: function (pos) {
                        $draggable.animate(pos, "fast", "linear");
                    }
                });

                // Disable prev or next droppable if the pagewidth == 1
                if ($droppable.data("col") == 1) {
                    $droppable.next().droppable("option", "disabled", true);
                    var nextDrag = $(".draggable[data-row='" + $droppable.data("row") + "'][data-col='2']");
                    if (nextDrag.length) {
                        // I need to revert nextDrag if there is one.
                        // I've tried this but it doesn't seem to work
                        nextDrag.data("uiDraggable").originalPosition = {
                           top: 0,
                           left: 0
                        }
                    }
                }
            },
            tolerance: "pointer"
        });

Solution

  • Took a little bit of work, I am never good with offsets and positioning. Here's the key:

      function returnItem(item, target) {
        // Get Origin
        var oPos = item.data("uiDraggable").originalPosition;
        // Adjust Postion using animation
        item.position({
          my: "top left",
          at: "top left+" + oPos.left,
          of: target,
          using: function(pos) {
            item.animate(pos, "fast", "linear");
          }
        });
      }
    

    Here is a working example based on the Draggable Snap to element grid example:

    https://jsfiddle.net/Twisty/a4ucb6y3/6/

    HTML

    <div id="target">
      <div class="snaptarget ui-widget-header" data-col="1" data-row="1" style="top: 0; left: 0;">
      </div>
      <div class="snaptarget ui-widget-header" data-col="2" data-row="1" style="top: 0; left: 80px;">
      </div>
      <div class="snaptarget ui-widget-header" data-col="1" data-row="2" style="top: 80px; left: 0;">
      </div>
      <div class="snaptarget ui-widget-header" data-col="2" data-row="2" style="top: 80px; left: 80px;">
      </div>
    </div>
    
    <br style="clear:both">
    
    <div id="source">
      <div id="drag-A" class="draggable ui-widget-content" style="left: 0;">
        <p>Drag A</p>
      </div>
      <div id="draggable2" class="draggable ui-widget-content" style="left: 80px;">
        <p>Drag B</p>
      </div>
      <div id="draggable3" class="draggable ui-widget-content" style="left: 160px;">
        <p>Drag C</p>
      </div>
      <div id="draggable4" class="draggable ui-widget-content" style="left: 240px;">
        <p>Drag D</p>
      </div>
    </div>
    

    CSS

    .draggable {
      width: 80px;
      height: 80px;
      font-size: .9em;
      position: absolute;
      top: 0;
    }
    
    .draggable p {
      text-align: center;
      height: 1em;
      margin-top: 30px;
    }
    
    #source {
      width: 320px;
      height: 80px;
      position: relative;
    }
    
    #target {
      width: 160px;
      height: 160px;
      position: relative
    }
    
    .snaptarget {
      width: 80px;
      height: 80px;
      position: absolute;
    }
    

    jQuery

    $(function() {
      function returnItem(item, target) {
        // Get Origin
        var oPos = item.data("uiDraggable").originalPosition;
        // Adjust Postion using animation
        di.position({
          my: "top left",
          at: "top left+" + oPos.left,
          of: target,
          using: function(pos) {
            item.animate(pos, "fast", "linear");
          }
        });
      }
    
      $(".draggable").draggable({
        scroll: false,
        snap: ".snaptarget",
        snapMode: "inner",
        stack: ".draggable",
        revert: "invalid",
        start: function(e, ui) {
          var off = $("#source").position();
          ui.helper.data("uiDraggable").originalPosition = {
            top: ui.position.top,
            left: ui.position.left
          };
        }
      });
    
      $(".snaptarget").droppable({
        accept: ".draggable",
        drop: function(event, ui) {
          var $draggable = $(ui.draggable);
          var $droppable = $(this);
    
          // This droppable is taken, so don't allow other draggables
          $droppable.droppable('option', 'accept', ui.draggable);
    
          // Disable prev or next droppable if the pagewidth == 1
          if ($droppable.data("col") == 1) {
            $droppable.next().droppable("option", "disabled", true);
            var nextDrag = $(".draggable[data-row='" + $droppable.data("row") + "'][data-col='2']");
            if (nextDrag.length) {
              // I need to revert nextDrag if there is one.
              returnItem(nextDrag, $("#source"));
            }
          }
        },
        tolerance: "pointer"
      });
    });
    

    In draggable, when we start to drag, we want to record the original position (in case we need to later revert). The revert option is set to invalid in case the user drags it off some other place weird.

    We add the position to data of the dragged item so that it can be read later.

    When that item is dropped is when the magic happens. You had done all the checking, just needed to return the item if it didn't fit. if nextDrag exists, we return it to it's source.

    Going forward, you may want to consider appending, cloning, and removing the elements in the start/stop events. As it is now, we're really only adjust the positioning of the elements, not their hierarchy in the DOM. Depending on what your needs are, this may not matter.