Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to snap elements to other draggable elements using interact.js

I'm making a draggable elements using interactjs.io

I need implement exactly the same behaviour that jQuery UI snap. You can see an example here in the official documentation:

The behaviour is: snaps to all other draggable elements

In interactjs.io, in the documentation, you have "Snapping" (link documentation), but I don't find the way of coding it.

I have created a fiddle here: Fiddle Link

This is my JS Code:

interact('.draggable')
  .draggable({
   onmove: dragMoveListener,
   snap: {},
  });


  function dragMoveListener (event) {
    var target = event.target,
        // keep the dragged position in the data-x/data-y attributes
        x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx,
        y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;

    // translate the element
    target.style.webkitTransform =
    target.style.transform =
      'translate(' + x + 'px, ' + y + 'px)';

    // update the position attributes
    target.setAttribute('data-x', x);
    target.setAttribute('data-y', y);
  }

I need modify the snap section code, to make the draggable items snapping with others.

snap: {}

Thanks!!

like image 918
chemitaxis Avatar asked Jan 15 '16 12:01

chemitaxis


2 Answers

The following code can give you some ideas to get the result that you want. It works with draggable elements of different sizes. Function targets are used to set the target points and lines.

You can test it in this jsfiddle.

var AXIS_RANGE = 12;
var CORNER_RANGE = 14;
var CORNER_EXCLUDE_AXIS = 8;
var AXIS_EXTRA_RANGE = -6;

var myItems = [];
var currentElement = null;
var offX1, offY1, offX2, offY2;

function getPosition(element) {
  return {
    x: parseFloat(element.getAttribute('data-x')) || 0,
    y: parseFloat(element.getAttribute('data-y')) || 0
  };
}

function isBetween(value, min, length) {
  return min - AXIS_EXTRA_RANGE < value && value < (min + length) + AXIS_EXTRA_RANGE;
}

function getDistance(value1, value2) {
  return Math.abs(value1 - value2);
}

function getSnapCoords(element, axis) {
  var result = {
    isOK: false
  };
  if (currentElement && currentElement !== element) {
    var pos = getPosition(element);
    var cur = getPosition(currentElement);
    var distX1a = getDistance(pos.x, cur.x);
    var distX1b = getDistance(pos.x, cur.x + currentElement.offsetWidth);
    var distX2a = getDistance(pos.x + element.offsetWidth, cur.x);
    var distX2b = getDistance(pos.x + element.offsetWidth, cur.x + currentElement.offsetWidth);
    var distY1a = getDistance(pos.y, cur.y);
    var distY1b = getDistance(pos.y, cur.y + currentElement.offsetHeight);
    var distY2a = getDistance(pos.y + element.offsetHeight, cur.y);
    var distY2b = getDistance(pos.y + element.offsetHeight, cur.y + currentElement.offsetHeight);
    var distXa = Math.min(distX1a, distX2a);
    var distXb = Math.min(distX1b, distX2b);
    var distYa = Math.min(distY1a, distY2a);
    var distYb = Math.min(distY1b, distY2b);
    if (distXa < distXb) {
      result.offX = offX1;
    } else {
      result.offX = offX2
    }
    if (distYa < distYb) {
      result.offY = offY1;
    } else {
      result.offY = offY2
    }
    var distX1 = Math.min(distX1a, distX1b);
    var distX2 = Math.min(distX2a, distX2b);
    var distY1 = Math.min(distY1a, distY1b);
    var distY2 = Math.min(distY2a, distY2b);
    var distX = Math.min(distX1, distX2);
    var distY = Math.min(distY1, distY2);
    var dist = Math.max(distX, distY);
    var acceptAxis = dist > CORNER_EXCLUDE_AXIS;

    result.x = distX1 < distX2 ? pos.x : pos.x + element.offsetWidth;
    result.y = distY1 < distY2 ? pos.y : pos.y + element.offsetHeight;

    var inRangeX1 = isBetween(pos.x, cur.x, currentElement.offsetWidth);
    var inRangeX2 = isBetween(cur.x, pos.x, element.offsetWidth);
    var inRangeY1 = isBetween(pos.y, cur.y, currentElement.offsetHeight);
    var inRangeY2 = isBetween(cur.y, pos.y, element.offsetHeight);

    switch (axis) {
      case "x":
        result.isOK = acceptAxis && (inRangeY1 || inRangeY2);
        break;
      case "y":
        result.isOK = acceptAxis && (inRangeX1 || inRangeX2);
        break;
      default:
        result.isOK = true;
        break;
    }
  }
  return result;
}

