Skip navigation.

The Roost

Programming, palaver, puffins!

Posts tagged with "code"

Drag and Drop on a Shoestring

, , , ...

While a dynamic three-column layout might be the Holy Grail of the CSS world, cross-browser drag and drop may very well be a similar prize in Javascript. There are plenty of libraries out there designed to take into account every angle, but because they consider all cases, usually they're huge and sometimes even noticably slow.

So what if you just want to take care of the 95% case, use something totally self-contained and tiny at the same time? Well, just follow these six steps to make your elements real dragons! :smile:

Step 1 - Choose Your Element
First things first: what do you want to drag? You'll need to know how to reference it through the DOM using javascript. This is usually as easy as applying an id attribute to the element and using var elem = document.getElementById('id');. That was easy, no?

<script type="text/javascript">

window.onload = function() {
  var elem = document.getElementById('dragon');

};

</script>

...

<span id="dragon">I'm draggable!</span>

Step 2 - Apply an OnMouseDown event
To make your element draggable, all you need to apply to it is one event. It gets somewhat more complex inside the event function, but in the element context, this is all you need.

<script type="text/javascript">

window.onload = function() {
  var elem = document.getElementById('dragon');
  elem.onmousedown = function(e) {
    e = e || event;

  };
};

</script>

...

<span id="dragon">I'm draggable!</span>

e = e || event is cross-browser code so that both Opera/FF/Safari and versions of IE can all reference the event object - which is generated each time the event fires - by referencing the e variable.

