logicaffeine_web/ui/
clipboard.rs1use std::future::Future;
13use std::pin::Pin;
14
15pub fn copy(text: &str) -> Pin<Box<dyn Future<Output = bool>>> {
18 #[cfg(target_arch = "wasm32")]
19 {
20 use wasm_bindgen::{JsCast, JsValue};
21 let Some(window) = web_sys::window() else { return Box::pin(std::future::ready(false)) };
22 let navigator = window.navigator();
23 let api = match js_sys::Reflect::get(&navigator, &JsValue::from_str("clipboard")) {
24 Ok(value) if !value.is_undefined() && !value.is_null() => Some(value.unchecked_into::<web_sys::Clipboard>()),
25 _ => None,
26 };
27 match api {
28 Some(clipboard) => {
29 let promise = clipboard.write_text(text);
31 Box::pin(async move { wasm_bindgen_futures::JsFuture::from(promise).await.is_ok() })
32 }
33 None => Box::pin(std::future::ready(copy_by_command(text))),
34 }
35 }
36 #[cfg(not(target_arch = "wasm32"))]
37 {
38 let _ = text;
39 Box::pin(std::future::ready(false))
40 }
41}
42
43pub fn copy_by_command(text: &str) -> bool {
47 #[cfg(target_arch = "wasm32")]
48 {
49 use wasm_bindgen::{JsCast, JsValue};
50 let Some(window) = web_sys::window() else { return false };
51 let Some(document) = window.document() else { return false };
52 let Some(body) = document.body() else { return false };
53 let Ok(element) = document.create_element("textarea") else { return false };
54 let Ok(area) = element.dyn_into::<web_sys::HtmlTextAreaElement>() else { return false };
55 area.set_value(text);
56 area.set_read_only(true);
57 let _ = area.set_attribute("aria-hidden", "true");
58 let _ = area.set_attribute("style", "position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;border:0;padding:0;margin:0");
59 if body.append_child(&area).is_err() {
60 return false;
61 }
62 let _ = area.focus();
63 area.select();
64 let copied = js_sys::Reflect::get(&document, &JsValue::from_str("execCommand"))
66 .ok()
67 .and_then(|f| f.dyn_into::<js_sys::Function>().ok())
68 .and_then(|f| f.call1(&document, &JsValue::from_str("copy")).ok())
69 .is_some_and(|v| v.is_truthy());
70 area.remove();
71 if let Ok(Some(selection)) = window.get_selection() {
72 let _ = selection.remove_all_ranges();
73 }
74 copied
75 }
76 #[cfg(not(target_arch = "wasm32"))]
77 {
78 let _ = text;
79 false
80 }
81}
82
83pub fn select_contents(id: &str) {
86 #[cfg(target_arch = "wasm32")]
87 {
88 use wasm_bindgen::JsCast;
89 let Some(window) = web_sys::window() else { return };
90 let Some(document) = window.document() else { return };
91 let Some(element) = document.get_element_by_id(id) else { return };
92 if let Some(focusable) = element.dyn_ref::<web_sys::HtmlElement>() {
93 let _ = focusable.focus();
94 }
95 let (Ok(Some(selection)), Ok(range)) = (window.get_selection(), document.create_range()) else { return };
96 let Ok(walker) = document.create_tree_walker_with_what_to_show(&element, 4 ) else { return };
97 let mut first: Option<web_sys::Node> = None;
98 let mut last: Option<web_sys::Node> = None;
99 while let Ok(Some(node)) = walker.next_node() {
100 if first.is_none() {
101 first = Some(node.clone());
102 }
103 last = Some(node);
104 }
105 let (Some(first), Some(last)) = (first, last) else { return };
106 let end = last.text_content().map_or(0, |t| t.encode_utf16().count() as u32);
107 if range.set_start(&first, 0).is_err() || range.set_end(&last, end).is_err() {
108 return;
109 }
110 let _ = selection.remove_all_ranges();
111 let _ = selection.add_range(&range);
112 }
113 #[cfg(not(target_arch = "wasm32"))]
114 let _ = id;
115}