diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 0b24f5563..92c86dec8 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -63,6 +63,9 @@ pub struct UserEvent { /// The event type IE "onclick" or "onmouseover" pub name: &'static str, + /// If the event is bubbles up through the vdom + pub bubbles: bool, + /// The event data to be passed onto the event handler pub data: Arc, } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 8db5efe56..ba6afcfe2 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -335,7 +335,7 @@ impl ScopeArena { log::trace!("calling listener {:?}", listener.event); if state.canceled.get() { // stop bubbling if canceled - break; + return; } let mut cb = listener.callback.borrow_mut(); @@ -349,6 +349,10 @@ impl ScopeArena { data: event.data.clone(), }); } + + if !event.bubbles { + return; + } } } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index fd0675f10..803767a09 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -4,6 +4,7 @@ use std::any::Any; use std::sync::Arc; use dioxus_core::{ElementId, EventPriority, UserEvent}; +use dioxus_html::event_bubbles; use dioxus_html::on::*; #[derive(serde::Serialize, serde::Deserialize)] @@ -47,8 +48,8 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { } = serde_json::from_value(val).unwrap(); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); - let name = event_name_from_type(&event); + let event = make_synthetic_event(&event, contents); UserEvent { @@ -56,6 +57,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { priority: EventPriority::Low, scope_id: None, element: mounted_dom_id, + bubbles: event_bubbles(name), data: event, } } diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index beac81167..67522e87a 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -1324,3 +1324,91 @@ pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) { _ => (true, Low), } } + +pub fn event_bubbles(evt: &str) -> bool { + match evt { + "copy" => true, + "cut" => true, + "paste" => true, + "compositionend" => true, + "compositionstart" => true, + "compositionupdate" => true, + "keydown" => true, + "keypress" => true, + "keyup" => true, + "focus" => false, + "focusout" => true, + "focusin" => true, + "blur" => false, + "change" => true, + "input" => true, + "invalid" => true, + "reset" => true, + "submit" => true, + "click" => true, + "contextmenu" => true, + "doubleclick" => true, + "dblclick" => true, + "drag" => true, + "dragend" => true, + "dragenter" => false, + "dragexit" => false, + "dragleave" => true, + "dragover" => true, + "dragstart" => true, + "drop" => true, + "mousedown" => true, + "mouseenter" => false, + "mouseleave" => false, + "mousemove" => true, + "mouseout" => true, + "scroll" => false, + "mouseover" => true, + "mouseup" => true, + "pointerdown" => true, + "pointermove" => true, + "pointerup" => true, + "pointercancel" => true, + "gotpointercapture" => true, + "lostpointercapture" => true, + "pointerenter" => false, + "pointerleave" => false, + "pointerover" => true, + "pointerout" => true, + "select" => true, + "touchcancel" => true, + "touchend" => true, + "touchmove" => true, + "touchstart" => true, + "wheel" => true, + "abort" => false, + "canplay" => true, + "canplaythrough" => true, + "durationchange" => true, + "emptied" => true, + "encrypted" => true, + "ended" => true, + "error" => false, + "loadeddata" => true, + "loadedmetadata" => true, + "loadstart" => false, + "pause" => true, + "play" => true, + "playing" => true, + "progress" => false, + "ratechange" => true, + "seeked" => true, + "seeking" => true, + "stalled" => true, + "suspend" => true, + "timeupdate" => true, + "volumechange" => true, + "waiting" => true, + "animationstart" => true, + "animationend" => true, + "animationiteration" => true, + "transitionend" => true, + "toggle" => true, + _ => panic!("unsupported event type {:?}", evt), + } +} diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index a08ef9c13..80df47c2c 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -48,10 +48,16 @@ extern "C" { pub fn CreatePlaceholder(this: &Interpreter, root: u64); #[wasm_bindgen(method)] - pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function); + pub fn NewEventListener( + this: &Interpreter, + name: &str, + root: u64, + handler: &Function, + bubbles: bool, + ); #[wasm_bindgen(method)] - pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); + pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str, bubbles: bool); #[wasm_bindgen(method)] pub fn SetText(this: &Interpreter, root: u64, text: JsValue); diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 0c129efda..759e83c9f 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -5,11 +5,64 @@ export function main() { window.ipc.postMessage(serializeIpcMessage("initialize")); } } + +class ListenerMap { + constructor(root) { + // bubbling events can listen at the root element + this.global = {}; + // non bubbling events listen at the element the listener was created at + this.local = {}; + this.root = root; + } + + createBubbling(event_name, handler) { + if (this.global[event_name] === undefined) { + this.global[event_name] = {}; + this.global[event_name].active = 1; + this.global[event_name].callback = handler; + this.root.addEventListener(event_name, handler); + } else { + this.global[event_name].active++; + } + } + + createNonBubbling(event_name, element, handler) { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + this.local[id][event_name] = handler; + element.addEventListener(event_name, handler); + } + + removeBubbling(event_name) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + + removeNonBubbling(element, event_name) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (this.local[id].length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, handler); + } + + removeAllNonBubbling(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } +} + export class Interpreter { constructor(root) { this.root = root; this.stack = [root]; - this.listeners = {}; + this.listeners = new ListenerMap(root); this.handlers = {}; this.lastNodeWasText = false; this.nodes = [root]; @@ -40,6 +93,7 @@ export class Interpreter { ReplaceWith(root_id, m) { let root = this.nodes[root_id]; let els = this.stack.splice(this.stack.length - m); + this.listeners.removeAllNonBubbling(root); root.replaceWith(...els); } InsertAfter(root, n) { @@ -54,6 +108,7 @@ export class Interpreter { } Remove(root) { let node = this.nodes[root]; + this.listeners.removeAllNonBubbling(node); if (node !== undefined) { node.remove(); } @@ -79,25 +134,24 @@ export class Interpreter { this.stack.push(el); this.nodes[root] = el; } - NewEventListener(event_name, root, handler) { + NewEventListener(event_name, root, handler, bubbles) { const element = this.nodes[root]; element.setAttribute("data-dioxus-id", `${root}`); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 1; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } else { - this.listeners[event_name]++; + if (bubbles) { + this.listeners.createBubbling(event_name, handler); + } + else { + this.listeners.createNonBubbling(event_name, element, handler); } } - RemoveEventListener(root, event_name) { + RemoveEventListener(root, event_name, bubbles) { const element = this.nodes[root]; element.removeAttribute(`data-dioxus-id`); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; + if (bubbles) { + this.listeners.removeBubbling(event_name) + } + else { + this.listeners.removeNonBubbling(element, event_name); } } SetText(root, text) { @@ -198,12 +252,9 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": - console.log(this.listeners); - // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event) => { - console.log(event); let target = event.target; if (target != null) { @@ -292,7 +343,8 @@ export class Interpreter { ); } }; - this.NewEventListener(edit.event_name, edit.root, handler); + this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name)); + break; case "SetText": this.SetText(edit.root, edit.text); @@ -607,3 +659,172 @@ const bool_attrs = { selected: true, truespeed: true, }; + +function event_bubbles(event) { + switch (event) { + case "copy": + return true; + case "cut": + return true; + case "paste": + return true; + case "compositionend": + return true; + case "compositionstart": + return true; + case "compositionupdate": + return true; + case "keydown": + return true; + case "keypress": + return true; + case "keyup": + return true; + case "focus": + return false; + case "focusout": + return true; + case "focusin": + return true; + case "blur": + return false; + case "change": + return true; + case "input": + return true; + case "invalid": + return true; + case "reset": + return true; + case "submit": + return true; + case "click": + return true; + case "contextmenu": + return true; + case "doubleclick": + return true; + case "dblclick": + return true; + case "drag": + return true; + case "dragend": + return true; + case "dragenter": + return false; + case "dragexit": + return false; + case "dragleave": + return true; + case "dragover": + return true; + case "dragstart": + return true; + case "drop": + return true; + case "mousedown": + return true; + case "mouseenter": + return false; + case "mouseleave": + return false; + case "mousemove": + return true; + case "mouseout": + return true; + case "scroll": + return false; + case "mouseover": + return true; + case "mouseup": + return true; + case "pointerdown": + return true; + case "pointermove": + return true; + case "pointerup": + return true; + case "pointercancel": + return true; + case "gotpointercapture": + return true; + case "lostpointercapture": + return true; + case "pointerenter": + return false; + case "pointerleave": + return false; + case "pointerover": + return true; + case "pointerout": + return true; + case "select": + return true; + case "touchcancel": + return true; + case "touchend": + return true; + case "touchmove": + return true; + case "touchstart": + return true; + case "wheel": + return true; + case "abort": + return false; + case "canplay": + return true; + case "canplaythrough": + return true; + case "durationchange": + return true; + case "emptied": + return true; + case "encrypted": + return true; + case "ended": + return true; + case "error": + return false; + case "loadeddata": + return true; + case "loadedmetadata": + return true; + case "loadstart": + return false; + case "pause": + return true; + case "play": + return true; + case "playing": + return true; + case "progress": + return false; + case "ratechange": + return true; + case "seeked": + return true; + case "seeking": + return true; + case "stalled": + return true; + case "suspend": + return true; + case "timeupdate": + return true; + case "volumechange": + return true; + case "waiting": + return true; + case "animationstart": + return true; + case "animationend": + return true; + case "animationiteration": + return true; + case "transitionend": + return true; + case "toggle": + return true; + } +} \ No newline at end of file diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs index 10d1ffd38..2c10925b5 100644 --- a/packages/liveview/src/events.rs +++ b/packages/liveview/src/events.rs @@ -6,6 +6,7 @@ use std::any::Any; use std::sync::Arc; use dioxus_core::{ElementId, EventPriority, UserEvent}; +use dioxus_html::event_bubbles; use dioxus_html::on::*; #[derive(serde::Serialize, serde::Deserialize)] @@ -46,6 +47,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { scope_id: None, element: mounted_dom_id, data: event, + bubbles: event_bubbles(name), } } diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 03cfd90e2..6d633eab8 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -9,7 +9,7 @@ use dioxus_html::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, S use dioxus_html::input_data::keyboard_types::Modifiers; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; -use dioxus_html::{on::*, KeyCode}; +use dioxus_html::{event_bubbles, on::*, KeyCode}; use std::{ any::Any, cell::{RefCell, RefMut}, @@ -187,6 +187,7 @@ impl InnerInputState { name: "focus", element: Some(id), data: Arc::new(FocusData {}), + bubbles: event_bubbles("focus"), }); resolved_events.push(UserEvent { scope_id: None, @@ -194,6 +195,7 @@ impl InnerInputState { name: "focusin", element: Some(id), data: Arc::new(FocusData {}), + bubbles: event_bubbles("focusin"), }); } if let Some(id) = old_focus { @@ -203,6 +205,7 @@ impl InnerInputState { name: "focusout", element: Some(id), data: Arc::new(FocusData {}), + bubbles: event_bubbles("focusout"), }); } } @@ -248,6 +251,7 @@ impl InnerInputState { name, element: Some(node.id), data, + bubbles: event_bubbles(name), }) } } @@ -649,6 +653,7 @@ impl RinkInputHandler { name: event, element: Some(node.id), data: data.clone(), + bubbles: event_bubbles(event), }); } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ebcdefbcc..0e434c1a0 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -8,6 +8,7 @@ //! - Partial delegation?> use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; +use dioxus_html::event_bubbles; use dioxus_interpreter_js::Interpreter; use js_sys::Function; use std::{any::Any, rc::Rc, sync::Arc}; @@ -45,6 +46,7 @@ impl WebsysDom { element: Some(ElementId(id)), scope_id: None, priority: dioxus_core::EventPriority::Medium, + bubbles: event.bubbles(), }); } Some(Err(e)) => { @@ -64,6 +66,7 @@ impl WebsysDom { element: None, scope_id: None, priority: dioxus_core::EventPriority::Low, + bubbles: event.bubbles(), }); } } @@ -121,12 +124,17 @@ impl WebsysDom { event_name, root, .. } => { let handler: &Function = self.handler.as_ref().unchecked_ref(); - self.interpreter.NewEventListener(event_name, root, handler); + self.interpreter.NewEventListener( + event_name, + root, + handler, + event_bubbles(event_name), + ); } - DomEdit::RemoveEventListener { root, event } => { - self.interpreter.RemoveEventListener(root, event) - } + DomEdit::RemoveEventListener { root, event } => self + .interpreter + .RemoveEventListener(root, event, event_bubbles(event)), DomEdit::RemoveAttribute { root, name, ns } => { self.interpreter.RemoveAttribute(root, name, ns) diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 24e46d11e..a6b7df999 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -1,5 +1,6 @@ use crate::dom::WebsysDom; use dioxus_core::{VNode, VirtualDom}; +use dioxus_html::event_bubbles; use wasm_bindgen::JsCast; use web_sys::{Comment, Element, Node, Text}; @@ -111,6 +112,7 @@ impl WebsysDom { listener.event, listener.mounted_node.get().unwrap().as_u64(), self.handler.as_ref().unchecked_ref(), + event_bubbles(listener.event), ); }