From 0aa73514735861951b082de75f46b0bc014b4bbe Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 15 Oct 2025 01:06:13 -0700 Subject: [PATCH] datatransfer object, drag_and_drop example, fix drag-and-drop (#4789) * wip: implement datatransfer * Editable kanban contents * desktop sorta works without crashing * lint --- Cargo.toml | 5 + examples/08-apis/drag_and_drop.rs | 122 +++++++++++++ packages/desktop/src/file_upload.rs | 14 +- packages/desktop/src/webview.rs | 11 +- packages/html/src/data_transfer.rs | 180 +++++++++++++++++++ packages/html/src/events/drag.rs | 210 ++++++++++++----------- packages/html/src/lib.rs | 2 + packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 2 +- packages/interpreter/src/ts/serialize.ts | 55 ++++-- packages/web/src/data_transfer.rs | 54 ++++++ packages/web/src/events/drag.rs | 24 ++- packages/web/src/lib.rs | 3 + 13 files changed, 562 insertions(+), 122 deletions(-) create mode 100644 examples/08-apis/drag_and_drop.rs create mode 100644 packages/html/src/data_transfer.rs create mode 100644 packages/web/src/data_transfer.rs diff --git a/Cargo.toml b/Cargo.toml index cdb13aef9..07e35f6ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -790,6 +790,11 @@ name = "websocket" path = "examples/07-fullstack/websocket.rs" doc-scrape-examples = true +[[example]] +name = "drag_and_drop" +path = "examples/08-apis/drag_and_drop.rs" +doc-scrape-examples = true + [[example]] name = "control_focus" path = "examples/08-apis/control_focus.rs" diff --git a/examples/08-apis/drag_and_drop.rs b/examples/08-apis/drag_and_drop.rs new file mode 100644 index 000000000..a10bcc1f4 --- /dev/null +++ b/examples/08-apis/drag_and_drop.rs @@ -0,0 +1,122 @@ +//! This example shows how to implement a simple drag-and-drop kanban board using Dioxus. +//! You can drag items between different categories and edit their contents. +//! +//! This example uses the `.data_transfer()` API to handle drag-and-drop events. When an item is dragged, +//! its ID is stored in the data transfer object. When the item is dropped into a new category, its ID is retrieved +//! from the data transfer object and used to update the item's category. +//! +//! Note that in a real-world application, you'll want more sophisticated drop handling, such as visual +//! feedback during dragging, and better drop-zone detection to allow dropping *between* items. + +use dioxus::prelude::*; + +fn main() { + dioxus::launch(app); +} + +struct Item { + id: usize, + name: String, + category: String, + contents: String, +} + +fn app() -> Element { + let mut items = use_signal(initial_kanban_data); + + rsx! { + div { + display: "flex", + gap: "20px", + flex_direction: "row", + for category in ["A", "B", "C"] { + div { + class: "category", + display: "flex", + flex_direction: "column", + gap: "10px", + padding: "10px", + flex_grow: "1", + border: "2px solid black", + min_height: "300px", + background_color: "#f0f0f0", + ondragover: |e| e.prevent_default(), + ondrop: move |e| { + if let Some(item_id) = e.data_transfer().get_data("text/plain").and_then(|data| data.parse::().ok()) { + if let Some(pos) = items.iter().position(|item| item.id == item_id) { + items.write()[pos].category = category.to_string(); + } + } + }, + h2 { "Category: {category}" } + for (index, item) in items.iter().enumerate().filter(|item| item.1.category == category) { + div { + key: "{item.id}", + width: "200px", + height: "50px", + border: "1px solid black", + padding: "10px", + class: "item", + draggable: "true", + background: "white", + cursor: "grab", + ondragstart: move |e| { + let id = items.read()[index].id.to_string(); + e.data_transfer().set_data("text/plain", &id).unwrap(); + }, + pre { webkit_user_select: "none", "{item.name}" } + input { + r#type: "text", + value: "{item.contents}", + oninput: move |e| { + items.write()[index].contents = e.value(); + } + } + } + } + } + } + } + } +} + +fn initial_kanban_data() -> Vec { + vec![ + Item { + id: 1, + name: "Item 1".into(), + category: "A".into(), + contents: "This is item 1".into(), + }, + Item { + id: 2, + name: "Item 2".into(), + category: "A".into(), + contents: "This is item 2".into(), + }, + Item { + id: 3, + name: "Item 3".into(), + category: "A".into(), + contents: "This is item 3".into(), + }, + Item { + id: 4, + name: "Item 4".into(), + category: "B".into(), + contents: "This is item 4".into(), + }, + Item { + id: 5, + name: "Item 5".into(), + category: "B".into(), + contents: "This is item 5".into(), + }, + Item { + id: 6, + name: "Item 6".into(), + category: "C".into(), + contents: "This is item 6".into(), + }, + ] +} diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 582f964b6..3c106de44 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -11,8 +11,9 @@ use dioxus_html::{ point_interaction::{ InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction, }, - FileData, FormValue, HasDragData, HasFileData, HasFormData, HasMouseData, NativeFileData, - SerializedFormData, SerializedFormObject, SerializedMouseData, SerializedPointInteraction, + FileData, FormValue, HasDataTransferData, HasDragData, HasFileData, HasFormData, HasMouseData, + NativeFileData, SerializedDataTransfer, SerializedFormData, SerializedFormObject, + SerializedMouseData, SerializedPointInteraction, }; use serde::{Deserialize, Serialize}; @@ -234,6 +235,7 @@ impl NativeFileHover { #[derive(Clone)] pub(crate) struct DesktopFileDragEvent { pub mouse: SerializedPointInteraction, + pub data_transfer: SerializedDataTransfer, pub files: Vec, } @@ -247,6 +249,12 @@ impl HasFileData for DesktopFileDragEvent { } } +impl HasDataTransferData for DesktopFileDragEvent { + fn data_transfer(&self) -> dioxus_html::DataTransfer { + dioxus_html::DataTransfer::new(self.data_transfer.clone()) + } +} + impl HasDragData for DesktopFileDragEvent { fn as_any(&self) -> &dyn std::any::Any { self @@ -389,3 +397,5 @@ impl NativeFileData for DesktopFileData { ) } } + +pub struct DesktopDataTransfer {} diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 481f0d2ba..ec0e8f01a 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -155,16 +155,17 @@ impl WebviewEdits { } dioxus_html::EventData::Drag(ref drag) => { // we want to override this with a native file engine, provided by the most recent drag event - let file_event = hovered_file.current().unwrap(); - let paths = match file_event { - wry::DragDropEvent::Enter { paths, .. } => paths, - wry::DragDropEvent::Drop { paths, .. } => paths, + let file_event = hovered_file.current(); + let file_paths = match file_event { + Some(wry::DragDropEvent::Enter { paths, .. }) => paths, + Some(wry::DragDropEvent::Drop { paths, .. }) => paths, _ => vec![], }; Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent { mouse: drag.mouse.clone(), - files: paths, + data_transfer: drag.data_transfer.clone(), + files: file_paths, }))) } _ => data.into_any(), diff --git a/packages/html/src/data_transfer.rs b/packages/html/src/data_transfer.rs new file mode 100644 index 000000000..08f4abb6a --- /dev/null +++ b/packages/html/src/data_transfer.rs @@ -0,0 +1,180 @@ +pub struct DataTransfer { + inner: Box, +} + +impl DataTransfer { + pub fn new(inner: impl NativeDataTransfer + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + #[cfg(feature = "serialize")] + pub fn store(&self, item: impl Serialize) -> Result<(), String> { + let serialized = serde_json::to_string(&item).map_err(|e| e.to_string())?; + self.set_data("application/json", &serialized) + } + + #[cfg(feature = "serialize")] + pub fn retrieve serde::Deserialize<'de>>(&self) -> Result, String> { + if let Some(data) = self.get_data("application/json") { + let deserialized = serde_json::from_str(&data).map_err(|e| e.to_string())?; + Ok(Some(deserialized)) + } else { + Ok(None) + } + } + + pub fn get_data(&self, format: &str) -> Option { + self.inner.get_data(format) + } + + pub fn get_as_text(&self) -> Option { + self.get_data("text/plain") + } + + pub fn set_data(&self, format: &str, data: &str) -> Result<(), String> { + self.inner.set_data(format, data) + } + + pub fn clear_data(&self, format: Option<&str>) -> Result<(), String> { + self.inner.clear_data(format) + } + + pub fn effect_allowed(&self) -> String { + self.inner.effect_allowed() + } + + pub fn set_effect_allowed(&self, effect: &str) { + self.inner.set_effect_allowed(effect) + } + + pub fn drop_effect(&self) -> String { + self.inner.drop_effect() + } + + pub fn set_drop_effect(&self, effect: &str) { + self.inner.set_drop_effect(effect) + } + + pub fn files(&self) -> Vec { + self.inner.files() + } +} + +pub trait NativeDataTransfer: Send + Sync { + fn get_data(&self, format: &str) -> Option; + fn set_data(&self, format: &str, data: &str) -> Result<(), String>; + fn clear_data(&self, format: Option<&str>) -> Result<(), String>; + fn effect_allowed(&self) -> String; + fn set_effect_allowed(&self, effect: &str); + fn drop_effect(&self) -> String; + fn set_drop_effect(&self, effect: &str); + fn files(&self) -> Vec; +} + +pub trait HasDataTransferData { + fn data_transfer(&self) -> DataTransfer; +} + +#[cfg(feature = "serialize")] +pub use ser::*; +#[cfg(feature = "serialize")] +use serde::Serialize; + +#[cfg(feature = "serialize")] +mod ser { + use crate::DragData; + + use super::*; + use serde::{Deserialize, Serialize}; + + /// A serialized version of DataTransfer + #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] + pub struct SerializedDataTransfer { + pub items: Vec, + pub files: Vec, + pub effect_allowed: String, + pub drop_effect: String, + } + + #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] + pub struct SerializedDataTransferItem { + pub kind: String, + pub type_: String, + pub data: String, + } + + impl NativeDataTransfer for SerializedDataTransfer { + fn get_data(&self, format: &str) -> Option { + self.items + .iter() + .find(|item| item.type_ == format) + .map(|item| item.data.clone()) + } + + fn set_data(&self, _format: &str, _data: &str) -> Result<(), String> { + // todo!() + // Err("Cannot set data on serialized DataTransfer".into()) + Ok(()) + } + + fn clear_data(&self, _format: Option<&str>) -> Result<(), String> { + // todo!() + // Err("Cannot clear data on serialized DataTransfer".into()) + Ok(()) + } + + fn effect_allowed(&self) -> String { + self.effect_allowed.clone() + } + + fn set_effect_allowed(&self, _effect: &str) { + // No-op + } + + fn drop_effect(&self) -> String { + self.drop_effect.clone() + } + + fn set_drop_effect(&self, _effect: &str) { + // No-op + } + + fn files(&self) -> Vec { + self.files + .iter() + .map(|f| crate::file_data::FileData::new(f.clone())) + .collect() + } + } + + impl From<&DragData> for SerializedDataTransfer { + fn from(_drag: &DragData) -> Self { + todo!() + // let items = vec![]; // drag.data_transfer().items().iter().map(|item| SerializedDataTransferItem { + // // kind: item.kind().to_string(), + // // type_: item.type_().to_string(), + // // data: item.data().unwrap_or_default(), + // // }).collect(); + + // let files = drag + // .files() + // .iter() + // .map(|f| crate::file_data::SerializedFileData { + // name: f.name().to_string(), + // size: f.size(), + // type_: f.type_().to_string(), + // }) + // .collect(); + + // Self { + // items, + // files, + // effect_allowed: drag.effect_allowed().to_string(), + // drop_effect: drag.drop_effect().to_string(), + // } + // } + } + } +} diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index 1d6d27d67..0767b917f 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -1,6 +1,9 @@ -use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{MouseButton, MouseButtonSet}; use crate::*; +use crate::{ + data_transfer::DataTransfer, + geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}, +}; use dioxus_core::Event; use keyboard_types::Modifiers; @@ -51,6 +54,11 @@ impl DragData { } } + /// The DataTransfer object is used to hold the data that is being dragged during a drag and drop operation. + pub fn data_transfer(&self) -> DataTransfer { + self.inner.data_transfer() + } + /// Downcast this event data to a specific type #[inline(always)] pub fn downcast(&self) -> Option<&T> { @@ -106,111 +114,115 @@ impl PointerInteraction for DragData { } #[cfg(feature = "serialize")] -/// A serialized version of DragData -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] -pub struct SerializedDragData { - pub mouse: crate::point_interaction::SerializedPointInteraction, - - pub files: Vec, -} +pub use ser::*; #[cfg(feature = "serialize")] -impl SerializedDragData { - fn new(drag: &DragData) -> Self { - Self { - mouse: crate::point_interaction::SerializedPointInteraction::from(drag), - files: vec![], +mod ser { + use super::*; + + /// A serialized version of DragData + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] + pub struct SerializedDragData { + pub mouse: crate::point_interaction::SerializedPointInteraction, + + pub data_transfer: crate::data_transfer::SerializedDataTransfer, + } + + impl SerializedDragData { + fn new(drag: &DragData) -> Self { + Self { + mouse: crate::point_interaction::SerializedPointInteraction::from(drag), + data_transfer: crate::data_transfer::SerializedDataTransfer::from(drag), + } + } + } + + impl HasDataTransferData for SerializedDragData { + fn data_transfer(&self) -> crate::data_transfer::DataTransfer { + crate::data_transfer::DataTransfer::new(self.data_transfer.clone()) + } + } + + impl HasDragData for SerializedDragData { + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + impl crate::file_data::HasFileData for SerializedDragData { + fn files(&self) -> Vec { + self.data_transfer + .files + .iter() + .map(|f| FileData::new(f.clone())) + .collect() + } + } + + impl HasMouseData for SerializedDragData { + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + impl InteractionLocation for SerializedDragData { + fn client_coordinates(&self) -> ClientPoint { + self.mouse.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.mouse.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.mouse.screen_coordinates() + } + } + + impl InteractionElementOffset for SerializedDragData { + fn element_coordinates(&self) -> ElementPoint { + self.mouse.element_coordinates() + } + + fn coordinates(&self) -> Coordinates { + self.mouse.coordinates() + } + } + + impl ModifiersInteraction for SerializedDragData { + fn modifiers(&self) -> Modifiers { + self.mouse.modifiers() + } + } + + impl PointerInteraction for SerializedDragData { + fn held_buttons(&self) -> MouseButtonSet { + self.mouse.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.mouse.trigger_button() + } + } + + impl serde::Serialize for DragData { + fn serialize(&self, serializer: S) -> Result { + SerializedDragData::new(self).serialize(serializer) + } + } + + impl<'de> serde::Deserialize<'de> for DragData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedDragData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) } } } -#[cfg(feature = "serialize")] -impl HasDragData for SerializedDragData { - fn as_any(&self) -> &dyn std::any::Any { - self - } -} - -#[cfg(feature = "serialize")] -impl crate::file_data::HasFileData for SerializedDragData { - fn files(&self) -> Vec { - self.files - .iter() - .map(|f| FileData::new(f.clone())) - .collect() - } -} - -#[cfg(feature = "serialize")] -impl HasMouseData for SerializedDragData { - fn as_any(&self) -> &dyn std::any::Any { - self - } -} - -#[cfg(feature = "serialize")] -impl InteractionLocation for SerializedDragData { - fn client_coordinates(&self) -> ClientPoint { - self.mouse.client_coordinates() - } - - fn page_coordinates(&self) -> PagePoint { - self.mouse.page_coordinates() - } - - fn screen_coordinates(&self) -> ScreenPoint { - self.mouse.screen_coordinates() - } -} - -#[cfg(feature = "serialize")] -impl InteractionElementOffset for SerializedDragData { - fn element_coordinates(&self) -> ElementPoint { - self.mouse.element_coordinates() - } - - fn coordinates(&self) -> Coordinates { - self.mouse.coordinates() - } -} - -#[cfg(feature = "serialize")] -impl ModifiersInteraction for SerializedDragData { - fn modifiers(&self) -> Modifiers { - self.mouse.modifiers() - } -} - -#[cfg(feature = "serialize")] -impl PointerInteraction for SerializedDragData { - fn held_buttons(&self) -> MouseButtonSet { - self.mouse.held_buttons() - } - - fn trigger_button(&self) -> Option { - self.mouse.trigger_button() - } -} - -#[cfg(feature = "serialize")] -impl serde::Serialize for DragData { - fn serialize(&self, serializer: S) -> Result { - SerializedDragData::new(self).serialize(serializer) - } -} - -#[cfg(feature = "serialize")] -impl<'de> serde::Deserialize<'de> for DragData { - fn deserialize>(deserializer: D) -> Result { - let data = SerializedDragData::deserialize(deserializer)?; - Ok(Self { - inner: Box::new(data), - }) - } -} - /// A trait for any object that has the data for a drag event -pub trait HasDragData: HasMouseData + crate::HasFileData { +pub trait HasDragData: HasMouseData + crate::HasFileData + crate::HasDataTransferData { /// return self as Any fn as_any(&self) -> &dyn std::any::Any; } diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index e4d09748d..7f357aa0b 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -25,10 +25,12 @@ pub mod events; pub(crate) mod file_data; pub use file_data::*; mod attribute_groups; +mod data_transfer; pub mod geometry; pub mod input_data; pub mod point_interaction; mod render_template; +pub use data_transfer::*; pub use bytes; diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index ba3f16643..83d5db4eb 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -[17669692872757955279, 11420464406527728232, 3770103091118609057, 5444526391971481782, 8889858244860485542, 5052021921702764563, 5113938988622536969, 11339769846046015954] \ No newline at end of file +[17669692872757955279, 11420464406527728232, 3770103091118609057, 5444526391971481782, 8889858244860485542, 5052021921702764563, 15193138787416885137, 11339769846046015954] \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 5316e7708..94c211790 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1 +1 @@ -function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent<"u"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent){let files=[];if(event.dataTransfer&&event.dataTransfer.files)for(let i=0;i0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}}function serializeScrollEvent(event){let scrollLeft=0,scrollTop=0,scrollWidth=0,scrollHeight=0,clientWidth=0,clientHeight=0;if(event.target instanceof Element)scrollLeft=event.target.scrollLeft,scrollTop=event.target.scrollTop,scrollWidth=event.target.scrollWidth,scrollHeight=event.target.scrollHeight,clientWidth=event.target.clientWidth,clientHeight=event.target.clientHeight;else if(event.target===document)scrollLeft=window.scrollX||document.documentElement.scrollLeft,scrollTop=window.scrollY||document.documentElement.scrollTop,scrollWidth=document.documentElement.scrollWidth,scrollHeight=document.documentElement.scrollHeight,clientWidth=document.documentElement.clientWidth,clientHeight=document.documentElement.clientHeight;return{scroll_left:scrollLeft,scroll_top:scrollTop,scroll_width:scrollWidth,scroll_height:scrollHeight,client_width:clientWidth,client_height:clientHeight}}function extractSerializedFormValues(event,target){let contents={values:[]},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){let formData=new FormData(form),contents=[];return formData.forEach((value,key)=>{if(value instanceof File){let fileData={path:value.name,size:value.size,last_modified:value.lastModified,content_type:value.type};contents.push({key,file:fileData})}else contents.push({key,text:value})}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i{let target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){event.preventDefault();let contents=serializeEvent(event,target),target_name=target.getAttribute("name")||"",requestData={event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles,target_name,values:contents.values};this.fetchAgainstHost("__file_dialog",requestData).then((response)=>response.json()).then((resp)=>{let formObjects=resp.values,dataTransfer=new DataTransfer;for(let formObject of formObjects)if(formObject.key==target_name&&formObject.file!=null){let file=new File([],formObject.file.path,{type:formObject.file.content_type,lastModified:formObject.file.last_modified});dataTransfer.items.add(file)}target.files=dataTransfer.files;let body={data:contents,element:target_id,bubbles:event.bubbles};contents.values=formObjects,this.sendSerializedEvent({...body,name:"input"}),this.sendSerializedEvent({...body,name:"change"})});return}}}}),this.ipc=window.ipc;let handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}fetchAgainstHost(path,data){let encoded_data=new TextEncoder().encode(JSON.stringify(data)),base64data=btoa(String.fromCharCode.apply(null,Array.from(encoded_data)));return fetch(`${this.baseUri}/${path}`,{method:"GET",headers:{"x-dioxus-data":base64data}})}sendIpcMessage(method,params={}){let body=JSON.stringify({method,params});this.ipc.postMessage(body)}scrollTo(id,options){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollIntoView(options),!0;return!1}scroll(id,x,y,behavior){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scroll({top:y,left:x,behavior}),!0;return!1}getScrollHeight(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollHeight}getScrollLeft(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollLeft}getScrollTop(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollTop}getScrollWidth(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollWidth}getClientRect(id){let node=this.nodes[id];if(node instanceof HTMLElement){let rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){let node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}handleWindowsDragDrop(){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent);let data=new DataTransfer,file=new File(["content"],"file.txt",{type:"text/plain"});data.items.add(file);let dragDropEvent=new DragEvent("drop",{bubbles:!0,cancelable:!0,dataTransfer:data});window.dxDragLastElement.dispatchEvent(dragDropEvent),window.dxDragLastElement=null}}handleWindowsDragOver(xPos,yPos){let displayScaleFactor=window.devicePixelRatio||1;xPos/=displayScaleFactor,yPos/=displayScaleFactor;let element=document.elementFromPoint(xPos,yPos);if(element!=window.dxDragLastElement){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent)}let dragOverEvent=new DragEvent("dragover",{bubbles:!0,cancelable:!0});element.dispatchEvent(dragOverEvent),window.dxDragLastElement=element}}handleWindowsDragLeave(){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent),window.dxDragLastElement=null}}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){let root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k{this.flushQueuedBytes(),this.markEditsFinished()})}waitForRequest(editsPath,required_server_key){this.edits=new WebSocket(editsPath);let authenticated=!1;this.edits.onclose=()=>{setTimeout(()=>{if(this.edits.url!=editsPath)return;this.waitForRequest(editsPath,required_server_key)},100)},this.edits.onmessage=(event)=>{let data=event.data;if(data instanceof Blob){if(!authenticated)return;data.arrayBuffer().then((buffer)=>{this.rafEdits(buffer)})}else if(typeof data==="string"){if(data===required_server_key){authenticated=!0;return}}}}markEditsFinished(){this.edits.send(new ArrayBuffer(0))}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent<"u"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent){let files=[];if(event.dataTransfer&&event.dataTransfer.files)for(let i=0;i{if(value instanceof File){let fileData={path:value.name,size:value.size,last_modified:value.lastModified,content_type:value.type};contents.push({key,file:fileData})}else contents.push({key,text:value})}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i{let target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){event.preventDefault();let contents=serializeEvent(event,target),target_name=target.getAttribute("name")||"",requestData={event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles,target_name,values:contents.values};this.fetchAgainstHost("__file_dialog",requestData).then((response)=>response.json()).then((resp)=>{let formObjects=resp.values,dataTransfer=new DataTransfer;for(let formObject of formObjects)if(formObject.key==target_name&&formObject.file!=null){let file=new File([],formObject.file.path,{type:formObject.file.content_type,lastModified:formObject.file.last_modified});dataTransfer.items.add(file)}target.files=dataTransfer.files;let body={data:contents,element:target_id,bubbles:event.bubbles};contents.values=formObjects,this.sendSerializedEvent({...body,name:"input"}),this.sendSerializedEvent({...body,name:"change"})});return}}}}),this.ipc=window.ipc;let handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}fetchAgainstHost(path,data){let encoded_data=new TextEncoder().encode(JSON.stringify(data)),base64data=btoa(String.fromCharCode.apply(null,Array.from(encoded_data)));return fetch(`${this.baseUri}/${path}`,{method:"GET",headers:{"x-dioxus-data":base64data}})}sendIpcMessage(method,params={}){let body=JSON.stringify({method,params});this.ipc.postMessage(body)}scrollTo(id,options){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollIntoView(options),!0;return!1}scroll(id,x,y,behavior){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scroll({top:y,left:x,behavior}),!0;return!1}getScrollHeight(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollHeight}getScrollLeft(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollLeft}getScrollTop(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollTop}getScrollWidth(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollWidth}getClientRect(id){let node=this.nodes[id];if(node instanceof HTMLElement){let rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){let node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}handleWindowsDragDrop(){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent);let data=new DataTransfer,file=new File(["content"],"file.txt",{type:"text/plain"});data.items.add(file);let dragDropEvent=new DragEvent("drop",{bubbles:!0,cancelable:!0,dataTransfer:data});window.dxDragLastElement.dispatchEvent(dragDropEvent),window.dxDragLastElement=null}}handleWindowsDragOver(xPos,yPos){let displayScaleFactor=window.devicePixelRatio||1;xPos/=displayScaleFactor,yPos/=displayScaleFactor;let element=document.elementFromPoint(xPos,yPos);if(element!=window.dxDragLastElement){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent)}let dragOverEvent=new DragEvent("dragover",{bubbles:!0,cancelable:!0});element.dispatchEvent(dragOverEvent),window.dxDragLastElement=element}}handleWindowsDragLeave(){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent),window.dxDragLastElement=null}}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){let root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k{this.flushQueuedBytes(),this.markEditsFinished()})}waitForRequest(editsPath,required_server_key){this.edits=new WebSocket(editsPath);let authenticated=!1;this.edits.onclose=()=>{setTimeout(()=>{if(this.edits.url!=editsPath)return;this.waitForRequest(editsPath,required_server_key)},100)},this.edits.onmessage=(event)=>{let data=event.data;if(data instanceof Blob){if(!authenticated)return;data.arrayBuffer().then((buffer)=>{this.rafEdits(buffer)})}else if(typeof data==="string"){if(data===required_server_key){authenticated=!0;return}}}}markEditsFinished(){this.edits.send(new ArrayBuffer(0))}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i 0 - ) { - files = { - files: { placeholder: [] }, - }; + let data_transfer = event.dataTransfer || new DataTransfer(); + let items = []; + let files = []; + let effect_allowed = data_transfer.effectAllowed; + let drop_effect = data_transfer.dropEffect; + + for (let i = 0; i < data_transfer.items.length; i++) { + let item = data_transfer.items[i]; + let data; + if (item.kind === "string") { + // we can only get the data if we do this synchronously + // which is a bit sad, but oh well. + data = data_transfer.getData(item.type); + } else { + data = item.getAsFile()?.name || ""; + } + + items.push({ + kind: item.kind, + type: item.type, + data + }); } + + for (let i = 0; i < data_transfer.files.length; i++) { + let file = data_transfer.files[i]; + files.push({ + name: file.name, + path: file.name, + size: file.size, + last_modified: file.lastModified, + content_type: file.type, + contents: undefined, // we don't serialize contents here + }); + } + return { mouse: { alt_key: event.altKey, @@ -340,7 +365,12 @@ function serializeDragEvent(event: DragEvent): SerializedEvent { shift_key: event.shiftKey, ...serializeMouseEvent(event), }, - files, + data_transfer: { + items, + files, + effect_allowed, + drop_effect, + }, }; } @@ -458,3 +488,6 @@ export function retrieveSelectValue(target: HTMLSelectElement): string[] { } return values; } + +export type SerializedDataTransfer = {} +export type SerializedDataTransferItem = {} diff --git a/packages/web/src/data_transfer.rs b/packages/web/src/data_transfer.rs new file mode 100644 index 000000000..c26214088 --- /dev/null +++ b/packages/web/src/data_transfer.rs @@ -0,0 +1,54 @@ +use dioxus_html::{FileData, NativeDataTransfer}; + +/// A wrapper around the web_sys::DataTransfer to implement NativeDataTransfer +#[derive(Clone)] +pub struct WebDataTransfer { + pub(crate) data: web_sys::DataTransfer, +} + +impl WebDataTransfer { + /// Create a new WebDataTransfer from a web_sys::DataTransfer + pub fn new(data: web_sys::DataTransfer) -> Self { + Self { data } + } +} + +unsafe impl Send for WebDataTransfer {} +unsafe impl Sync for WebDataTransfer {} + +impl NativeDataTransfer for WebDataTransfer { + fn get_data(&self, format: &str) -> Option { + self.data.get_data(format).ok() + } + fn set_data(&self, format: &str, data: &str) -> Result<(), String> { + self.data.set_data(format, data).map_err(|e| { + format!( + "Failed to set data for format {format}: {:?}", + e.as_string() + ) + }) + } + fn clear_data(&self, format: Option<&str>) -> Result<(), String> { + match format { + Some(f) => self.data.clear_data_with_format(f), + None => self.data.clear_data(), + } + .map_err(|e| format!("{:?}", e)) + } + fn effect_allowed(&self) -> String { + self.data.effect_allowed() + } + fn set_effect_allowed(&self, effect: &str) { + self.data.set_effect_allowed(effect); + } + fn drop_effect(&self) -> String { + self.data.drop_effect() + } + fn set_drop_effect(&self, effect: &str) { + self.data.set_drop_effect(effect); + } + + fn files(&self) -> Vec { + todo!() + } +} diff --git a/packages/web/src/events/drag.rs b/packages/web/src/events/drag.rs index ef842cada..2a0db6e36 100644 --- a/packages/web/src/events/drag.rs +++ b/packages/web/src/events/drag.rs @@ -1,11 +1,12 @@ -use crate::{WebFileData, WebFileEngine}; +use crate::{WebDataTransfer, WebFileData, WebFileEngine}; use super::{Synthetic, WebEventExt}; use dioxus_html::{ geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint}, input_data::{decode_mouse_button_set, MouseButton}, - FileData, HasDragData, HasFileData, HasMouseData, InteractionElementOffset, - InteractionLocation, Modifiers, ModifiersInteraction, PointerInteraction, + FileData, HasDataTransferData, HasDragData, HasFileData, HasMouseData, + InteractionElementOffset, InteractionLocation, Modifiers, ModifiersInteraction, + PointerInteraction, }; use web_sys::{DragEvent, FileReader}; @@ -72,6 +73,23 @@ impl HasDragData for Synthetic { } } +impl HasDataTransferData for Synthetic { + fn data_transfer(&self) -> dioxus_html::DataTransfer { + use wasm_bindgen::JsCast; + + if let Some(target) = self.event.dyn_ref::() { + if let Some(data) = target.data_transfer() { + let web_data_transfer = WebDataTransfer::new(data); + return dioxus_html::DataTransfer::new(web_data_transfer); + } + } + + // Return an empty DataTransfer if we couldn't get one from the event + let web_data_transfer = WebDataTransfer::new(web_sys::DataTransfer::new().unwrap()); + dioxus_html::DataTransfer::new(web_data_transfer) + } +} + impl HasFileData for Synthetic { fn files(&self) -> Vec { use wasm_bindgen::JsCast; diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 2a4146455..af275c0f2 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -30,6 +30,9 @@ pub use history::{HashHistory, WebHistory}; mod files; pub use files::*; +mod data_transfer; +pub use data_transfer::*; + #[cfg(all(feature = "devtools", debug_assertions))] mod devtools;