Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add drag image for DragSource #20098

Merged
merged 6 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading