/*
* 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.ui.dnd;
import java.util.Objects;
import com.vaadin.server.AbstractExtension;
import com.vaadin.shared.Registration;
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.AbstractComponent;
import com.vaadin.ui.dnd.event.DropEvent;
import com.vaadin.ui.dnd.event.DropListener;
/**
* Extension to make a component a drop target for HTML5 drag and drop
* functionality.
*
* @param <T>
* Type of the component to be extended.
* @author Vaadin Ltd
* @since 8.1
*/
public class DropTargetExtension<T extends AbstractComponent>
extends AbstractExtension {
/**
* Extends {@code target} component and makes it a drop target.
*
* @param target
* Component to be extended.
*/
public DropTargetExtension(T target) {
super.extend(target);
}
@Override
public void attach() {
super.attach();
registerDropTargetRpc();
}
/**
* Registers the server side RPC methods invoked from client side on
* <code>drop</code> event.
* <p>
* Override this method if you need to have a custom RPC interface for
* transmitting the drop event with more data. If just need to do additional
* things before firing the drop event, then you should override
* {@link #onDrop(String, DropEffect)} instead.
*/
protected void registerDropTargetRpc() {
registerRpc((DropTargetRpc) (dataTransferText, dropEffect) -> {
onDrop(dataTransferText,
DropEffect.valueOf(dropEffect.toUpperCase()));
});
}
/**
* Invoked when a <code>drop</code> has been received from client side.
* Fires the {@link DropEvent}.
*
* @param dataTransferText
* the data transfer of type 'text' for the drop
* @param dropEffect
* the drop effect
*/
protected void onDrop(String dataTransferText, DropEffect dropEffect) {
DropEvent<T> event = new DropEvent<>(getParent(), dataTransferText,
dropEffect, getUI().getActiveDragSource());
fireEvent(event);
}
/**
* Sets the drop effect for the current drop target. This is set to the
* dropEffect on {@code dragenter} and {@code dragover} events.
* <p>
* <em>NOTE: If the drop effect that doesn't match the dropEffect /
* effectAllowed of the drag source, it DOES NOT prevent drop on IE and
* Safari! For FireFox and Chrome the drop is prevented if there they don't
* match.</em>
* <p>
* Default value is browser dependent and can depend on e.g. modifier keys.
* <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 dropEffect
* the drop effect to be set or {@code null} to not modify
*/
public void setDropEffect(DropEffect dropEffect) {
if (!Objects.equals(getState(false).dropEffect, dropEffect)) {
getState().dropEffect = dropEffect;
}
}
/**
* Returns the drop effect for the current drop target.
*
* @return The drop effect of this drop target or {@code null} if none set
* @see #setDropEffect(DropEffect)
*/
public DropEffect getDropEffect() {
return getState(false).dropEffect;
}
/**
* Sets a criteria script in JavaScript to allow drop on this drop target.
* The script is executed when something is dragged on top of the target,
* and the drop is not allowed in case the script returns {@code false}. If
* no script is set, then the drop is always accepted, if the set
* {@link #setDropEffect(DropEffect) dropEffect} matches the drag source.
* <p>
* <b>IMPORTANT:</b> Construct the criteria script carefully and do not
* include untrusted sources such as user input. Always keep in mind that
* the script is executed on the client as is.
* <p>
* Example:
*
* <pre>
* target.setDropCriteria(
* // If dragged source contains a URL, allow it to be dropped
* "if (event.dataTransfer.types.includes('text/uri-list')) {"
* + " return true;" + "}" +
*
* // Otherwise cancel the event"
* "return false;");
* </pre>
*
* @param criteriaScript
* JavaScript to be executed when drop event happens or
* {@code null} to clear.
*/
public void setDropCriteria(String criteriaScript) {
if (!Objects.equals(getState(false).dropCriteria, criteriaScript)) {
getState().dropCriteria = criteriaScript;
}
}
/**
* Gets the criteria script that determines whether a drop is allowed. If
* the script returns {@code false}, then it is determined the drop is not
* allowed.
*
* @return JavaScript that executes when drop event happens.
* @see #setDropCriteria(String)
*/
public String getDropCriteria() {
return getState(false).dropCriteria;
}
/**
* Attaches drop listener for the current drop target.
* {@link DropListener#drop(DropEvent)} is called when drop event happens on
* the client side.
*
* @param listener
* Listener to handle drop event.
* @return Handle to be used to remove this listener.
*/
public Registration addDropListener(DropListener<T> listener) {
return addListener(DropEvent.class, listener, DropListener.DROP_METHOD);
}
@Override
protected DropTargetState getState() {
return (DropTargetState) super.getState();
}
@Override
protected DropTargetState getState(boolean markAsDirty) {
return (DropTargetState) super.getState(markAsDirty);
}
/**
* Returns the component this extension is attached to.
*
* @return Extended component.
*/
@Override
@SuppressWarnings("unchecked")
public T getParent() {
return (T) super.getParent();
}
}