Переглянути джерело

Merge pull request #452 from Demonthos/fix_nonbubbling_web_events

Fix nonbubbling web events
Jon Kelley 3 роки тому
батько
коміт
83288e274f

+ 3 - 0
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<dyn Any + Send + Sync>,
 }

+ 5 - 1
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;
+                            }
                         }
                     }
 

+ 3 - 1
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,
     }
 }

+ 88 - 0
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),
+    }
+}

+ 8 - 2
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);

+ 239 - 18
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;
+  }
+}

+ 2 - 0
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),
     }
 }
 

+ 6 - 1
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),
                         });
                     }
                 }

+ 12 - 4
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)

+ 2 - 0
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),
                     );
                 }