diff options
8 files changed, 232 insertions, 45 deletions
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 b7b2fb957d..617a0480ba 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 @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import com.google.gwt.dom.client.Element; @@ -235,6 +236,14 @@ public class GridConnector extends AbstractListingConnector updateHeaderCellFromState(row.getCell(getColumn(columnId)), cellState); }); + for (Map.Entry<CellState, Set<String>> cellGroupEntry : rowState.cellGroups.entrySet()) { + Set<String> group = cellGroupEntry.getValue(); + + Grid.Column<?, ?>[] columns = + group.stream().map(idToColumn::get).toArray(size->new Grid.Column<?, ?>[size]); + // Set state to be the same as first in group. + updateHeaderCellFromState(row.join(columns), cellGroupEntry.getKey()); + } } } diff --git a/client/src/main/java/com/vaadin/client/widgets/Grid.java b/client/src/main/java/com/vaadin/client/widgets/Grid.java index 276cde7666..e1633fdbd3 100644 --- a/client/src/main/java/com/vaadin/client/widgets/Grid.java +++ b/client/src/main/java/com/vaadin/client/widgets/Grid.java @@ -442,9 +442,9 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, private StaticSection<?> section; /** - * Map from set of spanned columns to cell meta data. + * Map from cell meta data to sets of spanned columns . */ - private Map<Set<Column<?, ?>>, CELLTYPE> cellGroups = new HashMap<>(); + private Map<CELLTYPE, Set<Column<?, ?>>> cellGroups = new HashMap<>(); /** * A custom style name for the row or null if none is set. @@ -461,9 +461,9 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, * null if not found */ public CELLTYPE getCell(Column<?, ?> column) { - Set<Column<?, ?>> cellGroup = getCellGroupForColumn(column); - if (cellGroup != null) { - return cellGroups.get(cellGroup); + CELLTYPE cell = getMergedCellForColumn(column); + if (cell != null) { + return cell; } return cells.get(column); } @@ -500,7 +500,7 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, throw new IllegalArgumentException( "Given column does not exists on row " + column); - } else if (getCellGroupForColumn(column) != null) { + } else if (getMergedCellForColumn(column) != null) { throw new IllegalStateException( "Column is already in a group."); } @@ -508,7 +508,7 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, } CELLTYPE joinedCell = createCell(); - cellGroups.put(columnGroup, joinedCell); + cellGroups.put(joinedCell, columnGroup); joinedCell.setSection(getSection()); calculateColspans(); @@ -549,12 +549,10 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, return join(columns); } - private Set<Column<?, ?>> getCellGroupForColumn( + private CELLTYPE getMergedCellForColumn( Column<?, ?> column) { - for (Set<Column<?, ?>> group : cellGroups.keySet()) { - if (group.contains(column)) { - return group; - } + for (Entry<CELLTYPE, Set<Column<?, ?>>> entry : cellGroups.entrySet()) { + if (entry.getValue().contains(column)) return entry.getKey(); } return null; } @@ -565,22 +563,22 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, cell.setColspan(1); } // Set colspan for grouped cells - for (Set<Column<?, ?>> group : cellGroups.keySet()) { - if (!checkMergedCellIsContinuous(group)) { + for (Entry<CELLTYPE, Set<Column<?, ?>>> entry : cellGroups.entrySet()) { + CELLTYPE mergedCell = entry.getKey(); + if (!checkMergedCellIsContinuous(entry.getValue())) { // on error simply break the merged cell - cellGroups.get(group).setColspan(1); + mergedCell.setColspan(1); } else { int colSpan = 0; - for (Column<?, ?> column : group) { + for (Column<?, ?> column : entry.getValue()) { if (!column.isHidden()) { colSpan++; } } // colspan can't be 0 - cellGroups.get(group).setColspan(Math.max(1, colSpan)); + mergedCell.setColspan(Math.max(1, colSpan)); } } - } private boolean checkMergedCellIsContinuous( @@ -2399,7 +2397,7 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, * An initial height that is given to new details rows before rendering the * appropriate widget that we then can be measure * - * @see GridSpacerUpdater + * @see Grid.GridSpacerUpdater */ private static final double DETAILS_ROW_INITIAL_HEIGHT = 50; diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index b9f5f5afbb..ee1547bfce 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -1540,6 +1540,25 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { public default HeaderCell getCell(Column<?, ?> column) { return getCell(column.getId()); } + + /** + * Merges columns cells in a row + * + * @param cells + * The cells to merge. Must be from the same row. + * @return The remaining visible cell after the merge + */ + HeaderCell join(Set<HeaderCell> cellsToMerge); + + /** + * Merges columns cells in a row + * + * @param cells + * The cells to merge. Must be from the same row. + * @return The remaining visible cell after the merge + */ + HeaderCell join(HeaderCell ... cellsToMerge); + } /** @@ -1599,6 +1618,13 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { * @return cell content type */ public GridStaticCellType getCellType(); + + /** + * Gets the column id where this cell is. + * + * @return column id for this cell + */ + public String getColumnId(); } /** @@ -1894,11 +1920,12 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { */ public void removeColumn(Column<T, ?> column) { if (columnSet.remove(column)) { - columnKeys.remove(column.getId()); - removeDataGenerator(column); - getHeader().removeColumn(column.getId()); + String columnId = column.getId(); + columnKeys.remove(columnId); column.remove(); - + getHeader().removeColumn(columnId); + getFooter().removeColumn(columnId); + getState(true).columnOrder.remove(columnId); } } diff --git a/server/src/main/java/com/vaadin/ui/components/grid/Header.java b/server/src/main/java/com/vaadin/ui/components/grid/Header.java index 31f80a9add..6a4b72c5f9 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/Header.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/Header.java @@ -17,11 +17,15 @@ package com.vaadin.ui.components.grid; import com.vaadin.ui.Grid; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + /** * Represents the header section of a Grid. - * + * * @author Vaadin Ltd. - * + * * @since 8.0 */ public abstract class Header extends StaticSection<Header.Row> { @@ -64,7 +68,7 @@ public abstract class Header extends StaticSection<Header.Row> { /** * Returns whether this row is the default header row. - * + * * @return {@code true} if this row is the default row, {@code false} * otherwise. */ @@ -74,13 +78,52 @@ public abstract class Header extends StaticSection<Header.Row> { /** * Sets whether this row is the default header row. - * + * * @param defaultHeader * {@code true} to set to default, {@code false} otherwise. */ protected void setDefault(boolean defaultHeader) { getRowState().defaultHeader = defaultHeader; } + + /** + * Merges columns cells in a row + * + * @param cellsToMerge + * the cells which should be merged + * @return the remaining visible cell after the merge + */ + @Override + public Grid.HeaderCell join(Set<Grid.HeaderCell> cellsToMerge) { + for (Grid.HeaderCell cell : cellsToMerge) { + checkIfAlreadyMerged(cell.getColumnId()); + } + + // Create new cell data for the group + Cell newCell = createCell(); + + Set<String> columnGroup = new HashSet<>(); + for (Grid.HeaderCell cell : cellsToMerge) { + columnGroup.add(cell.getColumnId()); + } + addMergedCell(newCell, columnGroup); + markAsDirty(); + return newCell; + } + + /** + * Merges columns cells in a row + * + * @param cellsToMerge + * the cells which should be merged + * @return the remaining visible cell after the merge + */ + @Override + public Grid.HeaderCell join(Grid.HeaderCell... cellsToMerge) { + Set<Grid.HeaderCell> headerCells = new HashSet<>(Arrays.asList(cellsToMerge)); + return join(headerCells); + } + } @Override @@ -99,7 +142,7 @@ public abstract class Header extends StaticSection<Header.Row> { /** * Returns the default row of this header. The default row displays column * captions and sort indicators. - * + * * @return the default row, or {@code null} if there is no default row */ public Row getDefaultRow() { diff --git a/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java b/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java index 678ab2a935..5c7d682b17 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java @@ -19,10 +19,12 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import com.vaadin.shared.ui.grid.GridStaticCellType; import com.vaadin.shared.ui.grid.SectionState; @@ -56,7 +58,7 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> private final RowState rowState = new RowState(); private final StaticSection<?> section; - private final Map<Object, CELL> cells = new LinkedHashMap<>(); + private final Map<String, CELL> cells = new LinkedHashMap<>(); /** * Creates a new row belonging to the given section. @@ -102,10 +104,17 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> * @param columnId * the id of the column from which to remove the cell */ - protected void removeCell(Object columnId) { + protected void removeCell(String columnId) { CELL cell = cells.remove(columnId); if (cell != null) { - rowState.cells.remove(cell.getCellState()); + rowState.cells.remove(columnId); + for (Iterator<Set<String>> iterator = rowState.cellGroups.values().iterator(); iterator.hasNext(); ) { + Set<String> group = iterator.next(); + group.remove(columnId); + if(group.size() < 2) { + iterator.remove(); + } + } } } @@ -143,6 +152,23 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> cell.detach(); } } + + void checkIfAlreadyMerged(String columnId) { + for (Set<String> cellGroup : getRowState().cellGroups.values()) { + if (cellGroup.contains(columnId)) { + throw new IllegalArgumentException( + "Cell " + columnId + " is already merged"); + } + } + if (!cells.containsKey(columnId)) { + throw new IllegalArgumentException( + "Cell " + columnId + " does not exist on this row"); + } + } + + void addMergedCell(CELL newCell, Set<String> columnGroup) { + rowState.cellGroups.put(newCell.getCellState(), columnGroup); + } } /** @@ -161,7 +187,7 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> cellState.columnId = id; } - String getColumnId() { + public String getColumnId() { return cellState.columnId; } @@ -411,6 +437,7 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> for (ROW row : rows) { row.removeCell(columnId); } + markAsDirty(); } /** diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/SectionState.java b/shared/src/main/java/com/vaadin/shared/ui/grid/SectionState.java index 39d2d2f2e9..dda6d3fe9f 100644 --- a/shared/src/main/java/com/vaadin/shared/ui/grid/SectionState.java +++ b/shared/src/main/java/com/vaadin/shared/ui/grid/SectionState.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import com.vaadin.shared.Connector; @@ -37,6 +38,9 @@ public class SectionState implements Serializable { /** The map from column ids to the cells in this row. */ public Map<String, CellState> cells = new HashMap<>(); + /** The map from a joint cell to column id sets in this row. */ + public Map<CellState, Set<String>> cellGroups = new HashMap<>(); + /** * Whether this row is the default header row. Always false for footer * rows. diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java b/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java index 7b48acd400..f5f02bbf60 100644 --- a/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java +++ b/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java @@ -1,15 +1,5 @@ package com.vaadin.tests.components.grid.basics; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.stream.Stream; - import com.vaadin.annotations.Widgetset; import com.vaadin.server.VaadinRequest; import com.vaadin.shared.Registration; @@ -36,6 +26,18 @@ import com.vaadin.ui.renderers.HtmlRenderer; import com.vaadin.ui.renderers.NumberRenderer; import com.vaadin.ui.renderers.ProgressBarRenderer; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + @Widgetset("com.vaadin.DefaultWidgetSet") public class GridBasics extends AbstractTestUIWithLog { @@ -50,9 +52,9 @@ public class GridBasics extends AbstractTestUIWithLog { public static final String CELL_STYLE_GENERATOR_EMPTY = "Empty string"; public static final String CELL_STYLE_GENERATOR_NULL = "Null"; - public static final String[] COLUMN_CAPTIONS = { "Column 0", "Column 1", + public static final String[] COLUMN_CAPTIONS = {"Column 0", "Column 1", "Column 2", "Row Number", "Date", "HTML String", "Big Random", - "Small Random" }; + "Small Random"}; private final Command toggleReorderListenerCommand = new Command() { private Registration registration = null; @@ -272,6 +274,9 @@ public class GridBasics extends AbstractTestUIWithLog { selectedItem -> col .setHidden(selectedItem.isChecked())) .setCheckable(true); + columnMenu + .addItem("Remove", + selectedItem -> grid.removeColumn(col)); } } @@ -396,7 +401,7 @@ public class GridBasics extends AbstractTestUIWithLog { } private <T> void addGridMethodMenu(MenuItem parent, String name, T value, - Consumer<T> method) { + Consumer<T> method) { parent.addItem(name, menuItem -> method.accept(value)); } @@ -437,6 +442,25 @@ public class GridBasics extends AbstractTestUIWithLog { headerMenu.addItem("Set no default row", menuItem -> { grid.setDefaultHeaderRow(null); }); + headerMenu.addItem("Merge Header Cells [0,0..1]", menuItem -> { + mergeHeaderСells(0, "0+1", 0, 1); + }); + headerMenu.addItem("Merge Header Cells [1,1..3]", menuItem -> { + mergeHeaderСells(1, "1+2+3", 1, 2, 3); + }); + headerMenu.addItem("Merge Header Cells [0,6..7]", menuItem -> { + mergeHeaderСells(0, "6+7", 6, 7); + }); + } + + private void mergeHeaderСells(int rowIndex, String jointCellText, int... columnIndexes) { + HeaderRow headerRow = grid.getHeaderRow(rowIndex); + List<Column<DataObject, ?>> columns = grid.getColumns(); + Set<Grid.HeaderCell> toMerge = new HashSet<>(); + for (int columnIndex : columnIndexes) { + toMerge.add(headerRow.getCell(columns.get(columnIndex))); + } + headerRow.join(toMerge).setText(jointCellText); } private void createFooterMenu(MenuItem footerMenu) { diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridHeaderFooterTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridHeaderFooterTest.java index bff780ee75..94f30fe279 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridHeaderFooterTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridHeaderFooterTest.java @@ -260,6 +260,61 @@ public class GridHeaderFooterTest extends GridBasicsTest { assertEquals("Column 1", getColumnHidingToggle(1).getText()); } + @Test + public void testHeaderMergedRemoveColumn() { + selectMenuPath("Component", "Header", "Append header row"); + selectMenuPath("Component", "Header", "Merge Header Cells [0,0..1]"); + + GridCellElement c00 = getGridElement().getHeaderCell(0, 0); + assertEquals("0+1", c00.getText()); + assertEquals("Colspan of cell [0,0]", "2", c00.getAttribute("colspan")); + + selectMenuPath("Component", "Columns", "Column 1", "Remove"); + selectMenuPath("Component", "Header", "Append header row"); + + c00 = getGridElement().getHeaderCell(0, 0); + assertEquals("Column 0", c00.getText()); + assertEquals("Colspan of cell [0,0]", "1", c00.getAttribute("colspan")); + + GridCellElement c01 = getGridElement().getHeaderCell(0, 1); + assertEquals("Column 2", c01.getText()); + + GridCellElement c10 = getGridElement().getHeaderCell(1, 0); + assertEquals("Header cell 0", c10.getText()); + + GridCellElement c11 = getGridElement().getHeaderCell(1, 1); + assertEquals("Header cell 2", c11.getText()); + + GridCellElement c20 = getGridElement().getHeaderCell(2, 0); + assertEquals("Header cell 0", c20.getText()); + + GridCellElement c21 = getGridElement().getHeaderCell(2, 1); + assertEquals("Header cell 1", c21.getText()); + + + } + + @Test + public void testHeaderMerge() { + selectMenuPath("Component", "Header", "Append header row"); + selectMenuPath("Component", "Header", "Merge Header Cells [0,0..1]"); + selectMenuPath("Component", "Header", "Merge Header Cells [1,1..3]"); + selectMenuPath("Component", "Header", "Merge Header Cells [0,6..7]"); + + GridCellElement mergedCell1 = getGridElement().getHeaderCell(0, 0); + assertEquals("0+1", mergedCell1.getText()); + assertEquals("Colspan, cell [0,0]", "2", mergedCell1.getAttribute("colspan")); + + GridCellElement mergedCell2 = getGridElement().getHeaderCell(1, 1); + assertEquals("1+2+3", mergedCell2.getText()); + assertEquals("Colspan of cell [1,1]", "3", mergedCell2.getAttribute("colspan")); + + GridCellElement mergedCell3 = getGridElement().getHeaderCell(0, 6); + assertEquals("6+7", mergedCell3.getText()); + assertEquals("Colspan of cell [0,6]", "2", mergedCell3.getAttribute("colspan")); + + } + private void toggleColumnHidable(int index) { selectMenuPath("Component", "Columns", "Column " + index, "Hidable"); } |