Scroll window when dragging Scriptaculous Sortables to edges

Written . Tagged JavaScript.

I’m using Scriptaculous Sortables for the admin pages of an artist’s portfolio: you can drag-and-drop sections, categories and images to change their displayed order, copy images between categories and so on.

When the list of images or categories grows long, sometimes you need to drag something vertically across several screenfuls. Alas, whereas Sortables allegedly have some support for scrolling containers, there is nothing built-in to scroll the entire viewport when necessary, which is what I wanted.

Google led me to one solution by Josh Goldberg. I implemented it, but it didn’t quite do what I want.

For one, it would check whether to scroll the viewport on the onChange event, which for Sortables is only triggered when the sort order changes. Also, whether to scroll at this time was determined by whether the top of the element was at a certain proximity to the top or bottom of the viewport, so the tops of tall elements would never get close enough to the bottom of the viewport to trigger the scroll.

So I made some modifications. Much of this code is by Mr. Goldberg. I’m assuming I am free to modify and redistribute it.

When creating Sortables, define the starteffect and endeffect like so:

1
2
3
4
5
Sortable.create('drag_images', {
  
  starteffect: scrollStart,
  endeffect: scrollEnd
});

That defines the functions to be run when the drag starts and ends.

The rest of the code follows. The idea of it is this:

When you start dragging, what element is being dragged is stored, and a function is triggered at short intervals (every 100 milliseconds) until the dragging stops.

Each time it is triggered, that function checks whether the top of the element is close (20 pixels) to the top of the viewport, or else if the bottom of the element is close to the bottom of the viewport. If so, it scrolls a ways (30 pixels) in that direction.

Code:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
var itemBeingDragged;
var scrollPoll;

function getWindowScroll() {
  var T, L, W, H;
  var w = window;
  with (w.document) {
    if (w.document.documentElement && documentElement.scrollTop) {
      T = documentElement.scrollTop;
      L = documentElement.scrollLeft;
    } else if (w.document.body) {
      T = body.scrollTop;
      L = body.scrollLeft;
    }
    if (w.innerWidth) {
      W = w.innerWidth;
      H = w.innerHeight;
    } else if (w.document.documentElement && documentElement.clientWidth) {
      W = documentElement.clientWidth;
      H = documentElement.clientHeight;
    } else {
      W = body.offsetWidth;
      H = body.offsetHeight
    }
  }
  return { top: T, left: L, width: W, height: H };
}

function findTopY(obj) {
  var curtop = 0;
  if (obj.offsetParent) {
    while (obj.offsetParent) {
      curtop += obj.offsetTop;
      obj = obj.offsetParent;
    }
  }
  else if (obj.y)
    curtop += obj.y;
  return curtop;
}

function findBottomY(obj) {
  return findTopY(obj) + obj.offsetHeight;
}

function scrollSome() {
  var scroller = getWindowScroll();
  var yTop = findTopY(itemBeingDragged);
  var yBottom = findBottomY(itemBeingDragged);

  if (yBottom > scroller.top + scroller.height - 20)
    window.scrollTo(0,scroller.top + 30);
    else if (yTop < scroller.top + 20)
    window.scrollTo(0,scroller.top - 30);
}

function scrollStart(e) {
  itemBeingDragged = e;
  scrollPoll = setInterval(scrollSome, 100);
}

function scrollEnd(e) {
  clearInterval(scrollPoll);
}

I’m assuming this works with pure Draggables (that are not Sortables) as well, but I haven’t actually tested that.

I’ve tested this code in Firefox 2.0 and Safari 2.0.4 for OS X, since that’s all I needed it for. If it breaks in other browsers, let me know.