seed/browser/
util.rs

1//! Provide a wrapper for commonly-used, but verbose `web_sys` features.
2//! This module is decoupled / independent.
3
4// @TODO refactor (ideally once `Unsized` and `Specialization` are stable)
5
6use std::borrow::Cow;
7use wasm_bindgen::closure::Closure;
8use wasm_bindgen::JsCast;
9
10pub use gloo_utils::{document, history, window};
11
12#[deprecated(
13    since = "0.8.0",
14    note = "see [`request_animation_frame`](fn.request_animation_frame.html)"
15)]
16pub type RequestAnimationFrameTime = f64;
17
18#[must_use]
19#[deprecated(
20    since = "0.8.0",
21    note = "see [`request_animation_frame`](fn.request_animation_frame.html)"
22)]
23pub struct RequestAnimationFrameHandle {
24    request_id: i32,
25    _closure: Closure<dyn FnMut(RequestAnimationFrameTime)>,
26}
27
28impl Drop for RequestAnimationFrameHandle {
29    fn drop(&mut self) {
30        window()
31            .cancel_animation_frame(self.request_id)
32            .expect("Problem cancelling animation frame request");
33    }
34}
35
36/// Convenience function to access the `web_sys` DOM body.
37pub fn body() -> web_sys::HtmlElement {
38    document().body().expect("Can't find the document's body")
39}
40
41/// Convenience function to access the `web_sys::HtmlDocument`.
42pub fn html_document() -> web_sys::HtmlDocument {
43    wasm_bindgen::JsValue::from(document()).unchecked_into::<web_sys::HtmlDocument>()
44}
45/// Convenience function to access the `web_sys::HtmlCanvasElement`.
46/// /// _Note:_ Returns `None` if there is no element with the given `id` or the element isn't `HtmlCanvasElement`.
47pub fn canvas(id: &str) -> Option<web_sys::HtmlCanvasElement> {
48    document()
49        .get_element_by_id(id)
50        .and_then(|element| element.dyn_into::<web_sys::HtmlCanvasElement>().ok())
51}
52
53/// Convenience function to access the `web_sys::CanvasRenderingContext2d`.
54pub fn canvas_context_2d(canvas: &web_sys::HtmlCanvasElement) -> web_sys::CanvasRenderingContext2d {
55    canvas
56        .get_context("2d")
57        .expect("Problem getting canvas context")
58        .expect("The canvas context is empty")
59        .dyn_into::<web_sys::CanvasRenderingContext2d>()
60        .expect("Problem casting as web_sys::CanvasRenderingContext2d")
61}
62
63#[deprecated(
64    since = "0.8.0",
65    note = "use [`Orders::after_next_render`](../../app/orders/trait.Orders.html#method.after_next_render) instead"
66)]
67/// Request the animation frame.
68pub fn request_animation_frame(
69    f: Closure<dyn FnMut(RequestAnimationFrameTime)>,
70) -> RequestAnimationFrameHandle {
71    let request_id = window()
72        .request_animation_frame(f.as_ref().unchecked_ref())
73        .expect("Problem requesting animation frame");
74
75    RequestAnimationFrameHandle {
76        request_id,
77        _closure: f,
78    }
79}
80
81/// Simplify getting the value of input elements; required due to the need to cast
82/// from general nodes/elements to `HTML_Elements`.
83///
84/// # Errors
85///
86/// Will return error if it's not possible to call `get_value` for given `target`.
87pub fn get_value(target: &web_sys::EventTarget) -> Result<String, &'static str> {
88    use web_sys::*;
89
90    macro_rules! get {
91        ($element:ty) => {
92            get!($element, |_| Ok(()))
93        };
94        ($element:ty, $result_callback:expr) => {
95            if let Some(input) = target.dyn_ref::<$element>() {
96                return $result_callback(input).map(|_| input.value().to_string());
97            }
98        };
99    }
100    // List of elements
101    // https://docs.rs/web-sys/0.3.25/web_sys/struct.HtmlMenuItemElement.html?search=value
102    // They should be ordered by expected frequency of use
103
104    get!(HtmlInputElement, |input: &HtmlInputElement| {
105        // https://www.w3schools.com/tags/att_input_value.asp
106        match input.type_().as_str() {
107            "file" => Err(r#"The value attribute cannot be used with <input type="file">."#),
108            _ => Ok(()),
109        }
110    });
111    get!(HtmlTextAreaElement);
112    get!(HtmlSelectElement);
113    get!(HtmlProgressElement);
114    get!(HtmlOptionElement);
115    get!(HtmlButtonElement);
116    get!(HtmlDataElement);
117    get!(HtmlMeterElement);
118    get!(HtmlLiElement);
119    get!(HtmlOutputElement);
120    get!(HtmlParamElement);
121
122    Err("Can't use function `get_value` for given element.")
123}
124
125#[allow(clippy::missing_errors_doc)]
126/// Similar to `get_value`.
127pub fn set_value(target: &web_sys::EventTarget, value: &str) -> Result<(), Cow<'static, str>> {
128    use web_sys::*;
129
130    macro_rules! set {
131        ($element:ty) => {
132            set!($element, |_| Ok(value))
133        };
134        ($element:ty, $value_result_callback:expr) => {
135            if let Some(input) = target.dyn_ref::<$element>() {
136                return $value_result_callback(input).map(|value| input.set_value(value));
137            }
138        };
139    }
140    // List of elements
141    // https://docs.rs/web-sys/0.3.25/web_sys/struct.HtmlMenuItemElement.html?search=set_value
142    // They should be ordered by expected frequency of use
143
144    if let Some(input) = target.dyn_ref::<HtmlInputElement>() {
145        return set_html_input_element_value(input, value);
146    }
147    set!(HtmlTextAreaElement);
148    set!(HtmlSelectElement);
149    set!(HtmlProgressElement, |_| value.parse().map_err(|error| {
150        Cow::from(format!(
151            "Can't parse value to `f64` for `HtmlProgressElement`. Error: {error:?}",
152        ))
153    }));
154    set!(HtmlOptionElement);
155    set!(HtmlButtonElement);
156    set!(HtmlDataElement);
157    set!(HtmlMeterElement, |_| value.parse().map_err(|error| {
158        Cow::from(format!(
159            "Can't parse value to `f64` for `HtmlMeterElement`. Error: {error:?}"
160        ))
161    }));
162    set!(HtmlLiElement, |_| value.parse().map_err(|error| {
163        Cow::from(format!(
164            "Can't parse value to `i32` for `HtmlLiElement`. Error: {error:?}"
165        ))
166    }));
167    set!(HtmlOutputElement);
168    set!(HtmlParamElement);
169
170    Err(Cow::from(
171        "Can't use function `set_value` for given element.",
172    ))
173}
174
175fn set_html_input_element_value(
176    input: &web_sys::HtmlInputElement,
177    value: &str,
178) -> Result<(), Cow<'static, str>> {
179    // Don't update if value hasn't changed
180    if value == input.value() {
181        return Ok(());
182    }
183
184    // In some cases we need to set selection manually because
185    // otherwise the cursor would jump at the end on some platforms.
186
187    // `selectionStart` and `selectionEnd`
188    // - "If this element is an input element, and selectionStart does not apply to this element, return null."
189    //   - https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart
190    // - => return values if the element type is:
191    //   -  `text`, `search`, `url`, `tel`, `password` and probably also `week`, `month`
192    //   - https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement
193    //   - https://html.spec.whatwg.org/multipage/input.html#do-not-apply
194    let selection_update_required = match input.type_().as_str() {
195        // https://www.w3schools.com/tags/att_input_value.asp
196        "file" => {
197            return Err(Cow::from(
198                r#"The value attribute cannot be used with <input type="file">."#,
199            ))
200        }
201        "text" | "password" | "search" | "tel" | "url" | "week" | "month" => true,
202        _ => false,
203    };
204
205    // We don't want to set selection in inactive input because
206    // that input would "steal" focus from the active element on some platforms.
207    if selection_update_required && is_active(input) {
208        let selection_start = input
209            .selection_start()
210            .expect("get `HtmlInputElement` selection start");
211        let selection_end = input
212            .selection_end()
213            .expect("get `HtmlInputElement` selection end");
214
215        input.set_value(value);
216
217        input
218            .set_selection_start(selection_start)
219            .expect("set `HtmlInputElement` selection start");
220        input
221            .set_selection_end(selection_end)
222            .expect("set `HtmlInputElement` selection end");
223    } else {
224        input.set_value(value);
225    }
226
227    Ok(())
228}
229
230/// Return true if passed element is active.
231fn is_active(element: &web_sys::Element) -> bool {
232    document().active_element().as_ref() == Some(element)
233}
234
235#[allow(clippy::missing_errors_doc)]
236/// Similar to `get_value`
237#[allow(dead_code)]
238pub fn get_checked(target: &web_sys::EventTarget) -> Result<bool, Cow<str>> {
239    if let Some(input) = target.dyn_ref::<web_sys::HtmlInputElement>() {
240        // https://www.w3schools.com/tags/att_input_checked.asp
241        return match input.type_().as_str() {
242            "file" => Err(Cow::from(
243                r#"The checked attribute can be used with <input type="checkbox"> and <input type="radio">."#,
244            )),
245            _ => Ok(input.checked()),
246        };
247    }
248    if let Some(input) = target.dyn_ref::<web_sys::HtmlMenuItemElement>() {
249        return Ok(input.checked());
250    }
251    Err(Cow::from(
252        "Only `HtmlInputElement` and `HtmlMenuItemElement` can be used in function `get_checked`.",
253    ))
254}
255
256#[allow(clippy::missing_errors_doc)]
257/// Similar to `set_value`.
258#[allow(clippy::unit_arg)]
259pub fn set_checked(target: &web_sys::EventTarget, value: bool) -> Result<(), Cow<str>> {
260    if let Some(input) = target.dyn_ref::<web_sys::HtmlInputElement>() {
261        // https://www.w3schools.com/tags/att_input_checked.asp
262        return match input.type_().as_str() {
263            "file" => Err(Cow::from(
264                r#"The checked attribute can be used with <input type="checkbox"> and <input type="radio">."#,
265            )),
266            _ => Ok(input.set_checked(value)),
267        };
268    }
269    if let Some(input) = target.dyn_ref::<web_sys::HtmlMenuItemElement>() {
270        return Ok(input.set_checked(value));
271    }
272    Err(Cow::from(
273        "Only `HtmlInputElement` and `HtmlMenuItemElement` can be used in function `set_checked`.",
274    ))
275}
276
277/// Convenience function for logging to the web browser's console.  See also
278/// the log! macro, which is more flexible.
279#[deprecated(note = "Use something like https://crates.io/crates/gloo-console instead")]
280pub fn log<T: std::fmt::Debug>(object: T) -> T {
281    web_sys::console::log_1(&format!("{:#?}", &object).into());
282    object
283}
284
285/// Similar to log, but for errors.
286#[deprecated(note = "Use something like https://crates.io/crates/gloo-console instead")]
287pub fn error<T: std::fmt::Debug>(object: T) -> T {
288    web_sys::console::error_1(&format!("{:#?}", &object).into());
289    object
290}