-
Notifications
You must be signed in to change notification settings - Fork 709
Drop Effects Design
HTML5’s drag & drop specification defines multiple drop effects, namely move
, copy
and link
. The spec does not actually define the semantics of what these effects do and we will leave that part to the developers using the directive (by just invoking the correct callbacks such as dnd-moved or dnd-copied). It does, however, define that the drop effect “controls the drag-and-drop feedback that the user is given during a drag-and-drop operation”, i.e. the browser should show different cursors.
- The developer can limit the available effects on dnd-draggable and dnd-list using the dnd-effect-allowed attribute. All options of the HTML5 spec can be used, namely
move
,copy
,link
,all
,copyMove
,copyLink
andlinkMove
. - The user can choose the drop effect using modifier keys (Ctrl and Alt) if there are multiple effects allowed (e.g.
all
). - The default effect is always move if it is allowed. Otherwise
copy
is preferred overlink
. Note that this precedence is not defined by the HTML5 spec, but rather something we choose for angular-drag-and-drop-lists. - The cursor always indicates the currently chosen drop effect. If this is not possible due to browser limitations, we make sure to never show the copy or link cursors when not actually using those drop effects, i.e. we prefer showing the move cursor when actually performing a copy over showing a copy cursor when actually performing a move.
These goals were only partly accomplished in v1 of angular-drag-and-drop-lists, and there were multiple issues with it.
First of all, we need to find out how the browsers that we want to support actually behave. Therefore, I tested Chrome, Firefox, Safari, IE and Edge using this fiddle https://jsfiddle.net/wd8zp8fn/.
- Standard: In dropzone events (e.g. dragover, drop) most browsers set dataTransfer.effectAllowed to the same value that we set in the dragstart event.
-
Safari on Mac: Safari on Mac always passes effectAllowed as
all
. However, it does limit the effectAllowed if the user presses modifier keys, i.e. when pressing Alt the effectAllowed becomescopy
and when pressing Ctrl it becomeslink
. If the user presses both Ctrl and Alt then the effectAllowed iscopyLink
. - Chrome on Mac: Chrome on Mac sends the same effectAllowed that we set in dragstart, but it also limits it if the user presses modifier keys, in the same way Safari does. This is actually the nicest behavior out there, if all browsers followed this then we wouldn’t need to implement anything to reach our above goals.
- IE: Internet Explorer throws an exception if the drop is coming from a different document and you are trying to access effectAllowed on the dataTransfer object.
-
Standard: Most browsers always pass dataTransfer.dropEffect as
none
in dropzone events. -
Firefox: Firefox chooses a drop effect for us depending on which effectAllowed we set in dragstart. The
dropEffect
precedence ismove
,copy
andlink
in that order. The user can also hold Ctrl to prefer copy over move if allowed. On Linux,link
seems to have higher precedence thanmove
. -
IE: If the effectAllowed that we set in dragstart is either
move
,copy
orlink
, then the dropEffect is set to this value. Otherwise, if we allowed multiple effects, then the dropEffect is alwaysnone
. -
Edge: Edge chooses a drop effect for us depending on which effectAllowed we set in dragstart. The
dropEffect
precedence iscopy
,link
andmove
in that order.
- Standard: Nearly all browsers allow us to choose the cursor to be displayed by setting dataTransfer.dropEffect to either move, copy or link in the dragover event. Note that the dropEffect we choose should align with the effectAllowed, otherwise the browser might decide not to allow the drop at all.
- Edge: Edge always shows a move cursor, and there doesn’t seem to be a way to get a copy or link cursor, even if we disallow a move effect via effectAllowed in dragstart.
- IE: The cursor is determined in dragstart and can not be changed. It is either link, copy or move (in that order), depending which is allowed.
Although the browser implementations are vastly differing, there is a way to reach our goals from above. The basic approach is to choose a dropEffect in the dragover and drop events based on the effectAllowed provided. We can utilize event.ctrlKey and event.altKey to implement the user choice. However, we still need two workarounds:
- Safari will always send all as effectAllowed. We therefore need to keep a global state with the actual effectAllowed.
- IE’s choice of cursors has the wrong precedence for us and does not allow to change the cursor in dragover. For example, if effectAllowed is copyMove, then there is no way for us to change the copy cursor to a move cursor, which is pretty bad for the user. As a workaround, if we’re in IE and there are multiple drop effects allowed, we choose one (usually move) in dragstart and only give that one to the browser. In the dropzone we then ignore the effectAllowed passed by the browser and only use our internal state. This way the user will see a move cursor if the effectAllowed is copyMove and when they drop the element a move will be performed. However, the user can still perform a copy operation by holding a modifier key, it will just not show a copy cursor.
- Store effectAllowed in dndState (needed for Safari and IE workaround)
- If IE (but not Edge) (detect by setData exception):
- Set effectAllowed to only one effect, in order to always get that cursor
- Determine dropEffect
- Start with [move, copy, link]
- Remove effects not in dataTransfer.effectAllowed
- Don’t do this if we are in IE, as we set an inaccurate dataTransfer.effectAllowed in dragstart
- Remove effects not in dndState.effectAllowed (not for external sources)
- If dnd-effect-allowed is set, remove effects further
- If ctrlKey and copy is allowed, use copy (Mac is already filtered to link in this case)
- If altKey and link is allowed, use link (Mac is already filtered to copy in this case)
- Take first of remaining options, or none if there is no such option
- If dropEffect is none, cancel drop
- Invoke dnd-dragover callback with dropEffect
- If not MSIE or external, write to dt.dropEffect, after calling event.preventDefault
- Determine dropEffect as in dragover
- If dropEffect is none, cancel drop
- Store dropEffect in dndState (required for dragend)
- If not MSIE, write to dataTransfer.dropEffect, after calling event.preventDefault
- Use what’s in dndState, as dataTransfer.dropEffect is unreliable and we probably don’t want to move to external documents anyways.