Skip to content

Instantly share code, notes, and snippets.

@alexreardon
Last active March 19, 2024 23:27
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexreardon/9ef479804a7519f713fe2274e076f1f3 to your computer and use it in GitHub Desktop.
Save alexreardon/9ef479804a7519f713fe2274e076f1f3 to your computer and use it in GitHub Desktop.
An explanation of the timing of drag and drop events

Drag and drop

This is a collection of knowledge I have built up regarding browser powered drag and drop functionality

Events

dragstart

  • timing: once as drag is starting
  • event.target: draggable Element
  • ๐Ÿ“ธ Called before drag preview image is taken
  • ๐Ÿ“ธ You can set the drag preview with event.dataTransfer.setDragImage()
  • You can set data for the drag (eg event.dataTransfer.setData(format, data))
  • ๐Ÿ’ป for iOS15 you must set a drag operations drag data (event.dataTransfer.setData('application/draggable-id', 'hi'))
  • You can set the drag effects allowed for a drag (eg event.dataTransfer.effectAllowed = 'copy';)
  • ๐Ÿšซ call event.preventDefault() to cancel dragging (useful for powering drag handles)

drag

  • timing: continually during a drag (throttled)
  • event.target: draggable Element
  • can be used to get information such as clientX,clientY
  • could also be used for edge detection (or could use dragover)
  • pretty useless right now as firefox sets clientX,clientY to 0
  • not helpful if draggable Element is removed during a drag

dragenter

  • timing: fired when entering a new element
  • event.target: potential drop target Element
  • called before "dragleave" is called on the old element ๐Ÿ˜ญ
  • ๐Ÿฉธ must call event.preventDefault() in order for Element to be a valid drop target
  • โ„น๏ธ is called on the initial draggable Element

dragleave

  • timing: fired when dragging out of an element.
  • event.target: potential drop target Element
  • called after dragenter is called on the new element ๐Ÿ˜ญ

dragover

  • timing: continually while over an Element (throttled)
  • event.target: potential drop target Element
  • ๐Ÿฉธ must call event.preventDefault() in order for Element to be a valid drop target

drop

  • timing: when a valid drop target is dropped on
  • event.target: drop target Element
  • will not fire if no valid drop target
  • ๐Ÿฉธ must call event.preventDefault() to prevent the default browser drop behaviour

dragend

  • timing: when drag has finished, whether it was successful or not
  • event.target: draggable Element
  • occurs after drop

Cancelling

When a drag operation is canceled:

  1. a dragleave event fires on the existing drop target
  2. the dataTransfer.dropEffect is set to none (but not on FF ๐Ÿ˜ข)

Drag previews (ghost images)

The browser will automatically take a snapshot image of the Element being dragged. This snapshot is what the user drags around. The snapshot is often referred to as a "ghost", "drag preview", "drag image" and more.

You can set a custom drag image by using the DataTransfer.setDragImage() function.

Options:

  1. an <img> (in the document, or not in the document). Note: The image must be decoded before "dragstart" in order to be used
  2. a <canvas>
  3. a visible Element.

Customizing the draggable Element

Commonly people want the dragging image to be a minor modification of the draggable Element (eg the same Element but with a different background color). So to do this you need to provide DataTransfer.setDragImage() with a visible Element (option 3).

Things you need to do:

  • render the draggable Element somewhere else
  • modify that Element with the changes you want
  • somehow make the new Element visible to the browser, but not to a user

This technique is a trainwreck. It is complicated and has a poor cross browser story (from my minor exploration).

A novel pattern: leveraging requestAnimationFrame

I found a (novel?) technique that allows simple and cheap creation of custom drag previews ๐Ÿคฉ

Background information about browser timing

  • Browsers execute tasks (javascript) in a task queue
  • Every ~1/60th of a second, after the task queue is empty, the browser will draw to the screen (style, layout, paint)
  • requestAnimationFrame schedules some javascript to run after the task queue, and just before drawing

Technique

  1. During a dragstart event, make any visual changes to the draggable Element that you want to be present in the drag preview image (eg a change in background color).
element.addEventListener('dragstart', () => {
  element.classList.add('drag-preview');
});
  1. ๐Ÿ“ธ After dragstart is finished, the browser will take a 'photo' of the element when the dragstart event is done

this behaviour is defined in the drag and drop processing model

  1. In your dragstart event listener, schedule a future animation frame to revert the changes that you made to the draggable Element. Because an animation frame runs before drawing (style, layout, paint) the user will never see the visual changes you made to the draggable Element in step 1
