Zac Fukuda
062

Make HTML Element Draggable with Class

This is an extended version of previous example Make Html Element Draggable within Browser Window, turning draggable logic into class.

Codebase

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Draggable</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="draggable-object">
    <div class="draggable-subject">Draggable</div>
  </div>
  <script src="draggable.js" defer></script>
  <script src="app.js" defer></script>
</body>
</html>
style.css
@charset "utf-8";

.draggable-object {
  position: absolute;
}

.draggable-subject {
  cursor: grab;
}
.draggable-subject:active {
  cursor: grabbing;
}

/* Only for demo */
html, body {
  width: 100%;
  height: 100%;
}

body {
  margin: 0;
  font-family: sans-serif;
}

.draggable-object {
  width: 160px;
  height: 160px;
  border: 1px solid #ddd;
  border-radius: 2px;
  display: inline-block;
}

.draggable-subject {
  background: #f0f0f0;
  padding: 4px 8px;
}

style.css is no different from the previous example.

draggable.js
class Draggable {
  positionLeft = 0
  positionTop = 0
  positionLeftMax = 9999
  positionTopMax = 9999
  unit = 'px'

  constructor(objectSelector, subjectSelector) {
    const object = document.querySelector(objectSelector)

    if (object === null) {
      throw new Error('Cannot find an HTMLElement with the selector "' + objectSelector + '".')
    }

    this.object = object
    this.subject = subjectSelector ? document.querySelector(subjectSelector) : object
    this.handleMouseDown = this.handleMouseDown.bind(this)
    this.handleMouseMove = this.handleMouseMove.bind(this)
    this.handleMouseUp = this.handleMouseUp.bind(this)
    this.setMaxPositions()
    this.subject.addEventListener('mousedown', this.handleMouseDown)
  }

  setMaxPositions() {
    this.positionLeftMax = innerWidth - this.object.offsetWidth
    this.positionTopMax = innerHeight - this.object.offsetHeight
  }

  clamp(value, min, max) {
    return Math.min(max, Math.max(min, value))
  }

  handleMouseDown(e) {
    e.preventDefault()
    document.addEventListener('mousemove', this.handleMouseMove)
    document.addEventListener('mouseup', this.handleMouseUp)
  }

  handleMouseMove(e) {
    this.positionLeft += e.movementX
    this.positionTop += e.movementY
    this.object.style.left = this.clamp(this.positionLeft, 0, this.positionLeftMax) + this.unit
    this.object.style.top = this.clamp(this.positionTop, 0, this.positionTopMax) + this.unit
  }

  handleMouseUp() {
    document.removeEventListener('mousemove', this.handleMouseMove)
    document.removeEventListener('mouseup', this.handleMouseUp)
  }
}
app.js
new Draggable('.draggable-object', '.draggable-subject')

Note

There is one modification that I made since the previous logic.

MouseUp Event on Document

In section Note in the previous article, I pointed out that mouseup event triggered outside browser—more specifically outside the bar area—will cause a draggable element to keep tracking mouse pointer even when the mouse is back in browser window.

This issue can be easily solved by adding mouseup event to document instead of the bar.

This modification also makes a draggable element more responsive to the mouse pointer. The mousemove event on the bar area could not respond to a quick mouse movement which results the mouse pointer goes outside the bar.