This article has been translated into French.
Here's a simple accessible drag and drop script. It works with both mouse and keyboard.
position: absolute
.position: fixed
.When the '#' link in the example boxes is activated (either by tabbing to it and hitting Enter or by clicking on it) the element can be dragged by the arrow keys. Pressing Enter or Escape releases it. (Feel free to change these keys, by the way. I'm not sure what the release keys ought to be, although Enter and Escape are both defensible.)
dragDrop
object you find below on this page.keyHTML
and keySpeed
properties to the values of
your choice (see below for an explanation).position: absolute
or
fixed
.initElement
function.
Send either an object or a string, which is interpreted as an ID. For instance:
dragDrop.initElement('test'); dragDrop.initElement(document.getElementById('test2'));
releaseElement
function.You should set two properties.
keyHTML
contains the HTML of the keyboard-accessible link that
every draggable object needs. I kept the HTML simple—just a link with a class for a bit of styling. You
can use any HTML construct that you like, but keep in mind that you need a link, since (apart from form
elements) links are the only elements that are reliably keyboard-focusable in all browsers; and keyboard users
need to be able to focus on something to trigger the script.
keySpeed
gives the speed of the keyboard drag and drop, in pixels per keypress event. I like
the value 10, but I encourage you to experiment with faster or slower movement.
There are seven more properties, but they're all internal to the script. Initially they're all set to
undefined
, and the relevant functions will assign values to them. (In fact, I could have left out these
property declarations entirely, but I like declaring the variables I need at the start of my script.)
Copy this object to your page (and don't forget the addEventSimple and removeEventSimple functions).
dragDrop = { keyHTML: '<a href="#" class="keyLink">#</a>', keySpeed: 10, // pixels per keypress event initialMouseX: undefined, initialMouseY: undefined, startX: undefined, startY: undefined, dXKeys: undefined, dYKeys: undefined, draggedObject: undefined, initElement: function (element) { if (typeof element == 'string') element = document.getElementById(element); element.onmousedown = dragDrop.startDragMouse; element.innerHTML += dragDrop.keyHTML; var links = element.getElementsByTagName('a'); var lastLink = links[links.length-1]; lastLink.relatedElement = element; lastLink.onclick = dragDrop.startDragKeys; }, startDragMouse: function (e) { dragDrop.startDrag(this); var evt = e || window.event; dragDrop.initialMouseX = evt.clientX; dragDrop.initialMouseY = evt.clientY; addEventSimple(document,'mousemove',dragDrop.dragMouse); addEventSimple(document,'mouseup',dragDrop.releaseElement); return false; }, startDragKeys: function () { dragDrop.startDrag(this.relatedElement); dragDrop.dXKeys = dragDrop.dYKeys = 0; addEventSimple(document,'keydown',dragDrop.dragKeys); addEventSimple(document,'keypress',dragDrop.switchKeyEvents); this.blur(); return false; }, startDrag: function (obj) { if (dragDrop.draggedObject) dragDrop.releaseElement(); dragDrop.startX = obj.offsetLeft; dragDrop.startY = obj.offsetTop; dragDrop.draggedObject = obj; obj.className += ' dragged'; }, dragMouse: function (e) { var evt = e || window.event; var dX = evt.clientX - dragDrop.initialMouseX; var dY = evt.clientY - dragDrop.initialMouseY; dragDrop.setPosition(dX,dY); return false; }, dragKeys: function(e) { var evt = e || window.event; var key = evt.keyCode; switch (key) { case 37: // left case 63234: dragDrop.dXKeys -= dragDrop.keySpeed; break; case 38: // up case 63232: dragDrop.dYKeys -= dragDrop.keySpeed; break; case 39: // right case 63235: dragDrop.dXKeys += dragDrop.keySpeed; break; case 40: // down case 63233: dragDrop.dYKeys += dragDrop.keySpeed; break; case 13: // enter case 27: // escape dragDrop.releaseElement(); return false; default: return true; } dragDrop.setPosition(dragDrop.dXKeys,dragDrop.dYKeys); if (evt.preventDefault) evt.preventDefault(); return false; }, setPosition: function (dx,dy) { dragDrop.draggedObject.style.left = dragDrop.startX + dx + 'px'; dragDrop.draggedObject.style.top = dragDrop.startY + dy + 'px'; }, switchKeyEvents: function () { // for Opera and Safari 1.3 removeEventSimple(document,'keydown',dragDrop.dragKeys); removeEventSimple(document,'keypress',dragDrop.switchKeyEvents); addEventSimple(document,'keypress',dragDrop.dragKeys); }, releaseElement: function() { removeEventSimple(document,'mousemove',dragDrop.dragMouse); removeEventSimple(document,'mouseup',dragDrop.releaseElement); removeEventSimple(document,'keypress',dragDrop.dragKeys); removeEventSimple(document,'keypress',dragDrop.switchKeyEvents); removeEventSimple(document,'keydown',dragDrop.dragKeys); dragDrop.draggedObject.className = dragDrop.draggedObject.className.replace(/dragged/,''); dragDrop.draggedObject = null; } }
A drag and drop is a way of moving an element across the screen. In order to be movable at all the element must
have position: absolute
or fixed
so that it can be moved by changing its coordinates
(style.top
and style.left
).
(In theory the element could have position: relative
,
but this is almost never useful. Besides, the relative case needs some extra position calculations that are not
part of this script.)
Setting the coordinates is pretty simple; it's finding the values that the coordinates should be set to that's the hard part of this script. Most of the script deals with finding them.
In addition, accessibility must be considered. Traditionally, drag and drop scripts work with the mouse, and all in all this remains the best option from a usability point of view. Nonetheless, in order to remain accessible for people who cannot use a mouse, the drag and drop should react to the keyboard, too.
Let's first review some basics.
Every drag and drop script starts with initialising the element. This job is done by the following function (method):
initElement: function (element) { if (typeof element == 'string') element = document.getElementById(element); element.onmousedown = dragDrop.startDragMouse; element.innerHTML += dragDrop.keyHTML; var links = element.getElementsByTagName('a'); var lastLink = links[links.length-1]; lastLink.relatedElement = element; lastLink.onclick = dragDrop.startDragKeys; },
If the function receives a string it interprets that string as an ID. Then it sets a mousedown
event for the entire element, in order to start up the mouse part of the script. Note that I use
traditional event handler registration; that's because I want the this
keyword to work normally in the startDragMouse
function.
Then it takes the keyHTML
the author has defined and add it to the element. This bit of HTML contains
one link, and since it's added to the end of the element, I'm certain that the last link in the element is the one
that should trigger the keyboard part of the script. The script sets a click event for this link to start up the
keyboard part of the script. It also stores a reference to the main object in relatedElement
; we'll need
this reference later on.
Now the script waits for the user to take action.
I decided on the following positioning approach: first I read out the initial position of the draggable
object at the time the dragging starts and store it in startX
and startY
. Later on the
script calculates the change in mouse position or the amount of arrow key strokes to determine how much
the element moves from this initial position.
The startX
and startY
variables are set by the startDrag
function, which is used both by the mouse and by the keyboard script.
startDrag: function (obj) { if (dragDrop.draggedObject) dragDrop.releaseElement(); dragDrop.startX = obj.offsetLeft; dragDrop.startY = obj.offsetTop; dragDrop.draggedObject = obj; obj.className += ' dragged'; },
First of all, if an element is still selected (in drag mode), release it. (We'll get back to
releaseElement
later.)
Then the function finds the current position of the element at the time the dragging starts through the
offsetLeft
and offsetTop
properties
(see the Find Position page)
and stores them in startX
and startY
for future
reference.
Then it stores a reference to the element in draggedObject
, and it
adds a class "dragged" to the element so that the author can define extra styles for an element that's being
dragged.
Sometimes you want to drag another element than the one the mousedown event takes place on—for instance
because a mousedown on a title bar (but nowhere else) should initiate the drag and drop. In that case,
make sure that draggedObject
refers to the object you want to drag.
Once the user moves the element either by mouse or by keyboard, the complicated parts of the script keep track of how much the position
of the element should change. This gives values dX
and dY
(change of X and Y). I
add these to startX
and startY
, which gives me the new position of the element.
This function does the actual repositioning:
setPosition: function (dx,dy) { dragDrop.draggedObject.style.left = dragDrop.startX + dx + 'px'; dragDrop.draggedObject.style.top = dragDrop.startY + dy + 'px'; },
It receives a dx
and dy
calculated by either the mouse or the keyboard scripts and adjusts
the object's style.top
and style.left
properties. The element moves.
That's pretty simple; the trick lies in finding the correct dx
and dy
. The mouse and
keyboard scripts do this quite differently, so we'll discuss them separately.
The mouse script is slightly more complicated than the keyboard script when it comes to calculations, but much simpler in terms of browser compatibility. Therefore we start with the mouse script.
First we have to discuss the events we need. Obviously, a drag and drop needs mousedown, mousemove and mouseup for selecting the element, dragging it, and releasing it.
Equally obviously, this sequence starts with a mousedown event on the element to be dragged. Therefore all draggable
elements need an onmousedown event handler that readies the element for dragging and dropping. We already saw
the line in startDrag
that takes care of this:
element.onmousedown = dragDrop.startDragMouse;
However, the mousemove and mouseup event should be set not on the element, but on the entire document. The reason is that the user may move the mouse wildly and quickly, and he might leave the dragged element behind. If the mousemove and mouseup functions were defined on the dragged element, the user would now lose control because the mouse is not over the element any more. That's bad usability.
If we define the mousemove and mouseup on the document, this problem disappears. Wherever the mouse is, the dragged element reacts to the mousemove and mouseup events. That's good (or at least better) usability.
In addition you should set the mousemove and mouseup events only when the dragging starts, and remove them when the user releases the element. This keeps your script clean and also saves some processing time because mousemove is only evaluated when necessary (ie. when the element is being dragged).
Once a mousedown event occurs on a draggable element, the startDragMouse
function is executed:
startDragMouse: function (e) { dragDrop.startDrag(this); var evt = e || window.event; dragDrop.initialMouseX = evt.clientX; dragDrop.initialMouseY = evt.clientY; addEventSimple(document,'mousemove',dragDrop.dragMouse); addEventSimple(document,'mouseup',dragDrop.releaseElement); return false; },
First it executes the startDrag
function we already discussed. Then it finds the current mouse
position and stores its coordinates in initialMouseX
and initialMouseY
. Later on we're
going to compare these values to the current mouse position.
Finally it returns false
; this is to suppress the default action of the mouse event: start selecting
text. We don't want any text to be selected while the dragging goes on; that'd be annoying.
Then it sets the mousemove and mouseup event handlers on the document, for the reasons discussed above. Because it's possible that the host page has its own mousemove and mouseup event handlers set on the document, I use my addEventSimple function that adds my event handlers without disturbing any that might already exist.
Now, whenever the user moves the mouse the dragMouse
function is executed.
dragMouse: function (e) { var evt = e || window.event; var dX = evt.clientX - dragDrop.initialMouseX; var dY = evt.clientY - dragDrop.initialMouseY; dragDrop.setPosition(dX,dY); return false; },
The function reads out the current mouse coordinates (clientX
and clientY
) and subtracts
initialMouseX
and initialMouseY
from these coordinates.
This results in the number of pixels the mouse has
moved since the start of the drag and drop. This is exactly what setPosition
expects, so we send dX
and dY
off.
Again we return false
to prevent the mousemove event from selecting text.
When the user releases the mouse, releaseElement
is called. We'll discuss that function later.
Now let's turn to the more difficult part: the keyboard script. Unlike a mouse drag and drop, there is no accepted standard user interface for a keyboard drag and drop (yet). Although the basic interaction is not terribly complicated, we should still briefly consider it.
The most obvious keys for moving the element are the arrow keys. That's pretty simple.
Activating and releasing the element is more tricky, though, and this is an area where my script could be improved.
I decided that the keyboard script can be activated through an extra link I write into all draggable elements. There aren't many other options; we need a link because links are reliably focusable in all browsers (OK, form fields are, too. You could use a checkbox, I suppose); and putting the link inside the draggable element seems the most logical placement (you could place them elsewhere, I suppose, but how is the user to know which link triggers which element?)
I decided that the element would be released when the user presses Enter or Escape; more or less because I couldn't think of any other keys. If you opt for other keys you should add the correct key codes here:
case 13: // enter case 27: // escape dragDrop.releaseElement(); return false;
The activation event is click. This event is accessible, since it reacts both to a mouse click and to an Enter key when the focus is on the element. Therefore the keyboard script can be activated by tabbing to the link and pressing Enter, or by clicking on the link.
(Strictly speaking, when you click on the link, the element is first activated in mouse mode (mousedown), then released (mouseup) and then activated in keyboard mode (click).
The rest of the events are more murky, though. The key events are a mess—especially when you want to read out the arrow keys.
The first problem is that we need an event that allows key repeating; i.e. if the user keeps the arrow keys depressed, the event should fire again and again, so that the dragged element keeps moving. By ancient custom we use the keypress event for this function.
Unfortunately IE does not fire the keypress event on arrow keys. That problem is partly offset by the fact that the keydown event in IE fires repeatedly. So superficially it seems as if we have to use keydown.
As you might have guessed it's not that simple. In Opera and Safari 1.3 the keydown does not repeat; so if the user keeps the key depressed, nothing happens after the first movement. In these browsers we therefore need keypress. (Mozilla and Safari 3 allow repeating both onkeydown and onkeypress; by far the most civilised solution as far as I'm concerned.)
So ideally we use the keypress event; if it's not supported we use the keydown event. But how do we switch events? How do we know if the keypress event is enabled?
My solution is to set an event handler for the keypress event. If this handler is executed, keypress is obviously supported and we can safely switch key events.
The startDragKeys
function sets event handlers for keydown and keypress:
addEventSimple(document,'keydown',dragDrop.dragKeys); addEventSimple(document,'keypress',dragDrop.switchKeyEvents);
Initially the keydown event triggers the dragKeys
function which performs the actual
dragging. This very first event always fires, and the element always moves. However, if we did nothing more,
the element would stop moving in Opera and Safari 1.3 .
That's why we also need keypress. The first keypress event triggers the switchKeyEvents
function,
which rearranges the event handlers:
switchKeyEvents: function () { removeEventSimple(document,'keydown',dragDrop.dragKeys); removeEventSimple(document,'keypress',dragDrop.switchKeyEvents); addEventSimple(document,'keypress',dragDrop.dragKeys); },
It removes the event handlers we just set and adds a new one: dragKeys
now fires on the
the keypress event instead of the keydown event. Since this function is only executed in browsers that
support keypress, we have switched key events from keydown to keypress only in these browsers.
When the user activates the link in the corner of the draggable element, startDragKeys
is called.
startDragKeys: function () { dragDrop.startDrag(this.relatedElement); dragDrop.dXKeys = dragDrop.dYKeys = 0; addEventSimple(document,'keydown',dragDrop.dragKeys); addEventSimple(document,'keypress',dragDrop.switchKeyEvents); this.blur(); return false; },
First it calls the startDrag
function we already discussed. It sends the relatedElement
to this function; a variable that contains a reference to the draggable element. (We set this variable in
initElement
.)
Then it sets the dXKeys
and dYKeys
variables to 0; these variables will keep
track of the change of position of the element.
Then the event handlers are set as discussed above.
Then (and this is a bit of a hack) it removes the focus from the link the user just clicked. I do this because of the Enter keystroke with which the user can release the dragged element. If I didn't remove the focus and the user hits Enter, the element is released, but immediately afterwards a click event would take place on the link, and the element would again be switched to drag mode. The net result would be that it's impossible to release the element by pressing Enter. If we remove the focus from the link, this problem disappears.
Finally it returns false
because the keystroke the user uses to activate the element should
not perform its default function (i.e. the Enter should not cause the link to be followed).
The function dragKeys
is responsible for the dragging by keystrokes.
dragKeys: function(e) { var evt = e || window.event; var key = evt.keyCode;
We start by reading out the code of the key the user pressed. (See also the Detecting keystrokes page.)
Then we use a switch
statement (see section 5H of the book) to decide
what we need to do about the keystroke. Purpose of this part of the script is to update dXKeys
and dYKeys
,
which contain the number of pixels the element has moved since the start of the drag and drop.
switch (key) { case 37: // left case 63234: dragDrop.dXKeys -= dragDrop.keySpeed; break; case 38: // up case 63232: dragDrop.dYKeys -= dragDrop.keySpeed; break; case 39: // right case 63235: dragDrop.dXKeys += dragDrop.keySpeed; break; case 40: // down case 63233: dragDrop.dYKeys += dragDrop.keySpeed; break;
The author sets the amount of pixels per keypress event in the keySpeed
variable. When the user
presses the left arrow, this amount is subtracted from dXKeys
, when he presses the right arrow
it's added. The same goes for up and down: then dYKeys
is adjusted.
The script also contains the cases 63232-63235. These are for Safari 1.3, which doesn't use the normal
keyCodes
37-40 for the arrow keys. (Safari 3 does, by the way.)
case 13: // enter case 27: // escape dragDrop.releaseElement(); return false;
If the user presses Enter or Escape the element is released (see below) and the function ends.
If you wish to change the keys that release the element, add their keyCodes
here.
default: return true; }
If the user presses any other key, the event is allowed to take its default action (i.e. the thing that would normally happen when that key is pressed) and the function ends.
dragDrop.setPosition(dragDrop.dXKeys,dragDrop.dYKeys);
Now dXKeys
or dYKeys
is updated, and we send both to the setPosition
function that changes the position of the dragged element.
if (evt.preventDefault) evt.preventDefault(); return false; },
Finally we have to prevent the default action of the key press; i.e. if the user presses the down arrow,
the page should not scroll down. This is done by calling the preventDefault
method of the event
in W3C compliant browsers, and by returning false
in IE.
When the user releases the element, releaseElement
is called. It removes all event handlers
the script might have set, removes the class "dragged", wipes the draggedObject
and waits for
new user actions.
releaseElement: function() { removeEventSimple(document,'mousemove',dragDrop.dragMouse); removeEventSimple(document,'mouseup',dragDrop.releaseElement); removeEventSimple(document,'keypress',dragDrop.dragKeys); removeEventSimple(document,'keypress',dragDrop.switchKeyEvents); removeEventSimple(document,'keydown',dragDrop.dragKeys); dragDrop.draggedObject.className = dragDrop.draggedObject.className.replace(/dragged/,''); dragDrop.draggedObject = null; }
You probably want to do something with the element once the user releases it. You can add your own function calls to the end of this function.