silkenweb/dom/dry/
shared_element.rs

1use std::{collections::HashMap, convert::identity, fmt};
2
3use caseless::default_caseless_match_str;
4use html_escape::encode_double_quoted_attribute;
5use indexmap::IndexMap;
6use itertools::Itertools;
7use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
8
9use super::{DryChild, LazyElementAction};
10use crate::{
11    clone,
12    dom::{
13        hydro::HydroNode,
14        private::{DomElement, EventStore, InstantiableDomElement},
15        wet::{WetElement, WetNode},
16    },
17    hydration::HydrationStats,
18    node::element::Namespace,
19    HEAD_ID_ATTRIBUTE,
20};
21
22pub struct SharedDryElement<Node> {
23    namespace: Namespace,
24    tag: String,
25    attributes: IndexMap<String, String>,
26    styles: IndexMap<String, String>,
27    children: Vec<Node>,
28    shadow_children: Vec<Node>,
29    hydrate_actions: Vec<LazyElementAction>,
30    next_sibling: Option<Node>,
31}
32
33impl<Node> SharedDryElement<Node> {
34    fn style_prop_text(&self) -> Option<String> {
35        if self.styles.is_empty() {
36            return None;
37        }
38
39        debug_assert!(!self.attributes.contains_key(STYLE_ATTR));
40
41        Some(
42            self.styles
43                .iter()
44                .map(|(name, value)| format!("{name}: {value};"))
45                .join(" "),
46        )
47    }
48}
49
50impl<Node: DryChild> SharedDryElement<Node> {
51    pub fn new(namespace: Namespace, tag: &str) -> Self {
52        Self {
53            namespace,
54            tag: tag.to_owned(),
55            attributes: IndexMap::new(),
56            styles: IndexMap::new(),
57            children: Vec::new(),
58            shadow_children: Vec::new(),
59            hydrate_actions: Vec::new(),
60            next_sibling: None,
61        }
62    }
63
64    pub fn first_child(&self) -> Option<&Node> {
65        self.children.first()
66    }
67
68    pub fn next_sibling(&self) -> Option<&Node> {
69        self.next_sibling.as_ref()
70    }
71
72    pub fn set_next_sibling(&mut self, next_sibling: Option<Node>) {
73        self.next_sibling = next_sibling;
74    }
75
76    pub fn append_child(&mut self, child: &Node) {
77        if let Some(last) = self.children.last_mut() {
78            last.set_next_sibling(Some(child));
79        }
80
81        self.children.push(child.clone());
82    }
83
84    pub fn insert_child_before(&mut self, index: usize, child: &Node, next_child: Option<&Node>) {
85        if index > 0 {
86            self.children[index - 1].set_next_sibling(Some(child));
87        }
88
89        child.set_next_sibling(next_child);
90
91        self.children.insert(index, child.clone());
92    }
93
94    pub fn replace_child(&mut self, index: usize, new_child: &Node, old_child: &Node) {
95        old_child.set_next_sibling(None);
96
97        if index > 0 {
98            self.children[index - 1].set_next_sibling(Some(new_child));
99        }
100
101        new_child.set_next_sibling(self.children.get(index + 1));
102
103        self.children[index] = new_child.clone();
104    }
105
106    pub fn remove_child(&mut self, index: usize, child: &Node) {
107        child.set_next_sibling(None);
108        if index > 0 {
109            self.children[index - 1].set_next_sibling(self.children.get(index + 1));
110        }
111
112        self.children.remove(index);
113    }
114
115    pub fn clear_children(&mut self) {
116        for child in &self.children {
117            child.set_next_sibling(None);
118        }
119
120        self.children.clear();
121    }
122
123    pub fn attach_shadow_children(&mut self, children: impl IntoIterator<Item = Node>) {
124        for child in children {
125            if let Some(previous_child) = self.shadow_children.last_mut() {
126                previous_child.set_next_sibling(Some(&child));
127            }
128
129            self.shadow_children.push(child);
130        }
131    }
132
133    pub fn add_class(&mut self, name: &str) {
134        self.attributes
135            .entry("class".to_owned())
136            .and_modify(|class| {
137                if !class.split_ascii_whitespace().any(|c| c == name) {
138                    if !class.is_empty() {
139                        class.push(' ');
140                    }
141
142                    class.push_str(name);
143                }
144            })
145            .or_insert_with(|| name.to_owned());
146    }
147
148    pub fn remove_class(&mut self, name: &str) {
149        if let Some(class) = self.attributes.get_mut("class") {
150            *class = class
151                .split_ascii_whitespace()
152                .filter(|&c| c != name)
153                .join(" ");
154        }
155    }
156
157    pub fn attribute<A>(&mut self, name: &str, value: A)
158    where
159        A: crate::attribute::Attribute,
160    {
161        assert_ne!(
162            name, "xmlns",
163            "\"xmlns\" must be set via a namespace at tag creation time"
164        );
165
166        if let Some(value) = value.text() {
167            self.attributes.insert(name.to_owned(), value.to_string());
168        } else {
169            self.attributes.shift_remove(name);
170        }
171    }
172
173    pub fn on(
174        &mut self,
175        name: &'static str,
176        f: impl FnMut(JsValue) + 'static,
177        events: &EventStore,
178    ) {
179        clone!(mut events);
180
181        self.hydrate_actions
182            .push(Box::new(move |element| element.on(name, f, &mut events)))
183    }
184
185    pub fn try_dom_element(&self) -> Option<web_sys::Element> {
186        None
187    }
188
189    pub fn style_property(&mut self, name: &str, value: &str) {
190        self.styles.insert(name.to_owned(), value.to_owned());
191    }
192
193    pub fn effect(&mut self, f: impl FnOnce(&web_sys::Element) + 'static) {
194        self.hydrate_actions
195            .push(Box::new(move |element| element.effect(f)))
196    }
197
198    pub fn observe_attributes(
199        &mut self,
200        f: impl FnMut(js_sys::Array, web_sys::MutationObserver) + 'static,
201        events: &EventStore,
202    ) {
203        clone!(mut events);
204
205        self.hydrate_actions.push(Box::new(move |element| {
206            element.observe_attributes(f, &mut events)
207        }))
208    }
209
210    pub fn clone_node(&self) -> Self {
211        Self {
212            namespace: self.namespace.clone(),
213            tag: self.tag.clone(),
214            attributes: self.attributes.clone(),
215            styles: self.styles.clone(),
216            children: Self::clone_children(&self.children),
217            shadow_children: Self::clone_children(&self.shadow_children),
218            hydrate_actions: Vec::new(),
219            next_sibling: None,
220        }
221    }
222
223    fn clone_children(children: &[Node]) -> Vec<Node> {
224        let children: Vec<Node> = children.iter().map(Node::clone_node).collect();
225
226        for (index, child) in children.iter().enumerate() {
227            child.set_next_sibling(children.get(index + 1));
228        }
229
230        children
231    }
232}
233
234impl SharedDryElement<HydroNode> {
235    pub fn hydrate_child(
236        self,
237        parent: &web_sys::Node,
238        skip_filtered: &impl Fn(Option<web_sys::Node>) -> Option<web_sys::Node>,
239        child: &web_sys::Node,
240        tracker: &mut HydrationStats,
241    ) -> WetElement {
242        clone!(mut child);
243
244        loop {
245            if let Some(elem_child) = child.dyn_ref::<web_sys::Element>() {
246                let dom_namespace = elem_child.namespace_uri().unwrap_or_default();
247                let dry_namespace = self.namespace.as_str();
248
249                if dry_namespace == dom_namespace
250                    && default_caseless_match_str(&elem_child.tag_name(), &self.tag)
251                {
252                    return self.hydrate_element(elem_child, tracker);
253                }
254            }
255
256            let next = skip_filtered(child.next_sibling());
257            tracker.node_removed(&child);
258            parent.remove_child(&child).unwrap_throw();
259
260            if let Some(next_child) = next {
261                child = next_child;
262            } else {
263                break;
264            }
265        }
266
267        let wet_child: WetElement = self.into();
268        let new_element = wet_child.dom_element();
269        parent.append_child(&new_element).unwrap_throw();
270        tracker.node_added(&new_element);
271
272        wet_child
273    }
274
275    pub fn hydrate(self, dom_elem: &web_sys::Element, tracker: &mut HydrationStats) -> WetElement {
276        let existing_namespace = dom_elem.namespace_uri().unwrap_or_default();
277        let new_namespace = &self.namespace;
278        let new_tag = &self.tag;
279
280        if new_namespace.as_str() == existing_namespace
281            && default_caseless_match_str(&dom_elem.tag_name(), new_tag)
282        {
283            self.hydrate_element(dom_elem, tracker)
284        } else {
285            let new_dom_elem = new_namespace.create_element(new_tag);
286
287            while let Some(child) = dom_elem.first_child() {
288                new_dom_elem.append_child(&child).unwrap_throw();
289            }
290
291            dom_elem
292                .replace_with_with_node_1(&new_dom_elem)
293                .unwrap_throw();
294            self.hydrate_element(&new_dom_elem, tracker)
295        }
296    }
297
298    pub fn hydrate_in_head(self, head: &WetElement, id: &str, tracker: &mut HydrationStats) {
299        let id = id.to_string();
300        let skip_filtered = move |mut node: Option<web_sys::Node>| {
301            while let Some(current) = node {
302                if current
303                    .dyn_ref::<web_sys::Element>()
304                    .is_some_and(|elem| elem.get_attribute(HEAD_ID_ATTRIBUTE).as_ref() == Some(&id))
305                {
306                    return Some(current);
307                }
308
309                node = current.next_sibling();
310            }
311
312            None
313        };
314
315        Self::hydrate_children(&head.dom_element(), &skip_filtered, self.children, tracker);
316    }
317
318    fn hydrate_element(
319        self,
320        dom_elem: &web_sys::Element,
321        tracker: &mut HydrationStats,
322    ) -> WetElement {
323        self.reconcile_attributes(dom_elem, tracker);
324        let mut elem = WetElement::from_element(dom_elem.clone());
325
326        Self::hydrate_children(dom_elem, &identity, self.children, tracker);
327
328        if !self.shadow_children.is_empty() {
329            let shadow_root = elem.create_shadow_root();
330            Self::hydrate_children(&shadow_root, &identity, self.shadow_children, tracker);
331        }
332
333        for event in self.hydrate_actions {
334            event(&mut elem);
335        }
336
337        elem
338    }
339
340    fn hydrate_children(
341        dom_elem: &web_sys::Node,
342        skip_filtered: &impl Fn(Option<web_sys::Node>) -> Option<web_sys::Node>,
343        children: impl IntoIterator<Item = HydroNode>,
344        tracker: &mut HydrationStats,
345    ) {
346        let mut children = children.into_iter();
347        let mut current_child = skip_filtered(dom_elem.first_child());
348
349        for child in children.by_ref() {
350            if let Some(node) = &current_child {
351                let hydrated_elem = child.hydrate_child(dom_elem, skip_filtered, node, tracker);
352                current_child = skip_filtered(hydrated_elem.dom_node().next_sibling());
353            } else {
354                Self::hydrate_with_new(dom_elem, child, tracker);
355                break;
356            }
357        }
358
359        for child in children {
360            Self::hydrate_with_new(dom_elem, child, tracker);
361        }
362
363        Self::remove_children_from(dom_elem, skip_filtered, current_child, tracker);
364    }
365
366    /// Remove `child` and all siblings after `child`
367    fn remove_children_from(
368        parent: &web_sys::Node,
369        skip_filtered: &impl Fn(Option<web_sys::Node>) -> Option<web_sys::Node>,
370        mut child: Option<web_sys::Node>,
371        tracker: &mut HydrationStats,
372    ) {
373        while let Some(node) = child {
374            let next_child = skip_filtered(node.next_sibling());
375            tracker.node_removed(&node);
376            parent.remove_child(&node).unwrap_throw();
377            child = next_child;
378        }
379    }
380
381    fn hydrate_with_new(parent: &web_sys::Node, child: HydroNode, tracker: &mut HydrationStats) {
382        let child = WetNode::from(child);
383        let new_child = child.dom_node();
384        parent.append_child(new_child).unwrap_throw();
385        tracker.node_added(new_child);
386    }
387
388    fn reconcile_attributes(&self, dom_elem: &web_sys::Element, tracker: &mut HydrationStats) {
389        let dom_attributes = dom_elem.attributes();
390        let mut dom_attr_map = HashMap::new();
391
392        for item_index in 0.. {
393            if let Some(attr) = dom_attributes.item(item_index) {
394                dom_attr_map.insert(attr.name(), attr.value());
395            } else {
396                break;
397            }
398        }
399
400        for (name, value) in &self.attributes {
401            Self::set_attribute(&mut dom_attr_map, name, value, dom_elem, tracker);
402        }
403
404        if let Some(style) = self.style_prop_text() {
405            Self::set_attribute(&mut dom_attr_map, STYLE_ATTR, &style, dom_elem, tracker)
406        }
407
408        for name in dom_attr_map.into_keys() {
409            if !name.starts_with("data-silkenweb") {
410                tracker.attribute_removed(dom_elem, &name);
411                dom_elem.remove_attribute(&name).unwrap_throw();
412            }
413        }
414    }
415
416    fn set_attribute(
417        dom_attr_map: &mut HashMap<String, String>,
418        name: &str,
419        value: &str,
420        dom_elem: &web_sys::Element,
421        tracker: &mut HydrationStats,
422    ) {
423        let set_attr = if let Some(existing_value) = dom_attr_map.remove(name) {
424            value != existing_value
425        } else {
426            true
427        };
428
429        if set_attr {
430            dom_elem.set_attribute(name, value).unwrap_throw();
431            tracker.attribute_set(dom_elem, name, value);
432        }
433    }
434}
435
436impl<Node: fmt::Display> SharedDryElement<Node> {
437    #[cfg(feature = "declarative-shadow-dom")]
438    fn write_shadow_dom(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439        if self.shadow_children.is_empty() {
440            return Ok(());
441        }
442
443        f.write_str(r#"<template shadowroot="open">"#)?;
444
445        for child in &self.shadow_children {
446            child.fmt(f)?;
447        }
448
449        f.write_str("</template>")?;
450
451        Ok(())
452    }
453
454    #[cfg(not(feature = "declarative-shadow-dom"))]
455    fn write_shadow_dom(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
456        Ok(())
457    }
458}
459
460impl<Node: fmt::Display> fmt::Display for SharedDryElement<Node> {
461    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462        write!(f, "<{}", self.tag)?;
463
464        for (name, value) in &self.attributes {
465            fmt_attr(f, name, value)?;
466        }
467
468        if let Some(style) = self.style_prop_text() {
469            fmt_attr(f, STYLE_ATTR, &style)?;
470        }
471
472        f.write_str(">")?;
473
474        self.write_shadow_dom(f)?;
475
476        for child in &self.children {
477            child.fmt(f)?;
478        }
479
480        let has_children = !self.children.is_empty();
481        let requires_closing_tag = !NO_CLOSING_TAG.contains(&self.tag.as_str());
482
483        if requires_closing_tag || has_children {
484            write!(f, "</{}>", self.tag)?;
485        }
486
487        Ok(())
488    }
489}
490
491fn fmt_attr(f: &mut fmt::Formatter, name: &str, value: &str) -> Result<(), fmt::Error> {
492    write!(f, " {}=\"{}\"", name, encode_double_quoted_attribute(value))?;
493    Ok(())
494}
495
496impl<Node: Into<WetNode>> From<SharedDryElement<Node>> for WetElement {
497    fn from(dry: SharedDryElement<Node>) -> Self {
498        let mut wet = WetElement::new(&dry.namespace, &dry.tag);
499
500        if let Some(style) = dry.style_prop_text() {
501            wet.attribute(STYLE_ATTR, &style);
502        }
503
504        for (name, value) in dry.attributes {
505            wet.attribute(&name, value);
506        }
507
508        for child in dry.children {
509            wet.append_child(&child.into());
510        }
511
512        if !dry.shadow_children.is_empty() {
513            wet.attach_shadow_children(dry.shadow_children.into_iter().map(|child| child.into()));
514        }
515
516        for action in dry.hydrate_actions {
517            action(&mut wet);
518        }
519
520        wet
521    }
522}
523
524const STYLE_ATTR: &str = "style";
525
526const NO_CLOSING_TAG: &[&str] = &[
527    "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
528    "source", "track", "wbr",
529];