RxJS - Drag and Drop

5 years ago

A basic drag and drop feature only cares about three types of events: mouseup, mousemove, and mousedown. It can get more complicated from there if we're thinking about target locations and moving around other DOM items, but at it's very core, we only care about these three events. RxJS allows us to abstract each of these steps, so we can focus on each, yet keep our code concise and readable.

The following example allows us to set up infinite drag and drop items based on the selector we provide. In our case, we have two circles with the class of drag-button. Let's break down all the steps:

  • css - Important: make sure your element position is marked as absolute. Otherwise, you're not going to be able to move it anywhere you want.
  • init - wire up our event using a selector with forEach. This allows us to demonstrate drag and drop working independently from one another. For a singleton, we could simply right this as sample.dragDrop(document.getElementById('#myControl')).
  • mouse events - as mentioned above, we care about three types of events:
    • mousedown - binds to the element we'll be dragging.
    • mousemove - binds to the root document and tells us where the mouse is on the page.
    • mouseup - binds to the root document to tell us when the mouse is up. This signals that we need to leave our object at this point.
    • Note: the last two events could be created based on a different element, like a container. Of course, you would need additional rules to handle special cases.
  • original coordinates - all the examples I found missed this, but this is critical if you integrating with a CSS framework like Bootstrap or Foundation as it could have side-effects on the positioning. This gives us the raw coordinates.
  • mouseDrag - This is our core Observable which we subscribe to and it has combines our events above based on rules we provide using RxJS operators
    • mouseDown.mergeMap - this will take any mouse down events and merge it with an inner Observable.
    • mouseMove.map - our inner Observable returns an object based on where the mouse is on the page relative to our original coordinates.
    • takeUntil(mouseUp) - finally we complete the stream when the mouse is up.

English Translation: When we mouse down on our element, we will move it where our mouse moves on our page (root document) until we mouse up on our root document...oh...and with a tiny bit of fuzzy math.

JavaScript (dragAndDrop.js)

const sample = {
  dragDrop: dragTarget => {
    const posList = []
    const mouseDown = Rx.Observable.fromEvent(dragTarget, 'mousedown')
    const mouseMove = Rx.Observable.fromEvent(document, 'mousemove')
    const mouseUp = Rx.Observable.fromEvent(document, 'mouseup')

    const originalTop = dragTarget.getBoundingClientRect().top - dragTarget.offsetWidth / 2
    const originalLeft = dragTarget.getBoundingClientRect().left

    const mouseDrag = mouseDown.mergeMap(md => {
      return mouseMove
        .map(mm => {
          mm.preventDefault()
          return {
            left: mm.pageX - md.offsetX - originalLeft,
            top: mm.pageY - md.offsetY - originalTop
          }
        })
        .takeUntil(mouseUp)
    })
    const subscription = mouseDrag.subscribe(pos => {
      dragTarget.style.top = pos.top + 'px'
      dragTarget.style.left = pos.left + 'px'
      posList.push(pos.top + ' X ' + pos.left)
      utility.updateResults('Next', JSON.stringify(posList, null, 2))
    })
  },
  init: selector => {
    document.querySelectorAll(selector).forEach(sample.dragDrop)
  }
}
const utility = {
  updateResults = (title, results) => {
    console.group()
    console.log(title)
    if(results) {
      console.log(results)
    }
    console.groupEnd()
  }
}

HTML (dragAndDrop.html)

<div class="bg-primary text-white drag-button"><br />And<br/>Me!</div>
<div class="bg-success text-white drag-button"><br />Drag<br/>Me</div>

<style>
  .drag-button {
    /* Required for Drag and Drop */
    position: absolute;

    /* Optional */
    z-index: 1;
    border-radius: 50px;
    cursor: move;
    height: 100px;
    text-align: center;
    width: 100px;
    z-index: 1;
  }
</style>

<script>
  sample.init('.drag-button')
</script>

CDN Reference

<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.3/Rx.min.js"
  integrity="sha256-hRKdKxNWF3kA5HoYA7GoSRILnmbQS4cwv23bJwqJlns=" crossorigin="anonymous"></script>

Screenshot



Discuss on Twitter