A Lesson For All Would-Be JavaScript Hackers
Here's a hacker's lesson I learnt the hard way — if you're going to make any changes or additions to a JavaScript library don't do it directly in the source code for that library.
Remember my Drag-n-Drop Sorting of Documents article from this time last year (Rough Cut? Huh!)? Here's a demo. It uses the scriptaculous drag and drop to convert a simple <ul> element in to a sortable list. Re-sorting this list sends a POST message to Domino to tell it the new order of the documents. To get this to work with Domino I had to hack the code a little..
The scriptaculous Sortable object has a method called serialize:
The Sortable object also provides a function to serialize the Sortable in a format suitable for HTTP GET or POST requests. This can be used to submit the order of the Sortable via an Ajax call. See Sortable.serialize
So, given a UL element with an id of "SortOrder" and three child LI items the serialize method will produce a string like:
SortOrder[]=IDofLI1&SortOrder[]=IDofLI2&SortOrder[]=IDofLI3
This can then be POSTed to the server for processing. Domino inerprets POST data with repeated keys as a multi-value item. The use of []s is a Rails/PHP convention which confuses Domino and so I had to get rid of them, so that the string would look like:
SortOrder=IDofLI1&SortOrder=IDofLI2&SortOrder=IDofLI3
To do this I found the serialize method in scriptaculous's dragdrop.js file and removed the [] from the line of code with the comment shown below.
var Sortable = { serialize = function(element) { element = $(element); var options = Object.extend(Sortable.options(element), arguments1 || {}); var name = encodeURIComponent( (arguments1 && arguments1.name) ? arguments1.name : element.id); if (options.tree) { return Sortable.tree(element, arguments1).children.map( function (item) { return name + Sortable._constructIndex(item) + "[id]=" + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); }).flatten().join('&'); } else { return Sortable.sequence(element, arguments[1).map( function(item) { return name + "[]=" + encodeURIComponent(item); //removed the [] from here! }).join('&'); } } }
This change I made in the source code itself. Cue one year later and I'm working on a new version of an application for a customer, which uses this sorting feature. This seems like a good time to upgrade the versions of Prototype and Scriptaculous in use to the most current, so I do. Expecting problems due to the upgrade, I test it all out, only to find the sorting feature is broken. After some messing about I found the extraneous [] characters and it dawned on me what I'd done wrong — the upgrade had over-written the old JS file and so my hack went with it.
To fix this the easiest option would have been to re-edit the scriptaculous dragdrop.js file and make the same change again. However, this would be stupid and have meant I'd learnt nothing from having wasted an hour of time scratching my head. What I needed to do was make the change, but not in the dragdrop.js file, so that any future upgrades wouldn't break the application.
What I did was leave the source code alone and then place the following snippet of code in my own "global.js" file:
Sortable.serialize = function(element) { element = $(element); var options = Object.extend(Sortable.options(element), arguments1 || {}); var name = encodeURIComponent( (arguments1 && arguments1.name) ? arguments1.name : element.id); if (options.tree) { return Sortable.tree(element, arguments1).children.map( function (item) { return name + Sortable._constructIndex(item) + "[id]=" + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); }).flatten().join('&'); } else { return Sortable.sequence(element, arguments[1).map( function(item) { return name + "=" + encodeURIComponent(item); }).join('&'); } }
As my global.js file was referenced below the scriptaculous files in the page's header it was loaded afterwards and so the original Sortable.serialize method was over-written by my own. Any upgrades to dragdrop.js in the future won't break my code.
Moral of the story being — if you're going to hack a JavaScript library do it in your own space.
Adendum (I can't now edit this post as all the <pre> formatting will be screwed up):
Anybody who has tried to use the drag sorting demo database in their own applications, which already had a copy of the scriptaculous files in it, might have been a bit stumped as to why it didn't work -- it had to use the dragdrop.js file included in the demo!. Sorry if I caused any undue stress.
Furthermore I've now updated the demo db for the original article and upload version 0.5, which shows the method I'm talking about here.
Overriding methods is always a safer bet. Good job on translating that topic. I ran into that temptation when I found that the YUI calendar had hard coded image paths.
You probably know this, but if you use list-style(-type):number (or an ordered list), the sort-order updates instantly. The power of CSS :)
For the user, it looks like it's instantly re-sorted..
Hi Tommy. No, I didn't know that. Nice tip. Thanks. I'll add in to the db now...
Hmm. Ie6 no like it. They all end being 1. Works great in FF though!
It looks like it has something to do with the style-attributes that are set by scriptaculous.
I've found a quick-and-dirty fix for that, although I'm not sure that it would work every time.
Try adding this when you submit the changes:
if(typeof ActiveXObject =="function"){
$('NewOrder').innerHTML = $('NewOrder').innerHTML.replace(/style="filter.*1"/i,"style=\"position:relative\"");
}
What it does, is resetting the style-tag on the item dropped, to style="position:relative" (using case-insensitive regular expression).
I've only tried this using the Javascript-console bookmarklet in IE, where it worked.
Here is way I do not like prototype.
With YUI you can just extend it to create your custom object (YAHOO.extend):
{Link}
Way much proper.
First way -> why
sorry.
YoGi. Not sure if it's the same thing or not but Prototype does have an extend method.
@Tommy:
IE fails spectacularly to re-order list numbering when the list container or any of the elements has "hasLayout".
Reference:
{Link}
My quick-and-dirty-fix removed the listeners on the node. :
This bookmarklet does the same (and sets the position to static, like the article suggested):
javascript:void $$('#NewOrder li').each(function(node){node.removeAttribute("style");node.style.position="static";})
Some of the nodes still are out of order after drag/drop (I override the list-style using web accessibility toolbar).. Could maybe have something to do with styles from the stylesheet?
The reason I'm taking an interest in this is that I had planned using CSS to display sort order for the user (making it seem like instant resorting), on a future project at work.
The real sorting would happen backend.
@Yogi: Extensibility is a part of Javascript, so any object you can prototype (not as in prototype.js but as in defining your own objects), you can later extend.
@Jake: Another way to tackle the problem would be to write an wrapper function that would call the scriptaculous method and then process the result for you. That way you don't override all that functionality (hence making your code responsible for more or as much stability as the method you are overriding) and you have a layer that, being specifically for the purpose of tweaking the outbound data, provides a centralized point for doing so.
So, you might have
// Domino specific wrapper for Scriptaculous.serialize()
function dominoSerialize(element) {
var tmpstr = serialize(element); // call to scriptaculous
regEx = /[\[\]]/g;
tmpStr = tmpStr.Replace(regEx,"");
return tmpStr;
}
I guess it boils down to an OO approach or an SOA approach. :-) Either is good - just depends on what your goals are. The later permits you to deflect concern about whether your code or theirs is the problem, if one arises.
I found a fix for IE and opera.. It comes down to display:list-item, and zoom:normal!important (and position:static for opera)
Instead of doing it from the browser, I actually downloaded ithe demo this time :)
ul.sort li{
cursor: move;
background-color:#f1f1f1;
margin:10px;
border:1px solid #CCC;
padding:4px;
display:list-item;
list-style-type:decimal;
zoom:normal!important;
list-style-position:inside;
}
/* opera hack */
@media all and (min-width: 0px){
ul.sort li{
position:static!important;
}
}
*erhm* The power of CSS! (finally)
with position:static (to get opera to show the numbers), the items doesn't float in the air, but they still re-sort.
list-style-position:inline puts the numbers inside the li-item, making it look just like the "hard-coded" numbers in your demo.
Let me know if it works.. I've might have forgotten something..
Thanks Tommy. It's obvious you know your stuff really well. I'll be keeping a keen eye on your blog hoping you make a go of it.
Thank you! Google is my friend.. :)
I also have to thank you for years of excellent code and examples to play with!!
I'm kinda lazy with the blog, but if I learn something I get really excited about, I'll probably post it..
The thing I'm most excited about right now is Mootools ({Link} I think it's 36kB packed with all included. Download a packed version for deployment in your applications, and an unpacked version with documentation for hacking/understanding how to extend the objects.
They have a really nice php-application that lets you select only what you need (with dependency-checks). I recently downloaded only the Cookie-class for an "older" project that mainly uses Prot./Sciptac. Works great!