Drag & Drop¶
Drag & Drop is one of the essential technologies in today's applications. An operation must have a starting point (e.g. where the pointer was tapped), may have any number of intermediate steps (widgets that the pointer moves over during a drag), and must either have an end point (the widget above which the pointer was released), or be canceled.
qooxdoo comes with a powerful event-based layer which supports drag&drop with full data exchange capabilities. Every widget can be configured to cooperate with drag&drop be it as sender (draggable), receiver (droppable) or both. A sender (drag target) can send data to any receiver (drop target).
You may like to see an example first:
Basics¶
To enable Drag & Drop the properties draggable and droppable must be enabled on the specific widgets. For list type sources or targets it's often enough to make the top-level widget drag- or droppable e.g. the list instead of the list items.
var dragTarget = new qx.ui.form.List();
dragTarget.setDraggable(true);
var dropTarget = new qx.ui.form.List();
dropTarget.setDroppable(true);
The basic drag&drop should start working with these properties enabled, but it will show the no-drop cursor over all potential targets. To fix this one needs to register actions (and optionally data types) supported by the drag target. This can be done during the dragstart
event which is fired on the drag target:
dragTarget.addListener("dragstart", function(e) {
e.addAction("move");
});
The drop target can then add a listener to react for the drop
event.
dropTarget.addListener("drop", function(e) {
alert(e.getRelatedTarget());
});
The listener now shows an alert box which should present the identification ID (classname + hash code) of the drag target. Theoretically this could already be used to transfer data from A to B.
Data Handling¶
qooxdoo also supports advanced data handling in drag&drop sessions. The basic idea is to register the supported drag data types and then let the drop target choose which one to handle (if any at all).
To register some types write a listener for dragstart
:
source.addListener("dragstart", function(e) {
e.addAction("move");
e.addType("qx/list-items");
e.addType("html/list");
});
This is basically only the registration for the types which could theoretically be delivered to the target. The IDs used are just strings. They have no special meaning. They could be identical to typical mime-types like text/plain
but there is no need for this.
The preparation of the data (if not directly available) is done lazily by the droprequest
event which will explained later. The next step is to let the target work with the incoming data. The following code block appends all the dropped children to the end of the list.
target.addListener("drop", function(e) {
var items = e.getData("qx/list-items");
for (var i=0, l=items.length; i<l; i++) {
this.add(items[i]);
}
});
The last step needed to get the thing to fly is to prepare the data for being dragged around. This might look like the following example:
source.addListener("droprequest", function(e)
{
var type = e.getCurrentType();
if (type == "qx/list-items")
{
var items = this.getSelection();
// Add data to manager
e.addData(type, items);
}
else if (type == "html/list")
{
// TODO: support for HTML markup
}
});
Support Multiple Actions¶
One thing one might consider is to add support for multiple actions. In the above example it would be imaginable to copy or move the items around. To make this possible one could add all supported actions during the drag
event. This might look like the following:
source.addListener("dragstart", function(e)
{
// Register supported actions
e.addAction("copy");
e.addAction("move");
// Register supported types
e.addType("qx/list-items");
e.addType("html/list");
});
The action to use is modifiable by the user through pressing of modifier keys during the drag&drop process. The preparation of the data is done through the droprequest
as well. Here one can use the action (call e.getCurrentAction()
to get the selected action) to apply different modifications on the original data. A modified version of the code listed above might look like the following:
source.addListener("droprequest", function(e)
{
var action = e.getCurrentAction();
var type = e.getCurrentType();
var result;
if (type === "qx/list-items")
{
result = this.getSelection();
if (action == "copy")
{
var copy = [];
for (var i=0, l=result.length; i<l; i++) {
copy[i] = result[i].clone();
}
result = copy;
}
}
else if (case == "html/list")
{
// TODO: support for HTML markup
}
// Remove selected items on move
if (action == "move")
{
var selection = this.getSelection();
for (var i=0, l=selection.length; i<l; i++) {
this.remove(selection[i]);
}
}
// Add data to manager
e.addData(type, result);
});
As known from major operating systems, exactly three actions are supported:
move
copy
alias
which could be combined in any way the developer likes. qooxdoo renders a matching cursor depending on the currently selected action during the drag&drop sequence. The event dragchange
is fired on the source widget on every change of the currently selected action. It is also fired on the target and is cancelable which enables the developers to allow only certain actions on targets.
Runtime checks¶
There are a few other pleasantries. For example it is possible for droppable
widgets to ignore a specific incoming data type. This can be done by preventing the default action on the incoming dragover
event:
target.addListener("dragover", function(e)
{
if (someRunTimeCheck()) {
e.preventDefault();
}
});
This could be used to dynamically accept or disallow specific types of drop events depending on the application status or any other given condition. The user then gets a nodrop
cursor to signal that the hovered target does not accept the data. To query the source object for supported types or actions one would call the methods supportsAction
or supportsType
on the incoming event object.
Something comparable is possible during the dragstart
event:
source.addListener("dragstart", function(e)
{
if (someRunTimeCheck()) {
e.preventDefault();
}
});
This prevents the dragging of data from the source widget when some runtime condition is not solved. This is especially useful to call some external functionality to check whether a desired action is possible. In this case it might also depend on the other properties of the source widget e.g. in a mail program it is possible to drag the selection of the tree to another folder, with one exception: the inbox. This could easily be solved with such a feature.
Drag Session¶
During the drag session the drag
event is fired for every move of the pointer. This event may be used to "attach" an image or widget to the pointer to indicate the type of data or object dragged around. It may also be used to render a line during a reordering drag&drop session (see next paragraph). It supports the methods getDocumentLeft
and getDocumentTop
known from the pointermove
event. This data may be used for the positioning of a cursor.
When hovering a widget the dragover
event is fired on the "interim" target. When leaving the widget the dragleave
event is fired. The dragover
is cancelable and has information about the related target (the source widget) through getRelatedTarget
on the incoming event object.
Another quite useful event is the dragend
event which is fired at every end of the drag session. This event is fired in both cases, when the transaction has modified anything or not. It is fired when pressing Escape or stopping the session any other way as well.
A typical sequence of events could look like this:
dragstart
on source (once)drag
on source (pointer move)dragover
on target (pointer over)dragchange
on source (action change)dragleave
on target (pointer out)drop
on target (once)droprequest
on source (normally once)dragend
on source (once)
Reordering Items¶
Items may also be reordered inside one widget using the drag&drop API. This action is normally not directly data related and may be used without adding any types to the drag&drop session.
reorder.addListener("dragstart", function(e) {
e.addAction("move");
});
reorder.addListener("drop", function(e)
{
// Using the selection sorted by the original index in the list
var sel = this.getSortedSelection();
// This is the original target hovered
var orig = e.getOriginalTarget();
for (var i=0, l=sel.length; i<l; i++)
{
// Insert before the marker
this.addBefore(sel[i], orig);
// Recover selection as it gets lost during child move
this.addToSelection(sel[i]);
}
});