# Drag&Drop and Drag&Resize in Pure JS Style
20 December 2017
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:
```html
```
and corresponding styles:
```css
#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.
```js
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.
```js
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.
```js
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.
```js
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](https://javascript.info/coordinates). 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.
```js
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:
```js
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.
```js
// 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.
```html
```
```css
#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:
```js
/*
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](https://cdn.guseyn.com(/html/demo/drag-drop-resize.html)) and all code [here](https://github.com/Guseyn/drag_drop_resize) (if you have questions, don't hesitate submiting issue there).
References
* [Window Coordinates](https://javascript.info/coordinates)
* [Event Listeners](https://www.w3schools.com/js/js_htmldom_eventlistener.asp)
* [Demo](https://cdn.guseyn.com(/html/demo/drag-drop-resize.html))
* [Code](https://github.com/Guseyn/drag_drop_resize)