From 4b2a7e74d2caead23122528b2e49c6016d1e6589 Mon Sep 17 00:00:00 2001 From: Jet Li Date: Sun, 24 Apr 2022 22:41:57 +0800 Subject: [PATCH] add use_clipboard hook --- README.md | 1 + crates/yew-hooks/Cargo.toml | 3 +- crates/yew-hooks/src/hooks/mod.rs | 2 + crates/yew-hooks/src/hooks/use_clipboard.rs | 355 ++++++++++++++++++ crates/yew-hooks/src/web_sys_ext.rs | 63 +++- examples/yew-app/src/routes/home.rs | 1 + examples/yew-app/src/routes/hooks/mod.rs | 2 + .../yew-app/src/routes/hooks/use_clipboard.rs | 76 ++++ examples/yew-app/src/routes/mod.rs | 3 + 9 files changed, 504 insertions(+), 2 deletions(-) create mode 100644 crates/yew-hooks/src/hooks/use_clipboard.rs create mode 100644 examples/yew-app/src/routes/hooks/use_clipboard.rs diff --git a/README.md b/README.md index 83f6f6a..2986d59 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ fn counter() -> Html { - `use_debounce_effect` - debounces an effect. - `use_throttle` - throttles a function. - `use_throttle_effect` - throttles an effect. +- `use_clipboard` - reads from or writes to clipboard for text/bytes. ### Lifecycles diff --git a/crates/yew-hooks/Cargo.toml b/crates/yew-hooks/Cargo.toml index 8216717..5079a0b 100644 --- a/crates/yew-hooks/Cargo.toml +++ b/crates/yew-hooks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yew-hooks" -version = "0.1.53" +version = "0.1.54" edition = "2018" authors = ["Jet Li "] categories = ["gui", "wasm", "web-programming"] @@ -50,6 +50,7 @@ features = [ "TouchList", "HtmlLinkElement", "HtmlCollection", + "Blob", ] [dev-dependencies] diff --git a/crates/yew-hooks/src/hooks/mod.rs b/crates/yew-hooks/src/hooks/mod.rs index 8c74872..efe9e41 100644 --- a/crates/yew-hooks/src/hooks/mod.rs +++ b/crates/yew-hooks/src/hooks/mod.rs @@ -1,6 +1,7 @@ mod use_async; mod use_before_unload; mod use_click_away; +mod use_clipboard; mod use_counter; mod use_debounce; mod use_debounce_effect; @@ -54,6 +55,7 @@ mod use_window_size; pub use use_async::*; pub use use_before_unload::*; pub use use_click_away::*; +pub use use_clipboard::*; pub use use_counter::*; pub use use_debounce::*; pub use use_debounce_effect::*; diff --git a/crates/yew-hooks/src/hooks/use_clipboard.rs b/crates/yew-hooks/src/hooks/use_clipboard.rs new file mode 100644 index 0000000..811e941 --- /dev/null +++ b/crates/yew-hooks/src/hooks/use_clipboard.rs @@ -0,0 +1,355 @@ +use std::rc::Rc; + +use gloo::file::Blob as GlooBlob; +use js_sys::{Array, ArrayBuffer, Object, Reflect, Uint8Array}; +use wasm_bindgen::UnwrapThrowExt; +use wasm_bindgen::{prelude::*, JsCast, JsValue}; +use web_sys::Blob; +use yew::prelude::*; + +use super::{use_state_ptr_eq, UseStatePtrEqHandle}; +use crate::web_sys_ext::{window, ClipboardItem}; + +/// State handle for the [`use_clipboard`] hook. +pub struct UseClipboardHandle { + /// The text that is read from or written to clipboard. + pub text: UseStatePtrEqHandle>, + /// The bytes that is read from or written to clipboard. + pub bytes: UseStatePtrEqHandle>>, + /// The mime type of the bytes that is read from or written to clipboard. + pub bytes_mime_type: UseStatePtrEqHandle>, + /// If the content is already copied. + pub copied: UseStatePtrEqHandle, + /// If the clipboard is supported. + pub is_supported: Rc, + + write_text: Rc, + write: Rc, Option)>, + read_text: Rc, + read: Rc, +} + +impl UseClipboardHandle { + /// Read bytes from clipboard. + pub fn read(&self) { + (self.read)() + } + + /// Read text from clipboard. + pub fn read_text(&self) { + (self.read_text)() + } + + /// Write bytes with mime type to clipboard. + pub fn write(&self, data: Vec, mime_type: Option) { + (self.write)(data, mime_type) + } + + /// Write text to clipboard. + pub fn write_text(&self, data: String) { + (self.write_text)(data) + } +} + +impl Clone for UseClipboardHandle { + fn clone(&self) -> Self { + Self { + text: self.text.clone(), + bytes: self.bytes.clone(), + bytes_mime_type: self.bytes_mime_type.clone(), + is_supported: self.is_supported.clone(), + copied: self.copied.clone(), + + write_text: self.write_text.clone(), + write: self.write.clone(), + read_text: self.read_text.clone(), + read: self.read.clone(), + } + } +} + +/// This hook is used to read from or write to clipboard for text or bytes. +/// e.g. copy plain text or copy `image/png` file to clipboard. +/// +/// # Example +/// +/// ```rust +/// # use yew::prelude::*; +/// # +/// use yew_hooks::use_clipboard; +/// +/// #[function_component(UseClipboard)] +/// fn clipboard() -> Html { +/// let clipboard = use_clipboard(); +/// +/// let onclick_write_text = { +/// let clipboard = clipboard.clone(); +/// Callback::from(move |_| { +/// clipboard.write_text("hello world!".to_owned()); +/// }) +/// }; +/// let onclick_read_text = { +/// let clipboard = clipboard.clone(); +/// Callback::from(move |_| { +/// clipboard.read_text(); +/// }) +/// }; +/// let onclick_write_bytes = { +/// let clipboard = clipboard.clone(); +/// Callback::from(move |_| { +/// clipboard.write(vec![], Some("image/png".to_owned())); +/// }) +/// }; +/// let onclick_read_bytes = { +/// let clipboard = clipboard.clone(); +/// Callback::from(move |_| { +/// clipboard.read(); +/// }) +/// }; +/// +/// html! { +///
+/// +/// +/// +/// +///

{ format!("Current text: {:?}", *clipboard.text) }

+///

{ format!("Copied: {:?}", *clipboard.copied) }

+///

{ format!("Is supported: {:?}", *clipboard.is_supported) }

+///

{ format!("Current bytes: {:?}", *clipboard.bytes) }

+///

{ format!("Current bytes mime type: {:?}", *clipboard.bytes_mime_type) }

+///
+/// } +/// } +/// ``` +pub fn use_clipboard() -> UseClipboardHandle { + let text = use_state_ptr_eq(|| None); + let bytes = use_state_ptr_eq(|| None); + let bytes_mime_type = use_state_ptr_eq(|| None); + let is_supported = use_ref(|| { + window() + .expect_throw("Can't find the global Window") + .navigator() + .clipboard() + .is_some() + }); + let copied = use_state_ptr_eq(|| false); + + let clipboard = use_ref(|| { + window() + .expect_throw("Can't find the global Window") + .navigator() + .clipboard() + }); + + let write_text = { + let clipboard = clipboard.clone(); + let text = text.clone(); + let copied = copied.clone(); + Rc::new(move |data: String| { + if let Some(clipboard) = &*clipboard { + let text = text.clone(); + let text2 = text.clone(); + let copied = copied.clone(); + let copied2 = copied.clone(); + let data2 = data.clone(); + let resolve_closure = Closure::wrap(Box::new(move |_| { + text.set(Some(data.clone())); + copied.set(true); + }) as Box); + let reject_closure = Closure::wrap(Box::new(move |_| { + text2.set(None); + copied2.set(false); + }) as Box); + let _ = clipboard + .write_text(&data2) + .then2(&resolve_closure, &reject_closure); + resolve_closure.forget(); + reject_closure.forget(); + } + }) + }; + + let write = { + let clipboard = clipboard.clone(); + let bytes = bytes.clone(); + let bytes_mime_type = bytes_mime_type.clone(); + let copied = copied.clone(); + Rc::new(move |data: Vec, mime_type: Option| { + if let Some(clipboard) = &*clipboard { + let blob = GlooBlob::new_with_options(&*data, mime_type.as_deref()); + let object = Object::new(); + if Reflect::set( + &object, + &JsValue::from(mime_type.as_deref()), + &JsValue::from(blob), + ) + .is_ok() + { + if let Ok(item) = ClipboardItem::new(&object) { + let items = Array::new(); + items.push(&item); + let bytes = bytes.clone(); + let bytes2 = bytes.clone(); + let bytes_mime_type = bytes_mime_type.clone(); + let bytes_mime_type2 = bytes_mime_type.clone(); + let copied = copied.clone(); + let copied2 = copied.clone(); + let resolve_closure = Closure::wrap(Box::new(move |_| { + bytes.set(Some(data.clone())); + bytes_mime_type.set(mime_type.clone()); + copied.set(true) + }) + as Box); + let reject_closure = Closure::wrap(Box::new(move |_| { + bytes2.set(None); + bytes_mime_type2.set(None); + copied2.set(false); + }) + as Box); + let _ = clipboard + .write(&items) + .then2(&resolve_closure, &reject_closure); + resolve_closure.forget(); + reject_closure.forget(); + } + } + } + }) + }; + + let read_text = { + let clipboard = clipboard.clone(); + let text = text.clone(); + Rc::new(move || { + if let Some(clipboard) = &*clipboard { + let text = text.clone(); + let text2 = text.clone(); + let resolve_closure = Closure::wrap(Box::new(move |data: JsValue| { + if let Some(data) = data.as_string() { + if data.is_empty() { + text.set(None); + } else { + text.set(Some(data)); + } + } else { + text.set(None); + } + }) as Box); + let reject_closure = Closure::wrap(Box::new(move |_| { + text2.set(None); + }) as Box); + let _ = clipboard + .read_text() + .then2(&resolve_closure, &reject_closure); + resolve_closure.forget(); + reject_closure.forget(); + } + }) + }; + + let read = { + let bytes = bytes.clone(); + let bytes_mime_type = bytes_mime_type.clone(); + Rc::new(move || { + if let Some(clipboard) = &*clipboard { + let bytes = bytes.clone(); + let bytes2 = bytes.clone(); + let bytes_mime_type = bytes_mime_type.clone(); + let bytes_mime_type2 = bytes_mime_type.clone(); + let resolve_closure = Closure::wrap(Box::new(move |items| { + let items = Array::from(&items); + let bytes = bytes.clone(); + for item in items.iter() { + if let Ok(item) = item.dyn_into::() { + for t in item.types().iter() { + if let Some(t) = t.as_string() { + let bytes = bytes.clone(); + let bytes2 = bytes.clone(); + let bytes_mime_type = bytes_mime_type.clone(); + let bytes_mime_type2 = bytes_mime_type.clone(); + let t2 = t.clone(); + let resolve_closure = + Closure::wrap(Box::new(move |blob: JsValue| { + if let Ok(blob) = blob.dyn_into::() { + let bytes = bytes.clone(); + let bytes2 = bytes.clone(); + let bytes_mime_type = bytes_mime_type.clone(); + let bytes_mime_type2 = bytes_mime_type.clone(); + let t = t.clone(); + let resolve_closure = Closure::wrap(Box::new( + move |buffer: JsValue| { + if let Ok(buffer) = + buffer.dyn_into::() + { + let data = + Uint8Array::new(&buffer).to_vec(); + bytes.set(Some(data)); + bytes_mime_type.set(Some(t.clone())); + } else { + bytes.set(None); + bytes_mime_type.set(None); + } + }, + ) + as Box); + let reject_closure = + Closure::wrap(Box::new(move |_| { + bytes2.set(None); + bytes_mime_type2.set(None); + }) + as Box); + let _ = blob + .array_buffer() + .then2(&resolve_closure, &reject_closure); + resolve_closure.forget(); + reject_closure.forget(); + } else { + bytes.set(None); + bytes_mime_type.set(None); + } + }) + as Box); + let reject_closure = Closure::wrap(Box::new(move |_| { + bytes2.set(None); + bytes_mime_type2.set(None); + }) + as Box); + let _ = + item.get_type(&t2).then2(&resolve_closure, &reject_closure); + resolve_closure.forget(); + reject_closure.forget(); + } else { + bytes.set(None); + bytes_mime_type.set(None); + } + } + } else { + bytes.set(None); + bytes_mime_type.set(None); + } + } + }) as Box); + let reject_closure = Closure::wrap(Box::new(move |_| { + bytes2.set(None); + bytes_mime_type2.set(None); + }) as Box); + let _ = clipboard.read().then2(&resolve_closure, &reject_closure); + resolve_closure.forget(); + reject_closure.forget(); + } + }) + }; + + UseClipboardHandle { + text, + bytes, + bytes_mime_type, + is_supported, + copied, + write_text, + write, + read_text, + read, + } +} diff --git a/crates/yew-hooks/src/web_sys_ext.rs b/crates/yew-hooks/src/web_sys_ext.rs index a32fe8a..f29fa64 100644 --- a/crates/yew-hooks/src/web_sys_ext.rs +++ b/crates/yew-hooks/src/web_sys_ext.rs @@ -4,7 +4,7 @@ #![allow(unused_imports)] #![allow(clippy::unused_unit)] use wasm_bindgen::{self, prelude::*}; -use web_sys::{DataTransfer, DomRectReadOnly, Element, Event}; +use web_sys::{DataTransfer, DomRectReadOnly, Element, Event, EventTarget}; #[wasm_bindgen] extern "C" { @@ -44,3 +44,64 @@ extern "C" { # [wasm_bindgen (structural , method , getter , js_class = "ClipboardEvent" , js_name = clipboardData)] pub fn clipboard_data(this: &ClipboardEvent) -> Option; } + +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = EventTarget , extends = :: js_sys :: Object , js_name = Clipboard , typescript_type = "Clipboard")] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Clipboard; + + # [wasm_bindgen (method , structural , js_class = "Clipboard" , js_name = read)] + pub fn read(this: &Clipboard) -> ::js_sys::Promise; + + # [wasm_bindgen (method , structural , js_class = "Clipboard" , js_name = readText)] + pub fn read_text(this: &Clipboard) -> ::js_sys::Promise; + + # [wasm_bindgen (method , structural , js_class = "Clipboard" , js_name = write)] + pub fn write(this: &Clipboard, data: &::wasm_bindgen::JsValue) -> ::js_sys::Promise; + + # [wasm_bindgen (method , structural , js_class = "Clipboard" , js_name = writeText)] + pub fn write_text(this: &Clipboard, data: &str) -> ::js_sys::Promise; +} + +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = :: js_sys :: Object , js_name = Navigator , typescript_type = "Navigator")] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Navigator; + + # [wasm_bindgen (structural , method , getter , js_class = "Navigator" , js_name = clipboard)] + pub fn clipboard(this: &Navigator) -> Option; +} + +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = EventTarget , extends = :: js_sys :: Object , js_name = Window , typescript_type = "Window")] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Window; + + # [wasm_bindgen (structural , method , getter , js_class = "Window" , js_name = navigator)] + pub fn navigator(this: &Window) -> Navigator; +} + +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = :: js_sys :: Object , js_name = ClipboardItem , typescript_type = "ClipboardItem")] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type ClipboardItem; + + #[wasm_bindgen(catch, constructor, js_class = "ClipboardItem")] + pub fn new(item: &::js_sys::Object) -> Result; + + # [wasm_bindgen (structural , method , getter , js_class = "ClipboardItem" , js_name = types)] + pub fn types(this: &ClipboardItem) -> ::js_sys::Array; + + # [wasm_bindgen (method , structural , js_class = "ClipboardItem" , js_name = getType)] + pub fn get_type(this: &ClipboardItem, type_: &str) -> ::js_sys::Promise; +} + +pub fn window() -> Option { + use wasm_bindgen::JsCast; + + js_sys::global().dyn_into::().ok() +} diff --git a/examples/yew-app/src/routes/home.rs b/examples/yew-app/src/routes/home.rs index abbd4ef..d14584d 100644 --- a/examples/yew-app/src/routes/home.rs +++ b/examples/yew-app/src/routes/home.rs @@ -46,6 +46,7 @@ pub fn home() -> Html {
  • to={AppRoute::UseDebounceEffect} classes="app-link" >{ "use_debounce_effect" }> { " - debounces an effect." }
  • to={AppRoute::UseThrottle} classes="app-link" >{ "use_throttle" }> { " - throttles a function." }
  • to={AppRoute::UseThrottleEffect} classes="app-link" >{ "use_throttle_effect" }> { " - throttles an effect." }
  • +
  • to={AppRoute::UseClipboard} classes="app-link" >{ "use_clipboard" }> { " - reads from or writes to clipboard for text/bytes." }
  • { "Lifecycles" }

    diff --git a/examples/yew-app/src/routes/hooks/mod.rs b/examples/yew-app/src/routes/hooks/mod.rs index 9cd5766..2b90e7f 100644 --- a/examples/yew-app/src/routes/hooks/mod.rs +++ b/examples/yew-app/src/routes/hooks/mod.rs @@ -2,6 +2,7 @@ mod use_async; mod use_before_unload; mod use_bool_toggle; mod use_click_away; +mod use_clipboard; mod use_counter; mod use_debounce; mod use_debounce_effect; @@ -57,6 +58,7 @@ pub use use_async::*; pub use use_before_unload::*; pub use use_bool_toggle::*; pub use use_click_away::*; +pub use use_clipboard::*; pub use use_counter::*; pub use use_debounce::*; pub use use_debounce_effect::*; diff --git a/examples/yew-app/src/routes/hooks/use_clipboard.rs b/examples/yew-app/src/routes/hooks/use_clipboard.rs new file mode 100644 index 0000000..c38afdb --- /dev/null +++ b/examples/yew-app/src/routes/hooks/use_clipboard.rs @@ -0,0 +1,76 @@ +use yew::prelude::*; + +use yew_hooks::{use_async_with_options, use_clipboard, UseAsyncOptions}; + +/// `use_clipboard` demo +#[function_component(UseClipboard)] +pub fn update() -> Html { + let clipboard = use_clipboard(); + let logo = use_async_with_options( + async move { + if let Ok(response) = reqwest::get( + "https://raw.githubusercontent.com/yewstack/yew/master/website/static/img/logo.png", + ) + .await + { + if let Ok(bytes) = response.bytes().await { + Ok(bytes.to_vec()) + } else { + Err("Bytes error") + } + } else { + Err("Response err") + } + }, + UseAsyncOptions::enable_auto(), + ); + + let onclick = { + let clipboard = clipboard.clone(); + Callback::from(move |_| { + clipboard.write_text("hello world!".to_owned()); + }) + }; + + let onclick_read_text = { + let clipboard = clipboard.clone(); + Callback::from(move |_| { + clipboard.read_text(); + }) + }; + + let onclick_write_bytes = { + let clipboard = clipboard.clone(); + Callback::from(move |_| { + if let Some(bytes) = &logo.data { + clipboard.write(bytes.clone(), Some("image/png".to_owned())); + } + }) + }; + + let onclick_read_bytes = { + let clipboard = clipboard.clone(); + Callback::from(move |_| { + clipboard.read(); + }) + }; + + html! { +
    +
    +
    + + + + +

    { format!("Current text: {:?}", *clipboard.text) }

    +

    { format!("Copied: {:?}", *clipboard.copied) }

    +

    { format!("Is supported: {:?}", *clipboard.is_supported) }

    +

    { format!("Current bytes: {:?}", *clipboard.bytes) }

    +

    { format!("Current bytes mime type: {:?}", *clipboard.bytes_mime_type) }

    +

    +
    +
    +
    + } +} diff --git a/examples/yew-app/src/routes/mod.rs b/examples/yew-app/src/routes/mod.rs index acc2e63..f4794fc 100644 --- a/examples/yew-app/src/routes/mod.rs +++ b/examples/yew-app/src/routes/mod.rs @@ -122,6 +122,8 @@ pub enum AppRoute { UseThrottleEffect, #[at("/use_favicon")] UseFavicon, + #[at("/use_clipboard")] + UseClipboard, #[not_found] #[at("/page-not-found")] PageNotFound, @@ -188,6 +190,7 @@ pub fn switch(routes: &AppRoute) -> Html { AppRoute::UseDebounceEffect => html! { }, AppRoute::UseThrottleEffect => html! { }, AppRoute::UseFavicon => html! { }, + AppRoute::UseClipboard => html! { }, AppRoute::PageNotFound => html! { }, } }