use crate::events::MountEvent;
use crate::{
dom::Dispatch,
html,
html::attributes::Special,
mt_dom::{Callback, NodeIdx},
Attribute,
};
use mt_dom::AttValue;
use std::{collections::HashMap, sync::Mutex};
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use web_sys::{
self, Element, Event, EventTarget, HtmlElement, HtmlInputElement,
HtmlTextAreaElement, Node, Text,
};
use lazy_static::lazy_static;
lazy_static! {
static ref DATA_SAURON_VDOM_ID_VALUE: Mutex<u32> = Mutex::new(0);
}
fn create_unique_identifier() -> u32 {
let mut elem_unique_id = DATA_SAURON_VDOM_ID_VALUE
.lock()
.expect("Unable to obtain lock");
*elem_unique_id += 1;
*elem_unique_id
}
pub(crate) const DATA_SAURON_VDOM_ID: &str = "data-sauron-vdom-id";
pub type ActiveClosure =
HashMap<u32, Vec<(&'static str, Closure<dyn FnMut(web_sys::Event)>)>>;
#[derive(Debug)]
pub struct CreatedNode {
pub node: Node,
pub(crate) closures: ActiveClosure,
}
impl CreatedNode {
pub fn without_closures(node: Node) -> Self {
CreatedNode {
node,
closures: HashMap::with_capacity(0),
}
}
pub fn create_text_node(txt: &str) -> Text {
crate::document().create_text_node(txt)
}
pub fn create_dom_node<DSP, MSG>(
program: &DSP,
node_idx_lookup: &mut HashMap<NodeIdx, Node>,
vnode: &crate::Node<MSG>,
focused_node: &mut Option<Node>,
node_idx: &mut Option<NodeIdx>,
) -> CreatedNode
where
MSG: 'static,
DSP: Clone + Dispatch<MSG> + 'static,
{
Self::create_dom_node_opt(
Some(program),
node_idx_lookup,
vnode,
focused_node,
node_idx,
)
}
pub fn create_dom_node_opt<DSP, MSG>(
program: Option<&DSP>,
node_idx_lookup: &mut HashMap<NodeIdx, Node>,
vnode: &crate::Node<MSG>,
focused_node: &mut Option<Node>,
node_idx: &mut Option<NodeIdx>,
) -> CreatedNode
where
MSG: 'static,
DSP: Clone + Dispatch<MSG> + 'static,
{
match vnode {
crate::Node::Text(txt) => {
let text_node = Self::create_text_node(&txt.text);
#[cfg(feature = "with-nodeidx-debug")]
if let Some(node_idx) = node_idx {
node_idx_lookup
.insert(*node_idx, text_node.clone().unchecked_into());
}
CreatedNode::without_closures(text_node.unchecked_into())
}
crate::Node::Element(element_node) => {
let created_element: CreatedNode = Self::create_element_node(
program,
node_idx_lookup,
element_node,
focused_node,
node_idx,
)
.into();
created_element
}
}
}
fn dispatch_mount_event<MSG>(velem: &crate::Element<MSG>, element: &Element)
where
MSG: 'static,
{
for att in velem.attrs.iter() {
if *att.name() == "mount" {
log::trace!("found a mount event");
for val in att.value().iter() {
match val {
AttValue::Callback(cb) => {
cb.emit(MountEvent {
target_node: element.clone().unchecked_into(),
});
}
AttValue::Plain(_) => {
log::warn!("mount should not be a plain value");
}
}
}
}
}
}
fn create_element_node<DSP, MSG>(
program: Option<&DSP>,
node_idx_lookup: &mut HashMap<NodeIdx, Node>,
velem: &crate::Element<MSG>,
focused_node: &mut Option<Node>,
node_idx: &mut Option<NodeIdx>,
) -> CreatedNode
where
MSG: 'static,
DSP: Clone + Dispatch<MSG> + 'static,
{
let document = crate::document();
let element = if let Some(ref namespace) = velem.namespace() {
document
.create_element_ns(Some(namespace), &velem.tag())
.expect("Unable to create element")
} else {
document
.create_element(&velem.tag())
.expect("Unable to create element")
};
Self::dispatch_mount_event(velem, &element);
if velem.is_focused() {
*focused_node = Some(element.clone().unchecked_into());
log::trace!("element is focused..{:?}", focused_node);
}
#[cfg(feature = "with-nodeidx-debug")]
if let Some(ref node_idx) = node_idx {
node_idx_lookup.insert(*node_idx, element.clone().unchecked_into());
}
let mut closures = ActiveClosure::new();
Self::set_element_attributes(
program,
&mut closures,
&element,
&velem.get_attributes().iter().collect::<Vec<_>>(),
);
#[cfg(feature = "with-nodeidx-debug")]
if let Some(node_idx) = node_idx {
Self::set_element_attributes(
program,
&mut closures,
&element,
&[&crate::prelude::attr("node_idx", *node_idx)],
);
}
let mut previous_node_was_text = false;
for child in velem.get_children().iter() {
node_idx.as_mut().map(|node_idx| *node_idx += 1);
match child {
crate::Node::Text(txt) => {
let current_node: &web_sys::Node = element.as_ref();
if previous_node_was_text {
let separator = document.create_comment("mordor");
current_node
.append_child(separator.as_ref())
.expect("Unable to append child");
}
let text_node = Self::create_text_node(&txt.text);
current_node
.append_child(&text_node)
.expect("Unable to append text node");
#[cfg(feature = "with-nodeidx-debug")]
if let Some(node_idx) = &node_idx {
node_idx_lookup
.insert(*node_idx, text_node.unchecked_into());
}
previous_node_was_text = true;
}
crate::Node::Element(_element_node) => {
previous_node_was_text = false;
let created_child = Self::create_dom_node_opt(
program,
node_idx_lookup,
child,
focused_node,
node_idx,
);
closures.extend(created_child.closures);
element
.append_child(&created_child.node)
.expect("Unable to append element node");
}
}
}
let node: Node = element.unchecked_into();
CreatedNode { node, closures }
}
pub fn set_element_attributes<DSP, MSG>(
program: Option<&DSP>,
closures: &mut ActiveClosure,
element: &Element,
attrs: &[&Attribute<MSG>],
) where
MSG: 'static,
DSP: Clone + Dispatch<MSG> + 'static,
{
let attrs = mt_dom::merge_attributes_of_same_name(attrs);
for att in attrs {
Self::set_element_attribute(program, closures, element, &att);
}
}
pub fn set_element_attribute<DSP, MSG>(
program: Option<&DSP>,
closures: &mut ActiveClosure,
element: &Element,
attr: &Attribute<MSG>,
) where
MSG: 'static,
DSP: Clone + Dispatch<MSG> + 'static,
{
let (callbacks, plain_values, func_values) =
html::attributes::partition_callbacks_from_plain_and_func_calls(
attr,
);
if let Some(merged_plain_values) =
html::attributes::merge_plain_attributes_values(&plain_values)
{
if let Some(ref namespace) = attr.namespace() {
element
.set_attribute_ns(
Some(namespace),
attr.name(),
&merged_plain_values,
)
.unwrap_or_else(|_| {
panic!(
"Error setting an attribute_ns for {:?}",
element
)
});
} else {
match *attr.name() {
"value" => {
if let Some(input) =
element.dyn_ref::<HtmlInputElement>()
{
input.set_value(&merged_plain_values);
} else if let Some(textarea) =
element.dyn_ref::<HtmlTextAreaElement>()
{
textarea.set_value(&merged_plain_values);
}
}
"checked" => {
if let Some(input) =
element.dyn_ref::<HtmlInputElement>()
{
let checked: bool = plain_values
.first()
.map(|av| {
av.get_simple()
.map(|v| v.as_bool())
.flatten()
})
.flatten()
.unwrap_or(false);
input.set_checked(checked);
}
}
_ => {
element
.set_attribute(attr.name(), &merged_plain_values)
.unwrap_or_else(|_| {
panic!(
"Error setting an attribute for {:?}",
element
)
});
}
}
}
} else {
element
.remove_attribute(attr.name())
.expect("must remove attribute");
}
if let Some(merged_func_values) =
html::attributes::merge_plain_attributes_values(&func_values)
{
match *attr.name() {
"inner_html" => element.set_inner_html(&merged_func_values),
_ => (),
}
}
for callback in callbacks {
let unique_id = create_unique_identifier();
element
.set_attribute(DATA_SAURON_VDOM_ID, &unique_id.to_string())
.expect("Could not set attribute on element");
closures.insert(unique_id, vec![]);
if let Some(program) = program {
let event_str = attr.name();
let current_elm: &EventTarget =
element.dyn_ref().expect("unable to cast to event targe");
if *event_str == "enter" {
let program_clone = program.clone();
let callback_clone = callback.clone();
let key_press_func: Closure<dyn FnMut(web_sys::Event)> =
Closure::wrap(Box::new(
move |event: web_sys::Event| {
let ke: &web_sys::KeyboardEvent = event
.dyn_ref()
.expect("should be a keyboard event");
if ke.key() == "Enter" {
let msg = callback_clone
.emit(Into::<Event>::into(event));
program_clone.dispatch(msg);
}
},
));
current_elm
.add_event_listener_with_callback(
"keypress",
key_press_func.as_ref().unchecked_ref(),
)
.expect("unable to attach enter event listener");
key_press_func.forget();
} else {
let closure_wrap: Closure<dyn FnMut(web_sys::Event)> =
create_closure_wrap(program, &callback);
current_elm
.add_event_listener_with_callback(
event_str,
closure_wrap.as_ref().unchecked_ref(),
)
.expect("Unable to attached event listener");
closures
.get_mut(&unique_id)
.expect("Unable to get closure")
.push((event_str, closure_wrap));
}
}
}
}
pub(crate) fn set_element_focus(element: &Element) {
let html_element: &HtmlElement = element.unchecked_ref();
html_element.focus().expect("must focus")
}
pub fn remove_element_attribute<MSG>(
element: &Element,
attr: &Attribute<MSG>,
) -> Result<(), JsValue> {
log::trace!("removing attribute: {}", attr.name());
element.remove_attribute(attr.name())?;
match *attr.name() {
"checked" => {
if let Some(input) = element.dyn_ref::<HtmlInputElement>() {
input.set_checked(false);
}
}
_ => (),
}
Ok(())
}
}
pub(crate) fn create_closure_wrap<DSP, MSG>(
program: &DSP,
callback: &Callback<crate::Event, MSG>,
) -> Closure<dyn FnMut(web_sys::Event)>
where
MSG: 'static,
DSP: Clone + Dispatch<MSG> + 'static,
{
let callback_clone = callback.clone();
let program_clone = program.clone();
Closure::wrap(Box::new(move |event: web_sys::Event| {
let msg = callback_clone.emit(event);
program_clone.dispatch(msg);
}))
}