1//! Provide a wrapper for commonly-used, but verbose `web_sys` features.
2//! This module is decoupled / independent.
34// @TODO refactor (ideally once `Unsized` and `Specialization` are stable)
56use std::borrow::Cow;
7use wasm_bindgen::closure::Closure;
8use wasm_bindgen::JsCast;
910pub use gloo_utils::{document, history, window};
1112#[deprecated(
13 since = "0.8.0",
14 note = "see [`request_animation_frame`](fn.request_animation_frame.html)"
15)]
16pub type RequestAnimationFrameTime = f64;
1718#[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}
2728impl Drop for RequestAnimationFrameHandle {
29fn drop(&mut self) {
30 window()
31 .cancel_animation_frame(self.request_id)
32 .expect("Problem cancelling animation frame request");
33 }
34}
3536/// 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}
4041/// 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}
5253/// 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}
6263#[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 {
71let request_id = window()
72 .request_animation_frame(f.as_ref().unchecked_ref())
73 .expect("Problem requesting animation frame");
7475 RequestAnimationFrameHandle {
76 request_id,
77 _closure: f,
78 }
79}
8081/// 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> {
88use web_sys::*;
8990macro_rules! get {
91 ($element:ty) => {
92get!($element, |_| Ok(()))
93 };
94 ($element:ty, $result_callback:expr) => {
95if let Some(input) = target.dyn_ref::<$element>() {
96return $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
103104get!(HtmlInputElement, |input: &HtmlInputElement| {
105// https://www.w3schools.com/tags/att_input_value.asp
106match input.type_().as_str() {
107"file" => Err(r#"The value attribute cannot be used with <input type="file">."#),
108_ => Ok(()),
109 }
110 });
111get!(HtmlTextAreaElement);
112get!(HtmlSelectElement);
113get!(HtmlProgressElement);
114get!(HtmlOptionElement);
115get!(HtmlButtonElement);
116get!(HtmlDataElement);
117get!(HtmlMeterElement);
118get!(HtmlLiElement);
119get!(HtmlOutputElement);
120get!(HtmlParamElement);
121122Err("Can't use function `get_value` for given element.")
123}
124125#[allow(clippy::missing_errors_doc)]
126/// Similar to `get_value`.
127pub fn set_value(target: &web_sys::EventTarget, value: &str) -> Result<(), Cow<'static, str>> {
128use web_sys::*;
129130macro_rules! set {
131 ($element:ty) => {
132set!($element, |_| Ok(value))
133 };
134 ($element:ty, $value_result_callback:expr) => {
135if let Some(input) = target.dyn_ref::<$element>() {
136return $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
143144if let Some(input) = target.dyn_ref::<HtmlInputElement>() {
145return set_html_input_element_value(input, value);
146 }
147set!(HtmlTextAreaElement);
148set!(HtmlSelectElement);
149set!(HtmlProgressElement, |_| value.parse().map_err(|error| {
150 Cow::from(format!(
151"Can't parse value to `f64` for `HtmlProgressElement`. Error: {error:?}",
152 ))
153 }));
154set!(HtmlOptionElement);
155set!(HtmlButtonElement);
156set!(HtmlDataElement);
157set!(HtmlMeterElement, |_| value.parse().map_err(|error| {
158 Cow::from(format!(
159"Can't parse value to `f64` for `HtmlMeterElement`. Error: {error:?}"
160))
161 }));
162set!(HtmlLiElement, |_| value.parse().map_err(|error| {
163 Cow::from(format!(
164"Can't parse value to `i32` for `HtmlLiElement`. Error: {error:?}"
165))
166 }));
167set!(HtmlOutputElement);
168set!(HtmlParamElement);
169170Err(Cow::from(
171"Can't use function `set_value` for given element.",
172 ))
173}
174175fn 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
180if value == input.value() {
181return Ok(());
182 }
183184// In some cases we need to set selection manually because
185 // otherwise the cursor would jump at the end on some platforms.
186187 // `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
194let selection_update_required = match input.type_().as_str() {
195// https://www.w3schools.com/tags/att_input_value.asp
196"file" => {
197return Err(Cow::from(
198r#"The value attribute cannot be used with <input type="file">."#,
199 ))
200 }
201"text" | "password" | "search" | "tel" | "url" | "week" | "month" => true,
202_ => false,
203 };
204205// We don't want to set selection in inactive input because
206 // that input would "steal" focus from the active element on some platforms.
207if selection_update_required && is_active(input) {
208let selection_start = input
209 .selection_start()
210 .expect("get `HtmlInputElement` selection start");
211let selection_end = input
212 .selection_end()
213 .expect("get `HtmlInputElement` selection end");
214215 input.set_value(value);
216217 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 }
226227Ok(())
228}
229230/// Return true if passed element is active.
231fn is_active(element: &web_sys::Element) -> bool {
232 document().active_element().as_ref() == Some(element)
233}
234235#[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>> {
239if let Some(input) = target.dyn_ref::<web_sys::HtmlInputElement>() {
240// https://www.w3schools.com/tags/att_input_checked.asp
241return match input.type_().as_str() {
242"file" => Err(Cow::from(
243r#"The checked attribute can be used with <input type="checkbox"> and <input type="radio">."#,
244 )),
245_ => Ok(input.checked()),
246 };
247 }
248if let Some(input) = target.dyn_ref::<web_sys::HtmlMenuItemElement>() {
249return Ok(input.checked());
250 }
251Err(Cow::from(
252"Only `HtmlInputElement` and `HtmlMenuItemElement` can be used in function `get_checked`.",
253 ))
254}
255256#[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>> {
260if let Some(input) = target.dyn_ref::<web_sys::HtmlInputElement>() {
261// https://www.w3schools.com/tags/att_input_checked.asp
262return match input.type_().as_str() {
263"file" => Err(Cow::from(
264r#"The checked attribute can be used with <input type="checkbox"> and <input type="radio">."#,
265 )),
266_ => Ok(input.set_checked(value)),
267 };
268 }
269if let Some(input) = target.dyn_ref::<web_sys::HtmlMenuItemElement>() {
270return Ok(input.set_checked(value));
271 }
272Err(Cow::from(
273"Only `HtmlInputElement` and `HtmlMenuItemElement` can be used in function `set_checked`.",
274 ))
275}
276277/// 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}
284285/// 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}