"DragView" possible with existing API?

I want to create a “DragView” which makes it possible to move its children to any position within that “DragView”, via dragging. Is this doable with the DragSource/DropTarget API vaadin provides?

Or is it better to create it myself with custom javascript and styles, which gives each added child a fixed position based on top and left pixels.

Update, did it myself:


import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Div;
import elemental.json.JsonObject;

import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

public class DragView extends Div {
    public final CopyOnWriteArrayList<Consumer<DragFinishEvent>> listeners = new CopyOnWriteArrayList<>();
    public final AtomicLong idCounter = new AtomicLong();

    public DragView() {
        setSizeFull();
        getStyle().set("border", "2px solid #ccc");
        getStyle().set("overflow", "hidden");
        UI.getCurrent().getElement().addEventListener("dragViewItemDragStopped", e -> {
                    JsonObject d = e.getEventData();
                    System.out.println(d.toJson());
                    String cId = d.get("event.detail.cid").asString();
                    int left = (int) d.get("event.detail.left").asNumber();
                    int top = (int) d.get("event.detail.top").asNumber();
                    Component target = null;
                    for (Component c : getChildren().toList()) {
                        String id = c.getId().get();
                        if(cId.equals(id)) {
                            target = c;
                            break;
                        }
                    }
                    Objects.requireNonNull(target);
                    DragFinishEvent finishEvent = new DragFinishEvent(target, left, top);
                    for (Consumer<DragFinishEvent> listener : listeners) {
                        listener.accept(finishEvent);
                    }
        })
                .addEventData("event.detail.cid")
                .addEventData("event.detail.left")
                .addEventData("event.detail.top");
    }

    public DragView onDragFinish(Consumer<DragFinishEvent> listener){
        listeners.add(listener);
        UI.getCurrent().addDetachListener(e -> {
            listeners.remove(listener);
        });
        return this;
    }

    @Override
    public void add(Collection<Component> components) {
        for (Component c : components) {
            c.setId(""+idCounter.getAndIncrement());
            c.getStyle().set("position", "absolute");
            c.getStyle().set("cursor", "pointer");
            c.addAttachListener(e -> {
                if(!e.isInitialAttach()) return;
                UI.getCurrent().getPage().executeJs("" +
                                "            const dragView = $0;\n" +
                        "            const item = $1;\n" +
                        "            item.addEventListener(\"mousedown\", startDrag);\n" +
                        "\n" +
                        "            function startDrag(e) {\n" +
                        "                const draggable = e.target;\n" +
                        "                const dragViewRect = dragView.getBoundingClientRect();\n" +
                        "\n" +
                        "                const initialX = e.clientX - draggable.getBoundingClientRect().left;\n" +
                        "                const initialY = e.clientY - draggable.getBoundingClientRect().top;\n" +
                        "\n" +
                        "                function moveElement(e) {\n" +
                        "                    e.preventDefault();\n" +
                        "                    const newX = e.clientX - initialX - dragViewRect.left;\n" +
                        "                    const newY = e.clientY - initialY - dragViewRect.top;\n" +
                        "\n" +
                        "                    draggable.style.left = newX + \"px\";\n" +
                        "                    draggable.style.top = newY + \"px\";\n" +
                        "                }\n" +
                        "\n" +
                        "                function stopDrag(e) {\n" +
                        "                    const event = new CustomEvent(\"dragViewItemDragStopped\", {\n" +
                        "                        bubbles: true,\n" +
                        "                        detail: {\n" +
                        "                            cid: draggable.id,\n" +
                        "                            left: parseInt(draggable.style.left),\n" +
                        "                            top: parseInt(draggable.style.top)\n" +
                        "                        }\n" +
                        "                    });\n" +
                        "                    dragView.dispatchEvent(event);\n" +
                        "\n" +
                        "                    document.removeEventListener(\"mousemove\", moveElement);\n" +
                        "                    document.removeEventListener(\"mouseup\", stopDrag);\n" +
                        "                }\n" +
                        "\n" +
                        "                document.addEventListener(\"mousemove\", moveElement);\n" +
                        "                document.addEventListener(\"mouseup\", stopDrag);\n" +
                        "\n" +
                        "                // Move the draggable to cursor position initially\n" +
                        "                const offsetX = e.clientX - dragViewRect.left - initialX;\n" +
                        "                const offsetY = e.clientY - dragViewRect.top - initialY;\n" +
                        "                draggable.style.left = offsetX + \"px\";\n" +
                        "                draggable.style.top = offsetY + \"px\";\n" +
                        "            }",this, c);
            });
        }
        super.add(components);
    }

    public static class DragFinishEvent {
        public Component target;
        public int left;
        public int top;

        public DragFinishEvent(Component target, int left, int top) {
            this.target = target;
            this.left = left;
            this.top = top;
        }
    }

}


There’s an addon that does something quite similar, you may be able to check its source code for ideas: sortable-layout - Vaadin Add-on Directory

Marcus already linked the addon. It’s also doable with drag/drop target but you will have to manage the drop before/after/between each element and the animation can be difficult.