#![doc = include_str!("../docs/api/external-content.md")]
use dioxus::html::HasFileData;
use dioxus::prelude::*;
use crate::core::{client_point, element_point, Point};
#[derive(Debug, Clone, PartialEq)]
pub enum ExternalPayload {
Url(String),
Html(String),
Text(String),
}
#[derive(Clone, PartialEq)]
pub struct ExternalDrop {
pub payloads: Vec<ExternalPayload>,
pub files: Vec<dioxus::html::FileData>,
pub client: Point,
pub element: Point,
}
impl ExternalDrop {
pub fn best(&self) -> Option<&ExternalPayload> {
self.payloads.first()
}
pub fn url(&self) -> Option<&str> {
self.payloads.iter().find_map(|p| match p {
ExternalPayload::Url(u) => Some(u.as_str()),
_ => None,
})
}
pub fn text(&self) -> Option<&str> {
self.payloads.iter().find_map(|p| match p {
ExternalPayload::Text(t) => Some(t.as_str()),
_ => None,
})
}
}
pub fn classify(evt: &DragEvent) -> Vec<ExternalPayload> {
let dt = evt.data_transfer();
let mut out = Vec::new();
if let Some(uris) = dt.get_data("text/uri-list") {
for line in uris.lines() {
let line = line.trim();
if !line.is_empty() && !line.starts_with('#') {
out.push(ExternalPayload::Url(line.to_string()));
}
}
}
if let Some(html) = dt.get_data("text/html") {
if !html.is_empty() {
out.push(ExternalPayload::Html(html));
}
}
if let Some(text) = dt.get_data("text/plain") {
if !text.is_empty() {
out.push(ExternalPayload::Text(text));
}
}
out
}
#[component]
pub fn ExternalDropZone(
on_drop: EventHandler<ExternalDrop>,
#[props(default)]
on_hover: Option<EventHandler<bool>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let mut depth = use_signal(|| 0u32);
let mut attributes = attributes;
crate::core::components::protect_attributes(
&mut attributes,
&[
"data-over",
"ondragover",
"ondragenter",
"ondragleave",
"ondrop",
],
);
rsx! {
div {
"data-over": if depth() > 0 { "true" },
ondragover: move |evt: DragEvent| {
evt.prevent_default();
},
ondragenter: move |evt: DragEvent| {
evt.prevent_default();
let d = depth() + 1;
depth.set(d);
if d == 1 {
if let Some(h) = &on_hover {
h.call(true);
}
}
},
ondragleave: move |_| {
let d = depth().saturating_sub(1);
depth.set(d);
if d == 0 {
if let Some(h) = &on_hover {
h.call(false);
}
}
},
ondrop: move |evt: DragEvent| {
evt.prevent_default();
depth.set(0);
if let Some(h) = &on_hover {
h.call(false);
}
let payloads = classify(&evt);
let files = evt.files();
if payloads.is_empty() && files.is_empty() {
return;
}
on_drop.call(ExternalDrop {
payloads,
files,
client: client_point(&evt),
element: element_point(&evt),
});
},
..attributes,
{children}
}
}
}
#[cfg(feature = "serde")]
pub mod typed {
use dioxus::html::DataTransfer;
use dioxus::prelude::*;
pub const MIME: &str = "application/json";
pub fn store_in<T: serde::Serialize>(dt: &DataTransfer, value: &T) -> Result<(), String> {
let json = serde_json::to_string(value).map_err(|e| e.to_string())?;
dt.set_data(MIME, &json)
}
pub fn retrieve_from<T: for<'de> serde::Deserialize<'de>>(
dt: &DataTransfer,
) -> Result<Option<T>, String> {
match dt.get_data(MIME) {
Some(json) if !json.trim().is_empty() => serde_json::from_str(&json)
.map(Some)
.map_err(|e| e.to_string()),
_ => Ok(None),
}
}
pub fn store<T: serde::Serialize>(evt: &DragEvent, value: &T) -> Result<(), String> {
store_in(&evt.data_transfer(), value)
}
pub fn retrieve<T: for<'de> serde::Deserialize<'de>>(
evt: &DragEvent,
) -> Result<Option<T>, String> {
retrieve_from(&evt.data_transfer())
}
}
#[cfg(feature = "serde")]
#[derive(Debug, Clone, PartialEq)]
pub struct TypedDrop<T> {
pub payload: T,
pub client: Point,
pub element: Point,
}
#[cfg(feature = "serde")]
#[component]
pub fn TypedDropZone<T: serde::de::DeserializeOwned + Clone + PartialEq + 'static>(
on_drop: EventHandler<TypedDrop<T>>,
#[props(default)]
on_invalid: Option<EventHandler<String>>,
#[props(default)]
on_hover: Option<EventHandler<bool>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let mut depth = use_signal(|| 0u32);
let mut attributes = attributes;
crate::core::components::protect_attributes(
&mut attributes,
&[
"data-over",
"ondragover",
"ondragenter",
"ondragleave",
"ondrop",
],
);
rsx! {
div {
"data-over": if depth() > 0 { "true" },
ondragover: move |evt: DragEvent| {
evt.prevent_default();
},
ondragenter: move |evt: DragEvent| {
evt.prevent_default();
let d = depth() + 1;
depth.set(d);
if d == 1 {
if let Some(h) = &on_hover {
h.call(true);
}
}
},
ondragleave: move |_| {
let d = depth().saturating_sub(1);
depth.set(d);
if d == 0 {
if let Some(h) = &on_hover {
h.call(false);
}
}
},
ondrop: move |evt: DragEvent| {
evt.prevent_default();
depth.set(0);
if let Some(h) = &on_hover {
h.call(false);
}
match typed::retrieve::<T>(&evt) {
Ok(Some(payload)) => on_drop.call(TypedDrop {
payload,
client: client_point(&evt),
element: element_point(&evt),
}),
Ok(None) => {}
Err(e) => {
if let Some(h) = &on_invalid {
h.call(e);
}
}
}
},
..attributes,
{children}
}
}
}