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.

DetailsManagerConnector.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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.HashMap;
  18. import java.util.Map;
  19. import com.google.gwt.core.client.Scheduler;
  20. import com.google.gwt.core.client.Scheduler.ScheduledCommand;
  21. import com.google.gwt.dom.client.Element;
  22. import com.google.gwt.user.client.ui.Widget;
  23. import com.vaadin.client.ComponentConnector;
  24. import com.vaadin.client.ConnectorMap;
  25. import com.vaadin.client.LayoutManager;
  26. import com.vaadin.client.ServerConnector;
  27. import com.vaadin.client.WidgetUtil;
  28. import com.vaadin.client.data.DataChangeHandler;
  29. import com.vaadin.client.extensions.AbstractExtensionConnector;
  30. import com.vaadin.client.ui.layout.ElementResizeEvent;
  31. import com.vaadin.client.ui.layout.ElementResizeListener;
  32. import com.vaadin.client.widget.grid.HeightAwareDetailsGenerator;
  33. import com.vaadin.client.widgets.Grid;
  34. import com.vaadin.shared.Registration;
  35. import com.vaadin.shared.ui.Connect;
  36. import com.vaadin.shared.ui.grid.DetailsManagerState;
  37. import com.vaadin.shared.ui.grid.GridState;
  38. import com.vaadin.ui.Grid.DetailsManager;
  39. import elemental.json.JsonObject;
  40. /**
  41. * Connector class for {@link DetailsManager} of the Grid component.
  42. *
  43. * @author Vaadin Ltd
  44. * @since 8.0
  45. */
  46. @Connect(DetailsManager.class)
  47. public class DetailsManagerConnector extends AbstractExtensionConnector {
  48. /* Map for tracking which details are open on which row */
  49. private Map<Integer, String> indexToDetailConnectorId = new HashMap<>();
  50. /* Boolean flag to avoid multiple refreshes */
  51. private boolean refreshing;
  52. /* Registration for data change handler. */
  53. private Registration dataChangeRegistration;
  54. private final Map<Element, ScheduledCommand> elementToResizeCommand = new HashMap<Element, Scheduler.ScheduledCommand>();
  55. private final ElementResizeListener detailsRowResizeListener = new ElementResizeListener() {
  56. @Override
  57. public void onElementResize(ElementResizeEvent e) {
  58. if (elementToResizeCommand.containsKey(e.getElement())) {
  59. Scheduler.get().scheduleFinally(
  60. elementToResizeCommand.get(e.getElement()));
  61. }
  62. }
  63. };
  64. /* calculated when the first details row is opened */
  65. private Double spacerCellBorderHeights = null;
  66. /**
  67. * DataChangeHandler for updating the visibility of detail widgets.
  68. */
  69. private final class DetailsChangeHandler implements DataChangeHandler {
  70. @Override
  71. public void resetDataAndSize(int estimatedNewDataSize) {
  72. // Full clean up
  73. indexToDetailConnectorId.clear();
  74. }
  75. @Override
  76. public void dataUpdated(int firstRowIndex, int numberOfRows) {
  77. for (int i = 0; i < numberOfRows; ++i) {
  78. int index = firstRowIndex + i;
  79. detachIfNeeded(index, getDetailsComponentConnectorId(index));
  80. }
  81. if (numberOfRows == 1) {
  82. getParent().singleDetailsOpened(firstRowIndex);
  83. }
  84. // Deferred opening of new ones.
  85. refreshDetails();
  86. }
  87. /* The remaining methods will do a full refresh for now */
  88. @Override
  89. public void dataRemoved(int firstRowIndex, int numberOfRows) {
  90. refreshDetails();
  91. }
  92. @Override
  93. public void dataAvailable(int firstRowIndex, int numberOfRows) {
  94. refreshDetails();
  95. }
  96. @Override
  97. public void dataAdded(int firstRowIndex, int numberOfRows) {
  98. refreshDetails();
  99. }
  100. }
  101. /**
  102. * Height aware details generator for client-side Grid.
  103. */
  104. private class CustomDetailsGenerator
  105. implements HeightAwareDetailsGenerator {
  106. @Override
  107. public Widget getDetails(int rowIndex) {
  108. String id = getDetailsComponentConnectorId(rowIndex);
  109. if (id == null) {
  110. return null;
  111. }
  112. Widget widget = getConnector(id).getWidget();
  113. getLayoutManager().addElementResizeListener(widget.getElement(),
  114. detailsRowResizeListener);
  115. elementToResizeCommand.put(widget.getElement(),
  116. createResizeCommand(rowIndex, widget.getElement()));
  117. return widget;
  118. }
  119. private ScheduledCommand createResizeCommand(final int rowIndex,
  120. final Element element) {
  121. return () -> {
  122. // It should not be possible to get here without calculating
  123. // the spacerCellBorderHeights or without having the details
  124. // row open, nor for this command to be triggered while
  125. // layout is running, but it's safer to check anyway.
  126. if (spacerCellBorderHeights != null
  127. && !getLayoutManager().isLayoutRunning()
  128. && getDetailsComponentConnectorId(rowIndex) != null) {
  129. double height = getLayoutManager().getOuterHeightDouble(
  130. element) + spacerCellBorderHeights;
  131. getWidget().setDetailsHeight(rowIndex, height);
  132. }
  133. };
  134. }
  135. @Override
  136. public double getDetailsHeight(int rowIndex) {
  137. // Case of null is handled in the getDetails method and this method
  138. // will not called if it returns null.
  139. String id = getDetailsComponentConnectorId(rowIndex);
  140. ComponentConnector componentConnector = getConnector(id);
  141. getLayoutManager().setNeedsMeasureRecursively(componentConnector);
  142. getLayoutManager().layoutNow();
  143. Element element = componentConnector.getWidget().getElement();
  144. if (spacerCellBorderHeights == null) {
  145. // If theme is changed, new details generator is created from
  146. // scratch, so this value doesn't need to be updated elsewhere.
  147. spacerCellBorderHeights = WidgetUtil
  148. .getBorderTopAndBottomThickness(
  149. element.getParentElement());
  150. }
  151. return getLayoutManager().getOuterHeightDouble(element);
  152. }
  153. private ComponentConnector getConnector(String id) {
  154. return (ComponentConnector) ConnectorMap.get(getConnection())
  155. .getConnector(id);
  156. }
  157. }
  158. @Override
  159. protected void extend(ServerConnector target) {
  160. getWidget().setDetailsGenerator(new CustomDetailsGenerator());
  161. dataChangeRegistration = getWidget().getDataSource()
  162. .addDataChangeHandler(new DetailsChangeHandler());
  163. }
  164. private void detachIfNeeded(int rowIndex, String id) {
  165. if (indexToDetailConnectorId.containsKey(rowIndex)) {
  166. if (indexToDetailConnectorId.get(rowIndex).equals(id)) {
  167. return;
  168. }
  169. if (id == null) {
  170. // Details have been hidden, listeners attached to the old
  171. // component need to be removed
  172. id = indexToDetailConnectorId.get(rowIndex);
  173. }
  174. // New or removed Details component, hide old one
  175. ComponentConnector connector = (ComponentConnector) ConnectorMap
  176. .get(getConnection()).getConnector(id);
  177. if (connector != null) {
  178. Element element = connector.getWidget().getElement();
  179. elementToResizeCommand.remove(element);
  180. getLayoutManager().removeElementResizeListener(element,
  181. detailsRowResizeListener);
  182. }
  183. getWidget().setDetailsVisible(rowIndex, false);
  184. indexToDetailConnectorId.remove(rowIndex);
  185. }
  186. }
  187. @Override
  188. public void onUnregister() {
  189. super.onUnregister();
  190. dataChangeRegistration.remove();
  191. dataChangeRegistration = null;
  192. indexToDetailConnectorId.clear();
  193. }
  194. @Override
  195. public GridConnector getParent() {
  196. return (GridConnector) super.getParent();
  197. }
  198. @Override
  199. public DetailsManagerState getState() {
  200. return (DetailsManagerState) super.getState();
  201. }
  202. private Grid<JsonObject> getWidget() {
  203. return getParent().getWidget();
  204. }
  205. /**
  206. * Returns the connector id for a details component.
  207. *
  208. * @param rowIndex
  209. * the row index of details component
  210. * @return connector id; {@code null} if row or id is not found
  211. */
  212. private String getDetailsComponentConnectorId(int rowIndex) {
  213. JsonObject row = getParent().getWidget().getDataSource()
  214. .getRow(rowIndex);
  215. if (row == null || !row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
  216. || row.getString(GridState.JSONKEY_DETAILS_VISIBLE).isEmpty()) {
  217. return null;
  218. }
  219. return row.getString(GridState.JSONKEY_DETAILS_VISIBLE);
  220. }
  221. private LayoutManager getLayoutManager() {
  222. return LayoutManager.get(getConnection());
  223. }
  224. /**
  225. * Schedules a deferred opening for new details components.
  226. */
  227. private void refreshDetails() {
  228. if (refreshing) {
  229. return;
  230. }
  231. refreshing = true;
  232. Scheduler.get().scheduleFinally(this::refreshDetailsVisibility);
  233. }
  234. private void refreshDetailsVisibility() {
  235. boolean shownDetails = false;
  236. for (int i = 0; i < getWidget().getDataSource().size(); ++i) {
  237. String id = getDetailsComponentConnectorId(i);
  238. detachIfNeeded(i, id);
  239. if (id == null) {
  240. continue;
  241. }
  242. indexToDetailConnectorId.put(i, id);
  243. getWidget().setDetailsVisible(i, true);
  244. shownDetails = true;
  245. }
  246. refreshing = false;
  247. getParent().detailsRefreshed(shownDetails);
  248. }
  249. }