In which I show how to harness jQuery UI’s Mouse plugin to roll your own
drag-and-drop handling, when Draggable is not flexible enough for you.

Overview

Sometimes you need tighter control over drag-and-drop logic than jQuery UI’s
Draggable and
Droppable plugins afford. For instance,
when I wrote up the Solitr game, I initially used
Draggable, but I ended up with an unmaintainable mess of auxiliary “drop-zone”
divs, and I also didn’t find the drop logic to be flexible enough for a game.

But simply binding to mousedown and mousemove events yourself will cause a
headache because you’d have to work around subtle cross-browser compatibility
issues.

Luckily, jQuery UI comes with a
Mouse
plugin. (Incidentally, Draggable derives from this.) We can use this
to handle mouseStart, mouseDrag, and mouseStop events in a way that works
consistently across browsers.

Setup

It’s not possible/useful to instantiate the mouse widget directly, but we can
easily subclass it to make it usable with our own custom event handlers. Simply
copy and paste the following code, which registers a custommouse plugin, to
get started:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$.widget('ui.custommouse', $.ui.mouse, {
  options: {
    mouseStart: function(e) {},
    mouseDrag: function(e) {},
    mouseStop: function(e) {},
    mouseCapture: function(e) { return true; }
  },
  // Forward events to custom handlers
  _mouseStart: function(e) { return this.options.mouseStart(e); },
  _mouseDrag: function(e) { return this.options.mouseDrag(e); },
  _mouseStop: function(e) { return this.options.mouseStop(e); },
  _mouseCapture: function(e) { return this.options.mouseCapture(e); }
  // Bookkeeping, inspired by Draggable
  widgetEventPrefix: 'custommouse',
  _init: function() {
    return this._mouseInit();
  },
  _create: function() {
    return this.element.addClass('ui-custommouse');
  },
  _destroy: function() {
    this._mouseDestroy();
    return this.element.removeClass('ui-custommouse');
  },
});

Now instantiate the custommouse plugin we just defined, and pass your own
event handlers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$('#containerElement').custommouse({
  mouseStart: function(e) {
    // Handle the start of a drag-and-drop sequence here ...
  },
  mouseDrag: function(e) {
    // Handle the dragging ...
  },
  mouseStop: function(e) {
    // Handle the drop ...
  },
  mouseCapture: function(e) {
    // Optional event handler: Return false here when you want to ignore a
    // drag-and-drop sequence, so the start/drag/stop events don't fire ...
    return true;
  }

  // Goodies from the Mouse plugin:
  // Minimum distance in pixels before dragging is triggered
  //distance: 1
  // Minimum time in milliseconds before dragging is triggered
  //delay: 0
});

Event Sequence

Say the user starts dragging horizontally at point 50, 50, with distance
set to 10. Then the event sequence is guaranteed to be as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Event         e.pageX, e.pageY  Notes
============= ================= =======================================
mouseCapture  50, 50            Subsequent events only trigger if true

    ... user drags until they reach 60, 50 ...

mouseStart    50, 50            Mouse cursor is already at 60, 50, but
                                this triggers "late" at the original
                                position, once minimum distance and
                                delay are exceeded

mouseDrag     60, 50            First mouseDrag fires event immediately
                                after mouseStart, at real cursor
                                position

    ... user keeps dragging ...

mouseDrag     63, 50

    ... and dragging ...

mouseDrag     68, 50

    ... and releases the mouse ...

mouseStop     68, 50            Perhaps this is not guaranteed to be in
                                the same position as the last mouseDrag

So much for the theory. Let me give you some practical hints on how to
implement this:

Practical Hints

There are many coordinate properties on the event object, but you should use
e.pageX and e.pageY, which are
standardized

by jQuery to return the coordinates relative to the top left corner of the
entire document.

The only exception is the
elementFromPoint
method, which on modern
browsers
takes
e.clientX and e.clientY and returns the element under that point.

1
2
3
4
5
mouseStart: function(e) {
  this.element = document.elementFromPoint(e.clientX, e.clientY);
  this.originalElementPosition = $(this.element).position();
  this.dragStart = { left: e.pageX, top: e.pageY };
}

Then in the mouseDrag handler, calculate the offset:

1
2
3
4
5
6
7
8
9
10
11
mouseDrag: function(e) {
  var dragOffset = {
    left: e.pageX - this.dragStart.left,
    top: e.pageY - this.dragStart.top
  };
  // Assuming the element is absolutely positioned already
  $(this.element).css({
    left: this.originalElementPosition.left + dragOffset.left,
    top: this.originalElementPosition.top + dragOffset.top
  });
}

Finally, in mouseStop, snap the element to the nearest drop point (or
whatever
logic

you want to implement), and update the application state if necessary.

Finally

It would be sweet to handle touch events to make this work on mobile devices.
Unfortunately, the Mouse plugin doesn’t support touch handling yet. I have a
feeling that there will a lot of issues with inconsistent browser behavior if I
try to do this myself, so I’m leaving it for now.

In any case, I hope that this post was helpful to you. If you have practical
insights or alternative techniques to share (perhaps even without using
jquery.ui.mouse), please leave a comment!

Read more at the source