Minimal Drag and Drop support in Javascript

Minimal Drag and Drop support in Javascript

One of the things that makes front-end web development so complicated is that the web was really, really not designed for the sort of "desktop app" emulation that we demand of it these days. Comprehensive attempts to redesign browser interfaces in a more app-friendly way — Java Applets and Adobe Flash, for instance — have mostly been rejected; for various reasons (some legitimate, some... less so) we stick with Javascript and DOM-based solutions to webapp design problems. One such problem is the drag and drop interface that desktop GUI users have come to expect as standard, but which HTML struggles with.

jQuery UI allows you to attach draggable or droppable attributes to HTML elements like divs to include drag-and-drop support, but jQuery UI is pretty heavyweight if all you really want is to allow users to drag elements around and respond to where they've been dragged. Supporting this functionality with "vanilla" (that is, nothing besides what's already included with the browser) Javascript is low footprint and, once you make sense of the standard event interface, not too hard to support. It also makes a handy demonstration of Javascript's partial function application support.

A very minimal drag and drop framework is illustrated in listing 1; it's demonstrated below with Sample #1, which you can drag and drop around the page.

function dragelem(evt) {
  var drag = document.getElementById("drag");
  drag.style.left = evt.pageX;
  drag.style.top = evt.pageY;
}

function drop(evt) {
  var drag = document.getElementById("drag");
  window.removeEventListener("mousemove", dragelem);
  window.removeEventListener("mouseup", drop);
}

function startdrag(evt)  {
  window.addEventListener("mousemove", dragelem);
  window.addEventListener("mouseup", drop);
}

window.onload = function()  {
  var drag = document.getElementById("drag");
  drag.addEventListener("mousedown", startdrag);
}

Listing 1: Minimal drag and drop support

Sample #1

A couple of notes. The startdrag function is attached to the element, but when a drag is started, the complementary mouseup and mousemove functions are attached to the window rather than the div itself. Mousemove events are only delivered to an HTML element if the mouse is actually inside it: if you attach the listeners to the element to be dragged, it can only be dragged down or to the right, since a drag outside its bounds is never delivered to it. Also, notice that when the element is dragged, its CSS left and top are set to the window event's pageX and pageY properties, to account for page scrolling. Finally, Sample #1 is absolutely positioned (otherwise I wouldn't be able to drag it around), so I have to insert some space above the following paragraph to prevent it from overlapping.

This works, but it leaves a bit to be desired. For one thing, every time you click on the element, its upper-left corner "jumps" down to where the mouse was clicked relative to it. Another limitation is that this approach can only work with a single element on the page: the window listeners themselves are hardcoded to the element whose id is drag. Fixing both of these requires a bit of "higher-order function" sleight-of-hand.

The "jumping" problem occurs because I don't keep track of, and can't pass in, the difference between the original upper-left corner and the original click position. An obvious solution would be to use an anonymous function like listing 2:

function startdrag(evt) {
  var drag = evt.target;
  var diff_x = evt.pageX - evt.target.offsetLeft;
  var diff_y = evt.pageY - evt.target.offsetTop;
  window.addEventListener("mousemove", function(evt)  {
    drag.style.left = evt.pageX - diff_x;
    drag.style.top = evt.pageY - diff_y;
  });
}

Listing 2: Anonymous function drag

This works, and retains the mouse position as desired, but there's a problem - I can't detach the function! The complementary removeEventListener function requires a function reference, but this is an anonymous function, so I can't provide one.

The solution is a pair of partially applied functions. I need to hand addEventListener a reference to a function that accepts a single argument of type Event, but I also need to provide some additional context (in this case, where in the target element the user actually clicked). The solution is to partially apply the function: provide the known arguments when the function is created and allow the caller to provide additional arguments when it's invoked. This way, I can keep a reference to the function to be detached.

There's one last trick, though: I can detach the mousemove handler in the mouseup handler this way, but I still can't detach the mouseup handler itself, since it doesn't exist until, of course, it's created. In this case, I can use an oxymoronically-described "named anonymous" function: one whose name only exists inside its own body so that it can refer to itself. (Note that I can't use this for both functions, since I have to be able to refer to the move handler outside its own body).

function dragElemFunction(drag, diff_x, diff_y) {
  return function(evt) {
    drag.style.left = evt.pageX - diff_x;
    drag.style.top = evt.pageY - diff_y;
  };
}

function dropElemFunction(dragFunction) {
  return function _dropElem(evt)  {
    window.removeEventListener("mousemove", dragFunction);
    window.removeEventListener("mouseup", _dropElem);
  };
}

function startdrag(evt) {
  var drag = evt.target;
  var diff_x = evt.pageX - evt.target.offsetLeft;
  var diff_y = evt.pageY - evt.target.offsetTop;
  var dragFunction = dragElemFunction(drag, diff_x, diff_y);
  var dropFunction = dropElemFunction(dragFunction);
  window.addEventListener("mousemove", dragFunction);
  window.addEventListener("mouseup", dropFunction);
}

Listing 3: Partially applied function solution

Sample #2

This also, incidentally, solves the other problem from listing 1: the id of the actual target element isn't referenced anywhere here, so it can be reused among multiple elements.

What I've done here is to create a new function that calls another with fixed arguments. There's actually been a fixed syntax for this in Javascript since ES 6: the bind function. You can re-implement listing 3 with built-ins as shown in listing 4:

function drag(drag, diff_x, diff_y, evt) {
  drag.style.left = evt.pageX - diff_x;
  drag.style.top = evt.pageY - diff_y;
}

function startDrag(evt) {
  ...
  var dragFunction = drag.bind(null, drag, diff_x, diff_y);
  window.addEventListener("mousemove", dragFunction);
  window.addEventListener("mouseup", function _drop(evt)  {
    window.removeEventListener("mousemove", dragFunction);
    window.removeEventListener("mouseup", _drop);
  });
}

Listing 4: bind support

The null passed in as the first parameter to bind is the value of the this parameter; I could have made this the element if I had wanted to, but I'd rather be a little bit more explicit about what's what.

Another annoyance here is that as I drag over other text, the text ends up being selected. This happens because events are propagated by default to the element's parents. This can be fixed easily by inserting a call to preventDefault at the end of each event handler:

function dragElemFunction(drag, diff_x, diff_y) {
  return function(evt) {
    drag.style.left = evt.pageX - diff_x;
    drag.style.top = evt.pageY - diff_y;
    evt.preventDefault();
  };
}

function dropElemFunction(dragFunction) {
  return function _dropElem(evt)  {
    document.removeEventListener("mousemove", dragFunction, false);
    document.removeEventListener("mouseup", _dropElem, false);
    evt.preventDefault();
  };
}

function startdrag(evt) {
  var drag = evt.target;
  var diff_x = evt.pageX - evt.target.offsetLeft;
  var diff_y = evt.pageY - evt.target.offsetTop;
  var dragFunction = dragElemFunction(drag, diff_x, diff_y);
  var dropFunction = dropElemFunction(dragFunction);
  document.addEventListener("mousemove", dragFunction, false);
  document.addEventListener("mouseup", dropFunction, false);
  evt.preventDefault();
}

Listing 5: prevent default

Add a comment:

Completely off-topic or spam comments will be removed at the discretion of the moderator.

You may preserve formatting (e.g. a code sample) by indenting with four spaces preceding the formatted line(s)

Name: Name is required
Email (will not be displayed publicly):
Comment:
Comment is required
Mohammed Al-Tawalbeh, 2023-03-21
Nothing
Mohammed Al-Tawalbeh, 2023-07-28
Sorry, my cat walked on my keyboard and typed that.
My Book

I'm the author of the book "Implementing SSL/TLS Using Cryptography and PKI". Like the title says, this is a from-the-ground-up examination of the SSL protocol that provides security, integrity and privacy to most application-level internet protocols, most notably HTTP. I include the source code to a complete working SSL implementation, including the most popular cryptographic algorithms (DES, 3DES, RC4, AES, RSA, DSA, Diffie-Hellman, HMAC, MD5, SHA-1, SHA-256, and ECC), and show how they all fit together to provide transport-layer security.

My Picture

Joshua Davies

Past Posts