A little earlier this year, a group of people here at Hofstra sat down and made a wish list of all the things we wished our portal could do but didn't. This wish list was based to some extent on our own experiences with Luminis, but also based on focus groups that had been run with faculty and students. I'll get back to the wish list in a future post-but one of the items that we listed was drag-and-drop channels.
Luminis' current channel layout system seemed to be too onerous for many users, and although we did allow users to customize one of their tabs-it seemed to be the rare user who bothered to do so. Wouldn't it be great if you could just drag your channels into place where you wanted them?
A couple of weeks later I went to Summit and sat at a presentation by Zach (Tirrell) in the developer's lounge(if you went to Summit but didn't go to the developer's lounge-you missed some great sessions). He demonstrated a number of cool things he was doing with Luminis-one of which was, you guessed it-drag and drop channels. (Later I saw an even more eye-popping demo by Jon Wheat that did Drag & Drop but with channels that weren't bound to vertical columns-channels could actually span more than one column!)
I spoke to Zach briefly after his great presentation and he said that doing drag and drop was easy, and he had knocked it off in a few hours.
I was determined to try it this summer and it turned out Zach was right, it wasn't too hard. (I admit though, I didn't knock it off in a few hours-I think Zach was assuming the implementor would have talent like he did :-)
Here's how to do it:
Go to script.aculo.us and download the latest version(at the time of this writing it was 1.6.1). After unpacking your zip (or tar or whatever). This should (amongst many other things) give you eight javascript files(seven of them are in the src folder, prototype.js is in lib)-upload them to your luminis server. We uploaded them to a folder called js located in /opt/luminis/webapps/luminis/. This is essentially document root so you can reference the folder as /js.
Next, like all Luminis mods, we're going to hack up nested-tables. Normal disclaimers apply-back this file up, hack may cause side effects such as sudden portal death, can't sue me when your system explodes, etc...etc.
You'll see a place where a number of other js files are being included (like clientsniffer.js and util.js). Add the following lines:
The scriptaculous libraries included above actually do all the heavy drag and drop lifting-we're just adapting them to work with Luminis channels. We also create an array to handle our draggable areas. We'll use that a little later on.
Find the channel template further down in nested-tables. There will be a tag that says something like xsl:template match="channel". This is where we'll be doing most of the work. The first thing we need to do is to assign each channel a unique element name in the page(you would think Luminis would do this already, but it doesn't seem to be the case). Luckily each channel does have a unique identifier which we can pull with a little xsl. We'll do it like this:
Insert this right after the line that sets up the unused xsl detached parameter and near the front of the channel template section. This just assigns Luminis' unique identifier for the channel to an enclosing DIV. This DIV will contain the draggable area.
I'm making use of the xsl:attribute tags above, because you'll find the SAX parser chokes on a lot of things otherwise(there are ways around quirky SAX behavior using JavaScript-but its kind of kludgy-this is a much better solution).
You'll see that this DIV has been inserted right before an opening table tag. Make sure you close the DIV off after the closing table tag-(this is near the bottom of the channel template section of nested-tables).
But what if we don't want the entire channel to react to drag events-but only one part of it (typically this would be done at the top of the channel). For that we need a second DIV.
Right after the DIV code you inserted above there is a table with an identifier of 'channel'. Inside the first row, there is a TD that has a class of bg3. That TD controls the colored bar that runs along the top of the channel, and where we'll set up our channel handle (the place that user's can grab for dragging).
A few notes here-make sure you close off your DIV. This should be right after the code segment that looks like this:
The second thing to note about the channel handle div above is that it uses a class of bg3-so in the td before the div, you can safely remove the same attribute(class=bg3). [Some people might be wondering why we just didn't assign the channel handle id to the td and skip using a div altogether. The reason is that I've been basically unsuccessful in using a td for a draggable handle using scriptaculous.]
Now that all our divs are in place, we need to create the code necessary to move them around. Let's go to the bottom of the channel template area in nested-tables. Right before the end of the template (you'll see a </xsl:template> tag) we're going to insert the following code:
The thing that drives me crazy with working with nested-tables is having to restart the server between every change. It pushes development time through the roof. I don't know about anyone else, but I'm in desperate need of a 'reload nested-tables.xsl' button somewhere in the administrator.
To alleviate some of this hassle, whenever working with JS I try to move my code to external JS files-this allows you to make a change and see it instantaneously on browser refresh.
So, the code that actually handles the dragging you'll see I've stored in a file called DragChannel.js. There is actually two other lines of JavaScript previous to that-I don't include them in the DragChannel.js file because once you go this route you lose the ability to pull xsl variables. Two variables we need are the baseURL (it will be clear why later) and also the channel id that we set up earlier.
Let's take a look at the code in DragChannel.js
Droppables.add(ChannelId,{onDrop: function(DragDiv,DropDiv)
{
MoveURL = window.location.href.substr(0, window.location.href.indexOf("cp/")+3);
MoveURL = MoveURL + BaseActionURL.substr(0, BaseActionURL.indexOf("uP")) + "target.n7.uP";
MoveURL = MoveURL + '?action=moveChannelHere&sourceID=' + DragDiv.id +'&method=insertBefore&elementID=' + DropDiv.id;
DragArray[DragDiv.id].options.revert = false;
location.href = MoveURL;
}});
The first thing we do is take the element id of the channel and make that into a new draggable. You'll see that we set the handle for the draggable-and we also set 'revert' to true. This allows us to snap the channel back into place when its dropped. We also assign this new draggable to an associative array using the element id as a key (you'll see why this comes in handy later).
If you were to try out the code right now you would see that you can actually drag channels around in Luminis!
The next thing we do is create a place to drop the channel. We use the same element id-which means not only are all channels draggable, but all channels are valid areas to drop the dragged channel.
When channels are dropped they just snap back into place-this isn't very exciting. What we really want to do is to tell Luminis to insert the dropped channel above the channel its dropped on. We can do this by simply decoding the URL string that Luminis uses to normally reconfigure channel placement in the "Manage Content/Layout" screen.
One of those URL strings looks something like this:
tag.401489a8b40a2027.render.userLayoutRootNode.target.n7.uP?action=moveChannelHere&&sourceID=u18623l1n83&&method=insertBefore&&elementID=u18623l1n53
This is pretty straightforward-there are parameters for moving the channel, for the id of the source and dropped on channel (which luckily for us is the element ids of the drag and dropped channels respectively due to the work we did earlier). Now you can understand why we needed that baseURL xsl variable-that number after "tag" is actually a session id. With the baseURL, and the channel ids we can construct this address (which is what we do in the onDrop function above). Once we have a correct address, we can simply pass it to window.location.href and walla! Instant Drag & Drop! The nice thing about doing drag & drop this way is that you maintain compatibility with Luminis' channel layout architecture.
A couple of notes: Even though the URL above (and the ones that you'll see if you view the source to the "Manage Content/Layout" screen) contain escaped ampersands, this will fail if you try to construct the URL(this threw me for a little bit). Use normal ampersands to delineate URL variables.
Secondly, you'll see where we use our associative DragArray to kill the drag of a channel on a successful drop. This allows us to snap channels back into place that were dropped at an invalid location(the onDrop function isn't called for this case) while leaving channels dropped if they were dropped in a valid location (you can change this behavior if you want, we just found it to be confusing to have channels that were dropped successfully snap back into their original location before the page refreshed with the new layout).
Although this is a really good start-for a number of reasons, its not ideal. I'll be posting a followup article with some advanced tricks on how to make your drag and drop channels *really* cool.
If anyone has any questions, please let me know.
Comments
Video
A video would be great to see it in action. YouTube is your friend.
very nice
A job well done to everyone involved with this mod. It worked like a charm. This is the sort of functionality that will keep users coming back to our portal instead of google and yahoo. Great job! I will be looking forward to your next post.
Casey Hergett
Arkansas Tech University
casey.hergett@atu.edu
Luminis IV integration
Fantastic article!
I'm currently using Luminis IV and I'm working on converting this method over to work with it. Once I have a solid run down on how to do it I'll post it.
Thanks again!
Lum IV Problems
I too was trying to get this working in Luminis IV. I've run into a few problems so far. The url that needs to be sent seems to be slightly different. Instead of "target.n7.uP" it looks like it needs to be "target.ctf92.uP".
Here's an example url I was playing with
"http://luminis.server.edu/render.userLayoutRootNode.target.ctf92.uP?action=moveChannelHere&sourceID=u11l1n420&method=appendAfter&elementID=u11l1n670"
What's interesting about this, is that it works as long as I've went into the "Content\Layout" screen first. Going into the "Content\Layout" screen seems to initialize the "ctf92" package. However, if I try drag and drop upon log in, it doesn't work. I can then click on the "Content\Layout" page and I get a "Failed to obtain channel definition for subscribe id: ctf92" error. Anyone else see this with Lum IV?
Thanks
Greg