Skip to content

cuba-platform/cuba-dnd

Repository files navigation

CUBA DnD Add-on

license Build Status

Overview

The add-on enables using drag and drop features in CUBA applications.

Dependencies

Main features

UI components with Drag-and-Drop functionality:

  • DDVerticalLayout;
  • DDHorizontalLayout;
  • DDGridLayout;
  • DDCssLayout;
  • DDAbsoluteLayout;
  • DragAndDropWrapper.

Installation

The add-on can be added to your project in one of the ways described below. Installation from the Marketplace is the simplest way. The last version of the add-on compatible with the used version of the platform will be installed. Also, you can install the add-on by coordinates choosing the required version of the add-on from the table.

In case you want to install the add-on by manual editing or by building from sources see the complete add-ons installation guide in CUBA Platform documentation.

From the Marketplace

  1. Open your application in CUBA Studio. Check the latest version of CUBA Studio on the CUBA Platform site.
  2. Go to CUBA -> Marketplace in the main menu.

marketplace

  1. Find the Drag & Drop add-on there.

addons

  1. Click Install and apply the changes. The add-on corresponding to the used platform version will be installed.

By Coordinates

  1. Open your application in CUBA Studio. Check the latest version of CUBA Studio on the CUBA Platform site.
  2. Go to CUBA -> Marketplace in the main menu.
  3. Click the icon in the upper-right corner.

by-coordinates

  1. Paste the add-on coordinates in the corresponding field as follows:

com.haulmont.addon.dnd:cuba-dnd-global:<add-on version>

where <add-on version> is compatible with the used version of the CUBA platform.

Platform Version Add-on Version
7.2.x 1.7.0
7.1.x 1.6.0
7.0.x 1.5.0
6.10.x 1.4.0
6.9.x 1.3.0
6.8.x 1.2.0
6.7.x 1.1.0
6.6.x 1.0.0
  1. Click Install and apply the changes. The add-on will be installed to your project.

Usage

This add-on contains components that implement Drag-and-Drop functionality. To handle a component's drop action, it is necessary to implement DropHandler interface that contains two methods:

  • drop(DragAndDropEvent event) - method that handles transferable component's drop;
  • getCriterion() - returns the AcceptCriterion that is used to evaluate whether the dragged component will be handed over to drop method.

In order to accept a Drop event, components must provide the AcceptCriterion object. AcceptCriterion interface has two main inheritors - ServerSideCriterion and AcceptCriterionWrapper. If you want to check an accept criterion on a browser side, you have to implement AcceptCriterionWrapper interface.

Quick Start

After installation the add-on in your CUBA application follow the steps below:

  1. Create a simple screen.
  2. Add the following namespace in the XML descriptor of the screen:
xmlns:dnd="http://schemas.haulmont.com/dnd/0.1/drag-and-drop.xsd"

Sample Task

Let's try to create a small todo-list app. It will provide a predefined set of todo-actions that can be added to the todo-list.

In this app we will implement the following features:

  • dragging components to the list;
  • reordering components in the list;
  • deleting components.

Step-by-Step Guide

Create a new CUBA project and add the given add-on to it.

In the GENERIC UI tab create a new screen using "Blank screen" template.

We will divide this screen into two panels: the first panel is a palette that contains actions, the second is a dashboard that contains the list of actions. First of all, add the next namespace to the XML descriptor of the screen:

xmlns:dnd="http://schemas.haulmont.com/dnd/0.1/drag-and-drop.xsd"

and add the following block:

<layout>
    <hbox id="root"
          height="100%"
          width="100%"
          spacing="true"
          expand="rootSpacer">
        <groupBox id="rootPalette"
                  caption="Todo actions"
                  height="100%"
                  width="200px">
            <dnd:dndVBoxLayout id="palette"
                               dragMode="CLONE"
                               height="AUTO"
                               spacing="true"
                               width="100%">
            </dnd:dndVBoxLayout>
        </groupBox>
        <groupBox id="rootDashboard"
                  caption="Todo today"
                  width="500px"
                  height="100%">
            <scrollBox width="100%" height="100%">
                <dnd:dndVBoxLayout id="dashboard"
                                   dragMode="CLONE"
                                   height="AUTO"
                                   spacing="true"
                                   width="100%">
                </dnd:dndVBoxLayout>
            </scrollBox>
        </groupBox>
        <label id="rootSpacer"/>
    </hbox>
</layout>

Component with the "rootPalette" id contains the set of predefined actions. These components can be dragged by users. Component with the "dashboard" id contains a todo-list.

Palette and Dashboard have property dragMode which indicates that their components can be dragged.

This is how the screen looks in the running app:

Next, we should add actions that can be dragged by users and added in their list. We will use buttons as the components for drag and drop. Add a set of buttons to the palette with follow ids: call, chat, meeting, buy.

Your XML descriptor should look like this:

