Fixing slow drag and drop with scriptaculous

Published on Feb 18, 2009 by Jamie Munro

Recently on a project I was working on, I was tasked with fixing drag and drop that was terribly slow.  The drag and drop was implemented with scriptaculous on a calendar system.  When you clicked an event to drag it it took about 5 seconds before the page would actually let you drag it!  This was clearly unacceptable and it has to be possible because Google Calendar is lightening fast.

 The first thing I did was download and setup jquery to see if it was related to how scriptaculous was created.  After setting up jquery, it was just as slow.  This lead me to believe that it was a fundamental problem with how the drag and drop was set up in both libraries.



After ruling out the library, I dove right into the drag and drop code.  I used Firebug to put several debugging lines to help understand the flow of how the drag and drop worked.  Basically I put a console.log() call at the beginning of every function.

Within a few minutes I had narrowed down the problem to two functions: show() and isAffected().

After a bit more review it was clearly a combination of both.  Inside the show() function it was looping through each one of my droppable elements and calling isAffected which returned true or false if the draggable element was touching the droppable.

I did some quick math once I found out that each droppable was being checked and found at that because it's a weekly calendar there is 672 droppable elements (7 days * 24 hours * 4 [15 minute increments]).  Clearly there has to be a better way.

The next few things was a bunch of trial and error.  I found that the draggable element passes in an X and Y of where the mouse currently is.  Using this, I created a calculation to determine which 15 minute increment droppable block was being hovered over.

Inside of dragdrop.js here is the original show function that is slow:


show: function(point, element) {
 if(!this.drops.length) return;
 
 if(this.last_active) this.deactivate(this.last_active);
  this.drops.each( function(drop) {
   if(Droppables.isAffected(point, element, drop)) {
    if(drop.onHover)
     drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
    if(drop.greedy) {
     Droppables.activate(drop);
     throw $break;
    }
  }
});


My updated dragdrop.js show function is:


show: function(point, element) {
    if(!this.drops.length) return;
   
    if(this.last_active) this.deactivate(this.last_active);
 // Don't loop, call isAffected
 // isAffected will return the id of the element being dropped on
 // if null, we couldn't find it
      var drop = Droppables.isAffected(point, element);
  
      if(drop.onHover)
         drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
      if(drop.greedy) {
        Droppables.activate(drop);
        //throw $break;
      }
  },


Below is my updated isAffected function:


isAffected: function(point, element) {
 // Step 1, determine column
 // x position / colWidth
 // Step 2, determine row
 // y position / rowWidth
 var column = Math.floor((point[0] + this.drops[0].gridStartLeft) / this.drops[0].colWidth);
  // Get the original top position of the element
  var origTop = Droppables.getOffSetTop(element);
  origTop -= this.drops[0].gridStart;
  var row = Math.ceil((origTop) / 13) + 4;
  // (column - 1) * 24 * 4 + row
  var pos = (column - 1) * 24 * 4 + row;

return this.drops[pos];
  },


As a note, this will not work 100% out of the box for you, it will require some tweaking of variables.

So, let's break it down a bit.  The first thing it does is take the X position of the mouse and adds "gridStartLeft".  "gridStartLeft" is a pixel value from the left of the browser to the start of my calendar grid.  "colWidth" contains my fixed value of how wide each column is.  The following returns a number between 1 and 7.

Next up, we need to do a similar thing with the Y position.  The first thing we need to do is determine where our grid starts.  Below is the getOffSetTop function:


getOffSetTop: function(el) {
   var ot=el.offsetTop;
 while((el=el.offsetParent) != null) { ot += el.offsetTop; }
 return ot;
  },


Now we have a pixel value of where our column starts.  Now we remove the grid start from it.  "gridStart" is how many pixels from the top of the browser to where our grid starts.

Now our all important calculation to get the row number.  The value "13" in the division is how many pixels tall my 15 minute increments are, this should be changed to match yours.

With the two calculations done our column variable contains a number between 1 and 7 and our row variable contains a number between 1 and 96.  The last step is to perform our calculation to determine where in our 672 element array does it fit.  The second last line performs this calculation and the last one returns the droppable object that is affected by our draggable element being moved over it.

Once you are done modifying the dragdrop.js file you will also need to update where you instantiate your droppable elements to include new parameters for "gridStartLeft" and "colWidth".

After applying these tweaks the 5 second delay before dragging completely disappeared and now my calendar is as fast as Google Calendar!

Thanks for reading and hopefully this will save you one big headache and make a very happy client!

Tags: Javascript | Optimization | scriptaculous | jquery | JavaScript | jQuery

Related Posts

blog comments powered by Disqus