person0
person0

Reputation: 1388

Moveable/draggable <div>

This is my updated and modified script, it works completely, except I would like to universalize it... observe the **** how can I make it so that I don't have to do function(e){BOX.Draggable.elemen = e.target || e.srcElement; elementDraggable(e); everytime I need to use the dragable function for a different element?

window.onload = addListeners;

var BOX = function(){
  return{
    Draggable: function(){}
  };
}();

function addListeners(){
  document.getElementById('div').addEventListener('contextmenu', menumove, false);
  **document.getElementById('div').addEventListener('mousedown', function(e){BOX.Draggable.elemen = e.target || e.srcElement; elementDraggable(e);}, false);**
}

function elementDraggable(e){
  var e = e || window.event;
  var div = BOX.Draggable.elemen;
  BOX.Draggable.innerX = e.clientX + window.pageXOffset - div.offsetLeft;
  BOX.Draggable.innerY = e.clientY + window.pageYOffset - div.offsetTop;

  window.addEventListener('mousemove', elementMove, false);
  window.addEventListener('mouseup', function(){
    window.removeEventListener('mousemove', elementMove, false);
    }, true);

  function elementMove(e){
    div.style.position = 'absolute';
    div.style.left = e.clientX + window.pageXOffset - BOX.Draggable.innerX + 'px';
    div.style.top = e.clientY + window.pageYOffset - BOX.Draggable.innerY + 'px';
  }
}

Upvotes: 66

Views: 226799

Answers (10)

vacsati
vacsati

Reputation: 567

Another approach to touch.

function draggable(container, handle) {
    let movable = handle ? handle : container;
    ['mousedown', 'touchstart'].forEach(event => {
        movable.addEventListener(event, e => {
            var c = e2c(e);
            var offsetX = c.x - parseInt(getComputedStyle(container).left);
            var offsetY = c.y - parseInt(getComputedStyle(container).top);
            function e2c(e){
                var client={x:0,y:0};
                if(typeof e.touches != "undefined" && e.touches.length > 0){
                    client.x = e.touches[0].clientX;
                    client.y = e.touches[0].clientY;
                }else{
                    client.x = e.clientX;
                    client.y = e.clientY;
                }
                return client;
            }
            function mouseMoveHandler(e) {
                var c = e2c(e);
                container.style.top = (c.y - offsetY) + 'px';
                container.style.left = (c.x - offsetX) + 'px';
                position[container.id].top=(c.y - offsetY);
                position[container.id].left=(c.x - offsetX);
            }
            function reset() {
                removeEventListener('mousemove', mouseMoveHandler);
                removeEventListener('touchmove', mouseMoveHandler);
                removeEventListener('mouseup', reset);
                removeEventListener('touchend', reset);
            }
            addEventListener('mousemove', mouseMoveHandler);
            addEventListener('touchmove', mouseMoveHandler);
            addEventListener('mouseup', reset);
            addEventListener('touchend', reset);
        });
    })
}

It's not that impressive, but it works.

makeDragable('#handle1', '#moveable1');
makeDragable('#handle2', '#moveable2');
makeDragable('#handle3', '#moveable3');

Upvotes: 0

Evan
Evan

Reputation: 2301

After trying jnoreiga's accepted answer, I found it very annoying that the dragged element abruptly snapped to the top left corner, rather than maintaining the same relative position.

This snippet prevents the awkward aforementioned behavior via an offset, and provides a simple interface to create draggable elements one at a time or en masse via a forEach call or similar.

window.onload = function() {
  draggable(document.getElementById('foo'));
}

function draggable(el) {
  el.addEventListener('mousedown', function(e) {
    var offsetX = e.clientX - parseInt(window.getComputedStyle(this).left);
    var offsetY = e.clientY - parseInt(window.getComputedStyle(this).top);
    
    function mouseMoveHandler(e) {
      el.style.top = (e.clientY - offsetY) + 'px';
      el.style.left = (e.clientX - offsetX) + 'px';
    }

    function reset() {
      window.removeEventListener('mousemove', mouseMoveHandler);
      window.removeEventListener('mouseup', reset);
    }

    window.addEventListener('mousemove', mouseMoveHandler);
    window.addEventListener('mouseup', reset);
  });
}
/* The only required styling is position: absolute */
#foo {
  position: absolute;
  border: 1px solid black;
  overflow: hidden;
}

/* Prevents inconsistent highlighting of element while being dragged
   Copied from https://stackoverflow.com/questions/826782 */
