Search code examples
javascriptjqueryhtmljquery-uidraggable

Snap to parent grid on start of drag


I'm fairly new to JavaScript & JQuery, so apologies if I'm missing a trick or two.

I've figured out how to get JQuery UI draggable objects to use the grid option, and, once the page has loaded, "snap to an imaginary grid" which all draggable objects have reference to (explained in code comments). However, I can't figure out how to get this behaviour to occur .on("dragstart").

HTML:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <script type="text/javascript" src="jquery-1.10.2.js"></script>
  <script type="text/javascript" src="jquery-ui.js"></script>
  <script type="text/javascript" src="dragger.js"></script>
</head>

<body>
  <div id="parent">
    <svg width="300" height="100" class="draggable" id="number1">
      <rect x="0" y="0" rx="10" ry="10" width="300" height="100" style="fill:rgb(121,0,121);stroke-width:3;stroke:rgb(0,0,0);">
    </svg>
    <svg width="300" height="100" class="draggable" id="letterA">
      <rect x="0" y="0" rx="100" ry="10" width="300" height="100" style="fill:rgb(0,121,121);stroke-width:3;stroke:rgb(255,0,0);">
    </svg>
  </div>
</body>

</html>

Note: There is a white gap between the two rectangles, via the second JavaScript below, this disappears once the block has been snapped to the grid. Alternatively, the 2 rectangles should be draggable onto each other and line up flush against one another to be considered snapped onto the grid.

JavaScript (dragger.js):

var roundedRemainder = function(numer, denom) {
  return numer - (Math.round(numer / denom) * denom);
}

var snapPosition = function(obj, granularity) {
  obj.position({
    my: "left top", // Unchanging reference point on draggable object
    at: "left top", // Unchanging reference point on reference object
    of: "#parent",  // The object that you want to move items with respect to.
    using: function(position, data) {
      var newPositions = {
          // Get the difference between the "imaginary grid" and the current grid
          left: function() {
            return roundedRemainder(position.left, granularity);
          },
          top: function() {
            return roundedRemainder(position.top, granularity);
          }
        }
        // Move to the imaginary grid
      $(this).css(newPositions);
      return newPositions;
    }
  });
}

$(function() {
  var gridSize = 50;
  $(".draggable")
    // Enable grid usage
    .draggable({
      grid: [gridSize, gridSize]
    })
    .on("dragstart", function(event, ui) {
      var newPos = snapPosition(ui.helper, gridSize);
    })
});

Proof the code in snapPosition works:

var roundedRemainder = function(numer, denom) {
  return numer - (Math.round(numer / denom) * denom);
}

$(function() {
  var gridSize = 50;
  $(".draggable")
    // Enable grid usage
    .draggable({
      grid: [gridSize, gridSize]
    })
    .position({
      my: "left top", // Unchanging reference point on draggable object
      at: "left top", // Unchanging reference point on reference object
      of: "#parent", // The object that you want to move items with respect to.
      using: function(position, data) {
        var newPositions = {
            // Get the difference between the "imaginary grid" and the current grid
            left: function() {
              return roundedRemainder(position.left, granularity);
            },
            top: function() {
              return roundedRemainder(position.top, granularity);
            }
          }
          // Move to the imaginary grid
        $(this).css(newPositions);
      }
    })
});

The first JavaScript is trying to change the position of the block once dragging starts, to snap it to the imaginary grid. The second does this automatically upon loading of the page, but never again. If I were to change the imaginary grids granularity from 50 to 79, for instance, dragging would not bring the objects back onto the grid as desired.

Is there somewhere I could look to learn how to do this? Is it doable?