element.addEventListener('dragstart', () => {
  element.classList.add('drag-preview');

  requestAnimationFrame(() => {
    element.classList.remove('drag-preview');
  });
});

Timing summary

  1. ๐Ÿ‘ฉโ€๐ŸŽจ Make visual changes to draggable Element inside dragstart that you want to be on your drag preview
  2. ๐Ÿ“ธ The browser will take a 'photo' of the element to be used as the drag preview
  3. โŽŒ in the next animation frame revert the changes in step 1

What about long tasks?

If there is a long task in dragstart will that cause the styles applied in dragstart to be visible on the draggable Element?

Nope! A long task does not cancel the next animation frames (or paint). A long task will push back the next animation frames and browser paints.

โ†’ test case

Gotcha: CSS transform

Browser generated drag previews do not work well with CSS transforms.

Errors

What happens if there is an error in "dragstart"?

  • Chrome: drag finished with "dragend"
  • Firefox: drag continues ๐Ÿ‘€
  • Safari: drag finished with "dragend"

The behaviour is the same as when you are in a drop target element. No additional drag events are fired; "dragend" is all you get

What happens if there is an error in "dragenter", "dragleave", "drag", "dragover"?

  • Chrome: drag continues
  • Firefox: drag continues
  • Safari: drag continues

What happens if there is an error outside of a drag event? eg in a setTimeout inside of "dragstart"?

  • Chrome: drag continues
  • Firefox: drag continues
  • Safari: drag continues

Drag finishing with a removed drag source

How do you know about a drag ending if the dragging "draggable" element has been removed?

  • "dragend" fires on the source element, so if that element is gone you won't get a "dragend"
  • You will get a "drop" event if there was a successful drop on a drop target. "drop" comes just before "dragend" and is dispatched on the drop target and not the "draggable" element
  • You can listen for other input events (eg mouse, pointer or keyboard events) to see if the drag is finished. If you see any of the following events during what you think is a drag operation, then you know the drag has finished:
    • If you see a "dragstart" event (a new drag is starting)
    • If you see a "pointerdown" event (these events happen before a "dragstart", so if you see one, then a drag is definitely finished - and maybe another one is about to start!)
    • If you see any user input events then you know that the last drag operation has finished

      HTML Spec: From the moment that the user agent is to initiate the drag-and-drop operation, until the end of the drag-and-drop operation, device input events (e.g. mouse and keyboard events) must be suppressed.

      • Sadly there is a bug in firefox where it lets through the first few user events during a drag (most I have seen is three). So what I do is listener for "pointermove". If I see > 20 of those events, then I know the last drag finished by an error. react-dnd waits 1s and then adds a "mousemove" listener which is used for the same purpose. Might want to explore also counting keyboard events

Quirks

event.dataTransfer.items

You can use event.dataTransfer.items to extract what entities are being dragged; most commonly useful for file dropping.

event.dataTransfer.items is a DataTransferItemList which is not an Array. DataTransferItemList does implement Symbol.iterator so you can use it in loops, or convert it to an Array.

What is the value of event.dataTransfer.items in "dragstart"

  • Chrome: items is populated (but items have identifying data stripped)
  • Safari: items is empty (this is correct as items should be disabled at this time)
  • Firefox: items is populated ๐Ÿ‘€

What happens if you try to access an item in "dragstart"

Do something like: event.dataTransfer.items[0].getAsFile

  • Chrome: you get null (makes sense, items have had their information stripped)
  • Safari: TypeError (makes sense, items is empty so we are doing undefined.getAsFile())
  • Firefox: you get null (respecting the security model)

Is event.dataTransfer.items live?

live: the collection will be mutated in place as changes occur

To put this question more concretely:

Will event.dataTransfer.items from "dragstart" become enabled (can you hold onto it?) or do you need to grab it again in "drop"?

If items is not live then you need to look the value up from the "drop" event

Generally, items is restricted so you can only access it properly in the "drop" event.

  • Chrome: not live
  • Firefox: not live (appears live, but if you try to access an item in "drop" you won't get anything)
  • Safari: not live

Can you find out how many files are being dragged during a drag?

  • Chrome: items.length is the correct amount of files
  • Safari: items.length is 0!
  • Firefox: items.length is the correct amount of files

You cannot reliably know how many files are being dragged, so don't rely on .length

Findings

Do not use event.dataTransfer.items in "dragstart" or hold onto it for future use; collect .items again in "drop"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment