From e0fcd1cfe0cea7dac124df0d86bf74f8d8c4f9be Mon Sep 17 00:00:00 2001 From: Aleksi Hietanen Date: Mon, 17 Oct 2016 10:13:33 +0300 Subject: [PATCH] Grid html/component content in headers Change-Id: Ie6129b51d15d4f30a6b4c034999ff02deec1c6a7 --- .../client/connectors/grid/GridConnector.java | 35 ++++- server/src/main/java/com/vaadin/ui/Grid.java | 62 ++++++++- .../ui/components/grid/StaticSection.java | 131 +++++++++++++++--- .../vaadin/shared/ui/grid/SectionState.java | 16 +++ .../components/grid/basics/GridBasics.java | 41 ++++-- .../grid/basics/GridBasicsTest.java | 39 ++++++ .../grid/basics/GridHeaderFooterTest.java | 72 +++++++++- 7 files changed, 361 insertions(+), 35 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 a19c955fc4..ad5f8200ea 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 @@ -54,6 +54,7 @@ import com.vaadin.client.widget.grid.sort.SortOrder; import com.vaadin.client.widgets.Grid; import com.vaadin.client.widgets.Grid.Column; import com.vaadin.client.widgets.Grid.FooterRow; +import com.vaadin.client.widgets.Grid.HeaderCell; import com.vaadin.client.widgets.Grid.HeaderRow; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.data.DataCommunicatorConstants; @@ -66,6 +67,7 @@ import com.vaadin.shared.ui.grid.GridConstants.Section; import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.shared.ui.grid.SectionState; +import com.vaadin.shared.ui.grid.SectionState.CellState; import com.vaadin.shared.ui.grid.SectionState.RowState; import elemental.json.JsonObject; @@ -255,14 +257,41 @@ public class GridConnector for (RowState rowState : state.rows) { HeaderRow row = grid.appendHeaderRow(); + if (rowState.defaultHeader) { + grid.setDefaultHeaderRow(row); + } + rowState.cells.forEach((columnId, cellState) -> { - row.getCell(getColumn(columnId)).setText(cellState.text); + updateHeaderCellFromState(row.getCell(getColumn(columnId)), + cellState); }); + } + } - if (rowState.defaultHeader) { - grid.setDefaultHeaderRow(row); + private void updateHeaderCellFromState(HeaderCell cell, + CellState cellState) { + switch (cellState.type) { + case TEXT: + cell.setText(cellState.text); + break; + case HTML: + cell.setHtml(cellState.html); + break; + case WIDGET: + ComponentConnector connector = (ComponentConnector) cellState.connector; + if (connector != null) { + cell.setWidget(connector.getWidget()); + } else { + // This happens if you do setVisible(false) on the component on + // the server side + cell.setWidget(null); } + break; + default: + throw new IllegalStateException( + "unexpected cell type: " + cellState.type); } + cell.setStyleName(cellState.styleName); } /** diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index db85ec1fb2..490f9bf8f2 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -54,6 +54,7 @@ import com.vaadin.shared.ui.grid.GridConstants; import com.vaadin.shared.ui.grid.GridConstants.Section; import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.GridStaticCellType; import com.vaadin.shared.ui.grid.HeightMode; import com.vaadin.shared.ui.grid.SectionState; import com.vaadin.shared.util.SharedUtil; @@ -1573,6 +1574,44 @@ public class Grid extends AbstractSingleSelect implements HasComponents { * the header caption to set, not null */ public void setText(String text); + + /** + * Returns the HTML content displayed in this cell. + * + * @return the html + * + */ + public String getHtml(); + + /** + * Sets the HTML content displayed in this cell. + * + * @param html + * the html to set + */ + public void setHtml(String html); + + /** + * Returns the component displayed in this cell. + * + * @return the component + */ + public Component getComponent(); + + /** + * Sets the component displayed in this cell. + * + * @param component + * the component to set + */ + public void setComponent(Component component); + + /** + * Returns the type of content stored in this cell. + * + * @return cell content type + */ + public GridStaticCellType getCellType(); } /** @@ -1628,6 +1667,11 @@ public class Grid extends AbstractSingleSelect implements HasComponents { private class HeaderImpl extends Header { + @Override + protected Grid getGrid() { + return Grid.this; + } + @Override protected SectionState getState(boolean markAsDirty) { return Grid.this.getState(markAsDirty).header; @@ -1641,6 +1685,11 @@ public class Grid extends AbstractSingleSelect implements HasComponents { private class FooterImpl extends Footer { + @Override + protected Grid getGrid() { + return Grid.this; + } + @Override protected SectionState getState(boolean markAsDirty) { return Grid.this.getState(markAsDirty).footer; @@ -1885,7 +1934,18 @@ public class Grid extends AbstractSingleSelect implements HasComponents { @Override public Iterator iterator() { - return Collections.unmodifiableSet(extensionComponents).iterator(); + Set componentSet = new LinkedHashSet<>(extensionComponents); + Header header = getHeader(); + for (int i = 0; i < header.getRowCount(); ++i) { + HeaderRow row = header.getRow(i); + getColumns().forEach(column -> { + HeaderCell cell = row.getCell(column); + if (cell.getCellType() == GridStaticCellType.WIDGET) { + componentSet.add(cell.getComponent()); + } + }); + } + return Collections.unmodifiableSet(componentSet).iterator(); } /** 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 e1c64f31e6..eb02be2bbc 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 @@ -1,12 +1,12 @@ /* * 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 @@ -24,16 +24,19 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import com.vaadin.shared.ui.grid.GridStaticCellType; import com.vaadin.shared.ui.grid.SectionState; import com.vaadin.shared.ui.grid.SectionState.CellState; import com.vaadin.shared.ui.grid.SectionState.RowState; +import com.vaadin.ui.Component; +import com.vaadin.ui.Grid; import com.vaadin.ui.Grid.Column; /** * Represents the header or footer section of a Grid. * * @author Vaadin Ltd. - * + * * @param * the type of the rows in the section * @@ -57,7 +60,7 @@ public abstract class StaticSection> /** * Creates a new row belonging to the given section. - * + * * @param section * the section of the row */ @@ -74,14 +77,14 @@ public abstract class StaticSection> /** * Returns the declarative tag name used for the cells in this row. - * + * * @return the cell tag name */ protected abstract String getCellTagName(); /** * Adds a cell to this section, corresponding to the given column id. - * + * * @param columnId * the id of the column for which to add a cell */ @@ -95,7 +98,7 @@ public abstract class StaticSection> /** * Removes the cell from this section that corresponds to the given * column id. If there is no such cell, does nothing. - * + * * @param columnId * the id of the column from which to remove the cell */ @@ -108,7 +111,7 @@ public abstract class StaticSection> /** * Returns the shared state of this row. - * + * * @return the row state */ protected RowState getRowState() { @@ -122,7 +125,7 @@ public abstract class StaticSection> * @param columnId * the id of the column * @return the cell for the given column - * + * * @throws IllegalArgumentException * if no cell was found for the column id */ @@ -134,6 +137,12 @@ public abstract class StaticSection> } return cell; } + + void detach() { + for (CELL cell : cells.values()) { + cell.detach(); + } + } } /** @@ -167,7 +176,7 @@ public abstract class StaticSection> /** * Returns the shared state of this cell. - * + * * @return the cell state */ protected CellState getCellState() { @@ -182,7 +191,9 @@ public abstract class StaticSection> */ public void setText(String text) { Objects.requireNonNull(text, "text cannot be null"); + removeComponentIfPresent(); cellState.text = text; + cellState.type = GridStaticCellType.TEXT; row.section.markAsDirty(); } @@ -194,20 +205,99 @@ public abstract class StaticSection> public String getText() { return cellState.text; } + + /** + * Returns the HTML content displayed in this cell. + * + * @return the html + * + */ + public String getHtml() { + if (cellState.type != GridStaticCellType.HTML) { + throw new IllegalStateException( + "Cannot fetch HTML from a cell with type " + + cellState.type); + } + return cellState.html; + } + + /** + * Sets the HTML content displayed in this cell. + * + * @param html + * the html to set, not null + */ + public void setHtml(String html) { + Objects.requireNonNull(html, "html cannot be null"); + removeComponentIfPresent(); + cellState.html = html; + cellState.type = GridStaticCellType.HTML; + row.section.markAsDirty(); + } + + /** + * Returns the component displayed in this cell. + * + * @return the component + */ + public Component getComponent() { + if (cellState.type != GridStaticCellType.WIDGET) { + throw new IllegalStateException( + "Cannot fetch Component from a cell with type " + + cellState.type); + } + return (Component) cellState.connector; + } + + /** + * Sets the component displayed in this cell. + * + * @param component + * the component to set, not null + */ + public void setComponent(Component component) { + Objects.requireNonNull(component, "component cannot be null"); + removeComponentIfPresent(); + component.setParent(row.section.getGrid()); + cellState.connector = component; + cellState.type = GridStaticCellType.WIDGET; + row.section.markAsDirty(); + } + + /** + * Returns the type of content stored in this cell. + * + * @return cell content type + */ + public GridStaticCellType getCellType() { + return cellState.type; + } + + private void removeComponentIfPresent() { + Component component = (Component) cellState.connector; + if (component != null) { + component.setParent(null); + cellState.connector = null; + } + } + + void detach() { + removeComponentIfPresent(); + } } private final List rows = new ArrayList<>(); /** * Creates a new row instance. - * + * * @return the new row */ protected abstract ROW createRow(); /** * Returns the shared state of this section. - * + * * @param markAsDirty * {@code true} to mark the state as modified, {@code false} * otherwise @@ -215,6 +305,8 @@ public abstract class StaticSection> */ protected abstract SectionState getState(boolean markAsDirty); + protected abstract Grid getGrid(); + protected abstract Collection> getColumns(); /** @@ -226,7 +318,7 @@ public abstract class StaticSection> /** * Adds a new row at the given index. - * + * * @param index * the index of the new row * @return the added row @@ -245,20 +337,21 @@ public abstract class StaticSection> /** * Removes the row at the given index. - * + * * @param index * the index of the row to remove * @throws IndexOutOfBoundsException * if {@code index < 0 || index >= getRowCount()} */ public void removeRow(int index) { - rows.remove(index); + ROW row = rows.remove(index); + row.detach(); getState(true).rows.remove(index); } /** * Removes the given row from this section. - * + * * @param row * the row to remove, not null * @throws IllegalArgumentException @@ -276,7 +369,7 @@ public abstract class StaticSection> /** * Returns the row at the given index. - * + * * @param index * the index of the row * @return the row at the index @@ -322,7 +415,7 @@ public abstract class StaticSection> /** * Returns an unmodifiable list of the rows in this section. - * + * * @return the rows in this section */ protected List getRows() { 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 55a8df99b8..39d2d2f2e9 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 @@ -21,6 +21,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import com.vaadin.shared.Connector; + /** * Shared state for Grid headers and footers. * @@ -45,9 +47,23 @@ public class SectionState implements Serializable { /** The state of a header or footer cell. */ public static class CellState implements Serializable { + public GridStaticCellType type = GridStaticCellType.TEXT; + + /** The style name for this cell. Null if none. */ + public String styleName = null; + /** The textual caption of this cell. */ public String text; + /** The html content of this cell. */ + public String html; + + /** + * The connector for the component that is set to be displayed in this + * cell. Null if none. + */ + public Connector connector = null; + /** The id of the column that this cell belongs to. */ public String columnId; } 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 f56767e3d3..4e9b154628 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 @@ -14,7 +14,7 @@ import com.vaadin.annotations.Widgetset; import com.vaadin.server.VaadinRequest; import com.vaadin.shared.Registration; import com.vaadin.shared.ui.grid.HeightMode; -import com.vaadin.tests.components.AbstractReindeerTestUIWithLog; +import com.vaadin.tests.components.AbstractTestUIWithLog; import com.vaadin.ui.Button; import com.vaadin.ui.Component; import com.vaadin.ui.Grid; @@ -36,7 +36,7 @@ import com.vaadin.ui.renderers.NumberRenderer; import com.vaadin.ui.renderers.ProgressBarRenderer; @Widgetset("com.vaadin.DefaultWidgetSet") -public class GridBasics extends AbstractReindeerTestUIWithLog { +public class GridBasics extends AbstractTestUIWithLog { public static final String ROW_STYLE_GENERATOR_ROW_NUMBERS_FOR_3_OF_4 = "Row numbers for 3/4"; public static final String ROW_STYLE_GENERATOR_NONE = "None"; @@ -229,6 +229,21 @@ public class GridBasics extends AbstractReindeerTestUIWithLog { .toArray(new Column[columnOrder.size()])); } }); + + MenuItem headerTypeMenu = columnMenu.addItem("Header Type", null); + headerTypeMenu.addItem("Text Header", selectedItem -> grid + .getDefaultHeaderRow().getCell(col).setText("Text Header")); + headerTypeMenu + .addItem("HTML Header", + selectedItem -> grid.getDefaultHeaderRow() + .getCell(col) + .setHtml("HTML Header")); + headerTypeMenu.addItem("Widget Header", selectedItem -> { + final Button button = new Button("Button Header"); + button.addClickListener(clickEvent -> log("Button clicked!")); + grid.getDefaultHeaderRow().getCell(col).setComponent(button); + }); + columnMenu .addItem("Sortable", selectedItem -> col @@ -278,12 +293,15 @@ public class GridBasics extends AbstractReindeerTestUIWithLog { : null)) .setCheckable(true); stateMenu - .addItem("Cell description generator", item -> grid.getColumns() - .stream().findFirst() - .ifPresent(c -> c.setDescriptionGenerator( - item.isChecked() ? t -> "Cell tooltip for row " - + t.getRowNumber() + ", Column 0" - : null))) + .addItem("Cell description generator", + item -> grid.getColumns().stream().findFirst() + .ifPresent( + c -> c.setDescriptionGenerator( + item.isChecked() + ? t -> "Cell tooltip for row " + + t.getRowNumber() + + ", Column 0" + : null))) .setCheckable(true); stateMenu.addItem("Item click listener", new Command() { @@ -414,9 +432,10 @@ public class GridBasics extends AbstractReindeerTestUIWithLog { private void createFooterMenu(MenuItem footerMenu) { footerMenu.addItem("Add default footer row", menuItem -> { FooterRow defaultFooter = grid.appendFooterRow(); - grid.getColumns().forEach( - column -> defaultFooter.getCell(column).setText(grid - .getDefaultHeaderRow().getCell(column).getText())); + grid.getColumns() + .forEach(column -> defaultFooter.getCell(column) + .setText(grid.getDefaultHeaderRow().getCell(column) + .getText())); footerMenu.removeChild(menuItem); }); footerMenu.addItem("Append footer row", menuItem -> { diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java index eed08032de..c128ba2634 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java @@ -165,4 +165,43 @@ public abstract class GridBasicsTest extends MultiBrowserTest { getGridElement().getCell(row, column).click(); } + protected WebElement getSidebarPopup() { + List elements = findElements( + By.className("v-grid-sidebar-popup")); + if (elements.isEmpty()) { + getSidebarOpenButton().click(); + elements = findElements(By.className("v-grid-sidebar-popup")); + } + return elements.isEmpty() ? null : elements.get(0); + } + + protected WebElement getSidebarPopupIfPresent() { + List elements = findElements( + By.className("v-grid-sidebar-popup")); + return elements.isEmpty() ? null : elements.get(0); + } + + protected WebElement getSidebarOpenButton() { + List elements = findElements( + By.className("v-grid-sidebar-button")); + return elements.isEmpty() ? null : elements.get(0); + } + + /** + * Returns the toggle inside the sidebar for hiding the column at the given + * index, or null if not found. + */ + protected WebElement getColumnHidingToggle(int columnIndex) { + WebElement sidebar = getSidebarPopup(); + List elements = sidebar + .findElements(By.className("column-hiding-toggle")); + for (WebElement e : elements) { + if ((e.getText().toLowerCase()) + .startsWith("column " + columnIndex)) { + return e; + } + } + return null; + } + } 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 681da7443f..2d0be2a733 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 @@ -26,6 +26,7 @@ import org.junit.Test; import com.vaadin.testbench.By; import com.vaadin.testbench.elements.GridElement.GridCellElement; +import com.vaadin.testbench.elements.NotificationElement; public class GridHeaderFooterTest extends GridBasicsTest { @@ -80,7 +81,7 @@ public class GridHeaderFooterTest extends GridBasicsTest { selectMenuPath("Component", "Header", "Prepend header row"); selectMenuPath("Component", "Header", "Set first row as default"); - assertHeaderTexts(0, GridBasics.COLUMN_CAPTIONS); + assertHeaderTexts(0, HEADER_TEXTS); } @Test @@ -137,6 +138,75 @@ public class GridHeaderFooterTest extends GridBasicsTest { assertFooterTexts(1, GridBasics.COLUMN_CAPTIONS); } + public void testDynamicallyChangingCellType() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Columns", "Column 0", "Header Type", + "Widget Header"); + GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0); + assertTrue(widgetCell.isElementPresent(By.className("v-button"))); + + selectMenuPath("Component", "Columns", "Column 1", "Header Type", + "HTML Header"); + GridCellElement htmlCell = getGridElement().getHeaderCell(0, 1); + assertEquals("HTML Header", + htmlCell.findElement( + By.className("v-grid-column-header-content")) + .getAttribute("innerHTML")); + + selectMenuPath("Component", "Columns", "Column 2", "Header Type", + "Text Header"); + GridCellElement textCell = getGridElement().getHeaderCell(0, 2); + + assertEquals("text header", textCell.getText().toLowerCase()); + } + + @Test + public void testButtonInHeader() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Columns", "Column 1", "Header Type", + "Widget Header"); + + getGridElement().findElements(By.className("v-button")).get(0).click(); + + assertTrue("Button click should be logged", + logContainsText("Button clicked!")); + } + + @Test + public void testRemoveComponentFromHeader() throws Exception { + openTestURL(); + selectMenuPath("Component", "Columns", "Column 1", "Header Type", + "Widget Header"); + selectMenuPath("Component", "Columns", "Column 1", "Header Type", + "Text Header"); + assertTrue("No notifications should've been shown", + !$(NotificationElement.class).exists()); + assertEquals("Header should've been reverted back to text header", + "text header", + getGridElement().getHeaderCell(0, 1).getText().toLowerCase()); + } + + @Test + public void testColumnHidingToggleCaption_settingWidgetToHeader_toggleCaptionStays() { + toggleColumnHidable(1); + getSidebarOpenButton().click(); + assertEquals("column 1", + getGridElement().getHeaderCell(0, 1).getText().toLowerCase()); + assertEquals("Column 1", getColumnHidingToggle(1).getText()); + + selectMenuPath("Component", "Columns", "Column 1", "Header Type", + "Widget Header"); + + getSidebarOpenButton().click(); + assertEquals("Column 1", getColumnHidingToggle(1).getText()); + } + + private void toggleColumnHidable(int index) { + selectMenuPath("Component", "Columns", "Column " + index, "Hidable"); + } + protected static void assertText(String expected, GridCellElement e) { // TBE.getText returns "" if the element is scrolled out of view String actual = e.findElement(By.tagName("div")) -- 2.39.5