Drag & Drop to precise position on Div

I want to move a component inside a target Div with pixel precision using Drag & Drop.
I can’t seem to get the position of the “shadow component” that is dragged around, has anyone solved this before?

CSS

.cardComponent {
    width: 180px;
    height: 220px;
    color: black;
    background: white;
    margin: 0;
    padding: 0;
    border-color: #0f728f;
    border-style: double;
    border-width: 10px;
    position: relative;
    z-index: 100;
    opacity: 1.0;
}

.dropTarget {
    width: 100%;
    height: 100%;
    background: #cdcdcd;
    z-index: 10;
}

Java


@Route("workflows")
@PageTitle("Workflows")
@Menu(order = 10, icon = "vaadin:clipboard-check", title = "Workflows")
@AnonymousAllowed
public class WorkflowView extends Div {

    private long DROP_AREA_X_OFFSET = 0;
    private long DROP_AREA_Y_OFFSET = 0;

    Div targetBox;
    CardComponent draggableCard;
    long mouseX = 0;
    long mouseY = 0;

    public WorkflowView() {
        setSizeFull();
        initUI();
        initUX();
        targetBox.add(draggableCard);
        add(targetBox);
    }

    private void initUI() {
        draggableCard = new CardComponent();
        draggableCard.addClassName("cardComponent");
        targetBox = new Div();
        targetBox.addClassName("dropTarget");
    }

    public void initUX() {
        createDropTarget();

        DragSource<CardComponent> dragSource = DragSource.create(draggableCard);
        dragSource.addDragStartListener(event -> {
            event.setDragData("Some data to transfer");
        });

        dragSource.addDragEndListener(event -> {
            String left = event.getComponent().getElement().getStyle().get("left");
            String top = event.getComponent().getElement().getStyle().get("top");
            System.out.println("addDragEndListener left = " + left + ", top = " + top);
            System.out.println("addDragEndListener mouseX = " + mouseX + ", mouseY = " + mouseY);
            draggableCard.getStyle().setLeft((mouseX ) + "px");
            draggableCard.getStyle().setTop((mouseY) + "px");
        });
    }

    private void createDropTarget() {
        DropTarget<Div> dropTarget = DropTarget.create(targetBox);
        dropTarget.setActive(true);

        dropTarget.addDropListener(event -> {
            event.getDragSourceComponent().ifPresent(component -> {
                String left = component.getElement().getStyle().get("left");
                String top = component.getElement().getStyle().get("top");
                System.out.println("addDropListener left = " + left + ", top = " + top);
            });
        });

        // This is just to be able to move Card with precision
        targetBox.addClickListener(event -> {
            draggableCard.getStyle().setLeft((event.getClientX() - DROP_AREA_X_OFFSET) + "px");
            draggableCard.getStyle().setTop((event.getClientY() - DROP_AREA_Y_OFFSET) + "px");
            // System.out.println("addClickListener left = " + (event.getClientX() - DROP_AREA_X_OFFSET) + " top = " + (event.getClientY() - DROP_AREA_Y_OFFSET));
        });
    }

    private void catchMousePositions(UI ui, Element element) {
        ui.getPage().executeJs(
    "$1.addEventListener('mousemove', function(e) {" +
              " $0.$server.updateMousePosition(e.clientX, e.clientY); });", this.getElement() , element
        );
    }

    private void releaseMousePositions(UI ui, Element element) {
        ui.getPage().executeJs("$0.removeEventListener('mousemove');", element);
    }

    // This only works if coming from another view the first time!?
    private void getBoundingClientRect(UI ui, Element element) {
        ui.getPage().executeJs("return $0.getBoundingClientRect();", element).then(jsonValue -> {
            DROP_AREA_X_OFFSET = Math.round(jsonValue.get("x").asDouble());
            DROP_AREA_Y_OFFSET = Math.round(jsonValue.get("y").asDouble());
        });
    }

    @Override
    protected void onAttach(AttachEvent attachEvent) {
        catchMousePositions(attachEvent.getUI(), targetBox.getElement());
        getBoundingClientRect(attachEvent.getUI(), targetBox.getElement());
    }

