Dragging an object from one location to another by grabbing it with mouse, holding the mouse button pressed, and then releasing the button to "drop" it to the other location is a common way to move, copy, or associate objects. For example, most operating systems allow dragging and dropping files between folders or dragging a document on a program to open it. In Vaadin, it is possible to drag and drop components and parts of certain components.
Dragged objects, or transferables, are essentially data
objects. You can drag and drop rows in Table
and nodes
in Tree
components, either within or between the
components. You can also drag entire components by wrapping them inside
DragAndDropWrapper
.
Dragging starts from a drag source, which defines the
transferable. Transferables implement the Transferable
interfaces. For trees and tables, which are bound to
Container
data sources, a node or row transferable is a
reference to an Item
in the Vaadin Data Model. Dragged
components are referenced with a
WrapperTransferable
. Starting dragging does not require
any client-server communication, you only need to enable dragging. All drag
and drop logic occurs in two operations: determining
(accepting) where dropping is allowed and actually
dropping. Drops can be done on a drop target, which
implements the DropTarget
interface. Three components
implement the interface: Tree
,
Table
, and
DragAndDropWrapper
. These accept and drop operations
need to be provided in a drop handler. Essentially all
you need to do to enable drag and drop is to enable dragging in the drag
source and implement the getAcceptCriterion()
and
drop()
methods in the
DropHandler
interface.
The client-server architecture of Vaadin causes special requirements for the drag and drop functionality. The logic for determining where a dragged object can be dropped, that is, accepting a drop, should normally be done on the client-side, in the browser. Server communications are too slow to have much of such logic on the server-side. The drag and drop feature therefore offers a number of ways to avoid the server communications to ensure a good user experience.
Most of the user-defined drag and drop logic occurs in a drop
handler, which is provided by implementing the
drop()
method in the
DropHandler
interface. A closely related definition
is the drop accept criterion, which is defined in the
getAcceptCriterion()
method in the same
interface. It is described in Section 12.12.4, “Accepting Drops” later.
The drop()
method gets a
DragAndDropEvent
as its parameters. The event
object provides references to two important object:
Transferable
and
TargetDetails
.
A Transferable
contains a reference to the object
(component or data item) that is being dragged. A tree or table item is
represented as a TreeTransferable
or
TableTransferable
object, which carries the item
identifier of the dragged tree or table item. These special transferables,
which are bound to some data in a container, are
DataBoundTransferable
. Dragged components are
represented as WrapperTransferable
objects, as the
components are wrapped in a DragAndDropWrapper
.
The TargetDetails
object provides information about
the exact location where the transferable object is being dropped. The
exact class of the details object depends on the drop target and you need
to cast it to the proper subclass to get more detailed information. If the
target is selection component, essentially a tree or a table, the
AbstractSelectTargetDetails
object tells the item
on which the drop is being made. For trees, the
TreeTargetDetails
gives some more details. For
wrapped components, the information is provided in a
WrapperDropDetails
object. In addition to the
target item or component, the details objects provide a drop
location. For selection components, the location can be
obtained with the getDropLocation()
and for
wrapped components with verticalDropLocation()
and horizontalDropLocation()
. The locations are
specified as either VerticalDropLocation
or
HorizontalDropLocation
objects. The drop location
objects specify whether the transferable is being dropped above, below, or
directly on (at the middle of) a component or item.
Dropping on a Tree
, Table
,
and a wrapped component is explained further in the following sections.
You can drag items from, to, or within a
Tree
. Making tree a drag source requires simply
setting the drag mode with
setDragMode()
. Tree
currently supports only one drag mode,
TreeDragMode.NODE
, which allows dragging single tree
nodes. While dragging, the dragged node is referenced with a
TreeTransferable
object, which is a
DataBoundTransferable
. The tree node is identified
by the item ID of the container item.
When a transferable is dropped on a tree, the drop location is stored in a
TreeTargetDetails
object, which identifies the
target location by item ID of the tree node on which the drop is made. You
can get the item ID with getItemIdOver()
method
in AbstractSelectTargetDetails
, which the
TreeTargetDetails
inherits. A drop can occur
directly on or above or below a node; the exact location is a
VerticalDropLocation
, which you can get with the
getDropLocation()
method.
In the example below, we have a Tree
and we allow
reordering the tree items by drag and drop.
final Tree tree = new Tree("Inventory"); tree.setContainerDataSource(TreeExample.createTreeContent()); layout.addComponent(tree); // Expand all items for (Iterator<?> it = tree.rootItemIds().iterator(); it.hasNext();) tree.expandItemsRecursively(it.next()); // Set the tree in drag source mode tree.setDragMode(TreeDragMode.NODE); // Allow the tree to receive drag drops and handle them tree.setDropHandler(new DropHandler() { public AcceptCriterion getAcceptCriterion() { return AcceptAll.get(); } public void drop(DragAndDropEvent event) { // Wrapper for the object that is dragged Transferable t = event.getTransferable(); // Make sure the drag source is the same tree if (t.getSourceComponent() != tree) return; TreeTargetDetails target = (TreeTargetDetails) event.getTargetDetails(); // Get ids of the dragged item and the target item Object sourceItemId = t.getData("itemId"); Object targetItemId = target.getItemIdOver(); // On which side of the target the item was dropped VerticalDropLocation location = target.getDropLocation(); HierarchicalContainer container = (HierarchicalContainer) tree.getContainerDataSource(); // Drop right on an item -> make it a child if (location == VerticalDropLocation.MIDDLE) tree.setParent(sourceItemId, targetItemId); // Drop at the top of a subtree -> make it previous else if (location == VerticalDropLocation.TOP) { Object parentId = container.getParent(targetItemId); container.setParent(sourceItemId, parentId); container.moveAfterSibling(sourceItemId, targetItemId); container.moveAfterSibling(targetItemId, sourceItemId); } // Drop below another item -> make it next else if (location == VerticalDropLocation.BOTTOM) { Object parentId = container.getParent(targetItemId); container.setParent(sourceItemId, parentId); container.moveAfterSibling(sourceItemId, targetItemId); } } });
Tree
defines some specialized accept
criteria for trees.
TargetInSubtree
(client-side)-1
means
infinite, that is, the entire sub-tree, and is therefore the
same as the simpler constructor.
TargetItemAllowsChildren
(client-side)setChildrenAllowed()
enabled for the
target item. The criterion does not require parameters, so the
class is a singleton and can be acquired with
Tree.TargetItemAllowsChildren.get()
. For
example, the following composite criterion accepts drops only
on nodes that allow children, but between all nodes:
return new Or (Tree.TargetItemAllowsChildren.get(), new Not(VerticalLocationIs.MIDDLE));
TreeDropCriterion
(server-side)getAllowedItemIds()
to return the
set. While the criterion is server-side, it is lazy-loading,
so that the list of accepted target nodes is loaded only once
from the server for each drag operation. See Section 12.12.4, “Accepting Drops” for an example.
In addition, the accept criteria defined in
AbstractSelect
are available for a
Tree
, as listed in Section 12.12.4, “Accepting Drops”.
You can drag items from, to, or within a
Table
. Making table a drag source requires simply
setting the drag mode with
setDragMode()
. Table
supports dragging both single rows, with
TableDragMode.ROW
, and multiple rows, with
TableDragMode.MULTIROW
. While dragging, the dragged
node or nodes are referenced with a
TreeTransferable
object, which is a
DataBoundTransferable
. Tree nodes are identified by
the item IDs of the container items.
When a transferable is dropped on a table, the drop location is stored in
a AbstractSelectTargetDetails
object, which
identifies the target row by its item ID. You can get the item ID with
getItemIdOver()
method. A drop can occur directly
on or above or below a row; the exact location is a
VerticalDropLocation
, which you can get with the
getDropLocation()
method from the details object.
Table
defines one specialized accept
criterion for tables.
TableDropCriterion
(server-side)getAllowedItemIds()
to return the
set. While the criterion is server-side, it is lazy-loading,
so that the list of accepted target items is loaded only once
from the server for each drag operation.
You can not drop the objects you are dragging around just anywhere. Before a drop is possible, the specific drop location on which the mouse hovers must be accepted. Hovering a dragged object over an accepted location displays an accept indicator, which allows the user to position the drop properly. As such checks have to be done all the time when the mouse pointer moves around the drop targets, it is not feasible to send the accept requests to the server-side, so drops on a target are normally accepted by a client-side accept criterion.
A drop handler must define the criterion on the objects which it accepts
to be dropped on the target. The criterion needs to be provided in the
getAcceptCriterion()
method of the
DropHandler
interface. A criterion is represented
in an AcceptCriterion
object, which can be a
composite of multiple criteria that are evaluated using logical
operations. There are two basic types of criteria:
client-side and server-side
criteria. The various built-in criteria allow accepting drops
based on the identity of the source and target components, and on the
data flavor of the dragged objects.
To allow dropping any transferable objects, you can return a universal
accept criterion, which you can get with
AcceptAll.get()
.
tree.setDropHandler(new DropHandler() { public AcceptCriterion getAcceptCriterion() { return AcceptAll.get(); } ...
The client-side criteria, which inherit the
ClientSideCriterion
, are verified on the
client-side, so server requests are not needed for verifying whether each
component on which the mouse pointer hovers would accept a certain object.
The following client-side criteria are define in com.vaadin.event.dd.acceptcriterion:
AcceptAll
And
ContainsDataFlavour
Not
Or
SourceIs
SourceIsTarget
TargetDetailIs
In addition, target components such as Tree
and
Table
define some component-specific
client-side accept criteria. See Section 12.12.2, “Dropping Items On a Tree
” for more details.
AbstractSelect
defines the following criteria
for all selection components, including Tree
and Table
.
AcceptItem
AbstractSelect
, is given as the first
parameter for the constructor. It is followed by a list of
allowed item identifiers in the drag source.
AcceptItem.ALL
TargetItemIs
AbstractSelect
) followed by a list of
allowed item identifiers.
VerticalLocationIs.MIDDLE
,
TOP
, and
BOTTOM
public AcceptCriterion getAcceptCriterion() { return new Not(VerticalLocationIs.MIDDLE); }
The server-side criteria are verified on the
server-side with the accept()
method of the
ServerSideCriterion
class. This allows fully
programmable logic for accepting drops, but the negative side is that
it causes a very large amount of server requests. A request is made
for every target position on which the pointer hovers. This problem is
eased in many cases by the component-specific lazy loading criteria
TableDropCriterion
and
TreeDropCriterion
. They do the server visit
once for each drag and drop operation and return all accepted rows or
nodes for current Transferable
at once.
The accept()
method gets the drag event as a
parameter so it can perform its logic much like in
drop()
.
public AcceptCriterion getAcceptCriterion() { // Server-side accept criterion that allows drops on any other // location except on nodes that may not have children ServerSideCriterion criterion = new ServerSideCriterion() { public boolean accept(DragAndDropEvent dragEvent) { TreeTargetDetails target = (TreeTargetDetails) dragEvent.getTargetDetails(); // The tree item on which the load hovers Object targetItemId = target.getItemIdOver(); // On which side of the target the item is hovered VerticalDropLocation location = target.getDropLocation(); if (location == VerticalDropLocation.MIDDLE) if (! tree.areChildrenAllowed(targetItemId)) return false; // Not accepted return true; // Accept everything else } }; return criterion; }
The server-side criteria base class
ServerSideCriterion
provides a generic
accept()
method. The more specific
TableDropCriterion
and
TreeDropCriterion
are conveniency extensions
that allow definiting allowed drop targets as a set of items. They
also provide some optimization by lazy loading, which reduces server
communications significantly.
public AcceptCriterion getAcceptCriterion() { // Server-side accept criterion that allows drops on any // other tree node except on node that may not have children TreeDropCriterion criterion = new TreeDropCriterion() { @Override protected Set<Object> getAllowedItemIds( DragAndDropEvent dragEvent, Tree tree) { HashSet<Object> allowed = new HashSet<Object>(); for (Iterator<Object> i = tree.getItemIds().iterator(); i.hasNext();) { Object itemId = i.next(); if (tree.hasChildren(itemId)) allowed.add(itemId); } return allowed; } }; return criterion; }
When a dragged object hovers on a drop target, an accept
indicator is displayed to show whether or not the location
is accepted. For MIDDLE
location, the indicator
is a box around the target (tree node, table row, or component). For
vertical drop locations, the accepted locations are shown as
horizontal lines, and for horizontal drop locations as vertical lines.
For DragAndDropWrapper
drop targets, you can
disable the accept indicators or drag hints with
the no-vertical-drag-hints
,
no-horizontal-drag-hints
, and
no-box-drag-hints
styles. You need to add the
styles to the layout that contains the wrapper,
not to the wrapper itself.
// Have a wrapper DragAndDropWrapper wrapper = new DragAndDropWrapper(c); layout.addComponent(wrapper); // Disable the hints layout.addStyleName("no-vertical-drag-hints"); layout.addStyleName("no-horizontal-drag-hints"); layout.addStyleName("no-box-drag-hints");
Dragging a component requires wrapping the source component within a
DragAndDropWrapper
. You can then allow dragging by
putting the wrapper (and the component) in drag mode with
setDragStartMode()
. The method supports two drag
modes: DragStartMode.WRAPPER
and
DragStartMode.COMPONENT
, which defines whether the
entire wrapper is shown as the drag image while dragging or just the
wrapped component.
// Have a component to drag final Button button = new Button("An Absolute Button"); // Put the component in a D&D wrapper and allow dragging it final DragAndDropWrapper buttonWrap = new DragAndDropWrapper(button); buttonWrap.setDragStartMode(DragStartMode.COMPONENT); // Set the wrapper to wrap tightly around the component buttonWrap.setSizeUndefined(); // Add the wrapper, not the component, to the layout layout.addComponent(buttonWrap, "left: 50px; top: 50px;");
The default height of DragAndDropWrapper
is
undefined, but the default width is 100%. If you want to ensure that the
wrapper fits tightly around the wrapped component, you should call
setSizeUndefined()
for the wrapper. Doing so, you
should make sure that the wrapped component does not have a relative size,
which would cause a paradox.
Dragged components are referenced in the
WrapperTransferable
. You can get the reference to
the dragged component with
getDraggedComponent()
. The method will return
null
if the transferable is not a component. Also HTML
5 drags (see later) are held in wrapper transferables.
Drops on a component are enabled by wrapping the component in a
DragAndDropWrapper
. The wrapper is an ordinary
component; the constructor takes the wrapped component as a parameter. You
just need to define the DropHandler
for the wrapper
with setDropHandler()
.
In the following example, we allow moving components in an absolute layout. Details on the drop handler are given later.
// A layout that allows moving its contained components // by dragging and dropping them final AbsoluteLayout absLayout = new AbsoluteLayout(); absLayout.setWidth("100%"); absLayout.setHeight("400px"); ... put some (wrapped) components in the layout ... // Wrap the layout to allow handling drops DragAndDropWrapper layoutWrapper = new DragAndDropWrapper(absLayout); // Handle moving components within the AbsoluteLayout layoutWrapper.setDropHandler(new DropHandler() { public AcceptCriterion getAcceptCriterion() { return AcceptAll.get(); } public void drop(DragAndDropEvent event) { ... } });
The drop handler receives the drop target details in a
WrapperTargetDetails
object, which implements
the TargetDetails
interface.
public void drop(DragAndDropEvent event) { WrapperTransferable t = (WrapperTransferable) event.getTransferable(); WrapperTargetDetails details = (WrapperTargetDetails) event.getTargetDetails();
The wrapper target details include a
MouseEventDetails
object, which you can get
with getMouseEvent()
. You can use it to get
the mouse coordinates for the position where the mouse button was
released and the drag ended. Similarly, you can find out the drag
start position from the transferable object (if it is a
WrapperTransferable
) with
getMouseDownEvent()
.
// Calculate the drag coordinate difference int xChange = details.getMouseEvent().getClientX() - t.getMouseDownEvent().getClientX(); int yChange = details.getMouseEvent().getClientY() - t.getMouseDownEvent().getClientY(); // Move the component in the absolute layout ComponentPosition pos = absLayout.getPosition(t.getSourceComponent()); pos.setLeftValue(pos.getLeftValue() + xChange); pos.setTopValue(pos.getTopValue() + yChange);
You can get the absolute x and y coordinates of the target wrapper
with getAbsoluteLeft()
and
getAbsoluteTop()
, which allows you to
translate the absolute mouse coordinates to coordinates relative to
the wrapper. Notice that the coordinates are really the position of
the wrapper, not the wrapped component; the wrapper reserves some
space for the accept indicators.
The verticalDropLocation()
and
horizontalDropLocation()
return the more
detailed drop location in the target.
The DragAndDropWrapper
allows dragging files from
outside the browser and dropping them on a component wrapped in the
wrapper. Dropped files are automatically uploaded to the application and
can be acquired from the wrapper with
getFiles()
. The files are represented as
Html5File
objects as defined in the inner
class. You can define an upload Receiver
to receive
the content of a file to an OutputStream
.
Dragging and dropping files to browser is supported in HTML 5 and requires a compatible browser, such as Mozilla Firefox 3.6 or newer.