1use dioxus::html::HasFileData;
11use dioxus::prelude::*;
12
13use crate::core::{client_point, element_point, Point};
14
15#[derive(Debug, Clone, PartialEq)]
18pub enum ExternalPayload {
19 Url(String),
21 Html(String),
23 Text(String),
25}
26
27#[derive(Clone, PartialEq)]
29pub struct ExternalDrop {
30 pub payloads: Vec<ExternalPayload>,
32 pub files: Vec<dioxus::html::FileData>,
34 pub client: Point,
35 pub element: Point,
36}
37
38impl ExternalDrop {
39 pub fn best(&self) -> Option<&ExternalPayload> {
41 self.payloads.first()
42 }
43
44 pub fn url(&self) -> Option<&str> {
47 self.payloads.iter().find_map(|p| match p {
48 ExternalPayload::Url(u) => Some(u.as_str()),
49 _ => None,
50 })
51 }
52
53 pub fn text(&self) -> Option<&str> {
55 self.payloads.iter().find_map(|p| match p {
56 ExternalPayload::Text(t) => Some(t.as_str()),
57 _ => None,
58 })
59 }
60}
61
62pub fn classify(evt: &DragEvent) -> Vec<ExternalPayload> {
64 let dt = evt.data_transfer();
65 let mut out = Vec::new();
66 if let Some(uris) = dt.get_data("text/uri-list") {
67 for line in uris.lines() {
68 let line = line.trim();
69 if !line.is_empty() && !line.starts_with('#') {
70 out.push(ExternalPayload::Url(line.to_string()));
71 }
72 }
73 }
74 if let Some(html) = dt.get_data("text/html") {
75 if !html.is_empty() {
76 out.push(ExternalPayload::Html(html));
77 }
78 }
79 if let Some(text) = dt.get_data("text/plain") {
80 if !text.is_empty() {
81 out.push(ExternalPayload::Text(text));
82 }
83 }
84 out
85}
86
87#[component]
89pub fn ExternalDropZone(
90 on_drop: EventHandler<ExternalDrop>,
91 #[props(default)]
93 on_hover: Option<EventHandler<bool>>,
94 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
95 children: Element,
96) -> Element {
97 let mut depth = use_signal(|| 0u32);
98
99 rsx! {
100 div {
101 ondragover: move |evt: DragEvent| {
102 evt.prevent_default();
103 },
104 ondragenter: move |evt: DragEvent| {
105 evt.prevent_default();
106 let d = depth() + 1;
107 depth.set(d);
108 if d == 1 {
109 if let Some(h) = &on_hover {
110 h.call(true);
111 }
112 }
113 },
114 ondragleave: move |_| {
115 let d = depth().saturating_sub(1);
116 depth.set(d);
117 if d == 0 {
118 if let Some(h) = &on_hover {
119 h.call(false);
120 }
121 }
122 },
123 ondrop: move |evt: DragEvent| {
124 evt.prevent_default();
125 depth.set(0);
126 if let Some(h) = &on_hover {
127 h.call(false);
128 }
129 let payloads = classify(&evt);
130 let files = evt.files();
131 if payloads.is_empty() && files.is_empty() {
132 return;
133 }
134 on_drop.call(ExternalDrop {
135 payloads,
136 files,
137 client: client_point(&evt),
138 element: element_point(&evt),
139 });
140 },
141 ..attributes,
142 {children}
143 }
144 }
145}
146
147#[cfg(feature = "serde")]
153pub mod typed {
154 use dioxus::prelude::*;
155
156 pub fn store<T: serde::Serialize>(evt: &DragEvent, value: &T) -> Result<(), String> {
158 let json = serde_json::to_string(value).map_err(|e| e.to_string())?;
159 evt.data_transfer().set_data("application/json", &json)
160 }
161
162 pub fn retrieve<T: for<'de> serde::Deserialize<'de>>(
164 evt: &DragEvent,
165 ) -> Result<Option<T>, String> {
166 match evt.data_transfer().get_data("application/json") {
167 Some(json) => serde_json::from_str(&json)
168 .map(Some)
169 .map_err(|e| e.to_string()),
170 None => Ok(None),
171 }
172 }
173}