tachys/renderer/
dom.rs

1#![allow(missing_docs)]
2
3//! See [`Renderer`](crate::renderer::Renderer) and [`Rndr`](crate::renderer::Rndr) for additional information.
4
5use super::{CastFrom, RemoveEventHandler};
6use crate::{
7    dom::{document, window},
8    ok_or_debug, or_debug,
9    view::{Mountable, ToTemplate},
10};
11use linear_map::LinearMap;
12use once_cell::unsync::Lazy;
13use rustc_hash::FxHashSet;
14use std::{any::TypeId, borrow::Cow, cell::RefCell};
15use wasm_bindgen::{intern, prelude::Closure, JsCast, JsValue};
16use web_sys::{AddEventListenerOptions, Comment, HtmlTemplateElement};
17
18/// A [`Renderer`](crate::renderer::Renderer) that uses `web-sys` to manipulate DOM elements in the browser.
19#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct Dom;
21
22thread_local! {
23    pub(crate) static GLOBAL_EVENTS: RefCell<FxHashSet<Cow<'static, str>>> = Default::default();
24}
25
26pub type Node = web_sys::Node;
27pub type Text = web_sys::Text;
28pub type Element = web_sys::Element;
29pub type Placeholder = web_sys::Comment;
30pub type Event = wasm_bindgen::JsValue;
31pub type ClassList = web_sys::DomTokenList;
32pub type CssStyleDeclaration = web_sys::CssStyleDeclaration;
33pub type TemplateElement = web_sys::HtmlTemplateElement;
34
35impl Dom {
36    pub fn intern(text: &str) -> &str {
37        intern(text)
38    }
39
40    pub fn create_element(tag: &str, namespace: Option<&str>) -> Element {
41        if let Some(namespace) = namespace {
42            document()
43                .create_element_ns(
44                    Some(Self::intern(namespace)),
45                    Self::intern(tag),
46                )
47                .unwrap()
48        } else {
49            document().create_element(Self::intern(tag)).unwrap()
50        }
51    }
52
53    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
54    pub fn create_text_node(text: &str) -> Text {
55        document().create_text_node(text)
56    }
57
58    pub fn create_placeholder() -> Placeholder {
59        thread_local! {
60            static COMMENT: Lazy<Comment> = Lazy::new(|| {
61                document().create_comment("")
62            });
63        }
64        COMMENT.with(|n| n.clone_node().unwrap().unchecked_into())
65    }
66
67    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
68    pub fn set_text(node: &Text, text: &str) {
69        node.set_node_value(Some(text));
70    }
71
72    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
73    pub fn set_attribute(node: &Element, name: &str, value: &str) {
74        or_debug!(node.set_attribute(name, value), node, "setAttribute");
75    }
76
77    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
78    pub fn remove_attribute(node: &Element, name: &str) {
79        or_debug!(node.remove_attribute(name), node, "removeAttribute");
80    }
81
82    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
83    pub fn insert_node(
84        parent: &Element,
85        new_child: &Node,
86        anchor: Option<&Node>,
87    ) {
88        ok_or_debug!(
89            parent.insert_before(new_child, anchor),
90            parent,
91            "insertNode"
92        );
93    }
94
95    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
96    pub fn remove_node(parent: &Element, child: &Node) -> Option<Node> {
97        ok_or_debug!(parent.remove_child(child), parent, "removeNode")
98    }
99
100    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
101    pub fn remove(node: &Node) {
102        node.unchecked_ref::<Element>().remove();
103    }
104
105    pub fn get_parent(node: &Node) -> Option<Node> {
106        node.parent_node()
107    }
108
109    pub fn first_child(node: &Node) -> Option<Node> {
110        #[cfg(debug_assertions)]
111        {
112            let node = node.first_child();
113            // if it's a comment node that starts with hot-reload, it's a marker that should be
114            // ignored
115            if let Some(node) = node.as_ref() {
116                if node.node_type() == 8
117                    && node
118                        .text_content()
119                        .unwrap_or_default()
120                        .starts_with("hot-reload")
121                {
122                    return Self::next_sibling(node);
123                }
124            }
125
126            node
127        }
128        #[cfg(not(debug_assertions))]
129        {
130            node.first_child()
131        }
132    }
133
134    pub fn next_sibling(node: &Node) -> Option<Node> {
135        #[cfg(debug_assertions)]
136        {
137            let node = node.next_sibling();
138            // if it's a comment node that starts with hot-reload, it's a marker that should be
139            // ignored
140            if let Some(node) = node.as_ref() {
141                if node.node_type() == 8
142                    && node
143                        .text_content()
144                        .unwrap_or_default()
145                        .starts_with("hot-reload")
146                {
147                    return Self::next_sibling(node);
148                }
149            }
150
151            node
152        }
153        #[cfg(not(debug_assertions))]
154        {
155            node.next_sibling()
156        }
157    }
158
159    pub fn log_node(node: &Node) {
160        web_sys::console::log_1(node);
161    }
162
163    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
164    pub fn clear_children(parent: &Element) {
165        parent.set_text_content(Some(""));
166    }
167
168    /// Mounts the new child before the marker as its sibling.
169    ///
170    /// ## Panics
171    /// The default implementation panics if `before` does not have a parent [`crate::renderer::types::Element`].
172    pub fn mount_before<M>(new_child: &mut M, before: &Node)
173    where
174        M: Mountable,
175    {
176        let parent = Element::cast_from(
177            Self::get_parent(before).expect("could not find parent element"),
178        )
179        .expect("placeholder parent should be Element");
180        new_child.mount(&parent, Some(before));
181    }
182
183    /// Tries to mount the new child before the marker as its sibling.
184    ///
185    /// Returns `false` if the child did not have a valid parent.
186    #[track_caller]
187    pub fn try_mount_before<M>(new_child: &mut M, before: &Node) -> bool
188    where
189        M: Mountable,
190    {
191        if let Some(parent) =
192            Self::get_parent(before).and_then(Element::cast_from)
193        {
194            new_child.mount(&parent, Some(before));
195            true
196        } else {
197            false
198        }
199    }
200
201    pub fn set_property(el: &Element, key: &str, value: &JsValue) {
202        or_debug!(
203            js_sys::Reflect::set(
204                el,
205                &wasm_bindgen::JsValue::from_str(key),
206                value,
207            ),
208            el,
209            "setProperty"
210        );
211    }
212
213    pub fn add_event_listener(
214        el: &Element,
215        name: &str,
216        cb: Box<dyn FnMut(Event)>,
217    ) -> RemoveEventHandler<Element> {
218        let cb = wasm_bindgen::closure::Closure::wrap(cb);
219        let name = intern(name);
220        or_debug!(
221            el.add_event_listener_with_callback(
222                name,
223                cb.as_ref().unchecked_ref()
224            ),
225            el,
226            "addEventListener"
227        );
228
229        // return the remover
230        RemoveEventHandler::new({
231            let name = name.to_owned();
232            // safe to construct this here, because it will only run in the browser
233            // so it will always be accessed or dropped from the main thread
234            let cb = send_wrapper::SendWrapper::new(cb);
235            move |el: &Element| {
236                or_debug!(
237                    el.remove_event_listener_with_callback(
238                        intern(&name),
239                        cb.as_ref().unchecked_ref()
240                    ),
241                    el,
242                    "removeEventListener"
243                )
244            }
245        })
246    }
247
248    pub fn add_event_listener_use_capture(
249        el: &Element,
250        name: &str,
251        cb: Box<dyn FnMut(Event)>,
252    ) -> RemoveEventHandler<Element> {
253        let cb = wasm_bindgen::closure::Closure::wrap(cb);
254        let name = intern(name);
255        let options = AddEventListenerOptions::new();
256        options.set_capture(true);
257        or_debug!(
258            el.add_event_listener_with_callback_and_add_event_listener_options(
259                name,
260                cb.as_ref().unchecked_ref(),
261                &options
262            ),
263            el,
264            "addEventListenerUseCapture"
265        );
266
267        // return the remover
268        RemoveEventHandler::new({
269            let name = name.to_owned();
270            // safe to construct this here, because it will only run in the browser
271            // so it will always be accessed or dropped from the main thread
272            let cb = send_wrapper::SendWrapper::new(cb);
273            move |el: &Element| {
274                or_debug!(
275                    el.remove_event_listener_with_callback(
276                        intern(&name),
277                        cb.as_ref().unchecked_ref()
278                    ),
279                    el,
280                    "removeEventListener"
281                )
282            }
283        })
284    }
285
286    pub fn event_target<T>(ev: &Event) -> T
287    where
288        T: CastFrom<Element>,
289    {
290        let el = ev
291            .unchecked_ref::<web_sys::Event>()
292            .target()
293            .expect("event.target not found")
294            .unchecked_into::<Element>();
295        T::cast_from(el).expect("incorrect element type")
296    }
297
298    pub fn add_event_listener_delegated(
299        el: &Element,
300        name: Cow<'static, str>,
301        delegation_key: Cow<'static, str>,
302        cb: Box<dyn FnMut(Event)>,
303    ) -> RemoveEventHandler<Element> {
304        let cb = Closure::wrap(cb);
305        let key = intern(&delegation_key);
306        or_debug!(
307            js_sys::Reflect::set(el, &JsValue::from_str(key), cb.as_ref()),
308            el,
309            "set property"
310        );
311
312        GLOBAL_EVENTS.with(|global_events| {
313            let mut events = global_events.borrow_mut();
314            if !events.contains(&name) {
315                // create global handler
316                let key = JsValue::from_str(key);
317                let handler = move |ev: web_sys::Event| {
318                    let target = ev.target();
319                    let node = ev.composed_path().get(0);
320                    let mut node = if node.is_undefined() || node.is_null() {
321                        JsValue::from(target)
322                    } else {
323                        node
324                    };
325
326                    // TODO reverse Shadow DOM retargetting
327                    // TODO simulate currentTarget
328
329                    while !node.is_null() {
330                        let node_is_disabled = js_sys::Reflect::get(
331                            &node,
332                            &JsValue::from_str("disabled"),
333                        )
334                        .unwrap()
335                        .is_truthy();
336                        if !node_is_disabled {
337                            let maybe_handler =
338                                js_sys::Reflect::get(&node, &key).unwrap();
339                            if !maybe_handler.is_undefined() {
340                                let f = maybe_handler
341                                    .unchecked_ref::<js_sys::Function>();
342                                let _ = f.call1(&node, &ev);
343
344                                if ev.cancel_bubble() {
345                                    return;
346                                }
347                            }
348                        }
349
350                        // navigate up tree
351                        if let Some(parent) =
352                            node.unchecked_ref::<web_sys::Node>().parent_node()
353                        {
354                            node = parent.into()
355                        } else if let Some(root) =
356                            node.dyn_ref::<web_sys::ShadowRoot>()
357                        {
358                            node = root.host().unchecked_into();
359                        } else {
360                            node = JsValue::null()
361                        }
362                    }
363                };
364
365                let handler =
366                    Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
367                let handler = Closure::wrap(handler).into_js_value();
368                window()
369                    .add_event_listener_with_callback(
370                        &name,
371                        handler.unchecked_ref(),
372                    )
373                    .unwrap();
374
375                // register that we've created handler
376                events.insert(name);
377            }
378        });
379
380        // return the remover
381        RemoveEventHandler::new({
382            let key = key.to_owned();
383            // safe to construct this here, because it will only run in the browser
384            // so it will always be accessed or dropped from the main thread
385            let cb = send_wrapper::SendWrapper::new(cb);
386            move |el: &Element| {
387                drop(cb.take());
388                or_debug!(
389                    js_sys::Reflect::delete_property(
390                        el,
391                        &JsValue::from_str(&key)
392                    ),
393                    el,
394                    "delete property"
395                );
396            }
397        })
398    }
399
400    pub fn class_list(el: &Element) -> ClassList {
401        el.class_list()
402    }
403
404    pub fn add_class(list: &ClassList, name: &str) {
405        or_debug!(list.add_1(name), list.unchecked_ref(), "add()");
406    }
407
408    pub fn remove_class(list: &ClassList, name: &str) {
409        or_debug!(list.remove_1(name), list.unchecked_ref(), "remove()");
410    }
411
412    pub fn style(el: &Element) -> CssStyleDeclaration {
413        el.unchecked_ref::<web_sys::HtmlElement>().style()
414    }
415
416    pub fn set_css_property(
417        style: &CssStyleDeclaration,
418        name: &str,
419        value: &str,
420    ) {
421        or_debug!(
422            style.set_property(name, value),
423            style.unchecked_ref(),
424            "setProperty"
425        );
426    }
427
428    pub fn remove_css_property(style: &CssStyleDeclaration, name: &str) {
429        or_debug!(
430            style.remove_property(name),
431            style.unchecked_ref(),
432            "removeProperty"
433        );
434    }
435
436    pub fn set_inner_html(el: &Element, html: &str) {
437        el.set_inner_html(html);
438    }
439
440    pub fn get_template<V>() -> TemplateElement
441    where
442        V: ToTemplate + 'static,
443    {
444        thread_local! {
445            static TEMPLATE_ELEMENT: Lazy<HtmlTemplateElement> =
446                Lazy::new(|| document().create_element("template").unwrap().unchecked_into());
447            static TEMPLATES: RefCell<LinearMap<TypeId, HtmlTemplateElement>> = Default::default();
448        }
449
450        TEMPLATES.with(|t| {
451            t.borrow_mut()
452                .entry(TypeId::of::<V>())
453                .or_insert_with(|| {
454                    let tpl = TEMPLATE_ELEMENT.with(|t| {
455                        t.clone_node()
456                            .unwrap()
457                            .unchecked_into::<HtmlTemplateElement>()
458                    });
459                    let mut buf = String::new();
460                    V::to_template(
461                        &mut buf,
462                        &mut String::new(),
463                        &mut String::new(),
464                        &mut String::new(),
465                        &mut Default::default(),
466                    );
467                    tpl.set_inner_html(&buf);
468                    tpl
469                })
470                .clone()
471        })
472    }
473
474    pub fn clone_template(tpl: &TemplateElement) -> Element {
475        tpl.content()
476            .clone_node_with_deep(true)
477            .unwrap()
478            .unchecked_into()
479    }
480
481    pub fn create_element_from_html(html: &str) -> Element {
482        // TODO can be optimized to cache HTML strings or cache <template>?
483        let tpl = document().create_element("template").unwrap();
484        tpl.set_inner_html(html);
485        let tpl = Self::clone_template(tpl.unchecked_ref());
486        tpl.first_element_child().unwrap_or(tpl)
487    }
488}
489
490impl Mountable for Node {
491    fn unmount(&mut self) {
492        todo!()
493    }
494
495    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
496        Dom::insert_node(parent, self, marker);
497    }
498
499    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
500        let parent = Dom::get_parent(self).and_then(Element::cast_from);
501        if let Some(parent) = parent {
502            child.mount(&parent, Some(self));
503            return true;
504        }
505        false
506    }
507}
508
509impl Mountable for Text {
510    fn unmount(&mut self) {
511        self.remove();
512    }
513
514    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
515        Dom::insert_node(parent, self, marker);
516    }
517
518    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
519        let parent =
520            Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
521        if let Some(parent) = parent {
522            child.mount(&parent, Some(self));
523            return true;
524        }
525        false
526    }
527}
528
529impl Mountable for Comment {
530    fn unmount(&mut self) {
531        self.remove();
532    }
533
534    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
535        Dom::insert_node(parent, self, marker);
536    }
537
538    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
539        let parent =
540            Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
541        if let Some(parent) = parent {
542            child.mount(&parent, Some(self));
543            return true;
544        }
545        false
546    }
547}
548
549impl Mountable for Element {
550    fn unmount(&mut self) {
551        self.remove();
552    }
553
554    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
555        Dom::insert_node(parent, self, marker);
556    }
557
558    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
559        let parent =
560            Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
561        if let Some(parent) = parent {
562            child.mount(&parent, Some(self));
563            return true;
564        }
565        false
566    }
567}
568
569impl CastFrom<Node> for Text {
570    fn cast_from(node: Node) -> Option<Text> {
571        node.clone().dyn_into().ok()
572    }
573}
574
575impl CastFrom<Node> for Comment {
576    fn cast_from(node: Node) -> Option<Comment> {
577        node.clone().dyn_into().ok()
578    }
579}
580
581impl CastFrom<Node> for Element {
582    fn cast_from(node: Node) -> Option<Element> {
583        node.clone().dyn_into().ok()
584    }
585}
586
587impl<T> CastFrom<JsValue> for T
588where
589    T: JsCast,
590{
591    fn cast_from(source: JsValue) -> Option<Self> {
592        source.dyn_into::<T>().ok()
593    }
594}
595
596impl<T> CastFrom<Element> for T
597where
598    T: JsCast,
599{
600    fn cast_from(source: Element) -> Option<Self> {
601        source.dyn_into::<T>().ok()
602    }
603}