/* * Copyright 2011 Google Inc. * * 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.google.gwt.user.cellview.client; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Overflow; import com.google.gwt.dom.client.Style.TableLayout; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableColElement; import com.google.gwt.dom.client.TableElement; import com.google.gwt.dom.client.TableSectionElement; import com.google.gwt.event.dom.client.ScrollEvent; import com.google.gwt.event.dom.client.ScrollHandler; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.CssResource.ImportedWithPrefix; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.resources.client.ImageResource.ImageOptions; import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState; import com.google.gwt.user.client.ui.CustomScrollPanel; import com.google.gwt.user.client.ui.FlexTable; import com.google.gwt.user.client.ui.HeaderPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.RequiresResize; import com.google.gwt.user.client.ui.ScrollPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; import com.google.gwt.view.client.ProvidesKey; /** * A tabular view with a fixed header and footer section and a scrollable data * section in the middle. This widget supports paging and columns. * * <p> * <h3>Columns</h3> The {@link Column} class defines the * {@link com.google.gwt.cell.client.Cell} used to render a column. Implement * {@link Column#getValue(Object)} to retrieve the field value from the row * object that will be rendered in the {@link com.google.gwt.cell.client.Cell}. * </p> * * <p> * <h3>Headers and Footers</h3> A {@link Header} can be placed at the top * (header) or bottom (footer) of the {@link DataGrid}. You can specify a header * as text using {@link #addColumn(Column, String)}, or you can create a custom * {@link Header} that can change with the value of the cells, such as a column * total. The {@link Header} will be rendered every time the row data changes or * the table is redrawn. If you pass the same header instance (==) into adjacent * columns, the header will span the columns. * </p> * * <p> * <h3>Examples</h3> * <dl> * <dt>Trivial example</dt> * <dd>{@example com.google.gwt.examples.cellview.CellTableExample}</dd> * <dt>FieldUpdater example</dt> * <dd>{@example com.google.gwt.examples.cellview.CellTableFieldUpdaterExample}</dd> * <dt>Key provider example</dt> * <dd>{@example com.google.gwt.examples.view.KeyProviderExample}</dd> * </dl> * </p> * * @param <T> the data type of each row */ public class DataGrid<T> extends AbstractCellTable<T> implements RequiresResize { /** * A ClientBundle that provides images for this widget. */ public interface Resources extends ClientBundle { /** * The loading indicator used while the table is waiting for data. */ @Source("cellTableLoading.gif") @ImageOptions(flipRtl = true) ImageResource dataGridLoading(); /** * Icon used when a column is sorted in ascending order. */ @Source("sortAscending.png") @ImageOptions(flipRtl = true) ImageResource dataGridSortAscending(); /** * Icon used when a column is sorted in descending order. */ @Source("sortDescending.png") @ImageOptions(flipRtl = true) ImageResource dataGridSortDescending(); /** * The styles used in this widget. */ @Source(Style.DEFAULT_CSS) Style dataGridStyle(); } /** * Styles used by this widget. */ @ImportedWithPrefix("gwt-CellTable") public interface Style extends CssResource { /** * The path to the default CSS styles used by this resource. */ String DEFAULT_CSS = "com/google/gwt/user/cellview/client/DataGrid.css"; /** * Applied to every cell. */ String dataGridCell(); /** * Applied to even rows. */ String dataGridEvenRow(); /** * Applied to cells in even rows. */ String dataGridEvenRowCell(); /** * Applied to the first column. */ String dataGridFirstColumn(); /** * Applied to the first column footers. */ String dataGridFirstColumnFooter(); /** * Applied to the first column headers. */ String dataGridFirstColumnHeader(); /** * Applied to footers cells. */ String dataGridFooter(); /** * Applied to headers cells. */ String dataGridHeader(); /** * Applied to the hovered row. */ String dataGridHoveredRow(); /** * Applied to the cells in the hovered row. */ String dataGridHoveredRowCell(); /** * Applied to the keyboard selected cell. */ String dataGridKeyboardSelectedCell(); /** * Applied to the keyboard selected row. */ String dataGridKeyboardSelectedRow(); /** * Applied to the cells in the keyboard selected row. */ String dataGridKeyboardSelectedRowCell(); /** * Applied to the last column. */ String dataGridLastColumn(); /** * Applied to the last column footers. */ String dataGridLastColumnFooter(); /** * Applied to the last column headers. */ String dataGridLastColumnHeader(); /** * Applied to odd rows. */ String dataGridOddRow(); /** * Applied to cells in odd rows. */ String dataGridOddRowCell(); /** * Applied to selected rows. */ String dataGridSelectedRow(); /** * Applied to cells in selected rows. */ String dataGridSelectedRowCell(); /** * Applied to header cells that are sortable. */ String dataGridSortableHeader(); /** * Applied to header cells that are sorted in ascending order. */ String dataGridSortedHeaderAscending(); /** * Applied to header cells that are sorted in descending order. */ String dataGridSortedHeaderDescending(); /** * Applied to the table. */ String dataGridWidget(); } /** * A simple widget wrapper around a table element. */ static class TableWidget extends Widget { private final TableColElement colgroup; private TableSectionElement section; private final TableElement tableElem; public TableWidget() { // Setup the table. tableElem = Document.get().createTableElement(); tableElem.setCellSpacing(0); tableElem.getStyle().setTableLayout(TableLayout.FIXED); tableElem.getStyle().setWidth(100.0, Unit.PCT); setElement(tableElem); // Add the colgroup. colgroup = Document.get().createColGroupElement(); tableElem.appendChild(colgroup); } public void addColumnStyleName(int index, String styleName) { ensureTableColElement(index).addClassName(styleName); } /** * Get the {@link TableColElement} at the specified index, creating it if * necessary. * * @param index the column index * @return the {@link TableColElement} */ public TableColElement ensureTableColElement(int index) { // Ensure that we have enough columns. for (int i = colgroup.getChildCount(); i <= index; i++) { colgroup.appendChild(Document.get().createColElement()); } return colgroup.getChild(index).cast(); } public void removeColumnStyleName(int index, String styleName) { if (index >= colgroup.getChildCount()) { return; } ensureTableColElement(index).removeClassName(styleName); } /** * Hide columns that aren't used in the table. * * @param start the first unused column index */ void hideUnusedColumns(int start) { /* * Set the width to zero for all col elements that appear after the last * column. Clearing the width would cause it to take up the remaining * width in a fixed layout table. */ int colCount = colgroup.getChildCount(); for (int i = start; i < colCount; i++) { setColumnWidth(i, "0px"); } } void setColumnWidth(int column, String width) { if (width == null) { ensureTableColElement(column).getStyle().clearWidth(); } else { ensureTableColElement(column).getStyle().setProperty("width", width); } } } /** * Adapter class to convert {@link Resources} to * {@link AbstractCellTable.Resources}. */ private static class ResourcesAdapter implements AbstractCellTable.Resources { private final DataGrid.Resources resources; private final StyleAdapter style; public ResourcesAdapter(DataGrid.Resources resources) { this.resources = resources; this.style = new StyleAdapter(resources.dataGridStyle()); } @Override public ImageResource sortAscending() { return resources.dataGridSortAscending(); } @Override public ImageResource sortDescending() { return resources.dataGridSortDescending(); } @Override public AbstractCellTable.Style style() { return style; } } /** * Adapter class to convert {@link Style} to {@link AbstractCellTable.Style}. */ private static class StyleAdapter implements AbstractCellTable.Style { private final DataGrid.Style style; public StyleAdapter(DataGrid.Style style) { this.style = style; } @Override public String cell() { return style.dataGridCell(); } @Override public String evenRow() { return style.dataGridEvenRow(); } @Override public String evenRowCell() { return style.dataGridEvenRowCell(); } @Override public String firstColumn() { return style.dataGridFirstColumn(); } @Override public String firstColumnFooter() { return style.dataGridFirstColumnFooter(); } @Override public String firstColumnHeader() { return style.dataGridFirstColumnHeader(); } @Override public String footer() { return style.dataGridFooter(); } @Override public String header() { return style.dataGridHeader(); } @Override public String hoveredRow() { return style.dataGridHoveredRow(); } @Override public String hoveredRowCell() { return style.dataGridHoveredRowCell(); } @Override public String keyboardSelectedCell() { return style.dataGridKeyboardSelectedCell(); } @Override public String keyboardSelectedRow() { return style.dataGridKeyboardSelectedRow(); } @Override public String keyboardSelectedRowCell() { return style.dataGridKeyboardSelectedRowCell(); } @Override public String lastColumn() { return style.dataGridLastColumn(); } @Override public String lastColumnFooter() { return style.dataGridLastColumnFooter(); } @Override public String lastColumnHeader() { return style.dataGridLastColumnHeader(); } @Override public String oddRow() { return style.dataGridOddRow(); } @Override public String oddRowCell() { return style.dataGridOddRowCell(); } @Override public String selectedRow() { return style.dataGridSelectedRow(); } @Override public String selectedRowCell() { return style.dataGridSelectedRowCell(); } @Override public String sortableHeader() { return style.dataGridSortableHeader(); } @Override public String sortedHeaderAscending() { return style.dataGridSortedHeaderAscending(); } @Override public String sortedHeaderDescending() { return style.dataGridSortedHeaderDescending(); } @Override public String widget() { return style.dataGridWidget(); } } private static final int DEFAULT_PAGESIZE = 50; private static Resources DEFAULT_RESOURCES; /** * Create the default loading indicator using the loading image in the * specified {@link Resources}. * * @param resources the resources containing the loading image * @return a widget loading indicator */ private static Widget createDefaultLoadingIndicator(Resources resources) { ImageResource loadingImg = resources.dataGridLoading(); if (loadingImg == null) { return null; } Image image = new Image(loadingImg); image.getElement().getStyle().setMarginTop(30.0, Unit.PX); return image; } private static Resources getDefaultResources() { if (DEFAULT_RESOURCES == null) { DEFAULT_RESOURCES = GWT.create(Resources.class); } return DEFAULT_RESOURCES; } final TableWidget tableData; final TableWidget tableFooter; final TableWidget tableHeader; private final FlexTable emptyTableWidgetContainer; private final HeaderPanel headerPanel; private final FlexTable loadingIndicatorContainer; private final Style style; private final Element tableDataContainer; private final ScrollPanel tableDataScroller; private final SimplePanel tableFooterContainer; private final Element tableFooterScroller; private final SimplePanel tableHeaderContainer; private final Element tableHeaderScroller; /** * Constructs a table with a default page size of 50. */ public DataGrid() { this(DEFAULT_PAGESIZE); } /** * Constructs a table with the given page size. * * @param pageSize the page size */ public DataGrid(final int pageSize) { this(pageSize, getDefaultResources()); } /** * Constructs a table with the given page size and the given * {@link ProvidesKey key provider}. * * @param pageSize the page size * @param keyProvider an instance of ProvidesKey<T>, or null if the record * object should act as its own key */ public DataGrid(int pageSize, ProvidesKey<T> keyProvider) { this(pageSize, getDefaultResources(), keyProvider); } /** * Constructs a table with the given page size with the specified * {@link Resources}. * * @param pageSize the page size * @param resources the resources to use for this widget */ public DataGrid(int pageSize, Resources resources) { this(pageSize, resources, null); } /** * Constructs a table with the given page size, the specified * {@link Resources}, and the given key provider. * * @param pageSize the page size * @param resources the resources to use for this widget * @param keyProvider an instance of ProvidesKey<T>, or null if the record * object should act as its own key */ public DataGrid(int pageSize, Resources resources, ProvidesKey<T> keyProvider) { this(pageSize, resources, keyProvider, createDefaultLoadingIndicator(resources)); } /** * Constructs a table with the given page size, the specified * {@link Resources}, and the given key provider. * * @param pageSize the page size * @param resources the resources to use for this widget * @param keyProvider an instance of ProvidesKey<T>, or null if the record * object should act as its own key * @param loadingIndicator the widget to use as a loading indicator, or null * to disable */ public DataGrid(int pageSize, Resources resources, ProvidesKey<T> keyProvider, Widget loadingIndicator) { super(new HeaderPanel(), pageSize, new ResourcesAdapter(resources), keyProvider); headerPanel = (HeaderPanel) getWidget(); // Inject the stylesheet. this.style = resources.dataGridStyle(); this.style.ensureInjected(); // Create the header and footer widgets.. tableHeader = new TableWidget(); tableHeader.section = tableHeader.tableElem.createTHead(); tableFooter = new TableWidget(); tableFooter.section = tableFooter.tableElem.createTFoot(); /* * Wrap the header and footer widgets in a div because we cannot set the * min-width of a table element. We set the width/min-width of the div * container instead. */ tableHeaderContainer = new SimplePanel(tableHeader); tableFooterContainer = new SimplePanel(tableFooter); /* * Get the element that wraps the container so we can adjust its scroll * position. */ headerPanel.setHeaderWidget(tableHeaderContainer); tableHeaderScroller = tableHeaderContainer.getElement().getParentElement(); headerPanel.setFooterWidget(tableFooterContainer); tableFooterScroller = tableFooterContainer.getElement().getParentElement(); /* * Set overflow to hidden on the scrollable elements so we can change the * scrollLeft position. */ tableHeaderScroller.getStyle().setOverflow(Overflow.HIDDEN); tableFooterScroller.getStyle().setOverflow(Overflow.HIDDEN); // Create the body. tableData = new TableWidget(); if (tableData.tableElem.getTBodies().getLength() > 0) { tableData.section = tableData.tableElem.getTBodies().getItem(0); } else { tableData.section = Document.get().createTBodyElement(); tableData.tableElem.appendChild(tableData.section); } tableDataScroller = new CustomScrollPanel(tableData); tableDataScroller.setHeight("100%"); headerPanel.setContentWidget(tableDataScroller); tableDataContainer = tableData.getElement().getParentElement(); /* * CustomScrollPanel applies the inline block style to the container * element, but we want the container to fill the available width. */ tableDataContainer.getStyle().setDisplay(Display.BLOCK); /* * Create the containers for the empty table message and loading indicator. * The containers are centered tables that contain one cell, which aligns * the widget in the center of the panel. */ emptyTableWidgetContainer = new FlexTable(); emptyTableWidgetContainer.getElement().setAttribute("align", "center"); loadingIndicatorContainer = new FlexTable(); loadingIndicatorContainer.getElement().setAttribute("align", "center"); // Set the loading indicator. setLoadingIndicator(loadingIndicator); // Can be null. // Synchronize the scroll positions of the three tables. tableDataScroller.addScrollHandler(new ScrollHandler() { @Override public void onScroll(ScrollEvent event) { int scrollLeft = tableDataScroller.getHorizontalScrollPosition(); tableHeaderScroller.setScrollLeft(scrollLeft); tableFooterScroller.setScrollLeft(scrollLeft); } }); } /** * Constructs a table with a default page size of 50, and the given * {@link ProvidesKey key provider}. * * @param keyProvider an instance of ProvidesKey<T>, or null if the record * object should act as its own key */ public DataGrid(ProvidesKey<T> keyProvider) { this(DEFAULT_PAGESIZE, keyProvider); } @Override public void addColumnStyleName(int index, String styleName) { tableHeader.addColumnStyleName(index, styleName); tableFooter.addColumnStyleName(index, styleName); tableData.addColumnStyleName(index, styleName); } /** * Clear the width of the tables in this widget, which causes them to fill the * available width. * * <p> * The table width is not the same as the width of this widget. If the tables * are narrower than this widget, there will be a gap between the table and * the edge of the widget. If the tables are wider than this widget, a * horizontal scrollbar will appear so the user can scroll horizontally. * </p> * * @see #setMinimumTableWidth(double, Unit) * @see #setTableWidth(double, Unit) */ public void clearTableWidth() { tableHeaderContainer.getElement().getStyle().clearWidth(); tableFooterContainer.getElement().getStyle().clearWidth(); tableDataContainer.getStyle().clearWidth(); } @Override public void onResize() { headerPanel.onResize(); } @Override public void removeColumnStyleName(int index, String styleName) { tableHeader.removeColumnStyleName(index, styleName); tableFooter.removeColumnStyleName(index, styleName); tableData.removeColumnStyleName(index, styleName); } @Override public void setEmptyTableWidget(Widget widget) { emptyTableWidgetContainer.setWidget(0, 0, widget); super.setEmptyTableWidget(widget); } @Override public void setLoadingIndicator(Widget widget) { loadingIndicatorContainer.setWidget(0, 0, widget); super.setLoadingIndicator(widget); } /** * Set the minimum width of the tables in this widget. If the widget become * narrower than the minimum width, a horizontal scrollbar will appear so the * user can scroll horizontally. * * <p> * Note that this method is not supported in IE6 and earlier versions of IE. * </p> * * @param value the width * @param unit the unit of the width * @see #setTableWidth(double, Unit) */ public void setMinimumTableWidth(double value, Unit unit) { /* * The min-width style attribute doesn't apply to tables, so we set the * min-width of the element that contains the table instead. The table width * is fixed at 100%. */ tableHeaderContainer.getElement().getStyle().setProperty("minWidth", value, unit); tableFooterContainer.getElement().getStyle().setProperty("minWidth", value, unit); tableDataContainer.getStyle().setProperty("minWidth", value, unit); } /** * Set the width of the tables in this widget. By default, the width is not * set and the tables take the available width. * * <p> * The table width is not the same as the width of this widget. If the tables * are narrower than this widget, there will be a gap between the table and * the edge of the widget. If the tables are wider than this widget, a * horizontal scrollbar will appear so the user can scroll horizontally. * </p> * * <p> * If your table has many columns and you want to ensure that the columns are * not truncated, you probably want to use * {@link #setMinimumTableWidth(double, Unit)} instead. That will ensure that * the table is wide enough, but it will still allow the table to expand to * 100% if the user has a wide screen. * </p> * * <p> * Note that setting the width in percentages will not work on older versions * of IE because it does not account for scrollbars when calculating the * width. * </p> * * @param value the width * @param unit the unit of the width * @see #setMinimumTableWidth(double, Unit) */ public void setTableWidth(double value, Unit unit) { /* * The min-width style attribute doesn't apply to tables, so we set the * min-width of the element that contains the table instead. For * consistency, we set the width of the container as well. */ tableHeaderContainer.getElement().getStyle().setWidth(value, unit); tableFooterContainer.getElement().getStyle().setWidth(value, unit); tableDataContainer.getStyle().setWidth(value, unit); } @Override protected void doSetColumnWidth(int column, String width) { if (width == null) { tableData.ensureTableColElement(column).getStyle().clearWidth(); tableHeader.ensureTableColElement(column).getStyle().clearWidth(); tableFooter.ensureTableColElement(column).getStyle().clearWidth(); } else { tableData.ensureTableColElement(column).getStyle().setProperty("width", width); tableHeader.ensureTableColElement(column).getStyle().setProperty("width", width); tableFooter.ensureTableColElement(column).getStyle().setProperty("width", width); } } @Override protected void doSetHeaderVisible(boolean isFooter, boolean isVisible) { if (isFooter) { headerPanel.setFooterWidget(isVisible ? tableFooterContainer : null); } else { headerPanel.setHeaderWidget(isVisible ? tableHeaderContainer : null); } } @Override protected TableSectionElement getTableBodyElement() { return tableData.section; } @Override protected TableSectionElement getTableFootElement() { return tableFooter.section; } @Override protected TableSectionElement getTableHeadElement() { return tableHeader.section; } /** * Called when the loading state changes. * * @param state the new loading state */ @Override protected void onLoadingStateChanged(LoadingState state) { Widget message = tableData; if (state == LoadingState.LOADING) { // Loading indicator. message = loadingIndicatorContainer; } else if (state == LoadingState.LOADED && getPresenter().isEmpty()) { // Empty table. message = emptyTableWidgetContainer; } // Switch out the message to display. tableDataScroller.setWidget(message); // Fire an event. super.onLoadingStateChanged(state); } @Override protected void refreshColumnWidths() { super.refreshColumnWidths(); // Hide unused col elements in the colgroup. int columnCount = getRealColumnCount(); tableHeader.hideUnusedColumns(columnCount); tableData.hideUnusedColumns(columnCount); tableFooter.hideUnusedColumns(columnCount); } }