<dnd:dndVBoxLayout id="palette"
                   dragMode="CLONE"
                   height="AUTO"
                   spacing="true"
                   width="100%">
      <button id="call"
              caption="Call"
              height="40px"
              width="100%"/>
      <button id="chat"
              caption="Chat"
              height="40px"
              width="100%"/>
      <button id="meeting"
              caption="Meeting"
              height="40px"
              width="100%"/>
      <button id="buy"
              caption="Buy"
              height="40px"
              width="100%"/>
</dnd:dndVBoxLayout>

Make sure that the buttons are added and they can be dragged:

In the screen controller set DropHandler to the dashboard component:

@Inject
private DDVerticalLayout dashboard;

@Override
public void init(Map<String, Object> params) {
    dashboard.setDropHandler(new DropHandler() {
       @Override
       public void drop(DragAndDropEvent event) {

       }

       @Override
       public AcceptCriterion getCriterion() {
           return AcceptCriterion.ACCEPT_ALL;
       }
   });
}

The methods drop() and getCriterion() are invoked every time when the dragged component is dropped to the layout. In the drop() method we should implement our logic to handle component's drop.

Method getCriterion() allows us to specify the criteria to accept dropping various components to this layout (e.g. only Labels and/or Buttons etc.). In this case we accept all components: AcceptCriterion.ACCEPT_ALL.

If we now run this app and try to drag a button to the dashboard, we will see that the dropping area is not highlighted. This is due to the dashboard property height which is set to AUTO. To expand this layout we should write some CSS in the extension theme of the app. To do this, we create a theme extension in Project Properties.

Add the following code to the halo-ext.scss:

.min-height{
    min-height:50px;
}

and in the XML descriptor of the screen add stylename="min-height" to the dashboard component. Launch the app and check:

Now we should create some panel to display the added action in the list. For this purpose, create the method that returns Component.

public Component createDashboardElement(Component component) {

}

For the main component added directly to the dashboard we will use GroupBoxLayout. Then we will add HBoxLayout to the GroupBoxLayout and fill it with the components:

  • Label with appropriate serial number;
  • Label with name of action;
  • LookupField;
  • Button to remove from list.

Firstly, create GroupBoxLayout and HBoxLayout, specify width 100% for both components and add spacing to HBoxLayout:

GroupBoxLayout groupBox = factory.create(GroupBoxLayout.class);
groupBox.setWidth("100%");

HBoxLayout layout = factory.create(HBoxLayout.class);
layout.setWidth("100%");
layout.setSpacing(true);

Next, create main components for this panel. No need to set LookupField width as it will be expanded in HBoxLayout:

Label countLabel = factory.create(Label.class);
countLabel.setId("countLabel");
countLabel.setWidth("30px");

Label titleLabel = factory.create(Label.class);
titleLabel.setValue(((Button) component).getCaption());
titleLabel.setWidth("60px");

LookupField lookupField = factory.create(LookupField.class);

Button deleteButton = factory.create(Button.class);
deleteButton.setIcon("font-icon:TIMES");

Create BaseAction for the delete button. We'll implement the logic of deletion a bit later:

BaseAction action = new BaseAction("remove") {
    @Override
    public void actionPerform(Component component) {
    }
};
action.setCaption("");
deleteButton.setAction(action);

It remains to add the created components:

layout.add(countLabel);
layout.add(titleLabel);
layout.add(lookupField);
layout.expand(lookupField);
layout.add(deleteButton);
groupBox.add(layout);

Now we should handle component's drop to the dashboard. DragAndDropEvent event variable contains information about which component is being dragged, its source layout, to which component, to which position etc. To get this information you should use the following classes:

  • LayoutBoundTransferable;
  • DDVerticalLayoutTargetDetails.

In the drop() method we get references to the objects LayoutBoundTransferable and DDVerticalLayoutTargetDetails.

LayoutBoundTransferable t = (LayoutBoundTransferable) event.getTransferable();
DDVerticalLayoutTargetDetails details = (DDVerticalLayoutTargetDetails) event.getTargetDetails();

LayoutBoundTransferable contains two main methods:

  • getSourceComponent() - return the layout from which transferable component was dragged;
  • getTransferableComponent() - return transferable component;
Component sourceLayout = t.getSourceComponent();
Component tComponent = t.getTransferableComponent();

DDVerticalLayoutTargetDetails contains information about layout to drop the component to (receiving layout). The main methods are:

  • getTarget() - return layout to which drop;
  • getOverComponent() - return child component of the target layout, to which drop transferable component;
  • getDropLocation() - return drop location (Middle, Top, Bottom);
  • getOverIndex() - return index of child component; Get references to the layout to drop the transferable component to and VerticalDropLocation:
DDVerticalLayout targetLayout = (DDVerticalLayout) details.getTarget();
VerticalDropLocation loc = details.getDropLocation();

Next we should check whether transferable component is null. It is possible if we drag highlighted html5 text, as it is not a component.

