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 rustc_hash::FxHashSet;
13use std::{
14    any::TypeId,
15    borrow::Cow,
16    cell::{LazyCell, RefCell},
17};
18use wasm_bindgen::{intern, prelude::Closure, JsCast, JsValue};
19use web_sys::{AddEventListenerOptions, Comment, HtmlTemplateElement};
20
21/// A [`Renderer`](crate::renderer::Renderer) that uses `web-sys` to manipulate DOM elements in the browser.
22#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub struct Dom;
24
25thread_local! {
26    pub(crate) static GLOBAL_EVENTS: RefCell<FxHashSet<Cow<'static, str>>> = Default::default();
27    pub static TEMPLATE_CACHE: RefCell<Vec<(Cow<'static, str>, web_sys::Element)>> = Default::default();
28}
29
30pub type Node = web_sys::Node;
31pub type Text = web_sys::Text;
32pub type Element = web_sys::Element;
33pub type Placeholder = web_sys::Comment;
34pub type Event = wasm_bindgen::JsValue;
35pub type ClassList = web_sys::DomTokenList;
36pub type CssStyleDeclaration = web_sys::CssStyleDeclaration;
37pub type TemplateElement = web_sys::HtmlTemplateElement;
38
39/// A microtask is a short function which will run after the current task has
40/// completed its work and when there is no other code waiting to be run before
41/// control of the execution context is returned to the browser's event loop.
42///
43/// Microtasks are especially useful for libraries and frameworks that need
44/// to perform final cleanup or other just-before-rendering tasks.
45///
46/// [MDN queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask)
47pub fn queue_microtask(task: impl FnOnce() + 'static) {
48    use js_sys::{Function, Reflect};
49
50    let task = Closure::once_into_js(task);
51    let window = window();
52    let queue_microtask =
53        Reflect::get(&window, &JsValue::from_str("queueMicrotask"))
54            .expect("queueMicrotask not available");
55    let queue_microtask = queue_microtask.unchecked_into::<Function>();
56    _ = queue_microtask.call1(&JsValue::UNDEFINED, &task);
57}
58
59fn queue(fun: Box<dyn FnOnce()>) {
60    use std::cell::{Cell, RefCell};
61
62    thread_local! {
63        static PENDING: Cell<bool> = const { Cell::new(false) };
64        static QUEUE: RefCell<Vec<Box<dyn FnOnce()>>> = RefCell::new(Vec::new());
65    }
66
67    QUEUE.with_borrow_mut(|q| q.push(fun));
68    if !PENDING.replace(true) {
69        queue_microtask(|| {
70            let tasks = QUEUE.take();
71            for task in tasks {
72                task();
73            }
74            PENDING.set(false);
75        })
76    }
77}
78
79impl Dom {
80    pub fn intern(text: &str) -> &str {
81        intern(text)
82    }
83
84    pub fn create_element(tag: &str, namespace: Option<&str>) -> Element {
85        if let Some(namespace) = namespace {
86            document()
87                .create_element_ns(
88                    Some(Self::intern(namespace)),
89                    Self::intern(tag),
90                )
91                .unwrap()
92        } else {
93            document().create_element(Self::intern(tag)).unwrap()
94        }
95    }
96
97    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
98    pub fn create_text_node(text: &str) -> Text {
99        document().create_text_node(text)
100    }
101
102    pub fn create_placeholder() -> Placeholder {
103        thread_local! {
104            static COMMENT: LazyCell<Comment> = LazyCell::new(|| {
105                document().create_comment("")
106            });
107        }
108        COMMENT.with(|n| n.clone_node().unwrap().unchecked_into())
109    }
110
111    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
112    pub fn set_text(node: &Text, text: &str) {
113        node.set_node_value(Some(text));
114    }
115
116    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
117    pub fn set_attribute(node: &Element, name: &str, value: &str) {
118        or_debug!(node.set_attribute(name, value), node, "setAttribute");
119    }
120
121    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
122    pub fn remove_attribute(node: &Element, name: &str) {
123        or_debug!(node.remove_attribute(name), node, "removeAttribute");
124    }
125
126    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
127    pub fn insert_node(
128        parent: &Element,
129        new_child: &Node,
130        anchor: Option<&Node>,
131    ) {
132        ok_or_debug!(
133            parent.insert_before(new_child, anchor),
134            parent,
135            "insertNode"
136        );
137    }
138
139    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
140    pub fn try_insert_node(
141        parent: &Element,
142        new_child: &Node,
143        anchor: Option<&Node>,
144    ) -> bool {
145        parent.insert_before(new_child, anchor).is_ok()
146    }
147
148    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
149    pub fn remove_node(parent: &Element, child: &Node) -> Option<Node> {
150        ok_or_debug!(parent.remove_child(child), parent, "removeNode")
151    }
152
153    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
154    pub fn remove(node: &Node) {
155        node.unchecked_ref::<Element>().remove();
156    }
157
158    pub fn get_parent(node: &Node) -> Option<Node> {
159        node.parent_node()
160    }
161
162    pub fn first_child(node: &Node) -> Option<Node> {
163        #[cfg(debug_assertions)]
164        {
165            let node = node.first_child();
166            // if it's a comment node that starts with hot-reload, it's a marker that should be
167            // ignored
168            if let Some(node) = node.as_ref() {
169                if node.node_type() == 8
170                    && node
171                        .text_content()
172                        .unwrap_or_default()
173                        .starts_with("hot-reload")
174                {
175                    return Self::next_sibling(node);
176                }
177            }
178
179            node
180        }
181        #[cfg(not(debug_assertions))]
182        {
183            node.first_child()
184        }
185    }
186
187    pub fn next_sibling(node: &Node) -> Option<Node> {
188        #[cfg(debug_assertions)]
189        {
190            let node = node.next_sibling();
191            // if it's a comment node that starts with hot-reload, it's a marker that should be
192            // ignored
193            if let Some(node) = node.as_ref() {
194                if node.node_type() == 8
195                    && node
196                        .text_content()
197                        .unwrap_or_default()
198                        .starts_with("hot-reload")
199                {
200                    return Self::next_sibling(node);
201                }
202            }
203
204            node
205        }
206        #[cfg(not(debug_assertions))]
207        {
208            node.next_sibling()
209        }
210    }
211
212    pub fn log_node(node: &Node) {
213        web_sys::console::log_1(node);
214    }
215
216    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
217    pub fn clear_children(parent: &Element) {
218        parent.set_text_content(Some(""));
219    }
220
221    /// Mounts the new child before the marker as its sibling.
222    ///
223    /// ## Panics
224    /// The default implementation panics if `before` does not have a parent [`crate::renderer::types::Element`].
225    pub fn mount_before<M>(new_child: &mut M, before: &Node)
226    where
227        M: Mountable,
228    {
229        let parent = Element::cast_from(
230            Self::get_parent(before).expect("could not find parent element"),
231        )
232        .expect("placeholder parent should be Element");
233        new_child.mount(&parent, Some(before));
234    }
235
236    /// Tries to mount the new child before the marker as its sibling.
237    ///
238    /// Returns `false` if the child did not have a valid parent.
239    #[track_caller]
240    pub fn try_mount_before<M>(new_child: &mut M, before: &Node) -> bool
241    where
242        M: Mountable,
243    {
244        if let Some(parent) =
245            Self::get_parent(before).and_then(Element::cast_from)
246        {
247            new_child.mount(&parent, Some(before));
248            true
249        } else {
250            false
251        }
252    }
253
254    pub fn set_property_or_value(el: &Element, key: &str, value: &JsValue) {
255        if key == "value" {
256            queue(Box::new({
257                let el = el.clone();
258                let value = value.clone();
259                move || {
260                    Self::set_property(&el, "value", &value);
261                }
262            }))
263        } else {
264            Self::set_property(el, key, value);
265        }
266    }
267
268    pub fn set_property(el: &Element, key: &str, value: &JsValue) {
269        or_debug!(
270            js_sys::Reflect::set(
271                el,
272                &wasm_bindgen::JsValue::from_str(key),
273                value,
274            ),
275            el,
276            "setProperty"
277        );
278    }
279
280    pub fn add_event_listener(
281        el: &Element,
282        name: &str,
283        cb: Box<dyn FnMut(Event)>,
284    ) -> RemoveEventHandler<Element> {
285        let cb = wasm_bindgen::closure::Closure::wrap(cb);
286        let name = intern(name);
287        or_debug!(
288            el.add_event_listener_with_callback(
289                name,
290                cb.as_ref().unchecked_ref()
291            ),
292            el,
293            "addEventListener"
294        );
295
296        // return the remover
297        RemoveEventHandler::new({
298            let name = name.to_owned();
299            // safe to construct this here, because it will only run in the browser
300            // so it will always be accessed or dropped from the main thread
301            let cb = send_wrapper::SendWrapper::new(cb);
302            move |el: &Element| {
303                or_debug!(
304                    el.remove_event_listener_with_callback(
305                        intern(&name),
306                        cb.as_ref().unchecked_ref()
307                    ),
308                    el,
309                    "removeEventListener"
310                )
311            }
312        })
313    }
314
315    pub fn add_event_listener_use_capture(
316        el: &Element,
317        name: &str,
318        cb: Box<dyn FnMut(Event)>,
319    ) -> RemoveEventHandler<Element> {
320        let cb = wasm_bindgen::closure::Closure::wrap(cb);
321        let name = intern(name);
322        let options = AddEventListenerOptions::new();
323        options.set_capture(true);
324        or_debug!(
325            el.add_event_listener_with_callback_and_add_event_listener_options(
326                name,
327                cb.as_ref().unchecked_ref(),
328                &options
329            ),
330            el,
331            "addEventListenerUseCapture"
332        );
333
334        // return the remover
335        RemoveEventHandler::new({
336            let name = name.to_owned();
337            // safe to construct this here, because it will only run in the browser
338            // so it will always be accessed or dropped from the main thread
339            let cb = send_wrapper::SendWrapper::new(cb);
340            move |el: &Element| {
341                or_debug!(
342                    el.remove_event_listener_with_callback_and_bool(
343                        intern(&name),
344                        cb.as_ref().unchecked_ref(),
345                        true
346                    ),
347                    el,
348                    "removeEventListener"
349                )
350            }
351        })
352    }
353
354    pub fn event_target<T>(ev: &Event) -> T
355    where
356        T: CastFrom<Element>,
357    {
358        let el = ev
359            .unchecked_ref::<web_sys::Event>()
360            .target()
361            .expect("event.target not found")
362            .unchecked_into::<Element>();
363        T::cast_from(el).expect("incorrect element type")
364    }
365
366    pub fn add_event_listener_delegated(
367        el: &Element,
368        name: Cow<'static, str>,
369        delegation_key: Cow<'static, str>,
370        cb: Box<dyn FnMut(Event)>,
371    ) -> RemoveEventHandler<Element> {
372        let cb = Closure::wrap(cb);
373        let key = intern(&delegation_key);
374        or_debug!(
375            js_sys::Reflect::set(el, &JsValue::from_str(key), cb.as_ref()),
376            el,
377            "set property"
378        );
379
380        GLOBAL_EVENTS.with(|global_events| {
381            let mut events = global_events.borrow_mut();
382            if !events.contains(&name) {
383                // create global handler
384                let key = JsValue::from_str(key);
385                let handler = move |ev: web_sys::Event| {
386                    let target = ev.target();
387                    let node = ev.composed_path().get(0);
388                    let mut node = if node.is_undefined() || node.is_null() {
389                        JsValue::from(target)
390                    } else {
391                        node
392                    };
393
394                    // TODO reverse Shadow DOM retargetting
395                    // TODO simulate currentTarget
396
397                    while !node.is_null() {
398                        let node_is_disabled = js_sys::Reflect::get(
399                            &node,
400                            &JsValue::from_str("disabled"),
401                        )
402                        .unwrap()
403                        .is_truthy();
404                        if !node_is_disabled {
405                            let maybe_handler =
406                                js_sys::Reflect::get(&node, &key).unwrap();
407                            if !maybe_handler.is_undefined() {
408                                let f = maybe_handler
409                                    .unchecked_ref::<js_sys::Function>();
410                                let _ = f.call1(&node, &ev);
411
412                                if ev.cancel_bubble() {
413                                    return;
414                                }
415                            }
416                        }
417
418                        // navigate up tree
419                        if let Some(parent) =
420                            node.unchecked_ref::<web_sys::Node>().parent_node()
421                        {
422                            node = parent.into()
423                        } else if let Some(root) =
424                            node.dyn_ref::<web_sys::ShadowRoot>()
425                        {
426                            node = root.host().unchecked_into();
427                        } else {
428                            node = JsValue::null()
429                        }
430                    }
431                };
432
433                let handler =
434                    Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
435                let handler = Closure::wrap(handler).into_js_value();
436                window()
437                    .add_event_listener_with_callback(
438                        &name,
439                        handler.unchecked_ref(),
440                    )
441                    .unwrap();
442
443                // register that we've created handler
444                events.insert(name);
445            }
446        });
447
448        // return the remover
449        RemoveEventHandler::new({
450            let key = key.to_owned();
451            // safe to construct this here, because it will only run in the browser
452            // so it will always be accessed or dropped from the main thread
453            let cb = send_wrapper::SendWrapper::new(cb);
454            move |el: &Element| {
455                drop(cb.take());
456                or_debug!(
457                    js_sys::Reflect::delete_property(
458                        el,
459                        &JsValue::from_str(&key)
460                    ),
461                    el,
462                    "delete property"
463                );
464            }
465        })
466    }
467
468    pub fn class_list(el: &Element) -> ClassList {
469        el.class_list()
470    }
471
472    pub fn add_class(list: &ClassList, name: &str) {
473        or_debug!(list.add_1(name), list.unchecked_ref(), "add()");
474    }
475
476    pub fn remove_class(list: &ClassList, name: &str) {
477        or_debug!(list.remove_1(name), list.unchecked_ref(), "remove()");
478    }
479
480    pub fn style(el: &Element) -> CssStyleDeclaration {
481        el.unchecked_ref::<web_sys::HtmlElement>().style()
482    }
483
484    pub fn set_css_property(
485        style: &CssStyleDeclaration,
486        name: &str,
487        value: &str,
488    ) {
489        or_debug!(
490            style.set_property(name, value),
491            style.unchecked_ref(),
492            "setProperty"
493        );
494    }
495
496    pub fn remove_css_property(style: &CssStyleDeclaration, name: &str) {
497        or_debug!(
498            style.remove_property(name),
499            style.unchecked_ref(),
500            "removeProperty"
501        );
502    }
503
504    pub fn set_inner_html(el: &Element, html: &str) {
505        el.set_inner_html(html);
506    }
507
508    pub fn get_template<V>() -> TemplateElement
509    where
510        V: ToTemplate + 'static,
511    {
512        thread_local! {
513            static TEMPLATE_ELEMENT: LazyCell<HtmlTemplateElement> =
514                LazyCell::new(|| document().create_element(Dom::intern("template")).unwrap().unchecked_into());
515            static TEMPLATES: RefCell<LinearMap<TypeId, HtmlTemplateElement>> = Default::default();
516        }
517
518        TEMPLATES.with(|t| {
519            t.borrow_mut()
520                .entry(TypeId::of::<V>())
521                .or_insert_with(|| {
522                    let tpl = TEMPLATE_ELEMENT.with(|t| {
523                        t.clone_node()
524                            .unwrap()
525                            .unchecked_into::<HtmlTemplateElement>()
526                    });
527                    let mut buf = String::new();
528                    V::to_template(
529                        &mut buf,
530                        &mut String::new(),
531                        &mut String::new(),
532                        &mut String::new(),
533                        &mut Default::default(),
534                    );
535                    tpl.set_inner_html(&buf);
536                    tpl
537                })
538                .clone()
539        })
540    }
541
542    pub fn clone_template(tpl: &TemplateElement) -> Element {
543        tpl.content()
544            .clone_node_with_deep(true)
545            .unwrap()
546            .unchecked_into()
547    }
548
549    pub fn create_element_from_html(html: Cow<'static, str>) -> Element {
550        let tpl = TEMPLATE_CACHE.with(|cache| {
551            let mut cache = cache.borrow_mut();
552            if let Some(tpl_content) = cache.iter().find_map(|(key, tpl)| {
553                (html == *key)
554                    .then_some(Self::clone_template(tpl.unchecked_ref()))
555            }) {
556                tpl_content
557            } else {
558                let tpl = document()
559                    .create_element(Self::intern("template"))
560                    .unwrap();
561                tpl.set_inner_html(&html);
562                let tpl_content = Self::clone_template(tpl.unchecked_ref());
563                cache.push((html, tpl));
564                tpl_content
565            }
566        });
567        tpl.first_element_child().unwrap_or(tpl)
568    }
569
570    pub fn create_svg_element_from_html(html: Cow<'static, str>) -> Element {
571        let tpl = TEMPLATE_CACHE.with(|cache| {
572            let mut cache = cache.borrow_mut();
573            if let Some(tpl_content) = cache.iter().find_map(|(key, tpl)| {
574                (html == *key)
575                    .then_some(Self::clone_template(tpl.unchecked_ref()))
576            }) {
577                tpl_content
578            } else {
579                let tpl = document()
580                    .create_element(Self::intern("template"))
581                    .unwrap();
582                let svg = document()
583                    .create_element_ns(
584                        Some(Self::intern("http://www.w3.org/2000/svg")),
585                        Self::intern("svg"),
586                    )
587                    .unwrap();
588                let g = document()
589                    .create_element_ns(
590                        Some(Self::intern("http://www.w3.org/2000/svg")),
591                        Self::intern("g"),
592                    )
593                    .unwrap();
594                g.set_inner_html(&html);
595                svg.append_child(&g).unwrap();
596                tpl.unchecked_ref::<TemplateElement>()
597                    .content()
598                    .append_child(&svg)
599                    .unwrap();
600                let tpl_content = Self::clone_template(tpl.unchecked_ref());
601                cache.push((html, tpl));
602                tpl_content
603            }
604        });
605
606        let svg = tpl.first_element_child().unwrap();
607        svg.first_element_child().unwrap_or(svg)
608    }
609}
610
611impl Mountable for Node {
612    fn unmount(&mut self) {
613        todo!()
614    }
615
616    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
617        Dom::insert_node(parent, self, marker);
618    }
619
620    fn try_mount(&mut self, parent: &Element, marker: Option<&Node>) -> bool {
621        Dom::try_insert_node(parent, self, marker)
622    }
623
624    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
625        let parent = Dom::get_parent(self).and_then(Element::cast_from);
626        if let Some(parent) = parent {
627            child.mount(&parent, Some(self));
628            return true;
629        }
630        false
631    }
632
633    fn elements(&self) -> Vec<crate::renderer::types::Element> {
634        vec![]
635    }
636}
637
638impl Mountable for Text {
639    fn unmount(&mut self) {
640        self.remove();
641    }
642
643    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
644        Dom::insert_node(parent, self, marker);
645    }
646
647    fn try_mount(&mut self, parent: &Element, marker: Option<&Node>) -> bool {
648        Dom::try_insert_node(parent, self, marker)
649    }
650
651    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
652        let parent =
653            Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
654        if let Some(parent) = parent {
655            child.mount(&parent, Some(self));
656            return true;
657        }
658        false
659    }
660
661    fn elements(&self) -> Vec<crate::renderer::types::Element> {
662        vec![]
663    }
664}
665
666impl Mountable for Comment {
667    fn unmount(&mut self) {
668        self.remove();
669    }
670
671    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
672        Dom::insert_node(parent, self, marker);
673    }
674
675    fn try_mount(&mut self, parent: &Element, marker: Option<&Node>) -> bool {
676        Dom::try_insert_node(parent, self, marker)
677    }
678
679    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
680        let parent =
681            Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
682        if let Some(parent) = parent {
683            child.mount(&parent, Some(self));
684            return true;
685        }
686        false
687    }
688
689    fn elements(&self) -> Vec<crate::renderer::types::Element> {
690        vec![]
691    }
692}
693
694impl Mountable for Element {
695    fn unmount(&mut self) {
696        self.remove();
697    }
698
699    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
700        Dom::insert_node(parent, self, marker);
701    }
702
703    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
704        let parent =
705            Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
706        if let Some(parent) = parent {
707            child.mount(&parent, Some(self));
708            return true;
709        }
710        false
711    }
712
713    fn elements(&self) -> Vec<crate::renderer::types::Element> {
714        vec![self.clone()]
715    }
716}
717
718impl CastFrom<Node> for Text {
719    fn cast_from(node: Node) -> Option<Text> {
720        node.clone().dyn_into().ok()
721    }
722}
723
724impl CastFrom<Node> for Comment {
725    fn cast_from(node: Node) -> Option<Comment> {
726        node.clone().dyn_into().ok()
727    }
728}
729
730impl CastFrom<Node> for Element {
731    fn cast_from(node: Node) -> Option<Element> {
732        node.clone().dyn_into().ok()
733    }
734}
735
736impl<T> CastFrom<JsValue> for T
737where
738    T: JsCast,
739{
740    fn cast_from(source: JsValue) -> Option<Self> {
741        source.dyn_into::<T>().ok()
742    }
743}
744
745impl<T> CastFrom<Element> for T
746where
747    T: JsCast,
748{
749    fn cast_from(source: Element) -> Option<Self> {
750        source.dyn_into::<T>().ok()
751    }
752}