    @Override
    protected void onDetach(DetachEvent detachEvent) {
        releaseMousePositions(detachEvent.getUI(), targetBox.getElement());
    }

    @ClientCallable
    public void updateMousePosition(long x, long y) {
        mouseX = x - DROP_AREA_X_OFFSET; mouseY = y - DROP_AREA_Y_OFFSET;
        // System.out.println("updateMousePosition mouseX = " + mouseX + ", mouseY = " + mouseY);
    }


    @Data
    public class CardComponent extends Div implements DragSource<CardComponent>, HasStyle {

        private String objType ="User ...";     // default
        private String description = "Enter desc ...";
        private VerticalLayout verticalLayout;

        public CardComponent() {
            // all cards will be draggable by default
            TextField tf = new TextField();
            tf.setWidth("100%");
            tf.getStyle().set("padding", "0");
            tf.getStyle().set("margin", "0");
            tf.setValue(objType);
            TextArea ta = new TextArea();
            ta.setSizeFull();
            ta.getStyle().set("padding", "0");
            ta.getStyle().set("margin", "0");
            ta.setValue(description);
            Hr hr = new Hr();
            hr.setWidth("100%");
            verticalLayout = new VerticalLayout(tf, hr, ta);
            verticalLayout.setSizeFull();
            add(verticalLayout);
            setDraggable(true);
        }

    }
}

There seem to be two problems:

  • My mouse movement handler is suspended while dragging
  • There is no position available on the drag image when dropping

So I made a hack to get the mouse position “shortly after the drop” and it kind of works, but it’s dirty so there may be situations where it doesn’t work. One small problem is that the final position depends on a little mouse jitter, to get a mouse move event, hence it is not pixel perfect placement.

Anyway, if you are interested, here is my “solution” and do feel free to come up with a better solution.

CSS

.workflowview {
    margin: 0;
    padding: 0;
}

.cardComponent {
    width: 180px;
    height: 220px;
    left: 0px;
    top: 0px;
    color: black;
    background: white;
    margin: 0;
    padding: 0;
    border-color: #0f728f;
    border-style: double;
    border-width: 10px;
    position: relative;
    z-index: 100;
    opacity: 1.0;
}

.cardComponent vaadin-text-field {
    width: 100%;
    padding: 0;
    margin: 0;
}

.cardComponent vaadin-text-area {
    width: 100%;
    height: 100%;
    padding: 0;
    margin: 0;
}

.dropTarget {
    width: 100%;
    height: 100%;
    background: #cdcdcd;
    z-index: 10;
}

Java

@Route("workflows")
@PageTitle("Workflows")
@Menu(order = 10, icon = "vaadin:clipboard-check", title = "Workflows")
@AnonymousAllowed
public class WorkflowView extends Div {

    private long DROP_AREA_X_OFFSET = 0;
    private long DROP_AREA_Y_OFFSET = 0;
    private long lastMouseX = 0;
    private long lastMouseY = 0;

    private Div targetBox;
    private CardComponent draggableCard;

    public WorkflowView() {
        setSizeFull();
        addClassName("workflowview");
        initUI();
        initUX();
        targetBox.add(draggableCard);
        add(targetBox);
    }

    private void initUI() {
        draggableCard = new CardComponent();
        targetBox = new Div();
        targetBox.addClassName("dropTarget");
    }

    public void initUX() {
        createDropTarget();

        DragSource<CardComponent> dragSource = DragSource.create(draggableCard);
        dragSource.addDragStartListener(event -> {
            draggableCard.setStartOffset(lastMouseX, lastMouseY);
            event.setDragData("Some data to transfer");
        });

        dragSource.addDragEndListener(event -> {
        });
    }

    private void createDropTarget() {
        DropTarget<Div> dropTarget = DropTarget.create(targetBox);
        dropTarget.setActive(true);

        dropTarget.addDropListener(event -> {
            event.getDragSourceComponent().ifPresent(component -> {
                draggableCard.setDragCount(2);      // Start countdown of mouse positions   BIG HACK !!
            });
        });
    }

