Skip to content

Commit

Permalink
feat: add drag image for DragSource (#20098)
Browse files Browse the repository at this point in the history
Adds `DragSource#setDragImage(ComponentdragImage )` and `DragSource#setDragImage(Component dragImage, int offsetX, int offsetY)`. API is used to set image component as a drag image for drag source component. Follows specification of HTML Drag and Drop API for DataTransfer#setDragImage() method.

Fixes: #6793
  • Loading branch information
tltv authored Oct 2, 2024
1 parent 623dae4 commit 95db483
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dnd.internal.DndUtil;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
import com.vaadin.flow.shared.Registration;

/**
Expand Down Expand Up @@ -300,6 +301,84 @@ default EffectAllowed getEffectAllowed() {
.toUpperCase(Locale.ENGLISH)));
}

/**
* Sets the drag image for the current drag source element. The image is
* applied automatically in the next drag start event in the browser. Drag
* image is shown by default with zero offset which means that pointer
* location is in the top left corner of the image.
* <p>
* {@code com.vaadin.flow.component.html.Image} is fully supported as a drag
* image component. Other components can be used as well, but the support
* may vary between browsers. If given component is visible element in the
* viewport, browser can show it as a drag image.
*
* @see <a href=
* "https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setDragImage">
* MDN web docs</a> for more information.
* @param dragImage
* the image to be used as drag image or null to remove it
*/
default void setDragImage(Component dragImage) {
setDragImage(dragImage, 0, 0);
}

/**
* Sets the drag image for the current drag source element. The image is
* applied automatically in the next drag start event in the browser.
* Coordinates define the offset of the pointer location from the top left
* corner of the image.
* <p>
* {@code com.vaadin.flow.component.html.Image} is fully supported as a drag
* image component. Other components can be used as well, but the support
* may vary between browsers. If given component is visible element in the
* viewport, browser can show it as a drag image.
*
* @see <a href=
* "https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setDragImage">
* MDN web docs</a> for more information.
* @param dragImage
* the image to be used as drag image or null to remove it
* @param offsetX
* the x-offset of the drag image
* @param offsetY
* the y-offset of the drag image
*/
default void setDragImage(Component dragImage, int offsetX, int offsetY) {
if (getDragImage() != null && getDragImage() != dragImage) {
// Remove drag image from the virtual children list if it's there.
if (getDraggableElement().getNode()
.hasFeature(VirtualChildrenList.class)) {
VirtualChildrenList childrenList = getDraggableElement()
.getNode().getFeature(VirtualChildrenList.class);
// dodging exception with empty list
if (childrenList.size() > 0) {
getDraggableElement()
.removeVirtualChild(getDragImage().getElement());
}
}
}
if (dragImage != null && !dragImage.isAttached()) {
getDraggableElement().appendVirtualChild(dragImage.getElement());
}
ComponentUtil.setData(getDragSourceComponent(),
DndUtil.DRAG_SOURCE_IMAGE, dragImage);
getDraggableElement().executeJs(
"window.Vaadin.Flow.dndConnector.setDragImage($0, $1, $2, $3)",
dragImage, (dragImage == null ? 0 : offsetX),
(dragImage == null ? 0 : offsetY), getDraggableElement());
}

/**
* Get server side drag image. This image is applied automatically in the
* next drag start event in the browser.
*
* @return Server side drag image if set, otherwise {@literal null}.
*/
default Component getDragImage() {
return (Component) ComponentUtil.getData(getDragSourceComponent(),
DndUtil.DRAG_SOURCE_IMAGE);
}

/**
* Attaches dragstart listener for the current drag source. The listener is
* triggered when dragstart event happens on the client side.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ public class DndUtil {
*/
public static final String DRAG_SOURCE_DATA_KEY = "drag-source-data";

/**
* Key for storing server side drag image for a
* {@link com.vaadin.flow.component.dnd.DragSource}.
*/
public static final String DRAG_SOURCE_IMAGE = "drag-source-image";

/**
* Key for storing an internal drag start listener registration for a
* {@link com.vaadin.flow.component.dnd.DragSource}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ window.Vaadin.Flow.dndConnector = {
}
event.currentTarget.classList.add('v-dragged');
}
if(event.currentTarget.__dragImage) {
event.dataTransfer.setDragImage(
event.currentTarget.__dragImage,
event.currentTarget.__dragImageOffsetX,
event.currentTarget.__dragImageOffsetY);
}
},

__dragendListener: function (event) {
Expand All @@ -106,5 +112,11 @@ window.Vaadin.Flow.dndConnector = {
element.removeEventListener('dragstart', this.__dragstartListener, false);
element.removeEventListener('dragend', this.__dragendListener, false);
}
},

setDragImage: function (dragImage, offsetX, offsetY, dragSource) {
dragSource.__dragImage = dragImage;
dragSource.__dragImageOffsetX = offsetX;
dragSource.__dragImageOffsetY = offsetY;
}
};
4 changes: 3 additions & 1 deletion flow-test-util/src/main/resources/dnd-simulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ function createEvent(typeOfEvent, effectAllowed, dropEffect) {
return this.data[key];
},
effectAllowed: effectAllowed,
dropEffect: dropEffect
dropEffect: dropEffect,
setDragImage: function (img) {
}
};
return event;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.vaadin.flow.uitest.ui;

import java.util.Optional;
import java.util.stream.Stream;

import com.vaadin.flow.component.Component;
Expand All @@ -24,7 +25,9 @@
import com.vaadin.flow.component.dnd.DropTarget;
import com.vaadin.flow.component.dnd.EffectAllowed;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.uitest.servlet.ViewTestLayout;

Expand All @@ -36,12 +39,16 @@ public class DnDView extends Div {
private int eventCounter = 0;

private boolean data;
private boolean dragImage;
private Component image = new Image("/images/gift.png", "Gift");

public DnDView() {
setWidth("1000px");
setHeight("800px");
getStyle().set("display", "flex");

Div startLane = createLane("start");

eventLog = new Div();
eventLog.add(new Text("Events:"));
eventLog.add(new NativeButton("Clear", event -> {
Expand All @@ -53,13 +60,32 @@ public DnDView() {
data = !data;
event.getSource().setText("Data: " + data);
}));
NativeButton toggleImage = new NativeButton("Toggle image", event -> {
if (image instanceof Image) {
image = event.getSource();
} else {
image = new Image("/images/gift.png", "Gift");
}
setDragImage(startLane, image);
});
toggleImage.setEnabled(false);
toggleImage.setId("button-toggle-image");
NativeButton toggleDragImageEnabled = new NativeButton(
"DragImage: " + dragImage, event -> {
dragImage = !dragImage;
toggleImage.setEnabled(dragImage);
event.getSource().setText("DragImage: " + dragImage);
setDragImage(startLane, image);
});
toggleDragImageEnabled.setId("button-toggle-drag-image-enabled");
eventLog.add(toggleDragImageEnabled);
eventLog.add(toggleImage);
eventLog.setHeightFull();
eventLog.setWidth("400px");
eventLog.getStyle().set("display", "inline-block").set("border",
"2px " + "solid");
add(eventLog);

Div startLane = createLane("start");
startLane.add(createDraggableBox(null));
Stream.of(EffectAllowed.values()).map(this::createDraggableBox)
.forEach(startLane::add);
Expand All @@ -84,6 +110,18 @@ public DnDView() {
noneDropLane, deactivatedLane);
}

private void setDragImage(Div startLane, Component image) {
startLane.getChildren().forEach(component -> {
if (component instanceof Div box) {
if (dragImage) {
DragSource.configure(box).setDragImage(image, 20, 20);
} else {
DragSource.configure(box).setDragImage(null);
}
}
});
}

private void addLogEntry(String eventDetails) {
Div div = new Div();
eventCounter++;
Expand Down Expand Up @@ -165,6 +203,13 @@ private Component createDraggableBox(EffectAllowed effectAllowed) {
}
dragSource.addDragStartListener(event -> {
addLogEntry("Start: " + event.getComponent().getText());
if (dragImage) {
Element dragElement = Optional
.ofNullable(DragSource.configure(event.getSource())
.getDragImage())
.map(Component::getElement).orElse(null);
addLogEntry("DragImage: " + dragElement);
}
if (data) {
dragSource.setDragData(identifier);
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,92 @@ public void testCopyEffectElement_disableTarget_dragOverTargetNotPresent() {
Assert.assertFalse(targetElement.hasClassName("v-drag-over-target"));
}

@Test
public void testSetDragImage_withImage() {
open();

clickElementWithJs("button-toggle-drag-image-enabled");

// effect could be anything, just testing the drag image.
TestBenchElement boxElement = getBoxElement("COPY");
clearEvents();

drag(boxElement);

waitForElementPresent(By.id("event-2"));

TestBenchElement eventlog = getEventlog(2);
String expected = "2: DragImage: <img alt=\"Gift\" src=\"/images/gift.png\">";
Assert.assertEquals("Invalid drag image", expected, eventlog.getText());
}

@Test
public void testSetDragImage_imageIsClearedWithNull() {
open();

clickElementWithJs("button-toggle-drag-image-enabled");
TestBenchElement boxElement = getBoxElement("COPY");
TestBenchElement laneElement = getLaneElement("COPY");
clearEvents();
dragAndDrop(boxElement, laneElement);

// clears drag image to null
clickElementWithJs("button-toggle-drag-image-enabled");

clearEvents();
dragAndDrop(boxElement, laneElement);
waitForElementPresent(By.id("event-3"));
Assert.assertEquals("Invalid event order", "1: Start: COPY",
getEventlog(1).getText());
Assert.assertEquals("Invalid event order", "2: Drop: COPY COPY",
getEventlog(2).getText());
}

@Test
public void testSetDragImage_withVisibleComponentInViewport() {
open();

clickElementWithJs("button-toggle-drag-image-enabled");
clickElementWithJs("button-toggle-image");

TestBenchElement boxElement = getBoxElement("COPY");
clearEvents();
drag(boxElement);

// need to wait for roundtrip, there should always be 3 events after dnd
// with drag image
waitForElementPresent(By.id("event-2"));

TestBenchElement eventlog = getEventlog(2);
String expected = "2: DragImage: <button id=\"button-toggle-image\">Toggle image</button>";
Assert.assertEquals("Invalid drag image", expected, eventlog.getText());
}

// visible component in viewport does not generate virtual element for drag
// image.
@Test
public void testSetDragImage_visibleComponentInViewportIsClearedWithNull() {
open();

clickElementWithJs("button-toggle-drag-image-enabled");
clickElementWithJs("button-toggle-image");
TestBenchElement boxElement = getBoxElement("COPY");
TestBenchElement laneElement = getLaneElement("COPY");
clearEvents();
dragAndDrop(boxElement, laneElement);

// clears drag image to null
clickElementWithJs("button-toggle-drag-image-enabled");

clearEvents();
dragAndDrop(boxElement, laneElement);
waitForElementPresent(By.id("event-3"));
Assert.assertEquals("Invalid event order", "1: Start: COPY",
getEventlog(1).getText());
Assert.assertEquals("Invalid event order", "2: Drop: COPY COPY",
getEventlog(2).getText());
}

private void dragBoxToLanes(TestBenchElement boxElement,
TestBenchElement laneElement, boolean dropShouldOccur) {
clearEvents();
Expand Down

0 comments on commit 95db483

Please sign in to comment.