.noselect {
    -webkit-user-select: none; /* Safari */
            user-select: none; /* Every other browser */
}
<div id="foo" class="noselect">This is a draggable div!</div>

Upvotes: 16

uingtea
uingtea

Reputation: 6554

This is modified @evan answer, it add handle

function draggable(container, handle) {
    let movable = handle ? handle : container;
    ['mousedown', 'touchstart'].forEach(event => {
        movable.addEventListener(event, e => {
            var offsetX = e.clientX - parseInt(getComputedStyle(container).left);
            var offsetY = e.clientY - parseInt(getComputedStyle(container).top);

            function mouseMoveHandler(e) {
                container.style.top = (e.clientY - offsetY) + 'px';
                container.style.left = (e.clientX - offsetX) + 'px';
            }

            function reset() {
                removeEventListener('mousemove', mouseMoveHandler);
                removeEventListener('mouseup', reset);
            }

            addEventListener('mousemove', mouseMoveHandler);
            addEventListener('mouseup', reset);
        });
    })
}

// box only
let boxOnly = document.querySelector('#boxOnly');
draggable(boxOnly);

// box with handle
let container = document.querySelector('#foo');
let header = document.querySelector('#foo .header')
draggable(container, header);
/* The only required styling is position: absolute */
#foo, #boxOnly {
  position: absolute;
  border: 1px solid black;
  user-select: none;
}
#boxOnly{left: 300px;}
.header { background: orange;margin: 0;text-align:center;  padding: 6px;}
p.header:active, div#boxOnly:active {cursor:move;}
<div id="foo">
  <p class="header">Header</p>
  <p>Content cannot be dragged, <br>use header</p>
</div>

<div id="boxOnly">
  <p>Content can be dragged</p>
</div>

Upvotes: 2

ospider
ospider

Reputation: 10391

I modified Shaedo's code a little bit, wraps it in a function and add a feature that you can drag an element by only parts of it or its children, say the title bar of a div. Note in this demo, you can only drag the red area to move the blue area.

function makeDragable(dragHandle, dragTarget) {
  // used to prevent dragged object jumping to mouse location
  let xOffset = 0;
  let yOffset = 0;

  let handle = document.querySelector(dragHandle);
  handle.addEventListener("mousedown", startDrag, true);
  handle.addEventListener("touchstart", startDrag, true);

  /*sets offset parameters and starts listening for mouse-move*/
  function startDrag(e) {
    e.preventDefault();
    e.stopPropagation();
    let dragObj = document.querySelector(dragTarget);
    
    // shadow element would take the original place of the dragged element, this is to make sure that every sibling will not reflow in the document
    let shadow = dragObj.cloneNode();
    shadow.id = ""
    // You can change the style of the shadow here
    shadow.style.opacity = 0.5
    dragObj.parentNode.insertBefore(shadow, dragObj.nextSibling);

    let rect = dragObj.getBoundingClientRect();
    dragObj.style.left = rect.left;
    dragObj.style.top = rect.top;
    dragObj.style.position = "absolute";
    dragObj.style.zIndex = 999999;

    /*Drag object*/
    function dragObject(e) {
      e.preventDefault();
      e.stopPropagation();
      if(e.type=="mousemove") {
        dragObj.style.left = e.clientX-xOffset + "px"; // adjust location of dragged object so doesn't jump to mouse position
        dragObj.style.top = e.clientY-yOffset + "px";
      } else if(e.type=="touchmove") {
        dragObj.style.left = e.targetTouches[0].clientX-xOffset +"px"; // adjust location of dragged object so doesn't jump to mouse position
        dragObj.style.top = e.targetTouches[0].clientY-yOffset +"px";
      }
    }
    /*End dragging*/
    document.addEventListener("mouseup", function() {
      // hide the shadow element, but still let it keep the room, you can delete the shadow element to let the siblings reflow if that is what you want
      shadow.style.opacity = 0
      shadow.style.zIndex = -999999
      window.removeEventListener('mousemove', dragObject, true);
      window.removeEventListener('touchmove', dragObject, true);
    }, true)

    if (e.type=="mousedown") {
      xOffset = e.clientX - rect.left; //clientX and getBoundingClientRect() both use viewable area adjusted when scrolling aka 'viewport'
      yOffset = e.clientY - rect.top;
      window.addEventListener('mousemove', dragObject, true);
    } else if(e.type=="touchstart") {
      xOffset = e.targetTouches[0].clientX - rect.left;
      yOffset = e.targetTouches[0].clientY - rect.top;
      window.addEventListener('touchmove', dragObject, true);
    }
  }
}

