Merge pull request #452 from Demonthos/fix_nonbubbling_web_events

Fix nonbubbling web events
This commit is contained in:
Jon Kelley 2022-06-24 17:05:32 -04:00 committed by GitHub
commit 83288e274f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 368 additions and 27 deletions

View file

@ -63,6 +63,9 @@ pub struct UserEvent {
/// The event type IE "onclick" or "onmouseover" /// The event type IE "onclick" or "onmouseover"
pub name: &'static str, 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 /// The event data to be passed onto the event handler
pub data: Arc<dyn Any + Send + Sync>, pub data: Arc<dyn Any + Send + Sync>,
} }

View file

@ -335,7 +335,7 @@ impl ScopeArena {
log::trace!("calling listener {:?}", listener.event); log::trace!("calling listener {:?}", listener.event);
if state.canceled.get() { if state.canceled.get() {
// stop bubbling if canceled // stop bubbling if canceled
break; return;
} }
let mut cb = listener.callback.borrow_mut(); let mut cb = listener.callback.borrow_mut();
@ -349,6 +349,10 @@ impl ScopeArena {
data: event.data.clone(), data: event.data.clone(),
}); });
} }
if !event.bubbles {
return;
}
} }
} }

View file

@ -4,6 +4,7 @@ use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
use dioxus_core::{ElementId, EventPriority, UserEvent}; use dioxus_core::{ElementId, EventPriority, UserEvent};
use dioxus_html::event_bubbles;
use dioxus_html::on::*; use dioxus_html::on::*;
#[derive(serde::Serialize, serde::Deserialize)] #[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(); } = serde_json::from_value(val).unwrap();
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
let name = event_name_from_type(&event); let name = event_name_from_type(&event);
let event = make_synthetic_event(&event, contents); let event = make_synthetic_event(&event, contents);
UserEvent { UserEvent {
@ -56,6 +57,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
priority: EventPriority::Low, priority: EventPriority::Low,
scope_id: None, scope_id: None,
element: mounted_dom_id, element: mounted_dom_id,
bubbles: event_bubbles(name),
data: event, data: event,
} }
} }

View file

@ -1324,3 +1324,91 @@ pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) {
_ => (true, Low), _ => (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),
}
}

View file

@ -48,10 +48,16 @@ extern "C" {
pub fn CreatePlaceholder(this: &Interpreter, root: u64); pub fn CreatePlaceholder(this: &Interpreter, root: u64);
#[wasm_bindgen(method)] #[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)] #[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)] #[wasm_bindgen(method)]
pub fn SetText(this: &Interpreter, root: u64, text: JsValue); pub fn SetText(this: &Interpreter, root: u64, text: JsValue);

View file

@ -5,11 +5,64 @@ export function main() {
window.ipc.postMessage(serializeIpcMessage("initialize")); 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 { export class Interpreter {
constructor(root) { constructor(root) {
this.root = root; this.root = root;
this.stack = [root]; this.stack = [root];
this.listeners = {}; this.listeners = new ListenerMap(root);
this.handlers = {}; this.handlers = {};
this.lastNodeWasText = false; this.lastNodeWasText = false;
this.nodes = [root]; this.nodes = [root];
@ -40,6 +93,7 @@ export class Interpreter {
ReplaceWith(root_id, m) { ReplaceWith(root_id, m) {
let root = this.nodes[root_id]; let root = this.nodes[root_id];
let els = this.stack.splice(this.stack.length - m); let els = this.stack.splice(this.stack.length - m);
this.listeners.removeAllNonBubbling(root);
root.replaceWith(...els); root.replaceWith(...els);
} }
InsertAfter(root, n) { InsertAfter(root, n) {
@ -54,6 +108,7 @@ export class Interpreter {
} }
Remove(root) { Remove(root) {
let node = this.nodes[root]; let node = this.nodes[root];
this.listeners.removeAllNonBubbling(node);
if (node !== undefined) { if (node !== undefined) {
node.remove(); node.remove();
} }
@ -79,25 +134,24 @@ export class Interpreter {
this.stack.push(el); this.stack.push(el);
this.nodes[root] = el; this.nodes[root] = el;
} }
NewEventListener(event_name, root, handler) { NewEventListener(event_name, root, handler, bubbles) {
const element = this.nodes[root]; const element = this.nodes[root];
element.setAttribute("data-dioxus-id", `${root}`); element.setAttribute("data-dioxus-id", `${root}`);
if (this.listeners[event_name] === undefined) { if (bubbles) {
this.listeners[event_name] = 1; this.listeners.createBubbling(event_name, handler);
this.handlers[event_name] = handler; }
this.root.addEventListener(event_name, handler); else {
} else { this.listeners.createNonBubbling(event_name, element, handler);
this.listeners[event_name]++;
} }
} }
RemoveEventListener(root, event_name) { RemoveEventListener(root, event_name, bubbles) {
const element = this.nodes[root]; const element = this.nodes[root];
element.removeAttribute(`data-dioxus-id`); element.removeAttribute(`data-dioxus-id`);
this.listeners[event_name]--; if (bubbles) {
if (this.listeners[event_name] === 0) { this.listeners.removeBubbling(event_name)
this.root.removeEventListener(event_name, this.handlers[event_name]); }
delete this.listeners[event_name]; else {
delete this.handlers[event_name]; this.listeners.removeNonBubbling(element, event_name);
} }
} }
SetText(root, text) { SetText(root, text) {
@ -198,12 +252,9 @@ export class Interpreter {
this.RemoveEventListener(edit.root, edit.event_name); this.RemoveEventListener(edit.root, edit.event_name);
break; break;
case "NewEventListener": case "NewEventListener":
console.log(this.listeners);
// this handler is only provided on desktop implementations since this // this handler is only provided on desktop implementations since this
// method is not used by the web implementation // method is not used by the web implementation
let handler = (event) => { let handler = (event) => {
console.log(event);
let target = event.target; let target = event.target;
if (target != null) { 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; break;
case "SetText": case "SetText":
this.SetText(edit.root, edit.text); this.SetText(edit.root, edit.text);
@ -607,3 +659,172 @@ const bool_attrs = {
selected: true, selected: true,
truespeed: 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;
}
}

View file

@ -6,6 +6,7 @@ use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
use dioxus_core::{ElementId, EventPriority, UserEvent}; use dioxus_core::{ElementId, EventPriority, UserEvent};
use dioxus_html::event_bubbles;
use dioxus_html::on::*; use dioxus_html::on::*;
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
@ -46,6 +47,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
scope_id: None, scope_id: None,
element: mounted_dom_id, element: mounted_dom_id,
data: event, data: event,
bubbles: event_bubbles(name),
} }
} }

View file

@ -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::keyboard_types::Modifiers;
use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons;
use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet};
use dioxus_html::{on::*, KeyCode}; use dioxus_html::{event_bubbles, on::*, KeyCode};
use std::{ use std::{
any::Any, any::Any,
cell::{RefCell, RefMut}, cell::{RefCell, RefMut},
@ -187,6 +187,7 @@ impl InnerInputState {
name: "focus", name: "focus",
element: Some(id), element: Some(id),
data: Arc::new(FocusData {}), data: Arc::new(FocusData {}),
bubbles: event_bubbles("focus"),
}); });
resolved_events.push(UserEvent { resolved_events.push(UserEvent {
scope_id: None, scope_id: None,
@ -194,6 +195,7 @@ impl InnerInputState {
name: "focusin", name: "focusin",
element: Some(id), element: Some(id),
data: Arc::new(FocusData {}), data: Arc::new(FocusData {}),
bubbles: event_bubbles("focusin"),
}); });
} }
if let Some(id) = old_focus { if let Some(id) = old_focus {
@ -203,6 +205,7 @@ impl InnerInputState {
name: "focusout", name: "focusout",
element: Some(id), element: Some(id),
data: Arc::new(FocusData {}), data: Arc::new(FocusData {}),
bubbles: event_bubbles("focusout"),
}); });
} }
} }
@ -248,6 +251,7 @@ impl InnerInputState {
name, name,
element: Some(node.id), element: Some(node.id),
data, data,
bubbles: event_bubbles(name),
}) })
} }
} }
@ -649,6 +653,7 @@ impl RinkInputHandler {
name: event, name: event,
element: Some(node.id), element: Some(node.id),
data: data.clone(), data: data.clone(),
bubbles: event_bubbles(event),
}); });
} }
} }

