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 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#[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
39pub 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 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 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 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 #[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 RemoveEventHandler::new({
298 let name = name.to_owned();
299 let el = el.clone();
300 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 RemoveEventHandler::new({
337 let name = name.to_owned();
338 let el = el.clone();
339 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 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 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 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 events.insert(name);
447 }
448 });
449
450 RemoveEventHandler::new({
452 let key = key.to_owned();
453 let el = el.clone();
454 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}