diff --git a/packages/html/src/events/pointer.rs b/packages/html/src/events/pointer.rs index 5540dbe62..ec351d3dc 100644 --- a/packages/html/src/events/pointer.rs +++ b/packages/html/src/events/pointer.rs @@ -10,6 +10,18 @@ pub struct PointerData { inner: Box, } +impl PointerData { + /// Create a new PointerData + pub fn new(data: impl HasPointerData + 'static) -> Self { + Self::from(data) + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + impl From for PointerData { fn from(e: E) -> Self { Self { inner: Box::new(e) } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index fb0dfef46..9799b50be 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -9,19 +9,14 @@ use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{ - event_bubbles, FileEngine, FormData, HasFormData, HasImageData, HtmlEventConverter, ImageData, - MountedData, PlatformEventData, ScrollData, -}; -use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel}; +use dioxus_html::{event_bubbles, PlatformEventData}; +use dioxus_interpreter_js::{minimal_bindings, save_template, Channel}; use futures_channel::mpsc; -use js_sys::Array; use rustc_hash::FxHashMap; -use std::{any::Any, collections::HashMap}; -use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast, JsValue}; +use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{Document, Element, Event}; -use crate::Config; +use crate::{load_document, virtual_event_from_websys_event, Config, WebEventConverter}; pub struct WebsysDom { document: Document, @@ -30,6 +25,7 @@ pub struct WebsysDom { templates: FxHashMap, max_template_id: u32, pub(crate) interpreter: Channel, + #[cfg(feature = "mounted")] event_channel: mpsc::UnboundedSender, } @@ -104,6 +100,7 @@ impl WebsysDom { interpreter, templates: FxHashMap::default(), max_template_id: 0, + #[cfg(feature = "mounted")] event_channel, } } @@ -253,10 +250,10 @@ impl WebsysDom { #[cfg(feature = "mounted")] for id in to_mount { - let node = get_node(id.0 as u32); + let node = dioxus_interpreter_js::get_node(id.0 as u32); if let Some(element) = node.dyn_ref::() { log::info!("mounted event fired: {}", id.0); - let data: MountedData = element.into(); + let data: dioxus_html::MountedData = element.into(); let data = PlatformEventData::new(Box::new(data)); let _ = self.event_channel.unbounded_send(UiEvent { name: "mounted".to_string(), @@ -269,283 +266,6 @@ impl WebsysDom { } } -struct WebEventConverter; - -fn downcast_event(event: &dioxus_html::PlatformEventData) -> &GenericWebSysEvent { - event - .downcast::() - .expect("event should be a GenericWebSysEvent") -} - -impl HtmlEventConverter for WebEventConverter { - fn convert_animation_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::AnimationData { - downcast_event(event).raw.clone().into() - } - - fn convert_clipboard_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::ClipboardData { - downcast_event(event).raw.clone().into() - } - - fn convert_composition_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::CompositionData { - downcast_event(event).raw.clone().into() - } - - fn convert_drag_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::DragData { - downcast_event(event).raw.clone().into() - } - - fn convert_focus_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FocusData { - downcast_event(event).raw.clone().into() - } - - fn convert_form_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FormData { - let event = downcast_event(event); - FormData::new(WebFormData::new(event.element.clone(), event.raw.clone())) - } - - fn convert_image_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::ImageData { - let event = downcast_event(event); - let error = event.raw.type_() == "error"; - ImageData::new(WebImageEvent::new(event.raw.clone(), error)) - } - - fn convert_keyboard_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::KeyboardData { - downcast_event(event).raw.clone().into() - } - - fn convert_media_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MediaData { - downcast_event(event).raw.clone().into() - } - - fn convert_mounted_data(&self, event: &dioxus_html::PlatformEventData) -> MountedData { - #[cfg(feature = "mounted")] - { - MountedData::from(downcast_event(event).element.clone()) - } - #[cfg(not(feature = "mounted"))] - { - panic!("mounted events are not supported without the mounted feature on the dioxus-web crate enabled") - } - } - - fn convert_mouse_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MouseData { - downcast_event(event).raw.clone().into() - } - - fn convert_pointer_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::PointerData { - downcast_event(event).raw.clone().into() - } - - fn convert_scroll_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::ScrollData { - ScrollData::from(downcast_event(event).raw.clone()) - } - - fn convert_selection_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::SelectionData { - downcast_event(event).raw.clone().into() - } - - fn convert_toggle_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::ToggleData { - downcast_event(event).raw.clone().into() - } - - fn convert_touch_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::TouchData { - downcast_event(event).raw.clone().into() - } - - fn convert_transition_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::TransitionData { - downcast_event(event).raw.clone().into() - } - - fn convert_wheel_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::WheelData { - downcast_event(event).raw.clone().into() - } -} - -struct GenericWebSysEvent { - raw: Event, - element: Element, -} - -// todo: some of these events are being casted to the wrong event type. -// We need tests that simulate clicks/etc and make sure every event type works. -pub fn virtual_event_from_websys_event( - event: web_sys::Event, - target: Element, -) -> PlatformEventData { - PlatformEventData::new(Box::new(GenericWebSysEvent { - raw: event, - element: target, - })) -} - -pub(crate) fn load_document() -> Document { - web_sys::window() - .expect("should have access to the Window") - .document() - .expect("should have access to the Document") -} - -struct WebImageEvent { - raw: Event, - error: bool, -} - -impl WebImageEvent { - fn new(raw: Event, error: bool) -> Self { - Self { raw, error } - } -} - -impl HasImageData for WebImageEvent { - fn load_error(&self) -> bool { - self.error - } - - fn as_any(&self) -> &dyn Any { - &self.raw as &dyn Any - } -} - -struct WebFormData { - element: Element, - raw: Event, -} - -impl WebFormData { - fn new(element: Element, raw: Event) -> Self { - Self { element, raw } - } -} - -impl HasFormData for WebFormData { - fn value(&self) -> String { - let target = &self.element; - target - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - // todo: special case more input types - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => "true".to_string(), - false => "false".to_string(), - } - }, - _ => { - input.value() - } - } - }) - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlTextAreaElement| input.value()) - }) - // select elements are NOT input events - because - why woudn't they be?? - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlSelectElement| input.value()) - }) - .or_else(|| { - target - .dyn_ref::() - .unwrap() - .text_content() - }) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener") - } - - fn values(&self) -> HashMap> { - let mut values = std::collections::HashMap::new(); - - // try to fill in form values - if let Some(form) = self.element.dyn_ref::() { - let form_data = get_form_data(form); - for value in form_data.entries().into_iter().flatten() { - if let Ok(array) = value.dyn_into::() { - if let Some(name) = array.get(0).as_string() { - if let Ok(item_values) = array.get(1).dyn_into::() { - let item_values = - item_values.iter().filter_map(|v| v.as_string()).collect(); - - values.insert(name, item_values); - } - } - } - } - } - - values - } - - fn files(&self) -> Option> { - #[cfg(not(feature = "file_engine"))] - let files = None; - #[cfg(feature = "file_engine")] - let files = self - .element - .dyn_ref() - .and_then(|input: &web_sys::HtmlInputElement| { - input.files().and_then(|files| { - crate::file_engine::WebFileEngine::new(files).map(|f| { - std::sync::Arc::new(f) as std::sync::Arc - }) - }) - }); - - files - } - - fn as_any(&self) -> &dyn Any { - &self.raw as &dyn Any - } -} - -// web-sys does not expose the keys api for form data, so we need to manually bind to it -#[wasm_bindgen(inline_js = r#" - export function get_form_data(form) { - let values = new Map(); - const formData = new FormData(form); - - for (let name of formData.keys()) { - values.set(name, formData.getAll(name)); - } - - return values; - } -"#)] -extern "C" { - fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map; -} - fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> { let target = event .target() diff --git a/packages/web/src/event.rs b/packages/web/src/event.rs new file mode 100644 index 000000000..8a4a2c204 --- /dev/null +++ b/packages/web/src/event.rs @@ -0,0 +1,420 @@ +use std::{any::Any, collections::HashMap}; + +use dioxus_html::{ + FileEngine, FormData, HasFormData, HasImageData, HtmlEventConverter, ImageData, MountedData, + PlatformEventData, ScrollData, +}; +use js_sys::Array; +use wasm_bindgen::{prelude::wasm_bindgen, JsCast}; +use web_sys::{Document, Element, Event}; + +pub(crate) struct WebEventConverter; + +fn downcast_event(event: &dioxus_html::PlatformEventData) -> &GenericWebSysEvent { + event + .downcast::() + .expect("event should be a GenericWebSysEvent") +} + +impl HtmlEventConverter for WebEventConverter { + fn convert_animation_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::AnimationData { + downcast_event(event).raw.clone().into() + } + + fn convert_clipboard_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::ClipboardData { + downcast_event(event).raw.clone().into() + } + + fn convert_composition_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::CompositionData { + downcast_event(event).raw.clone().into() + } + + fn convert_drag_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::DragData { + downcast_event(event).raw.clone().into() + } + + fn convert_focus_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FocusData { + downcast_event(event).raw.clone().into() + } + + fn convert_form_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FormData { + let event = downcast_event(event); + FormData::new(WebFormData::new(event.element.clone(), event.raw.clone())) + } + + fn convert_image_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::ImageData { + let event = downcast_event(event); + let error = event.raw.type_() == "error"; + ImageData::new(WebImageEvent::new(event.raw.clone(), error)) + } + + fn convert_keyboard_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::KeyboardData { + downcast_event(event).raw.clone().into() + } + + fn convert_media_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MediaData { + downcast_event(event).raw.clone().into() + } + + #[allow(unused_variables)] + fn convert_mounted_data(&self, event: &dioxus_html::PlatformEventData) -> MountedData { + #[cfg(feature = "mounted")] + { + MountedData::from(downcast_event(event).element.clone()) + } + #[cfg(not(feature = "mounted"))] + { + panic!("mounted events are not supported without the mounted feature on the dioxus-web crate enabled") + } + } + + fn convert_mouse_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MouseData { + downcast_event(event).raw.clone().into() + } + + fn convert_pointer_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::PointerData { + downcast_event(event).raw.clone().into() + } + + fn convert_scroll_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::ScrollData { + ScrollData::from(downcast_event(event).raw.clone()) + } + + fn convert_selection_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::SelectionData { + downcast_event(event).raw.clone().into() + } + + fn convert_toggle_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::ToggleData { + downcast_event(event).raw.clone().into() + } + + fn convert_touch_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::TouchData { + downcast_event(event).raw.clone().into() + } + + fn convert_transition_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::TransitionData { + downcast_event(event).raw.clone().into() + } + + fn convert_wheel_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::WheelData { + downcast_event(event).raw.clone().into() + } +} + +/// A extension trait for web-sys events that provides a way to get the event as a web-sys event. +pub trait WebEventExt { + /// Get the event as a web-sys event. + fn web_event(&self) -> &E; +} + +impl WebEventExt for dioxus_html::AnimationData { + fn web_event(&self) -> &web_sys::AnimationEvent { + self.downcast::() + .expect("event should be a WebAnimationEvent") + } +} + +impl WebEventExt for dioxus_html::ClipboardData { + fn web_event(&self) -> &web_sys::Event { + &self + .downcast::() + .expect("event should be a web_sys::Event") + } +} + +impl WebEventExt for dioxus_html::CompositionData { + fn web_event(&self) -> &web_sys::CompositionEvent { + self.downcast::() + .expect("event should be a WebCompositionEvent") + } +} + +impl WebEventExt for dioxus_html::DragData { + fn web_event(&self) -> &web_sys::MouseEvent { + self.downcast::() + .expect("event should be a WebMouseEvent") + } +} + +impl WebEventExt for dioxus_html::FocusData { + fn web_event(&self) -> &web_sys::FocusEvent { + self.downcast::() + .expect("event should be a WebFocusEvent") + } +} + +impl WebEventExt for dioxus_html::FormData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebFormData") + } +} + +impl WebEventExt for dioxus_html::ImageData { + fn web_event(&self) -> &WebImageEvent { + self.downcast::() + .expect("event should be a WebImageEvent") + } +} + +impl WebEventExt for dioxus_html::KeyboardData { + fn web_event(&self) -> &web_sys::KeyboardEvent { + self.downcast::() + .expect("event should be a WebKeyboardEvent") + } +} + +impl WebEventExt for dioxus_html::MediaData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebMediaEvent") + } +} + +impl WebEventExt for MountedData { + fn web_event(&self) -> &MountedData { + self.downcast::() + .expect("event should be a WebMountedEvent") + } +} + +impl WebEventExt for dioxus_html::MouseData { + fn web_event(&self) -> &web_sys::MouseEvent { + self.downcast::() + .expect("event should be a WebMouseEvent") + } +} + +impl WebEventExt for dioxus_html::PointerData { + fn web_event(&self) -> &web_sys::PointerEvent { + self.downcast::() + .expect("event should be a WebPointerEvent") + } +} + +impl WebEventExt for ScrollData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebScrollEvent") + } +} + +impl WebEventExt for dioxus_html::SelectionData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebSelectionEvent") + } +} + +impl WebEventExt for dioxus_html::ToggleData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebToggleEvent") + } +} + +impl WebEventExt for dioxus_html::TouchData { + fn web_event(&self) -> &web_sys::TouchEvent { + self.downcast::() + .expect("event should be a WebTouchEvent") + } +} + +impl WebEventExt for dioxus_html::TransitionData { + fn web_event(&self) -> &web_sys::TransitionEvent { + self.downcast::() + .expect("event should be a WebTransitionEvent") + } +} + +impl WebEventExt for dioxus_html::WheelData { + fn web_event(&self) -> &web_sys::WheelEvent { + self.downcast::() + .expect("event should be a WebWheelEvent") + } +} + +struct GenericWebSysEvent { + raw: Event, + element: Element, +} + +// todo: some of these events are being casted to the wrong event type. +// We need tests that simulate clicks/etc and make sure every event type works. +pub(crate) fn virtual_event_from_websys_event( + event: web_sys::Event, + target: Element, +) -> PlatformEventData { + PlatformEventData::new(Box::new(GenericWebSysEvent { + raw: event, + element: target, + })) +} + +pub(crate) fn load_document() -> Document { + web_sys::window() + .expect("should have access to the Window") + .document() + .expect("should have access to the Document") +} + +struct WebImageEvent { + raw: Event, + error: bool, +} + +impl WebImageEvent { + fn new(raw: Event, error: bool) -> Self { + Self { raw, error } + } +} + +impl HasImageData for WebImageEvent { + fn load_error(&self) -> bool { + self.error + } + + fn as_any(&self) -> &dyn Any { + &self.raw as &dyn Any + } +} + +struct WebFormData { + element: Element, + raw: Event, +} + +impl WebFormData { + fn new(element: Element, raw: Event) -> Self { + Self { element, raw } + } +} + +impl HasFormData for WebFormData { + fn value(&self) -> String { + let target = &self.element; + target + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => { + input.value() + } + } + }) + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + // select elements are NOT input events - because - why woudn't they be?? + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlSelectElement| input.value()) + }) + .or_else(|| { + target + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener") + } + + fn values(&self) -> HashMap> { + let mut values = std::collections::HashMap::new(); + + // try to fill in form values + if let Some(form) = self.element.dyn_ref::() { + let form_data = get_form_data(form); + for value in form_data.entries().into_iter().flatten() { + if let Ok(array) = value.dyn_into::() { + if let Some(name) = array.get(0).as_string() { + if let Ok(item_values) = array.get(1).dyn_into::() { + let item_values = + item_values.iter().filter_map(|v| v.as_string()).collect(); + + values.insert(name, item_values); + } + } + } + } + } + + values + } + + fn files(&self) -> Option> { + #[cfg(not(feature = "file_engine"))] + let files = None; + #[cfg(feature = "file_engine")] + let files = self + .element + .dyn_ref() + .and_then(|input: &web_sys::HtmlInputElement| { + input.files().and_then(|files| { + crate::file_engine::WebFileEngine::new(files).map(|f| { + std::sync::Arc::new(f) as std::sync::Arc + }) + }) + }); + + files + } + + fn as_any(&self) -> &dyn Any { + &self.raw as &dyn Any + } +} + +// web-sys does not expose the keys api for form data, so we need to manually bind to it +#[wasm_bindgen(inline_js = r#" + export function get_form_data(form) { + let values = new Map(); + const formData = new FormData(form); + + for (let name of formData.keys()) { + values.set(name, formData.getAll(name)); + } + + return values; + } +"#)] +extern "C" { + fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map; +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 016d805e3..002bdb916 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -69,6 +69,8 @@ mod cfg; mod dom; #[cfg(feature = "eval")] mod eval; +mod event; +pub use event::*; #[cfg(feature = "file_engine")] mod file_engine; #[cfg(all(feature = "hot_reload", debug_assertions))]