From 53cafefdef836d5193bdbe7623a16b346896b7a2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 23 Feb 2024 17:38:04 -0800 Subject: [PATCH] wip: add tests and slightly refactor interpreter code --- examples/form.rs | 17 +- packages/desktop/headless_tests/events.rs | 600 +++++++++++-------- packages/desktop/headless_tests/rendering.rs | 25 +- packages/desktop/headless_tests/utils.rs | 55 ++ packages/interpreter/src/interpreter.js | 295 +++++---- 5 files changed, 574 insertions(+), 418 deletions(-) create mode 100644 packages/desktop/headless_tests/utils.rs diff --git a/examples/form.rs b/examples/form.rs index 7da92e65c..7b305a7e7 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -4,19 +4,24 @@ //! in the "values" field. use dioxus::prelude::*; +use std::collections::HashMap; fn main() { - launch_desktop(app); + launch(app); } fn app() -> Element { + let mut values = use_signal(|| HashMap::new()); rsx! { div { h1 { "Form" } form { - onsubmit: move |ev| println!("Submitted {:?}", ev.values()), - oninput: move |ev| println!("Input {:?}", ev.values()), - input { r#type: "text", name: "username" } + oninput: move |ev| values.set(ev.values()), + input { + r#type: "text", + name: "username", + oninput: move |ev| values.set(ev.values()) + } input { r#type: "text", name: "full-name" } input { r#type: "password", name: "password" } input { r#type: "radio", name: "color", value: "red" } @@ -24,5 +29,9 @@ fn app() -> Element { button { r#type: "submit", value: "Submit", "Submit the form" } } } + div { + h1 { "Oninput Values" } + "{values:#?}" + } } } diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index f36320e7e..78d6a5568 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -1,73 +1,96 @@ +use std::{collections::HashMap, ops::Deref}; + use dioxus::html::geometry::euclid::Vector3D; use dioxus::prelude::*; use dioxus_core::prelude::consume_context; use dioxus_desktop::DesktopContext; +#[path = "./utils.rs"] +mod utils; + pub fn main() { - check_app_exits(app); + utils::check_app_exits(app); } -pub(crate) fn check_app_exits(app: fn() -> Element) { - use dioxus_desktop::tao::window::WindowBuilder; - use dioxus_desktop::Config; - // This is a deadman's switch to ensure that the app exits - let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); - let should_panic_clone = should_panic.clone(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(60)); - if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { - eprintln!("App did not exit in time"); - std::process::exit(exitcode::SOFTWARE); - } - }); +static RECEIVED_EVENTS: GlobalSignal = Signal::global(|| 0); - LaunchBuilder::desktop() - .with_cfg(Config::new().with_window(WindowBuilder::new().with_visible(true))) - .launch(app); - - // Stop deadman's switch - should_panic.store(false, std::sync::atomic::Ordering::SeqCst); -} - -fn mock_event(id: &'static str, value: &'static str) { - use_hook(move || { - spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(5000)).await; - - let js = format!( - r#" - //console.log("ran"); - // Dispatch a synthetic event - let event = {}; - let element = document.getElementById('{}'); - console.log(element, event); - element.dispatchEvent(event); - "#, - value, id - ); - - eval(&js).await.unwrap(); - }); - }) -} - -#[allow(deprecated)] fn app() -> Element { let desktop_context: DesktopContext = consume_context(); - let mut received_events = use_signal(|| 0); - // button - mock_event( + let received = RECEIVED_EVENTS(); + let expected = utils::EXPECTED_EVENTS(); + + if expected != 0 && received == expected { + println!("all events recieved"); + desktop_context.close(); + } + + rsx! { + div { + test_mounted {} + test_button {} + test_mouse_move_div {} + test_mouse_click_div {} + test_mouse_dblclick_div {} + test_mouse_down_div {} + test_mouse_up_div {} + test_mouse_scroll_div {} + test_key_down_div {} + test_key_up_div {} + test_key_press_div {} + test_focus_in_div {} + test_focus_out_div {} + test_form_input {} + } + } +} + +fn test_mounted() -> Element { + rsx! { + div { + width: "100px", + height: "100px", + onmounted: move |evt| async move { + let rect = evt.get_client_rect().await.unwrap(); + println!("rect: {:?}", rect); + assert_eq!(rect.width(), 100.0); + assert_eq!(rect.height(), 100.0); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_button() -> Element { + utils::mock_event( "button", r#"new MouseEvent("click", { - view: window, - bubbles: true, - cancelable: true, - button: 0, + view: window, + bubbles: true, + cancelable: true, + button: 0, })"#, ); - // mouse_move_div - mock_event( + + rsx! { + button { + id: "button", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_move_div() -> Element { + utils::mock_event( "mouse_move_div", r#"new MouseEvent("mousemove", { view: window, @@ -76,8 +99,27 @@ fn app() -> Element { buttons: 2, })"#, ); - // mouse_click_div - mock_event( + + rsx! { + div { + id: "mouse_move_div", + onmousemove: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_click_div() -> Element { + utils::mock_event( "mouse_click_div", r#"new MouseEvent("click", { view: window, @@ -87,8 +129,31 @@ fn app() -> Element { button: 2, })"#, ); - // mouse_dblclick_div - mock_event( + + rsx! { + div { + id: "mouse_click_div", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_dblclick_div() -> Element { + utils::mock_event( "mouse_dblclick_div", r#"new MouseEvent("dblclick", { view: window, @@ -98,8 +163,34 @@ fn app() -> Element { button: 2, })"#, ); - // mouse_down_div - mock_event( + + rsx! { + div { + id: "mouse_dblclick_div", + ondoubleclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary), + ); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_down_div() -> Element { + utils::mock_event( "mouse_down_div", r#"new MouseEvent("mousedown", { view: window, @@ -109,8 +200,31 @@ fn app() -> Element { button: 2, })"#, ); - // mouse_up_div - mock_event( + + rsx! { + div { + id: "mouse_down_div", + onmousedown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_up_div() -> Element { + utils::mock_event( "mouse_up_div", r#"new MouseEvent("mouseup", { view: window, @@ -120,8 +234,26 @@ fn app() -> Element { button: 0, })"#, ); - // wheel_div - mock_event( + + rsx! { + div { + id: "mouse_up_div", + onmouseup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_scroll_div() -> Element { + utils::mock_event( "wheel_div", r#"new WheelEvent("wheel", { view: window, @@ -132,8 +264,26 @@ fn app() -> Element { bubbles: true, })"#, ); - // key_down_div - mock_event( + + rsx! { + div { + id: "wheel_div", + width: "100px", + height: "100px", + background_color: "red", + onwheel: move |event| { + println!("{:?}", event.data); + let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else { + panic!("Expected delta to be in pixels") }; + assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_key_down_div() -> Element { + utils::mock_event( "key_down_div", r#"new KeyboardEvent("keydown", { key: "a", @@ -153,8 +303,24 @@ fn app() -> Element { bubbles: true, })"#, ); - // key_up_div - mock_event( + rsx! { + input { + id: "key_down_div", + onkeydown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location(), Location::Standard); + assert!(event.data.is_auto_repeating()); + assert!(event.data.is_composing()); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} +fn test_key_up_div() -> Element { + utils::mock_event( "key_up_div", r#"new KeyboardEvent("keyup", { key: "a", @@ -174,8 +340,25 @@ fn app() -> Element { bubbles: true, })"#, ); - // key_press_div - mock_event( + + rsx! { + input { + id: "key_up_div", + onkeyup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location(), Location::Standard); + assert!(!event.data.is_auto_repeating()); + assert!(!event.data.is_composing()); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} +fn test_key_press_div() -> Element { + utils::mock_event( "key_press_div", r#"new KeyboardEvent("keypress", { key: "a", @@ -195,197 +378,94 @@ fn app() -> Element { bubbles: true, })"#, ); - // focus_in_div - mock_event( - "focus_in_div", - r#"new FocusEvent("focusin", {bubbles: true})"#, - ); - // focus_out_div - mock_event( - "focus_out_div", - r#"new FocusEvent("focusout",{bubbles: true})"#, - ); - - if received_events() == 13 { - println!("all events recieved"); - desktop_context.close(); - } - rsx! { - div { - div { - width: "100px", - height: "100px", - onmounted: move |evt| async move { - let rect = evt.get_client_rect().await.unwrap(); - println!("rect: {:?}", rect); - assert_eq!(rect.width(), 100.0); - assert_eq!(rect.height(), 100.0); - received_events.with_mut(|x| *x += 1); - } - } - button { - id: "button", - onclick: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().is_empty()); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Primary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "mouse_move_div", - onmousemove: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!( - event - .data - .held_buttons() - .contains(dioxus_html::input_data::MouseButton::Secondary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "mouse_click_div", - onclick: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!( - event - .data - .held_buttons() - .contains(dioxus_html::input_data::MouseButton::Secondary), - ); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Secondary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "mouse_dblclick_div", - ondoubleclick: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!( - event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary), - ); - assert!( - event - .data - .held_buttons() - .contains(dioxus_html::input_data::MouseButton::Secondary), - ); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Secondary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "mouse_down_div", - onmousedown: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!( - event - .data - .held_buttons() - .contains(dioxus_html::input_data::MouseButton::Secondary), - ); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Secondary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "mouse_up_div", - onmouseup: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().is_empty()); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Primary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "wheel_div", - width: "100px", - height: "100px", - background_color: "red", - onwheel: move |event| { - println!("{:?}", event.data); - let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else { - panic!("Expected delta to be in pixels") }; - assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "key_down_div", - onkeydown: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert_eq!(event.data.key().to_string(), "a"); - assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location(), Location::Standard); - assert!(event.data.is_auto_repeating()); - assert!(event.data.is_composing()); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "key_up_div", - onkeyup: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert_eq!(event.data.key().to_string(), "a"); - assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location(), Location::Standard); - assert!(!event.data.is_auto_repeating()); - assert!(!event.data.is_composing()); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "key_press_div", - onkeypress: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert_eq!(event.data.key().to_string(), "a"); - assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location(), Location::Standard); - assert!(!event.data.is_auto_repeating()); - assert!(!event.data.is_composing()); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "focus_in_div", - onfocusin: move |event| { - println!("{:?}", event.data); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "focus_out_div", - onfocusout: move |event| { - println!("{:?}", event.data); - received_events.with_mut(|x| *x += 1); - } + input { + id: "key_press_div", + onkeypress: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location(), Location::Standard); + assert!(!event.data.is_auto_repeating()); + assert!(!event.data.is_composing()); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_focus_in_div() -> Element { + utils::mock_event( + "focus_in_div", + r#"new FocusEvent("focusin", {bubbles: true})"#, + ); + + rsx! { + input { + id: "focus_in_div", + onfocusin: move |event| { + println!("{:?}", event.data); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_focus_out_div() -> Element { + utils::mock_event( + "focus_out_div", + r#"new FocusEvent("focusout",{bubbles: true})"#, + ); + rsx! { + input { + id: "focus_out_div", + onfocusout: move |event| { + println!("{:?}", event.data); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_form_input() -> Element { + let mut values = use_signal(|| HashMap::new()); + + utils::mock_event_with_extra( + "form-username", + r#"new Event("input", { + bubbles: true, + cancelable: true, + composed: true, + })"#, + r#"element.value = "hello";"#, + ); + + let set_values = move |ev: FormEvent| { + values.set(ev.values()); + eprintln!("values: {:?}", values); + values.with_mut(|x| { + assert_eq!(x.get("username").unwrap().deref(), &["hello"]); + }); + }; + + rsx! { + div { + h1 { "Form" } + form { + id: "form", + oninput: move |ev| values.set(ev.values()), + input { + r#type: "text", + name: "username", + id: "form-username", + oninput: set_values, + } + input { r#type: "text", name: "full-name" } + input { r#type: "password", name: "password" } + input { r#type: "radio", name: "color", value: "red" } + input { r#type: "radio", name: "color", value: "blue" } + button { r#type: "submit", value: "Submit", "Submit the form" } } } } diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 882c70d21..15e2b9f59 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -2,28 +2,11 @@ use dioxus::prelude::*; use dioxus_core::Element; use dioxus_desktop::DesktopContext; +#[path = "./utils.rs"] +mod utils; + fn main() { - check_app_exits(check_html_renders); -} - -pub(crate) fn check_app_exits(app: fn() -> Element) { - use dioxus_desktop::Config; - use tao::window::WindowBuilder; - // This is a deadman's switch to ensure that the app exits - let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); - let should_panic_clone = should_panic.clone(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(5)); - if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { - std::process::exit(exitcode::SOFTWARE); - } - }); - - LaunchBuilder::desktop() - .with_cfg(Config::new().with_window(WindowBuilder::new().with_visible(true))) - .launch(app); - - should_panic.store(false, std::sync::atomic::Ordering::SeqCst); + utils::check_app_exits(check_html_renders); } fn use_inner_html(id: &'static str) -> Option { diff --git a/packages/desktop/headless_tests/utils.rs b/packages/desktop/headless_tests/utils.rs new file mode 100644 index 000000000..cd14a2fc1 --- /dev/null +++ b/packages/desktop/headless_tests/utils.rs @@ -0,0 +1,55 @@ +use dioxus::prelude::*; +use dioxus_core::Element; + +pub fn check_app_exits(app: fn() -> Element) { + use dioxus_desktop::tao::window::WindowBuilder; + use dioxus_desktop::Config; + // This is a deadman's switch to ensure that the app exits + let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let should_panic_clone = should_panic.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(60)); + if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { + eprintln!("App did not exit in time"); + std::process::exit(exitcode::SOFTWARE); + } + }); + + LaunchBuilder::desktop() + .with_cfg(Config::new().with_window(WindowBuilder::new().with_visible(true))) + .launch(app); + + // Stop deadman's switch + should_panic.store(false, std::sync::atomic::Ordering::SeqCst); +} + +pub static EXPECTED_EVENTS: GlobalSignal = Signal::global(|| 0); + +pub fn mock_event(id: &'static str, value: &'static str) { + mock_event_with_extra(id, value, ""); +} + +pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'static str) { + EXPECTED_EVENTS.with_mut(|x| *x += 1); + + use_hook(move || { + spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(5000)).await; + + let js = format!( + r#" + //console.log("ran"); + // Dispatch a synthetic event + let event = {}; + let element = document.getElementById('{}'); + console.log(element, event); + {} + element.dispatchEvent(event); + "#, + value, id, extra + ); + + eval(&js).await.unwrap(); + }); + }) +} diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 0e9800645..977df8ca2 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -8,155 +8,182 @@ class InterpreterConfig { // method is not used by the web implementation async function handler(event, name, bubbles, config) { let target = event.target; - if (target != null) { - let preventDefaultRequests = null; - // Some events can be triggered on text nodes, which don't have attributes - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); - } + if (target == null) { + return; + } - if (event.type === "click") { - // todo call prevent default if it's the right type of event - if (config.intercept_link_redirects) { - let a_element = target.closest("a"); - if (a_element != null) { - event.preventDefault(); + const realId = find_real_id(target); + if (realId === null) { + return; + } - let elementShouldPreventDefault = - preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - let aElementShouldPreventDefault = a_element.getAttribute( - `dioxus-prevent-default` + prevent_defaults(event, target, config); + + let contents = await serialize_event(event); + + // TODO: this should be liveview only + if ( + target.tagName === "INPUT" && + (event.type === "change" || event.type === "input") + ) { + const type = target.getAttribute("type"); + + if (type === "file") { + async function read_files() { + const files = target.files; + const file_contents = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) ); - let linkShouldPreventDefault = - aElementShouldPreventDefault && - aElementShouldPreventDefault.includes(`onclick`); - - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - window.interpreter.serializeIpcMessage("browser_open", { href }) - ); - } - } } - } + let file_engine = { + files: file_contents, + }; + contents.files = file_engine; - // also prevent buttons from submitting - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - } - - const realId = find_real_id(target); - - if ( - preventDefaultRequests && - preventDefaultRequests.includes(`on${event.type}`) - ) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - let contents = await serialize_event(event); - - // TODO: this should be liveview only - if ( - target.tagName === "INPUT" && - (event.type === "change" || event.type === "input") - ) { - const type = target.getAttribute("type"); - if (type === "file") { - async function read_files() { - const files = target.files; - const file_contents = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - let file_engine = { - files: file_contents, - }; - contents.files = file_engine; - - if (realId === null) { - return; - } - const message = window.interpreter.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }); - window.ipc.postMessage(message); + if (realId === null) { + return; } - read_files(); - return; + const message = window.interpreter.serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + window.ipc.postMessage(message); } - } - - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - const formData = new FormData(target); - - for (let name of formData.keys()) { - const fieldType = target.elements[name].type; - - switch (fieldType) { - case "select-multiple": - contents.values[name] = formData.getAll(name); - break; - - // add cases for fieldTypes that can hold multiple values here - default: - contents.values[name] = formData.get(name); - break; - } - } - } - - if ( - target.tagName === "SELECT" && - event.type === "input" - ) { - const selectData = target.options; - contents.values["options"] = []; - for (let i = 0; i < selectData.length; i++) { - let option = selectData[i]; - if (option.selected) { - contents.values["options"].push(option.value.toString()); - } - } - } - - if (realId === null) { + read_files(); return; } - window.ipc.postMessage( - window.interpreter.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }) - ); + } + + if ( + target.tagName === "FORM" && + (event.type === "submit" || event.type === "input") + ) { + const formData = new FormData(target); + + for (let name of formData.keys()) { + const fieldType = target.elements[name].type; + + switch (fieldType) { + case "select-multiple": + contents.values[name] = formData.getAll(name); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + contents.values[name] = formData.get(name); + break; + } + } + } + + if ( + target.tagName === "SELECT" && + event.type === "input" + ) { + const selectData = target.options; + contents.values["options"] = []; + for (let i = 0; i < selectData.length; i++) { + let option = selectData[i]; + if (option.selected) { + contents.values["options"].push(option.value.toString()); + } + } + } + + window.ipc.postMessage( + window.interpreter.serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }) + ); +} + +// Do our best to prevent the default action of the event +// This should: +// - prevent form submissions from navigating +// - prevent anchor tags from navigating +// - prevent buttons from submitting forms +function prevent_defaults(event, target, config) { + let preventDefaultRequests = null; + + // Some events can be triggered on text nodes, which don't have attributes + if (target instanceof Element) { + preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); + } + + if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { + event.preventDefault(); + } + + if (event.type === "submit") { + event.preventDefault(); + } + + // Attempt to intercept if the event is a click + intercept_form_submit(event, target, config, preventDefaultRequests); +} + +function intercept_form_submit(event, target, config, preventDefaultRequests) { + if (event.type !== "click") { + return; + } + + // todo call prevent default if it's the right type of event + if (!config.intercept_link_redirects) { + return; + } + + // prevent buttons in forms from submitting the form + if (target.tagName === "BUTTON" && event.type == "submit") { + event.preventDefault(); + } + + // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating + let a_element = target.closest("a"); + + if (a_element == null) { + return; + } + + event.preventDefault(); + + let elementShouldPreventDefault = + preventDefaultRequests && preventDefaultRequests.includes(`onclick`); + + let aElementShouldPreventDefault = a_element.getAttribute( + `dioxus-prevent-default` + ); + + let linkShouldPreventDefault = + aElementShouldPreventDefault && + aElementShouldPreventDefault.includes(`onclick`); + + if (!elementShouldPreventDefault && !linkShouldPreventDefault) { + const href = a_element.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.ipc.postMessage( + window.interpreter.serializeIpcMessage("browser_open", { href }) + ); + } } } function find_real_id(target) { let realId = null; + if (target instanceof Element) { realId = target.getAttribute(`data-dioxus-id`); } + // walk the tree to find the real element while (realId == null) { // we've reached the root we don't want to send an event @@ -169,6 +196,7 @@ function find_real_id(target) { realId = target.getAttribute(`data-dioxus-id`); } } + return realId; } @@ -421,7 +449,8 @@ async function serialize_event(event) { case "drop": { let files = null; if (event.dataTransfer && event.dataTransfer.files) { - files = await serializeFileList(event.dataTransfer.files); + files = ["a", "b", "c"]; + // files = await serializeFileList(event.dataTransfer.files); } return { mouse: get_mouse_data(event), files };