$('.draggable').each(function() {
  var pos = getPosition(this);
  this.style.transform = 'translate(' + pos.x + 'px, ' + pos.y + 'px)';
  myItems.push(getPosition(this));
});

interact('.draggable').draggable({
  onstart: function(event) {
    currentElement = event.target;
    var pos = getPosition(currentElement);
    offX1 = event.clientX - pos.x;
    offY1 = event.clientY - pos.y;
    offX2 = event.clientX - currentElement.offsetWidth - pos.x;
    offY2 = event.clientY - currentElement.offsetHeight - pos.y;
  },
  onmove: dragMoveListener,
  snap: {
    targets:
      (function() {
        var snapPoints = [];
        $('.draggable').each(function() {
          (function(element) {
            // Slide along the X axis
            snapPoints.push(
              function(x, y) {
                var data = getSnapCoords(element, "x");
                if (data.isOK) {
                  return {
                    x: data.x + data.offX,
                    range: AXIS_RANGE
                  };
                }
              });
            // Slide along the Y axis
            snapPoints.push(
              function(x, y) {
                var data = getSnapCoords(element, "y");
                if (data.isOK) {
                  return {
                    y: data.y + data.offY,
                    range: AXIS_RANGE
                  };
                }
              });
            // Snap to corner
            snapPoints.push(
              function(x, y) {
                var data = getSnapCoords(element);
                if (data.isOK) {
                  return {
                    x: data.x + data.offX,
                    y: data.y + data.offY,
                    range: CORNER_RANGE
                  };
                }
              });
          })(this);
        });
        return snapPoints;
      })()
  },
  onend: function(event) {
    $('.draggable').each(function() {
      currentElement = null;
      myItems.push(getPosition(this));
    });
  }
});

function dragMoveListener(event) {
  var target = event.target;
  var oldPos = getPosition(target);
  var x = oldPos.x + event.dx;
  var y = oldPos.y + event.dy;

  // keep the dragged position in the data-x/data-y attributes
  target.setAttribute('data-x', x);
  target.setAttribute('data-y', y);

  // translate the element
  target.style.webkitTransform =
    target.style.transform =
    'translate(' + x + 'px, ' + y + 'px)';

  $('#position').text('x: ' + x + ' - y: ' + y);

  var result = $.grep(myItems, function(e) {
    if (e.x == parseInt(target.getAttribute('data-x')) || e.y == parseInt(target.getAttribute('data-y')))
      return 1;
  });

  if (result.length >= 1)
    target.style.backgroundColor = '#CCC';
  else
    target.style.backgroundColor = '#FFF';
}
like image 161
ConnorsFan Avatar answered Nov 01 '22 11:11

ConnorsFan


I made a JSFiddle without using interact.js. I only used jQuery. I did not use interactjs.io as you implied that you only prefer it but don't require it.

The code works with elements of different sizes.

jQuery.fn.reverse = [].reverse;

/* Handle add button clicks*/
$(".add-draggable").click(function() {
  var newDraggable = jQuery("<div class='draggable'></div>");
  newDraggable.css({
    position: 'absolute',
    left: 150,
    top: 150
  })

  newDraggable.attr({
    'data-x': 150,
    'data-y': 150
  }).addClass("large");

  jQuery(".draggable-wapper").append(newDraggable)
});