makeDragable('#handle1', '#moveable1')
makeDragable('#handle2', '#moveable2')
makeDragable('#handle3', '#moveable3')
.moveable {
    width: 100px;
    height: 100px;
    background: blue;
    display: inline-block
}

.handle {
    width: 100px;
    height: 20px;
    cursor: move;
    background: red;
}

#moveable2 { background: green }
#moveable3 { background: teal }
<div id="moveable1" class="moveable">
    <div id="handle1" class="handle">
    </div>
</div>
<div id="moveable2" class="moveable">
    <div id="handle2" class="handle">
    </div>
</div>
<div id="moveable3" class="moveable">
    <div id="handle3" class="handle">
    </div>
</div>

Upvotes: 14

niente00
niente00

Reputation: 841

This is a nice no-jQuery script to drag a div: http://jsfiddle.net/g6m5t8co/1/

var mydragg = function() {
  return {
    move: function(divid, xpos, ypos) {
      divid.style.left = xpos + 'px';
      divid.style.top = ypos + 'px';
    },
    startMoving: function(divid, container, evt) {
      evt = evt || window.event;
      var posX = evt.clientX,
        posY = evt.clientY,
        divTop = divid.style.top,
        divLeft = divid.style.left,
        eWi = parseInt(divid.style.width),
        eHe = parseInt(divid.style.height),
        cWi = parseInt(document.getElementById(container).style.width),
        cHe = parseInt(document.getElementById(container).style.height);
      document.getElementById(container).style.cursor = 'move';
      divTop = divTop.replace('px', '');
      divLeft = divLeft.replace('px', '');
      var diffX = posX - divLeft,
        diffY = posY - divTop;
      document.onmousemove = function(evt) {
        evt = evt || window.event;
        var posX = evt.clientX,
          posY = evt.clientY,
          aX = posX - diffX,
          aY = posY - diffY;
        if (aX < 0) aX = 0;
        if (aY < 0) aY = 0;
        if (aX + eWi > cWi) aX = cWi - eWi;
        if (aY + eHe > cHe) aY = cHe - eHe;
        mydragg.move(divid, aX, aY);
      }
    },
    stopMoving: function(container) {
      var a = document.createElement('script');
      document.getElementById(container).style.cursor = 'default';
      document.onmousemove = function() {}
    },
  }
}();
#container {
  position: absolute;
  background-color: blue;
}

#elem {
  position: absolute;
  background-color: green;
  -webkit-user-select: none;
  -moz-user-select: none;
  -o-user-select: none;
  -ms-user-select: none;
  -khtml-user-select: none;
  user-select: none;
}
<div id='container' style="width: 600px;height: 400px;top:50px;left:50px;">
  <div id="elem" onmousedown='mydragg.startMoving(this,"container",event);' onmouseup='mydragg.stopMoving("container");' style="width: 200px;height: 100px;">
    <div style='width:100%;height:100%;padding:10px'>
      <select id=test>
        <option value=1>first
          <option value=2>second
      </select>
      <INPUT TYPE=text value="123">
    </div>
  </div>
</div>

Upvotes: 67

Takilleur
Takilleur

Reputation: 42

For all my friends:

    function initDrag(div) {
        div.addEventListener('mousedown', e => {
            if (e.target === div){
                startDrag(e, div);
            };
        });
    };

    function startDrag(e, div) {

        const offsetX = e.offsetX;
        const offsetY = e.offsetY;

        const controller = new AbortController();

        div.style.cursor = "grabbing";

        document.addEventListener('mousemove', e => {
            div.style.top = window.scrollY + e.clientY - offsetY + 'px';
            div.style.left = window.scrollX + e.clientX - offsetX + 'px';
        }, { signal: controller.signal }, true);

        'mouseup,blur'.split(',').forEach(e => {
            document.addEventListener(e, () => {
                controller.abort();
                div.style.cursor = "grab";
            }, { once: true }, true);
        });
    };

Upvotes: 2

HoldOffHunger
HoldOffHunger

Reputation: 20951

Any solution that uses clientY, clientX, pageY, or pageX within the dragend event will completely fail in Firefox. Source: Bugzilla: Bug #505521, Set screen coordinates during HTML5 drag event.

