One of my side projects is a Javascript drag-and-drop list using the modern drag and drop specification with the goal of allowing users to rearrange the order of items in a list. I quickly ran into a problem: the drag-and-drop spec was designed for dragging one object onto another single discrete object which is expecting a drag event. This does not translate well to a draggable list's use cases of dragging above, below, and between objects (or subobjects), or of dragging an item off of the drag area to bring it to the top or bottom of the list. To do anything fancy, we need to find a relationship between the coordinates of the MouseEvent parent of a drag event, and the coordinates of the elements on the screen.
Visual elements have these coordinate attributes:
- offsetTop
- scrollTop
Drag events have these coordinate attributes:
- clientY
- pageY
- screenY
There is no correlation between the two sets of coordinates. I tried summing the offsetTop of an item and its ancestors but found no correlation between that sum and any of the mouse coordinates. I also had no luck using the various page and scroll properties for window and document. Since I couldn't find the answer, I changed the question. Element.clientHeight reliably works across browsers, so we can do this:
- Save the initial drag event at the start of a drag.
- Calculate the difference between the start and end events.
- Count the heights of the elements to see where to place the dragged item.
- If we run out of elements, place the dragged item at the head or tail of the list.
This should work. The MouseEvent gives us three sets of coordinates, so we should be able to pick one and it should work.
Hah.
Among the problems:
- In Firefox, the clientY of the starting DragEvent is zero. This is bug #505521 which has been open since 2009.
- In Safari 5.1.7, the clientY of the starting DragEvent is measured from top of window while the clientY of the ending DragEvent is measured from the bottom of the window.
- In Safari, the pageY of the ending DragEvent is some ridiculous number that seems to be measured from some point over 500px off the bottom of the screen.
- In both Firefox and Safari, the differences in clientY, pageY, and screenY are different for the same beginning and ending mouse position.
- In Opera, the Y values for MouseEvents ending on the sidebar panel are different from the Y values for MouseEvents on a page at the same vertical level.
I decided to use the difference in screenY, even though there is the obvious bug that the math will be wrong if the screen scrolls in the middle of a drag, because it produces the least number of compatibility problems across browsers.
Side note: The best practice for defining class methods in Javascript is to use the prototype:
ClassName.prototype.method = function(){...}
This allows every instance of the class to use the same function instead of giving each instance its own copy of the function.
Member variables are not in scope in prototype methods; the method is expected to use this to access them. In the context of an event handler, however, this is not the containing object. Therefore, using prototyped methods as event handlers is not a good idea. A solution is to use the old-fashioned "this.method=" declaration which suffers inefficiency but does the job:
function ClassName {
this.method = function(){...}
I ran into this problem when I tried to fix my old-style drag-and-drop code to use the best practice instead.
Recommended reading: Douglas Crockford's tutorial: Private Members in Javascript.