jQuery Dragging With Collision Detection

I have the following HTML:

<div class="list" id="list">
    <div class="item" id="i1">Item 1</div>
    <div class="item" id="i2">Item 2</div>
    <div class="item" id="i3">Item 3</div>
</div>
<div class="timeline" id="timeline">
</div>

What I want to be able to do, with jQuery, is:

  1. Be able to drag .items from the #list into the #timeline
  2. .items can be dropped into the timeline as many times as required, eg. there could be 4 of item #i1 in the timeline.
  3. .items in the timeline must not overlap each other
  4. .items can be positioned at any place along the timeline so long as they do not overlap any other items on the timeline

So Ive gone for jQueryUI's Draggable and Droppable, and also gone for the jQueryUI Draggable Collision Plugin.

Here is the jQuery I have started with:

$('#list .item').draggable({
    helper: 'clone',
    revert: 'invalid',
    //the following are for the jquery-ui-dragggable-collision plugin
    obstacle: '#timeline .item',
    preventCollision: true
});
$('#timeline').droppable({
    accept: '.item'
});

My problem is that the jQueryUI Draggable Collision Plugin only works when you are dragging the original Div itself, and not dragging a helper. I need helpers so that I can achieve #2 (adding multiple copies of one item). But I need something like the Collision Plugin so I can achieve #3 (items not overlapping).

Does anybody know of a solution to this problem? Is there another plugin that does collision detection on the helper of a draggable object? Is there another approach I can try to get what I want to achieve?

Answers:

Answer

If you prefer a jsfiddle to that uses the jQueryUI Draggable Collision Plugin as you suggested, here is something to play around with: Link to jsfiddle

The approach uses the original helper in order to make use of collision functionality. The clone is generated in the start event function (and removed again in the stop event in case the dragging did not result in a successful drop):

$(function(){
    var draggableSelector = ".list .item:not(.dropped)";
    var init = function() {  
        $(draggableSelector).each(function(i){
            $(this)
                .draggable({
                    //helper: 'clone',
                    revert: 'invalid',
                    start: function(event,ui) {
                        var $clone = ui.helper.clone();
                        $clone
                            .removeClass("ui-draggable ui-draggable-dragging")
                            .insertAfter(ui.helper)
                        ;
                        $(this).data("clone",$clone);
                    },
                    stop: function(event,ui) {
                        if( $(".ui-draggable-dragging.dropped").length == 0) {
                            $(this).data("clone").remove();
                        };
                    },
                    //the following are for the jquery-ui-draggable-collision plugin
                    refreshPositions: true,
                    obstacle: '.item.dropped',
                    preventCollision: true,
                })
                .css("left", ( ($(this).width() + 5) * i) + "px")
            ;
        });

        $('.timeline').droppable({
            accept: '.item'
            ,drop: function(event,ui) {
                ui.draggable
                    .addClass("dropped")
                ;
                setTimeout(reinit, 500);
            }
        });                
    };

    var reinit = function() {
        $(".list .item.ui-draggable").draggable("destroy");
        init();
    }

    init();
});

Hope that this will be useful.

Answer

Here is an example i wrote for this question showing a simple drag and drop plugin with collision detection. It allows you to drop items onto a timeline as long as there is space for the item to exist without overlapping.

It is by no means a finished product but hopefully will show that code like this is not incredibly complex to write and trying to hack together massive conflicting plugins are not always the best option. Sometimes its best to start from scratch. Its fun, and a really good way to learn.

/*----------ON DOCUMENT READY----------*/

$(document).ready(function() {

  $("#timeline").timeline({
    items: ".item"
  });

});

/*----------THE TIMELINE PLUGIN----------*/

