/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.client.extensions; import com.google.gwt.animation.client.AnimationScheduler; import com.google.gwt.dom.client.DataTransfer; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Position; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; import com.vaadin.client.ComponentConnector; import com.vaadin.client.ServerConnector; import com.vaadin.client.annotations.OnStateChange; import com.vaadin.client.ui.AbstractComponentConnector; import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.dnd.DragSourceRpc; import com.vaadin.shared.ui.dnd.DragSourceState; import com.vaadin.shared.ui.dnd.DropEffect; import com.vaadin.ui.dnd.DragSourceExtension; import elemental.events.Event; import elemental.events.EventListener; import elemental.events.EventTarget; /** * Extension to add drag source functionality to a widget for using HTML5 drag * and drop. Client side counterpart of {@link DragSourceExtension}. * * @author Vaadin Ltd * @since 8.1 */ @Connect(DragSourceExtension.class) public class DragSourceExtensionConnector extends AbstractExtensionConnector { /** * Style suffix for indicating that the element is a drag source. */ protected static final String STYLE_SUFFIX_DRAGSOURCE = "-dragsource"; private static final String STYLE_NAME_DRAGGABLE = "v-draggable"; // Create event listeners private final EventListener dragStartListener = this::onDragStart; private final EventListener dragEndListener = this::onDragEnd; /** * Widget of the drag source component. */ private Widget dragSourceWidget; @Override protected void extend(ServerConnector target) { dragSourceWidget = ((ComponentConnector) target).getWidget(); // HTML5 DnD is by default not enabled for mobile devices if (BrowserInfo.get().isTouchDevice() && !getConnection() .getUIConnector().isMobileHTML5DndEnabled()) { return; } addDraggable(getDraggableElement()); addDragListeners(getDraggableElement()); ((AbstractComponentConnector) target).onDragSourceAttached(); } /** * Makes the given element draggable and adds class name. * * @param element * Element to be set draggable. */ protected void addDraggable(Element element) { element.setDraggable(Element.DRAGGABLE_TRUE); element.addClassName( getStylePrimaryName(element) + STYLE_SUFFIX_DRAGSOURCE); element.addClassName(STYLE_NAME_DRAGGABLE); } /** * Removes draggable and class name from the given element. * * @param element * Element to remove draggable from. */ protected void removeDraggable(Element element) { element.setDraggable(Element.DRAGGABLE_FALSE); element.removeClassName( getStylePrimaryName(element) + STYLE_SUFFIX_DRAGSOURCE); element.removeClassName(STYLE_NAME_DRAGGABLE); } /** * Adds dragstart and dragend event listeners to the given DOM element. * * @param element * DOM element to attach event listeners to. */ protected void addDragListeners(Element element) { EventTarget target = element.cast(); target.addEventListener(Event.DRAGSTART, dragStartListener); target.addEventListener(Event.DRAGEND, dragEndListener); } /** * Removes dragstart and dragend event listeners from the given DOM element. * * @param element * DOM element to remove event listeners from. */ protected void removeDragListeners(Element element) { EventTarget target = element.cast(); target.removeEventListener(Event.DRAGSTART, dragStartListener); target.removeEventListener(Event.DRAGEND, dragEndListener); } @Override public void onUnregister() { super.onUnregister(); Element dragSource = getDraggableElement(); removeDraggable(dragSource); removeDragListeners(dragSource); ((AbstractComponentConnector) getParent()).onDragSourceDetached(); } @OnStateChange("resources") private void prefetchDragImage() { String dragImageUrl = getResourceUrl( DragSourceState.RESOURCE_DRAG_IMAGE); if (dragImageUrl != null && !dragImageUrl.isEmpty()) { Image.prefetch(getConnection().translateVaadinUri(dragImageUrl)); } } /** * Event handler for the {@code dragstart} event. Called when {@code * dragstart} event occurs. * * @param event * browser event to be handled */ protected void onDragStart(Event event) { // 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(), getState().effectAllowed.getValue()); } // Set drag image setDragImage(nativeEvent); // Set text data parameter String dataTransferText = createDataTransferText(nativeEvent); // Always set something as the text data, or DnD won't work in FF ! if (dataTransferText == null) { dataTransferText = ""; } nativeEvent.getDataTransfer().setData(DragSourceState.DATA_TYPE_TEXT, dataTransferText); // Initiate firing server side dragstart event when there is a // DragStartListener attached on the server side if (hasEventListener(DragSourceState.EVENT_DRAGSTART)) { sendDragStartEventToServer(nativeEvent); } // Stop event bubbling nativeEvent.stopPropagation(); } /** * Fixes missing drag image for Safari by making the dragged element * position to relative if needed. Safari won't show drag image unless the * dragged element position is relative or absolute / fixed, but not with * display block for the latter. * <p> * This method is a NOOP for non-safari browser. * <p> * This fix is not needed if a custom drag image is used on Safari. * * @param draggedElement * the element that forms the drag image */ protected void fixDragImageForSafari(Element draggedElement) { if (!BrowserInfo.get().isSafari()) { return; } final Style style = draggedElement.getStyle(); final String position = style.getPosition(); // relative works always if ("relative".equalsIgnoreCase(position)) { return; } // absolute & fixed don't work when there is offset used if ("absolute".equalsIgnoreCase(position) || "fixed".equalsIgnoreCase(position)) { // FIXME #9261 need to figure out how to get absolute and fixed to // position work when there is offset involved, like in Grid. // The following hack with setting position to relative did not // work, nor did clearing top/right/bottom/left. } // for all other positions, set the position to relative and revert it // in an animation frame draggedElement.getStyle().setPosition(Position.RELATIVE); AnimationScheduler.get().requestAnimationFrame(timestamp -> { draggedElement.getStyle().setProperty("position", position); }, draggedElement); } /** * Creates data of type {@code "text"} for the {@code DataTransfer} object * of the given event. * * @param dragStartEvent * Event to set the data for. * @return Textual data to be set for the event or {@literal null}. */ protected String createDataTransferText(NativeEvent dragStartEvent) { return getState().dataTransferText; } /** * Initiates a server RPC for the drag start event. * <p> * This method is called only if there is a server side drag start event * handler attached. * * @param dragStartEvent * Client side dragstart event. */ protected void sendDragStartEventToServer(NativeEvent dragStartEvent) { getRpcProxy(DragSourceRpc.class).dragStart(); } /** * Sets the drag image to be displayed. * <p> * Override this method in case you need custom drag image setting. Called * from {@link #onDragStart(Event)}. * * @param dragStartEvent * The drag start event. */ protected void setDragImage(NativeEvent dragStartEvent) { String imageUrl = getResourceUrl(DragSourceState.RESOURCE_DRAG_IMAGE); if (imageUrl != null && !imageUrl.isEmpty()) { Image dragImage = new Image( getConnection().translateVaadinUri(imageUrl)); dragStartEvent.getDataTransfer() .setDragImage(dragImage.getElement(), 0, 0); } else { fixDragImageForSafari( (Element) dragStartEvent.getCurrentEventTarget().cast()); } } /** * Event handler for the {@code dragend} event. Called when {@code dragend} * event occurs. * * @param event * 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 (isAndoidChrome() && 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.getDataTransfer()); assert dropEffect != null : "Drop effect should never be null"; sendDragEndEventToServer(nativeEvent, DropEffect.valueOf(dropEffect.toUpperCase())); } } /** * Initiates a server RPC for the drag end event. * * @param dragEndEvent * Client side dragend event. * @param dropEffect * Drop effect of the dragend event, extracted from {@code * DataTransfer.dropEffect} parameter. */ protected void sendDragEndEventToServer(NativeEvent dragEndEvent, DropEffect dropEffect) { getRpcProxy(DragSourceRpc.class).dragEnd(dropEffect); } /** * Finds the draggable element within the widget. By default, returns the * topmost element. * <p> * Override this method to make some other than the root element draggable * instead. * <p> * In case you need to make more than whan element draggable, override * {@link #extend(ServerConnector)} instead. * * @return the draggable element in the parent widget. */ protected Element getDraggableElement() { 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) /*-{ dataTransfer.effectAllowed = effectAllowed; }-*/; /** * Returns the dropEffect for the given data transfer. * * @param dataTransfer * the data transfer with drop effect * @return the currently set drop effect */ protected static native String getDropEffect(DataTransfer dataTransfer) /*-{ return dataTransfer.dropEffect; }-*/; @Override public DragSourceState getState() { return (DragSourceState) super.getState(); } private native boolean getStylePrimaryName(Element element) /*-{ return @com.google.gwt.user.client.ui.UIObject::getStylePrimaryName(Lcom/google/gwt/dom/client/Element;)(element); }-*/; }