Skip to main content

dioxus_html/events/
clipboard.rs

1use dioxus_core::Event;
2
3use crate::data_transfer::{DataTransfer, HasDataTransferData};
4use crate::file_data::{FileData, HasFileData};
5
6pub type ClipboardEvent = Event<ClipboardData>;
7
8/// Data fired alongside the `copy`, `cut` and `paste` events.
9///
10/// Clipboard events expose the data being transferred through the system clipboard via a
11/// [`DataTransfer`] object — the same abstraction used by drag-and-drop. For a `paste`, read
12/// the incoming payload with [`ClipboardData::data_transfer`] (e.g.
13/// [`DataTransfer::get_as_text`] for text, or [`HasFileData::files`] for pasted files).
14///
15/// See <https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent> for the underlying
16/// DOM event.
17pub struct ClipboardData {
18    inner: Box<dyn HasClipboardData>,
19}
20
21impl<E: HasClipboardData> From<E> for ClipboardData {
22    fn from(e: E) -> Self {
23        Self { inner: Box::new(e) }
24    }
25}
26
27impl std::fmt::Debug for ClipboardData {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        let dt = self.data_transfer();
30        f.debug_struct("ClipboardData")
31            .field("text", &dt.get_as_text())
32            .field("files", &dt.files().len())
33            .finish()
34    }
35}
36
37impl PartialEq for ClipboardData {
38    fn eq(&self, other: &Self) -> bool {
39        // `data_transfer()` rebuilds (and on serialized renderers clones) the transfer,
40        // so grab each side once.
41        let (this, that) = (self.data_transfer(), other.data_transfer());
42        this.get_as_text() == that.get_as_text()
43    }
44}
45
46impl ClipboardData {
47    /// Create a new ClipboardData
48    pub fn new(inner: impl HasClipboardData) -> Self {
49        Self {
50            inner: Box::new(inner),
51        }
52    }
53
54    /// The [`DataTransfer`] holding the data carried by the clipboard event.
55    ///
56    /// On `paste` this exposes the data being pasted (read it with
57    /// [`DataTransfer::get_as_text`], [`DataTransfer::get_data`] or [`HasFileData::files`]).
58    pub fn data_transfer(&self) -> DataTransfer {
59        self.inner.data_transfer()
60    }
61
62    /// Downcast this event to a concrete event type
63    #[inline(always)]
64    pub fn downcast<T: 'static>(&self) -> Option<&T> {
65        self.inner.as_ref().as_any().downcast_ref::<T>()
66    }
67}
68
69impl HasFileData for ClipboardData {
70    fn files(&self) -> Vec<FileData> {
71        self.inner.files()
72    }
73}
74
75pub trait HasClipboardData: HasDataTransferData + HasFileData {
76    /// return self as Any
77    fn as_any(&self) -> &dyn std::any::Any;
78}
79
80#[cfg(feature = "serialize")]
81pub use ser::*;
82
83#[cfg(feature = "serialize")]
84mod ser {
85    use super::*;
86    use crate::data_transfer::SerializedDataTransfer;
87
88    /// A serialized version of [`ClipboardData`] used to transport the event across the
89    /// IPC boundary on non-web renderers.
90    #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
91    pub struct SerializedClipboardData {
92        pub data_transfer: SerializedDataTransfer,
93    }
94
95    impl SerializedClipboardData {
96        fn new(clipboard: &ClipboardData) -> Self {
97            let data_transfer = clipboard.data_transfer();
98            Self {
99                data_transfer: SerializedDataTransfer::from_data_transfer(&data_transfer),
100            }
101        }
102    }
103
104    impl HasDataTransferData for SerializedClipboardData {
105        fn data_transfer(&self) -> DataTransfer {
106            DataTransfer::new(self.data_transfer.clone())
107        }
108    }
109
110    impl HasFileData for SerializedClipboardData {
111        fn files(&self) -> Vec<FileData> {
112            self.data_transfer().files()
113        }
114    }
115
116    impl HasClipboardData for SerializedClipboardData {
117        fn as_any(&self) -> &dyn std::any::Any {
118            self
119        }
120    }
121
122    impl serde::Serialize for ClipboardData {
123        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
124            SerializedClipboardData::new(self).serialize(serializer)
125        }
126    }
127
128    impl<'de> serde::Deserialize<'de> for ClipboardData {
129        fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
130            let data = SerializedClipboardData::deserialize(deserializer)?;
131            Ok(Self {
132                inner: Box::new(data),
133            })
134        }
135    }
136}
137
138#[cfg(all(test, feature = "serialize"))]
139mod tests {
140    use super::*;
141
142    /// The browser-side serializer (packages/interpreter/src/ts/serialize.ts) emits a
143    /// `data_transfer` object for clipboard events, mirroring the drag serializer. Pin the
144    /// shape so a change on either side breaks the test instead of silently dropping the
145    /// pasted data.
146    #[test]
147    fn deserializes_paste_payload_from_js_interpreter() {
148        let payload = r#"{
149            "data_transfer": {
150                "items": [
151                    { "kind": "string", "type_": "text/plain", "data": "hello pasted" }
152                ],
153                "files": [],
154                "effect_allowed": "uninitialized",
155                "drop_effect": "none"
156            }
157        }"#;
158
159        let data: ClipboardData = serde_json::from_str(payload).unwrap();
160        assert_eq!(
161            data.data_transfer().get_as_text().as_deref(),
162            Some("hello pasted")
163        );
164        assert_eq!(
165            data.data_transfer().get_data("text/plain").as_deref(),
166            Some("hello pasted")
167        );
168        assert!(data.files().is_empty());
169    }
170
171    #[test]
172    fn paste_text_round_trips_through_serde() {
173        let payload = r#"{
174            "data_transfer": {
175                "items": [
176                    { "kind": "string", "type_": "text/plain", "data": "round trip" }
177                ],
178                "files": [],
179                "effect_allowed": "",
180                "drop_effect": ""
181            }
182        }"#;
183
184        let data: ClipboardData = serde_json::from_str(payload).unwrap();
185        let json = serde_json::to_string(&data).unwrap();
186        let back: ClipboardData = serde_json::from_str(&json).unwrap();
187        assert_eq!(
188            back.data_transfer().get_as_text().as_deref(),
189            Some("round trip")
190        );
191    }
192}