summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPekka Hyvönen <pekka@vaadin.com>2017-05-11 14:36:27 +0300
committerGitHub <noreply@github.com>2017-05-11 14:36:27 +0300
commitf72ac12fd257e218c370a4d686be4cc99c2a22d6 (patch)
treedf7937c1835dcd6517717332147cae383a27c05c
parente2e3058a497f43f34f2fcfadf6b63de9211be659 (diff)
downloadvaadin-framework-f72ac12fd257e218c370a4d686be4cc99c2a22d6.tar.gz
vaadin-framework-f72ac12fd257e218c370a4d686be4cc99c2a22d6.zip
Add mobile html5 dnd support using polyfill (#9282)
First step of mobile DND support. - Add mobile html5 dnd support using polyfill - Adds a switch for enabling mobile html5 dnd support - Adds polyfill only when needed - Ignore native Android Chrome drag start because doesn't work properly (no dragend event fired) - Add documentation on enabling mobile HTML5 DnD support - Add mention of drag-drop-polyfill license - Fixed issue in polyfill when not using "snapback" - Add mention of forked polyfill Fixes #9174
-rw-r--r--all/src/main/resources/WebContent/license.html8
-rw-r--r--all/src/main/resources/WebContent/licenses/bsd-2-clause-drag-drop-polyfill.txt9
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/grid/GridDragSourceConnector.java25
-rw-r--r--client/src/main/java/com/vaadin/client/extensions/DragSourceExtensionConnector.java60
-rw-r--r--client/src/main/java/com/vaadin/client/extensions/DropTargetExtensionConnector.java5
-rw-r--r--client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java27
-rw-r--r--documentation/advanced/advanced-dragndrop.asciidoc50
-rw-r--r--server/src/main/java/com/vaadin/server/Page.java46
-rw-r--r--server/src/main/java/com/vaadin/server/communication/UidlWriter.java7
-rw-r--r--server/src/main/java/com/vaadin/ui/UI.java74
-rw-r--r--server/src/main/resources/VAADIN/drag-drop-polyfill.min.js4
-rw-r--r--shared/src/main/java/com/vaadin/shared/ApplicationConstants.java9
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/ui/PageClientRpc.java13
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/ui/UIState.java5
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/grid/GridDragAndDrop.java14
-rw-r--r--uitest/src/main/java/com/vaadin/tests/dnd/DragAndDropCardShuffle.java38
16 files changed, 365 insertions, 29 deletions
diff --git a/all/src/main/resources/WebContent/license.html b/all/src/main/resources/WebContent/license.html
index fe37813e57..b051865193 100644
--- a/all/src/main/resources/WebContent/license.html
+++ b/all/src/main/resources/WebContent/license.html
@@ -188,11 +188,17 @@
<td>Bourbon</td>
<td><a href="licenses/the-mit-license.txt">The MIT License</a></td>
</tr>
+ <tr>
+ <td><a href="https://github.com/timruffles/ios-html5-drag-drop-shim">drag-drop-polyfill</a>**</td>
+ <td><a href="licenses/bsd-2-clause-drag-drop-polyfill.txt">BSD-2-clause</a></td>
+ </tr>
</tbody>
</table>
<p>* Not required by Vaadin, only used if provided by the user.<br/>
-^ Only if <tt>vaadin-push</tt> is used.</p>
+^ Only if <tt>vaadin-push</tt> is used.<br/>
+** Used only when mobile drag and drop is enabled.
+</p>
<h4>Vaadin Development Dependencies</h4>
diff --git a/all/src/main/resources/WebContent/licenses/bsd-2-clause-drag-drop-polyfill.txt b/all/src/main/resources/WebContent/licenses/bsd-2-clause-drag-drop-polyfill.txt
new file mode 100644
index 0000000000..36eea63972
--- /dev/null
+++ b/all/src/main/resources/WebContent/licenses/bsd-2-clause-drag-drop-polyfill.txt
@@ -0,0 +1,9 @@
+Copyright (c) 2013 Tim Ruffles
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/GridDragSourceConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/GridDragSourceConnector.java
index 134e76e7e5..64e9c2e7b5 100644
--- a/client/src/main/java/com/vaadin/client/connectors/grid/GridDragSourceConnector.java
+++ b/client/src/main/java/com/vaadin/client/connectors/grid/GridDragSourceConnector.java
@@ -74,8 +74,9 @@ public class GridDragSourceConnector extends DragSourceExtensionConnector {
protected void extend(ServerConnector target) {
gridConnector = (GridConnector) target;
- // Do not make elements draggable on touch devices
- if (BrowserInfo.get().isTouchDevice()) {
+ // HTML5 DnD is by default not enabled for mobile devices
+ if (BrowserInfo.get().isTouchDevice() && !getConnection()
+ .getUIConnector().isMobileHTML5DndEnabled()) {
return;
}
@@ -89,6 +90,15 @@ public class GridDragSourceConnector extends DragSourceExtensionConnector {
@Override
protected void onDragStart(Event event) {
+ NativeEvent nativeEvent = (NativeEvent) event;
+
+ // Do not allow drag starts from native Android Chrome, since it doesn't
+ // work properly (doesn't fire dragend reliably)
+ if (isAndoidChrome() && isNativeDragEvent(nativeEvent)) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
// Collect the keys of dragged rows
draggedItemKeys = getDraggedRows(event).stream()
@@ -195,8 +205,17 @@ public class GridDragSourceConnector extends DragSourceExtensionConnector {
@Override
protected void onDragEnd(Event event) {
+ NativeEvent nativeEvent = (NativeEvent) event;
+
+ // for android chrome we use the polyfill, in case browser fires a
+ // native dragend event after the polyfill, we need to ignore that one
+ if (isAndoidChrome() && isNativeDragEvent((nativeEvent))) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
// Ignore event if there are no items dragged
- if (draggedItemKeys.size() > 0) {
+ if (draggedItemKeys != null && draggedItemKeys.size() > 0) {
super.onDragEnd(event);
}
diff --git a/client/src/main/java/com/vaadin/client/extensions/DragSourceExtensionConnector.java b/client/src/main/java/com/vaadin/client/extensions/DragSourceExtensionConnector.java
index e718fb90d7..9e0a76788d 100644
--- a/client/src/main/java/com/vaadin/client/extensions/DragSourceExtensionConnector.java
+++ b/client/src/main/java/com/vaadin/client/extensions/DragSourceExtensionConnector.java
@@ -67,8 +67,9 @@ public class DragSourceExtensionConnector extends AbstractExtensionConnector {
protected void extend(ServerConnector target) {
dragSourceWidget = ((ComponentConnector) target).getWidget();
- // Do not make elements draggable on touch devices
- if (BrowserInfo.get().isTouchDevice()) {
+ // HTML5 DnD is by default not enabled for mobile devices
+ if (BrowserInfo.get().isTouchDevice() && !getConnection()
+ .getUIConnector().isMobileHTML5DndEnabled()) {
return;
}
@@ -159,6 +160,14 @@ public class DragSourceExtensionConnector extends AbstractExtensionConnector {
// Convert elemental event to have access to dataTransfer
NativeEvent nativeEvent = (NativeEvent) event;
+ // Do not allow drag starts from native Android Chrome, since it doesn't
+ // work properly (doesn't fire dragend reliably)
+ if (isAndoidChrome() && isNativeDragEvent(nativeEvent)) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
// Set effectAllowed parameter
if (getState().effectAllowed != null) {
setEffectAllowed(nativeEvent.getDataTransfer(),
@@ -281,11 +290,20 @@ public class DragSourceExtensionConnector extends AbstractExtensionConnector {
* browser event to be handled
*/
protected void onDragEnd(Event event) {
+ NativeEvent nativeEvent = (NativeEvent) event;
+
+ // for android chrome we use the polyfill, in case browser fires a
+ // native dragend event after the polyfill dragend, we need to ignore
+ // that one
+ if (isNativeDragEvent((nativeEvent))) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
// Initiate server start dragend event when there is a DragEndListener
// attached on the server side
if (hasEventListener(DragSourceState.EVENT_DRAGEND)) {
- String dropEffect = getDropEffect(
- ((NativeEvent) event).getDataTransfer());
+ String dropEffect = getDropEffect(nativeEvent.getDataTransfer());
assert dropEffect != null : "Drop effect should never be null";
@@ -318,6 +336,40 @@ public class DragSourceExtensionConnector extends AbstractExtensionConnector {
return dragSourceWidget.getElement();
}
+ /**
+ * Returns whether the given event is a native (android) drag start/end
+ * event, and not produced by the drag-drop-polyfill.
+ *
+ * @param nativeEvent
+ * the event to test
+ * @return {@code true} if native event, {@code false} if not (polyfill
+ * event)
+ */
+ protected boolean isNativeDragEvent(NativeEvent nativeEvent) {
+ return isTrusted(nativeEvent) || isComposed(nativeEvent);
+ }
+
+ /**
+ * Returns whether the current browser is Android Chrome.
+ *
+ * @return {@code true} if Android Chrome, {@code false} if not
+ *
+ */
+ protected boolean isAndoidChrome() {
+ BrowserInfo browserInfo = BrowserInfo.get();
+ return browserInfo.isAndroid() && browserInfo.isChrome();
+ }
+
+ private native boolean isTrusted(NativeEvent event)
+ /*-{
+ return event.isTrusted;
+ }-*/;
+
+ private native boolean isComposed(NativeEvent event)
+ /*-{
+ return event.isComposed;
+ }-*/;
+
private native void setEffectAllowed(DataTransfer dataTransfer,
String effectAllowed)
/*-{
diff --git a/client/src/main/java/com/vaadin/client/extensions/DropTargetExtensionConnector.java b/client/src/main/java/com/vaadin/client/extensions/DropTargetExtensionConnector.java
index 49592488de..acd6d48575 100644
--- a/client/src/main/java/com/vaadin/client/extensions/DropTargetExtensionConnector.java
+++ b/client/src/main/java/com/vaadin/client/extensions/DropTargetExtensionConnector.java
@@ -80,8 +80,9 @@ public class DropTargetExtensionConnector extends AbstractExtensionConnector {
protected void extend(ServerConnector target) {
dropTargetWidget = ((ComponentConnector) target).getWidget();
- // Do not make elements drop target on touch devices
- if (BrowserInfo.get().isTouchDevice()) {
+ // HTML5 DnD is by default not enabled for mobile devices
+ if (BrowserInfo.get().isTouchDevice() && !getConnection()
+ .getUIConnector().isMobileHTML5DndEnabled()) {
return;
}
diff --git a/client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java b/client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java
index 96e7aa1498..49a7e181b2 100644
--- a/client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java
+++ b/client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java
@@ -69,6 +69,8 @@ import com.vaadin.client.ValueMap;
import com.vaadin.client.annotations.OnStateChange;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
+import com.vaadin.client.extensions.DragSourceExtensionConnector;
+import com.vaadin.client.extensions.DropTargetExtensionConnector;
import com.vaadin.client.ui.AbstractConnector;
import com.vaadin.client.ui.AbstractSingleComponentContainerConnector;
import com.vaadin.client.ui.ClickEventHandler;
@@ -165,6 +167,11 @@ public class UIConnector extends AbstractSingleComponentContainerConnector
Window.Location.reload();
}
+
+ @Override
+ public void initializeMobileHtml5DndPolyfill() {
+ initializeMobileDndPolyfill();
+ }
});
registerRpc(ScrollClientRpc.class, new ScrollClientRpc() {
@Override
@@ -1160,6 +1167,20 @@ public class UIConnector extends AbstractSingleComponentContainerConnector
return activeTheme;
}
+ /**
+ * Returns whether HTML5 DnD extensions {@link DragSourceExtensionConnector}
+ * and {@link DropTargetExtensionConnector} and alike should be enabled for
+ * mobile devices.
+ * <p>
+ * By default, it is disabled.
+ *
+ * @return {@code true} if enabled, {@code false} if not
+ * @since 8.1
+ */
+ public boolean isMobileHTML5DndEnabled() {
+ return getState().enableMobileHTML5DnD;
+ }
+
private static Logger getLogger() {
return Logger.getLogger(UIConnector.class.getName());
}
@@ -1209,4 +1230,10 @@ public class UIConnector extends AbstractSingleComponentContainerConnector
return result;
}
}
+
+ // TODO add configuration to use custom drag start decider
+ private static native void initializeMobileDndPolyfill()
+ /*-{
+ $wnd.DragDropPolyfill.Initialize();
+ }-*/;
}
diff --git a/documentation/advanced/advanced-dragndrop.asciidoc b/documentation/advanced/advanced-dragndrop.asciidoc
index 6390eff201..1e6a3af86e 100644
--- a/documentation/advanced/advanced-dragndrop.asciidoc
+++ b/documentation/advanced/advanced-dragndrop.asciidoc
@@ -17,10 +17,6 @@ the other location is a common way to move, copy, or associate objects. For
example, most operating systems allow dragging and dropping files between
folders or dragging a document on a program to open it. Framework version 8.1 adds support for https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API[HTML5 drag and drop] features. This makes it possible to set components as drag sources that user can drag and drop, or to set them as drop targets to drop things on.
-[NOTE]
-====
-Note that the HTML5 drag and drop feature is not supported on touch devices.
-====
== Drag Source
@@ -145,8 +141,6 @@ TODO Add an example of drop criteria
When dragging data over a drop target and the drag over criteria passes, a style name is applied to indicate that the element accepts drops. This style name is the primary style name with `-drag-center` suffix, e.g. `v-label-drag-center`.
-===
-
////
TODO add back when supported with new API ?
[[advanced.dragndrop.external]]
@@ -166,6 +160,50 @@ compatible browser, such as Mozilla Firefox 3.6 or newer.
////
+[[advanced.dragndrop.mobile]]
+== Mobile Drag And Drop Support
+
+The HTML 5 Drag and Drop API is not yet supported by mobile browsers. To enable HTML5 DnD support on mobile devices, we have included
+an link:https://github.com/timruffles/ios-html5-drag-drop-shim/tree/rewrite:[external Polyfill]. Please note that this Polyfill is under the BSD 2 License.
+
+By default, the mobile DnD support is disabled, but you can enable it any time for a [classname]#UI#. Starting from the request where the support was enabled, all the added [classname]#DragSourceExtension#, [classname]#DropTargetExtension# and their subclasses will also work on mobile devices for that UI. The Polyfill is only loaded when the user is using a touch device.
+
+[source, java]
+----
+public class MyUI extends UI {
+ protected void init(VaadinRequest request) {
+ setMobileHtml5DndEnabled(true);
+ }
+}
+----
+
+[NOTE]
+====
+When disabling the support, you need to also remove all the [classname]#DragSourceExtension#, [classname]#DropTargetExtension# and their subclasses that were added when the mobile DnD support was enabled.
+====
+
+=== CSS Style Rules
+
+The Polyfill allows you to apply custom styling to enhance the user experience on touch devices. It is important to remember that these customizations are only used when the polyfill is loaded, and not possible for desktop DnD operations.
+
+The drag image can be customized using the `dnd-poly-drag-image` class name. You must NOT wrap the class rule with e.g. `.valo`, since that is not applied to the drag image element. The following styling can be used to make the drag image opaque and "snap back" when the user did not drop to a valid dropzone:
+
+[source, css]
+====
+.dnd-poly-drag-image {
+ opacity: .5 !important;
+}
+
+.dnd-poly-drag-image.dnd-poly-snapback {
+ transition-property: transform, -webkit-transform !important;
+ transition-duration: 200ms !important;
+ transition-timing-function: ease-out !important;
+}
+====
+
+More details can be found from the link:https://github.com/timruffles/ios-html5-drag-drop-shim/tree/rewrite:[Polyfill] website.
+
+
[[advanced.dragndrop.grid]]
== Drag and Drop Rows in Grid
diff --git a/server/src/main/java/com/vaadin/server/Page.java b/server/src/main/java/com/vaadin/server/Page.java
index 1cebe42802..385aa0bf9a 100644
--- a/server/src/main/java/com/vaadin/server/Page.java
+++ b/server/src/main/java/com/vaadin/server/Page.java
@@ -20,11 +20,15 @@ import java.io.Serializable;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.EventObject;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
+import com.vaadin.annotations.HtmlImport;
+import com.vaadin.annotations.StyleSheet;
import com.vaadin.event.EventRouter;
import com.vaadin.shared.Registration;
import com.vaadin.shared.ui.BorderStyle;
@@ -33,6 +37,7 @@ import com.vaadin.shared.ui.ui.PageState;
import com.vaadin.shared.ui.ui.UIConstants;
import com.vaadin.shared.ui.ui.UIState;
import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.Dependency;
import com.vaadin.ui.JavaScript;
import com.vaadin.ui.LegacyWindow;
import com.vaadin.ui.Link;
@@ -560,6 +565,8 @@ public class Page implements Serializable {
private String newPushState;
private String newReplaceState;
+ private List<Dependency> pendingDependencies;
+
public Page(UI uI, PageState state) {
this.uI = uI;
this.state = state;
@@ -1426,4 +1433,43 @@ public class Page implements Serializable {
private boolean hasEventRouter() {
return eventRouter != null;
}
+
+ /**
+ * Add a dependency that should be added to the current page.
+ * <p>
+ * These dependencies are always added before the dependencies included by
+ * using the annotations {@link HtmlImport}, {@link JavaScript} and
+ * {@link StyleSheet} during the same request.
+ * <p>
+ * Please note that these dependencies are always sent to the client side
+ * and not filtered out by any {@link DependencyFilter}.
+ *
+ * @param dependency
+ * the dependency to add
+ * @since 8.1
+ */
+ public void addDependency(Dependency dependency) {
+ if (pendingDependencies == null) {
+ pendingDependencies = new ArrayList<>();
+ }
+ pendingDependencies.add(dependency);
+ }
+
+ /**
+ * Returns all pending dependencies.
+ * <p>
+ * For internal use only, calling this method will clear the pending
+ * dependencies.
+ *
+ * @return the pending dependencies to the current page
+ * @since 8.1
+ */
+ public Collection<Dependency> getPendingDependencies() {
+ ArrayList<Dependency> copy = new ArrayList<>();
+ if (pendingDependencies != null) {
+ copy.addAll(pendingDependencies);
+ }
+ pendingDependencies = null;
+ return copy;
+ }
}
diff --git a/server/src/main/java/com/vaadin/server/communication/UidlWriter.java b/server/src/main/java/com/vaadin/server/communication/UidlWriter.java
index 7b5f83000c..7511ccd7ec 100644
--- a/server/src/main/java/com/vaadin/server/communication/UidlWriter.java
+++ b/server/src/main/java/com/vaadin/server/communication/UidlWriter.java
@@ -283,9 +283,10 @@ public class UidlWriter implements Serializable {
}
});
- List<Dependency> dependencies = Dependency
- .findAndFilterDependencies(newConnectorTypes, manager,
- new FilterContext(session));
+ List<Dependency> dependencies = new ArrayList<>();
+ dependencies.addAll(ui.getPage().getPendingDependencies());
+ dependencies.addAll(Dependency.findAndFilterDependencies(
+ newConnectorTypes, manager, new FilterContext(session)));
// Include dependencies in output if there are any
if (!dependencies.isEmpty()) {
diff --git a/server/src/main/java/com/vaadin/ui/UI.java b/server/src/main/java/com/vaadin/ui/UI.java
index 536b2603dd..1928aabe87 100644
--- a/server/src/main/java/com/vaadin/ui/UI.java
+++ b/server/src/main/java/com/vaadin/ui/UI.java
@@ -45,6 +45,7 @@ import com.vaadin.event.UIEvents.PollEvent;
import com.vaadin.event.UIEvents.PollListener;
import com.vaadin.event.UIEvents.PollNotifier;
import com.vaadin.event.dnd.DragSourceExtension;
+import com.vaadin.event.dnd.DropTargetExtension;
import com.vaadin.navigator.Navigator;
import com.vaadin.server.ClientConnector;
import com.vaadin.server.ComponentSizeValidator;
@@ -63,6 +64,7 @@ import com.vaadin.server.VaadinServlet;
import com.vaadin.server.VaadinSession;
import com.vaadin.server.VaadinSession.State;
import com.vaadin.server.communication.PushConnection;
+import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.Connector;
import com.vaadin.shared.EventId;
import com.vaadin.shared.MouseEventDetails;
@@ -71,12 +73,14 @@ import com.vaadin.shared.communication.PushMode;
import com.vaadin.shared.ui.WindowOrderRpc;
import com.vaadin.shared.ui.ui.DebugWindowClientRpc;
import com.vaadin.shared.ui.ui.DebugWindowServerRpc;
+import com.vaadin.shared.ui.ui.PageClientRpc;
import com.vaadin.shared.ui.ui.ScrollClientRpc;
import com.vaadin.shared.ui.ui.UIClientRpc;
import com.vaadin.shared.ui.ui.UIConstants;
import com.vaadin.shared.ui.ui.UIServerRpc;
import com.vaadin.shared.ui.ui.UIState;
import com.vaadin.ui.Component.Focusable;
+import com.vaadin.ui.Dependency.Type;
import com.vaadin.ui.Window.WindowOrderChangeListener;
import com.vaadin.ui.declarative.Design;
import com.vaadin.util.ConnectorHelper;
@@ -658,6 +662,8 @@ public abstract class UI extends AbstractSingleComponentContainer
private String embedId;
+ private boolean mobileHtml5DndPolyfillLoaded;
+
/**
* This method is used by Component.Focusable objects to request focus to
* themselves. Focus renders must be handled at window level (instead of
@@ -1834,6 +1840,74 @@ public abstract class UI extends AbstractSingleComponentContainer
}
/**
+ * Returns whether HTML5 DnD extensions {@link DragSourceExtension} and
+ * {@link DropTargetExtension} and alike should be enabled for mobile
+ * devices.
+ * <p>
+ * By default, it is disabled.
+ *
+ * @return {@code true} if enabled, {@code false} if not
+ * @since 8.1
+ * @see #setMobileHtml5DndEnabled(boolean)
+ */
+ public boolean isMobileHtml5DndEnabled() {
+ return getState(false).enableMobileHTML5DnD;
+ }
+
+ /**
+ * Enable or disable HTML5 DnD for mobile devices.
+ * <p>
+ * Usually you should enable the support in the {@link #init(VaadinRequest)}
+ * method. By default, it is disabled. This operation is NOOP when the user
+ * is not on a mobile device.
+ * <p>
+ * Changing this will effect all {@link DragSourceExtension} and
+ * {@link DropTargetExtension} (and subclasses) that have not yet been
+ * attached to the UI on the client side.
+ * <p>
+ * <em>NOTE: When disabling this after it has been enabled, it will not
+ * affect {@link DragSourceExtension} and {@link DropTargetExtension} (and
+ * subclasses) that have been previously added. Those extensions should be
+ * explicitly removed to make sure user cannot perform DnD operations
+ * anymore.</em>
+ *
+ * @param enabled
+ * {@code true} if enabled, {@code false} if not
+ * @since 8.1
+ */
+ public void setMobileHtml5DndEnabled(boolean enabled) {
+ if (getState(false).enableMobileHTML5DnD != enabled) {
+ getState().enableMobileHTML5DnD = enabled;
+
+ if (isMobileHtml5DndEnabled()) {
+ loadMobileHtml5DndPolyfill();
+ }
+ }
+ }
+
+ /**
+ * Load and initialize the mobile drag-drop-polyfill if needed and not yet
+ * done so.
+ */
+ private void loadMobileHtml5DndPolyfill() {
+ if (mobileHtml5DndPolyfillLoaded) {
+ return;
+ }
+ if (!getPage().getWebBrowser().isTouchDevice()) {
+ return;
+ }
+ mobileHtml5DndPolyfillLoaded = true;
+
+ String vaadinLocation = getSession().getService().getStaticFileLocation(
+ VaadinService.getCurrentRequest()) + "/VAADIN/";
+
+ getPage().addDependency(new Dependency(Type.JAVASCRIPT,
+ vaadinLocation + ApplicationConstants.MOBILE_DND_POLYFILL_JS));
+
+ getRpcProxy(PageClientRpc.class).initializeMobileHtml5DndPolyfill();
+ }
+
+ /**
* Event which is fired when the ordering of the windows is updated.
* <p>
* The other way to listen window position for specific window is
diff --git a/server/src/main/resources/VAADIN/drag-drop-polyfill.min.js b/server/src/main/resources/VAADIN/drag-drop-polyfill.min.js
new file mode 100644
index 0000000000..00d8ba0a98
--- /dev/null
+++ b/server/src/main/resources/VAADIN/drag-drop-polyfill.min.js
@@ -0,0 +1,4 @@
+/*! drag-drop-polyfill 2.0.0-beta.2 | Copyright (c) 2016 Tim Ruffles | BSD 2 License */
+/* Forked from original with this change: https://github.com/timruffles/ios-html5-drag-drop-shim/pull/99 */
+var DEBUG,DragDropPolyfill;!function(a){function b(){var a={dragEvents:"ondragstart"in document.documentElement,draggable:"draggable"in document.documentElement,touchEvents:"ontouchstart"in document.documentElement,userAgentSupportingNativeDnD:void 0},b=!!window.chrome||/chrome/i.test(navigator.userAgent);return a.userAgentSupportingNativeDnD=!(/iPad|iPhone|iPod|Android/.test(navigator.userAgent)||b&&a.touchEvents),DEBUG&&Object.keys(a).forEach(function(a){}),a}function c(a){if(a&&Object.keys(a).forEach(function(b){t[b]=a[b]}),!t.forceApply){var c=b();if(c.userAgentSupportingNativeDnD&&c.draggable&&c.dragEvents)return}document.addEventListener("touchstart",d)}function d(a){if(!u){var b=e(a);if(b)try{u=new C(a,t,b,f)}catch(c){throw f(t,a,3),c}}}function e(a){var b=a.target;do if(b.draggable!==!1&&b.getAttribute&&"true"===b.getAttribute("draggable"))return b;while((b=b.parentNode)&&b!==document.body)}function f(a,b,c){if(0===c&&a.defaultActionOverride)try{a.defaultActionOverride(b),b.defaultPrevented}catch(d){}u=null}function g(a){return 0===a.length?0:a.reduce(function(a,b){return b+a},0)/a.length}function h(a){return a&&a.tagName}function i(a,b){for(var c=0;c<a.changedTouches.length;c++){var d=a.changedTouches[c];if(d.identifier===b)return!0}return!1}function j(a,b,c,d,e,f,g){void 0===g&&(g=null);var h=b.changedTouches[0],i=new Event(c,{bubbles:!0,cancelable:d});i.dataTransfer=f,i.relatedTarget=g,i.screenX=h.screenX,i.screenY=h.screenY,i.clientX=h.clientX,i.clientY=h.clientY,i.pageX=h.pageX,i.pageY=h.pageY;var j=a.getBoundingClientRect();return i.offsetX=i.clientX-j.left,i.offsetY=i.clientY-j.top,i}function k(a,b,c){for(var d=[],e=[],f=0;f<b.touches.length;f++){var h=b.touches[f];d.push(h[a+"X"]),e.push(h[a+"Y"])}c.x=g(d),c.y=g(e)}function l(a,b){if(1===a.nodeType){for(var c=getComputedStyle(a),d=0;d<c.length;d++){var e=c[d];b.style.setProperty(e,c.getPropertyValue(e),c.getPropertyPriority(e))}b.style.pointerEvents="none",b.removeAttribute("id"),b.removeAttribute("class"),b.removeAttribute("draggable")}if(a.hasChildNodes())for(var d=0;d<a.childNodes.length;d++)l(a.childNodes[d],b.childNodes[d])}function m(a){var b=a.cloneNode(!0);return l(a,b),b.style.position="absolute",b.style.left="0px",b.style.top="0px",b.style.zIndex="999999",b.classList.add(z),b.classList.add(B),b}function n(a){return x.map(function(b){var c=a.style[b+"transform"];return c&&"none"!==c?c.replace(/translate\(\D*\d+[^,]*,\D*\d+[^,]*\)\s*/g,""):""})}function o(a,b,c,d,e){void 0===e&&(e=!0);var f=b.x,g=b.y;d&&(f+=d.x,g+=d.y),e&&(f-=parseInt(a.offsetWidth,10)/2,g-=parseInt(a.offsetHeight,10)/2);for(var h="translate3d("+f+"px,"+g+"px, 0)",i=0;i<x.length;i++){var j=x[i]+"transform";a.style[j]=h+" "+c[i]}}function p(a,b,c,d){var e=getComputedStyle(a);if("hidden"===e.visibility||"none"===e.display)return void d();var f=a.getBoundingClientRect(),g={x:f.left,y:f.top};g.x+=document.body.scrollLeft||document.documentElement.scrollLeft,g.y+=document.body.scrollTop||document.documentElement.scrollTop,g.x-=parseInt(e.marginLeft,10),g.y-=parseInt(e.marginTop,10),b.classList.add(A);var h=getComputedStyle(b);var i=parseFloat(h.transitionDuration);if(i===NaN ||i===0)return void d();var j=parseFloat(h.transitionDelay),k=Math.round(1e3*(i+j));o(b,g,c,void 0,!1),setTimeout(d,k)}function q(a,b){return a?a===v[0]?w[0]:0===a.indexOf(v[1])||a===v[7]?w[1]:0===a.indexOf(v[4])?w[3]:a===v[6]?w[2]:w[1]:3===b.nodeType&&"A"===b.tagName?w[3]:w[1]}function r(a,b,c,d,e,f,g){if(void 0===f&&(f=!0),void 0===g&&(g=null),DEBUG){var h=y+"debug",i=y+"event-target",k=y+"event-related-target";b.classList.add(h),b.classList.add(i),g&&(g.classList.add(h),g.classList.add(k))}var l=j(b,c,a,f,document.defaultView,e,g),m=!b.dispatchEvent(l);return d.g=0,DEBUG&&(b.classList.remove(i),g&&g.classList.remove(k)),m}function s(a,b){if(!a||a===v[7])return b;if(b===w[1]){if(0===a.indexOf(w[1]))return w[1]}else if(b===w[3]){if(0===a.indexOf(w[3])||a.indexOf("Link")>-1)return w[3]}else if(b===w[2]&&(0===a.indexOf(w[2])||a.indexOf("Move")>-1))return w[2];return w[0]}var t={iterationInterval:150};a.Initialize=c;var u,v=["none","copy","copyLink","copyMove","link","linkMove","move","all"],w=["none","copy","move","link"],x=["","-webkit-"],y="dnd-poly-",z=y+"drag-image",A=y+"snapback",B=y+"icon",C=function(){function a(a,b,c,d){this.h=a,this.i=b,this.j=c,this.k=d,this.l=0,this.m=null,this.o=null,this.p=a,this.q=a.changedTouches[0],this.s=this.t.bind(this),this.u=this.v.bind(this),document.addEventListener("touchmove",this.s),document.addEventListener("touchend",this.u),document.addEventListener("touchcancel",this.u)}return a.prototype.A=function(){var a=this;this.l=1,this.B=w[0],this.C={D:{},F:void 0,g:3,G:[]},this.H={x:null,y:null},this.I={x:null,y:null};var b=this.j;if(this.J=new D(this.C,function(c,d,e){b=c,"number"!=typeof d&&"number"!=typeof e||(a.K={x:d||0,y:e||0})}),this.C.g=2,this.J.dropEffect=w[0],r("dragstart",this.j,this.p,this.C,this.J))return this.l=3,this.L(),!1;if(k("page",this.p,this.I),this.M=m(b),this.N=n(this.M),!this.K)if(this.i.dragImageOffset)this.K={x:this.i.dragImageOffset.x,y:this.i.dragImageOffset.y};else if(this.i.dragImageCenterOnTouch)this.K={x:0,y:0};else{var c=b.getBoundingClientRect(),d=getComputedStyle(b);this.K={x:c.left-this.q.clientX-parseInt(d.marginLeft,10),y:c.top-this.q.clientY-parseInt(d.marginTop,10)}}return o(this.M,this.I,this.N,this.K,this.i.dragImageCenterOnTouch),document.body.appendChild(this.M),this.O=setInterval(function(){a.P||(a.P=!0,a.R(),a.P=!1)},this.i.iterationInterval),!0},a.prototype.L=function(){this.O&&(clearInterval(this.O),this.O=null),document.removeEventListener("touchmove",this.s),document.removeEventListener("touchend",this.u),document.removeEventListener("touchcancel",this.u),this.M&&(this.M.parentNode.removeChild(this.M),this.M=null),this.k(this.i,this.p,this.l)},a.prototype.t=function(a){var b=this;if(i(a,this.q.identifier)!==!1){if(this.p=a,0===this.l){var c=void 0;if(this.i.dragStartConditionOverride)try{c=this.i.dragStartConditionOverride(a)}catch(d){c=!1}else c=1===a.touches.length;return c?void(this.A()===!0&&(this.h.preventDefault(),a.preventDefault())):void this.L()}if(a.preventDefault(),k("client",a,this.H),k("page",a,this.I),this.i.dragImageTranslateOverride)try{var e=!1;if(this.i.dragImageTranslateOverride(a,{x:this.H.x,y:this.H.y},this.m,function(a,c){b.M&&(e=!0,b.H.x+=a,b.H.y+=c,b.I.x+=a,b.I.y+=c,o(b.M,b.I,b.N,b.K,b.i.dragImageCenterOnTouch))}),e)return}catch(d){}o(this.M,this.I,this.N,this.K,this.i.dragImageCenterOnTouch)}},a.prototype.v=function(a){if(i(a,this.q.identifier)!==!1){if(this.i.dragImageTranslateOverride)try{this.i.dragImageTranslateOverride(void 0,void 0,void 0,function(){})}catch(b){}if(0===this.l)return void this.L();a.preventDefault(),this.l="touchcancel"===a.type?3:2}},a.prototype.R=function(){var a=this;if(DEBUG)var b=y+"debug",c=y+"immediate-user-selection",d=y+"current-drop-target";var e=this.B;this.C.g=3,this.J.dropEffect=w[0];var f=r("drag",this.j,this.p,this.C,this.J);if(f&&(this.B=w[0]),f||2===this.l||3===this.l){var g=this.S(this.l);return g?void p(this.j,this.M,this.N,function(){a.T()}):void this.T()}var i=document.elementFromPoint(this.H.x,this.H.y),j=this.o;i!==this.m&&i!==this.o&&(DEBUG&&(this.m&&this.m.classList.remove(c),i&&(i.classList.add(b),i.classList.add(c))),this.m=i,null!==this.o&&(this.C.g=3,this.J.dropEffect=w[0],r("dragexit",this.o,this.p,this.C,this.J,!1)),null===this.m?this.o=this.m:(this.C.g=3,this.J.dropEffect=q(this.C.F,this.j),r("dragenter",this.m,this.p,this.C,this.J)?(this.o=this.m,this.B=s(this.J.effectAllowed,this.J.dropEffect)):this.m!==document.body&&(this.o=document.body))),j!==this.o&&h(j)&&(DEBUG&&j.classList.remove(d),this.C.g=3,this.J.dropEffect=w[0],r("dragleave",j,this.p,this.C,this.J,!1,this.o)),h(this.o)&&(DEBUG&&(this.o.classList.add(b),this.o.classList.add(d)),this.C.g=3,this.J.dropEffect=q(this.C.F,this.j),r("dragover",this.o,this.p,this.C,this.J)===!1?this.B=w[0]:this.B=s(this.J.effectAllowed,this.J.dropEffect)),e!==this.B&&this.M.classList.remove(y+e);var k=y+this.B;this.M.classList.contains(k)===!1&&this.M.classList.add(k)},a.prototype.S=function(a){if(DEBUG){var b=y+"immediate-user-selection",c=y+"current-drop-target";this.o&&this.o.classList.remove(c),this.m&&this.m.classList.remove(b)}var d=this.B===w[0]||null===this.o||3===a;return d?h(this.o)&&(this.C.g=3,this.J.dropEffect=w[0],r("dragleave",this.o,this.p,this.C,this.J,!1)):h(this.o)&&(this.C.g=1,this.J.dropEffect=this.B,r("drop",this.o,this.p,this.C,this.J)===!0?this.B=this.J.dropEffect:this.B=w[0]),d},a.prototype.T=function(){this.C.g=3,this.J.dropEffect=this.B,r("dragend",this.j,this.p,this.C,this.J,!1),this.l=2,this.L()},a}(),D=function(){function a(a,b){this.U=a,this.V=b,this.W=w[0]}return Object.defineProperty(a.prototype,"types",{get:function(){if(0!==this.U.g)return Object.freeze(this.U.G)},enumerable:!0,configurable:!0}),a.prototype.setData=function(a,b){if(2===this.U.g){if(a.indexOf(" ")>-1)throw new Error("illegal arg: type contains space");this.U.D[a]=b,this.U.G.indexOf(a)===-1&&this.U.G.push(a)}},a.prototype.getData=function(a){if(1===this.U.g||2===this.U.g)return this.U.D[a]||""},a.prototype.clearData=function(a){if(2===this.U.g){if(a&&this.U.D[a]){delete this.U.D[a];var b=this.U.G.indexOf(a);return void(b>-1&&this.U.G.splice(b,1))}this.U.D={},this.U.G=[]}},a.prototype.setDragImage=function(a,b,c){2===this.U.g&&this.V(a,b,c)},Object.defineProperty(a.prototype,"effectAllowed",{get:function(){return this.U.F},set:function(a){2===this.U.g&&v.indexOf(a)>-1&&(this.U.F=a)},enumerable:!0,configurable:!0}),Object.defineProperty(a.prototype,"dropEffect",{get:function(){return this.W},set:function(a){0!==this.U.g&&v.indexOf(a)>-1&&(this.W=a)},enumerable:!0,configurable:!0}),a}()}(DragDropPolyfill||(DragDropPolyfill={}));
+//# sourceMappingURL=drag-drop-polyfill.min.js.map
diff --git a/shared/src/main/java/com/vaadin/shared/ApplicationConstants.java b/shared/src/main/java/com/vaadin/shared/ApplicationConstants.java
index bb6ec330f7..b3490a434c 100644
--- a/shared/src/main/java/com/vaadin/shared/ApplicationConstants.java
+++ b/shared/src/main/java/com/vaadin/shared/ApplicationConstants.java
@@ -150,7 +150,7 @@ public class ApplicationConstants implements Serializable {
/**
* Name of the parameter used to transmit the push connection identifier.
- *
+ *
* @since 8.0.6
*/
public static final String PUSH_ID_PARAMETER = "v-pushId";
@@ -244,4 +244,11 @@ public class ApplicationConstants implements Serializable {
public static final String FRONTEND_URL_ES5_DEFAULT_VALUE = VAADIN_PROTOCOL_PREFIX
+ "frontend/es5/";
+ /**
+ * Name of the HTML5 DnD Polyfill JavaScript file.
+ *
+ * @since 8.1
+ */
+ public static final String MOBILE_DND_POLYFILL_JS = "drag-drop-polyfill.min.js";
+
}
diff --git a/shared/src/main/java/com/vaadin/shared/ui/ui/PageClientRpc.java b/shared/src/main/java/com/vaadin/shared/ui/ui/PageClientRpc.java
index acbc41b2c8..15419831e5 100644
--- a/shared/src/main/java/com/vaadin/shared/ui/ui/PageClientRpc.java
+++ b/shared/src/main/java/com/vaadin/shared/ui/ui/PageClientRpc.java
@@ -18,8 +18,21 @@ package com.vaadin.shared.ui.ui;
import com.vaadin.shared.communication.ClientRpc;
+/**
+ * Client side rpc class for Page / UI.
+ *
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
public interface PageClientRpc extends ClientRpc {
+ /**
+ * Reloads the page.
+ */
public void reload();
+ /**
+ * Initialize Mobile HTML5 Drag and Drop Polyfill.
+ */
+ public void initializeMobileHtml5DndPolyfill();
}
diff --git a/shared/src/main/java/com/vaadin/shared/ui/ui/UIState.java b/shared/src/main/java/com/vaadin/shared/ui/ui/UIState.java
index c2c7b5c27a..b1d6be2def 100644
--- a/shared/src/main/java/com/vaadin/shared/ui/ui/UIState.java
+++ b/shared/src/main/java/com/vaadin/shared/ui/ui/UIState.java
@@ -86,6 +86,11 @@ public class UIState extends AbstractSingleComponentContainerState {
tabIndex = 1;
}
+ /**
+ * Enable Mobile HTML5 DnD support.
+ */
+ public boolean enableMobileHTML5DnD = false;
+
public static class LoadingIndicatorConfigurationState
implements Serializable {
public int firstDelay = 300;
diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridDragAndDrop.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridDragAndDrop.java
index 529d8cf562..059c963658 100644
--- a/uitest/src/main/java/com/vaadin/tests/components/grid/GridDragAndDrop.java
+++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridDragAndDrop.java
@@ -51,6 +51,7 @@ public class GridDragAndDrop extends AbstractTestUIWithLog {
@Override
protected void setup(VaadinRequest request) {
+ getUI().setMobileHtml5DndEnabled(true);
// Drag source Grid
Grid<Person> left = createGridAndFillWithData(50);
@@ -117,18 +118,22 @@ public class GridDragAndDrop extends AbstractTestUIWithLog {
});
// Add drag start listener
- dragSource.addGridDragStartListener(
- event -> draggedItems = event.getDraggedItems());
+ dragSource.addGridDragStartListener(event -> {
+ draggedItems = event.getDraggedItems();
+ log("START: " + draggedItems.size() + ", :"
+ + draggedItems.stream().map(person -> person.getLastName())
+ .collect(Collectors.joining(" ")));
+ });
// Add drag end listener
dragSource.addGridDragEndListener(event -> {
+ log("END: dropEffect=" + event.getDropEffect());
if (event.getDropEffect() == DropEffect.MOVE
&& draggedItems != null) {
// If drop is successful, remove dragged item from source Grid
((ListDataProvider<Person>) grid.getDataProvider()).getItems()
.removeAll(draggedItems);
grid.getDataProvider().refreshAll();
-
// Remove reference to dragged items
draggedItems = null;
}
@@ -160,7 +165,8 @@ public class GridDragAndDrop extends AbstractTestUIWithLog {
items.addAll(index, draggedItems);
dataProvider.refreshAll();
- log("dragData=" + event.getDataTransferText() + ", target="
+ log("DROP: dragData=" + event.getDataTransferText()
+ + ", target="
+ event.getDropTargetRow().getFirstName() + " "
+ event.getDropTargetRow().getLastName()
+ ", location=" + event.getDropLocation());
diff --git a/uitest/src/main/java/com/vaadin/tests/dnd/DragAndDropCardShuffle.java b/uitest/src/main/java/com/vaadin/tests/dnd/DragAndDropCardShuffle.java
index 8a136304aa..8f72fe225e 100644
--- a/uitest/src/main/java/com/vaadin/tests/dnd/DragAndDropCardShuffle.java
+++ b/uitest/src/main/java/com/vaadin/tests/dnd/DragAndDropCardShuffle.java
@@ -27,6 +27,7 @@ import com.vaadin.server.VaadinRequest;
import com.vaadin.shared.ui.dnd.DropEffect;
import com.vaadin.shared.ui.dnd.EffectAllowed;
import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.ui.CheckBox;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.NativeSelect;
@@ -42,7 +43,7 @@ public class DragAndDropCardShuffle extends AbstractTestUIWithLog {
private final Label king = new Label("K");
// Create desk
- private final HorizontalLayout desk = new HorizontalLayout();
+ private HorizontalLayout desk = new HorizontalLayout();
private final List<DragSourceExtension<Label>> sources = new ArrayList<>();
private final List<DropTargetExtension<Label>> targets = new ArrayList<>();
@@ -62,10 +63,28 @@ public class DragAndDropCardShuffle extends AbstractTestUIWithLog {
dropEffect.setItems(DropEffect.values());
dropEffect.addValueChangeListener(event -> targets
.forEach(target -> target.setDropEffect(event.getValue())));
+ CheckBox enableMobileSupport = new CheckBox("Mobile Support", false);
+ enableMobileSupport.addValueChangeListener(event -> {
+ setMobileHtml5DndEnabled(event.getValue());
+
+ removeExtensions();
+ setupExtensions();
+ });
+
+ setupExtensions();
- // Create UI and add extensions
desk.addComponents(ace, jack, queen, king);
+ addComponents(new HorizontalLayout(effectAllowed, dropEffect,
+ enableMobileSupport), desk);
+
+ // Add styling
+ setStyle();
+ }
+
+ private void setupExtensions() {
+ // Create UI and add extensions
+
ace.setStyleName("card");
addDragSourceExtension(ace);
addDropTargetExtension(ace);
@@ -81,11 +100,20 @@ public class DragAndDropCardShuffle extends AbstractTestUIWithLog {
king.setStyleName("card");
addDragSourceExtension(king);
addDropTargetExtension(king);
+ }
- addComponents(new HorizontalLayout(effectAllowed, dropEffect), desk);
+ private void removeExtensions() {
+ ace.removeExtension(ace.getExtensions().iterator().next());
+ ace.removeExtension(ace.getExtensions().iterator().next());
- // Add styling
- setStyle();
+ jack.removeExtension(jack.getExtensions().iterator().next());
+ jack.removeExtension(jack.getExtensions().iterator().next());
+
+ queen.removeExtension(queen.getExtensions().iterator().next());
+ queen.removeExtension(queen.getExtensions().iterator().next());
+
+ king.removeExtension(king.getExtensions().iterator().next());
+ king.removeExtension(king.getExtensions().iterator().next());
}
private void addDragSourceExtension(Label source) {