Skip to main content

dioxus_dnd/
dragout.rs

1//! Drag *out* of your app — the mirror of [`crate::external`]. Wrap content
2//! in [`ExternalDragSource`] and users can drag it into other browser tabs,
3//! the URL bar, text editors, or any application that accepts the standard
4//! `DataTransfer` formats.
5//!
6//! ```rust,ignore
7//! ExternalDragSource {
8//!     content: OutboundContent::url("https://dioxuslabs.com", Some("Dioxus")),
9//!     a { href: "https://dioxuslabs.com", "Dioxus" }
10//! }
11//! ```
12//!
13//! No provider needed — the browser is the transport here, not the shared
14//! context. (For dragging typed Rust payloads between two of *your own*
15//! windows, see `external::typed` with the `serde` feature.)
16
17use dioxus::prelude::*;
18
19use crate::core::DropEffect;
20
21/// What to place on the outbound `DataTransfer`.
22#[derive(Debug, Clone, PartialEq)]
23pub enum OutboundContent {
24    /// Plain text (`text/plain`).
25    Text(String),
26    /// A link: written as `text/uri-list` *and* `text/plain` (and, with a
27    /// title, `text/html` as an anchor) so maximal targets understand it.
28    Url {
29        url: String,
30        /// Optional human title, used for the HTML representation.
31        title: Option<String>,
32    },
33    /// Rich content: `text/html` plus a plain-text fallback.
34    Html {
35        html: String,
36        /// Written as `text/plain` for targets that don't take HTML.
37        fallback_text: String,
38    },
39    /// Raw `(format, data)` pairs, written verbatim in order.
40    Custom(Vec<(String, String)>),
41}
42
43impl OutboundContent {
44    /// Convenience constructor for [`OutboundContent::Url`].
45    pub fn url(url: impl Into<String>, title: Option<&str>) -> Self {
46        Self::Url {
47            url: url.into(),
48            title: title.map(str::to_string),
49        }
50    }
51
52    /// The `(format, data)` pairs this content writes, in order. Pure, for
53    /// testability.
54    pub fn entries(&self) -> Vec<(String, String)> {
55        match self {
56            OutboundContent::Text(t) => vec![("text/plain".into(), t.clone())],
57            OutboundContent::Url { url, title } => {
58                let mut out = vec![
59                    ("text/uri-list".into(), url.clone()),
60                    ("text/plain".into(), url.clone()),
61                ];
62                if let Some(title) = title {
63                    out.push((
64                        "text/html".into(),
65                        format!(r#"<a href="{url}">{title}</a>"#),
66                    ));
67                }
68                out
69            }
70            OutboundContent::Html {
71                html,
72                fallback_text,
73            } => vec![
74                ("text/html".into(), html.clone()),
75                ("text/plain".into(), fallback_text.clone()),
76            ],
77            OutboundContent::Custom(pairs) => pairs.clone(),
78        }
79    }
80}
81
82/// Makes its children draggable *out of the app*, populating the native
83/// `DataTransfer` on drag start.
84#[component]
85pub fn ExternalDragSource(
86    /// The content written to the drag's `DataTransfer`.
87    content: OutboundContent,
88    /// Effect advertised to the receiving application. Defaults to `Copy`,
89    /// which is what outbound drags almost always mean.
90    #[props(default = DropEffect::Copy)]
91    effect: DropEffect,
92    /// Disable without unmounting.
93    #[props(default)]
94    disabled: bool,
95    #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
96    children: Element,
97) -> Element {
98    rsx! {
99        div {
100            draggable: !disabled,
101            ondragstart: move |evt: DragEvent| {
102                if disabled {
103                    return;
104                }
105                evt.stop_propagation();
106                let dt = evt.data_transfer();
107                for (format, data) in content.entries() {
108                    let _ = dt.set_data(&format, &data);
109                }
110                dt.set_effect_allowed(effect.as_str());
111            },
112            ..attributes,
113            {children}
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn url_content_covers_all_formats() {
124        let c = OutboundContent::url("https://example.com", Some("Example"));
125        let e = c.entries();
126        assert_eq!(e[0].0, "text/uri-list");
127        assert_eq!(e[1], ("text/plain".into(), "https://example.com".into()));
128        assert!(e[2].1.contains(r#"href="https://example.com""#));
129
130        // no title → no html entry
131        assert_eq!(OutboundContent::url("https://x.y", None).entries().len(), 2);
132    }
133}