Search code examples
javascriptvue.jsdrag-and-drop

Mousemove not firing outside window with user-select:none


I am making a small custom drag and drop library, but am running into an issue where mousemove/mouseup events don't fire outside of the window with user-select: none specified. I found a relevant stackoverflow question, but it refrences old IE/FF versions as the issue, wheras my drag and drop implementation doesn't work in any newer browsers.

Am I doing something wrong?

Relevant code (obviously code is not finished, just trying to get a kinda working implementation atm):

Draggable.vue Mount

this.$el.addEventListener('mousedown', (e) => {
      if (e.buttons === 1) {
        this.mouseStart = {
          x: e.clientX,
          y: e.clientY,
        }
        this.$el.addEventListener('mousemove', onMouseMove)
        this.$el.addEventListener('mouseup', onMouseUp)
      }
    })
    const onMouseMove = (e) => {
      if (this.mouseStart) {
        if (!dnd.dragging && Math.abs(this.mouseStart.x - e.clientX) > 2 ||
          Math.abs(this.mouseStart.y - e.clientY) > 2
        ) {
          dnd.startDrag(
            {
              x: this.mouseStart.x,
              y: this.mouseStart.y,
            },
            this.$el
          )
          this.mouseStart = null
        }
      }
    }
    const onMouseUp = (e) => {
      this.$el.removeEventListener('mousemove', onMouseMove)
      this.$el.removeEventListener('mouseup', onMouseUp)
      this.mouseStart = null
    }

Dnd.js

class Dnd extends EventEmitter {
  dragging = false
  mouseStartPos = null
  dragEle = null
  dragEleStartPos = null
  ghostComp = null

  onMouseUp = () => {
    this.stopDrag()
  }

  onMouseMove = (e) => {
    this.dragEle.style.top =
      this.dragEleStartPos.y + e.clientY - this.mouseStartPos.y + 'px'
    this.dragEle.style.left =
      this.dragEleStartPos.x + e.clientX - this.mouseStartPos.x + 'px'
  }

  startDrag(ctx, comp) {
    this.ghostComp = comp
    this.mouseStartPos = {
      x: ctx.x,
      y: ctx.y,
    }
    this.dragging = true
    this.dragEle = this.createDragElement(comp)
    document.body.addEventListener('mouseup', this.onMouseUp)
    document.body.addEventListener('mousemove', this.onMouseMove)
    this.ghostComp.classList.add('dnd-ghost')
  }

  endDrag() {
    this.dragEle.remove()
    this.ghostComp.classList.remove('dnd-ghost')

    this.dragging = false
    this.mouseStartPos = null
    this.dragEle = null
    this.ghostComp = null
  }

  stopDrag() {
    document.body.removeEventListener('mouseup', this.onMouseUp)
    document.body.removeEventListener('mousemove', this.onMouseMove)
    this.dragEle.classList.add('dnd-transition')

    this.dragEle.style.top = this.dragEleStartPos.y + 'px'
    this.dragEle.style.left = this.dragEleStartPos.x + 'px'
    setTimeout(this.endDrag.bind(this), 500)
  }

  createDragElement(node) {
    const cln = node.cloneNode(true)
    cln.classList.add('dnd-dragging')

    const rect = node.getBoundingClientRect()
    document.body.appendChild(cln)
    cln.style.top = rect.top + 'px'
    cln.style.left = rect.left + 'px'
    cln.style.width = rect.width + 'px'
    cln.style.height = rect.height + 'px'

    this.dragEleStartPos = { x: rect.left, y: rect.top }

    return cln
  }
}

export default new Dnd()

Solution

  • I was able to resolve this by attaching the event listener to window, and not document.body