seed/browser/dom/
virtual_dom_bridge.rs

1//! This file contains interactions with `web_sys`.
2
3use super::Namespace;
4use crate::virtual_dom::{At, AtValue, Attrs, El, Mailbox, Node, Style, Text};
5use std::borrow::Cow;
6use std::cmp::Ordering;
7use wasm_bindgen::JsCast;
8use web_sys::Document;
9
10/// Convenience function to reduce repetition
11fn set_style(el_ws: &web_sys::Node, style: &Style) {
12    el_ws
13        .dyn_ref::<web_sys::Element>()
14        .expect("Problem casting Node as Element while setting style")
15        .set_attribute("style", &style.to_string())
16        .expect("Problem setting style");
17}
18
19pub(crate) fn assign_ws_nodes_to_el<Ms>(document: &Document, el: &mut El<Ms>) {
20    let node_ws = make_websys_el(el, document);
21    el.node_ws = Some(node_ws);
22    for child in &mut el.children {
23        assign_ws_nodes(document, child);
24    }
25}
26pub(crate) fn assign_ws_nodes_to_text(document: &Document, text: &mut Text) {
27    text.node_ws = Some(
28        document
29            .create_text_node(&text.text)
30            .dyn_into::<web_sys::Node>()
31            .expect("Problem casting Text as Node."),
32    );
33}
34/// Recursively create `web_sys::Node`s, and place them in the vdom Nodes' fields.
35pub(crate) fn assign_ws_nodes<Ms>(document: &Document, node: &mut Node<Ms>) {
36    match node {
37        Node::Element(el) => assign_ws_nodes_to_el(document, el),
38        Node::Text(text) => assign_ws_nodes_to_text(document, text),
39        Node::Empty | Node::NoChange => (),
40    }
41}
42
43fn node_to_element(el_ws: &web_sys::Node) -> Result<&web_sys::Element, Cow<str>> {
44    if el_ws.node_type() == web_sys::Node::ELEMENT_NODE {
45        el_ws
46            .dyn_ref::<web_sys::Element>()
47            .ok_or_else(|| Cow::from("Problem casting Node as Element"))
48    } else {
49        Err(Cow::from("Node isn't Element!"))
50    }
51}
52
53fn set_attr_value(el_ws: &web_sys::Node, at: &At, at_value: &AtValue) {
54    match at_value {
55        AtValue::Some(value) => {
56            node_to_element(el_ws)
57                .and_then(|element| {
58                    element.set_attribute(at.as_str(), value).map_err(|error| {
59                        Cow::from(format!("Problem setting an attribute: {error:?}"))
60                    })
61                })
62                .unwrap_or_else(|err| {
63                    crate::error(err);
64                });
65        }
66        AtValue::None => {
67            node_to_element(el_ws)
68                .and_then(|element| {
69                    element.set_attribute(at.as_str(), "").map_err(|error| {
70                        Cow::from(format!("Problem setting an attribute: {error:?}"))
71                    })
72                })
73                .unwrap_or_else(|err| {
74                    crate::error(err);
75                });
76        }
77        AtValue::Ignored => {
78            node_to_element(el_ws)
79                .and_then(|element| {
80                    element.remove_attribute(at.as_str()).map_err(|error| {
81                        Cow::from(format!("Problem removing an attribute: {error:?}"))
82                    })
83                })
84                .unwrap_or_else(|err| {
85                    crate::error(err);
86                });
87        }
88    }
89}
90
91/// Create and return a `web_sys` Element from our virtual-dom `El`. The `web_sys`
92/// Element is a close analog to JS/DOM elements.
93///
94/// # References
95/// * [`web_sys` Element](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Element.html)
96/// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)
97/// * See also: [`web_sys` Node](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Node.html)
98pub(crate) fn make_websys_el<Ms>(el: &mut El<Ms>, document: &web_sys::Document) -> web_sys::Node {
99    let tag = el.tag.as_str();
100
101    let el_ws = el.namespace.as_ref().map_or_else(
102        || {
103            document
104                .create_element(tag)
105                .expect("Problem creating web-sys element")
106        },
107        |ns| {
108            document
109                .create_element_ns(Some(ns.as_str()), tag)
110                .expect("Problem creating web-sys element with namespace")
111        },
112    );
113
114    fix_attrs_order(&mut el.attrs);
115    for (at, attr_value) in &el.attrs.vals {
116        set_attr_value(&el_ws, at, attr_value);
117    }
118    if let Some(ns) = &el.namespace {
119        el_ws
120            .dyn_ref::<web_sys::Element>()
121            .expect("Problem casting Node as Element while setting an attribute")
122            .set_attribute("xmlns", ns.as_str())
123            .expect("Problem setting xlmns attribute");
124    }
125
126    // Style is just an attribute in the actual Dom, but is handled specially in our vdom;
127    // merge the different parts of style here.
128    if el.style.vals.keys().len() > 0 {
129        set_style(&el_ws, &el.style);
130    }
131
132    el_ws.into()
133}
134
135/// Similar to `attach_el_and_children`, but for text nodes
136pub fn attach_text_node(text: &mut Text, parent: &web_sys::Node) {
137    let node_ws = text.node_ws.take().expect("Missing websys node for Text");
138    parent
139        .append_child(&node_ws)
140        .expect("Problem appending text node");
141    text.node_ws.replace(node_ws);
142}
143
144/// Similar to `attach_el_and_children`, but without attaching the elemnt. Useful for
145/// patching, where we want to insert the element at a specific place.
146pub fn attach_children<Ms>(
147    children: &mut [Node<Ms>],
148    parent: &web_sys::Node,
149    mailbox: &Mailbox<Ms>,
150) {
151    for child in children.iter_mut() {
152        match child {
153            // Raise the active level once per recursion.
154            Node::Element(child_el) => attach_el_and_children(child_el, parent, mailbox),
155            Node::Text(child_text) => attach_text_node(child_text, parent),
156            Node::Empty | Node::NoChange => (),
157        }
158    }
159}
160
161/// Attaches the element, and all children, recursively. Only run this when creating a fresh vdom node, since
162/// it performs a rerender of the el and all children; eg a potentially-expensive op.
163/// This is where rendering occurs.
164pub fn attach_el_and_children<Ms>(el: &mut El<Ms>, parent: &web_sys::Node, mailbox: &Mailbox<Ms>) {
165    // No parent means we're operating on the top-level element; append it to the main div.
166    // This is how we call this function externally, ie not through recursion.
167    let el_ws = el
168        .node_ws
169        .as_ref()
170        .expect("Missing websys el in attach_el_and_children");
171
172    // Append the element
173
174    // todo: This error can occur with raw html elements, but am unsure of the cause.
175    if parent.append_child(el_ws).is_err() {
176        crate::error("Minor problem with html element (append)");
177    }
178
179    attach_children(&mut el.children, el_ws, mailbox);
180
181    // Note: Call `set_default_element_state` after child appending,
182    // otherwise it breaks autofocus in Firefox
183    set_default_element_state(el_ws, el);
184
185    wire_up_el(el, mailbox);
186}
187
188fn set_default_element_state<Ms>(el_ws: &web_sys::Node, el: &El<Ms>) {
189    // @TODO handle also other Auto* attributes?
190    // Set focus because of attribute "autofocus"
191    if let Some(at_value) = el.attrs.vals.get(&At::AutoFocus) {
192        match at_value {
193            AtValue::Some(_) | AtValue::None => el_ws
194                .dyn_ref::<web_sys::HtmlElement>()
195                .expect("Problem casting Node as HtmlElement while focusing")
196                .focus()
197                .expect("Problem focusing to an element."),
198            AtValue::Ignored => (),
199        }
200    }
201
202    // We set Textarea's initial value through non-standard attribute "value", so we have to simulate
203    // the standard way (i.e. `<textarea>A Value</textarea>`)
204    if let Some(textarea) = el_ws.dyn_ref::<web_sys::HtmlTextAreaElement>() {
205        if let Some(AtValue::Some(value)) = el.attrs.vals.get(&At::Value) {
206            textarea.set_value(value);
207        }
208    }
209}
210
211/// Recursively remove all children.
212pub fn _remove_children(el: &web_sys::Node) {
213    while let Some(child) = el.last_child() {
214        el.remove_child(&child).expect("Problem removing child");
215    }
216}
217
218// Update the attributes, style, text, and events of an element. Does not
219// process children, and assumes the tag is the same. Assume we've identfied
220// the most-correct pairing between new and old.
221pub(crate) fn patch_el_details<Ms>(
222    old: &mut El<Ms>,
223    new: &mut El<Ms>,
224    old_el_ws: &web_sys::Node,
225    mailbox: &Mailbox<Ms>,
226) {
227    fix_attrs_order(&mut new.attrs);
228
229    for (key, new_val) in &new.attrs.vals {
230        old.attrs.vals.get(key).map_or_else(
231            || {
232                set_attr_value(old_el_ws, key, new_val);
233            },
234            |old_val| {
235                if old_val != new_val {
236                    set_attr_value(old_el_ws, key, new_val);
237                }
238            },
239        );
240
241        // We handle value in the vdom using attributes, but the DOM needs
242        // to use set_value or set_checked.
243        match key {
244            At::Value => match new_val {
245                AtValue::Some(new_val) => crate::util::set_value(old_el_ws, new_val),
246                AtValue::None | AtValue::Ignored => crate::util::set_value(old_el_ws, ""),
247            },
248            At::Checked => match new_val {
249                AtValue::Some(_) | AtValue::None => crate::util::set_checked(old_el_ws, true),
250                AtValue::Ignored => crate::util::set_checked(old_el_ws, false),
251            },
252            _ => Ok(()),
253        }
254        .unwrap_or_else(|err| {
255            crate::error(err);
256        });
257    }
258    // Remove attributes that aren't in the new vdom.
259    for (key, old_val) in &old.attrs.vals {
260        if new.attrs.vals.get(key).is_none() {
261            // todo get to the bottom of this
262            old_el_ws.dyn_ref::<web_sys::Element>().map_or_else(
263                || {
264                    crate::error("Minor error on html element (setting attrs)");
265                },
266                |el| {
267                    el.remove_attribute(key.as_str())
268                        .expect("Removing an attribute");
269
270                    // We handle value in the vdom using attributes, but the DOM needs
271                    // to use set_value or set_checked.
272                    match key {
273                        At::Value => match old_val {
274                            AtValue::Some(_) => crate::util::set_value(old_el_ws, ""),
275                            _ => Ok(()),
276                        },
277                        At::Checked => match old_val {
278                            AtValue::Some(_) | AtValue::None => {
279                                crate::util::set_checked(old_el_ws, false)
280                            }
281                            AtValue::Ignored => Ok(()),
282                        },
283                        _ => Ok(()),
284                    }
285                    .unwrap_or_else(|err| {
286                        crate::error(err);
287                    });
288                },
289            );
290        }
291    }
292
293    // Patch event handlers and listeners.
294    new.event_handler_manager.attach_listeners(
295        old_el_ws.clone(),
296        Some(&mut old.event_handler_manager),
297        mailbox,
298    );
299
300    // Patch style.
301    if old.style != new.style {
302        // We can't patch each part of style; rewrite the whole attribute.
303        set_style(old_el_ws, &new.style);
304    }
305}
306
307/// Some elements have order-sensitive attributes.
308///
309/// See the [example](https://github.com/seed-rs/seed/issues/335) of such element.
310#[allow(clippy::match_same_arms)]
311fn fix_attrs_order(attrs: &mut Attrs) {
312    attrs.vals.sort_by(|at_a, _, at_b, _| {
313        // Move `At::Value` at the end.
314        match (at_a, at_b) {
315            (At::Value, At::Value) => Ordering::Equal,
316            (At::Value, _) => Ordering::Greater,
317            (_, At::Value) => Ordering::Less,
318            _ => Ordering::Equal,
319        }
320    });
321}
322
323#[allow(clippy::too_many_lines)]
324impl<Ms> From<&web_sys::Element> for El<Ms> {
325    /// Create a vdom node from a `web_sys::Element`.
326    /// Used in creating elements from html strings.
327    /// Includes children, recursively added.
328    #[allow(clippy::too_many_lines)]
329    fn from(ws_el: &web_sys::Element) -> Self {
330        let namespace = ws_el.namespace_uri().map(Namespace::from);
331        let mut el = match namespace {
332            // tag_name returns all caps for HTML, but Tag::from uses lowercase names for HTML
333            Some(Namespace::Html) => El::empty(ws_el.tag_name().to_lowercase().into()),
334            _ => El::empty(ws_el.tag_name().into()),
335        };
336
337        // Populate attributes
338        let mut attrs = Attrs::empty();
339        ws_el
340            .get_attribute_names()
341            .for_each(&mut |attr_name, _, _| {
342                let attr_name = attr_name
343                    .as_string()
344                    .expect("problem converting attr to string");
345                if let Some(attr_val) = ws_el.get_attribute(&attr_name) {
346                    attrs.add(attr_name.into(), &attr_val);
347                }
348            });
349        el.attrs = attrs;
350
351        // todo This is the same list in `shortcuts::element_svg!`.
352        // todo: Fix this repetition: Use `/scripts/populate_tags.rs`
353        // todo to consolodate these lists.
354        let svg_tags = [
355            "line",
356            "rect",
357            "circle",
358            "ellipse",
359            "polygon",
360            "polyline",
361            "mesh",
362            "path",
363            "defs",
364            "g",
365            "marker",
366            "mask",
367            "pattern",
368            "svg",
369            "switch",
370            "symbol",
371            "unknown",
372            "linearGradient",
373            "radialGradient",
374            "meshGradient",
375            "stop",
376            "image",
377            "use",
378            "altGlyph",
379            "altGlyphDef",
380            "altGlyphItem",
381            "glyph",
382            "glyphRef",
383            "textPath",
384            "text",
385            "tref",
386            "tspan",
387            "clipPath",
388            "cursor",
389            "filter",
390            "foreignObject",
391            "hathpath",
392            "meshPatch",
393            "meshRow",
394            "view",
395            "colorProfile",
396            "animate",
397            "animateColor",
398            "animateMotion",
399            "animateTransform",
400            "discard",
401            "mpath",
402            "set",
403            "desc",
404            "metadata",
405            "title",
406            "feBlend",
407            "feColorMatrix",
408            "feComponentTransfer",
409            "feComposite",
410            "feConvolveMatrix",
411            "feDiffuseLighting",
412            "feDisplacementMap",
413            "feDropShadow",
414            "feFlood",
415            "feFuncA",
416            "feFuncB",
417            "feFuncG",
418            "feFuncR",
419            "feGaussianBlur",
420            "feImage",
421            "feMerge",
422            "feMergeNode",
423            "feMorphology",
424            "feOffset",
425            "feSpecularLighting",
426            "feTile",
427            "feTurbulence",
428            "font",
429            "hkern",
430            "vkern",
431            "hatch",
432            "solidcolor",
433        ];
434
435        if svg_tags.contains(&ws_el.tag_name().as_str()) {
436            el.namespace = Some(Namespace::Svg);
437        }
438
439        if let Some(ref ns) = namespace {
440            // Prevent attaching a `xlmns` attribute to normal HTML elements.
441            if ns != &Namespace::Html {
442                el.namespace = namespace;
443            }
444        }
445
446        let children = ws_el.child_nodes();
447        for i in 0..children.length() {
448            let child = children
449                .get(i)
450                .expect("Can't find child in raw html element.");
451
452            if let Some(child_vdom) = node_from_ws(&child) {
453                el.children.push(child_vdom);
454            }
455        }
456        el
457    }
458}
459impl<Ms> From<&web_sys::Element> for Node<Ms> {
460    fn from(ws_el: &web_sys::Element) -> Node<Ms> {
461        Node::Element(ws_el.into())
462    }
463}
464
465/// Create a vdom node from a `web_sys::Node`.
466/// Used in creating elements from html strings.
467/// Includes children, recursively added.
468pub fn node_from_ws<Ms>(node: &web_sys::Node) -> Option<Node<Ms>> {
469    match node.node_type() {
470        web_sys::Node::ELEMENT_NODE => {
471            // Element node
472            let ws_el = node
473                .dyn_ref::<web_sys::Element>()
474                .expect("Problem casting Node as Element");
475
476            // Create the Element
477            Some(ws_el.into())
478        }
479        web_sys::Node::TEXT_NODE => Some(Node::new_text(
480            node.text_content().expect("Can't find text"),
481        )),
482        web_sys::Node::COMMENT_NODE => None,
483        node_type => {
484            crate::error(format!(
485                "HTML node type {node_type} is not supported by Seed"
486            ));
487            None
488        }
489    }
490}
491
492pub(crate) fn insert_el_and_children<Ms>(
493    el: &mut El<Ms>,
494    parent: &web_sys::Node,
495    next: Option<web_sys::Node>,
496    mailbox: &Mailbox<Ms>,
497) {
498    let el_ws = el.node_ws.take().expect("Missing websys el in insert_el");
499
500    insert_node(&el_ws, parent, next);
501    attach_children(&mut el.children, &el_ws, mailbox);
502
503    el.node_ws.replace(el_ws);
504    wire_up_el(el, mailbox);
505}
506
507/// Insert a new node into the specified part of the DOM tree.
508pub(crate) fn insert_node(
509    node: &web_sys::Node,
510    parent: &web_sys::Node,
511    next: Option<web_sys::Node>,
512) {
513    next.map_or_else(
514        || {
515            parent.append_child(node).expect("Problem inserting node");
516        },
517        |n| {
518            parent
519                .insert_before(node, Some(&n))
520                .expect("Problem inserting node");
521        },
522    );
523}
524
525pub(crate) fn remove_node(node: &web_sys::Node, parent: &web_sys::Node) {
526    parent
527        .remove_child(node)
528        .expect("Problem removing old el_ws when updating to empty");
529}
530
531pub(crate) fn replace_child(new: &web_sys::Node, old: &web_sys::Node, parent: &web_sys::Node) {
532    parent
533        .replace_child(new, old)
534        .expect("Problem replacing element");
535}
536
537#[inline]
538fn wire_up_el<Ms>(el: &mut El<Ms>, mailbox: &Mailbox<Ms>) {
539    let node_ws = el
540        .node_ws
541        .as_ref()
542        .expect("Missing websys el in attach_el_and_children");
543
544    for ref_ in &mut el.refs {
545        ref_.set(node_ws.clone());
546    }
547
548    el.event_handler_manager
549        .attach_listeners(node_ws.clone(), None, mailbox);
550
551    for handler in &el.insert_handlers {
552        let el_ws = node_ws
553            .dyn_ref::<web_sys::Element>()
554            .expect("Problem casting Node as Element while wiring up el");
555
556        let maybe_msg = handler.0(el_ws.clone());
557        mailbox.send(maybe_msg);
558    }
559}