0

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?

nlblack323
  • 155
  • 1
  • 10
  • So there's a couple of things falling over here. You can't use your `target` object as a lookup key in a plain object – it will be stringified (see [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) instead). The weird jumps for your second and third divs are because you're adding their `offset` positions from mousedown to their relative position at mousemove, so they'll gradually move further left and down. – motto Apr 08 '23 at 21:29
  • Coming from a python background, this works in python that's why I assumed it would by using plain objects. Does this imply that when stringified, the 3 will have the same string and hence only one lookup key will actually exist? Regarding the weird jumps, why they only or happen more severely to the last 2 squares? – nlblack323 Apr 08 '23 at 21:37
  • The `Map` object is a more general lookup in the style of python's `dict`, whereas JS object keys are strings, numbers and symbols, although if you use anything else as a key it will be stringified; in this case the stringified view may be browser dependent but similar to `[object HTMLDivElement]` in all three cases. The jumps only happen more severely to the last 2 squares because the initial `offsetTop` and `offsetLeft` of the first square are 0, since it starts at the top left of the page. – motto Apr 08 '23 at 21:43

2 Answers2

0

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>
motto
  • 2,888
  • 2
  • 2
  • 14
  • This solution breaks after multiple attempts to move a single div – Rojo Apr 08 '23 at 20:22
  • After editing, all the squares are moving as expected but this way, 2 event listeners are respawned on any mousedown. While this won't affect performance, it's wasteful, less readable and a bit convoluted. Do you think there's a cleaner way to achieve the same thing? – nlblack323 Apr 08 '23 at 21:47
  • The three event handlers are all attached at the initial setup. The `mouseup` and `mousemove` handlers are not respawned on `mousedown`. – motto Apr 08 '23 at 21:49
  • Yeah, I just noticed they are not, probably the formatting confused me. – nlblack323 Apr 08 '23 at 21:52
  • I apologise, I utilised the code from your snippet and the overzealous `tidy` functionality provided by the snippet interface – motto Apr 08 '23 at 21:54
  • Also there's an unhandled exception happening on mousdowns where no target exists. `Uncaught TypeError: maybeTarget.matches is not a function at HTMLDocument. (...)` – nlblack323 Apr 08 '23 at 22:02
  • Reformatted and adjusted to handle all cases I can think of. Please feel free to modify it to your needs. – motto Apr 08 '23 at 22:08
0

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.

Rojo
  • 2,749
  • 1
  • 13
  • 34
  • the third square has the same weird side effects – nlblack323 Apr 08 '23 at 20:29
  • @nlblack323 Yeah that happens when you scroll. Are your users going to scroll? I can add scroll support – Rojo Apr 08 '23 at 21:00
  • I'm going to use it for customizing a drag effect because all the existing known solutions look terrible or are cumbersome to use or import. So, yeah, it should work when dragged from anywhere to anywhere. Also what's wrong with what I did? why is it not working properly? – nlblack323 Apr 08 '23 at 21:16
  • @nlblack323 I added scroll support by using pageX and pageY instead of clientXY. And, to be honest, I'm not quite sure what exactly your code is doing. I'll look into a bit more. – Rojo Apr 08 '23 at 22:06
  • yeah, your snippet is currently working as expected. – nlblack323 Apr 08 '23 at 22:17
  • @nlblack323 I added a huge explanation. Essentially, you need to store the offset of the `div` when DOM is loaded and **never** update it. Your biggest mistake is updating it each time. – Rojo Apr 08 '23 at 22:26