Step 3 - Store Information On The Element Itself
Once the mousedown happens, a drag may or may not follow. In either case, we're going to assume one is imminent and store some useful values before we can no longer access them. First, we'll want to change the element's position CSS style to relative so we can drag it away from where it currently sits. Next, we'll want to store the current top and left styles so we know where the element will start from. (If you have previously positioned this element using right and/or bottom you'll need to use those instead). And finally, we'll want to record the position of the mouse cursor at the moment of the mousedown; we can get this information from the event object.

The trick here is that we are storing these values on the element itself. Since the element is always in context, attaching these values to the actual object we're dragging makes things quite convenient. This avoids the use of global variables which can potentially conflict with other scripts you may be using.

<script type="text/javascript">

window.onload = function() {
  var elem = document.getElementById('dragon');
  elem.onmousedown = function(e) {
    e = e || event;

    this.style.position = "relative";
    this.posX = Number(this.style.left.replace(/px/, ''));
    this.posY = Number(this.style.top.replace(/px/, ''));
    this.mouseX = e.clientX;
    this.mouseY = e.clientY;

  };
};

</script>

...

<span id="dragon">I'm draggable!</span>

Step 4 - Apply OnMouseMove and OnMouseUp Events to the Document
But aren't we moving the element? Sure we are, but we want to record all mouse movements from here on out, not just the ones which appear over the element we are dragging. For example, if you move your mouse fast enough while dragging, for some instants the cursor will be outside of the element and that event will not register with the element at all.

So instead, we'll apply the appropriate events to the document.documentElement instead. The document.documentElement is everywhere so there is no chance of us missing one of these events, unless the mouse moves entirely outside the viewport.

<script type="text/javascript">

window.onload = function() {
  var elem = document.getElementById('dragon');
  elem.onmousedown = function(e) {
    e = e || event;
    var self = this;

    this.style.position = "relative";
    this.posX = Number(this.style.left.replace(/px/, ''));
    this.posY = Number(this.style.top.replace(/px/, ''));
    this.mouseX = e.clientX;
    this.mouseY = e.clientY;

    document.documentElement.onmousemove = function(e) {
      e = e || event;

    };
    document.documentElement.onmouseup = function(e) {
      e = e || event;

      document.documentElement.onmousemove = null;
      document.documentElement.onmouseup = null;
    };
  };
};

</script>

...

<span id="dragon">I'm draggable!</span>

Two extra things to note in this addition. One is that we added two lines to the onmouseup function that apply a value of null to the onmousemove and onmouseup events of the document.documentElement. What's going on here? Well, these two lines occur as the last action of the mouseup event which cancel the functions previously set on the document.documentElement. Effectively, this just returns things to the calm and peaceful way they used to be.

The second addition is the var self = this line. We do this so that we have a variable to refer to the draggable element once we go inside the two document.documentElement functions. Once we are inside these functions, the variable this becomes a reference to the function's parent - in other words the document.documentElement itself - and it's previous meaning will be lost. By assigning the this variable to another variable (and the name self isn't special in this regard, I could have called it anything) we can then use it as a reference to the element from within the enclosed functions.

The skeleton of our drag and drop routine is now done! Nothing visual happens at the moment, but if you try dragging the element in a browser window, all the events required to perform a dragging action are firing away. Now we just have to do something visual with all those events.

Step 5 - Assume The Position
Now we're getting somewhere. All we need to do now is appropriately position the draggable element each time we recieve a new mousemove event with updated mouse cursor coordinates.

In doing this, we're going to have to get through some basic algebra, but hopefully you should understand it without any trouble. When the mousedown event was received, we recorded the position of the mouse. When we receive a mousemove event, we again receive the position of the mouse. If we subtract one value from the other (x-coord - x-coord and y-coord - y-coord) we end up with a value which tells us how far away from the starting point the mouse has moved. We use these values to adjust the original top and left styles - which we also recorded - to move our draggable element around the screen!

<script type="text/javascript">

window.onload = function() {
  var elem = document.getElementById('dragon');
  elem.onmousedown = function(e) {
    e = e || event;
    var self = this;

    this.style.position = "relative";
    this.posX = Number(this.style.left.replace(/px/, ''));
    this.posY = Number(this.style.top.replace(/px/, ''));
    this.mouseX = e.clientX;
    this.mouseY = e.clientY;

    document.documentElement.onmousemove = function(e) {
      e = e || event;

      self.style.left = (e.clientX - self.mouseX + self.posX) + "px";
      self.style.top = (e.clientY - self.mouseY + self.posY) + "px";
    };
    document.documentElement.onmouseup = function(e) {
      e = e || event;

      document.documentElement.onmousemove = null;
      document.documentElement.onmouseup = null;
    };
  };
};

</script>

...

<span id="dragon">I'm draggable!</span>

You know what? You're done! The code above works passably in Opera, Firefox, IE6 & 7, and Safari. And you thought more was required for drag and drop?

Step 6 - Customise
The code can be used as a starting point to create a full-featured drag and drop interface, implementing such things as limits (apply max and min to left and top in the onmousemove function) and/or actions to be performed on drop (execute statements within the onmouseup function). For example, here is a modification of the code above which tells you the distance you moved the element, and also returns it to its original position.

<script type="text/javascript">

window.onload = function() {
  var elem = document.getElementById('dragon');
  elem.onmousedown = function(e) {
    e = e || event;
    var self = this;

    this.style.position = "relative";
    this.posX = Number(this.style.left.replace(/px/, ''));
    this.posY = Number(this.style.top.replace(/px/, ''));
    this.mouseX = e.clientX;
    this.mouseY = e.clientY;

    document.documentElement.onmousemove = function(e) {
      e = e || event;

      self.style.left = (e.clientX - self.mouseX + self.posX) + "px";
      self.style.top = (e.clientY - self.mouseY + self.posY) + "px";
    };
    document.documentElement.onmouseup = function(e) {
      e = e || event;

      self.style.left = "0px";
      self.style.top = "0px";
      
      document.documentElement.onmousemove = null;
      document.documentElement.onmouseup = null;

      var distance = Math.round(Math.sqrt(
        Math.pow(e.clientX - self.mouseX, 2) +
        Math.pow(e.clientY - self.mouseY, 2)
      ));

      alert("You dragged the element a distance of " + 
             distance + " pixels!");
    };
  };
};

</script>

...

<span id="dragon">I'm draggable!</span>

Keep in mind that this code only applies to one particular case. If the element you're starting with is already absolutely positioned, you'll have to prepare it differently. You may need to find out the element's actual position within the document rather than relying only on recording the distance the mouse has moved. This handy function from Quirksmode.org can help with that.

If you need to know where an element was dropped, that is a whole other can of worms. The easiest situation is if your potential target elements are in fixed and absolute positions relative to the element you're dragging. So you can tell over which other element your dragged element was dropped just through some coordinate adding and subtracting. To find an arbitrary drop point, you'll have no other cross-browser recourse but to examine the page positions of all possible targets so see if they contain the coordinates of the mouse cursor during the mouseup event. Complicated, but definitely not impossible if you're determined.

Hopefully this script framework helps you get a head start on custom drag and drop functions. Good luck!

Improve (?) Your Javascript Coding Style

, , , ...

I've gotten pretty good at javascript over the years, and I've gotten to the point where I try to do things using the bare minimum number of bytes, while still having the code readable and looking nice. In this short article, I'll outline a few of my own little coding style tips to help keep your code looking tight.

Keep in mind that, for the most part, these aren't optimisations. They won't make your code run significantly faster, but will merely help you accomplish the same thing in fewer bytes. And to me, fewer bytes often means more elegant algorithms.

The By-Value Operators
This is a simple thing. You probably already know that a = a + b can be shortened with the Add-by-value operator to a += b; But you might not realize that almost all discrete math operators can work this way:

a = a + b;   becomes   a += b;   // addition
a = a - b;   becomes   a -= b;   // subtraction
a = a * b;   becomes   a *= b;   // multiplication
a = a / b;   becomes   a /= b;   // division
a = a % b;   becomes   a %= b;   // modulus
a = a << b;  becomes   a <<= b;  // bitwise shift left
a = a >> b;  becomes   a >>= b;  // bitwise shift right
a = a >>> b; becomes   a >>>= b; // zero-fill bitwise shift right
a = a & b;   becomes   a &= b;   // bitwise AND
a = a | b;   becomes   a |= b;   // bitwise OR
a = a ^ b;   becomes   a ^= b;   // bitwise XOR

Merging assignments
A cool thing about javascript, and many other computer languages, is the fact that assignments not only do the task of assignment, but they also return a value. This means that the following code will alert "Hello World!" as well as assigning it to the foo variable for later use.

alert(foo = "Hello World!");

Say we want to prompt the user for some info, check to see if they actually typed something, and if so store the contents into the foo variable. We can do this all in one step!

if (foo = prompt("Type a value for foo:")) {
  alert("You typed: " + foo);
} else alert("You didn't type anything!");

This not only works with the = operator, but with any operator which assigns a value. Consider the following code:

while (i < 30) {
  i++;
  // ... Do something
}

The increment operator assigns a value of i + 1 to i, after it has returned the i value. However, why have it on it's own line? The while loop expression is evaluated every loop, let's put the assignment there instead:

while (i++ < 30) {
  // ... Do something
}

Comma separation of assignments also plays a part in reducing the amount of code used. You can use comma separation to string together multiple assignments in various contexts:

var foo = 1;
var bar = 2;
var baz = 3;

// is equivalent to:

var foo = 1, bar = 2, baz = 3;

This also works in the for loop context:

var y = 0;
for (var x = 0; x < 10; x++) {
  y++;
  // ... Do something
}

// is equivalent to:

for (var x = 0, y = 0; x < 10; x++, y++) {
  // ... Do something
}

That neatens things up nicely, if I do say so myself. Lastly, we can assign the same value to multiple values in one go:

var x = 1, y = 2, z = 3;

x = 4;
y = 4;
z = 4;

// is equivalent to:

var x = 1, y = 2, z = 3;

x = y = z = 4;

This works because the javascript engine first assigns the number 4 to variable z, then that assignment returns the value 4 which gets assigned to y, then that assignment returns 4 which gets assigned to x!

The only caveat with these methods are that they don't all work if you are also using var to define the variable at the same time. For instance, the following example will cause a syntax error:

if (var foo = prompt("Type a value for foo:")) {
  alert("You typed: " + foo);
} else alert("You didn't type anything!");

You need to forego using var (which I don't recommend), or define foo as an in-scope variable beforehand.

The Boolean Flip
Say you have a boolean variable acting as a switch. A user action can turn it on (== true) and the same user action can turn it off (== false). Just like a light-switch! :smile: When I first started using javascript, I was implementing these switches like so:

var foo = false;

if ([user action]) {
  if (foo) {
    foo = false;
  } else foo = true;
}

That's quite a lot of code for something which should be simple. Then I learned the ( ) ? : syntax which allows you to use the if structure directly within assignment statements, so I started using this:

var foo = false;

if ([user action]) {
  foo = (foo) ? false : true;
}

Awesome! All on one line! I used this method for quite some time until I stumbled across an even more compact method within someone else's code. I was using the following method up until very recently:

var foo = false;

if ([user action]) {
  foo = !foo;
}

Here we assign the NOT value of foo to the foo variable. The NOT value is the always the opposite boolean value of the variable. So if foo is true, NOT makes it false, and if foo is false, NOT makes it true! We sure have come a long way from three lines and 45 bytes down to one line and 11 bytes! But there's an even smaller way. :smile:

Consider the scenario that we're dealing with really long variable names, or deeply nested objects. Suppose we have a boolean variable named myObject.property1.subproperty5.foo. Using the method above, we get:

var foo = false;

if ([user action]) {
  myObject.property1.subproperty5.foo = !myObject.property1.subproperty5.foo;
}

Man, we have to repeat the whole variable name; that's a lot of duplicated bytes. Isn't there a way we could do it so we only have to write it once? You could try prototyping a function onto the javascript Boolean object, but it has a funny way of dealing with its own evaluation which means you can't assign it from prototyped functions. So how?

The key lies in how javascript treats other types of data. In javascript, the values false, 0, null, undefined, NaN and "" (the empty string) all evaluate to false, while anything else evaluates to true. Is there some kind of operator we can apply so that true becomes something that evaluates to false, and false becomes something that evaluates to true? There is! It's the XOR operator: ^. XOR chiefly operates on binary data, taking two bits (0 or 1), comparing them; if they are different return 1 and if they are the same return 0. XOR works on booleans by first casting the true or false value into a 1 or 0 binary digit respectively, then working its magic. Example output follows:

var foo = true, bar = false;

var baz = foo ^ foo;   // false
var baz = foo ^ bar;   // true
var baz = bar ^ foo;   // true
var baz = bar ^ bar;   // false

If we examine the first and third lines, we see an interesting thing. Any boolean XORed with a value of true becomes the opposite of what it was! true ^ true = false and false ^ true = true. Then using the XOR-by-value method mentioned earlier, we can finally boil this code down to:

var foo = false;

if ([user action]) {
  myObject.property1.subproperty5.foo ^= 1;
}

The number 1 is equivalent to true and only takes one byte rather than four. And there you have it, the Boolean Flip using only the variable name and 5 extra bytes, two of which are whitespace! :smile: After this operation, the variable won't be a boolean anymore, but rather either a 1 or a 0. These values can be used in all situations where booleans are used unless you require strict equality (===) which is usually never.

What byte-saving javascript tricks do you know?
November 2009
S M T W T F S
October 2009December 2009
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 28
29 30