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` - 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` - The key to store.
37    /// - `V` - 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` - 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` - 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` - The key to store.
95    /// - `V` - 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` - 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(
141            &navigator,
142            &JsValue::from_str(BROWSER_NAVIGATOR_CLIPBOARD_KEY),
143        ) {
144            Ok(clipboard_obj) if !clipboard_obj.is_undefined() => {
145                let clipboard: Clipboard = navigator.clipboard();
146                let promise: Promise = clipboard.read_text();
147                let future: JsFuture = JsFuture::from(promise);
148                match future.await {
149                    Ok(value) => value
150                        .as_string()
151                        .unwrap_or_else(|| BROWSER_CLIPBOARD_NO_TEXT_CONTENT.to_string()),
152                    Err(_) => BROWSER_CLIPBOARD_READ_FAILED.to_string(),
153                }
154            }
155            _ => BROWSER_CLIPBOARD_UNAVAILABLE.to_string(),
156        }
157    }
158
159    /// Writes text to the system clipboard asynchronously.
160    ///
161    /// # Arguments
162    ///
163    /// - `T` - The text to write.
164    ///
165    /// # Returns
166    ///
167    /// - `bool` - Whether the write succeeded.
168    pub(crate) async fn clipboard_write_text<T>(text: T) -> bool
169    where
170        T: AsRef<str>,
171    {
172        let Some(window) = window() else {
173            return false;
174        };
175        let navigator: Navigator = window.navigator();
176        match js_sys::Reflect::get(
177            &navigator,
178            &JsValue::from_str(BROWSER_NAVIGATOR_CLIPBOARD_KEY),
179        ) {
180            Ok(clipboard_obj) if !clipboard_obj.is_undefined() => {
181                let clipboard: Clipboard = navigator.clipboard();
182                let promise: Promise = clipboard.write_text(text.as_ref());
183                let future: JsFuture = JsFuture::from(promise);
184                future.await.is_ok()
185            }
186            _ => false,
187        }
188    }
189
190    /// Reads the browser window inner dimensions.
191    ///
192    /// # Returns
193    ///
194    /// - `(i32, i32)` - The viewport's inner width and height in CSS pixels.
195    pub(crate) fn window_inner_size() -> (i32, i32) {
196        let Some(window) = window() else {
197            return (0, 0);
198        };
199        let width: i32 = window
200            .inner_width()
201            .ok()
202            .map(|value: JsValue| Number::from(value).value_of() as i32)
203            .unwrap_or_default();
204        let height: i32 = window
205            .inner_height()
206            .ok()
207            .map(|value: JsValue| Number::from(value).value_of() as i32)
208            .unwrap_or_default();
209        (width, height)
210    }
211
212    /// Reads the browser navigator user agent string.
213    ///
214    /// # Returns
215    ///
216    /// - `String` - The user agent string.
217    pub(crate) fn navigator_user_agent() -> String {
218        let Some(window) = window() else {
219            return String::new();
220        };
221        window
222            .navigator()
223            .user_agent()
224            .unwrap_or_else(|_: JsValue| BROWSER_VALUE_UNKNOWN.to_string())
225    }
226
227    /// Reads the browser navigator language.
228    ///
229    /// # Returns
230    ///
231    /// - `String` - The preferred language string.
232    pub(crate) fn navigator_language() -> String {
233        let Some(window) = window() else {
234            return String::new();
235        };
236        window
237            .navigator()
238            .language()
239            .unwrap_or_else(|| BROWSER_VALUE_UNKNOWN.to_string())
240    }
241
242    /// Reads the current browser location href.
243    ///
244    /// # Returns
245    ///
246    /// - `String` - The current full URL.
247    pub(crate) fn location_href() -> String {
248        let Some(window) = window() else {
249            return String::new();
250        };
251        window
252            .location()
253            .href()
254            .unwrap_or_else(|_error: JsValue| BROWSER_VALUE_UNKNOWN.to_string())
255    }
256
257    /// Reads the current browser location origin.
258    ///
259    /// # Returns
260    ///
261    /// - `String` - The origin portion of the URL.
262    pub(crate) fn location_origin() -> String {
263        let Some(window) = window() else {
264            return String::new();
265        };
266        window
267            .location()
268            .origin()
269            .unwrap_or_else(|_error: JsValue| BROWSER_VALUE_UNKNOWN.to_string())
270    }
271
272    /// Reads the current browser location pathname.
273    ///
274    /// # Returns
275    ///
276    /// - `String` - The pathname portion of the URL.
277    pub(crate) fn location_pathname() -> String {
278        let Some(window) = window() else {
279            return String::new();
280        };
281        window
282            .location()
283            .pathname()
284            .unwrap_or_else(|_error: JsValue| BROWSER_VALUE_UNKNOWN.to_string())
285    }
286
287    /// Creates a click event handler that sets a localStorage item.
288    ///
289    /// # Returns
290    ///
291    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to set the localStorage item.
292    pub fn on_local_storage_set(self) -> Option<Rc<dyn Fn(Event)>> {
293        Some(Rc::new(move |_: Event| {
294            let key: String = self.get_local_key().get();
295            let value: String = self.get_local_value().get();
296            if !key.is_empty() {
297                Self::local_storage_set(&key, &value);
298                self.get_local_result().set(format!("Set: {key} = {value}"));
299            }
300        }))
301    }
302
303    /// Creates a click event handler that gets a localStorage item.
304    ///
305    /// # Returns
306    ///
307    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to get the localStorage item.
308    pub fn on_local_storage_get(self) -> Option<Rc<dyn Fn(Event)>> {
309        Some(Rc::new(move |_: Event| {
310            let key: String = self.get_local_key().get();
311            let value: Option<String> = Self::local_storage_get(&key);
312            match value {
313                Some(v) => self.get_local_result().set(format!("Get: {key} = {v}")),
314                None => self
315                    .get_local_result()
316                    .set(format!("Key '{key}' not found")),
317            }
318        }))
319    }
320
321    /// Creates a click event handler that removes a localStorage item.
322    ///
323    /// # Returns
324    ///
325    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to remove the localStorage item.
326    pub fn on_local_storage_remove(self) -> Option<Rc<dyn Fn(Event)>> {
327        Some(Rc::new(move |_: Event| {
328            let key: String = self.get_local_key().get();
329            Self::local_storage_remove(&key);
330            self.get_local_result().set(format!("Removed key: {key}"));
331        }))
332    }
333
334    /// Creates a click event handler that sets a sessionStorage item.
335    ///
336    /// # Returns
337    ///
338    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to set the sessionStorage item.
339    pub fn on_session_storage_set(self) -> Option<Rc<dyn Fn(Event)>> {
340        Some(Rc::new(move |_: Event| {
341            let key: String = self.get_session_key().get();
342            let value: String = self.get_session_value().get();
343            if !key.is_empty() {
344                Self::session_storage_set(&key, &value);
345                self.get_session_result()
346                    .set(format!("Set: {key} = {value}"));
347            }
348        }))
349    }
350
351    /// Creates a click event handler that gets a sessionStorage item.
352    ///
353    /// # Returns
354    ///
355    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to get the sessionStorage item.
356    pub fn on_session_storage_get(self) -> Option<Rc<dyn Fn(Event)>> {
357        Some(Rc::new(move |_: Event| {
358            let key: String = self.get_session_key().get();
359            let value: Option<String> = Self::session_storage_get(&key);
360            match value {
361                Some(v) => self.get_session_result().set(format!("Get: {key} = {v}")),
362                None => self
363                    .get_session_result()
364                    .set(format!("Key '{key}' not found")),
365            }
366        }))
367    }
368
369    /// Creates a click event handler that removes a sessionStorage item.
370    ///
371    /// # Returns
372    ///
373    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to remove the sessionStorage item.
374    pub fn on_session_storage_remove(self) -> Option<Rc<dyn Fn(Event)>> {
375        Some(Rc::new(move |_: Event| {
376            let key: String = self.get_session_key().get();
377            Self::session_storage_remove(&key);
378            self.get_session_result().set(format!("Removed key: {key}"));
379        }))
380    }
381
382    /// Creates a click event handler that copies text to clipboard.
383    ///
384    /// # Returns
385    ///
386    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to copy text to clipboard.
387    pub fn on_clipboard_copy(self) -> Option<Rc<dyn Fn(Event)>> {
388        Some(Rc::new(move |_: Event| {
389            let text: String = self.get_clipboard_text().get();
390            let text_clone: String = text.clone();
391            let result: Signal<String> = self.get_clipboard_result();
392            if text.is_empty() {
393                result.set(BROWSER_COPY_EMPTY_TEXT.to_string());
394                return;
395            }
396            let Some(window) = window() else {
397                return;
398            };
399            let navigator: Navigator = window.navigator();
400            match js_sys::Reflect::get(
401                &navigator,
402                &JsValue::from_str(BROWSER_NAVIGATOR_CLIPBOARD_KEY),
403            ) {
404                Ok(clipboard_obj) if !clipboard_obj.is_undefined() => {
405                    spawn_local(async move {
406                        let success: bool = Self::clipboard_write_text(&text_clone).await;
407                        if success {
408                            result.set(BROWSER_COPY_SUCCEEDED.to_string());
409                        } else {
410                            result.set(BROWSER_COPY_FAILED.to_string());
411                        }
412                    });
413                }
414                _ => {
415                    result.set(BROWSER_CLIPBOARD_UNAVAILABLE.to_string());
416                }
417            }
418        }))
419    }
420
421    /// Creates a click event handler that reads text from clipboard.
422    ///
423    /// # Returns
424    ///
425    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to read text from clipboard.
426    pub fn on_clipboard_paste(self) -> Option<Rc<dyn Fn(Event)>> {
427        Some(Rc::new(move |_: Event| {
428            let result: Signal<String> = self.get_clipboard_result();
429            let Some(window) = window() else {
430                return;
431            };
432            let navigator: Navigator = window.navigator();
433            match js_sys::Reflect::get(
434                &navigator,
435                &JsValue::from_str(BROWSER_NAVIGATOR_CLIPBOARD_KEY),
436            ) {
437                Ok(clipboard_obj) if !clipboard_obj.is_undefined() => {
438                    spawn_local(async move {
439                        let text: String = Self::clipboard_read_text().await;
440                        result.set(format!("Pasted: {text}"));
441                    });
442                }
443                _ => {
444                    result.set(BROWSER_CLIPBOARD_UNAVAILABLE.to_string());
445                }
446            }
447        }))
448    }
449
450    /// Creates a click event handler that refreshes the window size display.
451    ///
452    /// # Returns
453    ///
454    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler to refresh the window size.
455    pub fn on_window_refresh_size(self) -> Option<Rc<dyn Fn(Event)>> {
456        Some(Rc::new(move |_: Event| {
457            let (width, height): (i32, i32) = Self::window_inner_size();
458            self.get_window_size().set(format!("{width} x {height}"));
459        }))
460    }
461
462    /// Creates a click event handler that logs a console message.
463    ///
464    /// # Arguments
465    ///
466    /// - `Signal<String>` - The console input signal.
467    ///
468    /// # Returns
469    ///
470    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler for console.log.
471    pub fn on_console_log(console_input: Signal<String>) -> Option<Rc<dyn Fn(Event)>> {
472        Some(Rc::new(move |_: Event| {
473            let raw: String = console_input.get();
474            let message: &str = if raw.is_empty() {
475                CONSOLE_LOG_DEFAULT_MESSAGE
476            } else {
477                &raw
478            };
479            Console::log(message);
480        }))
481    }
482
483    /// Creates a click event handler that warns a console message.
484    ///
485    /// # Arguments
486    ///
487    /// - `Signal<String>` - The console input signal.
488    ///
489    /// # Returns
490    ///
491    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler for console.warn.
492    pub fn on_console_warn(console_input: Signal<String>) -> Option<Rc<dyn Fn(Event)>> {
493        Some(Rc::new(move |_: Event| {
494            let raw: String = console_input.get();
495            let message: &str = if raw.is_empty() {
496                CONSOLE_WARN_DEFAULT_MESSAGE
497            } else {
498                &raw
499            };
500            Console::warn(message);
501        }))
502    }
503
504    /// Creates a click event handler that errors a console message.
505    ///
506    /// # Arguments
507    ///
508    /// - `Signal<String>` - The console input signal.
509    ///
510    /// # Returns
511    ///
512    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler for console.error.
513    pub fn on_console_error(console_input: Signal<String>) -> Option<Rc<dyn Fn(Event)>> {
514        Some(Rc::new(move |_: Event| {
515            let raw: String = console_input.get();
516            let message: &str = if raw.is_empty() {
517                CONSOLE_ERROR_DEFAULT_MESSAGE
518            } else {
519                &raw
520            };
521            Console::error(message);
522        }))
523    }
524}
525
526/// Default implementation for `UseEuvBrowser`.
527impl Default for UseEuvBrowser {
528    /// Constructs a default [`UseEuvBrowser`] value.
529    fn default() -> Self {
530        let window_size_val: String = {
531            let (width, height): (i32, i32) = UseEuvBrowser::window_inner_size();
532            format!("{width} x {height}")
533        };
534        UseEuvBrowser {
535            local_key: App::use_signal(String::new),
536            local_value: App::use_signal(String::new),
537            local_result: App::use_signal(String::new),
538            session_key: App::use_signal(String::new),
539            session_value: App::use_signal(String::new),
540            session_result: App::use_signal(String::new),
541            clipboard_text: App::use_signal(String::new),
542            clipboard_result: App::use_signal(String::new),
543            window_size: App::use_signal(move || window_size_val.clone()),
544            user_agent: App::use_signal(UseEuvBrowser::navigator_user_agent),
545            language: App::use_signal(UseEuvBrowser::navigator_language),
546            location_url: App::use_signal(UseEuvBrowser::location_href),
547            location_origin_val: App::use_signal(UseEuvBrowser::location_origin),
548            location_pathname_val: App::use_signal(UseEuvBrowser::location_pathname),
549            console_input: App::use_signal(String::new),
550        }
551    }
552}