    private void catchMousePositions(UI ui, Element element) {
        ui.getPage().executeJs(
"$1.addEventListener('mousemove', function(e) {" +
          " $0.$server.updateMousePosition(e.clientX, e.clientY); });", this.getElement() , element
        );
    }

    private void releaseMousePositions(UI ui, Element element) {
        ui.getPage().executeJs("$0.removeEventListener('mousemove');", element);
    }

    // This only works initially if coming from another view the first time or after the first drag of the Card it seems!?
    private void getBoundingClientRect(UI ui, Element element) {
        ui.getPage().executeJs("return $0.getBoundingClientRect();", element).then(jsonValue -> {
            DROP_AREA_X_OFFSET = Math.round(jsonValue.get("x").asDouble());
            DROP_AREA_Y_OFFSET = Math.round(jsonValue.get("y").asDouble());
        });
    }

    @Override
    protected void onAttach(AttachEvent attachEvent) {
        getBoundingClientRect(UI.getCurrent(), targetBox.getElement());
        catchMousePositions(attachEvent.getUI(), targetBox.getElement());
    }

    @Override
    protected void onDetach(DetachEvent detachEvent) {
        releaseMousePositions(detachEvent.getUI(), targetBox.getElement());
    }

    @ClientCallable
    public void updateMousePosition(long x, long y) {
        lastMouseX = x;
        lastMouseY = y;
        if (draggableCard.getDragCount() > 0 && x != 0 && y != 0) {
            draggableCard.setDragCount(draggableCard.getDragCount() - 1);       // Count down mouse position after drop - BIG HACK !!
            if (draggableCard.getDragCount() == 0) {
                draggableCard.getStyle().setLeft("" + (x - DROP_AREA_X_OFFSET - draggableCard.getStartOffsetX()) + "px");
                draggableCard.getStyle().setTop("" + (y - DROP_AREA_Y_OFFSET - draggableCard.getStartOffsetY()) + "px");
                // System.out.println("updateMousePosition X = " + draggableCard.getStyle().get("left") + ", Y = " + draggableCard.getStyle().get("top"));
            }
        }

    }

    @Data
    public class CardComponent extends Div implements DragSource<CardComponent>, HasStyle {

        private VerticalLayout verticalLayout;
        private int dragCount = 0;
        private long startOffsetX = 0;
        private long startOffsetY = 0;

        public CardComponent() {
            addClassName("cardComponent");
            TextField tf = new TextField();
            tf.setPlaceholder("Set actor ..");
            TextArea ta = new TextArea();
            ta.setPlaceholder("Enter description ..");
            Hr hr = new Hr();
            hr.setWidth("100%");
            verticalLayout = new VerticalLayout(tf, hr, ta);
            verticalLayout.setSizeFull();
            add(verticalLayout);
            // all cards will be draggable by default
            setDraggable(true);
        }

        public void setStartOffset(long offsetX, long offsetY) {
            // Apparently adding a CSS Class with "left" and "top" does not work as a guard against null values!?
            if (getStyle().get("left") != null) {
                startOffsetX = offsetX - Long.parseLong(getStyle().get("left").substring(0, getStyle().get("left").length() - 2)) - DROP_AREA_X_OFFSET;
                startOffsetY = offsetY - Long.parseLong(getStyle().get("top").substring(0, getStyle().get("top").length() - 2)) - DROP_AREA_Y_OFFSET;
            }
        }

    }
}

Hi! I’m bit behind my forum post queues, but this cauth my eye as I saw the same app on a differnet topic earlier :nerd_face: The drag and drop APIs in Vaadin are indeed quite limited since Vaadin 10 (7&8 had much more flexible API). I have also needed to hack around the limitations earlier.

Although Vaadin doesn’t expose clientX/clientY in the API, all drag events indeed extend from MouseEvent on client side. Thus “a slighly less hacky workaround” would be to attach custom DOM/Element level listeners for dragstart and drop events and read the values in those instead. I drafted an example here:

This is probably a closest exisiting Vaadin issue you should go and thumb up.

1 Like

There is now also a PR to add more “metadata” to the drag and drop API. If we can get this PR through, you shouldn’t need a custom DOM level event anymore.

1 Like