seed 0.1.6

A frontend framework for Rust, via WebAssembly
Documentation
//! This file contains interactions with web_sys.
use wasm_bindgen::JsCast;

use crate::dom_types;

/// Add a shim to make check logic more natural than the DOM handles it.
fn set_attr_shim(el_ws: &web_sys::Element, name: &str, val: &str) {
    let mut set_check = false;

    if name == "checked" {
        let input_el = el_ws.dyn_ref::<web_sys::HtmlInputElement>();
        if let Some(el) = input_el {
            match val {
                "true" => {
                    el.set_checked(true);
                    set_check = true;
                },
                "false" => {
                    el.set_checked(false);
                    set_check = true;
                },
                _ => ()
            }
        }
    }
    if !set_check {
        el_ws.set_attribute(name, val).expect("Problem setting an atrribute.");
    }
}

/// Create and return a web_sys Element from our virtual-dom El. The web_sys
/// Element is a close analog to JS/DOM elements.
/// web-sys reference: https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Element.html
/// Mozilla reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element\
/// See also: https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Node.html
pub fn make_websys_el<Ms: Clone>(el_vdom: &mut dom_types::El<Ms>, document: &web_sys::Document) -> web_sys::Element {
    // Create the DOM-analog element; it won't render until attached to something.
    let el_ws = document.create_element(&el_vdom.tag.as_str()).expect("Problem creating web-sys El");

    for (name, val) in &el_vdom.attrs.vals {
        set_attr_shim(&el_ws, name, val);
    }

    // Style is just an attribute in the actual Dom, but is handled specially in our vdom;
    // merge the different parts of style here.
    if el_vdom.style.vals.keys().len() > 0 {
        el_ws.set_attribute("style", &el_vdom.style.as_str()).expect("Problem setting style");
    }

    // We store text as Option<String>, but set_text_content uses Option<&str>.
    // A naive match Some(t) => Some(&t) does not work.
    // See https://stackoverflow.com/questions/31233938/converting-from-optionstring-to-optionstr
    let text = el_vdom.text.as_ref().map(String::as_ref);
    if el_vdom.markdown {
        el_ws.set_inner_html(text.unwrap())
    } else {
        el_ws.set_text_content(text);
    }

    // Don't attach listeners here: It'll cause a conflict when you try to
    // attach a second time. ?
    // todo delete these lines once events are sorted
//    for listener in &mut el_vdom.listeners {
//        listener.attach(&el_ws, mailbox.clone());
//    }

    el_ws
}

/// Attaches the element, and all children, recursively. Only run this when creating a fresh vdom node, since
/// it performs a rerender of the el and all children; eg a potentially-expensive op.
/// This is where rendering occurs.
pub fn attach_els<Ms: Clone>(el_vdom: &mut dom_types::El<Ms>, parent: &web_sys::Element) {
    // No parent means we're operating on the top-level element; append it to the main div.
    // This is how we call this function externally, ie not through recursion.

    // Don't render if we're dealing with a dummy element.
    // todo get this working. it produes panics
//    if el_vdom.is_dummy() == true { return }

    let el_ws = el_vdom.el_ws.take().expect("Missing websys el");

//    crate::log("Rendering element in attach");
    // Purge existing children.
//    parent.remove_child(&el_ws).expect("Missing el_ws");
    // Append its child while it's out of its element.
    parent.append_child(&el_ws).unwrap();

    // todo: It seesm like if text is present along with children, it'll bbe
    // todo shown before them instead of after. Fix this.

    for child in &mut el_vdom.children {
        // Raise the active level once per recursion.
        attach_els(child, &el_ws)
    }

    // Replace the web_sys el... Indiana-Jones-style.
    el_vdom.el_ws.replace(el_ws);
}

/// Recursively remove all children.
pub fn remove_children(el: &web_sys::Element) {
    while let Some(child) = el.last_child() {
        el.remove_child(&child).unwrap();
    }
}

/// Update the attributes, style, text, and events of an element. Does not
/// process children, and assumes the tag is the same. Assume we've identfied
/// the most-correct pairing between new and old.
pub fn patch_el_details<Ms: Clone>(old: &mut dom_types::El<Ms>, new: &mut dom_types::El<Ms>,
           old_el_ws: &web_sys::Element, document: &web_sys::Document) {

    if old.attrs != new.attrs {
        for (key, new_val) in &new.attrs.vals {
            match old.attrs.vals.get(key) {
                Some(old_val) => {
                    // The value's different
                    if old_val != new_val {
                        set_attr_shim(&old_el_ws, key, new_val);
                    }
                },
                None => old_el_ws.set_attribute(key, new_val).expect("Adding a new attribute")
            }
        }
        // Remove attributes that aren't in the new vdom.
        for name in old.attrs.vals.keys() {
            if new.attrs.vals.get(name).is_none() {
                old_el_ws.remove_attribute(name).expect("Removing an attribute");
            }
        }
    }

    // Patch style.
    if old.style != new.style {
        // We can't patch each part of style; rewrite the whole attribute.
        old_el_ws.set_attribute("style", &new.style.as_str())
            .expect("Setting style");
    }


    // Patch text
    if old.text != new.text {
        // This is not as straightforward as it looks: There can be multiple text nodes
        // in the DOM, even though our API only allows for 1 per element. If we
        // naively run set_text_content(), all child nodes will be removed.
        // Text is stored in special Text nodes that don't have a direct-relation to
        // the vdom.

        let text = new.text.clone().unwrap_or_default();

        if new.markdown {
            old_el_ws.set_inner_html(&text)
        } else {

            if old.text.is_none() {
                // There's no old node to find: Add it.
                let new_next_node = document.create_text_node(&text);


                old_el_ws.append_child(&new_next_node).unwrap();
            } else {
                // Iterating over a NodeList, unfortunately, is not as clean as you might expect.
                let children = old_el_ws.child_nodes();
                for i in 0..children.length() {
                    let node = children.item(i).unwrap();
                    // We've found it; there will be not more than 1 text node.
                    if node.node_type() == 3 {
                        node.set_text_content(Some(&text));
                        break;
                    }
                }
            }
        }
    }
}