You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GridConnector.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. /*
  2. * Copyright 2000-2016 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.connectors.grid;
  17. import java.util.ArrayList;
  18. import java.util.Collections;
  19. import java.util.HashMap;
  20. import java.util.HashSet;
  21. import java.util.List;
  22. import java.util.Map;
  23. import java.util.Objects;
  24. import java.util.Set;
  25. import java.util.stream.Collectors;
  26. import com.google.gwt.core.client.Scheduler;
  27. import com.google.gwt.dom.client.Element;
  28. import com.google.gwt.dom.client.EventTarget;
  29. import com.google.gwt.dom.client.NativeEvent;
  30. import com.google.gwt.event.shared.HandlerRegistration;
  31. import com.vaadin.client.ComponentConnector;
  32. import com.vaadin.client.ConnectorHierarchyChangeEvent;
  33. import com.vaadin.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler;
  34. import com.vaadin.client.DeferredWorker;
  35. import com.vaadin.client.HasComponentsConnector;
  36. import com.vaadin.client.MouseEventDetailsBuilder;
  37. import com.vaadin.client.TooltipInfo;
  38. import com.vaadin.client.WidgetUtil;
  39. import com.vaadin.client.annotations.OnStateChange;
  40. import com.vaadin.client.connectors.AbstractListingConnector;
  41. import com.vaadin.client.connectors.grid.ColumnConnector.CustomColumn;
  42. import com.vaadin.client.data.DataSource;
  43. import com.vaadin.client.ui.SimpleManagedLayout;
  44. import com.vaadin.client.widget.grid.CellReference;
  45. import com.vaadin.client.widget.grid.EventCellReference;
  46. import com.vaadin.client.widget.grid.events.BodyClickHandler;
  47. import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler;
  48. import com.vaadin.client.widget.grid.events.GridClickEvent;
  49. import com.vaadin.client.widget.grid.events.GridDoubleClickEvent;
  50. import com.vaadin.client.widget.grid.sort.SortEvent;
  51. import com.vaadin.client.widget.grid.sort.SortOrder;
  52. import com.vaadin.client.widgets.Grid;
  53. import com.vaadin.client.widgets.Grid.Column;
  54. import com.vaadin.client.widgets.Grid.FooterRow;
  55. import com.vaadin.client.widgets.Grid.HeaderRow;
  56. import com.vaadin.shared.MouseEventDetails;
  57. import com.vaadin.shared.data.sort.SortDirection;
  58. import com.vaadin.shared.ui.Connect;
  59. import com.vaadin.shared.ui.grid.GridClientRpc;
  60. import com.vaadin.shared.ui.grid.GridConstants;
  61. import com.vaadin.shared.ui.grid.GridConstants.Section;
  62. import com.vaadin.shared.ui.grid.GridServerRpc;
  63. import com.vaadin.shared.ui.grid.GridState;
  64. import com.vaadin.shared.ui.grid.ScrollDestination;
  65. import com.vaadin.shared.ui.grid.SectionState;
  66. import com.vaadin.shared.ui.grid.SectionState.CellState;
  67. import com.vaadin.shared.ui.grid.SectionState.RowState;
  68. import elemental.json.JsonObject;
  69. /**
  70. * A connector class for the typed Grid component.
  71. *
  72. * @author Vaadin Ltd
  73. * @since 8.0
  74. */
  75. @Connect(com.vaadin.ui.Grid.class)
  76. public class GridConnector extends AbstractListingConnector
  77. implements HasComponentsConnector, SimpleManagedLayout, DeferredWorker {
  78. private Set<Runnable> refreshDetailsCallbacks = new HashSet<>();
  79. private class ItemClickHandler
  80. implements BodyClickHandler, BodyDoubleClickHandler {
  81. @Override
  82. public void onClick(GridClickEvent event) {
  83. if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
  84. fireItemClick(event.getTargetCell(), event.getNativeEvent());
  85. }
  86. }
  87. @Override
  88. public void onDoubleClick(GridDoubleClickEvent event) {
  89. if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
  90. fireItemClick(event.getTargetCell(), event.getNativeEvent());
  91. }
  92. }
  93. private void fireItemClick(CellReference<?> cell,
  94. NativeEvent mouseEvent) {
  95. String rowKey = getRowKey((JsonObject) cell.getRow());
  96. String columnId = columnToIdMap.get(cell.getColumn());
  97. getRpcProxy(GridServerRpc.class).itemClick(rowKey, columnId,
  98. MouseEventDetailsBuilder
  99. .buildMouseEventDetails(mouseEvent));
  100. }
  101. }
  102. /* Map to keep track of all added columns */
  103. private Map<CustomColumn, String> columnToIdMap = new HashMap<>();
  104. private Map<String, CustomColumn> idToColumn = new HashMap<>();
  105. /* Child component list for HasComponentsConnector */
  106. private List<ComponentConnector> childComponents;
  107. private ItemClickHandler itemClickHandler = new ItemClickHandler();
  108. /**
  109. * Gets the string identifier of the given column in this grid.
  110. *
  111. * @param column
  112. * the column whose id to get
  113. * @return the string id of the column
  114. */
  115. public String getColumnId(Column<?, ?> column) {
  116. return columnToIdMap.get(column);
  117. }
  118. /**
  119. * Gets the column corresponding to the given string identifier.
  120. *
  121. * @param columnId
  122. * the id of the column to get
  123. * @return the column with the given id
  124. */
  125. public CustomColumn getColumn(String columnId) {
  126. return idToColumn.get(columnId);
  127. }
  128. @Override
  129. @SuppressWarnings("unchecked")
  130. public Grid<JsonObject> getWidget() {
  131. return (Grid<JsonObject>) super.getWidget();
  132. }
  133. /**
  134. * Method called for a row details refresh. Runs all callbacks if any
  135. * details were shown and clears the callbacks.
  136. *
  137. * @param detailsShown
  138. * True if any details were set visible
  139. */
  140. protected void detailsRefreshed(boolean detailsShown) {
  141. if (detailsShown) {
  142. refreshDetailsCallbacks.forEach(Runnable::run);
  143. }
  144. refreshDetailsCallbacks.clear();
  145. }
  146. /**
  147. * Method target for when one single details has been updated and we might
  148. * need to scroll it into view.
  149. *
  150. * @param rowIndex
  151. * index of updated row
  152. */
  153. protected void singleDetailsOpened(int rowIndex) {
  154. addDetailsRefreshCallback(() -> {
  155. if (rowHasDetails(rowIndex)) {
  156. getWidget().scrollToRow(rowIndex);
  157. }
  158. });
  159. }
  160. /**
  161. * Add a single use details runnable callback for when we get a call to
  162. * {@link #detailsRefreshed(boolean)}.
  163. *
  164. * @param refreshCallback
  165. * Details refreshed callback
  166. */
  167. private void addDetailsRefreshCallback(Runnable refreshCallback) {
  168. refreshDetailsCallbacks.add(refreshCallback);
  169. }
  170. /**
  171. * Check if we have details for given row.
  172. *
  173. * @param rowIndex
  174. * @return
  175. */
  176. private boolean rowHasDetails(int rowIndex) {
  177. JsonObject row = getWidget().getDataSource().getRow(rowIndex);
  178. return row != null && row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
  179. && !row.getString(GridState.JSONKEY_DETAILS_VISIBLE).isEmpty();
  180. }
  181. @Override
  182. protected void init() {
  183. super.init();
  184. // Remove default headers when initializing Grid widget
  185. while (getWidget().getHeaderRowCount() > 0) {
  186. getWidget().removeHeaderRow(0);
  187. }
  188. registerRpc(GridClientRpc.class, new GridClientRpc() {
  189. @Override
  190. public void scrollToRow(int row, ScrollDestination destination) {
  191. Scheduler.get().scheduleFinally(
  192. () -> getWidget().scrollToRow(row, destination));
  193. // Add details refresh listener and handle possible detail for
  194. // scrolled row.
  195. addDetailsRefreshCallback(() -> {
  196. if (rowHasDetails(row)) {
  197. getWidget().scrollToRow(row, destination);
  198. }
  199. });
  200. }
  201. @Override
  202. public void scrollToStart() {
  203. Scheduler.get()
  204. .scheduleFinally(() -> getWidget().scrollToStart());
  205. }
  206. @Override
  207. public void scrollToEnd() {
  208. Scheduler.get()
  209. .scheduleFinally(() -> getWidget().scrollToEnd());
  210. addDetailsRefreshCallback(() -> {
  211. if (rowHasDetails(getWidget().getDataSource().size() - 1)) {
  212. getWidget().scrollToEnd();
  213. }
  214. });
  215. }
  216. });
  217. getWidget().addSortHandler(this::handleSortEvent);
  218. getWidget().setRowStyleGenerator(rowRef -> {
  219. JsonObject json = rowRef.getRow();
  220. return json.hasKey(GridState.JSONKEY_ROWSTYLE)
  221. ? json.getString(GridState.JSONKEY_ROWSTYLE) : null;
  222. });
  223. getWidget().setCellStyleGenerator(cellRef -> {
  224. JsonObject row = cellRef.getRow();
  225. if (!row.hasKey(GridState.JSONKEY_CELLSTYLES)) {
  226. return null;
  227. }
  228. Column<?, JsonObject> column = cellRef.getColumn();
  229. if (column instanceof CustomColumn) {
  230. String id = ((CustomColumn) column).getConnectorId();
  231. JsonObject cellStyles = row
  232. .getObject(GridState.JSONKEY_CELLSTYLES);
  233. if (cellStyles.hasKey(id)) {
  234. return cellStyles.getString(id);
  235. }
  236. }
  237. return null;
  238. });
  239. getWidget().addColumnVisibilityChangeHandler(event -> {
  240. if (event.isUserOriginated()) {
  241. getRpcProxy(GridServerRpc.class).columnVisibilityChanged(
  242. getColumnId(event.getColumn()), event.isHidden());
  243. }
  244. });
  245. getWidget().addColumnReorderHandler(event -> {
  246. if (event.isUserOriginated()) {
  247. List<String> newColumnOrder = mapColumnsToIds(
  248. event.getNewColumnOrder());
  249. List<String> oldColumnOrder = mapColumnsToIds(
  250. event.getOldColumnOrder());
  251. getRpcProxy(GridServerRpc.class)
  252. .columnsReordered(newColumnOrder, oldColumnOrder);
  253. }
  254. });
  255. getWidget().addColumnResizeHandler(event -> {
  256. Column<?, JsonObject> column = event.getColumn();
  257. getRpcProxy(GridServerRpc.class).columnResized(getColumnId(column),
  258. column.getWidthActual());
  259. });
  260. /* Item click events */
  261. getWidget().addBodyClickHandler(itemClickHandler);
  262. getWidget().addBodyDoubleClickHandler(itemClickHandler);
  263. layout();
  264. }
  265. @SuppressWarnings("unchecked")
  266. @OnStateChange("columnOrder")
  267. void updateColumnOrder() {
  268. Scheduler.get()
  269. .scheduleFinally(() -> getWidget().setColumnOrder(
  270. getState().columnOrder.stream().map(this::getColumn)
  271. .toArray(size -> new Column[size])));
  272. }
  273. @OnStateChange("columnResizeMode")
  274. void updateColumnResizeMode() {
  275. getWidget().setColumnResizeMode(getState().columnResizeMode);
  276. }
  277. /**
  278. * Updates the grid header section on state change.
  279. */
  280. @OnStateChange("header")
  281. void updateHeader() {
  282. final Grid<JsonObject> grid = getWidget();
  283. final SectionState state = getState().header;
  284. while (grid.getHeaderRowCount() > 0) {
  285. grid.removeHeaderRow(0);
  286. }
  287. for (RowState rowState : state.rows) {
  288. HeaderRow row = grid.appendHeaderRow();
  289. if (rowState.defaultHeader) {
  290. grid.setDefaultHeaderRow(row);
  291. }
  292. updateStaticRow(rowState, row);
  293. }
  294. }
  295. @OnStateChange("rowHeight")
  296. void updateRowHeight() {
  297. double rowHeight = getState().rowHeight;
  298. if (rowHeight >= 0) {
  299. getWidget().getEscalator().getHeader()
  300. .setDefaultRowHeight(rowHeight);
  301. getWidget().getEscalator().getBody().setDefaultRowHeight(rowHeight);
  302. getWidget().getEscalator().getFooter()
  303. .setDefaultRowHeight(rowHeight);
  304. } else if (getWidget().isAttached()) {
  305. // finally to make sure column sizes have been set before this
  306. Scheduler.get()
  307. .scheduleFinally(() -> getWidget().resetSizesFromDom());
  308. }
  309. }
  310. private void updateStaticRow(RowState rowState,
  311. Grid.StaticSection.StaticRow row) {
  312. rowState.cells.forEach((columnId, cellState) -> {
  313. updateStaticCellFromState(row.getCell(getColumn(columnId)),
  314. cellState);
  315. });
  316. for (Map.Entry<CellState, Set<String>> cellGroupEntry : rowState.cellGroups
  317. .entrySet()) {
  318. Set<String> group = cellGroupEntry.getValue();
  319. Grid.Column<?, ?>[] columns = group.stream().map(idToColumn::get)
  320. .toArray(size -> new Grid.Column<?, ?>[size]);
  321. // Set state to be the same as first in group.
  322. updateStaticCellFromState(row.join(columns),
  323. cellGroupEntry.getKey());
  324. }
  325. row.setStyleName(rowState.styleName);
  326. }
  327. private void updateStaticCellFromState(Grid.StaticSection.StaticCell cell,
  328. CellState cellState) {
  329. switch (cellState.type) {
  330. case TEXT:
  331. cell.setText(cellState.text);
  332. break;
  333. case HTML:
  334. cell.setHtml(cellState.html);
  335. break;
  336. case WIDGET:
  337. ComponentConnector connector = (ComponentConnector) cellState.connector;
  338. if (connector != null) {
  339. cell.setWidget(connector.getWidget());
  340. } else {
  341. // This happens if you do setVisible(false) on the component on
  342. // the server side
  343. cell.setWidget(null);
  344. }
  345. break;
  346. default:
  347. throw new IllegalStateException(
  348. "unexpected cell type: " + cellState.type);
  349. }
  350. cell.setStyleName(cellState.styleName);
  351. }
  352. /**
  353. * Updates the grid footer section on state change.
  354. */
  355. @OnStateChange("footer")
  356. void updateFooter() {
  357. final Grid<JsonObject> grid = getWidget();
  358. final SectionState state = getState().footer;
  359. while (grid.getFooterRowCount() > 0) {
  360. grid.removeFooterRow(0);
  361. }
  362. for (RowState rowState : state.rows) {
  363. FooterRow row = grid.appendFooterRow();
  364. updateStaticRow(rowState, row);
  365. }
  366. }
  367. @Override
  368. public void setDataSource(DataSource<JsonObject> dataSource) {
  369. super.setDataSource(dataSource);
  370. getWidget().setDataSource(dataSource);
  371. }
  372. /**
  373. * Adds a column to the Grid widget. For each column a communication id
  374. * stored for client to server communication.
  375. *
  376. * @param column
  377. * column to add
  378. * @param id
  379. * communication id
  380. */
  381. public void addColumn(CustomColumn column, String id) {
  382. assert !columnToIdMap.containsKey(column) && !columnToIdMap
  383. .containsValue(id) : "Column with given id already exists.";
  384. getWidget().addColumn(column);
  385. columnToIdMap.put(column, id);
  386. idToColumn.put(id, column);
  387. }
  388. /**
  389. * Removes a column from Grid widget. This method also removes communication
  390. * id mapping for the column.
  391. *
  392. * @param column
  393. * column to remove
  394. */
  395. public void removeColumn(CustomColumn column) {
  396. assert columnToIdMap
  397. .containsKey(column) : "Given Column does not exist.";
  398. getWidget().removeColumn(column);
  399. String id = columnToIdMap.remove(column);
  400. idToColumn.remove(id);
  401. }
  402. @Override
  403. public void onUnregister() {
  404. super.onUnregister();
  405. }
  406. @Override
  407. public boolean isWorkPending() {
  408. return getWidget().isWorkPending();
  409. }
  410. @Override
  411. public void layout() {
  412. getWidget().onResize();
  413. }
  414. /**
  415. * Sends sort information from an event to the server-side of the Grid.
  416. *
  417. * @param event
  418. * the sort event
  419. */
  420. private void handleSortEvent(SortEvent<JsonObject> event) {
  421. List<String> columnIds = new ArrayList<>();
  422. List<SortDirection> sortDirections = new ArrayList<>();
  423. for (SortOrder so : event.getOrder()) {
  424. if (columnToIdMap.containsKey(so.getColumn())) {
  425. columnIds.add(columnToIdMap.get(so.getColumn()));
  426. sortDirections.add(so.getDirection());
  427. }
  428. }
  429. getRpcProxy(GridServerRpc.class).sort(columnIds.toArray(new String[0]),
  430. sortDirections.toArray(new SortDirection[0]),
  431. event.isUserOriginated());
  432. }
  433. /* HasComponentsConnector */
  434. @Override
  435. public void updateCaption(ComponentConnector connector) {
  436. // Details components don't support captions.
  437. }
  438. @Override
  439. public List<ComponentConnector> getChildComponents() {
  440. if (childComponents == null) {
  441. return Collections.emptyList();
  442. }
  443. return childComponents;
  444. }
  445. @Override
  446. public void setChildComponents(List<ComponentConnector> children) {
  447. childComponents = children;
  448. }
  449. @Override
  450. public HandlerRegistration addConnectorHierarchyChangeHandler(
  451. ConnectorHierarchyChangeHandler handler) {
  452. return ensureHandlerManager()
  453. .addHandler(ConnectorHierarchyChangeEvent.TYPE, handler);
  454. }
  455. @Override
  456. public GridState getState() {
  457. return (GridState) super.getState();
  458. }
  459. @Override
  460. public boolean hasTooltip() {
  461. // Always check for generated descriptions.
  462. return true;
  463. }
  464. @Override
  465. public TooltipInfo getTooltipInfo(Element element) {
  466. CellReference<JsonObject> cell = getWidget().getCellReference(element);
  467. if (cell != null) {
  468. JsonObject row = cell.getRow();
  469. if (row != null && (row.hasKey(GridState.JSONKEY_ROWDESCRIPTION)
  470. || row.hasKey(GridState.JSONKEY_CELLDESCRIPTION))) {
  471. Column<?, JsonObject> column = cell.getColumn();
  472. if (column instanceof CustomColumn) {
  473. JsonObject cellDescriptions = row
  474. .getObject(GridState.JSONKEY_CELLDESCRIPTION);
  475. String id = ((CustomColumn) column).getConnectorId();
  476. if (cellDescriptions != null
  477. && cellDescriptions.hasKey(id)) {
  478. return new TooltipInfo(cellDescriptions.getString(id));
  479. } else if (row.hasKey(GridState.JSONKEY_ROWDESCRIPTION)) {
  480. return new TooltipInfo(row
  481. .getString(GridState.JSONKEY_ROWDESCRIPTION));
  482. }
  483. }
  484. }
  485. }
  486. if (super.hasTooltip()) {
  487. return super.getTooltipInfo(element);
  488. } else {
  489. return null;
  490. }
  491. }
  492. @Override
  493. protected void sendContextClickEvent(MouseEventDetails details,
  494. EventTarget eventTarget) {
  495. // if element is the resize indicator, ignore the event
  496. if (isResizeHandle(eventTarget)) {
  497. WidgetUtil.clearTextSelection();
  498. return;
  499. }
  500. EventCellReference<JsonObject> eventCell = getWidget().getEventCell();
  501. Section section = eventCell.getSection();
  502. String rowKey = null;
  503. if (eventCell.isBody() && eventCell.getRow() != null) {
  504. rowKey = getRowKey(eventCell.getRow());
  505. }
  506. String columnId = getColumnId(eventCell.getColumn());
  507. getRpcProxy(GridServerRpc.class).contextClick(eventCell.getRowIndex(),
  508. rowKey, columnId, section, details);
  509. WidgetUtil.clearTextSelection();
  510. }
  511. private boolean isResizeHandle(EventTarget eventTarget) {
  512. if (Element.is(eventTarget)) {
  513. Element e = Element.as(eventTarget);
  514. if (e.getClassName().contains("-column-resize-handle")) {
  515. return true;
  516. }
  517. }
  518. return false;
  519. }
  520. private List<String> mapColumnsToIds(List<Column<?, JsonObject>> columns) {
  521. return columns.stream().map(this::getColumnId).filter(Objects::nonNull)
  522. .collect(Collectors.toList());
  523. }
  524. }