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}