To clarify:

  • JQuery = 1.10.2 (Same as in JQuery UI demo's)
  • JQuery UI = 1.11.4 (Same as in JQuery UI demo's)
  • Browser = Firefox on Ubuntu 14.04, everything up to date

I've already been through Google, but either terms akin to "start", "drag" and "position" aren't unique enough to narrow things down, or I haven't found the right place. I've also scoured through the JQuery (UI) archives.

Many thanks in advance!


Solution

  • Okay, so turns out there were a few examples of ignorance on my behalf. I'll go through them below to help others as well, but if you're just after a solution, then look no further:

    var roundedRemainder = function(numer, denom) {
      if (denom > 1) // Only calculate when there is a chance the draggable isn't on the grid.
        return numer - (Math.round(numer / denom) * denom); // Note: denom = 0 is invalid.
      else
        return 0; // If denom is invalid, or 1, then assume the draggable is on the grid.
    }
    
    $(function() {
      var gridSize = 79;
      var _this = this;
      $(".draggable")
        .draggable({
          // Enable grid constraints
          grid: [gridSize, gridSize],
    
          // At start of dragging (aka, only do once at the beginning)
          // snap the draggable object onto its parents grid.
          drag: function(event, ui) {
            var gridOffsetLeft;
            var gridOffsetTop;
            ui.helper.position({
              my: "left top", // For the top left of the draggable object
              at: "left top", // Link to the top left of the reference object
              of: $(this).parent(), // Make the reference object the parent of the draggable object
              // Calculate the grid offset from where the object ORIGINATED from
              using: function(position, data) {
                gridOffsetLeft = roundedRemainder(position.left, gridSize);
                gridOffsetTop = roundedRemainder(position.top, gridSize);
              }
            });
            // Calculate the total offset based off of the current
            // location of the draggable object, and the previously
            // calculated offset.
            gridOffsetLeft -= roundedRemainder(ui.position.left, gridSize);
            gridOffsetTop -= roundedRemainder(ui.position.top, gridSize);
    
            // Apply offsets & hence snap the draggable onto the
            // parents grid.
            ui.position.left += gridOffsetLeft;
            ui.position.top += gridOffsetTop;
          }
        })
    });
    <!doctype html>
    <html lang="en">
    
    <head>
      <meta charset="utf-8" />
      <script type="text/javascript" src="jquery-1.10.2.js"></script>
      <script type="text/javascript" src="jquery-ui.js"></script>
      <script type="text/javascript" src="dragger.js"></script>
    </head>
    
    <body>
      <div id="parent">
        <svg width="300" height="100" class="draggable" id="number1">
          <rect x="0" y="0" rx="10" ry="10" width="300" height="100" style="fill:rgb(121,0,121);stroke-width:3;stroke:rgb(0,0,0);">
        </svg>
        <svg width="300" height="100" class="draggable" id="letterA">
          <rect x="0" y="0" rx="100" ry="10" width="300" height="100" style="fill:rgb(0,121,121);stroke-width:3;stroke:rgb(255,0,0);">
        </svg>
      </div>
    </body>
    
    </html>

    Bugs:

    1. The first bug was in the way I was trying to separate the positioning function away from the dragging function. Specifically, when trying to send then over. I've not really understood this yet which is why it isn't in the solution, however I did read (and lost the link) a stack overflow which mentioned using var _then = then;, or using a binding method. If I find the link I'll edit the answer.
    2. The next bug relates to my specification of "at start". What I wanted as a method to only snap to the parent grid at the beginning of each drag. Thus, is sounded logical to use the start: or .on("dragstart", ...) functionality, as per the documentation. This lead into a preventDefault issue whereby either the start command was ignored, hence no snapping, but I could drag, or if I used event.preventDefault at the beginning of start, it would snap but no longer drag. Turns out that the drag: functionality only runs at the beginning of the drag once (please correct me if I'm wrong). By putting the snapping function back in there, another bug was solved.
    3. Finally, my original logic for snapping to the grid was faulty. It worked when using it just the once, but as soon as it was run every time the object was dragged, it would "creep" a little bit on every drag. This is because the "snap" functionality worked from the ORIGINAL location of the dragged object to the parent. As such, just adding that difference on every time, even when the object is already snapped, caused an error. To resolve this I simply took the difference from the original to current location of the object, to an identical grid centered on the original location of the object.

    (To be clear, there are 3 reference locations, the parent, the original location of the dragged object, and the current location of the dragged object).