com.vaadin.client.connectors.grid.GridConnector Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaadin-client Show documentation
Show all versions of vaadin-client Show documentation
Vaadin is a web application framework for Rich Internet Applications (RIA).
Vaadin enables easy development and maintenance of fast and
secure rich web
applications with a stunning look and feel and a wide browser support.
It features a server-side architecture with the majority of the logic
running
on the server. Ajax technology is used at the browser-side to ensure a
rich
and interactive user experience.
/*
* 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
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.connectors.grid;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
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.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
import com.vaadin.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler;
import com.vaadin.client.DeferredWorker;
import com.vaadin.client.HasComponentsConnector;
import com.vaadin.client.MouseEventDetailsBuilder;
import com.vaadin.client.TooltipInfo;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.annotations.OnStateChange;
import com.vaadin.client.connectors.AbstractListingConnector;
import com.vaadin.client.connectors.grid.ColumnConnector.CustomColumn;
import com.vaadin.client.data.DataSource;
import com.vaadin.client.ui.SimpleManagedLayout;
import com.vaadin.client.widget.grid.CellReference;
import com.vaadin.client.widget.grid.EventCellReference;
import com.vaadin.client.widget.grid.events.BodyClickHandler;
import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler;
import com.vaadin.client.widget.grid.events.GridClickEvent;
import com.vaadin.client.widget.grid.events.GridDoubleClickEvent;
import com.vaadin.client.widget.grid.sort.SortEvent;
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.HeaderRow;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.grid.GridClientRpc;
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.ScrollDestination;
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;
/**
* A connector class for the typed Grid component.
*
* @author Vaadin Ltd
* @since 8.0
*/
@Connect(com.vaadin.ui.Grid.class)
public class GridConnector extends AbstractListingConnector
implements HasComponentsConnector, SimpleManagedLayout, DeferredWorker {
private Set refreshDetailsCallbacks = new HashSet<>();
private class ItemClickHandler
implements BodyClickHandler, BodyDoubleClickHandler {
@Override
public void onClick(GridClickEvent event) {
if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
fireItemClick(event.getTargetCell(), event.getNativeEvent());
}
}
@Override
public void onDoubleClick(GridDoubleClickEvent event) {
if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
fireItemClick(event.getTargetCell(), event.getNativeEvent());
}
}
private void fireItemClick(CellReference> cell,
NativeEvent mouseEvent) {
String rowKey = getRowKey((JsonObject) cell.getRow());
String columnId = columnToIdMap.get(cell.getColumn());
getRpcProxy(GridServerRpc.class).itemClick(rowKey, columnId,
MouseEventDetailsBuilder
.buildMouseEventDetails(mouseEvent));
}
}
/* Map to keep track of all added columns */
private Map columnToIdMap = new HashMap<>();
private Map idToColumn = new HashMap<>();
/* Child component list for HasComponentsConnector */
private List childComponents;
private ItemClickHandler itemClickHandler = new ItemClickHandler();
/**
* Gets the string identifier of the given column in this grid.
*
* @param column
* the column whose id to get
* @return the string id of the column
*/
public String getColumnId(Column, ?> column) {
return columnToIdMap.get(column);
}
/**
* Gets the column corresponding to the given string identifier.
*
* @param columnId
* the id of the column to get
* @return the column with the given id
*/
public CustomColumn getColumn(String columnId) {
return idToColumn.get(columnId);
}
@Override
@SuppressWarnings("unchecked")
public Grid getWidget() {
return (Grid) super.getWidget();
}
/**
* Method called for a row details refresh. Runs all callbacks if any
* details were shown and clears the callbacks.
*
* @param detailsShown
* True if any details were set visible
*/
protected void detailsRefreshed(boolean detailsShown) {
if (detailsShown) {
refreshDetailsCallbacks.forEach(Runnable::run);
}
refreshDetailsCallbacks.clear();
}
/**
* Method target for when one single details has been updated and we might
* need to scroll it into view.
*
* @param rowIndex
* index of updated row
*/
protected void singleDetailsOpened(int rowIndex) {
addDetailsRefreshCallback(() -> {
if (rowHasDetails(rowIndex)) {
getWidget().scrollToRow(rowIndex);
}
});
}
/**
* Add a single use details runnable callback for when we get a call to
* {@link #detailsRefreshed(boolean)}.
*
* @param refreshCallback
* Details refreshed callback
*/
private void addDetailsRefreshCallback(Runnable refreshCallback) {
refreshDetailsCallbacks.add(refreshCallback);
}
/**
* Check if we have details for given row.
*
* @param rowIndex
* @return
*/
private boolean rowHasDetails(int rowIndex) {
JsonObject row = getWidget().getDataSource().getRow(rowIndex);
return row != null && row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
&& !row.getString(GridState.JSONKEY_DETAILS_VISIBLE).isEmpty();
}
@Override
protected void init() {
super.init();
registerRpc(GridClientRpc.class, new GridClientRpc() {
@Override
public void scrollToRow(int row, ScrollDestination destination) {
Scheduler.get().scheduleFinally(
() -> getWidget().scrollToRow(row, destination));
// Add details refresh listener and handle possible detail for
// scrolled row.
addDetailsRefreshCallback(() -> {
if (rowHasDetails(row)) {
getWidget().scrollToRow(row, destination);
}
});
}
@Override
public void scrollToStart() {
Scheduler.get()
.scheduleFinally(() -> getWidget().scrollToStart());
}
@Override
public void scrollToEnd() {
Scheduler.get()
.scheduleFinally(() -> getWidget().scrollToEnd());
addDetailsRefreshCallback(() -> {
if (rowHasDetails(getWidget().getDataSource().size() - 1)) {
getWidget().scrollToEnd();
}
});
}
});
getWidget().addSortHandler(this::handleSortEvent);
getWidget().setRowStyleGenerator(rowRef -> {
JsonObject json = rowRef.getRow();
return json.hasKey(GridState.JSONKEY_ROWSTYLE)
? json.getString(GridState.JSONKEY_ROWSTYLE) : null;
});
getWidget().setCellStyleGenerator(cellRef -> {
JsonObject row = cellRef.getRow();
if (!row.hasKey(GridState.JSONKEY_CELLSTYLES)) {
return null;
}
Column, JsonObject> column = cellRef.getColumn();
if (column instanceof CustomColumn) {
String id = ((CustomColumn) column).getConnectorId();
JsonObject cellStyles = row
.getObject(GridState.JSONKEY_CELLSTYLES);
if (cellStyles.hasKey(id)) {
return cellStyles.getString(id);
}
}
return null;
});
getWidget().addColumnVisibilityChangeHandler(event -> {
if (event.isUserOriginated()) {
getRpcProxy(GridServerRpc.class).columnVisibilityChanged(
getColumnId(event.getColumn()), event.isHidden());
}
});
getWidget().addColumnReorderHandler(event -> {
if (event.isUserOriginated()) {
List newColumnOrder = mapColumnsToIds(
event.getNewColumnOrder());
List oldColumnOrder = mapColumnsToIds(
event.getOldColumnOrder());
getRpcProxy(GridServerRpc.class)
.columnsReordered(newColumnOrder, oldColumnOrder);
}
});
getWidget().addColumnResizeHandler(event -> {
Column, JsonObject> column = event.getColumn();
getRpcProxy(GridServerRpc.class).columnResized(getColumnId(column),
column.getWidthActual());
});
/* Item click events */
getWidget().addBodyClickHandler(itemClickHandler);
getWidget().addBodyDoubleClickHandler(itemClickHandler);
layout();
}
@SuppressWarnings("unchecked")
@OnStateChange("columnOrder")
void updateColumnOrder() {
Scheduler.get()
.scheduleFinally(() -> getWidget().setColumnOrder(
getState().columnOrder.stream().map(this::getColumn)
.toArray(size -> new Column[size])));
}
@OnStateChange("columnResizeMode")
void updateColumnResizeMode() {
getWidget().setColumnResizeMode(getState().columnResizeMode);
}
/**
* Updates the grid header section on state change.
*/
@OnStateChange("header")
void updateHeader() {
Scheduler.get().scheduleFinally(() -> {
final Grid grid = getWidget();
final SectionState state = getState().header;
while (grid.getHeaderRowCount() > 0) {
grid.removeHeaderRow(0);
}
for (RowState rowState : state.rows) {
HeaderRow row = grid.appendHeaderRow();
if (rowState.defaultHeader) {
grid.setDefaultHeaderRow(row);
}
updateStaticRow(rowState, row);
}
});
}
private void updateStaticRow(RowState rowState,
Grid.StaticSection.StaticRow row) {
rowState.cells.forEach((columnId, cellState) -> {
updateStaticCellFromState(row.getCell(getColumn(columnId)),
cellState);
});
for (Map.Entry> cellGroupEntry : rowState.cellGroups
.entrySet()) {
Set 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.
updateStaticCellFromState(row.join(columns),
cellGroupEntry.getKey());
}
row.setStyleName(rowState.styleName);
}
private void updateStaticCellFromState(Grid.StaticSection.StaticCell 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);
}
/**
* Updates the grid footer section on state change.
*/
@OnStateChange("footer")
void updateFooter() {
final Grid grid = getWidget();
final SectionState state = getState().footer;
while (grid.getFooterRowCount() > 0) {
grid.removeFooterRow(0);
}
for (RowState rowState : state.rows) {
FooterRow row = grid.appendFooterRow();
updateStaticRow(rowState, row);
}
}
@Override
public void setDataSource(DataSource dataSource) {
super.setDataSource(dataSource);
getWidget().setDataSource(dataSource);
}
/**
* Adds a column to the Grid widget. For each column a communication id
* stored for client to server communication.
*
* @param column
* column to add
* @param id
* communication id
*/
public void addColumn(CustomColumn column, String id) {
assert !columnToIdMap.containsKey(column) && !columnToIdMap
.containsValue(id) : "Column with given id already exists.";
getWidget().addColumn(column);
columnToIdMap.put(column, id);
idToColumn.put(id, column);
}
/**
* Removes a column from Grid widget. This method also removes communication
* id mapping for the column.
*
* @param column
* column to remove
*/
public void removeColumn(CustomColumn column) {
assert columnToIdMap
.containsKey(column) : "Given Column does not exist.";
getWidget().removeColumn(column);
String id = columnToIdMap.remove(column);
idToColumn.remove(id);
}
@Override
public void onUnregister() {
super.onUnregister();
}
@Override
public boolean isWorkPending() {
return getWidget().isWorkPending();
}
@Override
public void layout() {
getWidget().onResize();
}
/**
* Sends sort information from an event to the server-side of the Grid.
*
* @param event
* the sort event
*/
private void handleSortEvent(SortEvent event) {
List columnIds = new ArrayList<>();
List sortDirections = new ArrayList<>();
for (SortOrder so : event.getOrder()) {
if (columnToIdMap.containsKey(so.getColumn())) {
columnIds.add(columnToIdMap.get(so.getColumn()));
sortDirections.add(so.getDirection());
}
}
getRpcProxy(GridServerRpc.class).sort(columnIds.toArray(new String[0]),
sortDirections.toArray(new SortDirection[0]),
event.isUserOriginated());
}
/* HasComponentsConnector */
@Override
public void updateCaption(ComponentConnector connector) {
// Details components don't support captions.
}
@Override
public List getChildComponents() {
if (childComponents == null) {
return Collections.emptyList();
}
return childComponents;
}
@Override
public void setChildComponents(List children) {
childComponents = children;
}
@Override
public HandlerRegistration addConnectorHierarchyChangeHandler(
ConnectorHierarchyChangeHandler handler) {
return ensureHandlerManager()
.addHandler(ConnectorHierarchyChangeEvent.TYPE, handler);
}
@Override
public GridState getState() {
return (GridState) super.getState();
}
@Override
public boolean hasTooltip() {
// Always check for generated descriptions.
return true;
}
@Override
public TooltipInfo getTooltipInfo(Element element) {
CellReference cell = getWidget().getCellReference(element);
if (cell != null) {
JsonObject row = cell.getRow();
if (row != null && (row.hasKey(GridState.JSONKEY_ROWDESCRIPTION)
|| row.hasKey(GridState.JSONKEY_CELLDESCRIPTION))) {
Column, JsonObject> column = cell.getColumn();
if (column instanceof CustomColumn) {
JsonObject cellDescriptions = row
.getObject(GridState.JSONKEY_CELLDESCRIPTION);
String id = ((CustomColumn) column).getConnectorId();
if (cellDescriptions != null
&& cellDescriptions.hasKey(id)) {
return new TooltipInfo(cellDescriptions.getString(id));
} else if (row.hasKey(GridState.JSONKEY_ROWDESCRIPTION)) {
return new TooltipInfo(row
.getString(GridState.JSONKEY_ROWDESCRIPTION));
}
}
}
}
if (super.hasTooltip()) {
return super.getTooltipInfo(element);
} else {
return null;
}
}
@Override
protected void sendContextClickEvent(MouseEventDetails details,
EventTarget eventTarget) {
// if element is the resize indicator, ignore the event
if (isResizeHandle(eventTarget)) {
WidgetUtil.clearTextSelection();
return;
}
EventCellReference eventCell = getWidget().getEventCell();
Section section = eventCell.getSection();
String rowKey = null;
if (eventCell.isBody() && eventCell.getRow() != null) {
rowKey = getRowKey(eventCell.getRow());
}
String columnId = getColumnId(eventCell.getColumn());
getRpcProxy(GridServerRpc.class).contextClick(eventCell.getRowIndex(),
rowKey, columnId, section, details);
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;
}
private List mapColumnsToIds(List> columns) {
return columns.stream().map(this::getColumnId).filter(Objects::nonNull)
.collect(Collectors.toList());
}
}