dioxus-dnd 3.0.1

Modular, accessible drag-and-drop for Dioxus: sortable lists, kanban boards, trees, grids, file drops, multi-select, touch support and more
Documentation
# External content in

Every other pattern in the crate handles drags your app started. This one
handles drags the outside world starts: selected text, a link pulled from
another tab, rich content from another application. The browser is the
transport here, not the shared context, so the machinery is different: no
provider, no payload type parameter, the real `DataTransfer` protocol.

API reference: [api/external-content.md](../api/external-content.md).
Live demo: the [Share](https://kindintelligence.github.io/dioxus-dnd/share)
page exercises both directions of the app boundary.

## The mental model

An external drag carries no Rust value, because the thing in flight was
never yours. What the browser delivers is a `DataTransfer`: a bag of
`(format, string)` entries, each a different representation of the same
content. `ExternalDropZone` absorbs the HTML5 boilerplate (the
`preventDefault` dance, enter/leave depth counting so hovering a child does
not flicker the hover state) and decodes that bag into `ExternalPayload`
values:

- `Url` from `text/uri-list`: links dragged from the URL bar, bookmarks,
  other tabs. One URL per line; `#` lines are comments and are skipped.
- `Html` from `text/html`: rich content, such as a selection dragged off a
  page.
- `Text` from `text/plain`.

The decoded list is ordered most specific first. No provider wraps any of
this, and there is no `accepts` filter, because the browser hides the data
while a drag hovers (`DataTransfer` protected mode): what actually arrived
can only be judged at drop time.

In-app drags never reach these zones. The core `Draggable` moves on pointer
events, not the native drag protocol, so an `ExternalDropZone` only ever
sees drags that crossed the app boundary - or came from an
`ExternalDragSource`, which is native by design (see
[Dragging out](drag-out.md)).

## A complete example

A reading list that accepts links dragged in from anywhere:

```rust,ignore
use dioxus::prelude::*;
use dioxus_dnd::prelude::*;

#[component]
fn ReadingList() -> Element {
    let mut links = use_signal(Vec::<String>::new);
    rsx! {
        ExternalDropZone {
            class: "rounded-xl border-2 border-dashed p-6 data-over:border-blue-500",
            on_drop: move |d: ExternalDrop| {
                if let Some(url) = d.url() {
                    links.write().push(url.to_string());
                }
            },
            "Drop a link here"
        }
        for url in links() {
            div { "{url}" }
        }
    }
}
```

While any drag hovers, the wrapper div carries `data-over="true"` (absent
otherwise), so highlighting is a CSS selector away. `on_hover` fires with
`true`/`false` on enter and leave when you need the signal in Rust.

## Reading the drop

`ExternalDrop` hands you everything the source offered: `payloads` (all
representations, most specific first), `files` (external drags can carry
those too), and `client`/`element` drop coordinates. Three helpers cover
the common questions:

- `best()` is the most specific payload, whatever it is.
- `url()` is the first URL, already parsed out of the `text/uri-list`.
- `text()` is the first plain-text payload.

A drop that decodes to nothing (no known formats, no files) is swallowed:
`on_drop` never fires for it. If files are your actual subject, prefer
`FileDropZone` from [File drops](file-drops.md), which adds filtering; the
`files` field here exists so a mixed drag loses nothing.

## Every payload is hostile

External payloads are attacker-controlled input, full stop.

- `ExternalPayload::Html` is arbitrary markup. Rendering it raw via
  `dangerous_inner_html` is stored or reflected XSS; sanitize first.
- `ExternalPayload::Url` may use any scheme, including `javascript:` and
  `data:`. Scheme-check before navigating to it or building an anchor
  from it.

The outbound direction guards this for you (generated anchors escape and
refuse dangerous schemes); inbound, the judgment is yours.

## Typed drops between your own apps

With the `serde` feature the boundary also speaks types. `TypedDropZone<T>`
decodes drops whose `DataTransfer` carries JSON under
`external::typed::MIME` (`application/json`) into a `TypedDrop<T>`. The
sending side is `TypedDragSource` from [Dragging out](drag-out.md), which
serializes a `Serialize` payload at drag start. Together they are the wire
between two separate Dioxus apps: two browser tabs running different
deployments, a web app and a desktop app, yours and someone else's.

Delivery is honest about the three cases: a drag with no typed entry at
all is silently ignored (it was never a typed drag), a typed entry that
decodes as `T` fires `on_drop`, and a typed entry whose JSON fails to
decode fires `on_invalid` with the error. The raw `external::typed::store`
and `retrieve` functions are public for custom handlers, and the format is
wire-compatible with dioxus-html's own `DataTransfer::store`/`retrieve`
helpers.

Between windows of one app, do not reach for this. A `DndWorld` carries
live Rust payloads through shared state with no serialization and no
`Serialize` bound; see [Multi-window drags](multi-window.md). The typed
transport exists precisely for the case `DndWorld` cannot reach: two
processes that share no memory.

## Gotchas

- **Hover cannot see the data.** `DataTransfer` protected mode hides
  content until drop, and Dioxus exposes no `types` list, so `data-over`
  lights for any drag hovering the zone - including drags a `TypedDropZone`
  will end up ignoring. Acceptance happens at drop time only.
- **Absent formats read as empty strings.** The DOM's `getData` returns
  `""` rather than null for formats a drag does not carry. `classify` and
  `typed::retrieve` both guard against this; treat the empty string as
  "not present" in any custom handler too.
- **Windows desktop trades file drops for `DataTransfer`.** wry's drop
  handler and HTML5 drag-and-drop are mutually exclusive per window, so a
  desktop window using the typed transport there gives up native file
  drops. See the platform notes in the crate README.
- **`serde` gates the typed half only.** `ExternalDropZone`,
  `ExternalDrop`, `ExternalPayload` and `classify` are always available;
  `TypedDropZone`, `TypedDrop` and `external::typed` need
  `features = ["serde"]`.

## Related

- [Dragging out]drag-out.md: the mirror direction, including the typed
  sending side.
- [File drops]file-drops.md: OS file drops with filtering.
- [Multi-window drags]multi-window.md: live payloads between windows of
  one app, no serialization.