com.vaadin.client.ui.dd.VDragAndDropManager Maven / Gradle / Ivy
Show all versions of vaadin-client Show documentation
/*
* 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.ui.dd;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.MouseEventDetailsBuilder;
import com.vaadin.client.Profiler;
import com.vaadin.client.UIDL;
import com.vaadin.client.VConsole;
import com.vaadin.client.ValueMap;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.VOverlay;
import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.dd.DragEventType;
/**
* Helper class to manage the state of drag and drop event on Vaadin client
* side. Can be used to implement most of the drag and drop operation
* automatically via cross-browser event preview method or just as a helper when
* implementing own low level drag and drop operation (like with HTML5 api).
*
* Singleton. Only one drag and drop operation can be active anyways. Use
* {@link #get()} to get instance.
*
* TODO cancel drag and drop if more than one touches !?
*/
public class VDragAndDropManager {
public static final String ACTIVE_DRAG_SOURCE_STYLENAME = "v-active-drag-source";
/**
* Implementation if this interface is provided as a parameter to
* DDEventHandleStrategy methods. The mediator instance allows to manage
* DnD.
*
* @since 7.4.4
*/
public interface DDManagerMediator {
/**
* Returns DnD manager instance.
*/
VDragAndDropManager getManager();
/**
* Returns current drag event.
*/
VDragEvent getDragEvent();
/**
* Clean up server communication callback.
*/
void clearServerCallback();
}
private final class DefaultDragAndDropEventHandler
implements NativePreviewHandler {
@Override
public void onPreviewNativeEvent(NativePreviewEvent event) {
if (getEventHandleStrategy().isDragInterrupted(event,
managerMediator)) {
// end drag if ESC is hit
interruptDrag();
event.cancel();
event.getNativeEvent().preventDefault();
return;
}
int typeInt = event.getTypeInt();
if (typeInt == Event.ONKEYDOWN) {
getEventHandleStrategy().handleKeyDownEvent(event,
managerMediator);
return;
}
NativeEvent nativeEvent = event.getNativeEvent();
currentDrag.setCurrentGwtEvent(nativeEvent);
String display = getEventHandleStrategy().updateDragImage(event,
managerMediator);
Element targetElement = getEventHandleStrategy()
.getTargetElement(event, managerMediator);
try {
if (handleDragImage(targetElement, event)) {
return;
}
} catch (RuntimeException e) {
// ApplicationConnection.getConsole().log(
// "ERROR during elementFromPoint hack.");
throw e;
} finally {
getEventHandleStrategy().restoreDragImage(display,
managerMediator, event);
}
getEventHandleStrategy().handleEvent(targetElement, event,
managerMediator);
}
private boolean handleDragImage(Element target,
NativePreviewEvent event) {
if (!WidgetUtil.isTouchEvent(event.getNativeEvent())
&& getDragElement() == null) {
return false;
} else if (target == null) {
// ApplicationConnection.getConsole().log(
// "Event on dragImage, ignored");
event.cancel();
event.getNativeEvent().stopPropagation();
return true;
} else if (getEventHandleStrategy().handleDragImageEvent(target,
event, managerMediator)) {
return true;
} else {
// just update element over and let the actual
// handling code do the thing
// ApplicationConnection.getConsole().log(
// "Target just modified on "
// + event.getType());
currentDrag.setElementOver(target);
return false;
}
}
}
/*
* #13381, #14796. The drag only actually starts when the mouse move or
* touch move event is more than 3 pixel away.
*/
public static final int MINIMUM_DISTANCE_TO_START_DRAG = 3;
private static VDragAndDropManager instance;
private HandlerRegistration handlerRegistration;
private VDragEvent currentDrag;
private DDManagerMediator managerMediator = new DDManagerMediator() {
@Override
public VDragAndDropManager getManager() {
return VDragAndDropManager.this;
}
@Override
public VDragEvent getDragEvent() {
return currentDrag;
}
@Override
public void clearServerCallback() {
serverCallback = null;
}
};
private DDEventHandleStrategy eventHandleStrategy;
/**
* If dragging is currently on a drophandler, this field has reference to it
*/
private VDropHandler currentDropHandler;
public VDropHandler getCurrentDropHandler() {
return currentDropHandler;
}
/**
* If drag and drop operation is not handled by {@link VDragAndDropManager}s
* internal handler, this can be used to update current {@link VDropHandler}
* .
*
* @param currentDropHandler
*/
public void setCurrentDropHandler(VDropHandler currentDropHandler) {
this.currentDropHandler = currentDropHandler;
}
private VDragEventServerCallback serverCallback;
private HandlerRegistration deferredStartRegistration;
public static VDragAndDropManager get() {
if (instance == null) {
instance = GWT.create(VDragAndDropManager.class);
}
return instance;
}
/* Singleton */
protected VDragAndDropManager() {
}
private final NativePreviewHandler defaultDragAndDropEventHandler = new DefaultDragAndDropEventHandler();
/**
* Flag to indicate if drag operation has really started or not. Null check
* of currentDrag field is not enough as a lazy start may be pending.
*/
private boolean isStarted;
/**
* This method is used to start Vaadin client side drag and drop operation.
* Operation may be started by virtually any Widget.
*
* Cancels possible existing drag. TODO figure out if this is always a bug
* if one is active. Maybe a good and cheap lifesaver thought.
*
* If possible, method automatically detects current {@link VDropHandler}
* and fires {@link VDropHandler#dragEnter(VDragEvent)} event on it.
*
* May also be used to control the drag and drop operation. If this option
* is used, {@link VDropHandler} is searched on mouse events and appropriate
* methods on it called automatically.
*
* @param transferable
* @param nativeEvent
* @param handleDragEvents
* if true, {@link VDragAndDropManager} handles the drag and drop
* operation GWT event preview.
* @return
*/
public VDragEvent startDrag(VTransferable transferable,
final NativeEvent startEvent, final boolean handleDragEvents) {
interruptDrag();
isStarted = false;
currentDrag = new VDragEvent(transferable, startEvent);
currentDrag.setCurrentGwtEvent(startEvent);
final Command startDrag = new Command() {
@Override
public void execute() {
isStarted = true;
addActiveDragSourceStyleName();
VDropHandler dh = null;
if (startEvent != null) {
dh = findDragTarget(Element.as(
currentDrag.getCurrentGwtEvent().getEventTarget()));
}
if (dh != null) {
// drag has started on a DropHandler, kind of drag over
// happens
currentDropHandler = dh;
dh.dragEnter(currentDrag);
}
if (handleDragEvents) {
handlerRegistration = Event.addNativePreviewHandler(
defaultDragAndDropEventHandler);
if (dragElement != null
&& dragElement.getParentElement() == null) {
attachDragElement();
}
}
// just capture something to prevent text selection in IE
Event.setCapture(RootPanel.getBodyElement());
}
private void addActiveDragSourceStyleName() {
ComponentConnector dragSource = currentDrag.getTransferable()
.getDragSource();
dragSource.getWidget()
.addStyleName(ACTIVE_DRAG_SOURCE_STYLENAME);
}
};
final int eventType = Event.as(startEvent).getTypeInt();
if (handleDragEvents && (eventType == Event.ONMOUSEDOWN
|| eventType == Event.ONTOUCHSTART)) {
// only really start drag event on mousemove
deferredStartRegistration = Event
.addNativePreviewHandler(new NativePreviewHandler() {
private int startX = WidgetUtil.getTouchOrMouseClientX(
currentDrag.getCurrentGwtEvent());
private int startY = WidgetUtil.getTouchOrMouseClientY(
currentDrag.getCurrentGwtEvent());
@Override
public void onPreviewNativeEvent(
NativePreviewEvent event) {
int typeInt = event.getTypeInt();
if (typeInt == -1 && event.getNativeEvent()
.getType().toLowerCase()
.contains("pointer")) {
/*
* Ignore PointerEvents since IE10 and IE11 send
* also MouseEvents for backwards compatibility.
*/
return;
}
switch (typeInt) {
case Event.ONMOUSEOVER:
if (dragElement == null) {
break;
}
EventTarget currentEventTarget = event
.getNativeEvent()
.getCurrentEventTarget();
if (Node.is(currentEventTarget)
&& !dragElement.isOrHasChild(
Node.as(currentEventTarget))) {
// drag image appeared below, ignore
break;
}
case Event.ONKEYDOWN:
case Event.ONKEYPRESS:
case Event.ONKEYUP:
case Event.ONBLUR:
case Event.ONFOCUS:
// don't cancel possible drag start
break;
case Event.ONMOUSEOUT:
if (dragElement == null) {
break;
}
EventTarget relatedEventTarget = event
.getNativeEvent()
.getRelatedEventTarget();
if (Node.is(relatedEventTarget)
&& !dragElement.isOrHasChild(
Node.as(relatedEventTarget))) {
// drag image appeared below, ignore
break;
}
case Event.ONMOUSEMOVE:
case Event.ONTOUCHMOVE:
int currentX = WidgetUtil
.getTouchOrMouseClientX(
event.getNativeEvent());
int currentY = WidgetUtil
.getTouchOrMouseClientY(
event.getNativeEvent());
if (Math.abs(
startX - currentX) > MINIMUM_DISTANCE_TO_START_DRAG
|| Math.abs(startY
- currentY) > MINIMUM_DISTANCE_TO_START_DRAG) {
ensureDeferredRegistrationCleanup();
currentDrag.setCurrentGwtEvent(
event.getNativeEvent());
startDrag.execute();
}
break;
default:
ensureDeferredRegistrationCleanup();
currentDrag = null;
clearDragElement();
break;
}
}
});
} else {
startDrag.execute();
}
return currentDrag;
}
protected void updateDragImagePosition(NativeEvent gwtEvent,
Element dragImage) {
if (gwtEvent != null && dragImage != null) {
Style style = dragImage.getStyle();
int clientY = WidgetUtil.getTouchOrMouseClientY(gwtEvent);
int clientX = WidgetUtil.getTouchOrMouseClientX(gwtEvent);
style.setTop(clientY, Unit.PX);
style.setLeft(clientX, Unit.PX);
}
}
/**
* First seeks the widget from this element, then iterates widgets until one
* implement HasDropHandler. Returns DropHandler from that.
*
* @param element
* @return
*/
protected VDropHandler findDragTarget(Element element) {
try {
Widget w = WidgetUtil.findWidget(element, null);
if (w == null) {
return null;
}
while (!(w instanceof VHasDropHandler)
|| !isDropEnabled((VHasDropHandler) w)) {
w = w.getParent();
if (w == null) {
break;
}
}
if (w == null) {
return null;
} else {
VDropHandler dh = ((VHasDropHandler) w).getDropHandler();
return dh;
}
} catch (Exception e) {
// ApplicationConnection.getConsole().log(
// "FIXME: Exception when detecting drop handler");
// e.printStackTrace();
return null;
}
}
/**
* Checks if the given {@link VHasDropHandler} really is able to accept
* drops.
*/
private static boolean isDropEnabled(VHasDropHandler target) {
VDropHandler dh = target.getDropHandler();
return dh != null && dh.getConnector().isEnabled();
}
/**
* Drag is ended (drop happened) on current drop handler. Calls drop method
* on current drop handler and does appropriate cleanup.
*/
public void endDrag() {
endDrag(true);
}
/**
* The drag and drop operation is ended, but drop did not happen. If
* operation is currently on a drop handler, its dragLeave method is called
* and appropriate cleanup happens.
*/
public void interruptDrag() {
endDrag(false);
}
private void endDrag(boolean doDrop) {
ensureDeferredRegistrationCleanup();
ensureHandlerRegistrationCleanup();
boolean sendTransferableToServer = false;
if (currentDropHandler != null) {
if (doDrop) {
// we have dropped on a drop target
sendTransferableToServer = currentDropHandler.drop(currentDrag);
if (sendTransferableToServer) {
doRequest(DragEventType.DROP);
/*
* Clean active source class name deferred until response is
* handled. E.g. hidden on start, removed in drophandler ->
* would flicker in case removed eagerly.
*/
final ComponentConnector dragSource = currentDrag
.getTransferable().getDragSource();
final ApplicationConnection client = currentDropHandler
.getApplicationConnection();
Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
@Override
public boolean execute() {
if (!client.getMessageSender().hasActiveRequest()) {
removeActiveDragSourceStyleName(dragSource);
return false;
}
return true;
}
}, 30);
}
} else {
currentDropHandler.dragLeave(currentDrag);
currentDrag.setCurrentGwtEvent(null);
}
currentDropHandler = null;
serverCallback = null;
visitId = 0; // reset to ignore ongoing server check
}
/*
* Remove class name indicating drag source when server visit is done
* iff server visit was not initiated. Otherwise it will be removed once
* the server visit is done.
*/
if (!sendTransferableToServer && currentDrag != null) {
removeActiveDragSourceStyleName(
currentDrag.getTransferable().getDragSource());
}
currentDrag = null;
clearDragElement();
// release the capture (set to prevent text selection in IE)
Event.releaseCapture(RootPanel.getBodyElement());
}
private void ensureHandlerRegistrationCleanup() {
if (handlerRegistration != null) {
handlerRegistration.removeHandler();
handlerRegistration = null;
}
}
private void ensureDeferredRegistrationCleanup() {
if (deferredStartRegistration != null) {
deferredStartRegistration.removeHandler();
deferredStartRegistration = null;
}
}
private void removeActiveDragSourceStyleName(
ComponentConnector dragSource) {
dragSource.getWidget().removeStyleName(ACTIVE_DRAG_SOURCE_STYLENAME);
}
private void clearDragElement() {
if (dragElement != null) {
if (dragElement.getParentElement() != null) {
dragElement.removeFromParent();
}
dragElement = null;
}
}
private int visitId = 0;
private Element dragElement;
/**
* Visits server during drag and drop procedure. Transferable and event type
* is given to server side counterpart of DropHandler.
*
* If another server visit is started before the current is received, the
* current is just dropped. TODO consider if callback should have
* interrupted() method for cleanup.
*
* @param acceptCallback
*/
public void visitServer(VDragEventServerCallback acceptCallback) {
doRequest(DragEventType.ENTER);
serverCallback = acceptCallback;
}
private void doRequest(DragEventType drop) {
if (currentDropHandler == null) {
return;
}
ComponentConnector paintable = currentDropHandler.getConnector();
ApplicationConnection client = currentDropHandler
.getApplicationConnection();
/*
* For drag events we are using special id that are routed to
* "drag service" which then again finds the corresponding DropHandler
* on server side.
*
* TODO add rest of the data in Transferable
*
* TODO implement partial updates to Transferable (currently the whole
* Transferable is sent on each request)
*/
visitId++;
client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID,
"visitId", visitId, false);
client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID,
"eventId", currentDrag.getEventId(), false);
client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID,
"dhowner", paintable, false);
VTransferable transferable = currentDrag.getTransferable();
client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID,
"component", transferable.getDragSource(), false);
client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID,
"type", drop.ordinal(), false);
if (currentDrag.getCurrentGwtEvent() != null) {
try {
MouseEventDetails mouseEventDetails = MouseEventDetailsBuilder
.buildMouseEventDetails(
currentDrag.getCurrentGwtEvent());
currentDrag.getDropDetails().put("mouseEvent",
mouseEventDetails.serialize());
} catch (Exception e) {
// NOP, (at least oophm on Safari) can't serialize html dd event
// to mouseevent
}
} else {
currentDrag.getDropDetails().put("mouseEvent", null);
}
client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID,
"evt", currentDrag.getDropDetails(), false);
client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID,
"tra", transferable.getVariableMap(), true);
}
public void handleServerResponse(ValueMap valueMap) {
if (serverCallback == null) {
return;
}
Profiler.enter("VDragAndDropManager.handleServerResponse");
UIDL uidl = (UIDL) valueMap.cast();
int visitId = uidl.getIntAttribute("visitId");
if (this.visitId == visitId) {
serverCallback.handleResponse(uidl.getBooleanAttribute("accepted"),
uidl);
serverCallback = null;
}
runDeferredCommands();
Profiler.leave("VDragAndDropManager.handleServerResponse");
}
/**
* Returns DnD strategy to handle native preview events used by the manager.
*
* Subclasses can override this method to return custom strategy or use GWT
* deferred binding.
*
* @return internal DnD native preview event handler
*/
protected DDEventHandleStrategy getEventHandleStrategy() {
if (eventHandleStrategy == null) {
eventHandleStrategy = GWT.create(DDEventHandleStrategy.class);
}
return eventHandleStrategy;
}
private void runDeferredCommands() {
if (deferredCommand != null) {
Command command = deferredCommand;
deferredCommand = null;
command.execute();
if (!isBusy()) {
runDeferredCommands();
}
}
}
void setDragElement(Element node) {
if (currentDrag != null) {
if (dragElement != null && dragElement != node) {
clearDragElement();
} else if (node == dragElement) {
return;
}
dragElement = node;
dragElement.addClassName("v-drag-element");
updateDragImagePosition(currentDrag.getCurrentGwtEvent(),
dragElement);
if (isStarted) {
attachDragElement();
}
}
}
Element getDragElement() {
return dragElement;
}
private void attachDragElement() {
if (dragElement != null && dragElement.getParentElement() == null) {
ApplicationConnection connection = getCurrentDragApplicationConnection();
Element dragImageParent;
if (connection == null) {
VConsole.error(
"Could not determine ApplicationConnection for current drag operation. The drag image will likely look broken");
dragImageParent = RootPanel.getBodyElement();
} else {
dragImageParent = VOverlay.getOverlayContainer(connection);
}
dragImageParent.appendChild(dragElement);
}
}
private Command deferredCommand;
private boolean isBusy() {
return serverCallback != null;
}
protected ApplicationConnection getCurrentDragApplicationConnection() {
if (currentDrag == null) {
return null;
}
final ComponentConnector dragSource = currentDrag.getTransferable()
.getDragSource();
if (dragSource == null) {
return null;
}
return dragSource.getConnection();
}
/**
* Method to que tasks until all dd related server visits are done
*
* @param command
*/
private void defer(Command command) {
deferredCommand = command;
}
/**
* Method to execute commands when all existing dd related tasks are
* completed (some may require server visit).
*
* Using this method may be handy if criterion that uses lazy initialization
* are used. Check
*
* TODO Optimization: consider if we actually only need to keep the last
* command in queue here.
*
* @param command
*/
public void executeWhenReady(Command command) {
if (isBusy()) {
defer(command);
} else {
command.execute();
}
}
}