All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.ui.components.grid.GridRowDragger Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */
package com.vaadin.ui.components.grid;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import com.vaadin.data.provider.DataProvider;
import com.vaadin.data.provider.ListDataProvider;
import com.vaadin.shared.ui.dnd.DropEffect;
import com.vaadin.shared.ui.grid.DropLocation;
import com.vaadin.shared.ui.grid.DropMode;
import com.vaadin.ui.Grid;
import com.vaadin.ui.Grid.Column;

/**
 * Allows dragging rows for reordering within a Grid and between two separate
 * Grids when the item type is the same.
 * 

* When dragging a selected row, all the visible selected rows are dragged. Note * that ONLY currently visible rows are taken into account. The drop mode for * the target grid is by default {@link DropMode#BETWEEN}. *

* To customize the settings for either the source or the target grid, use * {@link #getGridDragSource()} and {@link #getGridDropTarget()}.The drop target * grid has been set to not allow drops for a target row when the grid has been * sorted, since the visual drop target location would not match where the item * would actually be dropped into. Additionally, a grid MUST NOT be the target * of more than one GridRowDragger. *

* NOTE: this helper works only with {@link ListDataProvider} on both grids. * If you have another data provider, you should customize data provider * updating on drop with * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)} & * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} and add a * custom drop index calculator with * {@link #setDropIndexCalculator(DropIndexCalculator)}. *

* In case you are not using a {@link ListDataProvider} and don't have custom * handlers, {@link UnsupportedOperationException} is thrown on drop event. * * @param * The Grid bean type. * @author Vaadin Ltd * @since 8.2 */ public class GridRowDragger implements Serializable { private final GridDropTarget gridDropTarget; private final GridDragSource gridDragSource; private DropIndexCalculator dropTargetIndexCalculator = null; private SourceDataProviderUpdater sourceDataProviderUpdater = null; private TargetDataProviderUpdater targetDataProviderUpdater = null; /** * Set of items currently being dragged. */ private List draggedItems; private int shiftedDropIndex; /** * Enables DnD reordering for the rows in the given grid. *

* {@link DropMode#BETWEEN} is used. *

* NOTE: this only works when the grid has a * {@link ListDataProvider}. Use the custom handlers * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)} and * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} for * other data providers. *

* NOTE: When allowing the user to DnD reorder a grid's rows, you * should not allow the user to sort the grid since when the grid is sorted, * as the reordering doens't make any sense since the drop target cannot be * shown for the correct place due to the sorting. Sorting columns is * enabled by default for in-memory data provider grids. Sorting can be * disabled for columns with {@link Grid#getColumns()} and * {@link Column#setSortable(boolean)}. * * @param grid * Grid to be extended. */ public GridRowDragger(Grid grid) { this(grid, DropMode.BETWEEN); } /** * Enables DnD reordering the rows in the given grid with the given drop * mode. *

* NOTE: this only works when the grid has a * {@link ListDataProvider}. Use the custom handlers * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)} and * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} for * other data providers. *

* NOTE: When allowing the user to DnD reorder a grid's rows, you * should not allow the user to sort the grid since when the grid is sorted, * as the reordering doens't make any sense since the drop target cannot be * shown for the correct place due to the sorting. Sorting columns is * enabled by default for in-memory data provider grids. Sorting can be * disabled for columns with {@link Grid#getColumns()} and * {@link Column#setSortable(boolean)}. * * @param grid * the grid to enable row DnD reordering on * @param dropMode * DropMode to be used. */ public GridRowDragger(Grid grid, DropMode dropMode) { this(grid, grid, dropMode); } /** * Enables DnD moving of rows from the source grid to the target grid. *

* {@link DropMode#BETWEEN} is used. *

* NOTE: this only works when the grids have a * {@link ListDataProvider}. Use the custom handlers * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)} and * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} for * other data providers. * * @param source * the source grid dragged from. * @param target * the target grid dropped to. */ public GridRowDragger(Grid source, Grid target) { this(source, target, DropMode.BETWEEN); } /** * Enables DnD moving of rows from the source grid to the target grid with * the custom data provider updaters. *

* {@link DropMode#BETWEEN} is used. * * @param source * grid dragged from * @param target * grid dragged to * @param targetDataProviderUpdater * handler for updating target grid data provider * @param sourceDataProviderUpdater * handler for updating source grid data provider */ public GridRowDragger(Grid source, Grid target, TargetDataProviderUpdater targetDataProviderUpdater, SourceDataProviderUpdater sourceDataProviderUpdater) { this(source, target, DropMode.BETWEEN); this.targetDataProviderUpdater = targetDataProviderUpdater; this.sourceDataProviderUpdater = sourceDataProviderUpdater; } /** * Enables DnD moving of rows from the source grid to the target grid with * the given drop mode. *

* NOTE: this only works when the grids have a * {@link ListDataProvider}. Use the other constructors or custom * handlers {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)} * and {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} for * other data providers. * * @param source * the drag source grid * @param target * the drop target grid * @param dropMode * the drop mode to use */ public GridRowDragger(Grid source, Grid target, DropMode dropMode) { gridDragSource = new GridDragSource<>(source); gridDropTarget = new GridDropTarget<>(target, dropMode); gridDropTarget.setDropAllowedOnRowsWhenSorted(false); gridDragSource.addGridDragStartListener(event -> { draggedItems = event.getDraggedItems(); }); gridDropTarget.addGridDropListener(this::handleDrop); } /** * Sets the target data provider updater, which handles adding the dropped * items to the target grid. *

* By default, items are added to the index where they were dropped on for * any {@link ListDataProvider}. If another type of data provider is used, * this updater should be set to handle updating instead. * * @param targetDataProviderUpdater * the target drop handler to set, or {@code null} to remove */ public void setTargetDataProviderUpdater( TargetDataProviderUpdater targetDataProviderUpdater) { this.targetDataProviderUpdater = targetDataProviderUpdater; } /** * Returns the target grid data provider updater. * * @return target grid drop handler */ public TargetDataProviderUpdater getTargetDataProviderUpdater() { return targetDataProviderUpdater; } /** * Sets the source data provider updater, which handles removing items from * the drag source grid. *

* By default the items are removed from any {@link ListDataProvider}. If * another type of data provider is used, this updater should be set to * handle updating instead. *

* If you want to skip removing items from the source, you can use * {@link SourceDataProviderUpdater#NOOP}. * * @param sourceDataProviderUpdater * the drag source data provider updater to set, or {@code null} * to remove */ public void setSourceDataProviderUpdater( SourceDataProviderUpdater sourceDataProviderUpdater) { this.sourceDataProviderUpdater = sourceDataProviderUpdater; } /** * Returns the source grid data provider updater. *

* Default is {@code null} and the items are just removed from the source * grid, which only works for {@link ListDataProvider}. * * @return the source grid drop handler */ public SourceDataProviderUpdater getSourceDataProviderUpdater() { return sourceDataProviderUpdater; } /** * Sets the drop index calculator for the target grid. With this callback * you can have a custom drop location instead of the actual one. *

* By default, items are placed on the index they are dropped into in the * target grid. *

* If you want to always drop items to the end of the target grid, you can * use {@link DropIndexCalculator#alwaysDropToEnd()}. * * @param dropIndexCalculator * the drop index calculator */ public void setDropIndexCalculator( DropIndexCalculator dropIndexCalculator) { this.dropTargetIndexCalculator = dropIndexCalculator; } /** * Gets the drop index calculator. *

* Default is {@code null} and the dropped items are placed on the drop * location. * * @return the drop index calculator */ public DropIndexCalculator getDropIndexCalculator() { return dropTargetIndexCalculator; } /** * Returns the drop target grid to allow performing customizations such as * altering {@link DropEffect}. * * @return the drop target grid */ public GridDropTarget getGridDropTarget() { return gridDropTarget; } /** * Returns the drag source grid, exposing it for customizations. * * @return the drag source grid */ public GridDragSource getGridDragSource() { return gridDragSource; } /** * Returns the currently dragged items captured from the source grid no drag * start event, or {@code null} if no drag active. * * @return the currently dragged items or {@code null} */ protected List getDraggedItems() { return draggedItems; } /** * This method is triggered when there has been a drop on the target grid. *

* This method is protected only for testing reasons, you should not * override this but instead use * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)}, * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} and * {@link #setDropIndexCalculator(DropIndexCalculator)} to customize how to * handle the drops. * * @param event * the drop event on the target grid */ protected void handleDrop(GridDropEvent event) { // there is a case that the drop happened from some other grid than the // source one if (getDraggedItems() == null) { return; } // don't do anything if not supported data providers used without custom // handlers verifySupportedDataProviders(); shiftedDropIndex = -1; handleSourceGridDrop(event, getDraggedItems()); int index = calculateDropIndex(event); handleTargetGridDrop(event, index, getDraggedItems()); draggedItems = null; } private void handleSourceGridDrop(GridDropEvent event, final Collection droppedItems) { Grid source = getGridDragSource().getGrid(); if (getSourceDataProviderUpdater() != null) { getSourceDataProviderUpdater().removeItems(event.getDropEffect(), source.getDataProvider(), droppedItems); return; } ListDataProvider listDataProvider = (ListDataProvider) source .getDataProvider(); // use the existing data source to keep filters and sort orders etc. in // place. Collection sourceItems = listDataProvider.getItems(); // if reordering the same grid and dropping on top of one of the dragged // rows, need to calculate the new drop index before removing the items if (getGridDragSource().getGrid() == getGridDropTarget().getGrid() && event.getDropTargetRow().isPresent() && getDraggedItems().contains(event.getDropTargetRow().get())) { List sourceItemsList = (List) sourceItems; shiftedDropIndex = sourceItemsList .indexOf(event.getDropTargetRow().get()); shiftedDropIndex -= getDraggedItems().stream().filter( item -> sourceItemsList.indexOf(item) < shiftedDropIndex) .count(); } sourceItems.removeAll(droppedItems); // if reordering the same grid, DataProvider's refresh will be done later if (getGridDragSource().getGrid() != getGridDropTarget().getGrid()) { listDataProvider.refreshAll(); } } private void handleTargetGridDrop(GridDropEvent event, final int index, Collection droppedItems) { Grid target = getGridDropTarget().getGrid(); if (getTargetDataProviderUpdater() != null) { getTargetDataProviderUpdater().onDrop(event.getDropEffect(), target.getDataProvider(), index, droppedItems); return; } ListDataProvider listDataProvider = (ListDataProvider) target .getDataProvider(); // update the existing to keep filters etc. List targetItems = (List) listDataProvider.getItems(); if (index != Integer.MAX_VALUE) { targetItems.addAll(index, droppedItems); } else { targetItems.addAll(droppedItems); } // instead of using setItems or creating a new data provider, // refresh the existing one to keep filters etc. in place listDataProvider.refreshAll(); // if dropped to the end of the grid, the grid should scroll there so // that the dropped row is visible, but that is just recommended in // documentation and left for the users to take into use } private int calculateDropIndex(GridDropEvent event) { // use custom calculator if present if (getDropIndexCalculator() != null) { return getDropIndexCalculator().calculateDropIndex(event); } // if the source and target grids are the same, then the index has been // calculated before removing the items. In this case the drop location // is always above, since the items will be starting from that point on if (shiftedDropIndex != -1) { return shiftedDropIndex; } ListDataProvider targetDataProvider = (ListDataProvider) getGridDropTarget() .getGrid().getDataProvider(); List items = (List) targetDataProvider.getItems(); int index = items.size(); Optional dropTargetRow = event.getDropTargetRow(); if (dropTargetRow.isPresent()) { index = items.indexOf(dropTargetRow.get()) + (event.getDropLocation() == DropLocation.BELOW ? 1 : 0); } return index; } private void verifySupportedDataProviders() { verifySourceDataProvider(); verifyTargetDataProvider(); } @SuppressWarnings("unchecked") private void verifySourceDataProvider() { if (getSourceDataProviderUpdater() != null) { return; // custom updater is always fine } if (!(getSourceDataProvider() instanceof ListDataProvider)) { throwUnsupportedOperationExceptionForUnsupportedDataProvider(true); } if (!(((ListDataProvider) getSourceDataProvider()) .getItems() instanceof List)) { throwUnsupportedOperationExceptionForUnsupportedCollectionInListDataProvider( true); } } @SuppressWarnings("unchecked") private void verifyTargetDataProvider() { if (getTargetDataProviderUpdater() != null && getDropIndexCalculator() != null) { return; // custom updater and calculator is always fine } if (!(getTargetDataProvider() instanceof ListDataProvider)) { throwUnsupportedOperationExceptionForUnsupportedDataProvider(false); } if (!(((ListDataProvider) getTargetDataProvider()) .getItems() instanceof List)) { throwUnsupportedOperationExceptionForUnsupportedCollectionInListDataProvider( false); } } private DataProvider getSourceDataProvider() { return getGridDragSource().getGrid().getDataProvider(); } private DataProvider getTargetDataProvider() { return getGridDropTarget().getGrid().getDataProvider(); } private static void throwUnsupportedOperationExceptionForUnsupportedDataProvider( boolean sourceGrid) { throw new UnsupportedOperationException(new StringBuilder() .append(sourceGrid ? "Source " : "Target ") .append("grid does not have a ListDataProvider, cannot automatically ") .append(sourceGrid ? "remove " : "add ") .append("items. Use GridRowDragger.set") .append(sourceGrid ? "Source" : "Target") .append("DataProviderUpdater(...) ") .append(sourceGrid ? "" : "and setDropIndexCalculator(...) " + "to customize how to handle updating the data provider.") .toString()); } private static void throwUnsupportedOperationExceptionForUnsupportedCollectionInListDataProvider( boolean sourceGrid) { throw new UnsupportedOperationException(new StringBuilder() .append(sourceGrid ? "Source " : "Target ") .append("grid's ListDataProvider is not backed by a List-collection, cannot ") .append(sourceGrid ? "remove " : "add ") .append("items. Use a ListDataProvider backed by a List, or use GridRowDragger.set") .append(sourceGrid ? "Source" : "Target") .append("DataProviderUpdater(...) ") .append(sourceGrid ? "" : "and setDropIndexCalculator(...) ") .append(" to customize how to handle updating the data provider to customize how to handle updating the data provider.") .toString()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy