use std::rc::Rc;
use crate::{
AttrGroupValue, Computed, DomText, DropFileItem, JsJson,
computed::{
DropResource,
struct_mut::{VecDequeMut, VecMut},
},
dev::JsJsonListDecoder,
driver_module::{StaticString, api::api_callbacks, get_driver_dom},
};
use super::{
attr_value::{AttrValue, CssAttrValue},
callback::{Callback, Callback1},
dom_element_class::DomElementClassMerge,
dom_element_ref::DomElementRef,
dom_id::DomId,
dom_node::DomNode,
events::{ClickEvent, DropFileEvent, KeyDownEvent},
};
pub struct DomElement {
id_dom: DomId,
child_node: VecDequeMut<DomNode>,
subscriptions: VecMut<DropResource>,
class_manager: DomElementClassMerge,
}
impl DomElement {
pub fn new(name: impl Into<StaticString>) -> Self {
let name = name.into();
let id_dom = DomId::from_name(name.as_str());
get_driver_dom().create_node(id_dom, name);
let class_manager = DomElementClassMerge::new(id_dom);
Self {
id_dom,
child_node: VecDequeMut::new(),
subscriptions: VecMut::new(),
class_manager,
}
}
pub fn add_attr(&self, name: impl Into<StaticString>, value: impl Into<AttrValue>) {
let name = name.into();
let value = value.into();
match value {
AttrValue::String(value) => {
self.class_manager.set_attr_value(name, Some(value));
}
AttrValue::Computed(computed) => {
let class_manager = self.class_manager.clone();
self.subscribe(computed, move |value| {
class_manager.set_attr_value(name.clone(), Some(Rc::new(value)));
});
}
AttrValue::ComputedOpt(computed) => {
let class_manager = self.class_manager.clone();
self.subscribe(computed, move |value| {
class_manager.set_attr_value(name.clone(), value.map(Rc::new));
});
}
AttrValue::Value(value) => {
let class_manager = self.class_manager.clone();
self.subscribe(value.to_computed(), move |value| {
class_manager.set_attr_value(name.clone(), Some(Rc::new(value)));
});
}
AttrValue::ValueOpt(value) => {
let class_manager = self.class_manager.clone();
self.subscribe(value.to_computed(), move |value| {
class_manager.set_attr_value(name.clone(), value.map(Rc::new));
});
}
};
}
pub fn add_attr_group(
mut self,
values: impl IntoIterator<Item = (impl Into<String>, impl Into<AttrGroupValue>)>,
) -> Self {
for (key, value) in values.into_iter() {
self = self.add_attr_group_item(key.into(), value.into());
}
self
}
pub fn add_attr_group_item(self, key: String, value: AttrGroupValue) -> Self {
match (key.as_str(), value) {
("css", AttrGroupValue::Css { css, class_name }) => {
self.css_with_class_name(css, class_name)
}
("hook_key_down", AttrGroupValue::HookKeyDown(on_hook_key_down)) => {
self.hook_key_down_rc(on_hook_key_down)
}
("on_blur", AttrGroupValue::OnBlur(on_blur)) => self.on_blur_rc(on_blur),
("on_change", AttrGroupValue::OnChange(on_change)) => self.on_change_rc(on_change),
("on_click", AttrGroupValue::OnClick(on_click)) => self.on_click_rc(on_click),
("on_dropfile", AttrGroupValue::OnDropfile(on_dropfile)) => {
self.on_dropfile_rc(on_dropfile)
}
("on_input", AttrGroupValue::OnInput(on_input)) => self.on_input_rc(on_input),
("on_key_down", AttrGroupValue::OnKeyDown(on_key_down)) => {
self.on_key_down_rc(on_key_down)
}
("on_load", AttrGroupValue::OnLoad(on_load)) => self.on_load_rc(on_load),
("on_mouse_down", AttrGroupValue::OnMouseDown(on_mouse_down)) => {
self.on_mouse_down_rc(on_mouse_down)
}
("on_mouse_enter", AttrGroupValue::OnMouseEnter(on_mouse_enter)) => {
self.on_mouse_enter_rc(on_mouse_enter)
}
("on_mouse_leave", AttrGroupValue::OnMouseLeave(on_mouse_leave)) => {
self.on_mouse_leave_rc(on_mouse_leave)
}
("on_mouse_up", AttrGroupValue::OnMouseUp(on_mouse_up)) => {
self.on_mouse_up_rc(on_mouse_up)
}
("on_submit", AttrGroupValue::OnSubmit(on_submit))
| ("form", AttrGroupValue::OnSubmit(on_submit)) => self.on_submit_rc(on_submit),
(_, AttrGroupValue::AttrValue(value)) => self.attr(key, value),
(_, _) => {
crate::log::error!("Invalid attribute type for key {key}");
self
}
}
}
pub fn add_child(&self, child_node: impl Into<DomNode>) {
let child_node = child_node.into();
let child_id = child_node.id_dom();
get_driver_dom().insert_before(self.id_dom, child_id, None);
self.child_node.push(child_node);
}
pub fn add_child_text(&self, text: impl Into<String>) {
let text = text.into();
self.add_child(DomNode::Text {
node: DomText::new(text),
});
}
pub fn attr(self, name: impl Into<StaticString>, value: impl Into<AttrValue>) -> Self {
self.add_attr(name, value);
self
}
pub fn attrs<T: Into<AttrValue>>(self, attrs: Vec<(impl Into<StaticString>, T)>) -> Self {
for (name, value) in attrs.into_iter() {
self.add_attr(name, value)
}
self
}
pub fn child(self, child_node: impl Into<DomNode>) -> Self {
self.add_child(child_node);
self
}
pub fn child_text(self, text: impl Into<String>) -> Self {
self.add_child_text(text);
self
}
pub fn children<C: Into<DomNode>>(self, children: Vec<C>) -> Self {
for child_node in children.into_iter() {
self.add_child(child_node)
}
self
}
pub fn css(self, css: impl Into<CssAttrValue>) -> Self {
self.css_with_class_name(css, None)
}
pub fn css_with_class_name(
self,
css: impl Into<CssAttrValue>,
debug_class_name: Option<String>,
) -> Self {
let css = css.into();
match css {
CssAttrValue::Css(css) => {
self.class_manager.set_css(css, debug_class_name);
}
CssAttrValue::Computed(css) => {
let class_manager = self.class_manager.clone();
self.subscribe(css, move |css| {
class_manager.set_css(css, debug_class_name.clone());
});
}
}
self
}
pub fn get_ref(&self) -> DomElementRef {
DomElementRef::new(self.id_dom)
}
#[cfg(test)]
pub fn get_children(&self) -> &VecDequeMut<DomNode> {
&self.child_node
}
pub fn hook_key_down(self, on_hook_key_down: impl Into<Callback1<KeyDownEvent, bool>>) -> Self {
self.hook_key_down_rc(Rc::new(on_hook_key_down.into()))
}
pub fn hook_key_down_rc(self, on_hook_key_down: Rc<Callback1<KeyDownEvent, bool>>) -> Self {
let on_hook_key_down = self.install_callback1(on_hook_key_down);
self.add_event_listener("hook_keydown", move |data| match get_key_down_event(data) {
Ok(event) => {
let prevent_default = on_hook_key_down(event);
match prevent_default {
true => JsJson::True,
false => JsJson::False,
}
}
Err(error) => {
log::error!("export_websocket_callback_message -> params decode error -> {error}");
JsJson::False
}
})
}
pub fn id_dom(&self) -> DomId {
self.id_dom
}
pub fn on_blur(self, on_blur: impl Into<Callback<()>>) -> Self {
self.on_blur_rc(Rc::new(on_blur.into()))
}
pub fn on_blur_rc(self, on_blur: Rc<Callback<()>>) -> Self {
let on_blur = self.install_callback(on_blur);
self.add_event_listener("blur", move |_data| {
on_blur();
JsJson::Null
})
}
pub fn on_change(self, on_change: impl Into<Callback1<String, ()>>) -> Self {
self.on_change_rc(Rc::new(on_change.into()))
}
pub fn on_change_rc(self, on_change: Rc<Callback1<String, ()>>) -> Self {
let on_change = self.install_callback1(on_change);
self.add_event_listener("change", move |data| {
if let JsJson::String(text) = data {
on_change(text);
} else {
log::error!("Invalid data: on_change: {data:?}");
}
JsJson::Null
})
}
pub fn on_click(self, on_click: impl Into<Callback1<ClickEvent, ()>>) -> Self {
self.on_click_rc(Rc::new(on_click.into()))
}
pub fn on_click_rc(self, on_click: Rc<Callback1<ClickEvent, ()>>) -> Self {
let on_click = self.install_callback1(on_click);
self.add_event_listener("click", move |_data| {
let click_event = ClickEvent::default();
on_click(click_event.clone());
JsJson::from(click_event)
})
}
pub fn on_dropfile(self, on_dropfile: impl Into<Callback1<DropFileEvent, ()>>) -> Self {
self.on_dropfile_rc(Rc::new(on_dropfile.into()))
}
pub fn on_dropfile_rc(self, on_dropfile: Rc<Callback1<DropFileEvent, ()>>) -> Self {
let on_dropfile = self.install_callback1(on_dropfile);
self.add_event_listener("drop", move |data| {
let params = data.map_list(|mut params: JsJsonListDecoder| {
let files = params.get_vec("drop file", |item: JsJson| {
item.map_list(|mut item: JsJsonListDecoder| {
let name = item.get_string("name")?;
let data = item.get_buffer("data")?;
Ok(DropFileItem::new(name, data))
})
})?;
Ok(DropFileEvent::new(files))
});
match params {
Ok(params) => {
on_dropfile(params);
}
Err(error) => {
log::error!("on_dropfile -> params decode error -> {error}");
}
};
JsJson::Null
})
}
pub fn on_input(self, on_input: impl Into<Callback1<String, ()>>) -> Self {
self.on_input_rc(Rc::new(on_input.into()))
}
pub fn on_input_rc(self, on_input: Rc<Callback1<String, ()>>) -> Self {
let on_input = self.install_callback1(on_input);
self.add_event_listener("input", move |data| {
if let JsJson::String(text) = data {
on_input(text);
} else {
log::error!("Invalid data: on_input: {data:?}");
}
JsJson::Null
})
}
pub fn on_key_down(self, on_key_down: impl Into<Callback1<KeyDownEvent, bool>>) -> Self {
self.on_key_down_rc(Rc::new(on_key_down.into()))
}
pub fn on_key_down_rc(self, on_key_down: Rc<Callback1<KeyDownEvent, bool>>) -> Self {
let on_key_down = self.install_callback1(on_key_down);
self.add_event_listener("keydown", move |data| match get_key_down_event(data) {
Ok(event) => {
let prevent_default = on_key_down(event);
match prevent_default {
true => JsJson::True,
false => JsJson::False,
}
}
Err(error) => {
log::error!("export_websocket_callback_message -> params decode error -> {error}");
JsJson::False
}
})
}
pub fn on_load(self, on_load: impl Into<Callback<()>>) -> Self {
self.on_load_rc(Rc::new(on_load.into()))
}
pub fn on_load_rc(self, on_load: Rc<Callback<()>>) -> Self {
let on_load = self.install_callback(on_load);
self.add_event_listener("load", move |_data| {
on_load();
JsJson::Null
})
}
pub fn on_mouse_down(self, on_mouse_down: impl Into<Callback<bool>>) -> Self {
self.on_mouse_down_rc(Rc::new(on_mouse_down.into()))
}
pub fn on_mouse_down_rc(self, on_mouse_down: Rc<Callback<bool>>) -> Self {
let on_mouse_down = self.install_callback(on_mouse_down);
self.add_event_listener("mousedown", move |_data| {
if on_mouse_down() {
JsJson::True
} else {
JsJson::False
}
})
}
pub fn on_mouse_enter(self, on_mouse_enter: impl Into<Callback<()>>) -> Self {
self.on_mouse_enter_rc(Rc::new(on_mouse_enter.into()))
}
pub fn on_mouse_enter_rc(self, on_mouse_enter: Rc<Callback<()>>) -> Self {
let on_mouse_enter = self.install_callback(on_mouse_enter);
self.add_event_listener("mouseenter", move |_data| {
on_mouse_enter();
JsJson::Null
})
}
pub fn on_mouse_leave(self, on_mouse_leave: impl Into<Callback<()>>) -> Self {
self.on_mouse_leave_rc(Rc::new(on_mouse_leave.into()))
}
pub fn on_mouse_leave_rc(self, on_mouse_leave: Rc<Callback<()>>) -> Self {
let on_mouse_leave = self.install_callback(on_mouse_leave);
self.add_event_listener("mouseleave", move |_data| {
on_mouse_leave();
JsJson::Null
})
}
pub fn on_mouse_up(self, on_mouse_up: impl Into<Callback<bool>>) -> Self {
self.on_mouse_up_rc(Rc::new(on_mouse_up.into()))
}
pub fn on_mouse_up_rc(self, on_mouse_up: Rc<Callback<bool>>) -> Self {
let on_mouse_up = self.install_callback(on_mouse_up);
self.add_event_listener("mouseup", move |_data| {
if on_mouse_up() {
JsJson::True
} else {
JsJson::False
}
})
}
pub fn on_submit(self, on_submit: impl Into<Callback<()>>) -> Self {
self.on_submit_rc(Rc::new(on_submit.into()))
}
pub fn on_submit_rc(self, on_submit: Rc<Callback<()>>) -> Self {
let on_submit = self.install_callback(on_submit);
self.add_event_listener("submit", move |_data| {
on_submit();
JsJson::Null
})
}
pub fn append_drop_resource(&self, resource: DropResource) {
self.subscriptions.push(resource);
}
fn subscribe<T: Clone + PartialEq + 'static>(
&self,
value: Computed<T>,
call: impl Fn(T) + 'static,
) {
let client = value.subscribe(call);
self.subscriptions.push(client);
}
fn add_event_listener(
self,
name: &'static str,
callback: impl Fn(JsJson) -> JsJson + 'static,
) -> Self {
let (callback_id, drop) = api_callbacks().register(callback);
let drop_event = DropResource::new(move || {
get_driver_dom().callback_remove(self.id_dom, name, callback_id);
drop.off();
});
get_driver_dom().callback_add(self.id_dom, name, callback_id);
self.subscriptions.push(drop_event);
self
}
fn install_callback<R: 'static>(
&self,
callback: Rc<Callback<R>>,
) -> Rc<dyn Fn() -> R + 'static> {
let (callback, drop) = callback.subscribe();
if let Some(drop) = drop {
self.subscriptions.push(drop);
}
callback
}
fn install_callback1<T: 'static, R: 'static>(
&self,
callback: Rc<Callback1<T, R>>,
) -> Rc<dyn Fn(T) -> R + 'static> {
let (callback, drop) = callback.subscribe();
if let Some(drop) = drop {
self.subscriptions.push(drop);
}
callback
}
}
impl Drop for DomElement {
fn drop(&mut self) {
get_driver_dom().remove_node(self.id_dom);
}
}
fn get_key_down_event(data: JsJson) -> Result<KeyDownEvent, String> {
data.map_list(|mut params: JsJsonListDecoder| {
let key = params.get_string("key")?;
let code = params.get_string("code")?;
let alt_key = params.get_bool("altKey")?;
let ctrl_key = params.get_bool("ctrlKey")?;
let shift_key = params.get_bool("shiftKey")?;
let meta_key = params.get_bool("metaKey")?;
params.expect_no_more()?;
Ok(KeyDownEvent {
key,
code,
alt_key,
ctrl_key,
shift_key,
meta_key,
})
})
}