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 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 RemoveEventHandler::new({
336 let name = name.to_owned();
337 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 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 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}