$.fn.timeline = function(options) {

  var defaults = {
    items: "div"
  }

  var options = $.extend(defaults, options)

  return this.each(function() {

    //-----SETUP-----//

    //define all the vars we will need later
    var el = $(this);
    var items = $(options.items);
    var mousedown = false;
    var dragging = false;
    var activeItem = false;
    var placedItems = new Array();

    //make everything unselectable so it dosne interfere with dragging
    $("html").find("*").css({
      "user-select": "none",
      "-moz-user-select": "none",
      "-webkit-user-select": "none",
      "-ms-user-select": "none",
      "-o-user-select": "none",
    }).attr("unselectable", "true").unbind("onselectstart");

    //-----EVENTS-----//

    //log when the mouse is down anywhere on the doc
    $(document).mousedown(function() {
      mousedown = true;
    });

    //when the mouse is released
    $(document).mouseup(function(e) {
      //if was dragging an item attempt to place it
      if (mousedown && dragging) {
        placeItem(e);
      }
      //log that dragging has stopped
      mousedown = false;
      dragging = false;
    });

    //log when the mouse is pressed over an item
    items.mousedown(function() {
      dragging = true;
      //clone the active item and hide it ready for dragging
      activeItem = $(this).clone().appendTo("body").hide();
    });

    //when the mouse movers over the doc
    $(document).mousemove(function(e) {
      //if mouse was pressed over item attempt to drag
      if (mousedown && dragging) {
        dragItem(e);
      }
    });

    //-----FUNCTIONS-----//

    //drag the item around the screen
    function dragItem(e) {

      //if no active item done do owt
      if (!activeItem) {
        return false;
      }

      //work out where the drag anchor is
      var x = e.pageX - (activeItem.height() / 2);
      var y = e.pageY - (activeItem.width() / 2);

      //save the original position in case we cant place the item
      if (!activeItem.origPos) {
        activeItem.origPos = {
          x: x,
          y: y
        }
      }

      //drag the item
      activeItem.css({
        "position": "absolute",
        "top": y,
        "left": x,
        "z-index": "999",
        "opacity": 0.6,
        "display": "block"
      });

    }

    //attempt to place the item
    function placeItem(e) {

      //if no active item dont do owt
      if (!activeItem) {
        return false;
      }

      //define som vars needed later on
      var onTargetY = false;
      var onTargetX = false;
      var remove = false;
      var collision = false;

      //check if item is being relesed withing the timeline bounds
      if (e.pageY > el.position().top && e.pageY < el.position().top + el.height()) {
        onTargetY = true;
      }
      if (e.pageX > el.position().left && e.pageX < el.position().left + el.width()) {
        onTargetX = true;
      }

      //if on target attempt to drop on timeline
      if (onTargetX && onTargetY) {

        //snap to the left or right if dropped at the left or right edges
        var maxLeft = el.position().left;
        var maxRight = el.position().left + el.width() - activeItem.width();
        x = e.pageX - (activeItem.width() / 2);
        if (x < maxLeft) {
          x = maxLeft;
        } else if (x > maxRight) {
          x = maxRight;
        }

        //loop the items already on the timeline and check for collisions
        $.each(placedItems, function(i, item) {
          var itemMin = item.position().left;
          var itemMax = item.position().left + item.width();
          if (x + activeItem.width() > itemMin && x < itemMax) {
            collision = true;
          }
        });
        y = el.position().top;

      }

      //if there is a collision or the item is dropped outside the timeline 
      //set x and y back to original position and set removal flag to true
      if (collision || !onTargetX || !onTargetY) {
        x = activeItem.origPos.x;
        y = activeItem.origPos.y;
        remove = true;
        //if dropped inside the timeline and no collisions add item to the 
        //array of items inside the timeline
      } else {
        placedItems.push(activeItem);
      }

      //finally either animate the item back to where it started then remove it
      //or snap it into the timeline in the space found
      activeItem.animate({
        top: y,
        left: x
      }, {
        duration: 300,
        queue: false,
        complete: function() {

          //if remove flag set remove the item from the dom
          if (remove) {
            $(this).remove();
          }

          //some tidying up
          activeItem.css("opacity", 1);
          activeItem = false;

        }
      });

    }

  });

}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="list" id="list">
  <div class="item item1">Item 1</div>
  <div class="item item2">Item 2</div>
  <div class="item item3">Item 3</div>
</div>

<div class="timeline" id="timeline"></div>

Enjoy :).

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.