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            let el = el.clone();
300            // safe to construct this here, because it will only run in the browser
301            // so it will always be accessed or dropped from the main thread
302            let cb = send_wrapper::SendWrapper::new(move || {
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            move || cb()
313        })
314    }
315
316    pub fn add_event_listener_use_capture(
317        el: &Element,
318        name: &str,
319        cb: Box<dyn FnMut(Event)>,
320    ) -> RemoveEventHandler<Element> {
321        let cb = wasm_bindgen::closure::Closure::wrap(cb);
322        let name = intern(name);
323        let options = AddEventListenerOptions::new();
324        options.set_capture(true);
325        or_debug!(
326            el.add_event_listener_with_callback_and_add_event_listener_options(
327                name,
328                cb.as_ref().unchecked_ref(),
329                &options
330            ),
331            el,
332            "addEventListenerUseCapture"
333        );
334
335        // return the remover
336        RemoveEventHandler::new({
337            let name = name.to_owned();
338            let el = el.clone();
339            // safe to construct this here, because it will only run in the browser
340            // so it will always be accessed or dropped from the main thread
341            let cb = send_wrapper::SendWrapper::new(move || {
342                or_debug!(
343                    el.remove_event_listener_with_callback_and_bool(
344                        intern(&name),
345                        cb.as_ref().unchecked_ref(),
346                        true
347                    ),
348                    &el,
349                    "removeEventListener"
350                )
351            });
352            move || cb()
353        })
354    }
355
356    pub fn event_target<T>(ev: &Event) -> T
357    where
358        T: CastFrom<Element>,
359    {
360        let el = ev
361            .unchecked_ref::<web_sys::Event>()
362            .target()
363            .expect("event.target not found")
364            .unchecked_into::<Element>();
365        T::cast_from(el).expect("incorrect element type")
366    }
367
368    pub fn add_event_listener_delegated(
369        el: &Element,
370        name: Cow<'static, str>,
371        delegation_key: Cow<'static, str>,
372        cb: Box<dyn FnMut(Event)>,
373    ) -> RemoveEventHandler<Element> {
374        let cb = Closure::wrap(cb);
375        let key = intern(&delegation_key);
376        or_debug!(
377            js_sys::Reflect::set(el, &JsValue::from_str(key), cb.as_ref()),
378            el,
379            "set property"
380        );
381
382        GLOBAL_EVENTS.with(|global_events| {
383            let mut events = global_events.borrow_mut();
384            if !events.contains(&name) {
385                // create global handler
386                let key = JsValue::from_str(key);
387                let handler = move |ev: web_sys::Event| {
388                    let target = ev.target();
389                    let node = ev.composed_path().get(0);
390                    let mut node = if node.is_undefined() || node.is_null() {
391                        JsValue::from(target)
392                    } else {
393                        node
394                    };
395
396                    // TODO reverse Shadow DOM retargetting
397                    // TODO simulate currentTarget
398
399                    while !node.is_null() {
400                        let node_is_disabled = js_sys::Reflect::get(
401                            &node,
402                            &JsValue::from_str("disabled"),
403                        )
404                        .unwrap()
405                        .is_truthy();
406                        if !node_is_disabled {
407                            let maybe_handler =
408                                js_sys::Reflect::get(&node, &key).unwrap();
409                            if !maybe_handler.is_undefined() {
410                                let f = maybe_handler
411                                    .unchecked_ref::<js_sys::Function>();
412                                let _ = f.call1(&node, &ev);
413
414                                if ev.cancel_bubble() {
415                                    return;
416                                }
417                            }
418                        }
419
420                        // navigate up tree
421                        if let Some(parent) =
422                            node.unchecked_ref::<web_sys::Node>().parent_node()
423                        {
424                            node = parent.into()
425                        } else if let Some(root) =
426                            node.dyn_ref::<web_sys::ShadowRoot>()
427                        {
428                            node = root.host().unchecked_into();
429                        } else {
430                            node = JsValue::null()
431                        }
432                    }
433                };
434
435                let handler =
436                    Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
437                let handler = Closure::wrap(handler).into_js_value();
438                window()
439                    .add_event_listener_with_callback(
440                        &name,
441                        handler.unchecked_ref(),
442                    )
443                    .unwrap();
444
445                // register that we've created handler
446                events.insert(name);
447            }
448        });
449
450        // return the remover
451        RemoveEventHandler::new({
452            let key = key.to_owned();
453            let el = el.clone();
454            // safe to construct this here, because it will only run in the browser
455            // so it will always be accessed or dropped from the main thread
456            let el_cb = send_wrapper::SendWrapper::new((el, cb));
457            move || {
458                let (el, cb) = el_cb.take();
459                drop(cb);
460                or_debug!(
461                    js_sys::Reflect::delete_property(
462                        &el,
463                        &JsValue::from_str(&key)
464                    ),
465                    &el,
466                    "delete property"
467                );
468            }
469        })
470    }
471
472    pub fn class_list(el: &Element) -> ClassList {
473        el.class_list()
474    }
475
476    pub fn add_class(list: &ClassList, name: &str) {
477        or_debug!(list.add_1(name), list.unchecked_ref(), "add()");
478    }
479
480    pub fn remove_class(list: &ClassList, name: &str) {
481        or_debug!(list.remove_1(name), list.unchecked_ref(), "remove()");
482    }
483
484    pub fn style(el: &Element) -> CssStyleDeclaration {
485        el.unchecked_ref::<web_sys::HtmlElement>().style()
486    }
487
488    pub fn set_css_property(
489        style: &CssStyleDeclaration,
490        name: &str,
491        value: &str,
492    ) {
493        or_debug!(
494            style.set_property(name, value),
495            style.unchecked_ref(),
496            "setProperty"
497        );
498    }
499
500    pub fn remove_css_property(style: &CssStyleDeclaration, name: &str) {
501        or_debug!(
502            style.remove_property(name),
503            style.unchecked_ref(),
504            "removeProperty"
505        );
506    }
507
508    pub fn set_inner_html(el: &Element, html: &str) {
509        el.set_inner_html(html);
510    }
511
512    pub fn get_template<V>() -> TemplateElement
513    where
514        V: ToTemplate + 'static,
515    {
516        thread_local! {
517            static TEMPLATE_ELEMENT: LazyCell<HtmlTemplateElement> =
518                LazyCell::new(|| document().create_element(Dom::intern("template")).unwrap().unchecked_into());
519            static TEMPLATES: RefCell<LinearMap<TypeId, HtmlTemplateElement>> = Default::default();
520        }
521
522        TEMPLATES.with(|t| {
523            t.borrow_mut()
524                .entry(TypeId::of::<V>())
525                .or_insert_with(|| {
526                    let tpl = TEMPLATE_ELEMENT.with(|t| {
527                        t.clone_node()
528                            .unwrap()
529                            .unchecked_into::<HtmlTemplateElement>()
530                    });
531                    let mut buf = String::new();
532                    V::to_template(
533                        &mut buf,
534                        &mut String::new(),
535                        &mut String::new(),
536                        &mut String::new(),
537                        &mut Default::default(),
538                    );
539                    tpl.set_inner_html(&buf);
540                    tpl
541                })
542                .clone()
543        })
544    }
545
546    pub fn clone_template(tpl: &TemplateElement) -> Element {
547        tpl.content()
548            .clone_node_with_deep(true)
549            .unwrap()
550            .unchecked_into()
551    }
552
553    pub fn create_element_from_html(html: Cow<'static, str>) -> Element {
554        let tpl = TEMPLATE_CACHE.with(|cache| {
555            let mut cache = cache.borrow_mut();
556            if let Some(tpl_content) = cache.iter().find_map(|(key, tpl)| {
557                (html == *key)
558                    .then_some(Self::clone_template(tpl.unchecked_ref()))
559            }) {
560                tpl_content
561            } else {
562                let tpl = document()
563                    .create_element(Self::intern("template"))
564                    .unwrap();
565                tpl.set_inner_html(&html);
566                let tpl_content = Self::clone_template(tpl.unchecked_ref());
567                cache.push((html, tpl));
568                tpl_content
569            }
570        });
571        tpl.first_element_child().unwrap_or(tpl)
572    }
573
574    pub fn create_svg_element_from_html(html: Cow<'static, str>) -> Element {
575        let tpl = TEMPLATE_CACHE.with(|cache| {
576            let mut cache = cache.borrow_mut();
577            if let Some(tpl_content) = cache.iter().find_map(|(key, tpl)| {
578                (html == *key)
579                    .then_some(Self::clone_template(tpl.unchecked_ref()))
580            }) {
581                tpl_content
582            } else {
583                let tpl = document()
584                    .create_element(Self::intern("template"))
585                    .unwrap();
586                let svg = document()
587                    .create_element_ns(
588                        Some(Self::intern("http://www.w3.org/2000/svg")),
589                        Self::intern("svg"),
590                    )
591                    .unwrap();
592                let g = document()
593                    .create_element_ns(
594                        Some(Self::intern("http://www.w3.org/2000/svg")),
595                        Self::intern("g"),
596                    )
597                    .unwrap();
598                g.set_inner_html(&html);
599                svg.append_child(&g).unwrap();
600                tpl.unchecked_ref::<TemplateElement>()
601                    .content()
602                    .append_child(&svg)
603                    .unwrap();
604                let tpl_content = Self::clone_template(tpl.unchecked_ref());
605                cache.push((html, tpl));
606                tpl_content
607            }
608        });
609
610        let svg = tpl.first_element_child().unwrap();
611        svg.first_element_child().unwrap_or(svg)
612    }
613}
614
615impl Mountable for Node {
616    fn unmount(&mut self) {
617        todo!()
618    }
619
620    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
621        Dom::insert_node(parent, self, marker);
622    }
623
624    fn try_mount(&mut self, parent: &Element, marker: Option<&Node>) -> bool {
625        Dom::try_insert_node(parent, self, marker)
626    }
627
628    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
629        let parent = Dom::get_parent(self).and_then(Element::cast_from);
630        if let Some(parent) = parent {
631            child.mount(&parent, Some(self));
632            return true;
633        }
634        false
635    }
636
637    fn elements(&self) -> Vec<crate::renderer::types::Element> {
638        vec![]
639    }
640}
641
642impl Mountable for Text {
643    fn unmount(&mut self) {
644        self.remove();
645    }
646
647    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
648        Dom::insert_node(parent, self, marker);
649    }
650
651    fn try_mount(&mut self, parent: &Element, marker: Option<&Node>) -> bool {
652        Dom::try_insert_node(parent, self, marker)
653    }
654
655    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
656        let parent =
657            Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
658        if let Some(parent) = parent {
659            child.mount(&parent, Some(self));
660            return true;
661        }
662        false
663    }
664
665    fn elements(&self) -> Vec<crate::renderer::types::Element> {
666        vec![]
667    }
668}
669
670impl Mountable for Comment {
671    fn unmount(&mut self) {
672        self.remove();
673    }
674
675    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
676        Dom::insert_node(parent, self, marker);
677    }
678
679    fn try_mount(&mut self, parent: &Element, marker: Option<&Node>) -> bool {
680        Dom::try_insert_node(parent, self, marker)
681    }
682
683    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
684        let parent =
685            Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
686        if let Some(parent) = parent {
687            child.mount(&parent, Some(self));
688            return true;
689        }
690        false
691    }
692
693    fn elements(&self) -> Vec<crate::renderer::types::Element> {
694        vec![]
695    }
696}
697
698impl Mountable for Element {
699    fn unmount(&mut self) {
700        self.remove();
701    }
702
703    fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
704        Dom::insert_node(parent, self, marker);
705    }
706
707    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
708        let parent =
709            Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
710        if let Some(parent) = parent {
711            child.mount(&parent, Some(self));
712            return true;
713        }
714        false
715    }
716
717    fn elements(&self) -> Vec<crate::renderer::types::Element> {
718        vec![self.clone()]
719    }
720}
721
722impl CastFrom<Node> for Text {
723    fn cast_from(node: Node) -> Option<Text> {
724        node.clone().dyn_into().ok()
725    }
726}
727
728impl CastFrom<Node> for Comment {
729    fn cast_from(node: Node) -> Option<Comment> {
730        node.clone().dyn_into().ok()
731    }
732}
733
734impl CastFrom<Node> for Element {
735    fn cast_from(node: Node) -> Option<Element> {
736        node.clone().dyn_into().ok()
737    }
738}
739
740impl<T> CastFrom<JsValue> for T
741where
742    T: JsCast,
743{
744    fn cast_from(source: JsValue) -> Option<Self> {
745        source.dyn_into::<T>().ok()
746    }
747}
748
749impl<T> CastFrom<Element> for T
750where
751    T: JsCast,
752{
753    fn cast_from(source: Element) -> Option<Self> {
754        source.dyn_into::<T>().ok()
755    }
756}