Drag&Drop and Drag&Resize in Pure JS style

20 December 2017
js

Although there are a lot of libraries doing such simple things like drag&drop and drag&resize, sometimes we need to do specific functional interfaces where it's important to understand how these things really work. If you are begginer in js or just curious programmer, this article would be very useful for you (you will learn some tricks in event driven js system and how it works in the whole).

Let's start with drag&drop. Consider we have an element in another one where it could be dragged and drop. We would have something like the following html code:

<div id="wrapElm">
    <div id="elm"></div>
</div>

and corresponding styles:

#wrapElm {
  width: 300px
  height:300px
  position: absolute
  top:30px
  left:30px
  background: #cc3333
}

#elm {
  width: 90px
  height: 50px
  position: absolute
  top:0px
  left:0px
  background: #00bfff
}

You may notice that wrapElm and elm have absolute positions. But you can use default(static) positions for elements, in that case you have to use css margin properties instead of top and left properties for declaring position of the dragged element. I prefer work with absolute positionated elements, it's just easier and more convenient.

Let's look at useful functions first before moving futher.

1. The current horizontal/vertical position of the scroll bar. It's very important to get the scroll position in the browser, especially when you have big elements in your interface.

function scrollTopPosition () {
  return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
}

function scrollLeftPosition () {
  return window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft
}

2. Height and width of the browser window. If you don't have a wrapper element, you might need to use the browser window that limits region for dragging the element.

function clientHeight () {
  return window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
}

function clientWidth () {
  return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
}

3. Coordinates of the most top-and-left point in the document element. It's also useful when you use the browser window as a wrapper element.

function clientTopPosition () {
  return document.documentElement.clientTop || document.body.clientTop || 0
}

function clientLeftPosition () {
  return document.documentElement.clientLeft || document.body.clientLeft || 0
}

4. Coordinates of the element. We need to know a start position of the dragged element.

function elmCoordinates (elm) {
  let rect = elm.getBoundingClientRect()
  let clientTop = clientTopPosition()
  let clientLeft = clientLeftPosition()
  let scrollTop = scrollTopPosition()
  let scrollLeft = scrollLeftPosition()
  let top = rect.top - clientTop + scrollTop
  let left = rect.left - clientLeft + scrollLeft
  let bottom = rect.bottom - clientTop + scrollTop
  let right = rect.right - clientLeft + scrollLeft
  return {
     top: Math.round(top),
     left: Math.round(left),
     bottom: Math.round(bottom),
     right: Math.round(right)
  }
}

If you want to learn more about window coordinates and how they are calculated, I can suggest you to visit this page. Just remember that window coordinates start at the left-upper corner of the window.

5. Prohibit selection of the text on the dragged element. If the dragged element contains text, you probably should do it.

function prohibitSelection () {
  if (window.getSelection) {
    window.getSelection().removeAllRanges()
  }
}

The main trick you need to know for implementing such things is that you can invoke one event listener in another one on an element. So, the common scheme looks like this:

document.onmouseup = function () {
    //remove mousemove listener of elm 
}

elm.onmousedown = function (event) {
  let elmMouseMoveEvent = function () {
    //move elm making some calculations
  }
  document.addEventListener('mousemove', elmMouseMoveEvent)
}

Event onmousedown is needed to be bound to the element itself, not to the document. This solution is more flexible, so that you can use this scheme for several dragged elements. I don't why, but it's better to use function addEventListener for mousemove event on the document (dragging becomes more smooth). If you know why, please share your ideas in the comments below.

In fact it's very simple: while you're clicking on the element, mousemove event listener is created, so that you can move the element. And when we invoke mouseup event, we just remove all mousemove event listeners, so that mousemove works only if mousedown is invoked.

Let's say we want to make a function dragAndDrop with required parameter elm and optional parameter wrapElm (if it's missed we just use the body element instead).

So, I will write below the full body of dragAndDrop function with detailed explanation via comments in the code. I think it's more convenient for readers than explaining different parts of implemntation separately. The following function is also applicable for several dragged elements.

// All dragged elements
var elms = []

// All zIndexes of elements
var zIndexes = []

// The maximum value of all values of zIndex property among all elements
var maxZIndex = 0

