blob: 7164952b5b812cd925e721ba79d378a96e1c76eb [file] [log] [blame]
/*
* 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 = getColumnCount();
tableHeader.hideUnusedColumns(columnCount);
tableData.hideUnusedColumns(columnCount);
tableFooter.hideUnusedColumns(columnCount);
}
}