View file

@ -8,6 +8,7 @@
//! - Partial delegation?> //! - Partial delegation?>
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
use dioxus_html::event_bubbles;
use dioxus_interpreter_js::Interpreter; use dioxus_interpreter_js::Interpreter;
use js_sys::Function; use js_sys::Function;
use std::{any::Any, rc::Rc, sync::Arc}; use std::{any::Any, rc::Rc, sync::Arc};
@ -45,6 +46,7 @@ impl WebsysDom {
element: Some(ElementId(id)), element: Some(ElementId(id)),
scope_id: None, scope_id: None,
priority: dioxus_core::EventPriority::Medium, priority: dioxus_core::EventPriority::Medium,
bubbles: event.bubbles(),
}); });
} }
Some(Err(e)) => { Some(Err(e)) => {
@ -64,6 +66,7 @@ impl WebsysDom {
element: None, element: None,
scope_id: None, scope_id: None,
priority: dioxus_core::EventPriority::Low, priority: dioxus_core::EventPriority::Low,
bubbles: event.bubbles(),
}); });
} }
} }
@ -121,12 +124,17 @@ impl WebsysDom {
event_name, root, .. event_name, root, ..
} => { } => {
let handler: &Function = self.handler.as_ref().unchecked_ref(); 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 } => { DomEdit::RemoveEventListener { root, event } => self
self.interpreter.RemoveEventListener(root, event) .interpreter
} .RemoveEventListener(root, event, event_bubbles(event)),
DomEdit::RemoveAttribute { root, name, ns } => { DomEdit::RemoveAttribute { root, name, ns } => {
self.interpreter.RemoveAttribute(root, name, ns) self.interpreter.RemoveAttribute(root, name, ns)

View file

@ -1,5 +1,6 @@
use crate::dom::WebsysDom; use crate::dom::WebsysDom;
use dioxus_core::{VNode, VirtualDom}; use dioxus_core::{VNode, VirtualDom};
use dioxus_html::event_bubbles;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use web_sys::{Comment, Element, Node, Text}; use web_sys::{Comment, Element, Node, Text};
@ -111,6 +112,7 @@ impl WebsysDom {
listener.event, listener.event,
listener.mounted_node.get().unwrap().as_u64(), listener.mounted_node.get().unwrap().as_u64(),
self.handler.as_ref().unchecked_ref(), self.handler.as_ref().unchecked_ref(),
event_bubbles(listener.event),
); );
} }