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]);
  }
});