How do we get by this limitation? document's drop event also fires at the same exact time as the dragend event of the dragged element. But, we can see things like clientY and clientX here in firefox. So, let's just use that.

Two working demos, 100% JavaScript-Only Solution: SO Code Snippet and JSBin.

var startx = 0;
var starty = 0;
dragStartHandler = function(e) {
  startx = e.clientX;
  starty = e.clientY;
}

dragOverHandler = function(e) {
  e.preventDefault();
  return false;
}

dragEndHandler = function(e) {
  if(!startx || !starty) {
    return false;
  }
  
  var diffx = e.clientX - startx;
  var diffy = e.clientY - starty;
  
  var rect = e.target.getBoundingClientRect();

var offset = { 
                top: rect.top + window.scrollY, 
                left: rect.left + window.scrollX, 
            };
  
  var newleft = offset.left + diffx;
  var newtop = offset.top + diffy;
  
  e.target.style.position = 'absolute';
  e.target.style.left = newleft + 'px';
  e.target.style.top = newtop + 'px';
  
  startx = 0;
  starty = 0;
}

document.getElementsByClassName("draggable")[0].addEventListener('dragstart', dragStartHandler);

document.addEventListener('dragover', dragOverHandler);
document.addEventListener('drop', dragEndHandler);
.draggable {
  border: 1px solid black;
  cursor: move;
  width:250px;
};
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  
  <BR><BR><BR>

  <div id="draggable1" class="draggable" draggable="true">
    Hey, try to drag this element!
  </div>
  
</body>
</html>

Explanation:

  • dragStartHandler() : This is bound to the draggable element. Here, all we do is record the current x/y coordinates at start.
  • dragOverHandler() : This is bound to the document, so that we can override the default dragover behavior. This is needed to do any type of drag & dropping.
  • dragEndHandler() : This is bound to the document's drop. Normally, we would want this to bind to the element's dragend, but since clientY and clientX are missing, we bind it to the document. This just does exactly what you'd want to happen when dragend is called, except you have x/y coordinates.

The formula used is:

set style to: (current position) - (original position)

That is as complicated as it gets, but to calculate and apply the style, just for the x-dimension, the code is...

var diffx = e.clientX - startx;
var rect = e.target.getBoundingClientRect();
var offset = { 
        left: rect.left + window.scrollX, 
    };
var newleft = offset.left + diffx;
e.target.style.position = 'absolute';
e.target.style.left = newleft + 'px';

Upvotes: 1

Harijs Krūtainis
Harijs Krūtainis

Reputation: 1270

An additional method to "niente00" code.

init : function(className){
    var elements = document.getElementsByClassName(className);
    for (var i = 0; i < elements.length; i++){
        elements[i].onmousedown = function(){mydragg.startMoving(this,'container',event);};
        elements[i].onmouseup = function(){mydragg.stopMoving('container');};
        }
    }

Upvotes: 0

jnoreiga
jnoreiga

Reputation: 2174

Is jQuery an option for you? It makes what you are doing really simple since the code already exists.

http://jqueryui.com/demos/draggable/

Demo

JavaScript Code

window.onload = addListeners;

function addListeners(){
    document.getElementById('dxy').addEventListener('mousedown', mouseDown, false);
    window.addEventListener('mouseup', mouseUp, false);

}

function mouseUp()
{
    window.removeEventListener('mousemove', divMove, true);
}

function mouseDown(e){
  window.addEventListener('mousemove', divMove, true);
}

function divMove(e){
    var div = document.getElementById('dxy');
  div.style.position = 'absolute';
  div.style.top = e.clientY + 'px';
  div.style.left = e.clientX + 'px';
}​

Upvotes: 76

Niet the Dark Absol
Niet the Dark Absol

Reputation: 324790

Well, your movement code simplifies to:

div.style.position = "absolute";
div.style.top = e.clientY - (e.clientY - div.offsetTop) + "px";
div.style.left = e.clientX - (e.clientX - div.offsetLeft) + "px";

Basic math here - the e.clientX and e.clientY have absolutely no effect on the position here, so you're just taking the offsetLeft and reassigning it to the style.left, and the same for the top. Thus no movement whatsoever.

What you need to do is save the clientX and clientY when the mousedown happens, and do the subtraction based on that.

Oh and you're also setting the event listener wrong. The way it is now, you have it run divMove once and the return value (undefined here) is the function attached as the listener. Instead, use function(e) {divMove(dxy,e || window.event);}.

Upvotes: 12

Related Questions