Skip to main content

dioxus_dnd/
external.rs

1//! Drops arriving from *outside* your app - selected text, links dragged from
2//! another tab, content from other applications - plus typed serde payloads
3//! over the `DataTransfer` bridge for interop scenarios the Rust-side
4//! context can't reach.
5//!
6//! For drags between elements of your own app, prefer the core context: it
7//! carries any `Clone` type with zero serialization. Reach for this module
8//! when the *browser* is the transport.
9
10use dioxus::html::HasFileData;
11use dioxus::prelude::*;
12
13use crate::core::{client_point, element_point, Point};
14
15/// Content the browser handed us from an external drag, best-effort decoded
16/// in order of specificity.
17///
18/// **Untrusted input.** These payloads come from outside your app and are
19/// fully attacker-controlled. Treat them like any other external data:
20/// - [`ExternalPayload::Html`] is arbitrary markup - sanitize it before
21///   rendering via `dangerous_inner_html` (raw insertion is stored/reflected
22///   XSS).
23/// - [`ExternalPayload::Url`] may carry a `javascript:` or `data:` scheme -
24///   scheme-check before navigating to it or building an anchor from it.
25#[derive(Debug, Clone, PartialEq)]
26pub enum ExternalPayload {
27    /// `text/uri-list` - links dragged from the URL bar, bookmarks, other tabs.
28    /// May use any scheme; validate before use.
29    Url(String),
30    /// `text/html` - rich content (e.g. a selection dragged from a page).
31    /// Arbitrary untrusted markup; sanitize before rendering.
32    Html(String),
33    /// `text/plain`.
34    Text(String),
35}
36
37/// A decoded external drop.
38#[derive(Clone, PartialEq)]
39pub struct ExternalDrop {
40    /// All representations the browser offered, most specific first.
41    pub payloads: Vec<ExternalPayload>,
42    /// Files, if the drag carried any (also see [`crate::files`]).
43    pub files: Vec<dioxus::html::FileData>,
44    pub client: Point,
45    pub element: Point,
46}
47
48impl ExternalDrop {
49    /// The most specific text-ish payload, if any.
50    pub fn best(&self) -> Option<&ExternalPayload> {
51        self.payloads.first()
52    }
53
54    /// First URL payload, parsed out of `text/uri-list` (one URL per line,
55    /// `#` lines are comments).
56    pub fn url(&self) -> Option<&str> {
57        self.payloads.iter().find_map(|p| match p {
58            ExternalPayload::Url(u) => Some(u.as_str()),
59            _ => None,
60        })
61    }
62
63    /// First plain-text payload.
64    pub fn text(&self) -> Option<&str> {
65        self.payloads.iter().find_map(|p| match p {
66            ExternalPayload::Text(t) => Some(t.as_str()),
67            _ => None,
68        })
69    }
70}
71
72/// Decode an incoming drag event's `DataTransfer` into [`ExternalPayload`]s.
73pub fn classify(evt: &DragEvent) -> Vec<ExternalPayload> {
74    let dt = evt.data_transfer();
75    let mut out = Vec::new();
76    if let Some(uris) = dt.get_data("text/uri-list") {
77        for line in uris.lines() {
78            let line = line.trim();
79            if !line.is_empty() && !line.starts_with('#') {
80                out.push(ExternalPayload::Url(line.to_string()));
81            }
82        }
83    }
84    if let Some(html) = dt.get_data("text/html") {
85        if !html.is_empty() {
86            out.push(ExternalPayload::Html(html));
87        }
88    }
89    if let Some(text) = dt.get_data("text/plain") {
90        if !text.is_empty() {
91            out.push(ExternalPayload::Text(text));
92        }
93    }
94    out
95}
96
97/// A zone accepting drops that originate outside the app.
98///
99/// While a drag hovers the zone the div carries `data-over="true"` (absent
100/// otherwise) for styling without `on_hover` wiring.
101#[component]
102pub fn ExternalDropZone(
103    on_drop: EventHandler<ExternalDrop>,
104    /// Fired with `true`/`false` on hover enter/leave.
105    #[props(default)]
106    on_hover: Option<EventHandler<bool>>,
107    #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
108    children: Element,
109) -> Element {
110    let mut depth = use_signal(|| 0u32);
111
112    rsx! {
113        div {
114            "data-over": if depth() > 0 { "true" },
115            ondragover: move |evt: DragEvent| {
116                evt.prevent_default();
117            },
118            ondragenter: move |evt: DragEvent| {
119                evt.prevent_default();
120                let d = depth() + 1;
121                depth.set(d);
122                if d == 1 {
123                    if let Some(h) = &on_hover {
124                        h.call(true);
125                    }
126                }
127            },
128            ondragleave: move |_| {
129                let d = depth().saturating_sub(1);
130                depth.set(d);
131                if d == 0 {
132                    if let Some(h) = &on_hover {
133                        h.call(false);
134                    }
135                }
136            },
137            ondrop: move |evt: DragEvent| {
138                evt.prevent_default();
139                depth.set(0);
140                if let Some(h) = &on_hover {
141                    h.call(false);
142                }
143                let payloads = classify(&evt);
144                let files = evt.files();
145                if payloads.is_empty() && files.is_empty() {
146                    return;
147                }
148                on_drop.call(ExternalDrop {
149                    payloads,
150                    files,
151                    client: client_point(&evt),
152                    element: element_point(&evt),
153                });
154            },
155            ..attributes,
156            {children}
157        }
158    }
159}
160
161/// Typed payloads over the native `DataTransfer` (JSON-encoded under
162/// `application/json`, wire-compatible with dioxus-html's own
163/// `store`/`retrieve`). Useful when the browser must carry the data - e.g.
164/// dragging between two separate Dioxus apps or windows - at the cost of
165/// requiring `Serialize`/`Deserialize`.
166#[cfg(feature = "serde")]
167pub mod typed {
168    use dioxus::prelude::*;
169
170    /// Store a typed payload on the drag's `DataTransfer`. Call in `ondragstart`.
171    pub fn store<T: serde::Serialize>(evt: &DragEvent, value: &T) -> Result<(), String> {
172        let json = serde_json::to_string(value).map_err(|e| e.to_string())?;
173        evt.data_transfer().set_data("application/json", &json)
174    }
175
176    /// Retrieve a typed payload from a drop's `DataTransfer`. Call in `ondrop`.
177    pub fn retrieve<T: for<'de> serde::Deserialize<'de>>(
178        evt: &DragEvent,
179    ) -> Result<Option<T>, String> {
180        match evt.data_transfer().get_data("application/json") {
181            Some(json) => serde_json::from_str(&json)
182                .map(Some)
183                .map_err(|e| e.to_string()),
184            None => Ok(None),
185        }
186    }
187}