Skip to main content

euv_core/vdom/
impl.rs

1use crate::*;
2
3/// Maps each `Attribute` variant to its corresponding DOM attribute string.
4impl Attribute {
5    /// Returns the string representation of this attribute name for DOM binding.
6    pub fn as_str(&self) -> String {
7        match self {
8            Attribute::AccessKey => "accesskey".to_string(),
9            Attribute::Action => "action".to_string(),
10            Attribute::Alt => "alt".to_string(),
11            Attribute::AriaLabel => "aria-label".to_string(),
12            Attribute::AutoComplete => "autocomplete".to_string(),
13            Attribute::AutoFocus => "autofocus".to_string(),
14            Attribute::Checked => "checked".to_string(),
15            Attribute::Class => "class".to_string(),
16            Attribute::Cols => "cols".to_string(),
17            Attribute::ContentEditable => "contenteditable".to_string(),
18            Attribute::Data(name) => format!("data-{}", name),
19            Attribute::Dir => "dir".to_string(),
20            Attribute::Disabled => "disabled".to_string(),
21            Attribute::Draggable => "draggable".to_string(),
22            Attribute::EncType => "enctype".to_string(),
23            Attribute::For => "for".to_string(),
24            Attribute::Form => "form".to_string(),
25            Attribute::Height => "height".to_string(),
26            Attribute::Hidden => "hidden".to_string(),
27            Attribute::Href => "href".to_string(),
28            Attribute::Id => "id".to_string(),
29            Attribute::Lang => "lang".to_string(),
30            Attribute::Max => "max".to_string(),
31            Attribute::MaxLength => "maxlength".to_string(),
32            Attribute::Method => "method".to_string(),
33            Attribute::Min => "min".to_string(),
34            Attribute::MinLength => "minlength".to_string(),
35            Attribute::Multiple => "multiple".to_string(),
36            Attribute::Name => "name".to_string(),
37            Attribute::Pattern => "pattern".to_string(),
38            Attribute::Placeholder => "placeholder".to_string(),
39            Attribute::ReadOnly => "readonly".to_string(),
40            Attribute::Required => "required".to_string(),
41            Attribute::Rows => "rows".to_string(),
42            Attribute::Selected => "selected".to_string(),
43            Attribute::Size => "size".to_string(),
44            Attribute::SpellCheck => "spellcheck".to_string(),
45            Attribute::Src => "src".to_string(),
46            Attribute::Step => "step".to_string(),
47            Attribute::Style => "style".to_string(),
48            Attribute::TabIndex => "tabindex".to_string(),
49            Attribute::Target => "target".to_string(),
50            Attribute::Title => "title".to_string(),
51            Attribute::Type => "type".to_string(),
52            Attribute::Value => "value".to_string(),
53            Attribute::Width => "width".to_string(),
54            Attribute::Other(name) => name.clone(),
55        }
56    }
57}
58
59/// Clones a `DynamicNode` by cloning its `HookContext` (Copy) and `render_fn` (Rc).
60impl Clone for DynamicNode {
61    fn clone(&self) -> Self {
62        DynamicNode {
63            render_fn: Rc::clone(&self.render_fn),
64            hook_context: self.hook_context,
65        }
66    }
67}
68
69/// Converts a `VirtualNode` reference into an owned node.
70impl AsNode for VirtualNode {
71    fn as_node(&self) -> Option<VirtualNode> {
72        Some(self.clone())
73    }
74}
75
76/// Converts a `VirtualNode` reference into an owned node.
77impl AsNode for &VirtualNode {
78    fn as_node(&self) -> Option<VirtualNode> {
79        Some((*self).clone())
80    }
81}
82
83/// Converts a `String` into a text virtual node.
84impl AsNode for String {
85    fn as_node(&self) -> Option<VirtualNode> {
86        Some(VirtualNode::Text(TextNode::new(self.clone(), None)))
87    }
88}
89
90/// Converts a string slice into a text virtual node.
91impl AsNode for &str {
92    fn as_node(&self) -> Option<VirtualNode> {
93        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
94    }
95}
96
97/// Converts an `i32` into a text virtual node.
98impl AsNode for i32 {
99    fn as_node(&self) -> Option<VirtualNode> {
100        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
101    }
102}
103
104/// Converts an `i64` into a text virtual node.
105impl AsNode for i64 {
106    fn as_node(&self) -> Option<VirtualNode> {
107        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
108    }
109}
110
111/// Converts a `usize` into a text virtual node.
112impl AsNode for usize {
113    fn as_node(&self) -> Option<VirtualNode> {
114        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
115    }
116}
117
118/// Converts an `f32` into a text virtual node.
119impl AsNode for f32 {
120    fn as_node(&self) -> Option<VirtualNode> {
121        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
122    }
123}
124
125/// Converts an `f64` into a text virtual node.
126impl AsNode for f64 {
127    fn as_node(&self) -> Option<VirtualNode> {
128        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
129    }
130}
131
132/// Converts a `bool` into a text virtual node.
133impl AsNode for bool {
134    fn as_node(&self) -> Option<VirtualNode> {
135        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
136    }
137}
138
139/// Converts a signal into a reactive text virtual node.
140impl<T> AsNode for Signal<T>
141where
142    T: Clone + PartialEq + std::fmt::Display + 'static,
143{
144    fn as_node(&self) -> Option<VirtualNode> {
145        Some(self.as_reactive_text())
146    }
147}
148
149/// Converts a `VirtualNode` into itself via `IntoNode`.
150impl IntoNode for VirtualNode {
151    fn into_node(self) -> VirtualNode {
152        self
153    }
154}
155
156/// Wraps a `FnMut() -> VirtualNode` closure into a `DynamicNode` via `IntoNode`.
157///
158/// This enables writing `{move || html! { ... }}` directly in HTML markup
159/// without explicit `DynamicNode` construction.
160impl<F> IntoNode for F
161where
162    F: FnMut() -> VirtualNode + 'static,
163{
164    fn into_node(self) -> VirtualNode {
165        VirtualNode::Dynamic(DynamicNode {
166            render_fn: Rc::new(RefCell::new(self)),
167            hook_context: crate::reactive::create_hook_context(),
168        })
169    }
170}
171
172/// Converts a `String` into a text virtual node via `IntoNode`.
173impl IntoNode for String {
174    fn into_node(self) -> VirtualNode {
175        VirtualNode::Text(TextNode::new(self, None))
176    }
177}
178
179/// Converts a `&str` into a text virtual node via `IntoNode`.
180impl IntoNode for &str {
181    fn into_node(self) -> VirtualNode {
182        VirtualNode::Text(TextNode::new(self.to_string(), None))
183    }
184}
185
186/// Converts an `i32` into a text virtual node via `IntoNode`.
187impl IntoNode for i32 {
188    fn into_node(self) -> VirtualNode {
189        VirtualNode::Text(TextNode::new(self.to_string(), None))
190    }
191}
192
193/// Converts a `usize` into a text virtual node via `IntoNode`.
194impl IntoNode for usize {
195    fn into_node(self) -> VirtualNode {
196        VirtualNode::Text(TextNode::new(self.to_string(), None))
197    }
198}
199
200/// Converts a `bool` into a text virtual node via `IntoNode`.
201impl IntoNode for bool {
202    fn into_node(self) -> VirtualNode {
203        VirtualNode::Text(TextNode::new(self.to_string(), None))
204    }
205}
206
207/// Converts a signal into a reactive text virtual node via `IntoNode`.
208impl<T> IntoNode for Signal<T>
209where
210    T: Clone + PartialEq + std::fmt::Display + 'static,
211{
212    fn into_node(self) -> VirtualNode {
213        self.as_reactive_text()
214    }
215}
216
217/// Implementation of virtual node construction and property extraction.
218impl VirtualNode {
219    /// Creates a new element node with the given tag name.
220    pub fn get_element_node(tag_name: &str) -> Self {
221        VirtualNode::Element {
222            tag: Tag::Element(tag_name.to_string()),
223            attributes: Vec::new(),
224            children: Vec::new(),
225            key: None,
226        }
227    }
228
229    /// Creates a new text node with the given content.
230    pub fn get_text_node(content: &str) -> Self {
231        VirtualNode::Text(TextNode::new(content.to_string(), None))
232    }
233
234    /// Adds an attribute to this node if it is an element.
235    pub fn with_attribute(mut self, name: &str, value: AttributeValue) -> Self {
236        if let VirtualNode::Element {
237            ref mut attributes, ..
238        } = self
239        {
240            attributes.push(AttributeEntry::new(name.to_string(), value));
241        }
242        self
243    }
244
245    /// Adds a child node to this node if it is an element.
246    pub fn with_child(mut self, child: VirtualNode) -> Self {
247        if let VirtualNode::Element {
248            ref mut children, ..
249        } = self
250        {
251            children.push(child);
252        }
253        self
254    }
255
256    /// Returns true if this node is a component node.
257    pub fn is_component(&self) -> bool {
258        matches!(
259            self,
260            VirtualNode::Element {
261                tag: Tag::Component(_),
262                ..
263            }
264        )
265    }
266
267    /// Returns the tag name if this is an element or component node.
268    pub fn tag_name(&self) -> Option<String> {
269        match self {
270            VirtualNode::Element { tag, .. } => match tag {
271                Tag::Element(name) => Some(name.clone()),
272                Tag::Component(name) => Some(name.clone()),
273            },
274            _ => None,
275        }
276    }
277
278    /// Extracts a string property from this node if it is an element with the named attribute.
279    pub fn try_get_prop(&self, name: &Attribute) -> Option<String> {
280        let name_str: String = name.as_str();
281        if let VirtualNode::Element { attributes, .. } = self {
282            for attr in attributes {
283                if attr.get_name() == &name_str {
284                    match attr.get_value() {
285                        AttributeValue::Text(value) => return Some(value.clone()),
286                        AttributeValue::Signal(signal) => return Some(signal.get()),
287                        _ => {}
288                    }
289                }
290            }
291        }
292        None
293    }
294
295    /// Extracts a signal property from this node if it is an element with the named attribute.
296    ///
297    /// Returns the raw `Signal<String>` so components can reactively read the current value
298    /// and subscribe to future changes, rather than receiving a snapshot string.
299    pub fn try_get_signal_prop(&self, name: &Attribute) -> Option<Signal<String>> {
300        let name_str: String = name.as_str();
301        if let VirtualNode::Element { attributes, .. } = self {
302            for attr in attributes {
303                if attr.get_name() == &name_str
304                    && let AttributeValue::Signal(signal) = attr.get_value()
305                {
306                    return Some(*signal);
307                }
308            }
309        }
310        None
311    }
312
313    /// Extracts children from this node if it is an element.
314    pub fn get_children(&self) -> Vec<VirtualNode> {
315        if let VirtualNode::Element { children, .. } = self {
316            children.clone()
317        } else {
318            Vec::new()
319        }
320    }
321
322    /// Extracts text content from this node.
323    pub fn try_get_text(&self) -> Option<String> {
324        match self {
325            VirtualNode::Text(text_node) => Some(text_node.get_content().clone()),
326            VirtualNode::Element { children, .. } => {
327                children.first().and_then(VirtualNode::try_get_text)
328            }
329            _ => None,
330        }
331    }
332
333    /// Extracts an event handler from this node if it is an element with the named event attribute.
334    pub fn try_get_event(
335        &self,
336        name: &NativeEventName,
337    ) -> Option<crate::event::NativeEventHandler> {
338        let name_str: String = name.as_str();
339        if let VirtualNode::Element { attributes, .. } = self {
340            for attr in attributes {
341                if attr.get_name() == &name_str
342                    && let AttributeValue::Event(handler) = attr.get_value()
343                {
344                    return Some(handler.clone());
345                }
346            }
347        }
348        None
349    }
350
351    /// Extracts an event handler from this node by a custom attribute name.
352    pub fn try_get_callback(&self, name: &str) -> Option<crate::event::NativeEventHandler> {
353        if let VirtualNode::Element { attributes, .. } = self {
354            for attr in attributes {
355                if attr.get_name() == name
356                    && let AttributeValue::Event(handler) = attr.get_value()
357                {
358                    return Some(handler.clone());
359                }
360            }
361        }
362        None
363    }
364}
365
366/// Converts a signal into a reactive text node with listener wiring.
367impl<T> AsReactiveText for Signal<T>
368where
369    T: Clone + PartialEq + std::fmt::Display + 'static,
370{
371    fn as_reactive_text(&self) -> VirtualNode {
372        let signal: Signal<T> = *self;
373        let initial: String = signal.get().to_string();
374        let string_signal: Signal<String> = {
375            let boxed: Box<SignalInner<String>> = Box::new(SignalInner::new(initial.clone()));
376            Signal::from_inner(Box::leak(boxed) as *mut SignalInner<String>)
377        };
378        let source_signal: Signal<T> = *self;
379        let string_signal_clone: Signal<String> = string_signal;
380        source_signal.subscribe({
381            let source_signal: Signal<T> = source_signal;
382            move || {
383                let new_value: String = source_signal.get().to_string();
384                string_signal_clone.set(new_value);
385            }
386        });
387        VirtualNode::Text(TextNode::new(initial, Some(string_signal)))
388    }
389}
390
391/// Implementation of style CSS serialization.
392impl Style {
393    /// Adds a style property.
394    ///
395    /// Property names are automatically converted from snake_case to kebab-case
396    /// (e.g., `flex_direction` becomes `flex-direction`).
397    pub fn property<N, V>(mut self, name: N, value: V) -> Self
398    where
399        N: AsRef<str>,
400        V: AsRef<str>,
401    {
402        self.get_mut_properties().push(StyleProperty::new(
403            name.as_ref().replace('_', "-"),
404            value.as_ref().to_string(),
405        ));
406        self
407    }
408
409    /// Converts the style to a CSS string.
410    pub fn to_css_string(&self) -> String {
411        self.get_properties()
412            .iter()
413            .map(|p| format!("{}: {};", p.get_name(), p.get_value()))
414            .collect::<Vec<String>>()
415            .join(" ")
416    }
417}
418
419/// Provides a default empty style.
420impl Default for Style {
421    fn default() -> Self {
422        Self::new(Vec::new())
423    }
424}
425
426/// Implementation of StyleProperty construction.
427impl StyleProperty {
428    /// Creates a new style property with the given name and value.
429    pub fn new(name: String, value: String) -> Self {
430        let mut prop: StyleProperty = StyleProperty::default();
431        prop.set_name(name);
432        prop.set_value(value);
433        prop
434    }
435}
436
437/// Implementation of CssClass construction and style injection.
438impl CssClass {
439    /// Creates a new CSS class with the given name and style declarations.
440    pub fn new(name: String, style: String) -> Self {
441        let mut css_class: CssClass = CssClass::default();
442        css_class.set_name(name);
443        css_class.set_style(style);
444        css_class
445    }
446
447    /// Injects this class's styles into the DOM if not already present.
448    ///
449    /// Creates a `<style>` element with id `euv-css-injected` on first call,
450    /// then appends the class rule. Subsequent calls for the same class name
451    /// are no-ops. On first creation, also injects global CSS keyframes
452    /// required by built-in animations.
453    pub fn inject_style(&self) {
454        #[cfg(target_arch = "wasm32")]
455        {
456            let style_id: &str = "euv-css-injected";
457            let document: web_sys::Document = web_sys::window()
458                .expect("no global window exists")
459                .document()
460                .expect("no document exists");
461            let style_element: web_sys::HtmlStyleElement = match document
462                .get_element_by_id(style_id)
463            {
464                Some(el) => el.dyn_into::<web_sys::HtmlStyleElement>().unwrap(),
465                None => {
466                    let el: web_sys::HtmlStyleElement = document
467                        .create_element("style")
468                        .unwrap()
469                        .dyn_into::<web_sys::HtmlStyleElement>()
470                        .unwrap();
471                    el.set_id(style_id);
472                    let keyframes: &str = "@keyframes euv-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes euv-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes euv-scale-in { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes euv-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } } @keyframes euv-slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } }";
473                    let global: &str = "html, body, #app { height: 100%; margin: 0; padding: 0; overflow: hidden; }";
474                    el.set_inner_text(&format!("{} {}", global, keyframes));
475                    document.head().unwrap().append_child(&el).unwrap();
476                    el
477                }
478            };
479            let existing_css: String = style_element.inner_text();
480            let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
481            if !existing_css.contains(&class_rule) {
482                let new_css: String = if existing_css.is_empty() {
483                    class_rule
484                } else {
485                    format!("{}\n{}", existing_css, class_rule)
486                };
487                style_element.set_inner_text(&new_css);
488            }
489        }
490    }
491}