Custom jQuery UI Draggable bounds

jQuery-UI draggable is very good for creating draggable components. It was perfect for the creating jMatrixBrowse. jMatrixBrowse is a jQuery plugin that allows creating large browsable matrices (like Google Maps) in the browser. jQuery UI Draggable, like the other jQuery UI widgets, is highly customisable. But I feel that the customisability is still a bit under documented. You can find a whole lot of new customisation possibilities if you dive into its code. For the project, I required a custom containment handler to be called to decide if the containment relation is satisfied or not. jQuery UI Draggable already gives a containment option but that only allows to contain the draggable element inside an already existing element or by giving absolute bounds. I wanted to check these bounds based on the current matrix state.

Draggable uses the generatePosition method to generate the new position for the draggable. To check custom bounds, I extended the generatePosition method and implemented by custom bounds checks in this.

```javascript // Override the original _generatePosition in draggable to check for matrix bounds on drag. dragContainer.draggable().data("draggable")._generatePosition = function(event) { return generatePositionsForDrag(dragContainer.draggable().data("draggable"), event); }; ```

First, we need to override the _generatePosition for our draggable element. Next, we will implement the new generate position similar to the one in draggable just using our bounds instead.

/**
* Generate new positions of the draggable element. This is used to override
* the original generate positions which had no way of specifying dynamic
* containment. This checks if the drag is valid by looking at the matrix
* coordinates and returns the new top and left positions accordingly.
*
* @param {Object} draggable - draggable object data.
* @param {Object} event - event that initiated the drag.
* @returns {Object} positions - new position of the draggable.
*/
function generatePositionsForDrag(draggable, event) {
  var o = draggable.options, scroll = draggable.cssPosition == 'absolute' && !(draggable.scrollParent[0] != document && $.ui.contains(draggable.scrollParent[0], draggable.offsetParent[0])) ? draggable.offsetParent : draggable.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
  var pageX = event.pageX;
  var pageY = event.pageY;

  var newPosition = {
    top: (
            pageY															// The absolute mouse position
            - draggable.offset.click.top												// Click offset (relative to the element)
            - draggable.offset.relative.top												// Only for relative positioned nodes: Relative offset from element to offset parent
            - draggable.offset.parent.top												// The offsetParent's offset without borders (offset + border)
            + (jQuery.browser.safari && jQuery.browser.version < 526 && draggable.cssPosition == 'fixed' ? 0 : ( draggable.cssPosition == 'fixed' ? -draggable.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))
    ),
    left: (
            pageX															// The absolute mouse position
            - draggable.offset.click.left												// Click offset (relative to the element)
            - draggable.offset.relative.left												// Only for relative positioned nodes: Relative offset from element to offset parent
            - draggable.offset.parent.left												// The offsetParent's offset without borders (offset + border)
            + (jQuery.browser.safari && jQuery.browser.version < 526 && draggable.cssPosition == 'fixed' ? 0 : ( draggable.cssPosition == 'fixed' ? -draggable.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))
    )
  };

  // Impose contraints on newPosition to prevent crossing of matrix bounds. 
  // Compute change in position for the drag.
  var changeInPosition = {
    top: (draggable._convertPositionTo("absolute", newPosition).top - draggable.positionAbs.top),
    left: (draggable._convertPositionTo("absolute", newPosition).left - draggable.positionAbs.left)
  };

  return checkPositionBounds(newPosition, changeInPosition, draggable);
}

/**
 * Checks the bounds for the matirx from four directions to find if the bounds are violated and returns the new positions.
 * @param  {Object} newPosition - The new position of the container for which to check the bounds.
 * @param  {Object} changeInPosition - The change in position. {top:0, left:0} can be passed here.
 * @param  {Object} draggable - The draggable instance.
 * @returns {Object} newPosition of the container.
 */
function checkPositionBounds (newPosition, changeInPosition, draggable) {
  
  var firstRow = (_self.currentCell.row == 0)?1:0;
  var firstCol = (_self.currentCell.col == 0)?1:0;
  // Get element and container offsets
  var element = jQuery(_cellElements[firstRow][firstCol]);
  var containerOffset = _container.offset();
  var elementOffset = element.offset();

  // If we are at the topmost cell, then check that bounds from the top are maintained.
  if (_self.currentCell.row <= 1) {
    // The new posoition.top of the first element relative to cotainter.
    var top = changeInPosition.top + elementOffset.top - containerOffset.top;
    if (top > 0) { // The drag crosses matrix bounds from the top.
      newPosition.top = newPosition.top - top;
    }
  }

  // If we are at the leftmost cell, then check that bounds from the left are maintained.
  if (_self.currentCell.col <= 1) {
    // The new posoition.top of the first element relative to cotainter.
    var left = changeInPosition.left + elementOffset.left - containerOffset.left;
    if (left > 0) { // The drag crosses matrix bounds from the left.
      newPosition.left = newPosition.left - left;
    }
  }

  // Get element offset for last element
  element = jQuery(_cellElements[_cellElements.length-1][_cellElements[0].length-1]);
  elementOffset = element.offset();

  // If we are at the bottomost cell, then check that bounds from the bottom are maintained.
  if (_self.currentCell.row - _configuration.getNumberOfBackgroundCells() + _cellElements.length - 1 >= _api.getMatrixSize().height-1) {
    var containerBottom = (containerOffset.top + _container.height());
    var elementBottom = (changeInPosition.top + elementOffset.top + element.height());
    // The new posoition.bottom of the last element relative to cotainter.
    var bottom =  containerBottom - elementBottom;
    if (bottom > 0) { // The drag crosses matrix bounds from the bottom.
      newPosition.top = newPosition.top + bottom;
    }
  }

  // If we are at the leftmost cell, then check that bounds from the left are maintained.
  if (_self.currentCell.col - _configuration.getNumberOfBackgroundCells() + _cellElements[0].length - 1 >= _api.getMatrixSize().width-1) {
    // The new posoition.right of the first element relative to cotainter.
    var containerRight = (containerOffset.left + _container.width());
    var newElementRight = (changeInPosition.left + elementOffset.left + element.width());
    var right =  containerRight - newElementRight;
    if (right > 0) { // The drag crosses matrix bounds from the left.
      newPosition.left = newPosition.left + right;
    }
  }

  return newPosition;
}

All you need to check your own bounds now for draggable is to implement your own checkPositionBounds and you are good to go. I almost decided to drop using the jQuery UI draggable and create my own. But in the end, a close look at the draggable code saved me the pain to write a custom draggable. So, now if you ever need to implement something complicated with jQuery UI widget, don't just try creating your own clone. The functionality might itself be buried into the code, you just need to figure out where to look.

Published 25 Aug 2012

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter