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.

GridDropTargetConnector.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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.List;
  18. import java.util.Map;
  19. import java.util.Objects;
  20. import com.google.gwt.dom.client.Element;
  21. import com.google.gwt.dom.client.NativeEvent;
  22. import com.google.gwt.dom.client.TableRowElement;
  23. import com.google.gwt.user.client.Window;
  24. import com.vaadin.client.MouseEventDetailsBuilder;
  25. import com.vaadin.client.ServerConnector;
  26. import com.vaadin.client.WidgetUtil;
  27. import com.vaadin.client.extensions.DropTargetExtensionConnector;
  28. import com.vaadin.client.widget.escalator.RowContainer;
  29. import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer;
  30. import com.vaadin.client.widgets.Escalator;
  31. import com.vaadin.shared.MouseEventDetails;
  32. import com.vaadin.shared.Range;
  33. import com.vaadin.shared.ui.Connect;
  34. import com.vaadin.shared.ui.grid.DropLocation;
  35. import com.vaadin.shared.ui.grid.DropMode;
  36. import com.vaadin.shared.ui.grid.GridDropTargetRpc;
  37. import com.vaadin.shared.ui.grid.GridDropTargetState;
  38. import com.vaadin.shared.ui.grid.GridState;
  39. import com.vaadin.ui.components.grid.GridDropTarget;
  40. import elemental.events.Event;
  41. import elemental.json.JsonObject;
  42. /**
  43. * Makes Grid an HTML5 drop target. This is the client side counterpart of
  44. * {@link GridDropTarget}.
  45. *
  46. * @author Vaadin Ltd
  47. * @since 8.1
  48. */
  49. @Connect(GridDropTarget.class)
  50. public class GridDropTargetConnector extends DropTargetExtensionConnector {
  51. /**
  52. * Current style name
  53. */
  54. private String currentStyleName;
  55. private GridConnector gridConnector;
  56. /**
  57. * Class name to apply when an element is dragged over a row and the
  58. * location is {@link DropLocation#ON_TOP}.
  59. */
  60. private String styleDragCenter;
  61. /**
  62. * Class name to apply when an element is dragged over a row and the
  63. * location is {@link DropLocation#ABOVE}.
  64. */
  65. private String styleDragTop;
  66. /**
  67. * Class name to apply when an element is dragged over a row and the
  68. * location is {@link DropLocation#BELOW}.
  69. */
  70. private String styleDragBottom;
  71. /**
  72. * Class name to apply when dragged over an empty grid, or when dropping on
  73. * rows is not possible (see {@link #isDroppingOnRowsPossible()}).
  74. */
  75. private String styleDragEmpty;
  76. /**
  77. * The latest row that was dragged on top of, or the tablewrapper element
  78. * returned by {@link #getDropTargetElement()} if drop is not applicable for
  79. * any body rows. Need to store this so that can remove drop hint styling
  80. * when the target has changed since all browsers don't seem to always fire
  81. * the drag-enter drag-exit events in a consistent order.
  82. */
  83. private Element latestTargetElement;
  84. @Override
  85. protected void extend(ServerConnector target) {
  86. gridConnector = (GridConnector) target;
  87. super.extend(target);
  88. }
  89. @Override
  90. protected boolean isDropAllowedByCriteriaScript(NativeEvent event) {
  91. final String criteriaScript = getState().criteriaScript;
  92. if (criteriaScript == null) {
  93. return true;
  94. }
  95. return executeScript(event,
  96. getTargetElement(event.getEventTarget().cast()),
  97. getDropLocation(getTargetElement(event.getEventTarget().cast()),
  98. event).name(),
  99. criteriaScript);
  100. }
  101. private native boolean executeScript(NativeEvent event,
  102. Element targetElement, String dropLocation, String script)
  103. /*-{
  104. return new Function('event', 'targetElement', 'dropLocation', script)(event, targetElement, dropLocation);
  105. }-*/;
  106. /**
  107. * Inspects whether the current drop would happen on the whole grid instead
  108. * of specific row as the drop target. This is based on used drop mode,
  109. * whether dropping on sorted grid rows is allowed (determined on server
  110. * side and automatically updated to drop mode) and whether the grid is
  111. * empty.
  112. *
  113. * @return {@code true} when the drop target is the whole grid, or
  114. * {@code false} when it is one of the rows
  115. */
  116. protected boolean isDroppingOnRowsPossible() {
  117. if (getState().dropMode == DropMode.ON_GRID) {
  118. return false;
  119. }
  120. if (getEscalator().getVisibleRowRange().isEmpty()) {
  121. return false;
  122. }
  123. return true;
  124. }
  125. @Override
  126. protected void sendDropEventToServer(List<String> types,
  127. Map<String, String> data, String dropEffect,
  128. NativeEvent dropEvent) {
  129. Element targetElement = getTargetElement(
  130. (Element) dropEvent.getEventTarget().cast());
  131. DropLocation dropLocation = getDropLocation(targetElement, dropEvent);
  132. MouseEventDetails mouseEventDetails = MouseEventDetailsBuilder
  133. .buildMouseEventDetails(dropEvent, targetElement);
  134. String rowKey = null;
  135. // the target is either on a row element or the table wrapper
  136. if (TableRowElement.is(targetElement)) {
  137. rowKey = getRowData(targetElement.cast())
  138. .getString(GridState.JSONKEY_ROWKEY);
  139. }
  140. getRpcProxy(GridDropTargetRpc.class).drop(types, data, dropEffect,
  141. rowKey, dropLocation, mouseEventDetails);
  142. }
  143. /**
  144. * Get the row data as json object for the given row.
  145. *
  146. * @param row
  147. * table row element
  148. * @return row data as json object for the given row
  149. */
  150. protected JsonObject getRowData(TableRowElement row) {
  151. int rowIndex = ((Escalator.AbstractRowContainer) getGridBody())
  152. .getLogicalRowIndex(row);
  153. return gridConnector.getDataSource().getRow(rowIndex);
  154. }
  155. /**
  156. * Returns the location of the event within the row.
  157. *
  158. * @param target
  159. * drop target element
  160. * @param event
  161. * drop event
  162. * @return the drop location to use
  163. */
  164. protected DropLocation getDropLocation(Element target, NativeEvent event) {
  165. if (!isDroppingOnRowsPossible()) {
  166. return DropLocation.EMPTY;
  167. }
  168. if (TableRowElement.is(target)) {
  169. if (getState().dropMode == DropMode.BETWEEN) {
  170. if (getRelativeY(target,
  171. event) < (target.getOffsetHeight() / 2)) {
  172. return DropLocation.ABOVE;
  173. } else {
  174. return DropLocation.BELOW;
  175. }
  176. } else if (getState().dropMode == DropMode.ON_TOP_OR_BETWEEN) {
  177. if (getRelativeY(target, event) < getState().dropThreshold) {
  178. return DropLocation.ABOVE;
  179. } else if (target.getOffsetHeight() - getRelativeY(target,
  180. event) < getState().dropThreshold) {
  181. return DropLocation.BELOW;
  182. } else {
  183. return DropLocation.ON_TOP;
  184. }
  185. } else {
  186. return DropLocation.ON_TOP;
  187. }
  188. }
  189. return DropLocation.EMPTY;
  190. }
  191. private int getRelativeY(Element element, NativeEvent event) {
  192. int relativeTop = element.getAbsoluteTop() - Window.getScrollTop();
  193. return WidgetUtil.getTouchOrMouseClientY(event) - relativeTop;
  194. }
  195. @Override
  196. protected void onDragEnter(Event event) {
  197. // Generate style names for the drop target
  198. String styleRow = gridConnector.getWidget().getStylePrimaryName()
  199. + "-row";
  200. styleDragCenter = styleRow + STYLE_SUFFIX_DRAG_CENTER;
  201. styleDragTop = styleRow + STYLE_SUFFIX_DRAG_TOP;
  202. styleDragBottom = styleRow + STYLE_SUFFIX_DRAG_BOTTOM;
  203. styleDragEmpty = gridConnector.getWidget().getStylePrimaryName()
  204. + "-body" + STYLE_SUFFIX_DRAG_TOP;
  205. super.onDragEnter(event);
  206. }
  207. @Override
  208. protected void addDragOverStyle(NativeEvent event) {
  209. Element targetElement = getTargetElement(
  210. ((Element) event.getEventTarget().cast()));
  211. // Get required class name
  212. String className = getTargetClassName(targetElement, event);
  213. // it seems that sometimes the events are not fired in a consistent
  214. // order, and this could cause that the correct styles are not removed
  215. // from the previous target element in removeDragOverStyle(event)
  216. if (latestTargetElement != null
  217. && targetElement != latestTargetElement) {
  218. removeStyles(latestTargetElement);
  219. }
  220. latestTargetElement = targetElement;
  221. // Add or replace class name if changed
  222. if (!targetElement.hasClassName(className)) {
  223. if (currentStyleName != null) {
  224. targetElement.removeClassName(currentStyleName);
  225. }
  226. targetElement.addClassName(className);
  227. currentStyleName = className;
  228. }
  229. }
  230. private String getTargetClassName(Element target, NativeEvent event) {
  231. String className;
  232. switch (getDropLocation(target, event)) {
  233. case ABOVE:
  234. className = styleDragTop;
  235. break;
  236. case BELOW:
  237. className = styleDragBottom;
  238. break;
  239. case EMPTY:
  240. className = styleDragEmpty;
  241. break;
  242. case ON_TOP:
  243. default:
  244. className = styleDragCenter;
  245. break;
  246. }
  247. return className;
  248. }
  249. @Override
  250. protected void removeDragOverStyle(NativeEvent event) {
  251. // Remove all possible style names
  252. Element targetElement = getTargetElement(
  253. (Element) event.getEventTarget().cast());
  254. removeStyles(targetElement);
  255. }
  256. private void removeStyles(Element element) {
  257. element.removeClassName(styleDragCenter);
  258. element.removeClassName(styleDragTop);
  259. element.removeClassName(styleDragBottom);
  260. element.removeClassName(styleDragEmpty);
  261. }
  262. /**
  263. * Gets the target element for a dragover or drop event.
  264. *
  265. * @param source
  266. * the event target of the event
  267. * @return the element that should be handled as the target of the event
  268. */
  269. protected Element getTargetElement(Element source) {
  270. final Element tableWrapper = getDropTargetElement();
  271. final BodyRowContainer gridBody = getGridBody();
  272. final Range visibleRowRange = getEscalator().getVisibleRowRange();
  273. if (!isDroppingOnRowsPossible()) {
  274. return tableWrapper;
  275. }
  276. while (!Objects.equals(source, tableWrapper)) {
  277. // the drop might happen on top of header, body or footer rows
  278. if (TableRowElement.is(source)) {
  279. String parentTagName = source.getParentElement().getTagName();
  280. if ("thead".equalsIgnoreCase(parentTagName)) {
  281. // for empty grid or ON_TOP mode, drop as last row,
  282. // otherwise as above first visible row
  283. if (visibleRowRange.isEmpty()
  284. || getState().dropMode == DropMode.ON_TOP) {
  285. return tableWrapper;
  286. } else {
  287. return gridBody
  288. .getRowElement(visibleRowRange.getStart());
  289. }
  290. } else if ("tfoot".equalsIgnoreCase(parentTagName)) {
  291. // for empty grid or ON_TOP mode, drop as last row,
  292. // otherwise as below last visible row
  293. if (visibleRowRange.isEmpty()
  294. || getState().dropMode == DropMode.ON_TOP) {
  295. return tableWrapper;
  296. } else {
  297. return gridBody
  298. .getRowElement(visibleRowRange.getEnd() - 1);
  299. }
  300. } else { // parent is tbody
  301. return source;
  302. }
  303. }
  304. source = source.getParentElement();
  305. }
  306. // the drag is on top of the tablewrapper, if the drop mode is ON_TOP,
  307. // then there is no target row for the drop
  308. if (getState().dropMode == DropMode.ON_TOP) {
  309. return tableWrapper;
  310. }
  311. // if dragged under the last row to empty space, drop target
  312. // needs to be below the last row
  313. return gridBody.getRowElement(visibleRowRange.getEnd() - 1);
  314. }
  315. @Override
  316. protected Element getDropTargetElement() {
  317. /*
  318. * The drop target element, the <div class="v-grid-tablewrapper" />.
  319. * This is where the event listeners are added since then we can accept
  320. * drops on header, body and footer rows and the "empty area" outside
  321. * rows. Also it is used since then the drop hints for "empty" area can
  322. * be shown properly as the grid body would scroll.
  323. */
  324. return getEscalator().getTableWrapper();
  325. }
  326. private Escalator getEscalator() {
  327. return gridConnector.getWidget().getEscalator();
  328. }
  329. private RowContainer.BodyRowContainer getGridBody() {
  330. return getEscalator().getBody();
  331. }
  332. private boolean isGridSortedByUser() {
  333. return !gridConnector.getWidget().getSortOrder().isEmpty();
  334. }
  335. @Override
  336. public GridDropTargetState getState() {
  337. return (GridDropTargetState) super.getState();
  338. }
  339. }