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.

GridDragSourceConnector.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  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.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.Collections;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.function.Function;
  23. import java.util.stream.Collectors;
  24. import java.util.stream.Stream;
  25. import com.google.gwt.animation.client.AnimationScheduler;
  26. import com.google.gwt.dom.client.Element;
  27. import com.google.gwt.dom.client.NativeEvent;
  28. import com.google.gwt.dom.client.Style;
  29. import com.google.gwt.dom.client.Style.Float;
  30. import com.google.gwt.dom.client.Style.Unit;
  31. import com.google.gwt.dom.client.TableRowElement;
  32. import com.google.gwt.user.client.DOM;
  33. import com.google.gwt.user.client.ui.Image;
  34. import com.vaadin.client.BrowserInfo;
  35. import com.vaadin.client.ServerConnector;
  36. import com.vaadin.client.WidgetUtil;
  37. import com.vaadin.client.data.AbstractRemoteDataSource;
  38. import com.vaadin.client.extensions.DragSourceExtensionConnector;
  39. import com.vaadin.client.widget.escalator.RowContainer;
  40. import com.vaadin.client.widget.grid.selection.SelectionModel;
  41. import com.vaadin.client.widgets.Escalator;
  42. import com.vaadin.client.widgets.Grid;
  43. import com.vaadin.shared.Range;
  44. import com.vaadin.shared.data.DataCommunicatorConstants;
  45. import com.vaadin.shared.ui.Connect;
  46. import com.vaadin.shared.ui.dnd.DragSourceState;
  47. import com.vaadin.shared.ui.dnd.DropEffect;
  48. import com.vaadin.shared.ui.grid.GridDragSourceRpc;
  49. import com.vaadin.shared.ui.grid.GridDragSourceState;
  50. import com.vaadin.ui.components.grid.GridDragSource;
  51. import elemental.events.Event;
  52. import elemental.json.JsonObject;
  53. /**
  54. * Adds HTML5 drag and drop functionality to a
  55. * {@link com.vaadin.client.widgets.Grid Grid}'s rows. This is the client side
  56. * counterpart of {@link GridDragSource}.
  57. *
  58. * @author Vaadin Ltd
  59. * @since 8.1
  60. */
  61. @Connect(GridDragSource.class)
  62. public class GridDragSourceConnector extends DragSourceExtensionConnector {
  63. /**
  64. * Delay used to distinct between scroll and drag start in grid: if the user
  65. * doens't move the finger before this "timeout", it should be considered as
  66. * a drag start.
  67. * <p>
  68. * This default value originates from VScrollTable which uses it to
  69. * distinguish between scroll and context click (long tap).
  70. *
  71. * @see Escalator#setDelayToCancelTouchScroll(double)
  72. */
  73. private static final int TOUCH_SCROLL_TIMEOUT_DELAY = 500;
  74. private static final String STYLE_SUFFIX_DRAG_BADGE = "-drag-badge";
  75. private GridConnector gridConnector;
  76. /**
  77. * List of dragged items.
  78. */
  79. private List<JsonObject> draggedItems;
  80. private boolean touchScrollDelayUsed;
  81. private String draggedStyleName;
  82. @Override
  83. protected void extend(ServerConnector target) {
  84. gridConnector = (GridConnector) target;
  85. // HTML5 DnD is by default not enabled for mobile devices
  86. if (BrowserInfo.get().isTouchDevice()) {
  87. if (getConnection().getUIConnector().isMobileHTML5DndEnabled()) {
  88. // distinct between scroll and drag start
  89. gridConnector.getWidget().getEscalator()
  90. .setDelayToCancelTouchScroll(
  91. TOUCH_SCROLL_TIMEOUT_DELAY);
  92. touchScrollDelayUsed = true;
  93. } else {
  94. return;
  95. }
  96. }
  97. // Set newly added rows draggable
  98. getGridBody()
  99. .setNewRowCallback(rows -> rows.forEach(this::addDraggable));
  100. // Add drag listeners to body element
  101. addDragListeners(getGridBody().getElement());
  102. gridConnector.onDragSourceAttached();
  103. }
  104. @Override
  105. protected void onDragStart(Event event) {
  106. NativeEvent nativeEvent = (NativeEvent) event;
  107. // Make sure user is not actually scrolling
  108. if (touchScrollDelayUsed && gridConnector.getWidget().getEscalator()
  109. .isTouchScrolling()) {
  110. event.preventDefault();
  111. event.stopPropagation();
  112. return;
  113. }
  114. // Do not allow drag starts from native Android Chrome, since it doesn't
  115. // work properly (doesn't fire dragend reliably)
  116. if (isAndoidChrome() && isNativeDragEvent(nativeEvent)) {
  117. event.preventDefault();
  118. event.stopPropagation();
  119. return;
  120. }
  121. // Collect the keys of dragged rows
  122. draggedItems = getDraggedRows(nativeEvent);
  123. // Ignore event if there are no items dragged
  124. if (draggedItems.isEmpty()) {
  125. return;
  126. }
  127. // Construct style name to be added to dragged rows
  128. draggedStyleName = gridConnector.getWidget().getStylePrimaryName()
  129. + "-row" + STYLE_SUFFIX_DRAGGED;
  130. super.onDragStart(event);
  131. }
  132. @Override
  133. protected void setDragImage(NativeEvent dragStartEvent) {
  134. // do not call super since need to handle specifically
  135. // 1. use resource if set (never needs safari hack)
  136. // 2. add row count badge if necessary
  137. // 3. apply hacks for safari/mobile drag image if needed
  138. // Add badge showing the number of dragged columns
  139. String imageUrl = getResourceUrl(DragSourceState.RESOURCE_DRAG_IMAGE);
  140. if (imageUrl != null && !imageUrl.isEmpty()) {
  141. Image dragImage = new Image(
  142. getConnection().translateVaadinUri(imageUrl));
  143. dragStartEvent.getDataTransfer()
  144. .setDragImage(dragImage.getElement(), 0, 0);
  145. } else {
  146. Element draggedRowElement = (Element) dragStartEvent
  147. .getEventTarget().cast();
  148. Element badge;
  149. if (draggedItems.size() > 1) {
  150. badge = DOM.createSpan();
  151. badge.setClassName(
  152. gridConnector.getWidget().getStylePrimaryName() + "-row"
  153. + STYLE_SUFFIX_DRAG_BADGE);
  154. badge.setInnerHTML(draggedItems.size() + "");
  155. BrowserInfo browserInfo = BrowserInfo.get();
  156. if (browserInfo.isTouchDevice()) {
  157. // the drag image is centered on the touch coordinates
  158. // -> show the badge on the right edge of the row
  159. badge.getStyle().setFloat(Float.RIGHT);
  160. badge.getStyle().setMarginRight(20, Unit.PX);
  161. badge.getStyle().setMarginTop(-20, Unit.PX);
  162. } else if (browserInfo.isSafari()) {
  163. // On Safari, only the part of the row visible inside grid
  164. // is shown, and also the badge needs to be totally on top
  165. // of the row.
  166. Element tableWrapperDiv = getGridBody().getElement()
  167. .getParentElement().getParentElement();
  168. int mouseXRelativeToGrid = WidgetUtil
  169. .getRelativeX(tableWrapperDiv, dragStartEvent);
  170. if (mouseXRelativeToGrid < (tableWrapperDiv.getClientWidth()
  171. - 60)) {
  172. badge.getStyle().setMarginLeft(
  173. mouseXRelativeToGrid + 10, Unit.PX);
  174. } else {
  175. badge.getStyle().setMarginLeft(
  176. mouseXRelativeToGrid - 60, Unit.PX);
  177. }
  178. badge.getStyle().setMarginTop(-32, Unit.PX);
  179. } else {
  180. badge.getStyle().setMarginLeft(WidgetUtil.getRelativeX(
  181. draggedRowElement, dragStartEvent) + 10, Unit.PX);
  182. badge.getStyle().setMarginTop(-20, Unit.PX);
  183. }
  184. } else {
  185. badge = null;
  186. }
  187. final int frozenColumnCount = getGrid().getFrozenColumnCount();
  188. final Element selectionColumnCell = getGrid().getSelectionColumn()
  189. .isPresent()
  190. // -1 is used when even selection column is not frozen
  191. && frozenColumnCount != -1
  192. ? draggedRowElement
  193. .removeChild(
  194. draggedRowElement.getFirstChild())
  195. .cast()
  196. : null;
  197. final List<String> frozenCellsTransforms = new ArrayList<>();
  198. for (int i = 0; i < getGrid().getColumnCount(); i++) {
  199. if (i >= frozenColumnCount) {
  200. break;
  201. }
  202. if (getGrid().getColumn(i).isHidden()) {
  203. frozenCellsTransforms.add(null);
  204. continue;
  205. }
  206. Style style = ((Element) draggedRowElement.getChild(i).cast())
  207. .getStyle();
  208. frozenCellsTransforms.add(style.getProperty("transform"));
  209. style.clearProperty("transform");
  210. }
  211. if (badge != null) {
  212. draggedRowElement.appendChild(badge);
  213. }
  214. // The following hack is used since IE11 doesn't support custom drag
  215. // image.
  216. // 1. Remove multiple rows drag badge, if used
  217. // 2. add selection column cell back, if was removed
  218. // 3. reset frozen column transitions, if were cleared
  219. AnimationScheduler.get().requestAnimationFrame(timestamp -> {
  220. if (badge != null) {
  221. badge.removeFromParent();
  222. }
  223. for (int i = 0; i < frozenCellsTransforms.size(); i++) {
  224. String transform = frozenCellsTransforms.get(i);
  225. if (transform != null) {
  226. ((Element) draggedRowElement.getChild(i).cast())
  227. .getStyle().setProperty("transform", transform);
  228. }
  229. }
  230. if (selectionColumnCell != null) {
  231. draggedRowElement.insertFirst(selectionColumnCell);
  232. }
  233. }, (Element) dragStartEvent.getEventTarget().cast());
  234. fixDragImageOffsetsForDesktop(dragStartEvent, draggedRowElement);
  235. fixDragImageTransformForMobile(draggedRowElement);
  236. }
  237. }
  238. @Override
  239. protected Map<String, String> createDataTransferData(
  240. NativeEvent dragStartEvent) {
  241. Map<String, String> dataMap = super.createDataTransferData(
  242. dragStartEvent);
  243. // Add data provided by the generator functions
  244. getDraggedRows(dragStartEvent).forEach(row -> {
  245. Map<String, String> rowDragData = getRowDragData(row);
  246. rowDragData.forEach((type, data) -> {
  247. if (!(data == null || data.isEmpty())) {
  248. if (!dataMap.containsKey(type)) {
  249. dataMap.put(type, data);
  250. } else {
  251. // Separate data with new line character when multiple
  252. // rows are dragged
  253. dataMap.put(type, dataMap.get(type) + "\n" + data);
  254. }
  255. }
  256. });
  257. });
  258. return dataMap;
  259. }
  260. @Override
  261. protected void sendDragStartEventToServer(NativeEvent dragStartEvent) {
  262. // Start server RPC with dragged item keys
  263. getRpcProxy(GridDragSourceRpc.class).dragStart(draggedItems.stream()
  264. .map(row -> row.getString(DataCommunicatorConstants.KEY))
  265. .collect(Collectors.toList()));
  266. }
  267. private List<JsonObject> getDraggedRows(NativeEvent dragStartEvent) {
  268. List<JsonObject> draggedRows = new ArrayList<>();
  269. if (TableRowElement.is(dragStartEvent.getEventTarget())) {
  270. TableRowElement row = (TableRowElement) dragStartEvent
  271. .getEventTarget().cast();
  272. int rowIndex = ((Escalator.AbstractRowContainer) getGridBody())
  273. .getLogicalRowIndex(row);
  274. JsonObject rowData = gridConnector.getDataSource().getRow(rowIndex);
  275. if (dragMultipleRows(rowData)) {
  276. getSelectedVisibleRows().forEach(draggedRows::add);
  277. } else {
  278. draggedRows.add(rowData);
  279. }
  280. }
  281. return draggedRows;
  282. }
  283. @Override
  284. protected void onDragEnd(Event event) {
  285. NativeEvent nativeEvent = (NativeEvent) event;
  286. // for android chrome we use the polyfill, in case browser fires a
  287. // native dragend event after the polyfill, we need to ignore that one
  288. if (isAndoidChrome() && isNativeDragEvent((nativeEvent))) {
  289. event.preventDefault();
  290. event.stopPropagation();
  291. return;
  292. }
  293. // Ignore event if there are no items dragged
  294. if (draggedItems != null && !draggedItems.isEmpty()) {
  295. super.onDragEnd(event);
  296. }
  297. // Clear item list
  298. draggedItems = null;
  299. }
  300. @Override
  301. protected void sendDragEndEventToServer(NativeEvent dragEndEvent,
  302. DropEffect dropEffect) {
  303. // Send server RPC with dragged item keys
  304. getRpcProxy(GridDragSourceRpc.class).dragEnd(dropEffect,
  305. draggedItems.stream().map(
  306. row -> row.getString(DataCommunicatorConstants.KEY))
  307. .collect(Collectors.toList()));
  308. }
  309. /**
  310. * Tells if multiple rows are dragged. Returns true if multiple selection is
  311. * allowed and a selected row is dragged.
  312. *
  313. * @param draggedRow
  314. * Data of dragged row.
  315. * @return {@code true} if multiple rows are dragged, {@code false}
  316. * otherwise.
  317. */
  318. private boolean dragMultipleRows(JsonObject draggedRow) {
  319. SelectionModel<JsonObject> selectionModel = getGrid()
  320. .getSelectionModel();
  321. return selectionModel.isSelectionAllowed()
  322. && selectionModel instanceof MultiSelectionModelConnector.MultiSelectionModel
  323. && selectionModel.isSelected(draggedRow);
  324. }
  325. /**
  326. * Collects the data of all selected visible rows.
  327. *
  328. * @return List of data of all selected visible rows.
  329. */
  330. private List<JsonObject> getSelectedVisibleRows() {
  331. return getSelectedRowsInRange(getEscalator().getVisibleRowRange());
  332. }
  333. /**
  334. * Get all selected rows from a subset of rows defined by {@code range}.
  335. *
  336. * @param range
  337. * Range of indexes.
  338. * @return List of data of all selected rows in the given range.
  339. */
  340. private List<JsonObject> getSelectedRowsInRange(Range range) {
  341. List<JsonObject> selectedRows = new ArrayList<>();
  342. for (int i = range.getStart(); i < range.getEnd(); i++) {
  343. JsonObject row = gridConnector.getDataSource().getRow(i);
  344. if (SelectionModel.isItemSelected(row)) {
  345. selectedRows.add(row);
  346. }
  347. }
  348. return selectedRows;
  349. }
  350. /**
  351. * Gets drag data provided by the generator functions.
  352. *
  353. * @param row
  354. * The row data.
  355. * @return The generated drag data type mapped to the corresponding drag
  356. * data. If there are no generator functions, returns an empty map.
  357. */
  358. private Map<String, String> getRowDragData(JsonObject row) {
  359. // Collect a map of data types and data that is provided by the
  360. // generator functions set for this drag source
  361. if (row.hasKey(GridDragSourceState.JSONKEY_DRAG_DATA)) {
  362. JsonObject dragData = row
  363. .getObject(GridDragSourceState.JSONKEY_DRAG_DATA);
  364. return Arrays.stream(dragData.keys()).collect(
  365. Collectors.toMap(Function.identity(), dragData::get));
  366. }
  367. // Otherwise return empty map
  368. return Collections.emptyMap();
  369. }
  370. /**
  371. * Add {@code v-grid-row-dragged} class name to each row being dragged.
  372. *
  373. * @param event
  374. * The dragstart event.
  375. */
  376. @Override
  377. protected void addDraggedStyle(NativeEvent event) {
  378. getDraggedRowElementStream().forEach(
  379. rowElement -> rowElement.addClassName(draggedStyleName));
  380. }
  381. /**
  382. * Remove {@code v-grid-row-dragged} class name from dragged rows.
  383. *
  384. * @param event
  385. * The dragend event.
  386. */
  387. @Override
  388. protected void removeDraggedStyle(NativeEvent event) {
  389. getDraggedRowElementStream().forEach(
  390. rowElement -> rowElement.removeClassName(draggedStyleName));
  391. }
  392. /**
  393. * Get the dragged table row elements as a stream.
  394. *
  395. * @return Stream of dragged table row elements.
  396. */
  397. private Stream<TableRowElement> getDraggedRowElementStream() {
  398. return draggedItems.stream().map(
  399. row -> ((AbstractRemoteDataSource<JsonObject>) gridConnector
  400. .getDataSource()).indexOf(row))
  401. .map(getGridBody()::getRowElement);
  402. }
  403. @Override
  404. public void onUnregister() {
  405. // Remove draggable from all row elements in the escalator
  406. Range visibleRange = getEscalator().getVisibleRowRange();
  407. for (int i = visibleRange.getStart(); i < visibleRange.getEnd(); i++) {
  408. removeDraggable(getGridBody().getRowElement(i));
  409. }
  410. // Remove drag listeners from body element
  411. removeDragListeners(getGridBody().getElement());
  412. // Remove callback for newly added rows
  413. getGridBody().setNewRowCallback(null);
  414. if (touchScrollDelayUsed) {
  415. gridConnector.getWidget().getEscalator()
  416. .setDelayToCancelTouchScroll(-1);
  417. touchScrollDelayUsed = false;
  418. }
  419. super.onUnregister();
  420. }
  421. private Grid<JsonObject> getGrid() {
  422. return gridConnector.getWidget();
  423. }
  424. private Escalator getEscalator() {
  425. return getGrid().getEscalator();
  426. }
  427. private RowContainer.BodyRowContainer getGridBody() {
  428. return getEscalator().getBody();
  429. }
  430. @Override
  431. public GridDragSourceState getState() {
  432. return (GridDragSourceState) super.getState();
  433. }
  434. }