Scriptaculous Sortable: onUpdate and onChange events
Today I was working with some nexted Sortable lists using the Sortable class from Scriptaculous. My list looked something like this.
HTML:
For some reason the onUpdate event did not seem to fire. I found a post here mentioning that not only do the list items have to each have unique ID's, which mine clearly do, but they must also following a naming scheme something like [name]_[id] which mine seems to follow as well. Upon further investigation, it turns out that it needs be formatted with an [name]_[integer]. I assume this is required in order for the class to offer the sequence method.
Time to switch to jquery already.
I have heard good things about the documentation compared to prototype. I suppose I should give it a fair chance, have yet to use it to be honest.
I've been trying to find a way to use Scriptaculous sortables to allow the user to sort items into a weak ordering. (A weak ordering is an ordering that allows more than one item per position. Two common use cases: 1. A voter who is rearranging the candidates according to her order of preference may be indifferent between some of the candidates, so she needs to be able to place those candidates at the same position. 2. Tasks in a priority list may be dragged/dropped into the same priority position, so they'll share the available resources and take precedence over tasks of lower priority.)
Sorting into a weak ordering is different from typical Scriptaculous sorting because a new position must be created on the fly whenever an item is dragged into the margin between two non-empty positions, in case the user wants to drop it between those two positions. (Also, whenever a position is emptied by having its last item dragged away, the position must be deleted, or at least hidden.) From what I've read, Scriptaculous doesn't seem to support inserting new droppables on the fly; presumably when Sortable.create is called Scriptaculous builds an internal list of droppable elements specified by the containment parameter. So, typical Scriptaculous apps load (or dynamically create) all droppable containers before invoking Sortable.create. I haven't found any examples that demonstrate the capability of inserting new droppables on the fly.
Perhaps the onChange callback could be used successfully to create and delete positions on the fly, but I don't know yet if that can be made to work without a thorough understanding of how Scriptaculous works internally, and it might even be necessary to hack Scriptaculous to make it work. If this requires an understanding of Scriptaculous internals, a new version of Scriptaculous could break my app.
Have I been going about this the wrong way? A weak ordering is something like a one level tree. Perhaps Scriptaculous' sortable tree functionality is already capable of what I need.
I've been frustrated by Scriptaculous' poor documentation. (I'm also a bit disturbed by how overdue the latest version is, and by all the aging open bug tickets.) So, a few minutes ago I gave jquery UI a try, as Ken Sykora suggested above. The jqueryui.com website provides a simple demo of sortable (http://jqueryui.com/demos/sortable). Unfortunately, the demo performs so slowly at dragging that I consider it unusable. Their demo of draggable is slow too, so I doubt I could make it work adequately.
How about YUI or dojo? The YUI dragdrop demo (http://developer.yahoo.com/yui/examples/dragdrop/dd-reorder.html) performs reasonably fast. (It seems sluggish when snapping a dropped item into place, but perhaps the snap effect was intentionally set to be slow.) The dojo/dnd demos (http://docs.dojocampus.org/dojo/dnd#available-tests) perform fast too.
Since my intent is to use Ruby On Rails to construct a website that implements the best-ever voting method (Maximize Affirmed Majorities) it's natural to prefer Scriptaculous when feasible since Rails is partial to it, but I'm sure there are Rails website developers who find reasons to dump it (and Prototype too) in favor of YUI, dojo, or jquery.
Regards,
Steve 9-21-2009
I checked out the jQuery demo that you linked in FF 3.5 and IE8 and to me it seems to have pretty snappy performance, so I am not sure if I am possibly not doing the same thing as you when you noticed the sluggish performance. As far as the YUI and dojo, I will have to check those out when I get a bit more time.
I did stumble across your attempt at using scriptaculous to achieve this weak ordering and I noticed you mentioned there was some bugs when using it in IE7. I tested it in FF 3.5 and IE8 and it seemed to work comparably in both.
The one thing I don't fully understand about what it is you are trying to accomplish is that the items are still sortable within the three positions you are dropping them, which to me, implies that you are still giving one item precedence over another within a single position. Are you going to treat the order of the items within a position as arbitrary? If this is the case and you are wanting to just drop in items in to one of several ordered positions, why not just use normal draggable and droppable classes from scriptaculous with your items being the draggables and the positions being the droppables? Let me know if this makes sense or if I still don't have a clear picture of what it is you are trying to accomplish.
Hi Scott, thanks for replying so quickly!
Right, the relative order of items within the same position is arbitrary and won't matter. In a weak ordering, all items in the same position will be treated as equals.
I have a demo page that's newer than the code you probably saw:
http://alumnus.caltech.edu/~seppley/nonstrict_dragdrop_demo.html
It's still incapable of doing all that's needed since it only provides a fixed number (3) of droppable positions, but it works a little better than the code I posted in the bug ticket, and it has additional explanatory text.
To answer your last question: All examples of Scriptaculous sortables that I've looked at have a fixed number of droppables, all loaded prior to executing any Sortable.create calls. (That's consistent with the documentation for Sortable.create, which says all droppables must be loaded prior to calling Sortable.create.) Unfortunately, if my understanding is correct, to implement a weak ordering the number of droppable positions being displayed needs to vary, to allow the user to drop an item into a new position *between* other items' positions. This means additional droppables may need to be inserted into the display on the fly during dragging. Also, a position needs to be removed from the display immediately when its only remaining item is dragged away, emptying it.
Here's an example to illustrate. Suppose the user has been dragging some candidates and has reached this configuration:
1. Barack
2. Joe, Hillary, John, Mitt
Next she wants to place Hillary between Barack and the rest. As she drags Hillary above Joe, John & Mitt, a new position must appear between Barack and the rest, in case she wants to drop Hillary there. (Which she does.)
1. Barack
2. Hillary
3. Joe, John, Mitt
(If instead she continues dragging Hillary upward to Barack or changes her mind and drags Hillary back down, the newly inserted position becomes empty and must be removed.)
If I'm overlooking some simple technique to implement this, I hope someone will clue me in!
For the moment, I'm assuming I'm not clueless and it's not simple. Here's a set of techniques that might work:
1. Prepare a large collection of hidden (display:none) droppables before making any calls of Sortable.create. Later when Sortable.create is called for each droppable (hidden and unhidden), the containment parameter will include all droppables (hidden and unhidden). This will satisfy the documented requirement that all droppables must be loaded before calling Sortable.create.
2. I think there will need to be a nearly hidden (height: 1px?) empty droppable above and below each normal non-empty droppable, so that whenever an item is dragged out of a normal droppable, it will be hovering over a nearly hidden droppable. To arrange this means interleaving some of the hidden droppables with the normal non-empty droppables, and changing their css class from hidden to nearly hidden.
3. The hoverclass parameter of sortable can be used to make any nearly hidden droppable grow (height:auto) when an item is dragged over it. The growth will make it appear that a position has been inserted. (I expect that all the positions and items below it will appear to jump downward to make room.) If the item is dragged away without dropping it there, the droppable will shrink to nearly hidden size again, making it appear the position has been removed.
4. Hopefully the onUpdate handler can perform the rest of the needed steps:
4.1 When an item is dropped into a nearly hidden droppable (which terminates the hovering condition) change the class from nearly hidden to normal so the droppable won't wrongfully shrink back to nearly hidden size.
4.2 Maintain the interleaving. The dropping of an item into a nearly hidden droppable and/or the emptying of a normal droppable usually undermines the interleaving. So, in the case of a droppable that just changed from nearly hidden empty to normal non-empty, maintenance probably involves moving two droppables from the hidden collection, inserting one above and one below the changed droppable, and changing their css class from hidden to nearly hidden. In the case of a normal droppable that just became empty, it may involve moving one or two droppables to the hidden collection (changing their css class to hidden) and/or it may involve changing the emptied droppable to nearly hidden. (My intuition is that the algorithm to maintain the interleaving may involve optimizing between simplicity and speed.)
Hmm, now that I think about it, onChange might be needed too, to immediately handle the case where a normal droppable is emptied by having its last item dragged away. The emptied droppable should disappear right away, not waiting for the item to be dropped somewhere.
I'm unsure whether the nearly hidden droppables will need a height significantly larger than 1px. When dragging an item upward, if the nearly hidden droppable into which the item encroaches is too short, the user might drag upward past the droppable too quickly, not giving enough time to drop the item into it before it becomes empty and disappears again. (I don't think expect a similar problem dragging downward, since the lower positions and items will be pushed downward, staying away from the mouse pointer, as the hoverclass growth takes effect.) Unfortunately, increasing the height involves a tradeoff, since all the nearly hiddens would be much more visible (ugly). Perhaps a tall margin for the nearly hiddens would suffice, instead of using a significant height.
The onUpdate handler and the interleaved nearly hidden droppables would actually need to be a little more complicated, since there are also two edge cases (literally): The user must be able to drop an item above the topmost non-empty position (creating a new top position) or below the bottommost (creating a new bottommost).
Ok, I've just posted another demo page:
http://alumnus.caltech.edu/~seppley/nonstrict_dragdrop_demo_0.2.html
It doesn't yet have an onUpdate handler, but it has interleaved nearly hidden droppables and it uses hoverclass to make them grow (height:auto) whenever an item is dragged over one. It may give you a better idea of the appearance of insertion that I'm trying to achieve. (To save you a few seconds if you play with it, initially each of the three normal droppables contains at least one item.)
I expect the easiest part of the onUpdate handler will be step 4.1, changing the css class from nearly hidden to normal, so I'll try to code that soon and then I'll post it.
Cheers,
Steve [9-22-2009]
I implemented the onUpdate handler described in my previous post. It manages the classnames of the droppables and maintains the interleaving.
The new page is at:
http://alumnus.caltech.edu/~seppley/nonstrict_dragdrop_demo_0.4.html
I'm hoping that all that remains to do is write code that dynamically builds the hidden droppables (twice as many as the number of draggables) and sets up the initial interleaving.
I wouldn't be surprised if there's more to it, though... I'm concerned that Scriptaculous may barf when insertion of a droppable moved from the hidden collection during interleave maintenance changes the html order of the droppables without matching changes of their order in Scriptaculous' internal objects. I'm also concerned about Internet Explorer quirks.
I think I need to restart Windows now. Firebug breakpoints seem to have left the Windows mouse in a weird state. Either that, or my page has become very quirky somehow, both Firefox 3.5 and IE7.
If anyone has suggestions for improvements, please let me know.
That's all for now,
Steve [9-23-2009]
For the past week I've been playing around with drag&drop sorting of weak orderings using Scriptaculous Sortables. It appears it's important to minimize the amount of processing done by the onChange handler, else Scriptaculous can fall behind the mouse movements when the user moves the mouse far and fast. (With some clever coding, it might be possible to defer low priority onChange processing, helping Scriptaculous keep pace with the mouse.)
One scheme that ought to minimize onChange processing is to set up a second column to contain the empty slots. It should be slim to avoid wasting more screen space than necessary, but not extremely slim since the empty slots must be wide enough to offer decent-size drop targets.
The purpose of the empty slots is to allow the user to drop an item into a new position between the positions of other items. (Recall that a weak ordering has a variable number of positions, and each position can hold 1 or more items. One extreme is to have only one position with all items in it. The opposite extreme is a linear ordering, with one item in each position and as many positions as there are items.) There are several advantages to having the empty slots in a different column. One is that the non-empty slots won't be jerked upward or downward during dragging by the appearance or hiding of empty slots, or by the changing heights of slots that go from empty to non-empty or non-empty to empty. With the empty slots in the same column as the non-empties, my attempts to prevent the slot containing the dragged item from being jerked away from the item (unacceptable!) have required a significant amount of onChange processing, which I haven't yet got to behave adequately in IE7. (Also, when I submitted my most recent page to browsershots.com to see how various browsers render the page before anything is dragged, IE8 didn't even run it, showing "error on page" in the status bar even though there's no error in IE7, IE6, FF, Chrome, or any other browser. Yay Microsoft.)
I expect putting the empty slots in a different column will waste less screen space than putting them in the same column. In the single-column implementation that's least wasteful (which is trickiest, with lots of onChange processing) there must be 2 empty rows (slots), dynamically relocated as an item is dragged. One extra column seems less than 2 extra rows. If this is true, the user won't need to drag items as far (saving time) and won't need to scroll as often (occasionally saving time and reducing uncertainty).
A potential disadvantage of the 2-column scheme is that it might be less intuitive for some users, since there's an additional dimension in which to drag, not just vertically. Without testing with many users, who can say for certain?
The user needs to know that the empty slots are effectively "between" non-empty slots, even though they aren't literally between onscreen. To make it visually as intuitive as possible, the empty slots in the second column should be staggered relative to the non-empty slots, so each empty slot is half higher and half lower than a top or bottom edge of a non-empty slot. (Perhaps a clever hover class could increase the "betweenness" when an item is dragged over an empty slot. For example, a hovered-over empty slot could grow wider, encroaching partway across the main column, and it could have a higher z-index so that when it grows wider it will appear in front of the plane of the non-empty slots. Also, perhaps that would be a good time for the onChange handler to shift the lower non-empty and empty slots downward, to make a gap in the main column that makes it even more obvious that the hovered-over slot will fit in the gap.)
The dragStart callback can be used to make the empty slots visible. (If preferred, they can be visible all the time. I don't know which the users would understand better, but my instinct is to have them hidden when nothing is being dragged so the user will focus on the draggable items.) The dragEnd callback can be used to hide the empty slots. If an item is dropped onto an empty slot, dragEnd must move the slot into the main column (perhaps using a morph effect) and build additional empty slots as needed.
Here are a few more implementation details, in case anyone wants to try the 2-column scheme:
(1) I intend to place the second column to the left of the main column so it will stay visible and in a constant position if a silly user shrinks the width of the browser window.
(2) If the item being dragged was alone in a slot when the drag started, it wouldn't make sense to place empty slots next to that slot, since the item was already between the positions above and below.
(3) If the item being dragged was alone in a slot when the drag started, you can allow that slot to remain visible (and empty) when the item is dragged away, in case the user changes her mind and wants to drag it back. However, there's an alternative. You might want the onChange handler to hide that slot when the item is dragged away. (OnChange would also need to unhide an empty slot in the second column, in case the user changes her mind.) But this may cause trouble. If you choose to try it, you might want the onChange handler to check whether the item was dragged downward, and if so, also unhide an "opacity zero" element above the topmost slot. Its sudden height would compensate for the hiding of the emptied slot, preventing the slot below from being unacceptably jerked upward away from the item. (Later, dragEnd would need to re-hide that element, so the element can be reused.)