This post is my third, and probably last(at least for a while) on drag and drop channels in Luminis. If you haven't read the first two, "D&D Channels" and "AD&D Channels", make sure you read them first, as this post builds off of them.
Making the AJAX Connection
We're going to replace the location.href line at the bottom of the DragChannel.js file with some AJAX code to make the call back to Luminis with the URL we've constructed. Replace that line with the following:
ajaxpage(MoveURL, null, false);
Here's the (pretty standard) ajaxpage function we downloaded from Dynamic Drive (this has been modified slightly to make it a little more flexible):
var page_request = false;
function ajaxpage(url, callbackFunction, bustcachevar) {
var bustcachevar = 1 //bust potential caching of external pages after initial request? (1=yes, 0=no);
var bustcacheparameter = "";
if (window.XMLHttpRequest) // if Mozilla, Safari etc
page_request = new XMLHttpRequest()
else if (window.ActiveXObject){ // if IE
try {
page_request = new ActiveXObject("Msxml2.XMLHTTP")
}
catch (e){
try{
page_request = new ActiveXObject("Microsoft.XMLHTTP")
}
catch (e){}
}
}
else
return false
if (callbackFunction != null)
page_request.onreadystatechange equals callbackFunction;
if (bustcachevar) //if bust caching of external page
bustcacheparameter=(url.indexOf("?")!=-1)? "&"+new Date().getTime() : "?"+new Date().getTime()
page_request.open('GET', url+bustcacheparameter, true)
page_request.send(null)
}
[One note-you'll see one code segment above that uses the word "onreadystatechange equals" instead of the '=' sign. This is because for some reason Drupal here at LumDev was choking on it and kept reporting "terminated due to suspicious input data" - please make the appropriate change]
Now, calls to build the dropped channel are done in the background so you no longer have to refresh the page. This is *much* better.
Dragging to the Bottom
One thing you'll notice in all of this is that if you take a channel and try dragging it to the bottom of a column, you can't drop it. Right now, (through Sortables) all of our channels are Draggables, but they are also Droppables-valid areas for channels to be dropped on. This isn't the case for areas outside of the channels.
This condition is made somewhat more complicated by the way that Luminis handles channel reorganization. If you look at the URLs we constructed in the last article to integrate with Luminis' channel layout code, the URLs work by specifying that a channel has moved *before* (not after) another one, making it a little more difficult to position channels at the end of columns.
Being unable to target blank space as a valid droppable area also leads to another negative side-effect. If you empty out all of the channels in a column, you can no longer drop any channels there since the column no longer has any droppables. Clearly this deficiency needs to be addressed.
So, how would we drag a channel to the bottom of a column? The first thing we would have to do is create dummy channel divs at the bottom of each column (these channels wouldn't be real Luminis channels in any sense-just an area that we can make a valid droppable). We can do that by making changes to the Sortable template created in my last post.
You can insert this just above the location where Columns are added to the ColumnArray. At the bottom of each column, this creates a ColumnFooter div. This div has a class of 'DragChannel' which means that when the Sortables are created in DragChannel.js, it will be made a draggable and a droppable, like other channels. Unlike normal Luminis channels, we don't really want people to be able to move this div around (its job is to stay at the bottom of each column) so we simply don't create a drag handle for it.
Now, lets move onto the necessary changes inside DragChannel.js.
What we'd really like to do is for these ColumnFooter DIVs to extend to the bottom of the webpage in each column. In all likelihood, you'll have several columns of differing heights(due to the number and height of channels in it). In order for this scheme to work, whatever the height of your column is, its column footer has to have a height that extends from the bottom of your last channel to the bottom of the page-so no matter where along the bottom a channel is dragged, it will overlap our hidden false droppable(the ColumnFooter div). We can accomplish this with a little JS code. See the equalizeColumn function below:
This just loops through the column footers, finds their position (to determine the longest column) and adjust their heights so all columns are "equal". We need to call this function in two places. First, it needs to be called outside of any function so that the columns are initialized at the same height (this is necessary for an initial drag). Second, it needs to be called at the end of the onDrop function. After each successful drop, the heights of all of the columns have changed, so the column footer divs need to be adjusted accordingly.
Now that we have dynamically adjusting invisible droppable areas at the bottom of each column-how do we handle this? If we leave the rest of the code in place as is-the code that we have to create the Luminis URL dynamically would pull the id of the droppable area - in this case ColumnFooter. But this isn't a valid channel, and Luminis won't know what to make of it.
What we really need to do is if we detect that the droppable area is a footer, insert our channel as the last channel in the column. Luminis helps us out a little bit here-if you look at the URL string that Luminis uses to organize channels, it contains an action called 'insertBefore'. There is also another action called 'appendAfter' that essentially adds a channel to the end of the column. In this case we can use the original droppable (the column) in the onDrop function to pass as a parameter. Code for the revised onDrop function is below.
ColumnDiv = DropDiv;
Action = 'insertBefore';
ChannelList = getElementsByClass(DropDiv, "DIV", "DragChannel");
for (var j = 0; j < ChannelList.length; j++)
{
if (ChannelList[j].id == DragDiv.id)
{
DropDiv = ChannelList[j + 1];
if (DropDiv.id.indexOf("ColumnFooter") != -1)
{
Action = 'appendAfter';
DropDiv = ColumnDiv;
}
break;
}
}
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=' + Action + '&elementID=' + DropDiv.id;
ajaxpage(MoveURL, null, false);
equalizeColumns();
}
[One note-you'll see one code segment above that uses the word "onDrop equals" instead of the '=' sign. This is because for some reason Drupal here at LumDev was choking on it and kept reporting "terminated due to suspicious input data" - please make the appropriate change]
This not only allows you to drag to the bottom of a column, but also allows you to drag channels into a column that has been emptied. (Of course, if you wanted an emptied out column to vanish, you could probably try setting the column's display style to none-this is something I haven't done at the time of this writing).
That about wraps it up. Special thanks for this whole thing truly go to Zach Tirrell and Jon Wheat, who demonstrated what was really possible.
If anyone has any questions, let me know!
Comments
browser issues?
Is anyone else having problems getting this to work in IE 6 or 7? In IE the channel controls are all lined up on the right side of the screen and the channels are not draggable. I get 3 syntax errors on the page. I can get it to work in Firefox with the exception of I have to hit the 'Esc' key to actually "Drop" the channel. Otherwise it just hangs on to the channel while you frantically try to shake it off.
Casey Hergett
Arkansas Tech University
casey.hergett@atu.edu
RE: Browser issues?
I guess the first question would be what errors are you getting? We have this working in IE, Safari, & Firefox.
syntax error
I'm getting a sytax error with no details in IE. I've looked at the lines that it complains about but I cannot see anything that is messed up. I'm running Luminis version 3.3.3.16.
Casey Hergett
Arkansas Tech University
casey.hergett@atu.edu
DragChannel.js
could you send me a copy of your DragChannel.js?
Casey Hergett
Arkansas Tech University
casey.hergett@atu.edu
Yep
I get ColumnArray not defined pointing to my Dragchannel.js file
on this line :
for (i = 0; i < ColumnArray.length; i++)
What I did was add a declaration in the top of my regularLayout template where the setTimeout is like this :
<script language="JavaScript">
var ColumnArray = new Array();
setTimeout('initSession()', 1000);
</script>
Now I can drag channels around somewhat. Can't take them to the right most column or the top of the middle column ... but I'm getting there now.
control icons
Yea, mine did that too. I guess you could define a channel width in a style and apply that, but I ended up making the top title area a table in itself like this :
<div>
<xsl:attribute name="id"><xsl:text>ChannelHandle</xsl:text></xsl:attribute>
<xsl:attribute name="class"><xsl:text>bg3</xsl:text></xsl:attribute>
<TABLE align="center" id="channel" cellpadding="0" cellspacing="0" border="0" width="100%">
<TR>
<TD>
<span CLASS="uportal-head14-bold">
<xsl:value-of select="@title"/>
</span>
</TD>
<TD ALIGN="RIGHT" NOWRAP="NOWRAP" VALIGN="TOP">
<xsl:choose>
<xsl:when test="$detachedContent='true'">
<xsl:call-template name="detachedChannelControls"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="controls"/>
</xsl:otherwise>
</xsl:choose>
</TD>
</TR>
</TABLE>
</div>
Then continued the channel template content code with another table. Seems to work
Locked Channels
Is there anyway to not allow them to move the channels with this mod if its a locked channel? I know it doesn't move it once you move from channel to channel, but to let the end user that they cannot move it beforehand.
Thanks
Locking Channels
We've rewritten this code somewhat so that it can be turned on and off globally or on a tab basis-but not whether the channel is locked. The problem here is I don't think that that information is available from the normal screen. Since that information *IS* available from the Content/Layout screen, in theory you could use AJAX to retrieve that page and parse through for the information. Not fun I know, but a definite possibility. Once you have that info you could turn each draggable off on a channel by channel basis. (I haven't tried this at all but I suspect this would only solve half your problem since wouldn't each channel still be a droppable inside the sortable? I have to double check on that).
Reducing code size
Brian - this is an excellent tutorial. I am happy to report that with some minor tweaking for Luminis IV, that your instructions were successful! The one thing that I would suggest is leveraging the Prototype framework that you have already included for Scriptaculous. For example, Prototype.js already totally abstracts Ajax requests, so instead of including another file, you could simply do the following:
... MoveURL = window.location.href.substr(0, window.location.href.indexOf("cp/")+0); MoveURL = MoveURL + BaseActionURL.substr(0, BaseActionURL.indexOf("uP")) + "target.ctf92.uP"; MoveURL = MoveURL + '?action=moveChannelHere&sourceID=' + DragDiv.id + '&method=' + Action + '&elementID=' + DropDiv.id; new Ajax.Request(MoveURL);Its that easy. There are some other areas where code size can be slimmed, with using ".each" instead of a for statement. But, as you addressed in your 2nd article, you aren't claiming to be an expert in Scriptaculous or Prototype.
Fantastic work! I wish the developers at Sungard would wake up and smell the coffee.
You Rock
Hey Brian
it works successfully. Thanks alot
But i got one problem. when the session is about to expired, the warning meeage box ("you session will expire") appears behind channel draged at the same area of the warning message. so you are not able to see warning mesage anymore..it is gone with the wind :(
how should i solve this, please help