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

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