From 63f97a94924ab29f5e46096268477093055cce4e Mon Sep 17 00:00:00 2001 From: "Douglas M. Dwyer" Date: Sat, 17 Aug 2024 12:09:22 -0400 Subject: [PATCH 1/5] Implement WASM clipboard --- Cargo.lock | 82 ++++++++++++++++++++++++++ Cargo.toml | 4 ++ src/common.rs | 5 +- src/lib.rs | 2 + src/platform/mod.rs | 5 ++ src/platform/wasm.rs | 133 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 src/platform/wasm.rs diff --git a/Cargo.lock b/Cargo.lock index 2680970..50e4f80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,11 +25,13 @@ dependencies = [ "core-graphics", "env_logger", "image", + "js-sys", "log", "objc2", "objc2-app-kit", "objc2-foundation", "parking_lot", + "web-sys", "windows-sys", "wl-clipboard-rs", "x11rb", @@ -73,6 +75,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytecount" version = "0.6.3" @@ -342,6 +350,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -755,6 +772,61 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + [[package]] name = "wayland-backend" version = "0.3.2" @@ -828,6 +900,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 5bbceaa..c8a7557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,10 @@ wl-clipboard-rs = { version = "0.8", optional = true } image = { version = "0.25", optional = true, default-features = false, features = ["png"] } parking_lot = "0.12" +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = { version = "0.3.70", default-features = false } +web-sys = { version = "0.3.70", default-features = false, features = [ "Clipboard", "ClipboardEvent", "ClipboardItem", "DataTransfer", "Document", "FileList", "Navigator", "Window" ] } + [[example]] name = "get_image" required-features = ["image-data"] diff --git a/src/common.rs b/src/common.rs index 221a883..39872cd 100644 --- a/src/common.rs +++ b/src/common.rs @@ -90,7 +90,6 @@ impl std::fmt::Debug for Error { } impl Error { - #[cfg(windows)] pub(crate) fn unknown>(message: M) -> Self { Error::Unknown { description: message.into() } } @@ -174,9 +173,9 @@ impl Drop for ScopeGuard { /// Common trait for sealing platform extension traits. pub(crate) mod private { - // This is currently unused on macOS, so silence the warning which appears + // This is currently unused on macOS and WASM, so silence the warning which appears // since there's no extension traits making use of this trait sealing structure. - #[cfg_attr(target_vendor = "apple", allow(unreachable_pub))] + #[allow(unreachable_pub, unused)] pub trait Sealed {} impl Sealed for crate::Get<'_> {} diff --git a/src/lib.rs b/src/lib.rs index 57256d5..c074e55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,6 +131,7 @@ impl Clipboard { /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` + /// - On WASM: Currently unsupported /// /// # Errors /// @@ -226,6 +227,7 @@ impl Set<'_> { /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` + /// - On WASM: Currently unsupported #[cfg(feature = "image-data")] pub fn image(self, image: ImageData) -> Result<(), Error> { self.platform.image(image) diff --git a/src/platform/mod.rs b/src/platform/mod.rs index b336463..62f3110 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -15,3 +15,8 @@ pub use windows::*; mod osx; #[cfg(target_os = "macos")] pub(crate) use osx::*; + +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +pub(crate) use wasm::*; \ No newline at end of file diff --git a/src/platform/wasm.rs b/src/platform/wasm.rs new file mode 100644 index 0000000..ab6a69d --- /dev/null +++ b/src/platform/wasm.rs @@ -0,0 +1,133 @@ +#[cfg(feature = "image-data")] +use crate::common::ImageData; +use crate::common::Error; +use js_sys::wasm_bindgen::JsCast; +use std::borrow::Cow; + +pub(crate) struct Clipboard { + inner: web_sys::Clipboard, + window: web_sys::Window, + _paste_callback: web_sys::wasm_bindgen::closure::Closure +} + +impl Clipboard { + const GLOBAL_CLIPBOARD_OBJECT: &str = "__arboard_global_clipboard"; + + pub(crate) fn new() -> Result { + let window = web_sys::window().ok_or(Error::ClipboardNotSupported)?; + let inner = window.navigator().clipboard(); + + let window_clone = window.clone(); + let paste_callback = web_sys::wasm_bindgen::closure::Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { + if let Some(data_transfer) = e.clipboard_data() { + js_sys::Reflect::set(&window_clone, &Self::GLOBAL_CLIPBOARD_OBJECT.into(), &data_transfer.get_data("text").unwrap_or_default().into()) + .expect("Failed to set global clipboard object."); + } + }) as Box); + + // Set this event handler to execute before any child elements (third argument `true`) so that it is subsequently observed by other events. + window.document().ok_or(Error::ClipboardNotSupported)?.add_event_listener_with_callback_and_bool("paste", &paste_callback.as_ref().unchecked_ref(), true) + .map_err(|_| Error::unknown("Could not add paste event listener."))?; + + Ok(Self { + inner, + _paste_callback: paste_callback, + window + }) + } + + fn get_last_clipboard(&self) -> String { + js_sys::Reflect::get(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into()) + .ok().and_then(|x| x.as_string()).unwrap_or_default() + } + + fn set_last_clipboard(&self, value: &str) { + js_sys::Reflect::set(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into(), &value.into()) + .expect("Failed to set global clipboard object."); + } +} + +pub(crate) struct Clear<'clipboard> { + clipboard: &'clipboard mut Clipboard, +} + +impl<'clipboard> Clear<'clipboard> { + pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { + Self { clipboard } + } + + pub(crate) fn clear(self) -> Result<(), Error> { + let _ = self.clipboard.inner.write(&js_sys::Array::default()); + self.clipboard.set_last_clipboard(""); + Ok(()) + } +} + +pub(crate) struct Get<'clipboard> { + clipboard: &'clipboard mut Clipboard, +} + +impl<'clipboard> Get<'clipboard> { + pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { + Self { clipboard } + } + + pub(crate) fn text(self) -> Result { + Ok(self.clipboard.get_last_clipboard()) + } + + #[cfg(feature = "image-data")] + pub(crate) fn image(self) -> Result, Error> { + Err(Error::ConversionFailure) + } +} + +pub(crate) struct Set<'clipboard> { + clipboard: &'clipboard mut Clipboard, +} + +impl<'clipboard> Set<'clipboard> { + pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { + Self { + clipboard + } + } + + pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { + let _ = self.clipboard.inner.write_text(&data); + self.clipboard.set_last_clipboard(&data); + Ok(()) + } + + pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { + let alt = match alt { + Some(s) => s.into(), + None => String::new(), + }; + + self.clipboard.set_last_clipboard(&html); + let html_item = js_sys::Object::new(); + js_sys::Reflect::set(&html_item, &"text/html".into(), &html.into_owned().into()) + .expect("Failed to set HTML item text."); + + let alt_item = js_sys::Object::new(); + js_sys::Reflect::set(&alt_item, &"text/plain".into(), &alt.into()) + .expect("Failed to set alt item text."); + + let mut clipboard_items = js_sys::Array::default(); + clipboard_items.extend([ + web_sys::ClipboardItem::new_with_record_from_str_to_str_promise(&html_item) + .map_err(|_| Error::unknown("Failed to create HTML clipboard item."))?, + web_sys::ClipboardItem::new_with_record_from_str_to_str_promise(&alt_item) + .map_err(|_| Error::unknown("Failed to create alt clipboard item."))? + ]); + + let _ = self.clipboard.inner.write(&clipboard_items); + Ok(()) + } + + #[cfg(feature = "image-data")] + pub(crate) fn image(self, _: ImageData) -> Result<(), Error> { + Err(Error::ConversionFailure) + } +} \ No newline at end of file From 92a3ae997676b8cd3d890307cc50c23981b6ef2d Mon Sep 17 00:00:00 2001 From: "Douglas M. Dwyer" Date: Mon, 26 Aug 2024 16:50:54 -0400 Subject: [PATCH 2/5] Address PR feedback --- src/lib.rs | 6 ++++++ src/platform/wasm.rs | 25 +++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c074e55..f4f9258 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,6 +60,12 @@ pub use platform::SetExtWindows; /// /// This means that attempting operations in parallel has a high likelihood to return an error or /// deadlock. As such, it is recommended to avoid creating/operating clipboard objects on >1 thread. +/// +/// ## WASM +/// +/// The `Clipboard` is only available on the main browser thread; attempting to use it from a worker +/// will panic. In addition, the user must perform a paste action on the web document before +/// the clipboard contents become available to read with `arboard`. #[allow(rustdoc::broken_intra_doc_links)] pub struct Clipboard { pub(crate) platform: platform::Clipboard, diff --git a/src/platform/wasm.rs b/src/platform/wasm.rs index ab6a69d..08dcf72 100644 --- a/src/platform/wasm.rs +++ b/src/platform/wasm.rs @@ -3,11 +3,12 @@ use crate::common::ImageData; use crate::common::Error; use js_sys::wasm_bindgen::JsCast; use std::borrow::Cow; +use web_sys::wasm_bindgen::closure::Closure; pub(crate) struct Clipboard { inner: web_sys::Clipboard, window: web_sys::Window, - _paste_callback: web_sys::wasm_bindgen::closure::Closure + _paste_callback: Closure } impl Clipboard { @@ -18,9 +19,16 @@ impl Clipboard { let inner = window.navigator().clipboard(); let window_clone = window.clone(); - let paste_callback = web_sys::wasm_bindgen::closure::Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { + let paste_callback = Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { if let Some(data_transfer) = e.clipboard_data() { - js_sys::Reflect::set(&window_clone, &Self::GLOBAL_CLIPBOARD_OBJECT.into(), &data_transfer.get_data("text").unwrap_or_default().into()) + let object_to_set = if let Ok(text_data) = data_transfer.get_data("text") { + text_data.into() + } + else { + web_sys::wasm_bindgen::JsValue::NULL.clone() + }; + + js_sys::Reflect::set(&window_clone, &Self::GLOBAL_CLIPBOARD_OBJECT.into(), &object_to_set) .expect("Failed to set global clipboard object."); } }) as Box); @@ -36,9 +44,9 @@ impl Clipboard { }) } - fn get_last_clipboard(&self) -> String { + fn get_last_clipboard(&self) -> Option { js_sys::Reflect::get(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into()) - .ok().and_then(|x| x.as_string()).unwrap_or_default() + .ok().and_then(|x| x.as_string()) } fn set_last_clipboard(&self, value: &str) { @@ -73,7 +81,8 @@ impl<'clipboard> Get<'clipboard> { } pub(crate) fn text(self) -> Result { - Ok(self.clipboard.get_last_clipboard()) + self.clipboard.get_last_clipboard() + .ok_or_else(|| Error::ContentNotAvailable) } #[cfg(feature = "image-data")] @@ -105,9 +114,8 @@ impl<'clipboard> Set<'clipboard> { None => String::new(), }; - self.clipboard.set_last_clipboard(&html); let html_item = js_sys::Object::new(); - js_sys::Reflect::set(&html_item, &"text/html".into(), &html.into_owned().into()) + js_sys::Reflect::set(&html_item, &"text/html".into(), &(&*html).into()) .expect("Failed to set HTML item text."); let alt_item = js_sys::Object::new(); @@ -123,6 +131,7 @@ impl<'clipboard> Set<'clipboard> { ]); let _ = self.clipboard.inner.write(&clipboard_items); + self.clipboard.set_last_clipboard(&html); Ok(()) } From fcb36ab5b00aeaf7b92f3d31bd863b5c8e885cbc Mon Sep 17 00:00:00 2001 From: "Douglas M. Dwyer" Date: Mon, 26 Aug 2024 16:51:29 -0400 Subject: [PATCH 3/5] Run cargo fmt --- src/lib.rs | 4 +- src/platform/mod.rs | 2 +- src/platform/wasm.rs | 162 ++++++++++++++++++++++--------------------- 3 files changed, 86 insertions(+), 82 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f4f9258..df9b26a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,9 +60,9 @@ pub use platform::SetExtWindows; /// /// This means that attempting operations in parallel has a high likelihood to return an error or /// deadlock. As such, it is recommended to avoid creating/operating clipboard objects on >1 thread. -/// +/// /// ## WASM -/// +/// /// The `Clipboard` is only available on the main browser thread; attempting to use it from a worker /// will panic. In addition, the user must perform a paste action on the web document before /// the clipboard contents become available to read with `arboard`. diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 62f3110..cb02c0d 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -19,4 +19,4 @@ pub(crate) use osx::*; #[cfg(target_arch = "wasm32")] mod wasm; #[cfg(target_arch = "wasm32")] -pub(crate) use wasm::*; \ No newline at end of file +pub(crate) use wasm::*; diff --git a/src/platform/wasm.rs b/src/platform/wasm.rs index 08dcf72..817e353 100644 --- a/src/platform/wasm.rs +++ b/src/platform/wasm.rs @@ -1,58 +1,65 @@ +use crate::common::Error; #[cfg(feature = "image-data")] use crate::common::ImageData; -use crate::common::Error; use js_sys::wasm_bindgen::JsCast; use std::borrow::Cow; use web_sys::wasm_bindgen::closure::Closure; pub(crate) struct Clipboard { - inner: web_sys::Clipboard, - window: web_sys::Window, - _paste_callback: Closure + inner: web_sys::Clipboard, + window: web_sys::Window, + _paste_callback: Closure, } impl Clipboard { - const GLOBAL_CLIPBOARD_OBJECT: &str = "__arboard_global_clipboard"; - - pub(crate) fn new() -> Result { - let window = web_sys::window().ok_or(Error::ClipboardNotSupported)?; - let inner = window.navigator().clipboard(); - - let window_clone = window.clone(); - let paste_callback = Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { - if let Some(data_transfer) = e.clipboard_data() { - let object_to_set = if let Ok(text_data) = data_transfer.get_data("text") { - text_data.into() - } - else { - web_sys::wasm_bindgen::JsValue::NULL.clone() - }; - - js_sys::Reflect::set(&window_clone, &Self::GLOBAL_CLIPBOARD_OBJECT.into(), &object_to_set) - .expect("Failed to set global clipboard object."); - } - }) as Box); - - // Set this event handler to execute before any child elements (third argument `true`) so that it is subsequently observed by other events. - window.document().ok_or(Error::ClipboardNotSupported)?.add_event_listener_with_callback_and_bool("paste", &paste_callback.as_ref().unchecked_ref(), true) - .map_err(|_| Error::unknown("Could not add paste event listener."))?; - - Ok(Self { - inner, - _paste_callback: paste_callback, - window - }) - } - - fn get_last_clipboard(&self) -> Option { - js_sys::Reflect::get(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into()) - .ok().and_then(|x| x.as_string()) - } - - fn set_last_clipboard(&self, value: &str) { - js_sys::Reflect::set(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into(), &value.into()) - .expect("Failed to set global clipboard object."); - } + const GLOBAL_CLIPBOARD_OBJECT: &str = "__arboard_global_clipboard"; + + pub(crate) fn new() -> Result { + let window = web_sys::window().ok_or(Error::ClipboardNotSupported)?; + let inner = window.navigator().clipboard(); + + let window_clone = window.clone(); + let paste_callback = Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { + if let Some(data_transfer) = e.clipboard_data() { + let object_to_set = if let Ok(text_data) = data_transfer.get_data("text") { + text_data.into() + } else { + web_sys::wasm_bindgen::JsValue::NULL.clone() + }; + + js_sys::Reflect::set( + &window_clone, + &Self::GLOBAL_CLIPBOARD_OBJECT.into(), + &object_to_set, + ) + .expect("Failed to set global clipboard object."); + } + }) as Box); + + // Set this event handler to execute before any child elements (third argument `true`) so that it is subsequently observed by other events. + window + .document() + .ok_or(Error::ClipboardNotSupported)? + .add_event_listener_with_callback_and_bool( + "paste", + &paste_callback.as_ref().unchecked_ref(), + true, + ) + .map_err(|_| Error::unknown("Could not add paste event listener."))?; + + Ok(Self { inner, _paste_callback: paste_callback, window }) + } + + fn get_last_clipboard(&self) -> Option { + js_sys::Reflect::get(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into()) + .ok() + .and_then(|x| x.as_string()) + } + + fn set_last_clipboard(&self, value: &str) { + js_sys::Reflect::set(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into(), &value.into()) + .expect("Failed to set global clipboard object."); + } } pub(crate) struct Clear<'clipboard> { @@ -65,9 +72,9 @@ impl<'clipboard> Clear<'clipboard> { } pub(crate) fn clear(self) -> Result<(), Error> { - let _ = self.clipboard.inner.write(&js_sys::Array::default()); - self.clipboard.set_last_clipboard(""); - Ok(()) + let _ = self.clipboard.inner.write(&js_sys::Array::default()); + self.clipboard.set_last_clipboard(""); + Ok(()) } } @@ -81,13 +88,12 @@ impl<'clipboard> Get<'clipboard> { } pub(crate) fn text(self) -> Result { - self.clipboard.get_last_clipboard() - .ok_or_else(|| Error::ContentNotAvailable) + self.clipboard.get_last_clipboard().ok_or_else(|| Error::ContentNotAvailable) } #[cfg(feature = "image-data")] pub(crate) fn image(self) -> Result, Error> { - Err(Error::ConversionFailure) + Err(Error::ConversionFailure) } } @@ -97,15 +103,13 @@ pub(crate) struct Set<'clipboard> { impl<'clipboard> Set<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { - Self { - clipboard - } + Self { clipboard } } pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { - let _ = self.clipboard.inner.write_text(&data); - self.clipboard.set_last_clipboard(&data); - Ok(()) + let _ = self.clipboard.inner.write_text(&data); + self.clipboard.set_last_clipboard(&data); + Ok(()) } pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { @@ -113,30 +117,30 @@ impl<'clipboard> Set<'clipboard> { Some(s) => s.into(), None => String::new(), }; - - let html_item = js_sys::Object::new(); - js_sys::Reflect::set(&html_item, &"text/html".into(), &(&*html).into()) - .expect("Failed to set HTML item text."); - - let alt_item = js_sys::Object::new(); - js_sys::Reflect::set(&alt_item, &"text/plain".into(), &alt.into()) - .expect("Failed to set alt item text."); - - let mut clipboard_items = js_sys::Array::default(); - clipboard_items.extend([ - web_sys::ClipboardItem::new_with_record_from_str_to_str_promise(&html_item) - .map_err(|_| Error::unknown("Failed to create HTML clipboard item."))?, - web_sys::ClipboardItem::new_with_record_from_str_to_str_promise(&alt_item) - .map_err(|_| Error::unknown("Failed to create alt clipboard item."))? - ]); - - let _ = self.clipboard.inner.write(&clipboard_items); - self.clipboard.set_last_clipboard(&html); - Ok(()) + + let html_item = js_sys::Object::new(); + js_sys::Reflect::set(&html_item, &"text/html".into(), &(&*html).into()) + .expect("Failed to set HTML item text."); + + let alt_item = js_sys::Object::new(); + js_sys::Reflect::set(&alt_item, &"text/plain".into(), &alt.into()) + .expect("Failed to set alt item text."); + + let mut clipboard_items = js_sys::Array::default(); + clipboard_items.extend([ + web_sys::ClipboardItem::new_with_record_from_str_to_str_promise(&html_item) + .map_err(|_| Error::unknown("Failed to create HTML clipboard item."))?, + web_sys::ClipboardItem::new_with_record_from_str_to_str_promise(&alt_item) + .map_err(|_| Error::unknown("Failed to create alt clipboard item."))?, + ]); + + let _ = self.clipboard.inner.write(&clipboard_items); + self.clipboard.set_last_clipboard(&html); + Ok(()) } #[cfg(feature = "image-data")] pub(crate) fn image(self, _: ImageData) -> Result<(), Error> { - Err(Error::ConversionFailure) + Err(Error::ConversionFailure) } -} \ No newline at end of file +} From a619a480c9d76bc0adcf6f050f2893fc4491e18c Mon Sep 17 00:00:00 2001 From: "Douglas M. Dwyer" Date: Mon, 26 Aug 2024 17:43:25 -0400 Subject: [PATCH 4/5] Only add paste callback once --- src/platform/wasm.rs | 80 ++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/src/platform/wasm.rs b/src/platform/wasm.rs index 817e353..a1937fe 100644 --- a/src/platform/wasm.rs +++ b/src/platform/wasm.rs @@ -7,47 +7,61 @@ use web_sys::wasm_bindgen::closure::Closure; pub(crate) struct Clipboard { inner: web_sys::Clipboard, - window: web_sys::Window, - _paste_callback: Closure, + window: web_sys::Window } impl Clipboard { const GLOBAL_CLIPBOARD_OBJECT: &str = "__arboard_global_clipboard"; + const GLOBAL_CALLBACK_OBJECT: &str = "__arboard_global_callback"; pub(crate) fn new() -> Result { let window = web_sys::window().ok_or(Error::ClipboardNotSupported)?; let inner = window.navigator().clipboard(); - let window_clone = window.clone(); - let paste_callback = Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { - if let Some(data_transfer) = e.clipboard_data() { - let object_to_set = if let Ok(text_data) = data_transfer.get_data("text") { - text_data.into() - } else { - web_sys::wasm_bindgen::JsValue::NULL.clone() - }; - - js_sys::Reflect::set( - &window_clone, - &Self::GLOBAL_CLIPBOARD_OBJECT.into(), - &object_to_set, - ) - .expect("Failed to set global clipboard object."); - } - }) as Box); - - // Set this event handler to execute before any child elements (third argument `true`) so that it is subsequently observed by other events. - window - .document() - .ok_or(Error::ClipboardNotSupported)? - .add_event_listener_with_callback_and_bool( - "paste", - &paste_callback.as_ref().unchecked_ref(), - true, - ) - .map_err(|_| Error::unknown("Could not add paste event listener."))?; - - Ok(Self { inner, _paste_callback: paste_callback, window }) + // If the clipboard is being opened for the first time, add a paste callback + if js_sys::Reflect::get(&window, &Self::GLOBAL_CALLBACK_OBJECT.into()) + .map_err(|_| Error::ClipboardNotSupported)?.is_falsy() { + let window_clone = window.clone(); + + let paste_callback = Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { + if let Some(data_transfer) = e.clipboard_data() { + let object_to_set = if let Ok(text_data) = data_transfer.get_data("text") { + text_data.into() + } else { + web_sys::wasm_bindgen::JsValue::NULL.clone() + }; + + js_sys::Reflect::set( + &window_clone, + &Self::GLOBAL_CLIPBOARD_OBJECT.into(), + &object_to_set, + ) + .expect("Failed to set global clipboard object."); + } + }) as Box); + + // Set this event handler to execute before any child elements (third argument `true`) so that it is subsequently observed by other events. + window + .document() + .ok_or(Error::ClipboardNotSupported)? + .add_event_listener_with_callback_and_bool( + "paste", + &paste_callback.as_ref().unchecked_ref(), + true, + ) + .map_err(|_| Error::unknown("Could not add paste event listener."))?; + + js_sys::Reflect::set( + &window, + &Self::GLOBAL_CALLBACK_OBJECT.into(), + &web_sys::wasm_bindgen::JsValue::TRUE, + ) + .expect("Failed to set global callback flag."); + + paste_callback.forget(); + } + + Ok(Self { inner, window }) } fn get_last_clipboard(&self) -> Option { @@ -72,7 +86,7 @@ impl<'clipboard> Clear<'clipboard> { } pub(crate) fn clear(self) -> Result<(), Error> { - let _ = self.clipboard.inner.write(&js_sys::Array::default()); + let _ = self.clipboard.inner.write_text(""); self.clipboard.set_last_clipboard(""); Ok(()) } From 9a973f4bef31f084de2ef1ea09cd1e084c3635dd Mon Sep 17 00:00:00 2001 From: "Douglas M. Dwyer" Date: Mon, 26 Aug 2024 17:44:11 -0400 Subject: [PATCH 5/5] Run cargo fmt --- src/platform/wasm.rs | 88 ++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/src/platform/wasm.rs b/src/platform/wasm.rs index a1937fe..0867bfe 100644 --- a/src/platform/wasm.rs +++ b/src/platform/wasm.rs @@ -7,7 +7,7 @@ use web_sys::wasm_bindgen::closure::Closure; pub(crate) struct Clipboard { inner: web_sys::Clipboard, - window: web_sys::Window + window: web_sys::Window, } impl Clipboard { @@ -18,48 +18,50 @@ impl Clipboard { let window = web_sys::window().ok_or(Error::ClipboardNotSupported)?; let inner = window.navigator().clipboard(); - // If the clipboard is being opened for the first time, add a paste callback - if js_sys::Reflect::get(&window, &Self::GLOBAL_CALLBACK_OBJECT.into()) - .map_err(|_| Error::ClipboardNotSupported)?.is_falsy() { - let window_clone = window.clone(); - - let paste_callback = Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { - if let Some(data_transfer) = e.clipboard_data() { - let object_to_set = if let Ok(text_data) = data_transfer.get_data("text") { - text_data.into() - } else { - web_sys::wasm_bindgen::JsValue::NULL.clone() - }; - - js_sys::Reflect::set( - &window_clone, - &Self::GLOBAL_CLIPBOARD_OBJECT.into(), - &object_to_set, - ) - .expect("Failed to set global clipboard object."); - } - }) as Box); - - // Set this event handler to execute before any child elements (third argument `true`) so that it is subsequently observed by other events. - window - .document() - .ok_or(Error::ClipboardNotSupported)? - .add_event_listener_with_callback_and_bool( - "paste", - &paste_callback.as_ref().unchecked_ref(), - true, - ) - .map_err(|_| Error::unknown("Could not add paste event listener."))?; - - js_sys::Reflect::set( - &window, - &Self::GLOBAL_CALLBACK_OBJECT.into(), - &web_sys::wasm_bindgen::JsValue::TRUE, - ) - .expect("Failed to set global callback flag."); - - paste_callback.forget(); - } + // If the clipboard is being opened for the first time, add a paste callback + if js_sys::Reflect::get(&window, &Self::GLOBAL_CALLBACK_OBJECT.into()) + .map_err(|_| Error::ClipboardNotSupported)? + .is_falsy() + { + let window_clone = window.clone(); + + let paste_callback = Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { + if let Some(data_transfer) = e.clipboard_data() { + let object_to_set = if let Ok(text_data) = data_transfer.get_data("text") { + text_data.into() + } else { + web_sys::wasm_bindgen::JsValue::NULL.clone() + }; + + js_sys::Reflect::set( + &window_clone, + &Self::GLOBAL_CLIPBOARD_OBJECT.into(), + &object_to_set, + ) + .expect("Failed to set global clipboard object."); + } + }) as Box); + + // Set this event handler to execute before any child elements (third argument `true`) so that it is subsequently observed by other events. + window + .document() + .ok_or(Error::ClipboardNotSupported)? + .add_event_listener_with_callback_and_bool( + "paste", + &paste_callback.as_ref().unchecked_ref(), + true, + ) + .map_err(|_| Error::unknown("Could not add paste event listener."))?; + + js_sys::Reflect::set( + &window, + &Self::GLOBAL_CALLBACK_OBJECT.into(), + &web_sys::wasm_bindgen::JsValue::TRUE, + ) + .expect("Failed to set global callback flag."); + + paste_callback.forget(); + } Ok(Self { inner, window }) }