From 05435cc8d4a1d5232d1bd2482d6e5847a1e172a5 Mon Sep 17 00:00:00 2001 From: Tomi Virtanen Date: Mon, 30 Sep 2024 16:36:06 +0300 Subject: [PATCH 1/4] feat: add drag image for DragSource 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 --- flow-dnd/pom.xml | 5 + .../vaadin/flow/component/dnd/DragSource.java | 79 ++++++++++++++++ .../flow/component/dnd/internal/DndUtil.java | 6 ++ .../resources/frontend/dndConnector.js | 12 +++ .../src/main/resources/dnd-simulation.js | 4 +- .../com/vaadin/flow/uitest/ui/DnDView.java | 47 +++++++++- .../src/main/webapp/images/gift.png | Bin 0 -> 294 bytes .../java/com/vaadin/flow/uitest/ui/DnDIT.java | 88 ++++++++++++++++++ 8 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 flow-tests/test-root-context/src/main/webapp/images/gift.png diff --git a/flow-dnd/pom.xml b/flow-dnd/pom.xml index 4fbb4629fc5..ac8871f5e10 100644 --- a/flow-dnd/pom.xml +++ b/flow-dnd/pom.xml @@ -20,6 +20,11 @@ flow-server ${project.version} + + com.vaadin + flow-html-components + ${project.version} + org.osgi osgi.core diff --git a/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java b/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java index 38694a634b4..13c586c77e7 100644 --- a/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java +++ b/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java @@ -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; /** @@ -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. + *

+ * {@link 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 + * MDN web docs 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. + *

+ * {@link 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 + * MDN web docs 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. diff --git a/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/internal/DndUtil.java b/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/internal/DndUtil.java index 1450630d3e3..08ddccd78b7 100644 --- a/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/internal/DndUtil.java +++ b/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/internal/DndUtil.java @@ -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}. diff --git a/flow-dnd/src/main/resources/META-INF/resources/frontend/dndConnector.js b/flow-dnd/src/main/resources/META-INF/resources/frontend/dndConnector.js index 7dcb97512bf..3cb3d0387ac 100644 --- a/flow-dnd/src/main/resources/META-INF/resources/frontend/dndConnector.js +++ b/flow-dnd/src/main/resources/META-INF/resources/frontend/dndConnector.js @@ -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) { @@ -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; } }; diff --git a/flow-test-util/src/main/resources/dnd-simulation.js b/flow-test-util/src/main/resources/dnd-simulation.js index 2d893a1bbac..970e312540b 100644 --- a/flow-test-util/src/main/resources/dnd-simulation.js +++ b/flow-test-util/src/main/resources/dnd-simulation.js @@ -10,7 +10,9 @@ function createEvent(typeOfEvent, effectAllowed, dropEffect) { return this.data[key]; }, effectAllowed: effectAllowed, - dropEffect: dropEffect + dropEffect: dropEffect, + setDragImage: function (img) { + } }; return event; } diff --git a/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/DnDView.java b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/DnDView.java index bd7dfa51bf7..f6b3af9760e 100644 --- a/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/DnDView.java +++ b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/DnDView.java @@ -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; @@ -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; @@ -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 -> { @@ -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); @@ -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++; @@ -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); } diff --git a/flow-tests/test-root-context/src/main/webapp/images/gift.png b/flow-tests/test-root-context/src/main/webapp/images/gift.png new file mode 100644 index 0000000000000000000000000000000000000000..be9197119db2a3592cbf903abd7927e82df90a08 GIT binary patch literal 294 zcmV+>0oneEP)^z^e!xzhX z&rkMy2t_e-XJ(H8kW}>{!_K)kGi%K3c^HP*;}tv}LI_&`=u~w}L>&N3-}pvVJ0dz{ zTtsqTs}vX!aTjAu-usPnZp@^4p6is-#LV8z_9D`p6kz5lpRcO#DWx%lu%0U-JMVo> zME3yjA|mxkfwpa%Wm%pOF~4$G*z;*-hpMXfecyll3NW*kM*hq(#`VQZ9K5N(xy#PR s_?Lx}bMeP(9%TGq;MN{oi#4b41^2y|a$n{=hyVZp07*qoM6N<$g0@tJOaK4? literal 0 HcmV?d00001 diff --git a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/DnDIT.java b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/DnDIT.java index 5f2e234bc78..87492d19268 100644 --- a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/DnDIT.java +++ b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/DnDIT.java @@ -89,6 +89,94 @@ 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"); + TestBenchElement laneElement = getLaneElement("COPY"); + clearEvents(); + + dragAndDrop(boxElement, laneElement); + + waitForElementPresent(By.id("event-4")); + + TestBenchElement eventlog = getEventlog(2); + String expected = "2: DragImage: \"Gift\""; + 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"); + TestBenchElement laneElement = getLaneElement("COPY"); + clearEvents(); + dragAndDrop(boxElement, laneElement); + + // need to wait for roundtrip, there should always be 3 events after dnd + // with drag image + waitForElementPresent(By.id("event-3")); + + TestBenchElement eventlog = getEventlog(2); + String expected = "2: DragImage: "; + 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(); From d54baf96d131bf278f8d601bfc5ac49a1f746371 Mon Sep 17 00:00:00 2001 From: Tomi Virtanen Date: Tue, 1 Oct 2024 10:11:28 +0300 Subject: [PATCH 2/4] chore: removed unnecessary dependency --- flow-dnd/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/flow-dnd/pom.xml b/flow-dnd/pom.xml index ac8871f5e10..4fbb4629fc5 100644 --- a/flow-dnd/pom.xml +++ b/flow-dnd/pom.xml @@ -20,11 +20,6 @@ flow-server ${project.version} - - com.vaadin - flow-html-components - ${project.version} - org.osgi osgi.core From 34cfffdc95c0ba4f1a7bbe52273ed3de0a6ef37d Mon Sep 17 00:00:00 2001 From: Tomi Virtanen Date: Tue, 1 Oct 2024 10:25:47 +0300 Subject: [PATCH 3/4] chore: cleanup tests --- .../src/test/java/com/vaadin/flow/uitest/ui/DnDIT.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/DnDIT.java b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/DnDIT.java index 87492d19268..5e7b4928f38 100644 --- a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/DnDIT.java +++ b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/DnDIT.java @@ -97,12 +97,11 @@ public void testSetDragImage_withImage() { // effect could be anything, just testing the drag image. TestBenchElement boxElement = getBoxElement("COPY"); - TestBenchElement laneElement = getLaneElement("COPY"); clearEvents(); - dragAndDrop(boxElement, laneElement); + drag(boxElement); - waitForElementPresent(By.id("event-4")); + waitForElementPresent(By.id("event-2")); TestBenchElement eventlog = getEventlog(2); String expected = "2: DragImage: \"Gift\""; @@ -139,13 +138,12 @@ public void testSetDragImage_withVisibleComponentInViewport() { clickElementWithJs("button-toggle-image"); TestBenchElement boxElement = getBoxElement("COPY"); - TestBenchElement laneElement = getLaneElement("COPY"); clearEvents(); - dragAndDrop(boxElement, laneElement); + drag(boxElement); // need to wait for roundtrip, there should always be 3 events after dnd // with drag image - waitForElementPresent(By.id("event-3")); + waitForElementPresent(By.id("event-2")); TestBenchElement eventlog = getEventlog(2); String expected = "2: DragImage: "; From 917814d8b0c29eabb1d1f3f954e9db157016cb0b Mon Sep 17 00:00:00 2001 From: Tomi Virtanen Date: Tue, 1 Oct 2024 10:36:56 +0300 Subject: [PATCH 4/4] chore: update link in Javadoc --- .../main/java/com/vaadin/flow/component/dnd/DragSource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java b/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java index 13c586c77e7..e67dd3dd17b 100644 --- a/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java +++ b/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java @@ -307,7 +307,7 @@ default EffectAllowed getEffectAllowed() { * image is shown by default with zero offset which means that pointer * location is in the top left corner of the image. *

- * {@link com.vaadin.flow.component.html.Image} is fully supported as a drag + * {@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. @@ -328,7 +328,7 @@ default void setDragImage(Component dragImage) { * Coordinates define the offset of the pointer location from the top left * corner of the image. *

- * {@link com.vaadin.flow.component.html.Image} is fully supported as a drag + * {@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.