1use std::cmp::Ordering;
2use std::fmt::Debug;
3
4use wasm_bindgen::prelude::Closure;
5use web_sys::{
6 Element, FocusEvent, HtmlElement, HtmlInputElement, HtmlLabelElement, HtmlTextAreaElement, InputEvent, KeyboardEvent, MouseEvent,
7 Node,
8};
9
10use wasm_bindgen::JsCast;
11use web_sys::console::warn_1;
12
13use crate::node::{RespoComponent, RespoEffectType, RespoEvent, RespoEventMark, RespoEventMarkFn, RespoNode};
14
15use super::renderer::load_coord_target_tree;
16use super::util;
17use crate::node::dom_change::{ChildDomOp, DomChange, RespoCoord};
18
19use crate::app::renderer::build_dom_tree;
20
21pub fn patch_tree<T>(
22 tree: &RespoNode<T>,
23 old_tree: &RespoNode<T>,
24 mount_target: &Node,
25 changes: &[DomChange<T>],
26 handle_event: RespoEventMarkFn,
27) -> Result<(), String>
28where
29 T: Debug + Clone,
30{
31 if mount_target.child_nodes().length() != 1 {
34 return Err(format!(
35 "expected a single node under mount target, got: {:?}",
36 mount_target.child_nodes().length()
37 ));
38 }
39
40 for op in changes {
42 if let DomChange::Effect {
43 coord,
44 effect_type,
45 skip_indexes,
46 ..
47 } = op
48 {
49 if effect_type == &RespoEffectType::BeforeUpdate {
50 let target = find_coord_dom_target(&mount_target.first_child().ok_or("mount position")?, op.get_dom_path())?;
51 let target_tree = if effect_type == &RespoEffectType::BeforeUnmount {
52 load_coord_target_tree(old_tree, coord)?
53 } else {
54 load_coord_target_tree(tree, coord)?
55 };
56 if let RespoNode::Component(RespoComponent { effects, .. }) = target_tree {
57 for (idx, effect) in effects.iter().enumerate() {
58 if !skip_indexes.contains(&(idx as u32)) {
59 effect.0.as_ref().run(effect_type.to_owned(), &target)?;
60 }
61 }
62 } else {
63 crate::util::warn_log!("expected component for effects, got: {}", target_tree);
64 }
65 }
66 }
67 }
68
69 for op in changes {
70 let target = find_coord_dom_target(&mount_target.first_child().ok_or("mount position")?, op.get_dom_path())?;
72 match op {
73 DomChange::ModifyAttrs { set, unset, .. } => {
74 let el = target.dyn_ref::<Element>().expect("load as element");
75 for (k, v) in set {
76 let k = k.as_ref();
77 if k == "innerText" {
78 el.dyn_ref::<HtmlElement>().ok_or("to html element")?.set_inner_text(v);
79 } else if k == "innerHTML" {
80 el.set_inner_html(v);
81 } else if k == "htmlFor" {
82 el.dyn_ref::<HtmlLabelElement>().ok_or("to label element")?.set_html_for(v);
83 } else if k == "value" {
84 match el.tag_name().as_str() {
85 "INPUT" => {
86 let input_el = el.dyn_ref::<HtmlInputElement>().expect("to input");
87 let prev_value = input_el.value();
88 if &prev_value != v {
89 input_el.set_value(v);
90 }
91 }
92 "TEXTAREA" => {
93 let textarea_el = el.dyn_ref::<HtmlTextAreaElement>().expect("to textarea");
94 let prev_value = textarea_el.value();
95 if &prev_value != v {
96 textarea_el.set_value(v);
97 }
98 }
99 name => {
100 return Err(format!("unsupported value for {}", name));
101 }
102 }
103 } else {
104 el.set_attribute(k, v).expect("to set attribute");
105 }
106 }
107 for k in unset {
108 let k = k.as_ref();
109 if k == "innerText" {
110 el.dyn_ref::<HtmlElement>().ok_or("to html element")?.set_inner_text("");
111 } else if k == "innerHTML" {
112 el.set_inner_html("");
113 } else if k == "value" {
114 let input_el = el.dyn_ref::<HtmlInputElement>().expect("to input");
115 let prev_value = input_el.value();
116 if !prev_value.is_empty() {
117 input_el.set_value("");
118 }
119 } else {
120 el.remove_attribute(k).expect("to remove attribute");
121 }
122 }
123 }
124 DomChange::ModifyStyle { set, unset, .. } => {
125 let style = target.dyn_ref::<HtmlElement>().expect("into html element").style();
126 for s in unset {
127 style.remove_property(s).expect("remove style");
128 }
129 for (k, v) in set {
130 style.set_property(k, v).expect("set style");
131 }
132 }
133 DomChange::ModifyEvent { add, remove, coord, .. } => {
134 let el = target.dyn_ref::<Element>().expect("to element");
135 for k in add.iter() {
136 attach_event(el, k, coord, handle_event.to_owned())?;
137 }
138 let el = el.dyn_ref::<HtmlElement>().expect("html element");
139 for k in remove {
140 match k.as_ref() {
141 "click" => {
142 el.set_onclick(None);
143 }
144 "input" => {
145 el.set_oninput(None);
146 }
147 _ => warn_1(&format!("TODO event {}", k).into()),
148 }
149 }
150 }
151 DomChange::ReplaceElement { node, coord, .. } => {
152 let parent = target.parent_element().expect("load parent");
153 let new_element = build_dom_tree(node, coord, handle_event.to_owned()).expect("build element");
154 parent
155 .dyn_ref::<Node>()
156 .expect("to node")
157 .insert_before(&new_element, Some(&target))
158 .expect("element inserted");
159 target.dyn_ref::<Element>().expect("get node").remove();
160 }
161 DomChange::ModifyChildren { operations, coord, .. } => {
162 let base_tree = load_coord_target_tree(tree, coord)?;
163 let old_base_tree = load_coord_target_tree(old_tree, coord)?;
164 for op in operations {
165 let handler = handle_event.to_owned();
166 match op {
167 ChildDomOp::Append(k, node) => {
168 let mut next_coord = coord.to_owned();
169 next_coord.push(RespoCoord::Key(k.to_owned()));
170 let new_element = build_dom_tree(node, &next_coord, handler).expect("new element");
171 target
172 .dyn_ref::<Node>()
173 .expect("to node")
174 .append_child(&new_element)
175 .expect("element appended");
176 }
177 ChildDomOp::Prepend(k, node) => {
178 let mut next_coord = coord.to_owned();
179 next_coord.push(RespoCoord::Key(k.to_owned()));
180 let new_element = build_dom_tree(node, &next_coord, handler).expect("new element");
181 if target.child_nodes().length() == 0 {
182 target
183 .dyn_ref::<Node>()
184 .expect("to node")
185 .append_child(&new_element)
186 .expect("element appended");
187 } else {
188 let base = target.dyn_ref::<Node>().expect("to node").first_child().ok_or("to first child")?;
189 target
190 .dyn_ref::<Node>()
191 .expect("to node")
192 .insert_before(&new_element, Some(&base))
193 .expect("element appended");
194 }
195 }
196 ChildDomOp::RemoveAt(idx) => {
197 let child = target
198 .dyn_ref::<Element>()
199 .expect("get node")
200 .children()
201 .item(*idx)
202 .ok_or_else(|| {
203 util::warn_log!("child not found at {:?}", coord);
204 format!("child to remove not found at {}", &idx)
205 })?;
206 target.remove_child(&child).expect("child removed");
207 }
208 ChildDomOp::InsertAfter(idx, k, node) => {
209 let children = target.dyn_ref::<Element>().expect("get node").children();
210 if idx >= &children.length() {
211 return Err(format!("child to insert not found at {}", &idx));
212 } else {
213 let handler = handle_event.to_owned();
214 let mut next_coord = coord.to_owned();
215 next_coord.push(RespoCoord::Key(k.to_owned()));
216 let new_element = build_dom_tree(node, &next_coord, handler).expect("new element");
217 match (idx + 1).cmp(&children.length()) {
218 Ordering::Less => {
219 let child = children.item(*idx + 1).ok_or_else(|| format!("child not found at {}", &idx))?;
220 target.insert_before(&new_element, Some(&child)).expect("element inserted");
221 }
222 Ordering::Equal => {
223 target.append_child(&new_element).expect("element appended");
224 }
225 Ordering::Greater => {
226 return Err(format!("out of bounds: {} of {} at coord {:?}", idx, &children.length(), coord));
227 }
228 }
229 }
230 }
231 ChildDomOp::NestedEffect {
232 nested_coord,
233 nested_dom_path: nesteed_dom_path,
234 effect_type,
235 skip_indexes,
236 } => {
237 let target_tree = if effect_type == &RespoEffectType::BeforeUnmount {
238 load_coord_target_tree(&old_base_tree, nested_coord)?
239 } else {
240 load_coord_target_tree(&base_tree, nested_coord)?
241 };
242 let nested_el = find_coord_dom_target(&target, nesteed_dom_path)?;
243 if let RespoNode::Component(RespoComponent { effects, .. }) = target_tree {
244 for (idx, effect) in effects.iter().enumerate() {
245 if !skip_indexes.contains(&(idx as u32)) {
246 effect.0.run(effect_type.to_owned(), &nested_el)?;
247 }
248 }
249 } else {
250 crate::util::warn_log!("expected component for effects, got: {}", target_tree);
251 }
252 }
253 }
254 }
255 }
256
257 DomChange::Effect {
258 coord,
259 effect_type,
260 skip_indexes,
261 ..
262 } => {
263 if effect_type == &RespoEffectType::BeforeUpdate {
264 continue;
266 }
267 let target_tree = if effect_type == &RespoEffectType::BeforeUnmount {
268 load_coord_target_tree(old_tree, coord)?
269 } else {
270 load_coord_target_tree(tree, coord)?
271 };
272 if let RespoNode::Component(RespoComponent { effects, .. }) = target_tree {
273 for (idx, effect) in effects.iter().enumerate() {
274 if !skip_indexes.contains(&(idx as u32)) {
275 effect.0.run(effect_type.to_owned(), &target)?;
276 }
277 }
278 } else {
279 crate::util::warn_log!("expected component for effects, got: {}", target_tree);
280 }
281 }
282 }
283 }
284 Ok(())
285}
286
287fn find_coord_dom_target(mount_target: &Node, coord: &[u32]) -> Result<Node, String> {
288 let mut target = mount_target.to_owned();
289 for idx in coord {
290 let child = target.child_nodes().item(idx.to_owned());
291 if child.is_none() {
292 return Err(format!("no child at index {}", &idx));
293 }
294 target = child.ok_or_else(|| format!("does not find child at index: {}", &idx))?;
295 }
296 Ok(target)
297}
298
299pub fn attach_event(element: &Element, key: &str, coord: &[RespoCoord], handle_event: RespoEventMarkFn) -> Result<(), String> {
300 let coord = coord.to_owned();
301 match key {
303 "click" => {
304 let handler = Closure::wrap(Box::new(move |e: MouseEvent| {
305 let wrap_event = RespoEvent::Click {
306 client_x: e.client_x() as f64,
307 client_y: e.client_y() as f64,
308 original_event: e,
309 };
310 handle_event
311 .run(RespoEventMark::new("click", &coord, wrap_event))
312 .expect("handle click event");
313 }) as Box<dyn FnMut(MouseEvent)>);
314 element
315 .dyn_ref::<HtmlElement>()
316 .expect("convert to html element")
317 .set_onclick(Some(handler.as_ref().unchecked_ref()));
318 handler.forget();
319 }
320
321 "dblclick" => {
322 let handler = Closure::wrap(Box::new(move |e: MouseEvent| {
323 let wrap_event = RespoEvent::Click {
324 client_x: e.client_x() as f64,
325 client_y: e.client_y() as f64,
326 original_event: e,
327 };
328 handle_event
329 .run(RespoEventMark::new("dblclick", &coord, wrap_event))
330 .expect("handle dblclick event");
331 }) as Box<dyn FnMut(MouseEvent)>);
332 element
333 .dyn_ref::<HtmlElement>()
334 .expect("convert to html element")
335 .set_ondblclick(Some(handler.as_ref().unchecked_ref()));
336 handler.forget();
337 }
338 "input" => {
339 let handler = Closure::wrap(Box::new(move |e: InputEvent| {
340 let target = e.target().expect("get target");
341 let el = target.dyn_ref::<Element>().unwrap();
342 let value = match el.tag_name().as_str() {
343 "INPUT" => el.dyn_ref::<HtmlInputElement>().expect("to convert to html input element").value(),
344 "TEXTAREA" => el
345 .dyn_ref::<HtmlTextAreaElement>()
346 .expect("to convert to html text area element")
347 .value(),
348 _ => {
349 return;
351 }
352 };
353 let wrap_event = RespoEvent::Input { value, original_event: e };
354 handle_event
355 .run(RespoEventMark::new("input", &coord, wrap_event))
356 .expect("handle input event");
357 }) as Box<dyn FnMut(InputEvent)>);
358 match element.tag_name().as_str() {
359 "INPUT" => {
360 element
361 .dyn_ref::<HtmlInputElement>()
362 .expect("convert to html input element")
363 .set_oninput(Some(handler.as_ref().unchecked_ref()));
364 }
365 "TEXTAREA" => {
366 element
367 .dyn_ref::<HtmlTextAreaElement>()
368 .expect("convert to html textarea element")
369 .set_oninput(Some(handler.as_ref().unchecked_ref()));
370 }
371 _ => {
372 return Err(format!("unsupported input event: {}", element.tag_name()));
373 }
374 }
375
376 handler.forget();
377 }
378 "change" => {
379 let handler = Closure::wrap(Box::new(move |e: InputEvent| {
380 let wrap_event = RespoEvent::Input {
381 value: e
382 .target()
383 .expect("to reach event target")
384 .dyn_ref::<HtmlInputElement>()
385 .expect("to convert to html input element")
386 .value(),
387 original_event: e,
388 };
389 handle_event
390 .run(RespoEventMark::new("change", &coord, wrap_event))
391 .expect("handle change event");
392 }) as Box<dyn FnMut(InputEvent)>);
393 match element.tag_name().as_str() {
394 "INPUT" => {
395 element
396 .dyn_ref::<HtmlInputElement>()
397 .expect("convert to html input element")
398 .set_onchange(Some(handler.as_ref().unchecked_ref()));
399 handler.forget();
400 }
401 "TEXTAREA" => {
402 element
403 .dyn_ref::<HtmlTextAreaElement>()
404 .expect("convert to html input element")
405 .set_onchange(Some(handler.as_ref().unchecked_ref()));
406 handler.forget();
407 }
408 _ => {
409 util::warn_log!("not handled change event for element: {}", element.tag_name());
410 }
411 }
412 }
413 "keydown" => {
414 let handler = Closure::wrap(Box::new(move |e: KeyboardEvent| {
415 let wrap_event = RespoEvent::Keyboard {
417 key: e.key(),
418 key_code: e.key_code(),
419 shift_key: e.shift_key(),
420 ctrl_key: e.ctrl_key(),
421 alt_key: e.alt_key(),
422 meta_key: e.meta_key(),
423 repeat: e.repeat(),
424 original_event: e,
425 };
426 handle_event
427 .run(RespoEventMark::new("keydown", &coord, wrap_event))
428 .expect("handle keydown event");
429 }) as Box<dyn FnMut(KeyboardEvent)>);
430
431 match element.tag_name().as_str() {
432 "INPUT" => {
433 element
434 .dyn_ref::<HtmlInputElement>()
435 .expect("convert to html input element")
436 .set_onkeydown(Some(handler.as_ref().unchecked_ref()));
437 handler.forget();
438 }
439 "TEXTAREA" => {
440 element
441 .dyn_ref::<HtmlTextAreaElement>()
442 .expect("convert to html input element")
443 .set_onkeydown(Some(handler.as_ref().unchecked_ref()));
444 handler.forget();
445 }
446 _ => {
447 util::warn_log!("not handled keydown event for element: {}", element.tag_name());
448 }
449 }
450 }
451 "keyup" => {
452 let handler = Closure::wrap(Box::new(move |e: KeyboardEvent| {
453 let wrap_event = RespoEvent::Keyboard {
454 key: e.key(),
455 key_code: e.key_code(),
456 shift_key: e.shift_key(),
457 ctrl_key: e.ctrl_key(),
458 alt_key: e.alt_key(),
459 meta_key: e.meta_key(),
460 repeat: e.repeat(),
461 original_event: e,
462 };
463 handle_event
464 .run(RespoEventMark::new("keyup", &coord, wrap_event))
465 .expect("handle keyup event");
466 }) as Box<dyn FnMut(KeyboardEvent)>);
467 match element.tag_name().as_str() {
468 "INPUT" => {
469 element
470 .dyn_ref::<HtmlInputElement>()
471 .expect("convert to html input element")
472 .set_onkeyup(Some(handler.as_ref().unchecked_ref()));
473 handler.forget();
474 }
475 "TEXTAREA" => {
476 element
477 .dyn_ref::<HtmlTextAreaElement>()
478 .expect("convert to html input element")
479 .set_onkeyup(Some(handler.as_ref().unchecked_ref()));
480 handler.forget();
481 }
482 _ => {
483 util::warn_log!("not handled keyup event for element: {}", element.tag_name());
484 }
485 }
486 }
487 "keypress" => {
488 let handler = Closure::wrap(Box::new(move |e: KeyboardEvent| {
489 let wrap_event = RespoEvent::Keyboard {
490 key: e.key(),
491 key_code: e.key_code(),
492 shift_key: e.shift_key(),
493 ctrl_key: e.ctrl_key(),
494 alt_key: e.alt_key(),
495 meta_key: e.meta_key(),
496 repeat: e.repeat(),
497 original_event: e,
498 };
499 handle_event
500 .run(RespoEventMark::new("keypress", &coord, wrap_event))
501 .expect("handle keypress event");
502 }) as Box<dyn FnMut(KeyboardEvent)>);
503 match element.tag_name().as_str() {
504 "INPUT" => {
505 element
506 .dyn_ref::<HtmlInputElement>()
507 .expect("convert to html input element")
508 .set_onkeypress(Some(handler.as_ref().unchecked_ref()));
509 handler.forget();
510 }
511 "TEXTAREA" => {
512 element
513 .dyn_ref::<HtmlTextAreaElement>()
514 .expect("convert to html input element")
515 .set_onkeypress(Some(handler.as_ref().unchecked_ref()));
516 handler.forget();
517 }
518 _ => {
519 util::warn_log!("not handled keypress event for element: {}", element.tag_name());
520 }
521 }
522 }
523 "focus" => {
524 let handler = Closure::wrap(Box::new(move |e: FocusEvent| {
525 handle_event
526 .run(RespoEventMark::new("focus", &coord, RespoEvent::Focus(e)))
527 .expect("handle focus event");
528 }) as Box<dyn FnMut(FocusEvent)>);
529 element
530 .dyn_ref::<HtmlInputElement>()
531 .expect("convert to html input element")
532 .set_onfocus(Some(handler.as_ref().unchecked_ref()));
533 handler.forget();
534 }
535 "blur" => {
536 let handler = Closure::wrap(Box::new(move |e: FocusEvent| {
537 handle_event
538 .run(RespoEventMark::new("blur", &coord, RespoEvent::Focus(e)))
539 .expect("handle blur event");
540 }) as Box<dyn FnMut(FocusEvent)>);
541 element
542 .dyn_ref::<HtmlInputElement>()
543 .expect("convert to html input element")
544 .set_onblur(Some(handler.as_ref().unchecked_ref()));
545 handler.forget();
546 }
547 _ => {
548 warn_1(&format!("unhandled event: {}", key).into());
549 }
550 }
551 Ok(())
552}