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!