atproto blogging
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}