Flex supports drag'n'drop natively between components that extends ListBase, so you can enable it simply setting dragEnabled and dropEnabled properties to true, anyway there is an important exception: you cannot drag element from a List (TileList, DataGrid...) to a Tree or viceversa.
If you never worked with custom drag'n'drop probably you don't know how it works internally and what causes this apparent bug. The most intuitive explanation is that Tree's data providers are hierarchical structures, on the contrary List components have plain collections, so flex sdk developers left us the task of decide what to do in our particular situations.
On live docs there is a section about this topic: http://livedocs.adobe.com/flex/3/html/help.html?content=dragdrop_5.html
Every event listener connected to drag'n'drop (dragEnter, dragOver, dragComplete...) takes as argument a DragEvent that has a payload of dragged data categorized by a simple string that identifies its "format" The "source" property if this event has two important methods:
event.source.hasFormat("format")
event.source.dataForFormat("format")
Calling the first one you can know if the current drag action has data for the passed format, with the second one you get the data for that format.
By default, if you use native drag'n'drop between List components, the format used is "items", on the contrary if you works with Tree, the format is "treeItems"; so a List cannot accepts an item from a List because it's expecting a "treeItems" format but receives a "items" one.
In these situations you can work with custom drag'n'drop, I wrote a simple example and I'm going to explain the most important parts.
First of all we create a List and a Tree with two data providers, notice that we are adding listeners for all the drag'n'drop events.
<mx:ArrayCollection id="listDP">
<mx:Object label="list item 1"/>
<mx:Object label="list item 2"/>
<mx:Object label="list item 3"/>
</mx:ArrayCollection>
<mx:ArrayCollection id="treeDP">
<mx:Object label="tree item 1"/>
<mx:Object label="tree item 2"/>
<mx:Object label="tree item 3">
<mx:children>
<mx:ArrayCollection>
<mx:Object label="child item 1"/>
<mx:Object label="child item 2"/>
</mx:ArrayCollection>
</mx:children>
</mx:Object>
</mx:ArrayCollection>
<mx:List
id="list"
width="50%"
dragEnabled="true"
dataProvider="{listDP}"
dragEnter="onListDragEnter(event)"
dragOver="onListDragOver(event)"
dragExit="onListDragExit(event)"
dragDrop="onListDragDrop(event)"
dragComplete="onListDragComplete(event)"
labelField="label"
dragMoveEnabled="true"
>
</mx:List>
<mx:Tree
id="tree"
width="50%"
dragEnabled="true"
dataProvider="{treeDP}"
dragEnter="onTreeDragEnter(event)"
dragOver="onTreeDragOver(event)"
dragExit="onTreeDragExit(event)"
dragDrop="onTreeDragDrop(event)"
labelField="label"
>
</mx:Tree>
Here we are preventing the default behaviour calling preventDefault on the event and we show the drop feedback when the mouse is moving on the component and hide it when the mouse exits without dropping anything (the drop feedback is that black horizontal line that indicates in what position the item would be dropped).
public function onListDragEnter(event:DragEvent):void
{
event.preventDefault();
DragManager.acceptDragDrop(event.target as UIComponent);
list.showDropFeedback(event);
}
public function onListDragOver(event:DragEvent):void
{
event.preventDefault();
list.showDropFeedback(event);
}
public function onListDragExit(event:DragEvent):void
{
event.preventDefault();
list.hideDropFeedback(event);
}
Here we takes the items from the DragEvent for both "items" and "treeItems" format and add them to our data provider at the calculated position.
public function onListDragDrop(event:DragEvent):void
{
event.preventDefault();
list.hideDropFeedback(event);
var index:int = list.calculateDropIndex(event);
//support drag from tree and other listbase components
var items:Array = new Array();
if(event.dragSource.hasFormat("treeItems"))
items = items.concat(event.dragSource.dataForFormat("treeItems") as Array);
if(event.dragSource.hasFormat("items"))
items = items.concat(event.dragSource.dataForFormat("items") as Array);
for each(var item:Object in items)
{
var obj:Object = new Object()
obj.label = item.label as String;
(list.dataProvider as ArrayCollection).addItemAt(obj, index);
}
}
The dragComplete event is dispatched from the drag source, so we have to remove the dragged elements if we moved (and not copied) them.
public function onListDragComplete(event:DragEvent):void
{
event.preventDefault();
if(event.action == DragManager.MOVE && list.dragMoveEnabled)
{
//remove moved elements
var items:Array = event.dragSource.dataForFormat("items") as Array;
var coll:ArrayCollection = list.dataProvider as ArrayCollection;
for each(var item:Object in items)
{
if(coll.contains(item))
{
coll.removeItemAt(coll.getItemIndex(item));
}
}
}
}
Here there are the same listeners for the Tree, the most important difference is the position calculated for the drop because we are working with hierarchical data and branches can be closed or opened.
public function onTreeDragEnter(event:DragEvent):void
{
event.preventDefault();
DragManager.acceptDragDrop(event.target as UIComponent);
tree.showDropFeedback(event);
}
public function onTreeDragOver(event:DragEvent):void
{
event.preventDefault();
tree.showDropFeedback(event);
}
public function onTreeDragExit(event:DragEvent):void
{
event.preventDefault();
tree.hideDropFeedback(event);
}
public function onTreeDragDrop(event:DragEvent):void
{
event.preventDefault();
tree.hideDropFeedback(event);
var index:int = tree.calculateDropIndex(event);
var items:Array = new Array();
//we are supporting items from lists and trees
if(event.dragSource.hasFormat("treeItems"))
items = items.concat(event.dragSource.dataForFormat("treeItems") as Array);
if(event.dragSource.hasFormat("items"))
items = items.concat(event.dragSource.dataForFormat("items") as Array);
//by default we add elements to the top level, but we could
//calculate the position in the hierarchical structure using the datadescriptor
if(index > (tree.dataProvider as ICollectionView).length)
index = (tree.dataProvider as ICollectionView).length;
for each(var item:Object in items)
{
var obj:Object = new Object()
obj.label = item.label as String;
(tree.dataProvider as ArrayCollection).addItemAt(obj, index);
}
}
Comments (5)
Hi, there... thanks for the nice example. I was working on something similar and came nearly to the same Solution. But i couldn't figure out how to calculate the hierarchical position.
As you wrote 'but we could
calculate the position in the hierarchical structure using the datadescriptor'
Can you please explain this?
Greetings Florian
Posted by Florian | December 2, 2008 8:10 PM
Posted on December 2, 2008 20:10
Hi, Thx for the example... Tried something similar, helped a lot. But can you maybe explain how to calculate the hierarchical position, cant figure this out.
Thx, in advance!
Flo
Posted by Florian | December 3, 2008 5:59 PM
Posted on December 3, 2008 17:59
Hi Florian, this is a modified version of onTreeDragDrop that calculate the hierarchical position:
public function onTreeDragDrop(event:DragEvent):void
{
event.preventDefault();
tree.hideDropFeedback(event);
var index:int = tree.calculateDropIndex(event);
var items:Array = new Array();
//we are supporting items from lists and trees
if(event.dragSource.hasFormat("treeItems"))
items = items.concat(event.dragSource.dataForFormat("treeItems") as Array);
if(event.dragSource.hasFormat("items"))
items = items.concat(event.dragSource.dataForFormat("items") as Array);
var parentItem:Object;
if(tree.dataDescriptor.isBranch(tree.indexToItemRenderer(index-1).data))
parentItem = tree.indexToItemRenderer(index-1).data;
else
parentItem = tree.getParentItem(tree.indexToItemRenderer(index-1).data);
var position:int = 0;
while(tree.indexToItemRenderer(--index).data != parentItem)
{
position++;
}
for each(var item:Object in items)
{
var obj:Object = new Object()
obj.label = item.label as String;
tree.dataDescriptor.addChildAt(parentItem, obj,position++);
}
}
Posted by Emanuele Tatti | December 4, 2008 11:28 AM
Posted on December 4, 2008 11:28
Very helpful. Thanks
Posted by RajPrabha | December 15, 2008 6:49 AM
Posted on December 15, 2008 06:49
Hi guys,
After looking at the Tree source code, I found a kind of "hack" to get easily the parent item of the drop position. Here is the code:
var dropParentPackage:IYourDataType = myTree.mx_internal::_dropData.parent as IYourDataType;
This is a very ugly way of doing because it's using the mx_internal namespace, that we are not supposed to know the existence :) But in this case, Tree class uses internally a very complex algorithm to gather some info about the drop position. Look at the Tree source code to see what data you can get by using this hacky method :)
Hope this helps,
Avangel.
Posted by Avangel | August 31, 2009 12:35 AM
Posted on August 31, 2009 00:35