Skip to main content

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