// initiate blocks
// This is done in revers as when the element is absolutly positioned .poisition() will retrun differnt values for the next element (mostly x will be 0 for all elements)
$(".draggable").reverse().each(function(i, e) {
  _this = jQuery(this);
  position = _this.position();

  _this.css({
    position: 'absolute',
    left: position.left,
    top: position.top
  }).attr("data-y", position.top).attr("data-x", position.left);
});

// Set some variabkles

// Used to differentiate clicks on elements from dragging
var isDragging = false;

// Store coordiators of all elements 
var coord;

// The moving element
var element = null;

// The offset to which the moving element snaps to the target element
// in percentage
var snappingYOffset = 20;
var snappingXOffset = 20;


$(".draggable-wapper").on("mousedown", ".draggable", function() {

  _this = element = jQuery(this);
  coord = [];
  isDragging = true;

  // Update coord
  jQuery(".draggable").each(function(i, e) {
    if (i == element.index())
      return true;

    ele = jQuery(e);
    var position = ele.position();
    var elementData = getData(ele);

    coord[i] = {
      leftX: position.left,
      rightX: position.left + ele.outerWidth(),
      topY: position.top,
      bottomY: position.top + ele.outerHeight()
    }

    jQuery.extend(coord[i], elementData);
  });

  _this.removeData("last-position");
});

jQuery(document).on("mousemove", function(e) {
    if (!isDragging)
      return;


    var lastPosition = _this.data("last-position");
    element.addClass("moving");

    if (typeof lastPosition != 'undefined') {
      // get difference to detemine new position
      var xDelta = e.clientX - lastPosition.x;
      var yDelta = e.clientY - lastPosition.y;

      var elementX = parseInt(element.attr("data-x"));
      var elementY = parseInt(element.attr("data-y"));

      element.attr({
        "data-x": elementX + xDelta,
        "data-y": elementY + yDelta
      }).css({
        "left": elementX + xDelta,
        "top": elementY + yDelta
      });

      // find which element is closer to moving elements and within offset limits
      filterArray(coord, _this);
    }

    // Save values for next itertation
    var position = {
      x: e.clientX,
      y: e.clientY
    };

    element.data("last-position", position);
  })
  .on("mouseup", function() {
    if (isDragging) {
      isDragging = false;
      element.removeClass("moving");
    }
  });

function filterArray(array, element) {
  // Set coord for moving element 
  // x1: left, x2: right, y1: top, y2: bottom

  var elementX1 = parseInt(element.attr("data-x"));
  var elementX2 = elementX1 + element.outerWidth();

  var elementY1 = parseInt(element.attr("data-y"));
  var elementY2 = elementY1 + element.outerHeight();

  // Show value inside element
  element.html('y:' + elementY1 + '<br> x: ' + elementX1);

  var result = {};

  // Loop through other elements and match the closeset
  array.forEach(function(value, index, originalArray) {
    // Get coordinators of each element
    // x1: left, x2: right, y1: top, y2: bottom
    var x1 = value['leftX'];
    var x2 = value['rightX'];

    var y1 = value['topY'];
    var y2 = value['bottomY'];

    // Get which element is bigger; the moving or the target element
    var biggerDim = bigger(element, {
      height: value['height'],
      width: value['width']
    });

    // Show values inside element
    jQuery(".draggable").eq(index).html('y:' + y1 + '<br> x: ' + x1);

    // Get offset for partiuclar element
    var xOffset = value['xOffset'];
    var yOffset = value['yOffset'];

    // yRange checks if moving element is moving within the Y range of target element
    // This requried to snap if true
    var yRange = (biggerDim.height == 'moving') ? y1 >= (elementY1 - yOffset) && y2 <= (elementY2 + yOffset) : elementY1 > (y1 - yOffset) && elementY2 < (y2 + yOffset);

    // xRange checks if moving element is moving within the X range of target element
    // This requried to snap if true
    var xRange = (biggerDim.width == 'moving') ? x1 > (elementX1 - xOffset) && x2 < (elementX2 + xOffset) : elementX1 > (x1 - xOffset) && elementX2 < (x2 + xOffset);

    // Is source element (moving) within the Y range 
    if (yRange) {

      // Is source element within right range of target
      if (elementX1 >= (x2 - xOffset) && elementX1 <= (x2 + xOffset)) {
        // Left side of the moving element
        element.css({
          "left": x2
        });
        // Is source element within left range of target
      } else if (elementX2 >= (x1 - xOffset) && elementX2 <= (x1 + xOffset)) {
        // right side of the moving element
        element.css({
          "left": x1 - element.outerWidth()
        });
      }
    }

    // Is source element (moving) within the X range of target
    if (xRange) {
      if (elementY1 >= (y2 - yOffset) && elementY1 <= (y2 + yOffset)) {
        // Top side of the moving element
        element.css({
          "top": y2
        });
      } else if (elementY2 >= (y1 - yOffset) && elementY2 <= (y1 + yOffset)) {
        // bottom side of the moving element
        element.css({
          "top": y1 - element.outerHeight()
        });
      }
    }
  });
}

