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.

DropTargetExtensionConnector.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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.extensions;
  17. import java.util.ArrayList;
  18. import java.util.HashMap;
  19. import java.util.HashSet;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.Set;
  23. import com.google.gwt.core.client.JsArrayString;
  24. import com.google.gwt.dom.client.DataTransfer;
  25. import com.google.gwt.dom.client.Element;
  26. import com.google.gwt.dom.client.NativeEvent;
  27. import com.google.gwt.user.client.ui.Widget;
  28. import com.vaadin.client.BrowserInfo;
  29. import com.vaadin.client.MouseEventDetailsBuilder;
  30. import com.vaadin.client.ServerConnector;
  31. import com.vaadin.client.ui.AbstractComponentConnector;
  32. import com.vaadin.shared.MouseEventDetails;
  33. import com.vaadin.shared.ui.Connect;
  34. import com.vaadin.shared.ui.dnd.DropEffect;
  35. import com.vaadin.shared.ui.dnd.DropTargetRpc;
  36. import com.vaadin.shared.ui.dnd.DropTargetState;
  37. import com.vaadin.shared.ui.dnd.criteria.Payload;
  38. import com.vaadin.ui.dnd.DropTargetExtension;
  39. import elemental.events.Event;
  40. import elemental.events.EventListener;
  41. import elemental.events.EventTarget;
  42. /**
  43. * Extension to add drop target functionality to a widget for using HTML5 drag
  44. * and drop. Client side counterpart of {@link DropTargetExtension}.
  45. *
  46. * @author Vaadin Ltd
  47. * @since 8.1
  48. */
  49. @Connect(DropTargetExtension.class)
  50. public class DropTargetExtensionConnector extends AbstractExtensionConnector {
  51. /**
  52. * Style name suffix for dragging data over the center of the drop target.
  53. */
  54. protected static final String STYLE_SUFFIX_DRAG_CENTER = "-drag-center";
  55. /**
  56. * Style name suffix for dragging data over the top part of the drop target.
  57. */
  58. protected static final String STYLE_SUFFIX_DRAG_TOP = "-drag-top";
  59. /**
  60. * Style name suffix for dragging data over the bottom part of the drop
  61. * target.
  62. */
  63. protected static final String STYLE_SUFFIX_DRAG_BOTTOM = "-drag-bottom";
  64. /**
  65. * Style name suffix for indicating that the element is drop target.
  66. */
  67. protected static final String STYLE_SUFFIX_DROPTARGET = "-droptarget";
  68. // Create event listeners
  69. private final EventListener dragEnterListener = this::onDragEnter;
  70. private final EventListener dragOverListener = this::onDragOver;
  71. private final EventListener dragLeaveListener = this::onDragLeave;
  72. private final EventListener dropListener = this::onDrop;
  73. private Widget dropTargetWidget;
  74. /**
  75. * Class name to apply when an element is dragged over the center of the
  76. * target.
  77. */
  78. private String styleDragCenter;
  79. @Override
  80. protected void extend(ServerConnector target) {
  81. dropTargetWidget = ((AbstractComponentConnector) target).getWidget();
  82. // HTML5 DnD is by default not enabled for mobile devices
  83. if (BrowserInfo.get().isTouchDevice() && !getConnection()
  84. .getUIConnector().isMobileHTML5DndEnabled()) {
  85. return;
  86. }
  87. addDropListeners(getDropTargetElement());
  88. ((AbstractComponentConnector) target).onDropTargetAttached();
  89. // Add drop target indicator to the drop target element
  90. addDropTargetStyle();
  91. }
  92. /**
  93. * Adds dragenter, dragover, dragleave and drop event listeners to the given
  94. * DOM element.
  95. *
  96. * @param element
  97. * DOM element to attach event listeners to.
  98. */
  99. private void addDropListeners(Element element) {
  100. EventTarget target = element.cast();
  101. target.addEventListener(Event.DRAGENTER, dragEnterListener);
  102. target.addEventListener(Event.DRAGOVER, dragOverListener);
  103. target.addEventListener(Event.DRAGLEAVE, dragLeaveListener);
  104. target.addEventListener(Event.DROP, dropListener);
  105. }
  106. /**
  107. * Removes dragenter, dragover, dragleave and drop event listeners from the
  108. * given DOM element.
  109. *
  110. * @param element
  111. * DOM element to remove event listeners from.
  112. */
  113. private void removeDropListeners(Element element) {
  114. EventTarget target = element.cast();
  115. target.removeEventListener(Event.DRAGENTER, dragEnterListener);
  116. target.removeEventListener(Event.DRAGOVER, dragOverListener);
  117. target.removeEventListener(Event.DRAGLEAVE, dragLeaveListener);
  118. target.removeEventListener(Event.DROP, dropListener);
  119. }
  120. @Override
  121. public void onUnregister() {
  122. AbstractComponentConnector parent = (AbstractComponentConnector) getParent();
  123. // parent is null when the component has been removed,
  124. // clean up only if only the extension was removed
  125. if (parent != null) {
  126. parent.onDropTargetDetached();
  127. removeDropListeners(getDropTargetElement());
  128. // Remove drop target indicator
  129. removeDropTargetStyle();
  130. dropTargetWidget = null;
  131. }
  132. super.onUnregister();
  133. }
  134. /**
  135. * Finds the drop target element within the widget. By default, returns the
  136. * topmost element.
  137. *
  138. * @return the drop target element in the parent widget.
  139. */
  140. protected Element getDropTargetElement() {
  141. return dropTargetWidget.getElement();
  142. }
  143. /**
  144. * Event handler for the {@code dragenter} event.
  145. * <p>
  146. * Override this method in case custom handling for the dragstart event is
  147. * required. If the drop is allowed, the event should prevent default.
  148. *
  149. * @param event
  150. * browser event to be handled
  151. */
  152. protected void onDragEnter(Event event) {
  153. NativeEvent nativeEvent = (NativeEvent) event;
  154. // Generate style name for drop target
  155. styleDragCenter = dropTargetWidget.getStylePrimaryName()
  156. + STYLE_SUFFIX_DRAG_CENTER;
  157. if (isDropAllowed(nativeEvent)) {
  158. addDragOverStyle(nativeEvent);
  159. setDropEffect(nativeEvent);
  160. // According to spec, need to call this for allowing dropping, the
  161. // default action would be to reject as target
  162. event.preventDefault();
  163. } else {
  164. // Remove drop effect
  165. nativeEvent.getDataTransfer()
  166. .setDropEffect(DataTransfer.DropEffect.NONE);
  167. }
  168. }
  169. /**
  170. * Set the drop effect for the dragenter / dragover event, if one has been
  171. * set from server side.
  172. * <p>
  173. * From Moz Foundation: "You can modify the dropEffect property during the
  174. * dragenter or dragover events, if for example, a particular drop target
  175. * only supports certain operations. You can modify the dropEffect property
  176. * to override the user effect, and enforce a specific drop operation to
  177. * occur. Note that this effect must be one listed within the effectAllowed
  178. * property. Otherwise, it will be set to an alternate value that is
  179. * allowed."
  180. *
  181. * @param event
  182. * the dragenter or dragover event.
  183. */
  184. private void setDropEffect(NativeEvent event) {
  185. if (getState().dropEffect != null) {
  186. DataTransfer.DropEffect dropEffect = DataTransfer.DropEffect
  187. // the valueOf() needs to have equal string and name()
  188. // doesn't return in all upper case
  189. .valueOf(getState().dropEffect.name().toUpperCase());
  190. event.getDataTransfer().setDropEffect(dropEffect);
  191. }
  192. }
  193. /**
  194. * Event handler for the {@code dragover} event.
  195. * <p>
  196. * Override this method in case custom handling for the dragover event is
  197. * required. If the drop is allowed, the event should prevent default.
  198. *
  199. * @param event
  200. * browser event to be handled
  201. */
  202. protected void onDragOver(Event event) {
  203. NativeEvent nativeEvent = (NativeEvent) event;
  204. if (isDropAllowed(nativeEvent)) {
  205. setDropEffect(nativeEvent);
  206. // Add drag over indicator in case the element doesn't have one
  207. addDragOverStyle(nativeEvent);
  208. // Prevent default to allow drop
  209. nativeEvent.preventDefault();
  210. nativeEvent.stopPropagation();
  211. } else {
  212. // Remove drop effect
  213. nativeEvent.getDataTransfer()
  214. .setDropEffect(DataTransfer.DropEffect.NONE);
  215. // Remove drag over indicator
  216. removeDragOverStyle(nativeEvent);
  217. }
  218. }
  219. /**
  220. * Event handler for the {@code dragleave} event.
  221. * <p>
  222. * Override this method in case custom handling for the dragleave event is
  223. * required.
  224. *
  225. * @param event
  226. * browser event to be handled
  227. */
  228. protected void onDragLeave(Event event) {
  229. removeDragOverStyle((NativeEvent) event);
  230. }
  231. /**
  232. * Event handler for the {@code drop} event.
  233. * <p>
  234. * Override this method in case custom handling for the drop event is
  235. * required. If the drop is allowed, the event should prevent default.
  236. *
  237. * @param event
  238. * browser event to be handled
  239. */
  240. protected void onDrop(Event event) {
  241. NativeEvent nativeEvent = (NativeEvent) event;
  242. if (isDropAllowed(nativeEvent)) {
  243. JsArrayString typesJsArray = getTypes(
  244. nativeEvent.getDataTransfer());
  245. /* Handle event if transfer doesn't contain files.
  246. *
  247. * Spec: "Dragging files can currently only happen from outside a
  248. * browsing context, for example from a file system manager
  249. * application."
  250. * Thus there cannot be at the same time both files and other data
  251. * dragged
  252. */
  253. if (!containsFiles(typesJsArray)) {
  254. nativeEvent.preventDefault();
  255. nativeEvent.stopPropagation();
  256. List<String> types = new ArrayList<>();
  257. Map<String, String> data = new HashMap<>();
  258. for (int i = 0; i < typesJsArray.length(); i++) {
  259. String type = typesJsArray.get(i);
  260. types.add(type);
  261. data.put(type, nativeEvent.getDataTransfer().getData(type));
  262. }
  263. sendDropEventToServer(types, data, DragSourceExtensionConnector
  264. .getDropEffect(nativeEvent.getDataTransfer()), nativeEvent);
  265. }
  266. }
  267. removeDragOverStyle(nativeEvent);
  268. }
  269. private boolean isDropAllowed(NativeEvent event) {
  270. // there never should be a drop when effect has been set to none
  271. if (getState().dropEffect != null
  272. && getState().dropEffect == DropEffect.NONE) {
  273. return false;
  274. }
  275. // TODO #9246: Should add verification for checking effectAllowed and
  276. // dropEffect from event and comparing that to target's dropEffect.
  277. // Currently Safari, Edge and IE don't follow the spec by allowing drop
  278. // if those don't match
  279. // Allow by default when criteria not set
  280. boolean allowed = true;
  281. // Execute criteria script
  282. if (getState().criteriaScript != null) {
  283. allowed = executeScript(event, getState().criteriaScript);
  284. }
  285. // Execute criterion defined via API
  286. if (allowed && getState().criteria != null && !getState().criteria
  287. .isEmpty()) {
  288. // Collect payload data types
  289. Set<Payload> payloadSet = new HashSet<>();
  290. JsArrayString typesJsArray = getTypes(event.getDataTransfer());
  291. for (int i = 0; i < typesJsArray.length(); i++) {
  292. String type = typesJsArray.get(i);
  293. if (type.startsWith(Payload.ITEM_PREFIX)) {
  294. payloadSet.add(Payload.parse(type));
  295. }
  296. }
  297. // Compare payload against criteria
  298. switch (getState().criteriaMatch) {
  299. case ALL:
  300. allowed = getState().criteria.stream()
  301. .allMatch(criterion -> criterion.resolve(payloadSet));
  302. break;
  303. case ANY:
  304. default:
  305. allowed = getState().criteria.stream()
  306. .anyMatch(criterion -> criterion.resolve(payloadSet));
  307. }
  308. }
  309. return allowed;
  310. }
  311. /**
  312. * Tells if the given array of types contains files.
  313. * <p>
  314. * According to HTML specification, if any files are being dragged, {@code
  315. * dataTransfer.types} will contain the string "Files". See
  316. * https://html.spec.whatwg.org/multipage/interaction.html#the-datatransfer-interface:dom-datatransfer-types-2
  317. *
  318. * @param types
  319. * Array of data types.
  320. * @return {@code} true if given array contains {@code "Files"}, {@code
  321. * false} otherwise.
  322. */
  323. private boolean containsFiles(JsArrayString types) {
  324. for (int i = 0; i < types.length(); i++) {
  325. if ("Files".equals(types.get(i))) {
  326. return true;
  327. }
  328. }
  329. return false;
  330. }
  331. /**
  332. * Initiates a server RPC for the drop event.
  333. *
  334. * @param types
  335. * List of data types from {@code DataTransfer.types} object.
  336. * @param data
  337. * Map containing all types and corresponding data from the
  338. * {@code
  339. * DataTransfer} object.
  340. * @param dropEffect
  341. * The desired drop effect.
  342. */
  343. protected void sendDropEventToServer(List<String> types,
  344. Map<String, String> data, String dropEffect,
  345. NativeEvent dropEvent) {
  346. // Build mouse event details for the drop event
  347. MouseEventDetails mouseEventDetails = MouseEventDetailsBuilder
  348. .buildMouseEventDetails(dropEvent, getDropTargetElement());
  349. // Send data to server with RPC
  350. getRpcProxy(DropTargetRpc.class)
  351. .drop(types, data, dropEffect, mouseEventDetails);
  352. }
  353. /**
  354. * Add class name for the drop target element indicating that data can be
  355. * dropped onto it. The class name has the following format:
  356. *
  357. * <pre>
  358. * [primaryStyleName]-droptarget
  359. * </pre>
  360. *
  361. * The added class name is update automatically by the framework when the
  362. * primary style name changes.
  363. */
  364. protected void addDropTargetStyle() {
  365. getDropTargetElement()
  366. .addClassName(getStylePrimaryName(getDropTargetElement())
  367. + STYLE_SUFFIX_DROPTARGET);
  368. }
  369. /**
  370. * Remove class name from the drop target element indication that data can
  371. * be dropped onto it.
  372. */
  373. protected void removeDropTargetStyle() {
  374. getDropTargetElement()
  375. .removeClassName(getStylePrimaryName(getDropTargetElement())
  376. + STYLE_SUFFIX_DROPTARGET);
  377. }
  378. /**
  379. * Add class that indicates that the component is a target while data is
  380. * being dragged over it.
  381. * <p>
  382. * This is triggered on {@link #onDragEnter(Event) dragenter} and
  383. * {@link #onDragOver(Event) dragover} events pending if the drop is
  384. * possible. The drop is possible if the drop effect for the target and
  385. * source do match and the drop criteria script evaluates to true or is not
  386. * set.
  387. *
  388. * @param event
  389. * the dragenter or dragover event that triggered the indication.
  390. */
  391. protected void addDragOverStyle(NativeEvent event) {
  392. getDropTargetElement().addClassName(styleDragCenter);
  393. }
  394. /**
  395. * Remove the drag over indicator class name from the target element.
  396. * <p>
  397. * This is triggered on {@link #onDrop(Event) drop},
  398. * {@link #onDragLeave(Event) dragleave} and {@link #onDragOver(Event)
  399. * dragover} events pending on whether the drop has happened or if it is not
  400. * possible. The drop is not possible if the drop effect for the source and
  401. * target don't match or if there is a drop criteria script that evaluates
  402. * to false.
  403. *
  404. * @param event
  405. * the event that triggered the removal of the indicator
  406. */
  407. protected void removeDragOverStyle(NativeEvent event) {
  408. getDropTargetElement().removeClassName(styleDragCenter);
  409. }
  410. private native JsArrayString getTypes(DataTransfer dataTransfer)
  411. /*-{
  412. return dataTransfer.types;
  413. }-*/;
  414. private native boolean executeScript(NativeEvent event, String script)
  415. /*-{
  416. return new Function('event', script)(event);
  417. }-*/;
  418. private native boolean getStylePrimaryName(Element element)
  419. /*-{
  420. return @com.google.gwt.user.client.ui.UIObject::getStylePrimaryName(Lcom/google/gwt/dom/client/Element;)(element);
  421. }-*/;
  422. @Override
  423. public DropTargetState getState() {
  424. return (DropTargetState) super.getState();
  425. }
  426. }