sauron_core/dom/
dom_attr.rs

1use crate::vdom::AttributeName;
2use crate::vdom::Namespace;
3use crate::vdom::Style;
4use crate::vdom::Value;
5#[cfg(feature = "ensure-attr-set")]
6use crate::vdom::{CHECKED, DISABLED, OPEN, VALUE};
7use wasm_bindgen::intern;
8#[cfg(feature = "ensure-attr-set")]
9use wasm_bindgen::JsCast;
10use wasm_bindgen::{closure::Closure, JsValue};
11use web_sys;
12use web_sys::Element;
13#[cfg(feature = "ensure-attr-set")]
14use web_sys::{
15    HtmlButtonElement, HtmlDataElement, HtmlDetailsElement, HtmlFieldSetElement, HtmlInputElement,
16    HtmlLiElement, HtmlLinkElement, HtmlMeterElement, HtmlOptGroupElement, HtmlOptionElement,
17    HtmlOutputElement, HtmlParamElement, HtmlProgressElement, HtmlSelectElement, HtmlStyleElement,
18    HtmlTextAreaElement,
19};
20
21/// a dom version of the Attribute, thereby removing the MSG generic
22#[derive(Debug)]
23pub struct DomAttr {
24    /// namespace of the attribute
25    pub namespace: Option<&'static str>,
26    /// the name of the attribute
27    pub name: &'static str,
28    /// the value of the attribute
29    pub value: Vec<DomAttrValue>,
30}
31
32/// a dom version of the Attribute value, thereby removing the MSG generic
33#[derive(Debug)]
34pub enum DomAttrValue {
35    /// simple value
36    Simple(Value),
37    /// a style
38    Style(Vec<Style>),
39    /// event listeners
40    EventListener(Closure<dyn FnMut(web_sys::Event)>),
41    /// an empty value, can also represents null values from JsValue
42    Empty,
43}
44
45/// a struct where the listeners, plain values, styles and function call values are grouped
46/// separately
47pub struct GroupedDomAttrValues {
48    /// the listeners of the event listeners
49    pub listeners: Vec<Closure<dyn FnMut(web_sys::Event)>>,
50    /// plain attribute values
51    pub plain_values: Vec<Value>,
52    /// style attribute values
53    pub styles: Vec<Style>,
54}
55
56impl DomAttr {
57    /// return the values grouped into listeners, plain, styles and function calls
58    pub(crate) fn group_values(self) -> GroupedDomAttrValues {
59        let mut listeners = vec![];
60        let mut plain_values = vec![];
61        let mut styles = vec![];
62        for av in self.value {
63            match av {
64                DomAttrValue::Simple(v) => {
65                    plain_values.push(v);
66                }
67                DomAttrValue::Style(s) => {
68                    styles.extend(s);
69                }
70                DomAttrValue::EventListener(cb) => {
71                    listeners.push(cb);
72                }
73                DomAttrValue::Empty => (),
74            }
75        }
76        GroupedDomAttrValues {
77            listeners,
78            plain_values,
79            styles,
80        }
81    }
82
83    /// set the style of this element
84    pub(crate) fn set_element_style(
85        element: &Element,
86        attr_name: AttributeName,
87        styles: Vec<Style>,
88    ) {
89        if let Some(merged_styles) = Style::merge_to_string(&styles) {
90            // set the styles
91            element
92                .set_attribute(attr_name, &merged_styles)
93                .unwrap_or_else(|_| panic!("Error setting an attribute_ns for {element:?}"));
94        } else {
95            //if the merged attribute is blank of empty when string is trimmed
96            //remove the attribute
97            element
98                .remove_attribute(attr_name)
99                .expect("must remove attribute");
100        }
101    }
102
103    /// set simple values
104    pub(crate) fn set_element_simple_values(
105        element: &Element,
106        attr_name: AttributeName,
107        attr_namespace: Option<Namespace>,
108        plain_values: Vec<Value>,
109    ) {
110        if let Some(merged_plain_values) = Value::merge_to_string(plain_values.iter()) {
111            if let Some(namespace) = attr_namespace {
112                // Warning NOTE: set_attribute_ns should only be called
113                // when you meant to use a namespace
114                // using this with None will error in the browser with:
115                // NamespaceError: An attempt was made to create or change an object in a way which is incorrect with regard to namespaces
116                element
117                    .set_attribute_ns(Some(namespace), attr_name, &merged_plain_values)
118                    .unwrap_or_else(|_| panic!("Error setting an attribute_ns for {element:?}"));
119            } else {
120                #[cfg(feature = "ensure-attr-set")]
121                if *VALUE == attr_name {
122                    element
123                        .set_attribute(attr_name, &merged_plain_values)
124                        .unwrap_or_else(|_| panic!("Error setting an attribute for {element:?}"));
125                    Self::set_value_str(element, &merged_plain_values);
126                    Self::set_numeric_values(element, &plain_values);
127                } else if *OPEN == attr_name {
128                    let is_open: bool = plain_values
129                        .first()
130                        .and_then(|v| v.as_bool())
131                        .unwrap_or(false);
132
133                    element
134                        .set_attribute(attr_name, &is_open.to_string())
135                        .unwrap_or_else(|_| panic!("Error setting an attribute for {element:?}"));
136                    Self::set_open(element, is_open);
137                } else if *CHECKED == attr_name {
138                    let is_checked: bool = plain_values
139                        .first()
140                        .and_then(|v| v.as_bool())
141                        .unwrap_or(false);
142
143                    element
144                        .set_attribute(attr_name, &is_checked.to_string())
145                        .unwrap_or_else(|_| panic!("Error setting an attribute for {element:?}"));
146                    Self::set_checked(element, is_checked)
147                } else if *DISABLED == attr_name {
148                    let is_disabled: bool = plain_values
149                        .first()
150                        .and_then(|v| v.as_bool())
151                        .unwrap_or(false);
152
153                    element
154                        .set_attribute(attr_name, &is_disabled.to_string())
155                        .unwrap_or_else(|_| panic!("Error setting an attribute for {element:?}"));
156                    Self::set_disabled(element, is_disabled);
157                } else if "inner_html" == attr_name {
158                    panic!("Setting inner_html is not allowed, as it breaks the tracking of the DomTree, use html-parse instead")
159                } else {
160                    element
161                        .set_attribute(attr_name, &merged_plain_values)
162                        .unwrap_or_else(|_| panic!("Error setting an attribute for {element:?}"));
163                }
164                #[cfg(not(feature = "ensure-attr-set"))]
165                element
166                    .set_attribute(attr_name, &merged_plain_values)
167                    .unwrap_or_else(|_| panic!("Error setting an attribute for {element:?}"));
168            }
169        }
170    }
171
172    /// remove the elemnt dom attr
173    pub(crate) fn remove_element_dom_attr(
174        element: &Element,
175        attr: &DomAttr,
176    ) -> Result<(), JsValue> {
177        #[cfg(feature = "ensure-attr-set")]
178        if *VALUE == attr.name {
179            DomAttr::set_value_str(element, "");
180        } else if *OPEN == attr.name {
181            DomAttr::set_open(element, false);
182        } else if *CHECKED == attr.name {
183            DomAttr::set_checked(element, false);
184        } else if *DISABLED == attr.name {
185            DomAttr::set_disabled(element, false);
186        }
187        //actually remove the element
188        element.remove_attribute(intern(attr.name))?;
189
190        Ok(())
191    }
192
193    /// explicitly call `set_checked` function on the html element
194    /// since setting the attribute to false will not unchecked it.
195    ///
196    /// There are only 2 elements where set_checked is applicable:
197    /// - input
198    /// - menuitem
199    #[cfg(feature = "ensure-attr-set")]
200    pub(crate) fn set_checked(element: &Element, is_checked: bool) {
201        if let Some(input) = element.dyn_ref::<HtmlInputElement>() {
202            input.set_checked(is_checked);
203        }
204    }
205
206    /// explicitly call set_open for details
207    /// since setting the attribute `open` to false will not close it.
208    ///
209    /// TODO: HtmlDialogElement ( but it is not supported on firefox and in safari, only works on chrome)
210    ///
211    /// Applies to:
212    ///  - dialog
213    ///  - details
214    #[cfg(feature = "ensure-attr-set")]
215    pub(crate) fn set_open(element: &Element, is_open: bool) {
216        if let Some(details) = element.dyn_ref::<HtmlDetailsElement>() {
217            details.set_open(is_open);
218        }
219    }
220
221    /// explicitly call on `set_disabled`
222    /// since setting the attribute `disabled` false will not enable it.
223    ///
224    /// These are 10 elements that we can call `set_disabled` function to.
225    /// - input
226    /// - button
227    /// - textarea
228    /// - style
229    /// - link
230    /// - select
231    /// - option
232    /// - optgroup
233    /// - fieldset
234    /// - menuitem
235    ///
236    /// TODO: use macro to simplify this code
237    #[cfg(feature = "ensure-attr-set")]
238    pub(crate) fn set_disabled(element: &Element, is_disabled: bool) {
239        if let Some(elm) = element.dyn_ref::<HtmlInputElement>() {
240            elm.set_disabled(is_disabled);
241        } else if let Some(elm) = element.dyn_ref::<HtmlButtonElement>() {
242            elm.set_disabled(is_disabled);
243        } else if let Some(elm) = element.dyn_ref::<HtmlTextAreaElement>() {
244            elm.set_disabled(is_disabled);
245        } else if let Some(elm) = element.dyn_ref::<HtmlStyleElement>() {
246            elm.set_disabled(is_disabled);
247        } else if let Some(elm) = element.dyn_ref::<HtmlLinkElement>() {
248            elm.set_disabled(is_disabled);
249        } else if let Some(elm) = element.dyn_ref::<HtmlSelectElement>() {
250            elm.set_disabled(is_disabled);
251        } else if let Some(elm) = element.dyn_ref::<HtmlOptionElement>() {
252            elm.set_disabled(is_disabled);
253        } else if let Some(elm) = element.dyn_ref::<HtmlOptGroupElement>() {
254            elm.set_disabled(is_disabled);
255        } else if let Some(elm) = element.dyn_ref::<HtmlFieldSetElement>() {
256            elm.set_disabled(is_disabled);
257        }
258    }
259
260    /// we explicitly call the `set_value` function in the html element
261    ///
262    /// TODO: use macro to simplify this code
263    #[cfg(feature = "ensure-attr-set")]
264    pub(crate) fn set_value_str(element: &Element, value: &str) {
265        if let Some(elm) = element.dyn_ref::<HtmlInputElement>() {
266            elm.set_value(value);
267        } else if let Some(elm) = element.dyn_ref::<HtmlTextAreaElement>() {
268            elm.set_value(value);
269        } else if let Some(elm) = element.dyn_ref::<HtmlSelectElement>() {
270            elm.set_value(value);
271        } else if let Some(elm) = element.dyn_ref::<HtmlOptionElement>() {
272            elm.set_value(value);
273        } else if let Some(elm) = element.dyn_ref::<HtmlButtonElement>() {
274            elm.set_value(value);
275        } else if let Some(elm) = element.dyn_ref::<HtmlDataElement>() {
276            elm.set_value(value);
277        } else if let Some(elm) = element.dyn_ref::<HtmlOutputElement>() {
278            elm.set_value(value);
279        } else if let Some(elm) = element.dyn_ref::<HtmlParamElement>() {
280            elm.set_value(value);
281        }
282    }
283
284    /// set the value of this element with an i32 value
285    #[cfg(feature = "ensure-attr-set")]
286    pub(crate) fn set_value_i32(element: &Element, value: i32) {
287        if let Some(elm) = element.dyn_ref::<HtmlLiElement>() {
288            elm.set_value(value);
289        }
290    }
291
292    /// set the value of this element with an f64 value
293    #[cfg(feature = "ensure-attr-set")]
294    pub(crate) fn set_value_f64(element: &Element, value: f64) {
295        if let Some(elm) = element.dyn_ref::<HtmlMeterElement>() {
296            elm.set_value(value);
297        } else if let Some(elm) = element.dyn_ref::<HtmlProgressElement>() {
298            elm.set_value(value);
299        }
300    }
301
302    /// set the element attribute value with the first numerical value found in values
303    #[cfg(feature = "ensure-attr-set")]
304    pub(crate) fn set_numeric_values(element: &Element, values: &[Value]) {
305        let value_i32 = values.first().and_then(|v| v.as_i32());
306
307        let value_f64 = values.first().and_then(|v| v.as_f64());
308
309        if let Some(value_i32) = value_i32 {
310            Self::set_value_i32(element, value_i32);
311        }
312        if let Some(value_f64) = value_f64 {
313            Self::set_value_f64(element, value_f64);
314        }
315    }
316}
317
318impl DomAttrValue {
319    /// return the value if it is a Simple variant
320    pub fn as_simple(&self) -> Option<&Value> {
321        match self {
322            Self::Simple(v) => Some(v),
323            _ => None,
324        }
325    }
326
327    /// make a string representation of this value if it is a simple value
328    pub fn as_string(&self) -> Option<String> {
329        let simple = self.as_simple()?;
330        Some(simple.to_string())
331    }
332
333    /// return i32 if it can be converted as such
334    pub fn as_i32(&self) -> Option<i32> {
335        let simple = self.as_simple()?;
336        simple.as_i32()
337    }
338
339    /// return i64 if it can be converted as such
340    pub fn as_i64(&self) -> Option<i64> {
341        let simple = self.as_simple()?;
342        simple.as_i64()
343    }
344
345    /// return i32 if it can be converted as such
346    pub fn as_f32(&self) -> Option<f32> {
347        let simple = self.as_simple()?;
348        simple.as_f32()
349    }
350
351    /// return i64 if it can be converted as such
352    pub fn as_f64(&self) -> Option<f64> {
353        let simple = self.as_simple()?;
354        simple.as_f64()
355    }
356}