/* Find which element is bigger */
function bigger(moving, target) {
  var width1 = moving.outerWidth();
  var height1 = moving.outerHeight();

  var width2 = target.width;
  var height2 = target.height;

  var result = {
    width: 'target',
    height: 'target'
  };

  if (width1 > width2)
    result.width = 'moving';

  if (height1 > height2)
    result.height = 'moving';

  return result;
}

/* Get data releted to a certain element */
function getData(ele) {
  var height = ele.outerHeight();
  var width = ele.outerWidth();

  var xOffset = (width * snappingXOffset) / 100;
  var yOffset = (height * snappingYOffset) / 100;

  return {
    height: height,
    width: width,
    xOffset: xOffset,
    yOffset: yOffset
  }
}
.draggable {
  background-color: green;
  border: 1px solid white;
  box-sizing: border-box;
  cursor: move;
  float: left;
  padding: 5px;
  position: relative;
  color: white;
  font-family: "calibri", -webkit-touch-callout: none;
  /* iOS Safari */
  -webkit-user-select: none;
  /* Chrome/Safari/Opera */
  -khtml-user-select: none;
  /* Konqueror */
  -moz-user-select: none;
  /* Firefox */
  -ms-user-select: none;
  /* Internet Explorer/Edge */
  user-select: none;
  /* Non-prefixed version, currently
                                  not supported by any browser */
}
.draggable.large {
  height: 300px;
  width: 100px;
  font-size: 16px;
}
.draggable.small {
  height: 50px;
  width: 50px;
  font-size: 12px;
}
.draggable.medium {
  height: 100px;
  width: 80px;
  font-size: 12px;
}
.draggable-wapper {
  float: left;
  position: relative;
}
.moving {
  z-index: 2;
  background-color: purple;
}
.add-draggable {
  background-color: green;
  border: 1px solid #0a5e1d;
  border-radius: 5px;
  color: white;
  cursor: pointer;
  font-size: 19px;
  padding: 10px 20px;
  position: absolute;
  right: 15px;
  top: 15px;
  transition: all 0.5s ease 0s;
  font-family: "calibri",
}
.add-draggable:hover {
  opacity: 0.9;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class='draggable-wapper'>
  <div class='draggable small'></div>
  <div class='draggable large'></div>
  <div class='draggable large'></div>
  <div class='draggable large'></div>
  <div class='draggable small'></div>
  <div class='draggable medium'></div>
  <div class='draggable medium'></div>
</div>

<div class='add-draggable'>
  Add
</div>
like image 1
Kalimah Avatar answered Nov 01 '22 11:11

Kalimah