com.vaadin.flow.component.dnd.DragSource Maven / Gradle / Ivy
/*
* Copyright 2000-2024 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.flow.component.dnd;
import java.util.Locale;
import org.slf4j.LoggerFactory;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.HasElement;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dnd.internal.DndUtil;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.Style;
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
import com.vaadin.flow.shared.Registration;
/**
* Mixin interface that provides basic drag source API for any component.
*
* This can be used by either implementing this interface, or with the static
* API {@link #create(Component)}, {@link #configure(Component)} or
* {@link #configure(Component, boolean)}.
*
* @param
* the type of the drag source component
* @see DropTarget
* @author Vaadin Ltd
* @since 2.0
*/
@JsModule(DndUtil.DND_CONNECTOR)
public interface DragSource extends HasElement {
/**
* Makes the given component draggable and gives access to the generic drag
* source API for the component.
*
* The given component will be always set as draggable, if this is not
* desired, use either method {@link #configure(Component, boolean)} or
* {@link #setDraggable(boolean)}.
*
* @param component
* the component to make draggable
* @param
* the type of the component
* @return drag source API mapping to the component
* @see #configure(Component)
* @see #configure(Component, boolean)
*/
static DragSource create(T component) {
return configure(component, true);
}
/**
* Gives access to the generic drag source API for the given component.
*
* Unlike {@link #create(Component)} and
* {@link #configure(Component, boolean)}, this method does not change the
* active drop target status of the given component.
*
* @param component
* the component to make draggable
* @param
* the type of the component
* @return drag source API mapping to the component
* @see #create(Component)
* @see #configure(Component, boolean)
*/
static DragSource configure(T component) {
return new DragSource() {
@Override
public T getDragSourceComponent() {
return component;
}
};
}
/**
* Gives access to the generic drag source API for the given component and
* applies the given draggable status to it.
*
* This method is a shorthand for calling {@link #configure(Component)} and
* {@link #setDraggable(boolean)}.
*
* The component draggable state can be changed later on with
* {@link #setDraggable(boolean)}.
*
* @param component
* the component to make draggable
* @param draggable
* {@code true} to make draggable, {@code false} to not
* @param
* the type of the component
* @return drag source API mapping to the component
* @see #create(Component)
* @see #configure(Component, boolean)
*/
static DragSource configure(T component,
boolean draggable) {
DragSource dragSource = configure(component);
dragSource.setDraggable(draggable);
return dragSource;
}
/**
* Returns the drag source component. This component is used in the drag
* start and end events, and set as active drag source for the UI when
* dragged.
*
* The default implementation of this method returns {@code this}. This
* method exists for type safe access for the drag source component.
*
* @return the drag source component
*/
/*
* Implementation note: This could be mayhaps removed and replaced with
* magic that digs the component from an element. But keeping this method
* adds some convenience to the static usage. And it enables using an
* element that is not the root element of this component as the draggable
* element.
*/
default T getDragSourceComponent() {
return (T) this;
};
@Override
default Element getElement() {
return getDragSourceComponent().getElement();
}
/**
* Returns the element where the {@code draggable} attribute is applied,
* making it draggable by the user. By default it is the element of the
* component returned by {@link #getDragSourceComponent()}.
*
* Override this method to provide another element to be draggable instead
* of the root element of the component.
*
* @return the element made draggable
* @since 2.1
*/
/*
* Implementation note: this is added so that user can change the draggable
* element to be something else than the root element of the component.
*/
default Element getDraggableElement() {
return getDragSourceComponent().getElement();
}
/**
* Sets this component as draggable. By default it is not.
*
* @param draggable
* {@code true} for enable dragging, {@code false} to prevent
*/
default void setDraggable(boolean draggable) {
if (draggable == isDraggable()) {
return;
}
if (draggable) {
// The attribute is an enumerated one and not a Boolean one.
getDraggableElement().setProperty("draggable",
Boolean.TRUE.toString());
DndUtil.updateDragSourceActivation(this);
// store & clear the component as active drag source for the UI
Registration startListenerRegistration = addDragStartListener(
event -> getDragSourceComponent().getUI()
.orElseThrow(() -> new IllegalStateException(
"DragSource not attached to an UI but received a drag start event."))
.getInternals().setActiveDragSourceComponent(
getDragSourceComponent()));
Registration endListenerRegistration = addDragEndListener(
event -> getDragSourceComponent().getUI()
.orElse(UI.getCurrent()).getInternals()
.setActiveDragSourceComponent(null));
ComponentUtil.setData(getDragSourceComponent(),
DndUtil.START_LISTENER_REGISTRATION_KEY,
startListenerRegistration);
ComponentUtil.setData(getDragSourceComponent(),
DndUtil.END_LISTENER_REGISTRATION_KEY,
endListenerRegistration);
} else {
getDraggableElement().removeProperty("draggable");
DndUtil.updateDragSourceActivation(this);
// clear listeners for setting active data source
Object startListenerRegistration = ComponentUtil.getData(
getDragSourceComponent(),
DndUtil.START_LISTENER_REGISTRATION_KEY);
if (startListenerRegistration instanceof Registration) {
((Registration) startListenerRegistration).remove();
}
Object endListenerRegistration = ComponentUtil.getData(
getDragSourceComponent(),
DndUtil.END_LISTENER_REGISTRATION_KEY);
if (endListenerRegistration instanceof Registration) {
((Registration) endListenerRegistration).remove();
}
}
// only onetime thing when in development mode
DndUtil.reportUsage();
}
/**
* Is this component currently draggable. By default it is not.
*
* @return {@code true} draggable, {@code false} if not
*/
default boolean isDraggable() {
return getDraggableElement().hasProperty("draggable");
}
/**
* Set server side drag data. This data is available in the drop event and
* can be used to transfer data between drag source and {@link DropTarget}
* if they are in the same UI.
*
* The drag data can be set also in the drag start event listener added with
* {@link #addDragStartListener(ComponentEventListener)} using
* {@link DragStartEvent#setDragData(Object)}.
*
* @param data
* Data to transfer to drop event.
* @see DropEvent#getDragData()
* @see DragStartEvent#setDragData(Object)
* @see DragEndEvent#clearDragData()
*/
default void setDragData(Object data) {
ComponentUtil.setData(getDragSourceComponent(),
DndUtil.DRAG_SOURCE_DATA_KEY, data);
}
/**
* Get server side drag data. This data is available in the drop event and
* can be used to transfer data between drag source and drop target if they
* are in the same UI.
*
* @return Server side drag data if set, otherwise {@literal null}.
*/
default Object getDragData() {
return ComponentUtil.getData(getDragSourceComponent(),
DndUtil.DRAG_SOURCE_DATA_KEY);
}
/**
* Sets the allowed effects for the current drag source element. Used for
* setting client side {@code DataTransfer.effectAllowed} parameter for the
* drag event.
*
* By default the value is {@link EffectAllowed#UNINITIALIZED} which is
* equivalent to {@link EffectAllowed#ALL}.
*
* NOTE: The effect should be set in advance, setting it after the
* user has started dragging and the {@link DragStartEvent} has been fired
* is too late - it will take effect only for next drag operation.
*
* NOTE 2: Edge, Safari and IE11 will allow the drop to occur even when
* the effect allowed does not match the drop effect set on the drop target.
* Chrome and Firefox prevent the drop if those do not match.
*
* @param effect
* Effects to allow for this draggable element. Cannot be {@code
* null}.
* @see
* MDN web docs for more information.
*/
default void setEffectAllowed(EffectAllowed effect) {
if (effect == null) {
throw new IllegalArgumentException("Allowed effect cannot be null");
}
getDraggableElement().setProperty(
DndUtil.EFFECT_ALLOWED_ELEMENT_PROPERTY,
effect.getClientPropertyValue());
}
/**
* Returns the allowed effects for the current drag source element. Used to
* set client side {@code DataTransfer.effectAllowed} parameter for the drag
* event.
*
* @return effects that are allowed for this draggable element.
*/
default EffectAllowed getEffectAllowed() {
return EffectAllowed.valueOf(getDraggableElement().getProperty(
DndUtil.EFFECT_ALLOWED_ELEMENT_PROPERTY,
EffectAllowed.UNINITIALIZED.getClientPropertyValue()
.toUpperCase(Locale.ENGLISH)));
}
/**
* Sets the drag image for the current drag source element. The image is
* applied automatically in the next drag start event in the browser. Drag
* image is shown by default with zero offset which means that pointer
* location is in the top left corner of the image.
*
* {@code com.vaadin.flow.component.html.Image} is fully supported as a drag
* image component. Other components can be used as well, but the support
* may vary between browsers. If given component is visible element in the
* viewport, browser can show it as a drag image.
*
* @see
* MDN web docs for more information.
* @param dragImage
* the image to be used as drag image or null to remove it
*/
default void setDragImage(Component dragImage) {
setDragImage(dragImage, 0, 0);
}
/**
* Sets the drag image for the current drag source element. The image is
* applied automatically in the next drag start event in the browser.
* Coordinates define the offset of the pointer location from the top left
* corner of the image.
*
* {@code com.vaadin.flow.component.html.Image} is fully supported as a drag
* image component. Other components can be used as well, but the support
* may vary between browsers. If given component is visible element in the
* viewport, browser can show it as a drag image.
*
* @see
* MDN web docs for more information.
* @param dragImage
* the image to be used as drag image or null to remove it
* @param offsetX
* the x-offset of the drag image
* @param offsetY
* the y-offset of the drag image
*/
default void setDragImage(Component dragImage, int offsetX, int offsetY) {
if (dragImage != null && !dragImage.isVisible()) {
throw new IllegalStateException(
"Drag image element is not visible and will not show.\nMake element visible to use as drag image!");
}
if (getDragImage() != null && getDragImage() != dragImage) {
// Remove drag image from the virtual children list if it's there.
if (getDraggableElement().getNode()
.hasFeature(VirtualChildrenList.class)) {
VirtualChildrenList childrenList = getDraggableElement()
.getNode().getFeature(VirtualChildrenList.class);
// dodging exception with empty list
if (childrenList.size() > 0) {
getDraggableElement()
.removeVirtualChild(getDragImage().getElement());
}
}
}
if (dragImage != null && !dragImage.isAttached()) {
if (!getDragSourceComponent().isAttached()) {
getDragSourceComponent().addAttachListener(event -> {
if (!dragImage.isAttached()
&& dragImage.getParent().isEmpty()) {
appendDragElement(dragImage.getElement());
}
event.unregisterListener();
});
} else {
appendDragElement(dragImage.getElement());
}
}
ComponentUtil.setData(getDragSourceComponent(),
DndUtil.DRAG_SOURCE_IMAGE, dragImage);
getDraggableElement().executeJs(
"window.Vaadin.Flow.dndConnector.setDragImage($0, $1, $2, $3)",
dragImage, (dragImage == null ? 0 : offsetX),
(dragImage == null ? 0 : offsetY), getDraggableElement());
}
private void appendDragElement(Element dragElement) {
if (dragElement.getTag().equals("img")) {
getDraggableElement().appendVirtualChild(dragElement);
} else {
LoggerFactory.getLogger(DragSource.class).debug(
"Attaching child to dom in position -100,-100. Consider adding the component manually to not get overlapping components on drag for element.");
getDraggableElement().appendChild(dragElement);
Style style = dragElement.getStyle();
style.set("position", "absolute");
style.set("top", "-100px");
style.set("left", "-100px");
style.set("display", "none");
}
}
/**
* Get server side drag image. This image is applied automatically in the
* next drag start event in the browser.
*
* @return Server side drag image if set, otherwise {@literal null}.
*/
default Component getDragImage() {
return (Component) ComponentUtil.getData(getDragSourceComponent(),
DndUtil.DRAG_SOURCE_IMAGE);
}
/**
* Attaches dragstart listener for the current drag source. The listener is
* triggered when dragstart event happens on the client side.
*
* @param listener
* Listener to handle dragstart event.
* @return Handle to be used to remove this listener.
*/
default Registration addDragStartListener(
ComponentEventListener> listener) {
return ComponentUtil.addListener(getDragSourceComponent(),
DragStartEvent.class, (ComponentEventListener) listener);
}
/**
* Attaches dragend listener for the current drag source.The listener is
* triggered when dragend event happens on the client side.
*
* @param listener
* Listener to handle dragend event.
* @return Handle to be used to remove this listener.
*/
default Registration addDragEndListener(
ComponentEventListener> listener) {
return ComponentUtil.addListener(getDragSourceComponent(),
DragEndEvent.class, (ComponentEventListener) listener);
}
}