/* * 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.connectors.grid; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import com.google.gwt.animation.client.AnimationScheduler; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.TableRowElement; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Image; import com.vaadin.client.BrowserInfo; import com.vaadin.client.ServerConnector; import com.vaadin.client.WidgetUtil; import com.vaadin.client.extensions.DragSourceExtensionConnector; import com.vaadin.client.widget.escalator.RowContainer; import com.vaadin.client.widget.grid.selection.SelectionModel; import com.vaadin.client.widgets.Escalator; import com.vaadin.client.widgets.Grid; import com.vaadin.shared.Range; 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.grid.GridDragSourceRpc; import com.vaadin.shared.ui.grid.GridDragSourceState; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.ui.components.grid.GridDragSource; import elemental.events.Event; import elemental.json.Json; import elemental.json.JsonArray; import elemental.json.JsonObject; /** * Adds HTML5 drag and drop functionality to a * {@link com.vaadin.client.widgets.Grid Grid}'s rows. This is the client side * counterpart of {@link GridDragSource}. * * @author Vaadin Ltd * @since 8.1 */ @Connect(GridDragSource.class) public class GridDragSourceConnector extends DragSourceExtensionConnector { private static final String STYLE_SUFFIX_DRAG_BADGE = "-drag-badge"; private GridConnector gridConnector; /** * List of dragged item keys. */ private List<String> draggedItemKeys; @Override protected void extend(ServerConnector target) { gridConnector = (GridConnector) target; // HTML5 DnD is by default not enabled for mobile devices if (BrowserInfo.get().isTouchDevice() && !getConnection() .getUIConnector().isMobileHTML5DndEnabled()) { return; } // Set newly added rows draggable getGridBody().setNewEscalatorRowCallback( rows -> rows.forEach(this::addDraggable)); // Add drag listeners to body element addDragListeners(getGridBody().getElement()); } @Override protected void onDragStart(Event event) { NativeEvent nativeEvent = (NativeEvent) event; // Do not allow drag starts from native Android Chrome, since it doesn't // work properly (doesn't fire dragend reliably) if (isAndoidChrome() && isNativeDragEvent(nativeEvent)) { event.preventDefault(); event.stopPropagation(); return; } // Collect the keys of dragged rows draggedItemKeys = getDraggedRows(nativeEvent).stream() .map(row -> row.getString(GridState.JSONKEY_ROWKEY)) .collect(Collectors.toList()); // Ignore event if there are no items dragged if (draggedItemKeys.size() == 0) { return; } super.onDragStart(event); } @Override protected void setDragImage(NativeEvent dragStartEvent) { // do not call super since need to handle specifically // 1. use resource if set (never needs safari hack) // 2. add number badge if necessary (with safari hack if needed) // 3. just use normal (with safari hack if needed) // Add badge showing the number of dragged columns String imageUrl = getResourceUrl(DragSourceState.RESOURCE_DRAG_IMAGE); if (imageUrl != null && !imageUrl.isEmpty()) { Image dragImage = new Image( getConnection().translateVaadinUri(imageUrl)); dragStartEvent.getDataTransfer() .setDragImage(dragImage.getElement(), 0, 0); } else { Element draggedRowElement = (Element) dragStartEvent .getEventTarget().cast(); if (draggedItemKeys.size() > 1) { Element badge = DOM.createSpan(); badge.setClassName( gridConnector.getWidget().getStylePrimaryName() + "-row" + STYLE_SUFFIX_DRAG_BADGE); badge.setInnerHTML(draggedItemKeys.size() + ""); badge.getStyle().setMarginLeft( getRelativeX(draggedRowElement, dragStartEvent) + 10, Style.Unit.PX); badge.getStyle().setMarginTop( getRelativeY(draggedRowElement, dragStartEvent) - draggedRowElement.getOffsetHeight() + 10, Style.Unit.PX); draggedRowElement.appendChild(badge); // Remove badge on the next animation frame. Drag image will // still contain the badge. AnimationScheduler.get().requestAnimationFrame(timestamp -> { badge.removeFromParent(); }, (Element) dragStartEvent.getEventTarget().cast()); } fixDragImageForSafari(draggedRowElement); } } private int getRelativeY(Element element, NativeEvent event) { int relativeTop = element.getAbsoluteTop() - Window.getScrollTop(); return WidgetUtil.getTouchOrMouseClientY(event) - relativeTop; } private int getRelativeX(Element element, NativeEvent event) { int relativeLeft = element.getAbsoluteLeft() - Window.getScrollLeft(); return WidgetUtil.getTouchOrMouseClientX(event) - relativeLeft; } @Override protected String createDataTransferText(NativeEvent dragStartEvent) { JsonArray dragData = toJsonArray(getDraggedRows(dragStartEvent).stream() .map(this::getDragData).collect(Collectors.toList())); return dragData.toJson(); } @Override protected void sendDragStartEventToServer(NativeEvent dragStartEvent) { // Start server RPC with dragged item keys getRpcProxy(GridDragSourceRpc.class).dragStart(draggedItemKeys); } private List<JsonObject> getDraggedRows(NativeEvent dragStartEvent) { List<JsonObject> draggedRows = new ArrayList<>(); if (TableRowElement.is(dragStartEvent.getEventTarget())) { TableRowElement row = (TableRowElement) dragStartEvent .getEventTarget().cast(); int rowIndex = ((Escalator.AbstractRowContainer) getGridBody()) .getLogicalRowIndex(row); JsonObject rowData = gridConnector.getDataSource().getRow(rowIndex); if (dragMultipleRows(rowData)) { getSelectedVisibleRows().forEach(draggedRows::add); } else { draggedRows.add(rowData); } } return draggedRows; } @Override protected void onDragEnd(Event event) { NativeEvent nativeEvent = (NativeEvent) event; // for android chrome we use the polyfill, in case browser fires a // native dragend event after the polyfill, we need to ignore that one if (isAndoidChrome() && isNativeDragEvent((nativeEvent))) { event.preventDefault(); event.stopPropagation(); return; } // Ignore event if there are no items dragged if (draggedItemKeys != null && draggedItemKeys.size() > 0) { super.onDragEnd(event); } // Clear item key list draggedItemKeys = null; } @Override protected void sendDragEndEventToServer(NativeEvent dragEndEvent, DropEffect dropEffect) { // Send server RPC with dragged item keys getRpcProxy(GridDragSourceRpc.class).dragEnd(dropEffect, draggedItemKeys); } /** * Tells if multiple rows are dragged. Returns true if multiple selection is * allowed and a selected row is dragged. * * @param draggedRow * Data of dragged row. * @return {@code true} if multiple rows are dragged, {@code false} * otherwise. */ private boolean dragMultipleRows(JsonObject draggedRow) { SelectionModel<JsonObject> selectionModel = getGrid() .getSelectionModel(); return selectionModel.isSelectionAllowed() && selectionModel instanceof MultiSelectionModelConnector.MultiSelectionModel && selectionModel.isSelected(draggedRow); } /** * Collects the data of all selected visible rows. * * @return List of data of all selected visible rows. */ private List<JsonObject> getSelectedVisibleRows() { return getSelectedRowsInRange(getEscalator().getVisibleRowRange()); } /** * Get all selected rows from a subset of rows defined by {@code range}. * * @param range * Range of indexes. * @return List of data of all selected rows in the given range. */ private List<JsonObject> getSelectedRowsInRange(Range range) { List<JsonObject> selectedRows = new ArrayList<>(); for (int i = range.getStart(); i < range.getEnd(); i++) { JsonObject row = gridConnector.getDataSource().getRow(i); if (SelectionModel.isItemSelected(row)) { selectedRows.add(row); } } return selectedRows; } /** * Converts a list of {@link JsonObject}s to a {@link JsonArray}. * * @param objects * List of json objects. * @return Json array containing all json objects. */ private JsonArray toJsonArray(List<JsonObject> objects) { JsonArray array = Json.createArray(); for (int i = 0; i < objects.size(); i++) { array.set(i, objects.get(i)); } return array; } /** * Gets drag data from the row data if exists or returns complete row data * otherwise. * * @param row * Row data. * @return Drag data if present or row data otherwise. */ private JsonObject getDragData(JsonObject row) { return row.hasKey(GridDragSourceState.JSONKEY_DRAG_DATA) ? row.getObject(GridDragSourceState.JSONKEY_DRAG_DATA) : row; } @Override public void onUnregister() { super.onUnregister(); // Remove draggable from all row elements in the escalator Range visibleRange = getEscalator().getVisibleRowRange(); for (int i = visibleRange.getStart(); i < visibleRange.getEnd(); i++) { removeDraggable(getGridBody().getRowElement(i)); } // Remove drag listeners from body element removeDragListeners(getGridBody().getElement()); // Remove callback for newly added rows getGridBody().setNewEscalatorRowCallback(null); } private Grid<JsonObject> getGrid() { return gridConnector.getWidget(); } private Escalator getEscalator() { return getGrid().getEscalator(); } private RowContainer.BodyRowContainer getGridBody() { return getEscalator().getBody(); } @Override public GridDragSourceState getState() { return (GridDragSourceState) super.getState(); } }