at main 191 lines 6.5 kB view raw
1//! Browser clipboard implementation. 2//! 3//! Implements `ClipboardPlatform` for browser environments using the 4//! ClipboardEvent's DataTransfer API for sync access and the async 5//! Clipboard API for custom MIME types. 6 7use weaver_editor_core::ClipboardPlatform; 8 9/// Browser clipboard context wrapping a ClipboardEvent's DataTransfer. 10/// 11/// Created from a clipboard event (copy, cut, paste) to provide sync 12/// clipboard access. Also spawns async tasks for custom MIME types. 13pub struct BrowserClipboard { 14 data_transfer: Option<web_sys::DataTransfer>, 15} 16 17impl BrowserClipboard { 18 /// Create from a ClipboardEvent. 19 /// 20 /// Call this in your copy/cut/paste event handler. 21 pub fn from_event(evt: &web_sys::ClipboardEvent) -> Self { 22 Self { 23 data_transfer: evt.clipboard_data(), 24 } 25 } 26 27 /// Create an empty clipboard context (for testing or non-event contexts). 28 pub fn empty() -> Self { 29 Self { 30 data_transfer: None, 31 } 32 } 33} 34 35impl ClipboardPlatform for BrowserClipboard { 36 fn write_text(&self, text: &str) { 37 // Sync write via DataTransfer (immediate fallback). 38 if let Some(dt) = &self.data_transfer { 39 if let Err(e) = dt.set_data("text/plain", text) { 40 tracing::warn!("Clipboard sync write failed: {:?}", e); 41 } 42 } 43 44 // Async write for custom MIME type (enables internal paste detection). 45 let text = text.to_string(); 46 wasm_bindgen_futures::spawn_local(async move { 47 if let Err(e) = crate::events::write_clipboard_with_custom_type(&text).await { 48 tracing::debug!("Clipboard async write failed: {:?}", e); 49 } 50 }); 51 } 52 53 fn write_html(&self, html: &str, plain_text: &str) { 54 // Sync write of plain text fallback. 55 if let Some(dt) = &self.data_transfer { 56 if let Err(e) = dt.set_data("text/plain", plain_text) { 57 tracing::warn!("Clipboard sync write (plain) failed: {:?}", e); 58 } 59 } 60 61 // Async write for HTML. 62 let html = html.to_string(); 63 let plain = plain_text.to_string(); 64 wasm_bindgen_futures::spawn_local(async move { 65 if let Err(e) = write_html_to_clipboard(&html, &plain).await { 66 tracing::warn!("Clipboard HTML write failed: {:?}", e); 67 } 68 }); 69 } 70 71 fn read_text(&self) -> Option<String> { 72 let dt = self.data_transfer.as_ref()?; 73 74 // Try our custom MIME type first (internal paste). 75 if let Ok(text) = dt.get_data("text/x-weaver-md") { 76 if !text.is_empty() { 77 return Some(text); 78 } 79 } 80 81 // Fall back to plain text. 82 dt.get_data("text/plain").ok().filter(|s| !s.is_empty()) 83 } 84} 85 86/// Write HTML and plain text to clipboard using the async Clipboard API. 87/// 88/// This uses the navigator.clipboard API and doesn't require a clipboard event. 89/// Suitable for keyboard-triggered copy operations like CopyAsHtml. 90pub async fn write_html_to_clipboard( 91 html: &str, 92 plain_text: &str, 93) -> Result<(), wasm_bindgen::JsValue> { 94 use js_sys::{Array, Object, Reflect}; 95 use wasm_bindgen::JsValue; 96 use web_sys::{Blob, BlobPropertyBag, ClipboardItem}; 97 98 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; 99 let clipboard = window.navigator().clipboard(); 100 101 // Create HTML blob. 102 let html_parts = Array::new(); 103 html_parts.push(&JsValue::from_str(html)); 104 let html_opts = BlobPropertyBag::new(); 105 html_opts.set_type("text/html"); 106 let html_blob = Blob::new_with_str_sequence_and_options(&html_parts, &html_opts)?; 107 108 // Create plain text blob. 109 let text_parts = Array::new(); 110 text_parts.push(&JsValue::from_str(plain_text)); 111 let text_opts = BlobPropertyBag::new(); 112 text_opts.set_type("text/plain"); 113 let text_blob = Blob::new_with_str_sequence_and_options(&text_parts, &text_opts)?; 114 115 // Create ClipboardItem with both types. 116 let item_data = Object::new(); 117 Reflect::set(&item_data, &JsValue::from_str("text/html"), &html_blob)?; 118 Reflect::set(&item_data, &JsValue::from_str("text/plain"), &text_blob)?; 119 120 let clipboard_item = ClipboardItem::new_with_record_from_str_to_blob_promise(&item_data)?; 121 let items = Array::new(); 122 items.push(&clipboard_item); 123 124 wasm_bindgen_futures::JsFuture::from(clipboard.write(&items)).await?; 125 tracing::debug!("Wrote {} bytes of HTML to clipboard", html.len()); 126 Ok(()) 127} 128 129// === Dioxus event handlers === 130 131/// Handle a Dioxus paste event. 132/// 133/// Extracts text from the clipboard event and inserts at cursor. 134#[cfg(feature = "dioxus")] 135pub fn handle_paste<D: weaver_editor_core::EditorDocument>( 136 evt: dioxus_core::Event<dioxus_html::ClipboardData>, 137 doc: &mut D, 138) { 139 use dioxus_web::WebEventExt; 140 use wasm_bindgen::JsCast; 141 142 evt.prevent_default(); 143 144 let base_evt = evt.as_web_event(); 145 if let Some(clipboard_evt) = base_evt.dyn_ref::<web_sys::ClipboardEvent>() { 146 let clipboard = BrowserClipboard::from_event(clipboard_evt); 147 weaver_editor_core::clipboard_paste(doc, &clipboard); 148 } else { 149 tracing::warn!("[PASTE] Failed to cast to ClipboardEvent"); 150 } 151} 152 153/// Handle a Dioxus cut event. 154/// 155/// Copies selection to clipboard, then deletes it. 156#[cfg(feature = "dioxus")] 157pub fn handle_cut<D: weaver_editor_core::EditorDocument>( 158 evt: dioxus_core::Event<dioxus_html::ClipboardData>, 159 doc: &mut D, 160) { 161 use dioxus_web::WebEventExt; 162 use wasm_bindgen::JsCast; 163 164 evt.prevent_default(); 165 166 let base_evt = evt.as_web_event(); 167 if let Some(clipboard_evt) = base_evt.dyn_ref::<web_sys::ClipboardEvent>() { 168 let clipboard = BrowserClipboard::from_event(clipboard_evt); 169 weaver_editor_core::clipboard_cut(doc, &clipboard); 170 } 171} 172 173/// Handle a Dioxus copy event. 174/// 175/// Copies selection to clipboard. Only prevents default if there was a selection. 176#[cfg(feature = "dioxus")] 177pub fn handle_copy<D: weaver_editor_core::EditorDocument>( 178 evt: dioxus_core::Event<dioxus_html::ClipboardData>, 179 doc: &D, 180) { 181 use dioxus_web::WebEventExt; 182 use wasm_bindgen::JsCast; 183 184 let base_evt = evt.as_web_event(); 185 if let Some(clipboard_evt) = base_evt.dyn_ref::<web_sys::ClipboardEvent>() { 186 let clipboard = BrowserClipboard::from_event(clipboard_evt); 187 if weaver_editor_core::clipboard_copy(doc, &clipboard) { 188 evt.prevent_default(); 189 } 190 } 191}