diff options
4 files changed, 478 insertions, 1 deletions
diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss index e4a4a1d920..d0bae911db 100644 --- a/WebContent/VAADIN/themes/base/grid/grid.scss +++ b/WebContent/VAADIN/themes/base/grid/grid.scss @@ -52,6 +52,27 @@ $v-grid-editor-background-color: $v-grid-row-background-color !default; border: $v-grid-border; } + .#{$primaryStyleName} .header-drag-table { + border-spacing: 0; + table-layout: fixed; + width: inherit; // a decent default fallback + + .#{$primaryStyleName}-header { + + > .#{$primaryStyleName}-cell { + border: $v-grid-border; + opacity: 0.9; + filter: alpha(opacity=90); // IE8 + } + + > .#{$primaryStyleName}-drop-marker { + background-color: #197de1; + position: absolute; + width: 3px; + } + } + } + // Common cell styles .#{$primaryStyleName}-cell { diff --git a/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java new file mode 100644 index 0000000000..c8b651bd4e --- /dev/null +++ b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java @@ -0,0 +1,240 @@ +/* + * Copyright 2000-2014 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 java.util.logging.Logger; + +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.shared.HandlerRegistration; +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.vaadin.client.WidgetUtil; +import com.vaadin.client.widgets.Grid; + +/** + * A simple event handler for elements that can be drag and dropped. Loosely + * based on {@link VDragAndDropManager}, but without any Vaadin related stuff. + * Properly handles drag start, cancel and end. For example, used in + * {@link Grid} column header reordering. + * <p> + * The showing of the dragged element, drag hints and reacting to drop/cancel is + * delegated to {@link DragAndDropCallback} implementation. + * + * @since + * @author Vaadin Ltd + */ +public class DragAndDropHandler { + + /** + * Callback interface for drag and drop. + */ + public interface DragAndDropCallback { + /** + * Called when the drag has started. + */ + void showDragElement(); + + /** + * Called on drag. + * + * @param event + * the event related to the drag + */ + void updateDragElement(NativePreviewEvent event); + + /** + * Called when the has ended on a drop or cancel. + */ + void removeDragElement(); + + /** + * Called when the drag has ended on a drop. + */ + void onDrop(); + + /** + * Called when the drag has been canceled before drop. + */ + void onDragCancel(); + } + + private HandlerRegistration dragStartNativePreviewHandlerRegistration; + private HandlerRegistration dragHandlerRegistration; + + private boolean dragging; + + private DragAndDropCallback callback; + + private final NativePreviewHandler dragHandler = new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + if (dragging) { + final int typeInt = event.getTypeInt(); + switch (typeInt) { + case Event.ONKEYDOWN: + int keyCode = event.getNativeEvent().getKeyCode(); + if (keyCode == KeyCodes.KEY_ESCAPE) { + // end drag if ESC is hit + cancelDrag(event); + } + break; + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + callback.updateDragElement(event); + // prevent text selection on IE + event.getNativeEvent().preventDefault(); + break; + case Event.ONTOUCHCANCEL: + cancelDrag(event); + break; + case Event.ONTOUCHEND: + /* Avoid simulated event on drag end */ + event.getNativeEvent().preventDefault(); + //$FALL-THROUGH$ + case Event.ONMOUSEUP: + callback.updateDragElement(event); + callback.onDrop(); + stopDrag(); + event.cancel(); + break; + default: + break; + } + } else { + stopDrag(); + } + } + + private void cancelDrag(NativePreviewEvent event) { + callback.onDragCancel(); + callback.removeDragElement(); + stopDrag(); + event.cancel(); + event.getNativeEvent().preventDefault(); + } + + }; + + private static Logger getLogger() { + return Logger.getLogger(DragAndDropHandler.class.getName()); + } + + /** + * This method can be called to trigger drag and drop on any grid element + * that can be dragged and dropped. + * + * @param dragStartingEvent + * the drag triggering event, usually a {@link Event#ONMOUSEDOWN} + * or {@link Event#ONTOUCHSTART} event on the draggable element + * + * @param callback + * the callback that will handle actual drag and drop related + * operations + */ + public void onDragStartOnDraggableElement( + final NativeEvent dragStartingEvent, + final DragAndDropCallback callback) { + dragStartNativePreviewHandlerRegistration = Event + .addNativePreviewHandler(new NativePreviewHandler() { + + private int startX = WidgetUtil + .getTouchOrMouseClientX(dragStartingEvent); + private int startY = WidgetUtil + .getTouchOrMouseClientY(dragStartingEvent); + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + final 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: + case Event.ONMOUSEOUT: + // we don't care + 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.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + int currentX = WidgetUtil + .getTouchOrMouseClientX(event + .getNativeEvent()); + int currentY = WidgetUtil + .getTouchOrMouseClientY(event + .getNativeEvent()); + if (Math.abs(startX - currentX) > 3 + || Math.abs(startY - currentY) > 3) { + removeNativePreviewHandlerRegistration(); + startDrag(event, callback); + } + break; + default: + // on any other events, clean up this preview + // listener + removeNativePreviewHandlerRegistration(); + break; + } + } + }); + } + + private void startDrag(NativePreviewEvent event, + DragAndDropCallback callback) { + dragging = true; + // just capture something to prevent text selection in IE + Event.setCapture(RootPanel.getBodyElement()); + this.callback = callback; + dragHandlerRegistration = Event.addNativePreviewHandler(dragHandler); + callback.showDragElement(); + callback.updateDragElement(event); + } + + private void stopDrag() { + dragging = false; + if (dragHandlerRegistration != null) { + dragHandlerRegistration.removeHandler(); + dragHandlerRegistration = null; + } + Event.releaseCapture(RootPanel.getBodyElement()); + if (callback != null) { + callback.removeDragElement(); + callback = null; + } + } + + private void removeNativePreviewHandlerRegistration() { + if (dragStartNativePreviewHandlerRegistration != null) { + dragStartNativePreviewHandlerRegistration.removeHandler(); + dragStartNativePreviewHandlerRegistration = null; + } + } +} diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 71962d6953..b0e18cc2ab 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -36,6 +36,8 @@ import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.DivElement; 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.NodeList; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; @@ -53,6 +55,8 @@ import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.touch.client.Point; import com.google.gwt.user.client.DOM; 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.Timer; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; @@ -68,6 +72,8 @@ import com.vaadin.client.renderers.ComplexRenderer; import com.vaadin.client.renderers.Renderer; import com.vaadin.client.renderers.WidgetRenderer; import com.vaadin.client.ui.SubPartAware; +import com.vaadin.client.ui.dd.DragAndDropHandler; +import com.vaadin.client.ui.dd.DragAndDropHandler.DragAndDropCallback; import com.vaadin.client.widget.escalator.Cell; import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.EscalatorUpdater; @@ -2853,6 +2859,153 @@ public class Grid<T> extends ResizeComposite implements private boolean enabled = true; + private boolean columnReorderingAllowed; + + private DragAndDropHandler dndHandler = new DragAndDropHandler(); + + private DragAndDropCallback headerCellDndCallback = new DragAndDropCallback() { + + /** + * Elements for displaying the dragged column(s) and drop marker + * properly + */ + private Element table; + private Element tableHeader; + /** Marks the column drop location */ + private Element dropMarker; + /** A copy of the dragged column(s), moves with cursor. */ + private Element dragElement; + /** Tracks index of the column whose left side the drop would occur */ + private int latestColumnDropIndex; + /** + * Makes sure that drag cancel doesn't cause anything unwanted like sort + */ + private HandlerRegistration columnSortPreventRegistration; + + private void initHeaderDragElementDOM() { + if (table == null) { + tableHeader = DOM.createTHead(); + dropMarker = DOM.createDiv(); + tableHeader.appendChild(dropMarker); + table = DOM.createTable(); + table.appendChild(tableHeader); + table.setClassName("header-drag-table"); + } + // update the style names on each run in case primary name has been + // modified + tableHeader.setClassName(escalator.getHeader().getElement() + .getClassName()); + dropMarker.setClassName(getStylePrimaryName() + "-drop-marker"); + getElement().appendChild(table); + } + + @Override + public void updateDragElement(NativePreviewEvent event) { + int clientX = WidgetUtil.getTouchOrMouseClientX(event + .getNativeEvent()); + resolveDragElementHorizontalPosition(clientX); + updateDragDropMarker(clientX); + } + + private void updateDragDropMarker(final int clientX) { + RowContainer header = escalator.getHeader(); + NodeList<TableCellElement> cells = header.getRowElement( + eventCell.getRowIndex()).getCells(); + double dropMarkerLeft = 0 - escalator.getScrollLeft(); + latestColumnDropIndex = 0; + for (int i = 0; i < cells.getLength(); i++, latestColumnDropIndex++) { + TableCellElement cellElement = cells.getItem(i); + int cellX = cellElement.getAbsoluteLeft(); + int cellWidth = cellElement.getOffsetWidth(); + if (clientX < cellX || clientX < cellX + (cellWidth / 2)) { + break; + } else { + dropMarkerLeft += cellWidth; + } + } + if (dropMarkerLeft > header.getElement().getOffsetWidth() + || dropMarkerLeft < 0) { + dropMarkerLeft = -10000000; + } + dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX); + } + + private void resolveDragElementVerticalPosition() { + dragElement.getStyle().setTop(-10, Unit.PX); + } + + private void resolveDragElementHorizontalPosition(final int clientX) { + int left = clientX - table.getAbsoluteLeft(); + left = Math.max(0, Math.min(left, table.getClientWidth())); + left -= dragElement.getClientWidth() / 2; + dragElement.getStyle().setLeft(left, Unit.PX); + } + + @Override + public void showDragElement() { + initHeaderDragElementDOM(); + // TODO this clones also some unwanted style names, should confirm + // with UX what we want to show (focus/sort indicator) + dragElement = DOM.clone(eventCell.getElement(), true); + dragElement.getStyle().clearWidth(); + dropMarker.getStyle().setProperty("height", + dragElement.getStyle().getHeight()); + tableHeader.appendChild(dragElement); + // might need to change this on fly once sorting with multiple + // header rows is possible + resolveDragElementVerticalPosition(); + } + + @Override + public void removeDragElement() { + table.removeFromParent(); + dragElement.removeFromParent(); + } + + @Override + public void onDrop() { + final int draggedColumnIndex = eventCell.getColumnIndex(); + if (latestColumnDropIndex != draggedColumnIndex + && latestColumnDropIndex != (draggedColumnIndex + 1)) { + List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> reordered = new ArrayList<Column<?, T>>( + columns); + Column<?, T> moved = reordered.remove(draggedColumnIndex); + if (draggedColumnIndex < latestColumnDropIndex) { + latestColumnDropIndex--; + } + reordered.add(latestColumnDropIndex, moved); + @SuppressWarnings("unchecked") + Column<?, T>[] array = reordered.toArray(new Column[reordered + .size()]); + setColumnOrder(array); + } // else no reordering + } + + @Override + public void onDragCancel() { + // cancel next click so that we may prevent column sorting if + // mouse was released on top of the dragged cell + if (columnSortPreventRegistration == null) { + columnSortPreventRegistration = Event + .addNativePreviewHandler(new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent( + NativePreviewEvent event) { + if (event.getTypeInt() == Event.ONCLICK) { + event.cancel(); + event.getNativeEvent().preventDefault(); + columnSortPreventRegistration + .removeHandler(); + columnSortPreventRegistration = null; + } + } + }); + } + } + }; + /** * Enumeration for easy setting of selection mode. */ @@ -4649,7 +4802,7 @@ public class Grid<T> extends ResizeComposite implements + getColumnCount() + ")"); } - this.frozenColumnCount = numberOfColumns; + frozenColumnCount = numberOfColumns; updateFrozenColumns(); } @@ -4949,6 +5102,10 @@ public class Grid<T> extends ResizeComposite implements if (!isElementInChildWidget(e)) { + if (handleHeaderCellDragStartEvent(event, container)) { + return; + } + // Sorting through header Click / KeyUp if (handleHeaderDefaultRowEvent(event, container)) { return; @@ -5097,6 +5254,35 @@ public class Grid<T> extends ResizeComposite implements return true; } + private boolean handleHeaderCellDragStartEvent(Event event, + RowContainer container) { + if (!columnReorderingAllowed) { + return false; + } + if (container != escalator.getHeader()) { + return false; + } + // for now only support reordering of default row as the only row + if (!getHeader().getRow(eventCell.getRowIndex()).isDefault() + || getHeader().getRowCount() != 1) { + return false; + } + if (eventCell.getColumnIndex() < escalator.getColumnConfiguration() + .getFrozenColumnCount()) { + return false; + } + if (event.getTypeInt() == Event.ONMOUSEDOWN + && event.getButton() == NativeEvent.BUTTON_LEFT + || event.getTypeInt() == Event.ONTOUCHSTART) { + dndHandler.onDragStartOnDraggableElement(event, + headerCellDndCallback); + event.preventDefault(); + event.stopPropagation(); + return true; + } + return false; + } + private Point rowEventTouchStartingPoint; private CellStyleGenerator<T> cellStyleGenerator; private RowStyleGenerator<T> rowStyleGenerator; @@ -5906,6 +6092,27 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns whether columns can be reordered with drag and drop. + * + * @since + * @return <code>true</code> if columns can be reordered, false otherwise + */ + public boolean isColumnReorderingAllowed() { + return columnReorderingAllowed; + } + + /** + * Sets whether column reordering with drag and drop is allowed or not. + * + * @since + * @param columnReorderingAllowed + * specifies whether column reordering is allowed + */ + public void setColumnReorderingAllowed(boolean columnReorderingAllowed) { + this.columnReorderingAllowed = columnReorderingAllowed; + } + + /** * Sets a new column order for the grid. All columns which are not ordered * here will remain in the order they were before as the last columns of * grid. diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java index 0452aa65d1..bfe8b3cf4d 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java @@ -649,6 +649,15 @@ public class GridBasicClientFeaturesWidget extends grid.setEnabled(!grid.isEnabled()); } }, "Component", "State"); + + addMenuCommand("Column Reordering", new ScheduledCommand() { + + @Override + public void execute() { + grid.setColumnReorderingAllowed(!grid + .isColumnReorderingAllowed()); + } + }, "Component", "State"); } private void createColumnsMenu() { |