Skip to main content

euv_ui/component/browser/hook/
impl.rs

1use super::*;
2
3/// Implementation of browser API functionality.
4impl UseEuvBrowser {
5    /// Creates browser API state for localStorage, sessionStorage, clipboard, and navigator access.
6    ///
7    /// # Returns
8    ///
9    /// - `UseEuvBrowser` - The browser API state.
10    pub fn use_browser_state() -> UseEuvBrowser {
11        UseEuvBrowser::default()
12    }
13
14    /// Reads a value from the browser localStorage.
15    ///
16    /// # Arguments
17    ///
18    /// - `K: AsRef<str>` - The key to look up.
19    ///
20    /// # Returns
21    ///
22    /// - `Option<String>` - The stored value if found, or None.
23    pub fn local_storage_get<K>(key: K) -> Option<String>
24    where
25        K: AsRef<str>,
26    {
27        let window: Window = window()?;
28        let storage: Storage = window.local_storage().ok()??;
29        storage.get_item(key.as_ref()).ok()?
30    }
31
32    /// Writes a key-value pair to the browser localStorage.
33    ///
34    /// # Arguments
35    ///
36    /// - `K: AsRef<str>` - The key to store.
37    /// - `V: AsRef<str>` - The value to store.
38    pub fn local_storage_set<K, V>(key: K, value: V)
39    where
40        K: AsRef<str>,
41        V: AsRef<str>,
42    {
43        let Some(window) = window() else {
44            return;
45        };
46        let storage: Storage = match window.local_storage() {
47            Ok(Some(local_storage)) => local_storage,
48            _ => return,
49        };
50        let _: Result<(), JsValue> = storage.set_item(key.as_ref(), value.as_ref());
51    }
52
53    /// Removes a key from the browser localStorage.
54    ///
55    /// # Arguments
56    ///
57    /// - `K: AsRef<str>` - The key to remove.
58    pub(crate) fn local_storage_remove<K>(key: K)
59    where
60        K: AsRef<str>,
61    {
62        let Some(window) = window() else {
63            return;
64        };
65        let storage: Storage = match window.local_storage() {
66            Ok(Some(local_storage)) => local_storage,
67            _ => return,
68        };
69        let _: Result<(), JsValue> = storage.remove_item(key.as_ref());
70    }
71
72    /// Reads a value from the browser sessionStorage.
73    ///
74    /// # Arguments
75    ///
76    /// - `K: AsRef<str>` - The key to look up.
77    ///
78    /// # Returns
79    ///
80    /// - `Option<String>` - The stored value if found, or None.
81    pub(crate) fn session_storage_get<K>(key: K) -> Option<String>
82    where
83        K: AsRef<str>,
84    {
85        let window: Window = window()?;
86        let storage: Storage = window.session_storage().ok()??;
87        storage.get_item(key.as_ref()).ok()?
88    }
89
90    /// Writes a key-value pair to the browser sessionStorage.
91    ///
92    /// # Arguments
93    ///
94    /// - `K: AsRef<str>` - The key to store.
95    /// - `V: AsRef<str>` - The value to store.
96    pub(crate) fn session_storage_set<K, V>(key: K, value: V)
97    where
98        K: AsRef<str>,
99        V: AsRef<str>,
100    {
101        let Some(window) = window() else {
102            return;
103        };
104        let storage: Storage = match window.session_storage() {
105            Ok(Some(session_storage)) => session_storage,
106            _ => return,
107        };
108        let _: Result<(), JsValue> = storage.set_item(key.as_ref(), value.as_ref());
109    }
110
111    /// Removes a key from the browser sessionStorage.
112    ///
113    /// # Arguments
114    ///
115    /// - `K: AsRef<str>` - The key to remove.
116    pub(crate) fn session_storage_remove<K>(key: K)
117    where
118        K: AsRef<str>,
119    {
120        let Some(window) = window() else {
121            return;
122        };
123        let storage: Storage = match window.session_storage() {
124            Ok(Some(session_storage)) => session_storage,
125            _ => return,
126        };
127        let _: Result<(), JsValue> = storage.remove_item(key.as_ref());
128    }
129
130    /// Reads text from the system clipboard asynchronously.
131    ///
132    /// # Returns
133    ///
134    /// - `String` - The clipboard text content, or an error message.
135    pub(crate) async fn clipboard_read_text() -> String {
136        let Some(window) = window() else {
137            return String::new();
138        };
139        let navigator: Navigator = window.navigator();
140        match Reflect::get(&navigator, &JsValue::from_str("clipboard")) {
141            Ok(clipboard_obj) if !clipboard_obj.is_undefined() => {
142                let clipboard: Clipboard = navigator.clipboard();
143                let promise: Promise = clipboard.read_text();
144                let future: JsFuture = JsFuture::from(promise);
145                match future.await {
146                    Ok(value) => value
147                        .as_string()
148                        .unwrap_or_else(|| "No text content".to_string()),
149                    Err(_) => "Failed to read clipboard".to_string(),
150                }
151            }
152            _ => "Clipboard API not available (requires secure context)".to_string(),
153        }
154    }
155
156    /// Writes text to the system clipboard asynchronously.
157    ///
158    /// # Arguments
159    ///
160    /// - `T: AsRef<str>` - The text to write.
161    ///
162    /// # Returns
163    ///
164    /// - `bool` - Whether the write succeeded.
165    pub(crate) async fn clipboard_write_text<T>(text: T) -> bool
166    where
167        T: AsRef<str>,
168    {
169        let Some(window) = window() else {
170            return false;
171        };
172        let navigator: Navigator = window.navigator();
173        match js_sys::Reflect::get(&navigator, &JsValue::from_str("clipboard")) {
174            Ok(clipboard_obj) if !clipboard_obj.is_undefined() => {
175                let clipboard: Clipboard = navigator.clipboard();
176                let promise: Promise = clipboard.write_text(text.as_ref());
177                let future: JsFuture = JsFuture::from(promise);
178                future.await.is_ok()
179            }
180            _ => false,
181        }
182    }
183
184    /// Reads the browser window inner dimensions.
185    ///
186    /// # Returns
187    ///
188    /// - `(i32, i32)` - The viewport's inner width and height in CSS pixels.
189    pub(crate) fn window_inner_size() -> (i32, i32) {
190        let Some(window) = window() else {
191            return (0, 0);
192        };
193        let width: i32 = window
194            .inner_width()
195            .ok()
196            .map(|value: JsValue| Number::from(value).value_of() as i32)
197            .unwrap_or_default();
198        let height: i32 = window
199            .inner_height()
200            .ok()
201            .map(|value: JsValue| Number::from(value).value_of() as i32)
202            .unwrap_or_default();
203        (width, height)
204    }
205
206    /// Reads the browser navigator user agent string.
207    ///
208    /// # Returns
209    ///
210    /// - `String` - The user agent string.
211    pub(crate) fn navigator_user_agent() -> String {
212        let Some(window) = window() else {
213            return String::new();
214        };
215        window
216            .navigator()
217            .user_agent()
218            .unwrap_or_else(|_: JsValue| "Unknown".to_string())
219    }
220
221    /// Reads the browser navigator language.
222    ///
223    /// # Returns
224    ///
225    /// - `String` - The preferred language string.
226    pub(crate) fn navigator_language() -> String {
227        let Some(window) = window() else {
228            return String::new();
229        };
230        window
231            .navigator()
232            .language()
233            .unwrap_or_else(|| "Unknown".to_string())
234    }
235
236    /// Reads the current browser location href.
237    ///
238    /// # Returns
239    ///
240    /// - `String` - The current full URL.
241    pub(crate) fn location_href() -> String {
242        let Some(window) = window() else {
243            return String::new();
244        };
245        window
246            .location()
247            .href()
248            .unwrap_or_else(|_error: JsValue| "Unknown".to_string())
249    }
250
251    /// Reads the current browser location origin.
252    ///
253    /// # Returns
254    ///
255    /// - `String` - The origin portion of the URL.
256    pub(crate) fn location_origin() -> String {
257        let Some(window) = window() else {
258            return String::new();
259        };
260        window
261            .location()
262            .origin()
263            .unwrap_or_else(|_error: JsValue| "Unknown".to_string())
264    }
265
266    /// Reads the current browser location pathname.
267    ///
268    /// # Returns
269    ///
270    /// - `String` - The pathname portion of the URL.
271    pub(crate) fn location_pathname() -> String {
272        let Some(window) = window() else {
273            return String::new();
274        };
275        window
276            .location()
277            .pathname()
278            .unwrap_or_else(|_error: JsValue| "Unknown".to_string())
279    }
280
281    /// Creates a click event handler that sets a localStorage item.
282    ///
283    /// # Arguments
284    ///
285    /// - `UseEuvBrowser` - The browser API state.
286    ///
287    /// # Returns
288    ///
289    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to set the localStorage item.
290    pub fn on_local_storage_set(self) -> Option<Rc<dyn Fn(Event)>> {
291        Some(Rc::new(move |_: Event| {
292            let key: String = self.get_local_key().get();
293            let value: String = self.get_local_value().get();
294            if !key.is_empty() {
295                Self::local_storage_set(&key, &value);
296                self.get_local_result().set(format!("Set: {key} = {value}"));
297            }
298        }))
299    }
300
301    /// Creates a click event handler that gets a localStorage item.
302    ///
303    /// # Arguments
304    ///
305    /// - `UseEuvBrowser` - The browser API state.
306    ///
307    /// # Returns
308    ///
309    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to get the localStorage item.
310    pub fn on_local_storage_get(self) -> Option<Rc<dyn Fn(Event)>> {
311        Some(Rc::new(move |_: Event| {
312            let key: String = self.get_local_key().get();
313            let value: Option<String> = Self::local_storage_get(&key);
314            match value {
315                Some(v) => self.get_local_result().set(format!("Get: {key} = {v}")),
316                None => self
317                    .get_local_result()
318                    .set(format!("Key '{key}' not found")),
319            }
320        }))
321    }
322
323    /// Creates a click event handler that removes a localStorage item.
324    ///
325    /// # Arguments
326    ///
327    /// - `UseEuvBrowser` - The browser API state.
328    ///
329    /// # Returns
330    ///
331    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to remove the localStorage item.
332    pub fn on_local_storage_remove(self) -> Option<Rc<dyn Fn(Event)>> {
333        Some(Rc::new(move |_: Event| {
334            let key: String = self.get_local_key().get();
335            Self::local_storage_remove(&key);
336            self.get_local_result().set(format!("Removed key: {key}"));
337        }))
338    }
339
340    /// Creates a click event handler that sets a sessionStorage item.
341    ///
342    /// # Arguments
343    ///
344    /// - `UseEuvBrowser` - The browser API state.
345    ///
346    /// # Returns
347    ///
348    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to set the sessionStorage item.
349    pub fn on_session_storage_set(self) -> Option<Rc<dyn Fn(Event)>> {
350        Some(Rc::new(move |_: Event| {
351            let key: String = self.get_session_key().get();
352            let value: String = self.get_session_value().get();
353            if !key.is_empty() {
354                Self::session_storage_set(&key, &value);
355                self.get_session_result()
356                    .set(format!("Set: {key} = {value}"));
357            }
358        }))
359    }
360
361    /// Creates a click event handler that gets a sessionStorage item.
362    ///
363    /// # Arguments
364    ///
365    /// - `UseEuvBrowser` - The browser API state.
366    ///
367    /// # Returns
368    ///
369    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to get the sessionStorage item.
370    pub fn on_session_storage_get(self) -> Option<Rc<dyn Fn(Event)>> {
371        Some(Rc::new(move |_: Event| {
372            let key: String = self.get_session_key().get();
373            let value: Option<String> = Self::session_storage_get(&key);
374            match value {
375                Some(v) => self.get_session_result().set(format!("Get: {key} = {v}")),
376                None => self
377                    .get_session_result()
378                    .set(format!("Key '{key}' not found")),
379            }
380        }))
381    }
382
383    /// Creates a click event handler that removes a sessionStorage item.
384    ///
385    /// # Arguments
386    ///
387    /// - `UseEuvBrowser` - The browser API state.
388    ///
389    /// # Returns
390    ///
391    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to remove the sessionStorage item.
392    pub fn on_session_storage_remove(self) -> Option<Rc<dyn Fn(Event)>> {
393        Some(Rc::new(move |_: Event| {
394            let key: String = self.get_session_key().get();
395            Self::session_storage_remove(&key);
396            self.get_session_result().set(format!("Removed key: {key}"));
397        }))
398    }
399
400    /// Creates a click event handler that copies text to clipboard.
401    ///
402    /// # Arguments
403    ///
404    /// - `UseEuvBrowser` - The browser API state.
405    ///
406    /// # Returns
407    ///
408    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to copy text to clipboard.
409    pub fn on_clipboard_copy(self) -> Option<Rc<dyn Fn(Event)>> {
410        Some(Rc::new(move |_: Event| {
411            let text: String = self.get_clipboard_text().get();
412            let text_clone: String = text.clone();
413            let result: Signal<String> = self.get_clipboard_result();
414            if text.is_empty() {
415                result.set("Please enter text to copy".to_string());
416                return;
417            }
418            let Some(window) = window() else {
419                return;
420            };
421            let navigator: Navigator = window.navigator();
422            match js_sys::Reflect::get(&navigator, &JsValue::from_str("clipboard")) {
423                Ok(clipboard_obj) if !clipboard_obj.is_undefined() => {
424                    spawn_local(async move {
425                        let success: bool = Self::clipboard_write_text(&text_clone).await;
426                        if success {
427                            result.set("Copied to clipboard!".to_string());
428                        } else {
429                            result.set("Failed to copy".to_string());
430                        }
431                    });
432                }
433                _ => {
434                    result.set("Clipboard API not available (requires secure context)".to_string());
435                }
436            }
437        }))
438    }
439
440    /// Creates a click event handler that reads text from clipboard.
441    ///
442    /// # Arguments
443    ///
444    /// - `UseEuvBrowser` - The browser API state.
445    ///
446    /// # Returns
447    ///
448    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to read text from clipboard.
449    pub fn on_clipboard_paste(self) -> Option<Rc<dyn Fn(Event)>> {
450        Some(Rc::new(move |_: Event| {
451            let result: Signal<String> = self.get_clipboard_result();
452            let Some(window) = window() else {
453                return;
454            };
455            let navigator: Navigator = window.navigator();
456            match js_sys::Reflect::get(&navigator, &JsValue::from_str("clipboard")) {
457                Ok(clipboard_obj) if !clipboard_obj.is_undefined() => {
458                    spawn_local(async move {
459                        let text: String = Self::clipboard_read_text().await;
460                        result.set(format!("Pasted: {text}"));
461                    });
462                }
463                _ => {
464                    result.set("Clipboard API not available (requires secure context)".to_string());
465                }
466            }
467        }))
468    }
469
470    /// Creates a click event handler that refreshes the window size display.
471    ///
472    /// # Arguments
473    ///
474    /// - `UseEuvBrowser` - The browser API state.
475    ///
476    /// # Returns
477    ///
478    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to refresh the window size.
479    pub fn on_window_refresh_size(self) -> Option<Rc<dyn Fn(Event)>> {
480        Some(Rc::new(move |_: Event| {
481            let (width, height): (i32, i32) = Self::window_inner_size();
482            self.get_window_size().set(format!("{width} x {height}"));
483        }))
484    }
485
486    /// Creates a click event handler that logs a console message.
487    ///
488    /// # Arguments
489    ///
490    /// - `Signal<String>` - The console input signal.
491    ///
492    /// # Returns
493    ///
494    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler for console.log.
495    pub fn on_console_log(console_input: Signal<String>) -> Option<Rc<dyn Fn(Event)>> {
496        Some(Rc::new(move |_: Event| {
497            let raw: String = console_input.get();
498            let message: &str = if raw.is_empty() {
499                CONSOLE_LOG_DEFAULT_MESSAGE
500            } else {
501                &raw
502            };
503            Console::log(message);
504        }))
505    }
506
507    /// Creates a click event handler that warns a console message.
508    ///
509    /// # Arguments
510    ///
511    /// - `Signal<String>` - The console input signal.
512    ///
513    /// # Returns
514    ///
515    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler for console.warn.
516    pub fn on_console_warn(console_input: Signal<String>) -> Option<Rc<dyn Fn(Event)>> {
517        Some(Rc::new(move |_: Event| {
518            let raw: String = console_input.get();
519            let message: &str = if raw.is_empty() {
520                CONSOLE_WARN_DEFAULT_MESSAGE
521            } else {
522                &raw
523            };
524            Console::warn(message);
525        }))
526    }
527
528    /// Creates a click event handler that errors a console message.
529    ///
530    /// # Arguments
531    ///
532    /// - `Signal<String>` - The console input signal.
533    ///
534    /// # Returns
535    ///
536    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler for console.error.
537    pub fn on_console_error(console_input: Signal<String>) -> Option<Rc<dyn Fn(Event)>> {
538        Some(Rc::new(move |_: Event| {
539            let raw: String = console_input.get();
540            let message: &str = if raw.is_empty() {
541                CONSOLE_ERROR_DEFAULT_MESSAGE
542            } else {
543                &raw
544            };
545            Console::error(message);
546        }))
547    }
548}
549
550/// Default implementation for `UseEuvBrowser`.
551impl Default for UseEuvBrowser {
552    /// Constructs a default [`UseEuvBrowser`] value.
553    fn default() -> Self {
554        let window_size_val: String = {
555            let (width, height): (i32, i32) = UseEuvBrowser::window_inner_size();
556            format!("{width} x {height}")
557        };
558        UseEuvBrowser {
559            local_key: App::use_signal(String::new),
560            local_value: App::use_signal(String::new),
561            local_result: App::use_signal(String::new),
562            session_key: App::use_signal(String::new),
563            session_value: App::use_signal(String::new),
564            session_result: App::use_signal(String::new),
565            clipboard_text: App::use_signal(String::new),
566            clipboard_result: App::use_signal(String::new),
567            window_size: App::use_signal(move || window_size_val.clone()),
568            user_agent: App::use_signal(UseEuvBrowser::navigator_user_agent),
569            language: App::use_signal(UseEuvBrowser::navigator_language),
570            location_url: App::use_signal(UseEuvBrowser::location_href),
571            location_origin_val: App::use_signal(UseEuvBrowser::location_origin),
572            location_pathname_val: App::use_signal(UseEuvBrowser::location_pathname),
573            console_input: App::use_signal(String::new),
574        }
575    }
576}