if (tComponent == null) {
    return;
}

Define the index of component to which we drop (indexTo) and the index of transferable component in the target layout (indexFrom). The last index is necessary if the component is replaced in its layout. If a component is dragged from another layout, indexFrom will be -1.

int indexTo = details.getOverIndex();
int indexFrom = targetLayout.indexOf(tComponent);

Create reference to the object that will be added to the layout and check whether component is dragged from another layout, or it is just a reordering within the source layout;

Component componentToAdd;
if (sourceLayout == targetLayout) {

} else {

}

If this condition is fulfilled, we should assign the componentToAdd reference to the dragged component. Then we should check whether the component has changed its position or not. If the component has not changed its location, then finish method.

componentToAdd = tComponent;
if (indexFrom == indexTo) {
    return;
}

If the component has changed its location, we must delete it from its layout. Then check if the index of the receiving component is bigger than the index of the previous location, then we need to reduce it by one. This is because of removing the component from layout.

targetLayout.remove(tComponent);
if (indexTo > indexFrom) {
    indexTo--;
}

If the dragged component does not hit any position, indexTo will be -1. Since the component has already been removed from the layout, it must be added to its previous position.

if (indexTo == -1) {
    targetLayout.add(componentToAdd, indexFrom);
}

In case when component is dragged from another layout, it is necessary to create an appropriate component to display it in the dashboard. If the dragged component does not hit any of the components in the layout, we need to add it to the end of the list of layout components.

if (sourceLayout == targetLayout) {
    // some code
} else {
    componentToAdd = createDashboardElement(tComponent);
    if (indexTo == -1) {
        targetLayout.add(componentToAdd, targetLayout.getOwnComponents().size());
    }
}

Next we should check if the component hits to the child component of layout. If it hits, we check the drop location. If location is MIDDLE or BOTTOM, we must increase indexTo by one to add below a component to which the component was dragged.

if (indexTo != -1) {
    if (loc == VerticalDropLocation.MIDDLE || loc == VerticalDropLocation.BOTTOM) {
        indexTo++;
    }
    targetLayout.add(componentToAdd, indexTo);
}

As a result, the drop method looks like:

public void drop(DragAndDropEvent event) {
    LayoutBoundTransferable t = (LayoutBoundTransferable) event.getTransferable();
    DDVerticalLayoutTargetDetails details = (DDVerticalLayoutTargetDetails) event.getTargetDetails();

    Component sourceLayout = t.getSourceComponent();
    DDVerticalLayout targetLayout = (DDVerticalLayout) details.getTarget();
    Component tComponent = t.getTransferableComponent();

    VerticalDropLocation loc = details.getDropLocation();

    int indexTo = details.getOverIndex();
    int indexFrom = targetLayout.indexOf(tComponent);

    if (tComponent == null) {
        return;
    }
    Component componentToAdd;

    if (sourceLayout == targetLayout) {
        componentToAdd = tComponent;
        if (indexFrom == indexTo) {
            return;
        }
        targetLayout.remove(tComponent);
        if (indexTo > indexFrom) {
            indexTo--;
        }
        if (indexTo == -1) {
            targetLayout.add(tComponent, indexFrom);
        }
    } else {
        componentToAdd = createDashboardElement(tComponent);
        if (indexTo == -1) {
            targetLayout.add(componentToAdd, targetLayout.getOwnComponents().size());
        }
    }

    if (indexTo != -1) {
        if (loc == VerticalDropLocation.MIDDLE || loc == VerticalDropLocation.BOTTOM) {
            indexTo++;
        }
        targetLayout.add(componentToAdd, indexTo);
    }
}

Launch the app and check:

Note that serial numbers are not assigned to dashboard components. Create the method to fix it:

public void updateDashboardComponents(DDVerticalLayout layout) {

}

We need to set values to all countLabel. To achieve this, it is necessary to get the list of child dashboard components.

List<Component> components = new ArrayList<>(layout.getOwnComponents());
int count = 0;
for (Component component : components) {
     GroupBoxLayout groupBox = (GroupBoxLayout) component;
     HBoxLayout hBoxLayout = (HBoxLayout) Iterables.get(groupBox.getComponents(), 0);
     Label label = (Label)  Iterables.get(hBoxLayout.getComponents(), 0);
     label.setValue(++count);
}

Now at the end of drop() method add the follow invocation:

updateDashboardComponents(targetLayout);

It remains to implement the component removing from the dashboard. Add this code to actionPerfom() method:

public void actionPerform(Component component) {
    HBoxLayout hBox = (HBoxLayout) component.getParent();
    GroupBoxLayout groupBox = (GroupBoxLayout) hBox.getParent();
    DDVerticalLayout ddLayout = (DDVerticalLayout) groupBox.getParent();
    ddLayout.remove(groupBox);
    updateDashboardComponents(ddLayout);
}

Launch the app and check that the serial numbers are updated and the components are removed.