diff options
6 files changed, 619 insertions, 14 deletions
diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java index d88a50b7e5..cc3dff651d 100644 --- a/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java +++ b/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java @@ -95,6 +95,31 @@ public class ColumnConnector extends AbstractExtensionConnector { column.setHidable(getState().hidable); } + @OnStateChange("resizable") + void updateResizable() { + column.setResizable(getState().resizable); + } + + @OnStateChange("width") + void updateWidth() { + column.setWidth(getState().width); + } + + @OnStateChange("minWidth") + void updateMinWidth() { + column.setMinimumWidth(getState().minWidth); + } + + @OnStateChange("maxWidth") + void updateMaxWidth() { + column.setMaximumWidth(getState().maxWidth); + } + + @OnStateChange("expandRatio") + void updateExpandRatio() { + column.setExpandRatio(getState().expandRatio); + } + @Override public void onUnregister() { super.onUnregister(); diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java index fd31622cfe..ed07d1d3f1 100644 --- a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java +++ b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java @@ -163,6 +163,11 @@ public class GridConnector getColumnId(event.getColumn()), event.isHidden()); } }); + getWidget().addColumnResizeHandler(event -> { + Column<?, JsonObject> column = event.getColumn(); + getRpcProxy(GridServerRpc.class).columnResized(getColumnId(column), + column.getWidthActual()); + }); /* Item click events */ getWidget().addBodyClickHandler(itemClickHandler); @@ -366,6 +371,12 @@ public class GridConnector protected void sendContextClickEvent(MouseEventDetails details, EventTarget eventTarget) { + // if element is the resize indicator, ignore the event + if (isResizeHandle(eventTarget)) { + WidgetUtil.clearTextSelection(); + return; + } + EventCellReference<JsonObject> eventCell = getWidget().getEventCell(); Section section = eventCell.getSection(); @@ -381,4 +392,14 @@ public class GridConnector WidgetUtil.clearTextSelection(); } + + private boolean isResizeHandle(EventTarget eventTarget) { + if (Element.is(eventTarget)) { + Element e = Element.as(eventTarget); + if (e.getClassName().contains("-column-resize-handle")) { + return true; + } + } + return false; + } } diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index b77e2ca7f7..94f8c9aee7 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -70,6 +70,11 @@ import elemental.json.JsonValue; public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { @Deprecated + private static final Method COLUMN_RESIZE_METHOD = ReflectTools.findMethod( + ColumnResizeListener.class, "columnResize", + ColumnResizeEvent.class); + + @Deprecated private static final Method ITEM_CLICK_METHOD = ReflectTools .findMethod(ItemClickListener.class, "accept", ItemClick.class); @@ -80,6 +85,69 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { ColumnVisibilityChangeEvent.class); /** + * An event listener for column resize events in the Grid. + * + * @since 7.6 + */ + public interface ColumnResizeListener extends Serializable { + + /** + * Called when the columns of the grid have been resized. + * + * @param event + * An event providing more information + */ + void columnResize(ColumnResizeEvent event); + } + + /** + * An event that is fired when a column is resized, either programmatically + * or by the user. + * + * @since 7.6 + */ + public static class ColumnResizeEvent extends Component.Event { + + private final Column<?, ?> column; + private final boolean userOriginated; + + /** + * + * @param source + * the grid where the event originated from + * @param userOriginated + * <code>true</code> if event is a result of user + * interaction, <code>false</code> if from API call + */ + public ColumnResizeEvent(Grid<?> source, Column<?, ?> column, + boolean userOriginated) { + super(source); + this.column = column; + this.userOriginated = userOriginated; + } + + /** + * Returns the column that was resized. + * + * @return the resized column. + */ + public Column<?, ?> getColumn() { + return column; + } + + /** + * Returns <code>true</code> if the column resize was done by the user, + * <code>false</code> if not and it was triggered by server side code. + * + * @return <code>true</code> if event is a result of user interaction + */ + public boolean isUserOriginated() { + return userOriginated; + } + + } + + /** * An event fired when an item in the Grid has been clicked. * * @param <T> @@ -466,7 +534,12 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { @Override public void columnResized(String id, double pixels) { - // TODO Auto-generated method stub + final Column<T, ?> column = getColumn(id); + if (column != null && column.isResizable()) { + column.getState().width = pixels; + fireColumnResizeEvent(column, true); + markAsDirty(); + } } } @@ -943,6 +1016,242 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { } /** + * Sets the ratio with which the column expands. + * <p> + * By default, all columns expand equally (treated as if all of them had + * an expand ratio of 1). Once at least one column gets a defined expand + * ratio, the implicit expand ratio is removed, and only the defined + * expand ratios are taken into account. + * <p> + * If a column has a defined width ({@link #setWidth(double)}), it + * overrides this method's effects. + * <p> + * <em>Example:</em> A grid with three columns, with expand ratios 0, 1 + * and 2, respectively. The column with a <strong>ratio of 0 is exactly + * as wide as its contents requires</strong>. The column with a ratio of + * 1 is as wide as it needs, <strong>plus a third of any excess + * space</strong>, because we have 3 parts total, and this column + * reserves only one of those. The column with a ratio of 2, is as wide + * as it needs to be, <strong>plus two thirds</strong> of the excess + * width. + * + * @param expandRatio + * the expand ratio of this column. {@code 0} to not have it + * expand at all. A negative number to clear the expand + * value. + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @see #setWidth(double) + */ + public Column<T, V> setExpandRatio(int expandRatio) + throws IllegalStateException { + checkColumnIsAttached(); + if (expandRatio != getExpandRatio()) { + getState().expandRatio = expandRatio; + getParent().markAsDirty(); + } + return this; + } + + /** + * Returns the column's expand ratio. + * + * @return the column's expand ratio + * @see #setExpandRatio(int) + */ + public int getExpandRatio() { + return getState(false).expandRatio; + } + + /** + * Clears the expand ratio for this column. + * <p> + * Equal to calling {@link #setExpandRatio(int) setExpandRatio(-1)} + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public Column<T, V> clearExpandRatio() throws IllegalStateException { + return setExpandRatio(-1); + } + + /** + * Returns the width (in pixels). By default a column is 100px wide. + * + * @return the width in pixels of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public double getWidth() throws IllegalStateException { + checkColumnIsAttached(); + return getState(false).width; + } + + /** + * Sets the width (in pixels). + * <p> + * This overrides any configuration set by any of + * {@link #setExpandRatio(int)}, {@link #setMinimumWidth(double)} or + * {@link #setMaximumWidth(double)}. + * + * @param pixelWidth + * the new pixel width of the column + * @return the column itself + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @throws IllegalArgumentException + * thrown if pixel width is less than zero + */ + public Column<T, V> setWidth(double pixelWidth) + throws IllegalStateException, IllegalArgumentException { + checkColumnIsAttached(); + if (pixelWidth < 0) { + throw new IllegalArgumentException( + "Pixel width should be greated than 0 (in " + toString() + + ")"); + } + if (pixelWidth != getWidth()) { + getState().width = pixelWidth; + getParent().markAsDirty(); + getParent().fireColumnResizeEvent(this, false); + } + return this; + } + + /** + * Returns whether this column has an undefined width. + * + * @since 7.6 + * @return whether the width is undefined + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public boolean isWidthUndefined() { + checkColumnIsAttached(); + return getState(false).width < 0; + } + + /** + * Marks the column width as undefined. An undefined width means the + * grid is free to resize the column based on the cell contents and + * available space in the grid. + * + * @return the column itself + */ + public Column<T, V> setWidthUndefined() { + checkColumnIsAttached(); + if (!isWidthUndefined()) { + getState().width = -1; + getParent().markAsDirty(); + getParent().fireColumnResizeEvent(this, false); + } + return this; + } + + /** + * Sets the minimum width for this column. + * <p> + * This defines the minimum guaranteed pixel width of the column + * <em>when it is set to expand</em>. + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @see #setExpandRatio(int) + */ + public Column<T, V> setMinimumWidth(double pixels) + throws IllegalStateException { + checkColumnIsAttached(); + + final double maxwidth = getMaximumWidth(); + if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) { + throw new IllegalArgumentException("New minimum width (" + + pixels + ") was greater than maximum width (" + + maxwidth + ")"); + } + getState().minWidth = pixels; + getParent().markAsDirty(); + return this; + } + + /** + * Return the minimum width for this column. + * + * @return the minimum width for this column + * @see #setMinimumWidth(double) + */ + public double getMinimumWidth() { + return getState(false).minWidth; + } + + /** + * Sets the maximum width for this column. + * <p> + * This defines the maximum allowed pixel width of the column <em>when + * it is set to expand</em>. + * + * @param pixels + * the maximum width + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @see #setExpandRatio(int) + */ + public Column<T, V> setMaximumWidth(double pixels) { + checkColumnIsAttached(); + + final double minwidth = getMinimumWidth(); + if (pixels >= 0 && pixels < minwidth && minwidth >= 0) { + throw new IllegalArgumentException("New maximum width (" + + pixels + ") was less than minimum width (" + minwidth + + ")"); + } + + getState().maxWidth = pixels; + getParent().markAsDirty(); + return this; + } + + /** + * Returns the maximum width for this column. + * + * @return the maximum width for this column + * @see #setMaximumWidth(double) + */ + public double getMaximumWidth() { + return getState(false).maxWidth; + } + + /** + * Sets whether this column can be resized by the user. + * + * @since 7.6 + * @param resizable + * {@code true} if this column should be resizable, + * {@code false} otherwise + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public Column<T, V> setResizable(boolean resizable) { + checkColumnIsAttached(); + if (resizable != isResizable()) { + getState().resizable = resizable; + getParent().markAsDirty(); + } + return this; + } + + /** + * Gets the caption of the hiding toggle for this column. + * + * @since 7.5.0 + * @see #setHidingToggleCaption(String) + * @return the caption for the hiding toggle for this column + */ + public String getHidingToggleCaption() { + return getState(false).hidingToggleCaption; + } + + /** * Sets the caption of the hiding toggle for this column. Shown in the * toggle for this column in the grid's sidebar when the column is * {@link #isHidable() hidable}. @@ -966,17 +1275,6 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { } /** - * Gets the caption of the hiding toggle for this column. - * - * @since 7.5.0 - * @see #setHidingToggleCaption(String) - * @return the caption for the hiding toggle for this column - */ - public String getHidingToggleCaption() { - return getState(false).hidingToggleCaption; - } - - /** * Hides or shows the column. By default columns are visible before * explicitly hiding them. * @@ -1042,8 +1340,24 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { } /** - * Checks whether this column is attached and throws an - * {@link IllegalStateException} if it is not. + * Returns whether this column can be resized by the user. Default is + * {@code true}. + * <p> + * <em>Note:</em> the column can be programmatically resized using + * {@link #setWidth(double)} and {@link #setWidthUndefined()} regardless + * of the returned value. + * + * @since 7.6 + * @return {@code true} if this column is resizable, {@code false} + * otherwise + */ + public boolean isResizable() { + return getState(false).resizable; + } + + /** + * Checks if column is attached and throws an + * {@link IllegalStateException} if it is not * * @throws IllegalStateException * if the column is no longer attached to any grid @@ -1449,6 +1763,20 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { } /** + * Registers a new column resize listener. + * + * @param listener + * the listener to register, not null + * @return a registration for the listener + */ + public Registration addColumnResizeListener(ColumnResizeListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + addListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD); + return () -> removeListener(ColumnResizeEvent.class, listener, + COLUMN_RESIZE_METHOD); + } + + /** * Adds an item click listener. The listener is called when an item of this * {@code Grid} is clicked. * @@ -1503,4 +1831,9 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { markAsDirty(); } } + + private void fireColumnResizeEvent(Column<?, ?> column, + boolean userOriginated) { + fireEvent(new ColumnResizeEvent(this, column, userOriginated)); + } } diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java b/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java index c789988cb5..8296dd8660 100644 --- a/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java +++ b/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java @@ -33,5 +33,29 @@ public class ColumnState extends SharedState { /** Whether the column can be hidden by the user. */ public boolean hidable = false; + /** + * Column width in pixels. Default column width is + * {@value GridConstants#DEFAULT_COLUMN_WIDTH_PX}. + */ + public double width = GridConstants.DEFAULT_COLUMN_WIDTH_PX; + + /** How much of the remaining space this column will reserve. */ + public int expandRatio = GridConstants.DEFAULT_EXPAND_RATIO; + + /** + * The maximum expansion width of this column. -1 for "no maximum". If + * maxWidth is less than the calculated width, maxWidth is ignored. + */ + public double maxWidth = GridConstants.DEFAULT_MAX_WIDTH; + + /** + * The minimum expansion width of this column. -1 for "no minimum". If + * minWidth is less than the calculated width, minWidth will win. + */ + public double minWidth = GridConstants.DEFAULT_MIN_WIDTH; + + /** Whether this column is resizable by the user. */ + public boolean resizable = true; + public Connector renderer; } diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridColumnResizing.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridColumnResizing.java new file mode 100644 index 0000000000..67e6473850 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridColumnResizing.java @@ -0,0 +1,52 @@ +package com.vaadin.tests.components.grid; + +import java.util.Arrays; + +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.tests.data.bean.Person; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.Column; +import com.vaadin.ui.Label; +import com.vaadin.ui.TextField; +import com.vaadin.ui.renderers.NumberRenderer; + +public class GridColumnResizing extends AbstractTestUI { + + @Override + protected void setup(VaadinRequest request) { + TextField input = new TextField(); + Label isResizedLabel = new Label("not resized"); + Grid<Person> grid = new Grid<>(); + Column<Person, String> nameColumn = grid.addColumn("Name", + Person::getFirstName); + Column<Person, Number> ageColumn = grid.addColumn("Age", Person::getAge, + new NumberRenderer()); + grid.addColumnResizeListener(event -> { + if (event.isUserOriginated()) { + isResizedLabel.setValue("client resized"); + } else { + isResizedLabel.setValue("server resized"); + } + }); + grid.setItems(Arrays.asList(Person.createTestPerson1(), + Person.createTestPerson2())); + + addComponent(input); + addButton("set width", event -> nameColumn + .setWidth(Double.parseDouble(input.getValue()))); + addButton("set expand ratio", event -> { + nameColumn.setExpandRatio(4); + ageColumn.setExpandRatio(1); + }); + addButton("set min width", event -> nameColumn + .setMinimumWidth(Double.parseDouble(input.getValue()))); + addButton("set max width", event -> nameColumn + .setMaximumWidth(Double.parseDouble(input.getValue()))); + addButton("toggle resizable", + event -> nameColumn.setResizable(!nameColumn.isResizable())); + + addComponents(grid, isResizedLabel); + } + +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridColumnResizingTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridColumnResizingTest.java new file mode 100644 index 0000000000..3f2a08d979 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridColumnResizingTest.java @@ -0,0 +1,150 @@ +package com.vaadin.tests.components.grid; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.testbench.elements.GridElement; +import com.vaadin.testbench.elements.GridElement.GridCellElement; +import com.vaadin.testbench.elements.LabelElement; +import com.vaadin.testbench.elements.TextFieldElement; +import com.vaadin.tests.tb3.MultiBrowserTest; + +public class GridColumnResizingTest extends MultiBrowserTest { + + @Test + public void serverSetWidth() { + openTestURL(); + + serverSideSetWidth(50); + assertColumnWidth(50, 0); + + serverSideSetWidth(500); + assertColumnWidth(500, 0); + } + + @Test + public void setResizable() { + openTestURL(); + ButtonElement toggleResizableButton = $(ButtonElement.class).get(4); + GridCellElement cell = getGrid().getHeaderCell(0, 0); + + Assert.assertEquals(true, cell.isElementPresent( + By.cssSelector("div.v-grid-column-resize-handle"))); + + toggleResizableButton.click(); + Assert.assertEquals(false, cell.isElementPresent( + By.cssSelector("div.v-grid-column-resize-handle"))); + } + + @Test + public void setExpandRatio() { + openTestURL(); + ButtonElement setExpandRatioButton = $(ButtonElement.class).get(1); + + setExpandRatioButton.click(); + assertColumnWidthWithThreshold(375, 0, 2); + assertColumnWidthWithThreshold(125, 1, 2); + } + + @Test + public void setMinimumWidth() { + openTestURL(); + + setMinWidth(100); + serverSideSetWidth(50); + assertColumnWidth(100, 0); + + serverSideSetWidth(150); + dragResizeColumn(0, 0, -100); + assertColumnWidth(100, 0); + } + + @Test + public void setMaximumWidth() { + openTestURL(); + + serverSideSetWidth(50); + setMaxWidth(100); + + serverSideSetWidth(150); + assertColumnWidth(100, 0); + + // TODO add the following when grid column width recalculation has been + // fixed in the case where the sum of column widths exceeds the visible + // area + + // serverSideSetWidth(50); + // dragResizeColumn(0, 0, 200); + // assertColumnWidth(100, 0); + } + + @Test + public void resizeEventListener() { + openTestURL(); + + Assert.assertEquals("not resized", + $(LabelElement.class).get(1).getText()); + + serverSideSetWidth(150); + Assert.assertEquals("server resized", + $(LabelElement.class).get(1).getText()); + + dragResizeColumn(0, 0, 100); + Assert.assertEquals("client resized", + $(LabelElement.class).get(1).getText()); + } + + private GridElement getGrid() { + return $(GridElement.class).first(); + } + + private void serverSideSetWidth(double width) { + TextFieldElement textField = $(TextFieldElement.class).first(); + ButtonElement setWidthButton = $(ButtonElement.class).get(0); + textField.clear(); + textField.sendKeys(String.valueOf(width), Keys.ENTER); + setWidthButton.click(); + } + + private void setMinWidth(double minWidth) { + TextFieldElement textField = $(TextFieldElement.class).first(); + ButtonElement setMinWidthButton = $(ButtonElement.class).get(2); + textField.clear(); + textField.sendKeys(String.valueOf(minWidth), Keys.ENTER); + setMinWidthButton.click(); + } + + private void setMaxWidth(double maxWidth) { + TextFieldElement textField = $(TextFieldElement.class).first(); + ButtonElement setMaxWidthButton = $(ButtonElement.class).get(3); + textField.clear(); + textField.sendKeys(String.valueOf(maxWidth), Keys.ENTER); + setMaxWidthButton.click(); + } + + private void dragResizeColumn(int columnIndex, int posX, int offset) { + GridCellElement headerCell = getGrid().getHeaderCell(0, columnIndex); + Dimension size = headerCell.getSize(); + new Actions(getDriver()) + .moveToElement(headerCell, size.getWidth() + posX, + size.getHeight() / 2) + .clickAndHold().moveByOffset(offset, 0).release().perform(); + } + + private void assertColumnWidth(int width, int columnIndex) { + Assert.assertEquals(width, + getGrid().getCell(0, columnIndex).getSize().getWidth()); + } + + private void assertColumnWidthWithThreshold(int width, int columnIndex, + int threshold) { + Assert.assertTrue( + Math.abs(getGrid().getCell(0, columnIndex).getSize().getWidth() + - width) <= threshold); + } +} |