Drag and drop

This article has been translated into French.

Here's a simple accessible drag and drop script. It works with both mouse and keyboard.

This is a drag and drop element with position: absolute.
This is a drag and drop element with 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.)

Use

  1. Copy the dragDrop object you find below on this page.
  2. Copy my addEventSimple and removeEventSimple functions; the object needs them.
  3. Set the keyHTML and keySpeed properties to the values of your choice (see below for an explanation).
  4. Make sure that all elements you want to be drag and droppable have position: absolute or fixed.
  5. Send all elements you want to be drag and droppable to the object's 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'));
  6. The script automatically adds a class="dragged" to an element that's being dragged. You can use this for some CSS effects.
  7. If you want to do something with the element once the user has dropped it, add your own function calls to the releaseElement function.

Properties

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.)

dragDrop object

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;
	}
}

What a drag and drop is

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.

Basics

Let's first review some basics.

Initialising an element

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.

Basic positioning data

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.

Clarification of startX and startY

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

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.

The events

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).

Mousedown

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.

Clarification of initialMouseX and initialMouseY

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.

Mousemove

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.

Clarification of dX and dY

Mouseup

When the user releases the mouse, releaseElement is called. We'll discuss that function later.

The keyboard script

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.

Basic interaction

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 events

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.

Initialising the key script

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).

Dragging by keystrokes

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.

Clarification of key movement

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.

Releasing the element

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.