// All mousemove event listeners of all elements 
var mouseMoveEvents = []

function dragAndDrop (elm, wrapElm) {
  /* Push in elms array every dragged element,
     which is applied in this function */ 
  elms.push(elm)
  // Four parameters for limiting region for dragged elements
  let topLimit
  let leftLimit
  let bottomLimit
  let rightLimit
  /*  We also need zIndex of the current element for changing
      this property of the element while we're dragging it
      and restoring zIndex of the element when dragging is stoped */
  let elmZIndex = elm.style.zIndex || 0
  if (wrapElm) {
      // If wrapElm is specified in the arguments of this function
      let wrapElmCrds = elmCoordinates(wrapElm)
      let scrollTop = scrollTopPosition()
      let scrollLeft = scrollLeftPosition()
      topLimit = wrapElmCrds.top + scrollTop
      leftLimit = wrapElmCrds.left + scrollLeft
      bottomLimit = topLimit + wrapElm.offsetHeight
      rightLimit = leftLimit + wrapElm.offsetWidth
  } else {
      // otherwise we calculate limiting values by body element
      topLimit = clientTopPosition()
      leftLimit = clientLeftPosition()
      bottomLimit = clientHeight()
      rightLimit = clientWidth()
  }
  // Also for futher calculations we need height and width of the elm
  let elmHeight = elm.offsetHeight
  let elmWidth = elm.offsetWidth
  document.onmouseup = function() {
    /*  Removing all mousemove event listeners 
        and restoring zIndexes properties to their initial values */
    for (let i = 0 i < elms.length i++) {
      document.removeEventListener('mousemove', mouseMoveEvents[i])
      elms[i].style.zIndex = zIndexes[i]
      elms[i].style.cursor = 'default'
    }
    mouseMoveEvents.length = 0
  }
  elm.onmousedown = function(event) {
    let e = event || window.event
    // If left button of the mouse is pressed
    if ((e.which && e.which == 1) || (e.button && e.button == 1)) {
      // Getting start position of the element
      let elmCrds = elmCoordinates(elm)
      let elmTop = elmCrds.top
      let elmLeft = elmCrds.left
      let elmBottom = elmCrds.bottom
      let elmRight = elmCrds.right
      // Getting start position of the cursor
      let yStart = e.clientY
      let xStart = e.clientX
      /* Calculating distance between start position
         of the cursor and edges of the element */
      let deltaBetweenCursorPositionAndElmTop = yStart - elmTop
      let deltaBetweenCursorPositionAndElmLeft = xStart - elmLeft
      let deltaBetweenCursorPositionAndElmBottom = elmBottom - yStart
      let deltaBetweenCursorPositionAndElmRight = elmRight - xStart
      // Making zIndex of the current element as high as possible
      elm.style.zIndex = maxZIndex + 1
      // Changing cursor type of the element
      elm.style.cursor = 'move'
      // Declaring function for the current elm
      let elmMouseMoveEvent = function(event) {
        let e = event || window.event
        // Not allowing text selection while we're dragging the element
        prohibitSelection()
        // Getting current scroll position (in the moving process)  
        let scrollTop = scrollTopPosition()
        let scrollLeft = scrollLeftPosition()
        // Getting current cursor position (in moving process)
        let curY = e.clientY
        let curX = e.clientX
        // Get distances for moving
        let yMove = elmTop + curY - yStart
        let xMove = elmLeft + curX - xStart
        /*  Calculating current position of the element 
            in end of the movement
            you can't use method elmCoordinates() 
            because element's top and left is not changed yet */
        let curElmTop = curY - deltaBetweenCursorPositionAndElmTop
          + scrollTop
        let curElmLeft = curX - deltaBetweenCursorPositionAndElmLeft
          + scrollLeft
        let curElmBottom = curY + deltaBetweenCursorPositionAndElmBottom 
          + scrollTop
        let curElmRight = curX + deltaBetweenCursorPositionAndElmRight  
          + scrollLeft
        /*  Checking if the dragged element is completely inside
            the wrapper element
            if yes, the element is moving (properties top and left
            is changing)
            otherwise, the element adjoins to the edje 
            of the wrapper element */
        if (curElmTop < topLimit) {
          elm.style.top = '0px'
        } else if (curElmBottom > bottomLimit) {
          elm.style.top = bottomLimit - elmHeight - topLimit + 'px'
        } else {
          elm.style.top = yMove - topLimit + 'px'
        }
        if (curElmLeft < leftLimit) {
          elm.style.left = '0px'
        } else if (curElmRight > rightLimit) {
          elm.style.left = rightLimit - elmWidth - leftLimit + 'px'
        } else {
          elm.style.left = xMove - leftLimit + 'px'
        }
      }
      /*  Adding event listener with function elmMouseMoveEvent() 
          for the document on mousemove with the current element */
      document.addEventListener('mousemove', elmMouseMoveEvent)
      mouseMoveEvents.push(elmMouseMoveEvent)
    }
  }
  // Adding value of zIndex of the current elm into zIndexes 
  zIndexes.push(elm.style.zIndex)
  // Calculating maxZindex considering value of zIndex of the current element
  maxZIndex = zIndexes.reduce(function(a, b) {
    return Math.max(a, b)
  })
}

