diff options
author | Andrei Volykhin <andrei.volykhin@gmail.com> | 2025-03-29 02:34:04 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-28 23:34:04 +0000 |
commit | 5f5bf87eee34856ec5c1260a3c05b024fc68c99b (patch) | |
tree | 8f70a2c690ef62628f352c52b8d551db03514e4c /components/script/dom | |
parent | ed3dd8fbe03f41ee816cbe87c898a0cdc71d1115 (diff) | |
download | servo-5f5bf87eee34856ec5c1260a3c05b024fc68c99b.tar.gz servo-5f5bf87eee34856ec5c1260a3c05b024fc68c99b.zip |
dom: Track "removed" event listener status (#36163)
The DOM event listener "removed" status should be supported to track
the following situations (with immediate effect of listener removal):
- Removing a later event listener while an earlier listener
for the same event is running
- Nested usage (recursively dispatch another event) of "once" listeners
https://dom.spec.whatwg.org/#event-listener-removed
During event dispatching requires to clone event listeners list
on "invoke" step https://dom.spec.whatwg.org/#concept-event-listener-invoke
and the lowercase "event listener" concept in Servo is EventListenerEntry
https://dom.spec.whatwg.org/#concept-event-listener
Bug: #25479, #25090
Signed-off-by: Andrei Volykhin <andrei.volykhin@gmail.com>
Diffstat (limited to 'components/script/dom')
-rw-r--r-- | components/script/dom/event.rs | 65 | ||||
-rw-r--r-- | components/script/dom/eventtarget.rs | 139 |
2 files changed, 107 insertions, 97 deletions
diff --git a/components/script/dom/event.rs b/components/script/dom/event.rs index a9dd421b7e0..1347713f489 100644 --- a/components/script/dom/event.rs +++ b/components/script/dom/event.rs @@ -29,7 +29,7 @@ use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_w use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; use crate::dom::element::Element; -use crate::dom::eventtarget::{CompiledEventListener, EventTarget, ListenerPhase}; +use crate::dom::eventtarget::{EventListeners, EventTarget, ListenerPhase}; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlinputelement::InputActivationState; use crate::dom::htmlslotelement::HTMLSlotElement; @@ -1151,10 +1151,7 @@ fn invoke( event.current_target.set(Some(&segment.invocation_target)); // Step 6. Let listeners be a clone of event’s currentTarget attribute value’s event listener list. - let listeners = - segment - .invocation_target - .get_listeners_for(&event.type_(), Some(phase), can_gc); + let listeners = segment.invocation_target.get_listeners_for(&event.type_()); // Step 7. Let invocationTargetInShadowTree be struct’s invocation-target-in-shadow-tree. let invocation_target_in_shadow_tree = segment.invocation_target_in_shadow_tree; @@ -1207,8 +1204,8 @@ fn invoke( /// <https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke> fn inner_invoke( event: &Event, - listeners: &[CompiledEventListener], - _phase: ListenerPhase, + listeners: &EventListeners, + phase: ListenerPhase, invocation_target_in_shadow_tree: bool, timeline_window: Option<&Window>, can_gc: CanGc, @@ -1217,25 +1214,21 @@ fn inner_invoke( let mut found = false; // Step 2. For each listener in listeners, whose removed is false: - for listener in listeners { - // FIXME(#25479): We need an "if !listener.removed()" here, - // but there's a subtlety. Where Servo is currently using the - // CompiledEventListener, we really need something that maps to - // https://dom.spec.whatwg.org/#concept-event-listener - // which is not the same thing as the EventListener interface. - // script::dom::eventtarget::EventListenerEntry is the closest - // match we have, and is already holding the "once" flag, - // but it's not a drop-in replacement. - - // Steps 2.1 and 2.3-2.4 are not done because `listeners` contain only the - // relevant ones for this invoke call during the dispatch algorithm. - // TODO: Step 2.1 If event’s type attribute value is not listener’s type, then continue. + for listener in listeners.iter() { + if listener.borrow().removed() { + continue; + } + + // Step 2.1 If event’s type attribute value is not listener’s type, then continue. // Step 2.2. Set found to true. found = true; - // TODO Step 2.3 If phase is "capturing" and listener’s capture is false, then continue. - // TODO Step 2.4 If phase is "bubbling" and listener’s capture is true, then continue. + // Step 2.3 If phase is "capturing" and listener’s capture is false, then continue. + // Step 2.4 If phase is "bubbling" and listener’s capture is true, then continue. + if listener.borrow().phase() != phase { + continue; + } let event_target = event .GetCurrentTarget() @@ -1243,12 +1236,20 @@ fn inner_invoke( // Step 2.5 If listener’s once is true, then remove an event listener given event’s currentTarget // attribute value and listener. - if let CompiledEventListener::Listener(event_listener) = listener { - event_target.remove_listener_if_once(&event.type_(), event_listener); + if listener.borrow().once() { + event_target.remove_listener(&event.type_(), listener); } + let Some(compiled_listener) = + listener + .borrow() + .get_compiled_listener(&event_target, &event.type_(), can_gc) + else { + continue; + }; + // Step 2.6 Let global be listener callback’s associated realm’s global object. - let global = listener.associated_global(); + let global = compiled_listener.associated_global(); // Step 2.7 Let currentEvent be undefined. let mut current_event = None; @@ -1264,18 +1265,22 @@ fn inner_invoke( } // Step 2.9 If listener’s passive is true, then set event's in passive listener flag. - if let CompiledEventListener::Listener(event_listener) = listener { - event.set_in_passive_listener(event_target.is_passive(&event.type_(), event_listener)); - } + event.set_in_passive_listener(event_target.is_passive(&event.type_(), listener)); - // Step 2.10 If global is a Window object, then record timing info for event listener given event and listener. + // Step 2.10 If global is a Window object, then record timing info for event listener + // given event and listener. // Step 2.11 Call a user object’s operation with listener’s callback, "handleEvent", « event », // and event’s currentTarget attribute value. If this throws an exception exception: // Step 2.10.1 Report exception for listener’s callback’s corresponding JavaScript object’s // associated realm’s global object. // TODO Step 2.10.2 Set legacyOutputDidListenersThrowFlag if given. let marker = TimelineMarker::start("DOMEvent".to_owned()); - listener.call_or_handle_event(&event_target, event, ExceptionHandling::Report, can_gc); + compiled_listener.call_or_handle_event( + &event_target, + event, + ExceptionHandling::Report, + can_gc, + ); if let Some(window) = timeline_window { window.emit_timeline_marker(marker.end()); } diff --git a/components/script/dom/eventtarget.rs b/components/script/dom/eventtarget.rs index 43e62b528d8..ea76bbf2a8b 100644 --- a/components/script/dom/eventtarget.rs +++ b/components/script/dom/eventtarget.rs @@ -307,11 +307,36 @@ impl CompiledEventListener { // (as distinct from https://dom.spec.whatwg.org/#callbackdef-eventlistener) #[derive(Clone, DenyPublicFields, JSTraceable, MallocSizeOf)] /// A listener in a collection of event listeners. -struct EventListenerEntry { +pub(crate) struct EventListenerEntry { phase: ListenerPhase, listener: EventListenerType, once: bool, passive: Option<bool>, + removed: bool, +} + +impl EventListenerEntry { + pub(crate) fn phase(&self) -> ListenerPhase { + self.phase + } + + pub(crate) fn once(&self) -> bool { + self.once + } + + pub(crate) fn removed(&self) -> bool { + self.removed + } + + /// <https://html.spec.whatwg.org/multipage/#getting-the-current-value-of-the-event-handler> + pub(crate) fn get_compiled_listener( + &self, + owner: &EventTarget, + ty: &Atom, + can_gc: CanGc, + ) -> Option<CompiledEventListener> { + self.listener.get_compiled_listener(owner, ty, can_gc) + } } impl std::cmp::PartialEq for EventListenerEntry { @@ -320,19 +345,21 @@ impl std::cmp::PartialEq for EventListenerEntry { } } -#[derive(JSTraceable, MallocSizeOf)] +#[derive(Clone, JSTraceable, MallocSizeOf)] /// A mix of potentially uncompiled and compiled event listeners of the same type. -struct EventListeners(Vec<EventListenerEntry>); +pub(crate) struct EventListeners( + #[ignore_malloc_size_of = "Rc"] Vec<Rc<RefCell<EventListenerEntry>>>, +); impl Deref for EventListeners { - type Target = Vec<EventListenerEntry>; - fn deref(&self) -> &Vec<EventListenerEntry> { + type Target = Vec<Rc<RefCell<EventListenerEntry>>>; + fn deref(&self) -> &Vec<Rc<RefCell<EventListenerEntry>>> { &self.0 } } impl DerefMut for EventListeners { - fn deref_mut(&mut self) -> &mut Vec<EventListenerEntry> { + fn deref_mut(&mut self) -> &mut Vec<Rc<RefCell<EventListenerEntry>>> { &mut self.0 } } @@ -346,7 +373,7 @@ impl EventListeners { can_gc: CanGc, ) -> Option<CommonEventHandler> { for entry in &self.0 { - if let EventListenerType::Inline(ref inline) = entry.listener { + if let EventListenerType::Inline(ref inline) = entry.borrow().listener { // Step 1.1-1.8 and Step 2 return get_compiled_handler(inline, owner, ty, can_gc); } @@ -356,30 +383,7 @@ impl EventListeners { None } - // https://html.spec.whatwg.org/multipage/#getting-the-current-value-of-the-event-handler - fn get_listeners( - &self, - phase: Option<ListenerPhase>, - owner: &EventTarget, - ty: &Atom, - can_gc: CanGc, - ) -> Vec<CompiledEventListener> { - self.0 - .iter() - .filter_map(|entry| { - if phase.is_none() || Some(entry.phase) == phase { - // Step 1.1-1.8, 2 - entry.listener.get_compiled_listener(owner, ty, can_gc) - } else { - None - } - }) - .collect() - } - fn has_listeners(&self) -> bool { - // TODO: add, and take into account, a 'removed' field? - // https://dom.spec.whatwg.org/#event-listener-removed !self.0.is_empty() } } @@ -420,18 +424,11 @@ impl EventTarget { } } - pub(crate) fn get_listeners_for( - &self, - type_: &Atom, - specific_phase: Option<ListenerPhase>, - can_gc: CanGc, - ) -> Vec<CompiledEventListener> { + pub(crate) fn get_listeners_for(&self, type_: &Atom) -> EventListeners { self.handlers .borrow() .get(type_) - .map_or(vec![], |listeners| { - listeners.get_listeners(specific_phase, self, type_, can_gc) - }) + .map_or(EventListeners(vec![]), |listeners| listeners.clone()) } pub(crate) fn dispatch_event(&self, event: &Event, can_gc: CanGc) -> EventStatus { @@ -439,7 +436,14 @@ impl EventTarget { } pub(crate) fn remove_all_listeners(&self) { - *self.handlers.borrow_mut() = Default::default(); + let mut handlers = self.handlers.borrow_mut(); + for (_, entries) in handlers.iter() { + entries + .iter() + .for_each(|entry| entry.borrow_mut().removed = true); + } + + *handlers = Default::default(); } /// <https://dom.spec.whatwg.org/#default-passive-value> @@ -488,50 +492,49 @@ impl EventTarget { let idx = entries .iter() - .position(|entry| matches!(entry.listener, EventListenerType::Inline(_))); + .position(|entry| matches!(entry.borrow().listener, EventListenerType::Inline(_))); match idx { Some(idx) => match listener { // Replace if there's something to replace with, // but remove entirely if there isn't. Some(listener) => { - entries[idx].listener = EventListenerType::Inline(listener.into()); + entries[idx].borrow_mut().listener = EventListenerType::Inline(listener.into()); }, None => { - entries.remove(idx); + entries.remove(idx).borrow_mut().removed = true; }, }, None => { if let Some(listener) = listener { - entries.push(EventListenerEntry { + entries.push(Rc::new(RefCell::new(EventListenerEntry { phase: ListenerPhase::Bubbling, listener: EventListenerType::Inline(listener.into()), once: false, passive: None, - }); + removed: false, + }))); } }, } } - pub(crate) fn remove_listener_if_once(&self, ty: &Atom, listener: &Rc<EventListener>) { + pub(crate) fn remove_listener(&self, ty: &Atom, entry: &Rc<RefCell<EventListenerEntry>>) { let mut handlers = self.handlers.borrow_mut(); - let listener = EventListenerType::Additive(listener.clone()); if let Some(entries) = handlers.get_mut(ty) { - entries.retain(|e| e.listener != listener || !e.once) + if let Some(position) = entries.iter().position(|e| *e == *entry) { + entries.remove(position).borrow_mut().removed = true; + } } } /// Determines the `passive` attribute of an associated event listener - pub(crate) fn is_passive(&self, ty: &Atom, listener: &Rc<EventListener>) -> bool { - let handlers = self.handlers.borrow(); - let listener_instance = EventListenerType::Additive(listener.clone()); - - handlers - .get(ty) - .and_then(|entries| entries.iter().find(|e| e.listener == listener_instance)) - .is_some_and(|entry| entry.passive.unwrap_or(self.default_passive_value(ty))) + pub(crate) fn is_passive(&self, ty: &Atom, listener: &Rc<RefCell<EventListenerEntry>>) -> bool { + listener + .borrow() + .passive + .unwrap_or(self.default_passive_value(ty)) } fn get_inline_event_listener(&self, ty: &Atom, can_gc: CanGc) -> Option<CommonEventHandler> { @@ -810,7 +813,7 @@ impl EventTarget { None => return, }; let mut handlers = self.handlers.borrow_mut(); - let entry = match handlers.entry(Atom::from(ty)) { + let entries = match handlers.entry(Atom::from(ty)) { Occupied(entry) => entry.into_mut(), Vacant(entry) => entry.insert(EventListeners(vec![])), }; @@ -820,14 +823,16 @@ impl EventTarget { } else { ListenerPhase::Bubbling }; - let new_entry = EventListenerEntry { + let new_entry = Rc::new(RefCell::new(EventListenerEntry { phase, listener: EventListenerType::Additive(listener), once: options.once, passive: options.passive, - }; - if !entry.contains(&new_entry) { - entry.push(new_entry); + removed: false, + })); + + if !entries.contains(&new_entry) { + entries.push(new_entry); } } @@ -842,21 +847,21 @@ impl EventTarget { return; }; let mut handlers = self.handlers.borrow_mut(); - let entry = handlers.get_mut(&Atom::from(ty)); - if let Some(entry) = entry { + if let Some(entries) = handlers.get_mut(&Atom::from(ty)) { let phase = if options.capture { ListenerPhase::Capturing } else { ListenerPhase::Bubbling }; - let old_entry = EventListenerEntry { + let old_entry = Rc::new(RefCell::new(EventListenerEntry { phase, listener: EventListenerType::Additive(listener.clone()), once: false, passive: None, - }; - if let Some(position) = entry.iter().position(|e| *e == old_entry) { - entry.remove(position); + removed: false, + })); + if let Some(position) = entries.iter().position(|e| *e == old_entry) { + entries.remove(position).borrow_mut().removed = true; } } } |