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 12KB

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