/*
* 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.dom.client.DataTransfer;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
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.ui.AbstractComponentConnector;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.dnd.DragSourceState;
import com.vaadin.shared.ui.dnd.DropEffect;
import com.vaadin.shared.ui.dnd.DropTargetRpc;
import com.vaadin.shared.ui.dnd.DropTargetState;
import com.vaadin.ui.dnd.DropTargetExtension;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.EventTarget;
/**
* Extension to add drop target functionality to a widget for using HTML5 drag
* and drop. Client side counterpart of {@link DropTargetExtension}.
*
* @author Vaadin Ltd
* @since 8.1
*/
@Connect(DropTargetExtension.class)
public class DropTargetExtensionConnector extends AbstractExtensionConnector {
/**
* Style name suffix for dragging data over the center of the drop target.
*/
protected static final String STYLE_SUFFIX_DRAG_CENTER = "-drag-center";
/**
* Style name suffix for dragging data over the top part of the drop target.
*/
protected static final String STYLE_SUFFIX_DRAG_TOP = "-drag-top";
/**
* Style name suffix for dragging data over the bottom part of the drop
* target.
*/
protected static final String STYLE_SUFFIX_DRAG_BOTTOM = "-drag-bottom";
// Create event listeners
private final EventListener dragEnterListener = this::onDragEnter;
private final EventListener dragOverListener = this::onDragOver;
private final EventListener dragLeaveListener = this::onDragLeave;
private final EventListener dropListener = this::onDrop;
/**
* Widget of the drop target component.
*/
private Widget dropTargetWidget;
/**
* Class name to apply when an element is dragged over the center of the
* target.
*/
private String styleDragCenter;
@Override
protected void extend(ServerConnector target) {
dropTargetWidget = ((ComponentConnector) target).getWidget();
// HTML5 DnD is by default not enabled for mobile devices
if (BrowserInfo.get().isTouchDevice() && !getConnection()
.getUIConnector().isMobileHTML5DndEnabled()) {
return;
}
addDropListeners(getDropTargetElement());
((AbstractComponentConnector) target).onDropTargetAttached();
}
/**
* Adds dragenter, dragover, dragleave and drop event listeners to the given
* DOM element.
*
* @param element
* DOM element to attach event listeners to.
*/
private void addDropListeners(Element element) {
EventTarget target = element.cast();
target.addEventListener(Event.DRAGENTER, dragEnterListener);
target.addEventListener(Event.DRAGOVER, dragOverListener);
target.addEventListener(Event.DRAGLEAVE, dragLeaveListener);
target.addEventListener(Event.DROP, dropListener);
}
/**
* Removes dragenter, dragover, dragleave and drop event listeners from the
* given DOM element.
*
* @param element
* DOM element to remove event listeners from.
*/
private void removeDropListeners(Element element) {
EventTarget target = element.cast();
target.removeEventListener(Event.DRAGENTER, dragEnterListener);
target.removeEventListener(Event.DRAGOVER, dragOverListener);
target.removeEventListener(Event.DRAGLEAVE, dragLeaveListener);
target.removeEventListener(Event.DROP, dropListener);
}
@Override
public void onUnregister() {
super.onUnregister();
removeDropListeners(getDropTargetElement());
((AbstractComponentConnector) getParent()).onDropTargetDetached();
}
/**
* Finds the drop target element within the widget. By default, returns the
* topmost element.
*
* @return the drop target element in the parent widget.
*/
protected Element getDropTargetElement() {
return dropTargetWidget.getElement();
}
/**
* Event handler for the {@code dragenter} event.
* <p>
* Override this method in case custom handling for the dragstart event is
* required. If the drop is allowed, the event should prevent default.
*
* @param event
* browser event to be handled
*/
protected void onDragEnter(Event event) {
NativeEvent nativeEvent = (NativeEvent) event;
// Generate style name for drop target
styleDragCenter = dropTargetWidget.getStylePrimaryName()
+ STYLE_SUFFIX_DRAG_CENTER;
if (isDropAllowed(nativeEvent)) {
addTargetClassIndicator(nativeEvent);
setDropEffect(nativeEvent);
// According to spec, need to call this for allowing dropping, the
// default action would be to reject as target
event.preventDefault();
} else {
// Remove drop effect
nativeEvent.getDataTransfer()
.setDropEffect(DataTransfer.DropEffect.NONE);
}
}
/**
* Set the drop effect for the dragenter / dragover event, if one has been
* set from server side.
* <p>
* From Moz Foundation: "You can modify the dropEffect property during the
* dragenter or dragover events, if for example, a particular drop target
* only supports certain operations. You can modify the dropEffect property
* to override the user effect, and enforce a specific drop operation to
* occur. Note that this effect must be one listed within the effectAllowed
* property. Otherwise, it will be set to an alternate value that is
* allowed."
*
* @param event
* the dragenter or dragover event.
*/
private void setDropEffect(NativeEvent event) {
if (getState().dropEffect != null) {
DataTransfer.DropEffect dropEffect = DataTransfer.DropEffect
// the valueOf() needs to have equal string and name()
// doesn't return in all upper case
.valueOf(getState().dropEffect.name().toUpperCase());
event.getDataTransfer().setDropEffect(dropEffect);
}
}
/**
* Event handler for the {@code dragover} event.
* <p>
* Override this method in case custom handling for the dragover event is
* required. If the drop is allowed, the event should prevent default.
*
* @param event
* browser event to be handled
*/
protected void onDragOver(Event event) {
NativeEvent nativeEvent = (NativeEvent) event;
if (isDropAllowed(nativeEvent)) {
setDropEffect(nativeEvent);
// Add drop target indicator in case the element doesn't have one
addTargetClassIndicator(nativeEvent);
// Prevent default to allow drop
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
} else {
// Remove drop effect
nativeEvent.getDataTransfer()
.setDropEffect(DataTransfer.DropEffect.NONE);
// Remove drop target indicator
removeTargetClassIndicator(nativeEvent);
}
}
/**
* Event handler for the {@code dragleave} event.
* <p>
* Override this method in case custom handling for the dragleave event is
* required.
*
* @param event
* browser event to be handled
*/
protected void onDragLeave(Event event) {
removeTargetClassIndicator((NativeEvent) event);
}
/**
* Event handler for the {@code drop} event.
* <p>
* Override this method in case custom handling for the drop event is
* required. If the drop is allowed, the event should prevent default.
*
* @param event
* browser event to be handled
*/
protected void onDrop(Event event) {
NativeEvent nativeEvent = (NativeEvent) event;
if (isDropAllowed(nativeEvent)) {
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
String dataTransferText = nativeEvent.getDataTransfer()
.getData(DragSourceState.DATA_TYPE_TEXT);
String dropEffect = DragSourceExtensionConnector
.getDropEffect(nativeEvent.getDataTransfer());
sendDropEventToServer(dataTransferText, dropEffect, nativeEvent);
}
removeTargetClassIndicator(nativeEvent);
}
private boolean isDropAllowed(NativeEvent event) {
// there never should be a drop when effect has been set to none
if (getState().dropEffect != null
&& getState().dropEffect == DropEffect.NONE) {
return false;
}
// TODO #9246: Should add verification for checking effectAllowed and
// dropEffect from event and comparing that to target's dropEffect.
// Currently Safari, Edge and IE don't follow the spec by allowing drop
// if those don't match
if (getState().dropCriteria != null) {
return executeScript(event, getState().dropCriteria);
}
// Allow when criteria not set
return true;
}
/**
* Initiates a server RPC for the drop event.
*
* @param dataTransferText
* Client side textual data that can be set for the drag source
* and is transferred to the drop target.
* @param dropEffect
* the desired drop effect
* @param dropEvent
* Client side drop event.
*/
protected void sendDropEventToServer(String dataTransferText,
String dropEffect, NativeEvent dropEvent) {
getRpcProxy(DropTargetRpc.class).drop(dataTransferText, dropEffect);
}
/**
* Add class that indicates that the component is a target.
* <p>
* This is triggered on {@link #onDragEnter(Event) dragenter} and
* {@link #onDragOver(Event) dragover} events pending if the drop is
* possible. The drop is possible if the drop effect for the target and
* source do match and the drop criteria script evaluates to true or is not
* set.
*
* @param event
* the dragenter or dragover event that triggered the indication.
*/
protected void addTargetClassIndicator(NativeEvent event) {
getDropTargetElement().addClassName(styleDragCenter);
}
/**
* Remove the drag target indicator class name from the target element.
* <p>
* This is triggered on {@link #onDrop(Event) drop},
* {@link #onDragLeave(Event) dragleave} and {@link #onDragOver(Event)
* dragover} events pending on whether the drop has happened or if it is not
* possible. The drop is not possible if the drop effect for the source and
* target don't match or if there is a drop criteria script that evaluates
* to false.
*
* @param event
* the event that triggered the removal of the indicator
*/
protected void removeTargetClassIndicator(NativeEvent event) {
getDropTargetElement().removeClassName(styleDragCenter);
}
private native boolean executeScript(NativeEvent event, String script)
/*-{
return new Function('event', script)(event);
}-*/;
@Override
public DropTargetState getState() {
return (DropTargetState) super.getState();
}
}