skip to Main Content

Based on the suggestions here, if only one element is required to move, the below is possibly a simple way to do it:

(function () {
    let offset = [0, 0];
    let target = document.querySelector('.target');
    let isDown = false;
    target.addEventListener('mousedown', function(e) {
        isDown = true;
        target.style.position = 'relative';
        offset = [
            target.offsetLeft - e.clientX,
            target.offsetTop - e.clientY
        ];
    }, true);

    document.addEventListener('mouseup', function() {
        isDown = false;
    }, true);

    document.addEventListener('mousemove', function(e) {
        event.preventDefault();
        if (isDown) {
            target.style.left = (e.clientX + offset[0]) + 'px';
            target.style.top = (e.clientY + offset[1]) + 'px';
        }
    }, true);
})();
 

.target {
    width: 100px;
    height: 100px;
    background-color: #0000FF;
}
 

<div class="target"></div>

What if there are 2 divs? what if there’s 1000? that’s when the approach above won’t be as convenient because if we have 1000 divs, then we need another 1000 event listeners keeping track of 1000 offset as well as isDown variables. During an earlier attempt, I tried to get rid of the offset and isDown logic using $('.target').offset() and make the calculation happen inside mousemove handler, but I failed. Here’s an attempt to do what I described earlier which is what I’m trying to improve:

(function() {
    let targetsData = {};
    let targets = document.querySelectorAll('.target');
    Array.prototype.map.call(targets, (target) => {
        targetsData[target] = { 'mousedown': false, 'offset': [0, 0] };
        target.addEventListener('mousedown', (e) => {
            target.style.position = 'relative';
            targetsData[target]['mousedown'] = true;
            targetsData[target]['offset'] = [
                target.offsetLeft - e.clientX,
                target.offsetTop - e.clientY
            ];
        });
        target.addEventListener('mouseup', (e) => {
            targetsData[target]['mousedown'] = false;
        });
        target.addEventListener('mousemove', (e) => {
            e.preventDefault();
            if (targetsData[target]['mousedown']) {
                let offset = targetsData[target]['offset']
                target.style.left = (e.clientX + offset[0]) + 'px';
                target.style.top = (e.clientY + offset[1]) + 'px';
            }
        });
    });
})();
 

.target {
    width: 100px;
    height: 100px;
    background-color: #0000FF;
    margin-bottom: 5px
}
 

<div class="target"></div>
<div class="target"></div>
<div class="target"></div>

Only the first square from the top works and if dragged quickly, some weird effects start to happen and the other ones’ side effects seem to be worse:

effect

What’s happening here? What would be a way to efficiently make all of the squares move properly?

2

Answers


  1. There’s a lot of possibilities – it’s not really such a big deal to have many event handlers, although managing them can be problematic if you’re not using a framework.

    A fairly straightforward option is actually to use a single mousedown handler registered on the document, and check in the event handler if the event target (or any of its ancestors) is one of the draggable elements:

    Note: initial version did not handle re-dragging gracefully

    (function() {
        let offset = [0, 0];
        let target = null;
        
        function targetIfMatches(e, selector) {
            let maybeTarget = e.target;
            while (maybeTarget && maybeTarget.matches) {
                if (maybeTarget.matches(selector))
                    return maybeTarget;
                maybeTarget = maybeTarget.parentNode;
            }
            return null;
        }
    
        function handleMouseDown(e) {
            // Check event target and ancestors to see if any of them are a target
            target = targetIfMatches(e, ".target");
    
            if (target) {
                target.style.position = 'relative';
                offset = [
                    [e.pageX - parseFloat(target.style.left || 0)],
                    [e.pageY - parseFloat(target.style.top || 0)]
                ];
            }
        }
    
        function handleMouseMove(e) {
            e.preventDefault();
            if (target) {
                target.style.left = (e.pageX - offset[0]) + 'px';
                target.style.top = (e.pageY - offset[1]) + 'px';
            }
        }
        
        function handleMouseUp(e) {
            target = null;
        }
    
        document.addEventListener('mousedown', handleMouseDown, true);
        document.addEventListener('mouseup', handleMouseUp, true);
        document.addEventListener('mousemove', handleMouseMove, true);
    })();
    .target {
      width: 100px;
      height: 100px;
      background-color: #0000FF;
      text-align: center;
      font: 20px/100px sans-serif;
      color: white;
      cursor: move;
      margin: 5px;
    }
    <div class="target">1</div>
    <div class="target">2</div>
    <div class="target">3</div>
    <div class="target">4</div>
    <div class="target">5</div>
    <div class="target">6</div>
    Login or Signup to reply.
  2. You don’t need an array of offsets. A global one will work fine. Either way, you can store individual values within the element itself. This will save some memory and time. I also dislike the idea of adding an offset. Instead, subtract the offset of the mouse from the current mouse position. This offset will be equal to the offset of the div from when the DOM first loaded plus the offset of the mouse position from where the div currently is.

    (function() {
      let targets = document.querySelectorAll('.target');
      let offsetX;
      let offsetY;
      Array.prototype.map.call(targets, (target) => {
        target.isMouseDown = false;
        target.initialOffsetLeft = target.offsetLeft;
        target.initialOffsetTop = target.offsetTop;
        target.addEventListener('mousedown', (e) => {
          target.style.position = 'relative';
          target.isMouseDown = true;
          offsetX = target.initialOffsetLeft + e.offsetX;
          offsetY = target.initialOffsetTop + e.offsetY;
        });
        document.addEventListener('mouseup', (e) => {
          target.isMouseDown = false;
        });
        document.addEventListener('mousemove', (e) => {
          e.preventDefault();
          if (target.isMouseDown) {
            target.style.left = e.pageX - offsetX + 'px';
            target.style.top = e.pageY - offsetY + 'px';
          }
        });
      });
    })();
    .target {
      width: 300px;
      height: 300px;
      background-color: #0000FF;
      margin-bottom: 5px;
    }
    <div class="target"></div>
    <div class="target"></div>
    <div class="target"></div>

    One major difference between our codes is where we place our event handlers. I place my mouseup and mousemove events on document rather than the element itself. This way, it doesn’t matter where in the document the mouse is. Your code requires the mouse to be over the div in order to handle mouse movements or mouseup. And then there’s the difference in offset. Easy part first, I use event.pageXY instead of event.clientXY in order to account for the scroll. Next, I stored the initial offset of each div within the div itself. I add the initial offset to mouse’s offset relative to the div for every mousedown to set offsetXY. Finally, I subtract the mouse’s offset from the mouse’s position to set the position of the div.

    Note: I originally labeled offsetXY as initialXY. I realized that the term initial misrepresents the variable. Just a change in variable names.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search