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 once_cell::unsync::Lazy;
13use rustc_hash::FxHashSet;
14use std::{any::TypeId, borrow::Cow, cell::RefCell};
15use wasm_bindgen::{intern, prelude::Closure, JsCast, JsValue};
16use web_sys::{AddEventListenerOptions, Comment, HtmlTemplateElement};
17
18#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct Dom;
21
22thread_local! {
23 pub(crate) static GLOBAL_EVENTS: RefCell<FxHashSet<Cow<'static, str>>> = Default::default();
24}
25
26pub type Node = web_sys::Node;
27pub type Text = web_sys::Text;
28pub type Element = web_sys::Element;
29pub type Placeholder = web_sys::Comment;
30pub type Event = wasm_bindgen::JsValue;
31pub type ClassList = web_sys::DomTokenList;
32pub type CssStyleDeclaration = web_sys::CssStyleDeclaration;
33pub type TemplateElement = web_sys::HtmlTemplateElement;
34
35impl Dom {
36 pub fn intern(text: &str) -> &str {
37 intern(text)
38 }
39
40 pub fn create_element(tag: &str, namespace: Option<&str>) -> Element {
41 if let Some(namespace) = namespace {
42 document()
43 .create_element_ns(
44 Some(Self::intern(namespace)),
45 Self::intern(tag),
46 )
47 .unwrap()
48 } else {
49 document().create_element(Self::intern(tag)).unwrap()
50 }
51 }
52
53 #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
54 pub fn create_text_node(text: &str) -> Text {
55 document().create_text_node(text)
56 }
57
58 pub fn create_placeholder() -> Placeholder {
59 thread_local! {
60 static COMMENT: Lazy<Comment> = Lazy::new(|| {
61 document().create_comment("")
62 });
63 }
64 COMMENT.with(|n| n.clone_node().unwrap().unchecked_into())
65 }
66
67 #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
68 pub fn set_text(node: &Text, text: &str) {
69 node.set_node_value(Some(text));
70 }
71
72 #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
73 pub fn set_attribute(node: &Element, name: &str, value: &str) {
74 or_debug!(node.set_attribute(name, value), node, "setAttribute");
75 }
76
77 #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
78 pub fn remove_attribute(node: &Element, name: &str) {
79 or_debug!(node.remove_attribute(name), node, "removeAttribute");
80 }
81
82 #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
83 pub fn insert_node(
84 parent: &Element,
85 new_child: &Node,
86 anchor: Option<&Node>,
87 ) {
88 ok_or_debug!(
89 parent.insert_before(new_child, anchor),
90 parent,
91 "insertNode"
92 );
93 }
94
95 #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
96 pub fn remove_node(parent: &Element, child: &Node) -> Option<Node> {
97 ok_or_debug!(parent.remove_child(child), parent, "removeNode")
98 }
99
100 #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
101 pub fn remove(node: &Node) {
102 node.unchecked_ref::<Element>().remove();
103 }
104
105 pub fn get_parent(node: &Node) -> Option<Node> {
106 node.parent_node()
107 }
108
109 pub fn first_child(node: &Node) -> Option<Node> {
110 #[cfg(debug_assertions)]
111 {
112 let node = node.first_child();
113 if let Some(node) = node.as_ref() {
116 if node.node_type() == 8
117 && node
118 .text_content()
119 .unwrap_or_default()
120 .starts_with("hot-reload")
121 {
122 return Self::next_sibling(node);
123 }
124 }
125
126 node
127 }
128 #[cfg(not(debug_assertions))]
129 {
130 node.first_child()
131 }
132 }
133
134 pub fn next_sibling(node: &Node) -> Option<Node> {
135 #[cfg(debug_assertions)]
136 {
137 let node = node.next_sibling();
138 if let Some(node) = node.as_ref() {
141 if node.node_type() == 8
142 && node
143 .text_content()
144 .unwrap_or_default()
145 .starts_with("hot-reload")
146 {
147 return Self::next_sibling(node);
148 }
149 }
150
151 node
152 }
153 #[cfg(not(debug_assertions))]
154 {
155 node.next_sibling()
156 }
157 }
158
159 pub fn log_node(node: &Node) {
160 web_sys::console::log_1(node);
161 }
162
163 #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
164 pub fn clear_children(parent: &Element) {
165 parent.set_text_content(Some(""));
166 }
167
168 pub fn mount_before<M>(new_child: &mut M, before: &Node)
173 where
174 M: Mountable,
175 {
176 let parent = Element::cast_from(
177 Self::get_parent(before).expect("could not find parent element"),
178 )
179 .expect("placeholder parent should be Element");
180 new_child.mount(&parent, Some(before));
181 }
182
183 #[track_caller]
187 pub fn try_mount_before<M>(new_child: &mut M, before: &Node) -> bool
188 where
189 M: Mountable,
190 {
191 if let Some(parent) =
192 Self::get_parent(before).and_then(Element::cast_from)
193 {
194 new_child.mount(&parent, Some(before));
195 true
196 } else {
197 false
198 }
199 }
200
201 pub fn set_property(el: &Element, key: &str, value: &JsValue) {
202 or_debug!(
203 js_sys::Reflect::set(
204 el,
205 &wasm_bindgen::JsValue::from_str(key),
206 value,
207 ),
208 el,
209 "setProperty"
210 );
211 }
212
213 pub fn add_event_listener(
214 el: &Element,
215 name: &str,
216 cb: Box<dyn FnMut(Event)>,
217 ) -> RemoveEventHandler<Element> {
218 let cb = wasm_bindgen::closure::Closure::wrap(cb);
219 let name = intern(name);
220 or_debug!(
221 el.add_event_listener_with_callback(
222 name,
223 cb.as_ref().unchecked_ref()
224 ),
225 el,
226 "addEventListener"
227 );
228
229 RemoveEventHandler::new({
231 let name = name.to_owned();
232 let cb = send_wrapper::SendWrapper::new(cb);
235 move |el: &Element| {
236 or_debug!(
237 el.remove_event_listener_with_callback(
238 intern(&name),
239 cb.as_ref().unchecked_ref()
240 ),
241 el,
242 "removeEventListener"
243 )
244 }
245 })
246 }
247
248 pub fn add_event_listener_use_capture(
249 el: &Element,
250 name: &str,
251 cb: Box<dyn FnMut(Event)>,
252 ) -> RemoveEventHandler<Element> {
253 let cb = wasm_bindgen::closure::Closure::wrap(cb);
254 let name = intern(name);
255 let options = AddEventListenerOptions::new();
256 options.set_capture(true);
257 or_debug!(
258 el.add_event_listener_with_callback_and_add_event_listener_options(
259 name,
260 cb.as_ref().unchecked_ref(),
261 &options
262 ),
263 el,
264 "addEventListenerUseCapture"
265 );
266
267 RemoveEventHandler::new({
269 let name = name.to_owned();
270 let cb = send_wrapper::SendWrapper::new(cb);
273 move |el: &Element| {
274 or_debug!(
275 el.remove_event_listener_with_callback(
276 intern(&name),
277 cb.as_ref().unchecked_ref()
278 ),
279 el,
280 "removeEventListener"
281 )
282 }
283 })
284 }
285
286 pub fn event_target<T>(ev: &Event) -> T
287 where
288 T: CastFrom<Element>,
289 {
290 let el = ev
291 .unchecked_ref::<web_sys::Event>()
292 .target()
293 .expect("event.target not found")
294 .unchecked_into::<Element>();
295 T::cast_from(el).expect("incorrect element type")
296 }
297
298 pub fn add_event_listener_delegated(
299 el: &Element,
300 name: Cow<'static, str>,
301 delegation_key: Cow<'static, str>,
302 cb: Box<dyn FnMut(Event)>,
303 ) -> RemoveEventHandler<Element> {
304 let cb = Closure::wrap(cb);
305 let key = intern(&delegation_key);
306 or_debug!(
307 js_sys::Reflect::set(el, &JsValue::from_str(key), cb.as_ref()),
308 el,
309 "set property"
310 );
311
312 GLOBAL_EVENTS.with(|global_events| {
313 let mut events = global_events.borrow_mut();
314 if !events.contains(&name) {
315 let key = JsValue::from_str(key);
317 let handler = move |ev: web_sys::Event| {
318 let target = ev.target();
319 let node = ev.composed_path().get(0);
320 let mut node = if node.is_undefined() || node.is_null() {
321 JsValue::from(target)
322 } else {
323 node
324 };
325
326 while !node.is_null() {
330 let node_is_disabled = js_sys::Reflect::get(
331 &node,
332 &JsValue::from_str("disabled"),
333 )
334 .unwrap()
335 .is_truthy();
336 if !node_is_disabled {
337 let maybe_handler =
338 js_sys::Reflect::get(&node, &key).unwrap();
339 if !maybe_handler.is_undefined() {
340 let f = maybe_handler
341 .unchecked_ref::<js_sys::Function>();
342 let _ = f.call1(&node, &ev);
343
344 if ev.cancel_bubble() {
345 return;
346 }
347 }
348 }
349
350 if let Some(parent) =
352 node.unchecked_ref::<web_sys::Node>().parent_node()
353 {
354 node = parent.into()
355 } else if let Some(root) =
356 node.dyn_ref::<web_sys::ShadowRoot>()
357 {
358 node = root.host().unchecked_into();
359 } else {
360 node = JsValue::null()
361 }
362 }
363 };
364
365 let handler =
366 Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
367 let handler = Closure::wrap(handler).into_js_value();
368 window()
369 .add_event_listener_with_callback(
370 &name,
371 handler.unchecked_ref(),
372 )
373 .unwrap();
374
375 events.insert(name);
377 }
378 });
379
380 RemoveEventHandler::new({
382 let key = key.to_owned();
383 let cb = send_wrapper::SendWrapper::new(cb);
386 move |el: &Element| {
387 drop(cb.take());
388 or_debug!(
389 js_sys::Reflect::delete_property(
390 el,
391 &JsValue::from_str(&key)
392 ),
393 el,
394 "delete property"
395 );
396 }
397 })
398 }
399
400 pub fn class_list(el: &Element) -> ClassList {
401 el.class_list()
402 }
403
404 pub fn add_class(list: &ClassList, name: &str) {
405 or_debug!(list.add_1(name), list.unchecked_ref(), "add()");
406 }
407
408 pub fn remove_class(list: &ClassList, name: &str) {
409 or_debug!(list.remove_1(name), list.unchecked_ref(), "remove()");
410 }
411
412 pub fn style(el: &Element) -> CssStyleDeclaration {
413 el.unchecked_ref::<web_sys::HtmlElement>().style()
414 }
415
416 pub fn set_css_property(
417 style: &CssStyleDeclaration,
418 name: &str,
419 value: &str,
420 ) {
421 or_debug!(
422 style.set_property(name, value),
423 style.unchecked_ref(),
424 "setProperty"
425 );
426 }
427
428 pub fn remove_css_property(style: &CssStyleDeclaration, name: &str) {
429 or_debug!(
430 style.remove_property(name),
431 style.unchecked_ref(),
432 "removeProperty"
433 );
434 }
435
436 pub fn set_inner_html(el: &Element, html: &str) {
437 el.set_inner_html(html);
438 }
439
440 pub fn get_template<V>() -> TemplateElement
441 where
442 V: ToTemplate + 'static,
443 {
444 thread_local! {
445 static TEMPLATE_ELEMENT: Lazy<HtmlTemplateElement> =
446 Lazy::new(|| document().create_element("template").unwrap().unchecked_into());
447 static TEMPLATES: RefCell<LinearMap<TypeId, HtmlTemplateElement>> = Default::default();
448 }
449
450 TEMPLATES.with(|t| {
451 t.borrow_mut()
452 .entry(TypeId::of::<V>())
453 .or_insert_with(|| {
454 let tpl = TEMPLATE_ELEMENT.with(|t| {
455 t.clone_node()
456 .unwrap()
457 .unchecked_into::<HtmlTemplateElement>()
458 });
459 let mut buf = String::new();
460 V::to_template(
461 &mut buf,
462 &mut String::new(),
463 &mut String::new(),
464 &mut String::new(),
465 &mut Default::default(),
466 );
467 tpl.set_inner_html(&buf);
468 tpl
469 })
470 .clone()
471 })
472 }
473
474 pub fn clone_template(tpl: &TemplateElement) -> Element {
475 tpl.content()
476 .clone_node_with_deep(true)
477 .unwrap()
478 .unchecked_into()
479 }
480
481 pub fn create_element_from_html(html: &str) -> Element {
482 let tpl = document().create_element("template").unwrap();
484 tpl.set_inner_html(html);
485 let tpl = Self::clone_template(tpl.unchecked_ref());
486 tpl.first_element_child().unwrap_or(tpl)
487 }
488}
489
490impl Mountable for Node {
491 fn unmount(&mut self) {
492 todo!()
493 }
494
495 fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
496 Dom::insert_node(parent, self, marker);
497 }
498
499 fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
500 let parent = Dom::get_parent(self).and_then(Element::cast_from);
501 if let Some(parent) = parent {
502 child.mount(&parent, Some(self));
503 return true;
504 }
505 false
506 }
507}
508
509impl Mountable for Text {
510 fn unmount(&mut self) {
511 self.remove();
512 }
513
514 fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
515 Dom::insert_node(parent, self, marker);
516 }
517
518 fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
519 let parent =
520 Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
521 if let Some(parent) = parent {
522 child.mount(&parent, Some(self));
523 return true;
524 }
525 false
526 }
527}
528
529impl Mountable for Comment {
530 fn unmount(&mut self) {
531 self.remove();
532 }
533
534 fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
535 Dom::insert_node(parent, self, marker);
536 }
537
538 fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
539 let parent =
540 Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
541 if let Some(parent) = parent {
542 child.mount(&parent, Some(self));
543 return true;
544 }
545 false
546 }
547}
548
549impl Mountable for Element {
550 fn unmount(&mut self) {
551 self.remove();
552 }
553
554 fn mount(&mut self, parent: &Element, marker: Option<&Node>) {
555 Dom::insert_node(parent, self, marker);
556 }
557
558 fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
559 let parent =
560 Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
561 if let Some(parent) = parent {
562 child.mount(&parent, Some(self));
563 return true;
564 }
565 false
566 }
567}
568
569impl CastFrom<Node> for Text {
570 fn cast_from(node: Node) -> Option<Text> {
571 node.clone().dyn_into().ok()
572 }
573}
574
575impl CastFrom<Node> for Comment {
576 fn cast_from(node: Node) -> Option<Comment> {
577 node.clone().dyn_into().ok()
578 }
579}
580
581impl CastFrom<Node> for Element {
582 fn cast_from(node: Node) -> Option<Element> {
583 node.clone().dyn_into().ok()
584 }
585}
586
587impl<T> CastFrom<JsValue> for T
588where
589 T: JsCast,
590{
591 fn cast_from(source: JsValue) -> Option<Self> {
592 source.dyn_into::<T>().ok()
593 }
594}
595
596impl<T> CastFrom<Element> for T
597where
598 T: JsCast,
599{
600 fn cast_from(source: Element) -> Option<Self> {
601 source.dyn_into::<T>().ok()
602 }
603}