1#![allow(missing_docs)]
2
3use 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#[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
38pub 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 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 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 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 #[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 RemoveEventHandler::new({
297 let name = name.to_owned();
298 let el = el.clone();
299 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 RemoveEventHandler::new({
336 let name = name.to_owned();
337 let el = el.clone();
338 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 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 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 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 events.insert(name);
445 }
446 });
447
448 RemoveEventHandler::new({
450 let key = key.to_owned();
451 let el = el.clone();
452 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}