yew_hooks/hooks/
use_clipboard.rs

1use std::rc::Rc;
2
3use gloo::file::Blob as GlooBlob;
4use js_sys::{Array, ArrayBuffer, Object, Reflect, Uint8Array};
5use wasm_bindgen::UnwrapThrowExt;
6use wasm_bindgen::{prelude::*, JsCast, JsValue};
7use web_sys::Blob;
8use yew::prelude::*;
9
10use super::{use_state_ptr_eq, UseStatePtrEqHandle};
11use crate::web_sys_ext::{window, ClipboardItem};
12
13/// State handle for the [`use_clipboard`] hook.
14pub struct UseClipboardHandle {
15    /// The text that is read from or written to clipboard.
16    pub text: UseStatePtrEqHandle<Option<String>>,
17    /// The bytes that is read from or written to clipboard.
18    pub bytes: UseStatePtrEqHandle<Option<Vec<u8>>>,
19    /// The mime type of the bytes that is read from or written to clipboard.
20    pub bytes_mime_type: UseStatePtrEqHandle<Option<String>>,
21    /// If the content is already copied.
22    pub copied: UseStatePtrEqHandle<bool>,
23    /// If the clipboard is supported.
24    pub is_supported: Rc<bool>,
25
26    write_text: Rc<dyn Fn(String)>,
27    write: Rc<dyn Fn(Vec<u8>, Option<String>)>,
28    read_text: Rc<dyn Fn()>,
29    read: Rc<dyn Fn()>,
30}
31
32impl UseClipboardHandle {
33    /// Read bytes from clipboard.
34    pub fn read(&self) {
35        (self.read)();
36    }
37
38    /// Read text from clipboard.
39    pub fn read_text(&self) {
40        (self.read_text)();
41    }
42
43    /// Write bytes with mime type to clipboard.
44    pub fn write(&self, data: Vec<u8>, mime_type: Option<String>) {
45        (self.write)(data, mime_type);
46    }
47
48    /// Write text to clipboard.
49    pub fn write_text(&self, data: String) {
50        (self.write_text)(data);
51    }
52}
53
54impl Clone for UseClipboardHandle {
55    fn clone(&self) -> Self {
56        Self {
57            text: self.text.clone(),
58            bytes: self.bytes.clone(),
59            bytes_mime_type: self.bytes_mime_type.clone(),
60            is_supported: self.is_supported.clone(),
61            copied: self.copied.clone(),
62
63            write_text: self.write_text.clone(),
64            write: self.write.clone(),
65            read_text: self.read_text.clone(),
66            read: self.read.clone(),
67        }
68    }
69}
70
71/// This hook is used to read from or write to clipboard for text or bytes.
72/// e.g. copy plain text or copy `image/png` file to clipboard.
73///
74/// # Example
75///
76/// ```rust
77/// # use yew::prelude::*;
78/// #
79/// use yew_hooks::prelude::*;
80///
81/// #[function_component(UseClipboard)]
82/// fn clipboard() -> Html {
83///     let clipboard = use_clipboard();
84///
85///     let onclick_write_text = {
86///         let clipboard = clipboard.clone();
87///         Callback::from(move |_| {
88///             clipboard.write_text("hello world!".to_owned());
89///         })
90///     };
91///     let onclick_read_text = {
92///         let clipboard = clipboard.clone();
93///         Callback::from(move |_| {
94///             clipboard.read_text();
95///         })
96///     };
97///     let onclick_write_bytes = {
98///         let clipboard = clipboard.clone();
99///         Callback::from(move |_| {
100///             clipboard.write(vec![], Some("image/png".to_owned()));
101///         })
102///     };
103///     let onclick_read_bytes = {
104///         let clipboard = clipboard.clone();
105///         Callback::from(move |_| {
106///             clipboard.read();
107///         })
108///     };
109///     
110///     html! {
111///         <div>
112///             <button onclick={onclick_write_text}>{ "Write text to clipboard" }</button>
113///             <button onclick={onclick_read_text}>{ "Read text from clipboard" }</button>
114///             <button onclick={onclick_write_bytes}>{ "Write bytes to clipboard" }</button>
115///             <button onclick={onclick_read_bytes}>{ "Read bytes from clipboard" }</button>
116///             <p>{ format!("Current text: {:?}", *clipboard.text) }</p>
117///             <p>{ format!("Copied: {:?}", *clipboard.copied) }</p>
118///             <p>{ format!("Is supported: {:?}", *clipboard.is_supported) }</p>
119///             <p>{ format!("Current bytes: {:?}", *clipboard.bytes) }</p>
120///             <p>{ format!("Current bytes mime type: {:?}", *clipboard.bytes_mime_type) }</p>
121///         </div>
122///     }
123/// }
124/// ```
125#[hook]
126pub fn use_clipboard() -> UseClipboardHandle {
127    let text = use_state_ptr_eq(|| None);
128    let bytes = use_state_ptr_eq(|| None);
129    let bytes_mime_type = use_state_ptr_eq(|| None);
130    let is_supported = use_memo((), |_| {
131        window()
132            .expect_throw("Can't find the global Window")
133            .navigator()
134            .clipboard()
135            .is_some()
136    });
137    let copied = use_state_ptr_eq(|| false);
138
139    let clipboard = use_memo((), |_| {
140        window()
141            .expect_throw("Can't find the global Window")
142            .navigator()
143            .clipboard()
144    });
145
146    let write_text = {
147        let clipboard = clipboard.clone();
148        let text = text.clone();
149        let copied = copied.clone();
150        Rc::new(move |data: String| {
151            if let Some(clipboard) = &*clipboard {
152                let text = text.clone();
153                let text2 = text.clone();
154                let copied = copied.clone();
155                let copied2 = copied.clone();
156                let data2 = data.clone();
157                let resolve_closure = Closure::wrap(Box::new(move |_| {
158                    text.set(Some(data.clone()));
159                    copied.set(true);
160                }) as Box<dyn FnMut(JsValue)>);
161                let reject_closure = Closure::wrap(Box::new(move |_| {
162                    text2.set(None);
163                    copied2.set(false);
164                }) as Box<dyn FnMut(JsValue)>);
165                let _ = clipboard
166                    .write_text(&data2)
167                    .then2(&resolve_closure, &reject_closure);
168                resolve_closure.forget();
169                reject_closure.forget();
170            }
171        })
172    };
173
174    let write = {
175        let clipboard = clipboard.clone();
176        let bytes = bytes.clone();
177        let bytes_mime_type = bytes_mime_type.clone();
178        let copied = copied.clone();
179        Rc::new(move |data: Vec<u8>, mime_type: Option<String>| {
180            if let Some(clipboard) = &*clipboard {
181                let blob = GlooBlob::new_with_options(&*data, mime_type.as_deref());
182                let object = Object::new();
183                if Reflect::set(
184                    &object,
185                    &JsValue::from(mime_type.as_deref()),
186                    &JsValue::from(blob),
187                )
188                .is_ok()
189                {
190                    if let Ok(item) = ClipboardItem::new(&object) {
191                        let items = Array::new();
192                        items.push(&item);
193                        let bytes = bytes.clone();
194                        let bytes2 = bytes.clone();
195                        let bytes_mime_type = bytes_mime_type.clone();
196                        let bytes_mime_type2 = bytes_mime_type.clone();
197                        let copied = copied.clone();
198                        let copied2 = copied.clone();
199                        let resolve_closure = Closure::wrap(Box::new(move |_| {
200                            bytes.set(Some(data.clone()));
201                            bytes_mime_type.set(mime_type.clone());
202                            copied.set(true);
203                        })
204                            as Box<dyn FnMut(JsValue)>);
205                        let reject_closure = Closure::wrap(Box::new(move |_| {
206                            bytes2.set(None);
207                            bytes_mime_type2.set(None);
208                            copied2.set(false);
209                        })
210                            as Box<dyn FnMut(JsValue)>);
211                        let _ = clipboard
212                            .write(&items)
213                            .then2(&resolve_closure, &reject_closure);
214                        resolve_closure.forget();
215                        reject_closure.forget();
216                    }
217                }
218            }
219        })
220    };
221
222    let read_text = {
223        let clipboard = clipboard.clone();
224        let text = text.clone();
225        Rc::new(move || {
226            if let Some(clipboard) = &*clipboard {
227                let text = text.clone();
228                let text2 = text.clone();
229                let resolve_closure = Closure::wrap(Box::new(move |data: JsValue| {
230                    data.as_string().map_or_else(
231                        || {
232                            text.set(None);
233                        },
234                        |data| {
235                            if data.is_empty() {
236                                text.set(None);
237                            } else {
238                                text.set(Some(data));
239                            }
240                        },
241                    );
242                }) as Box<dyn FnMut(JsValue)>);
243                let reject_closure = Closure::wrap(Box::new(move |_| {
244                    text2.set(None);
245                }) as Box<dyn FnMut(JsValue)>);
246                let _ = clipboard
247                    .read_text()
248                    .then2(&resolve_closure, &reject_closure);
249                resolve_closure.forget();
250                reject_closure.forget();
251            }
252        })
253    };
254
255    let read = {
256        let bytes = bytes.clone();
257        let bytes_mime_type = bytes_mime_type.clone();
258        Rc::new(move || {
259            if let Some(clipboard) = &*clipboard {
260                let bytes = bytes.clone();
261                let bytes2 = bytes.clone();
262                let bytes_mime_type = bytes_mime_type.clone();
263                let bytes_mime_type2 = bytes_mime_type.clone();
264                let resolve_closure = Closure::wrap(Box::new(move |items| {
265                    let items = Array::from(&items);
266                    let bytes = bytes.clone();
267                    for item in items.iter() {
268                        item.dyn_into::<ClipboardItem>().map_or_else(
269                            |_| {
270                                bytes.set(None);
271                                bytes_mime_type.set(None);
272                            },
273                            |item| {
274                                for t in item.types().iter() {
275                                    t.as_string().map_or_else(
276                                        || {
277                                            bytes.set(None);
278                                            bytes_mime_type.set(None);
279                                        },
280                                        |t| {
281                                            let bytes = bytes.clone();
282                                            let bytes2 = bytes.clone();
283                                            let bytes_mime_type = bytes_mime_type.clone();
284                                            let bytes_mime_type2 = bytes_mime_type.clone();
285                                            let t2 = t.clone();
286                                            let resolve_closure =
287                                                Closure::wrap(Box::new(move |blob: JsValue| {
288                                                    blob.dyn_into::<Blob>().map_or_else(
289                                                        |_| {
290                                                            bytes.set(None);
291                                                            bytes_mime_type.set(None);
292                                                        },
293                                                        |blob| {
294                                                            let bytes = bytes.clone();
295                                                            let bytes2 = bytes.clone();
296                                                            let bytes_mime_type =
297                                                                bytes_mime_type.clone();
298                                                            let bytes_mime_type2 =
299                                                                bytes_mime_type.clone();
300                                                            let t = t.clone();
301                                                            let resolve_closure = Closure::wrap(
302                                                                Box::new(move |buffer: JsValue| {
303                                                                    buffer
304                                                               .dyn_into::<ArrayBuffer>()
305                                               .map_or_else(
306                                                           |_| {
307                                                                       bytes.set(None);
308                                                                       bytes_mime_type.set(None);
309                                                                   },
310                                                           |buffer| {
311                                                                       let data = Uint8Array::new(
312                                                                                   &buffer,
313                                                                               )
314                                                                       .to_vec();
315                                                                       bytes.set(Some(data));
316                                                                       bytes_mime_type
317                                                                           .set(Some(t.clone()));
318                                                                   },
319                                                       );
320                                                                })
321                                                                    as Box<dyn FnMut(JsValue)>,
322                                                            );
323                                                            let reject_closure =
324                                                                Closure::wrap(Box::new(move |_| {
325                                                                    bytes2.set(None);
326                                                                    bytes_mime_type2.set(None);
327                                                                })
328                                                                    as Box<dyn FnMut(JsValue)>);
329                                                            let _ = blob.array_buffer().then2(
330                                                                &resolve_closure,
331                                                                &reject_closure,
332                                                            );
333                                                            resolve_closure.forget();
334                                                            reject_closure.forget();
335                                                        },
336                                                    );
337                                                })
338                                                    as Box<dyn FnMut(JsValue)>);
339                                            let reject_closure = Closure::wrap(Box::new(move |_| {
340                                                bytes2.set(None);
341                                                bytes_mime_type2.set(None);
342                                            })
343                                                as Box<dyn FnMut(JsValue)>);
344                                            let _ = item
345                                                .get_type(&t2)
346                                                .then2(&resolve_closure, &reject_closure);
347                                            resolve_closure.forget();
348                                            reject_closure.forget();
349                                        },
350                                    );
351                                }
352                            },
353                        );
354                    }
355                }) as Box<dyn FnMut(JsValue)>);
356                let reject_closure = Closure::wrap(Box::new(move |_| {
357                    bytes2.set(None);
358                    bytes_mime_type2.set(None);
359                }) as Box<dyn FnMut(JsValue)>);
360                let _ = clipboard.read().then2(&resolve_closure, &reject_closure);
361                resolve_closure.forget();
362                reject_closure.forget();
363            }
364        })
365    };
366
367    UseClipboardHandle {
368        text,
369        bytes,
370        bytes_mime_type,
371        copied,
372        is_supported,
373        write_text,
374        write,
375        read_text,
376        read,
377    }
378}