Now let's look at how drag&resize could be implemented. Consider the following html template for element that can be dragged&resized.

<div id="elm" style="">
  <div id="resize-drag-elm"></div>
</div>
#elm {
  width: 100px; 
  height: 100px;
  position: absolute;
  margin-top: 30px; 
  top: 0px;
  left:400px;
  background: #ff8000; 
}

#resize-drag-elm {
  width: 25px;
  height: 25px;
  position: absolute;
  bottom: 0;
  right: 0;
  background: #d3d3d3;
  cursor: nwse-resize;
}

As you can see, resize-drag-elm - is the element that you have to drag for resizing the element with id elm. Usually it's placed in the right-bottom corner of the main element. So, dragAndResize function for this html pattern would be something like this:

/*
  elm - the main element that we want to be resizable,
  resizeDragElm - the element you have to drag for resizing the main element
  minH - the minimal height of the elm
  minW - the minimal width of the elm
*/
function dragAndResize (elm, resizeDragElm, minH, minW) {
  // Setting mousedown event on the resizeDragElm
  resizeDragElm.onmousedown = function (event) {
    let e = event || window.event
    // Removing mousemove event listener
    document.addEventListener('mouseup', function () {
      document.onmousemove = null
      elm.style.cursor = "default"
    })
    // If left button of the mouse is pressed
    if ((e.which && e.which == 1) || (e.button && e.button == 1)) {
      /* Getting values for futher calculations
        (like in the dragAndDrop function) */
        let elmCrds = elmCoordinates(elm);
        let elmHeight = elm.offsetHeight;
        let elmWidth = elm.offsetWidth;
        let yStart = e.clientY;
        let xStart = e.clientX;
        let yLimit = yStart - elmCrds.top;
        let xLimit = xStart - elmCrds.left;
        // Setting mousedown event on the resizeDragElm
        document.onmousemove = function (event) {  
          /* Not allowing text selection while we're dragging
             the resizeDragElm */
          prohibitSelection();
          let e = event || window.event;
          // Get distances for resizing 
          let y = e.clientY - yStart;
          let x = e.clientX - xStart;
          // Calculating newHeight and newWidth
          let newHeight = elmHeight + y;
          let newWidth = elmWidth + x;
          // Changing height and width of the elm considering minH and minW
          if (newHeight >= minH && newWidth >= minW) {
            directCursor(y, x, resizeDragElm);
            elm.style.height = newHeight + 'px';
            elm.style.width = newWidth + 'px';
          }

        }
      }
    return false;
  }
}

// Changing cursor type that depends on direction of resizing
function directCursor (y, x, elm) {
  if ((y >= 0 && x >= 0) || (y <= 0 && x < 0)) {
    elm.style.cursor = 'nwse-resize';
  } else if ((y >= 0 && x < 0) || (y <= 0 && x >= 0)) {
    elm.style.cursor = 'nesw-resize';
  }
}

So, that's it. Hope, this article was useful for you. You can find demo here and all code here (if you have questions, don't hesitate submiting issue there).

References