diff options
Diffstat (limited to 'components/script/dom/event.rs')
-rw-r--r-- | components/script/dom/event.rs | 379 |
1 files changed, 253 insertions, 126 deletions
diff --git a/components/script/dom/event.rs b/components/script/dom/event.rs index 3c6bf956e9e..cc960c7ba1b 100644 --- a/components/script/dom/event.rs +++ b/components/script/dom/event.rs @@ -8,16 +8,20 @@ use crate::dom::bindings::codegen::Bindings::EventBinding; use crate::dom::bindings::codegen::Bindings::EventBinding::{EventConstants, EventMethods}; use crate::dom::bindings::codegen::Bindings::PerformanceBinding::DOMHighResTimeStamp; use crate::dom::bindings::codegen::Bindings::PerformanceBinding::PerformanceBinding::PerformanceMethods; +use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::error::Fallible; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector}; -use crate::dom::bindings::root::{DomRoot, DomSlice, MutNullableDom}; +use crate::dom::bindings::root::{DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; use crate::dom::document::Document; +use crate::dom::element::Element; use crate::dom::eventtarget::{CompiledEventListener, EventTarget, ListenerPhase}; use crate::dom::globalscope::GlobalScope; -use crate::dom::node::Node; +use crate::dom::htmlinputelement::InputActivationState; +use crate::dom::mouseevent::MouseEvent; +use crate::dom::node::{Node, ShadowIncluding}; use crate::dom::performance::reduce_timing_resolution; use crate::dom::virtualmethods::vtable_for; use crate::dom::window::Window; @@ -122,14 +126,24 @@ impl Event { } // https://dom.spec.whatwg.org/#event-path + // TODO: shadow roots put special flags in the path, + // and it will stop just being a list of bare EventTargets fn construct_event_path(&self, target: &EventTarget) -> Vec<DomRoot<EventTarget>> { let mut event_path = vec![]; - // The "invoke" algorithm is only used on `target` separately, - // so we don't put it in the path. if let Some(target_node) = target.downcast::<Node>() { - for ancestor in target_node.ancestors() { + // ShadowIncluding::Yes might be closer to right than ::No, + // but still wrong since things about the path change when crossing + // shadow attachments; getting it right needs to change + // more than just that. + for ancestor in target_node.inclusive_ancestors(ShadowIncluding::No) { event_path.push(DomRoot::from_ref(ancestor.upcast::<EventTarget>())); } + // Most event-target-to-parent relationships are node parent + // relationships, but the document-to-global one is not, + // so that's handled separately here. + // (an EventTarget.get_parent_event_target could save + // some redundancy, especially when shadow DOM relationships + // also need to be respected) let top_most_ancestor_or_target = event_path .last() .cloned() @@ -139,6 +153,11 @@ impl Event { event_path.push(DomRoot::from_ref(document.window().upcast())); } } + } else { + // a non-node EventTarget, likely a global. + // No parent to propagate up to, but we still + // need it on the path. + event_path.push(DomRoot::from_ref(target)); } event_path } @@ -147,49 +166,185 @@ impl Event { pub fn dispatch( &self, target: &EventTarget, - target_override: Option<&EventTarget>, + legacy_target_override: bool, + // TODO legacy_did_output_listeners_throw_flag for indexeddb ) -> EventStatus { - assert!(!self.dispatching()); - assert!(self.initialized()); - assert_eq!(self.phase.get(), EventPhase::None); - assert!(self.GetCurrentTarget().is_none()); - // Step 1. self.dispatching.set(true); // Step 2. - self.target.set(Some(target_override.unwrap_or(target))); + let target_override_document; // upcasted EventTarget's lifetime depends on this + let target_override = if legacy_target_override { + target_override_document = target + .downcast::<Window>() + .expect("legacy_target_override must be true only when target is a Window") + .Document(); + target_override_document.upcast::<EventTarget>() + } else { + target + }; - if self.stop_propagation.get() { - // If the event's stop propagation flag is set, we can skip everything because - // it prevents the calls of the invoke algorithm in the spec. + // Step 3 - since step 5 always happens, we can wait until 5.5 - // Step 10-12. - self.clear_dispatching_flags(); + // Step 4 TODO: "retargeting" concept depends on shadow DOM - // Step 14. - return self.status(); - } + // Step 5, outer if-statement, is always true until step 4 is implemented + // Steps 5.1-5.2 TODO: touch target lists don't exist yet - // Step 3-4. + // Steps 5.3 and most of 5.9 + // A change in whatwg/dom#240 specifies that + // the event path belongs to the event itself, rather than being + // a local variable of the dispatch algorithm, but this is mostly + // related to shadow DOM requirements that aren't otherwise + // implemented right now. The path also needs to contain + // various flags instead of just bare event targets. let path = self.construct_event_path(&target); rooted_vec!(let event_path <- path.into_iter()); - // Steps 5-9. In a separate function to short-circuit various things easily. - dispatch_to_listeners(self, target, event_path.r()); - - // Default action. - if let Some(target) = self.GetTarget() { - if let Some(node) = target.downcast::<Node>() { - let vtable = vtable_for(&node); - vtable.handle_event(self); + + // Step 5.4 + let is_activation_event = self.is::<MouseEvent>() && self.type_() == atom!("click"); + + // Step 5.5 + let mut activation_target = if is_activation_event { + target + .downcast::<Element>() + .and_then(|e| e.as_maybe_activatable()) + } else { + // Step 3 + None + }; + + // Steps 5-6 - 5.7 are shadow DOM slot things + + // Step 5.9.8.1, not covered in construct_event_path + // This what makes sure that clicking on e.g. an <img> inside + // an <a> will cause activation of the activatable ancestor. + if is_activation_event && activation_target.is_none() && self.bubbles.get() { + for object in event_path.iter() { + if let Some(activatable_ancestor) = object + .downcast::<Element>() + .and_then(|e| e.as_maybe_activatable()) + { + activation_target = Some(activatable_ancestor); + // once activation_target isn't null, we stop + // looking at ancestors for it. + break; + } + } + } + + // Steps 5.10-5.11 are shadow DOM + + // Not specified in dispatch spec overtly; this is because + // the legacy canceled activation behavior of a checkbox + // or radio button needs to know what happened in the + // corresponding pre-activation behavior. + let mut pre_activation_result: Option<InputActivationState> = None; + + // Step 5.12 + if is_activation_event { + if let Some(maybe_checkbox) = activation_target { + pre_activation_result = maybe_checkbox.legacy_pre_activation_behavior(); + } + } + + let timeline_window = match DomRoot::downcast::<Window>(target.global()) { + Some(window) => { + if window.need_emit_timeline_marker(TimelineMarkerType::DOMEvent) { + Some(window) + } else { + None + } + }, + _ => None, + }; + + // Step 5.13 + for object in event_path.iter().rev() { + if &**object == &*target { + self.phase.set(EventPhase::AtTarget); + } else { + self.phase.set(EventPhase::Capturing); + } + + // setting self.target is step 1 of invoke, + // but done here because our event_path isn't a member of self + // (without shadow DOM, target_override is always the + // target to set to) + self.target.set(Some(target_override)); + invoke( + timeline_window.as_deref(), + object, + self, + Some(ListenerPhase::Capturing), + ); + } + + // Step 5.14 + for object in event_path.iter() { + let at_target = &**object == &*target; + if at_target || self.bubbles.get() { + self.phase.set(if at_target { + EventPhase::AtTarget + } else { + EventPhase::Bubbling + }); + + self.target.set(Some(target_override)); + invoke( + timeline_window.as_deref(), + object, + self, + Some(ListenerPhase::Bubbling), + ); + } + } + + // Step 6 + self.phase.set(EventPhase::None); + + // FIXME: The UIEvents spec still expects firing an event + // to carry a "default action" semantic, but the HTML spec + // has removed this concept. Nothing in either spec currently + // (as of Jan 11 2020) says that, e.g., a keydown event on an + // input element causes a character to be typed; the UIEvents + // spec assumes the HTML spec is covering it, and the HTML spec + // no longer specifies any UI event other than mouse click as + // causing an element to perform an action. + // Compare: + // https://w3c.github.io/uievents/#default-action + // https://dom.spec.whatwg.org/#action-versus-occurance + if !self.DefaultPrevented() { + if let Some(target) = self.GetTarget() { + if let Some(node) = target.downcast::<Node>() { + let vtable = vtable_for(&node); + vtable.handle_event(self); + } } } - // Step 10-12. - self.clear_dispatching_flags(); + // Step 7 + self.current_target.set(None); + + // Step 8 TODO: if path were in the event struct, we'd clear it now + + // Step 9 + self.dispatching.set(false); + self.stop_propagation.set(false); + self.stop_immediate.set(false); + + // Step 10 TODO: condition is always false until there's shadow DOM - // Step 14. - self.status() + // Step 11 + if let Some(activation_target) = activation_target { + if self.DefaultPrevented() { + activation_target.legacy_canceled_activation_behavior(pre_activation_result); + } else { + activation_target.activation_behavior(self, target); + } + } + + return self.status(); } pub fn status(&self) -> EventStatus { @@ -206,18 +361,6 @@ impl Event { } #[inline] - // https://dom.spec.whatwg.org/#concept-event-dispatch Steps 10-12. - fn clear_dispatching_flags(&self) { - assert!(self.dispatching.get()); - - self.dispatching.set(false); - self.stop_propagation.set(false); - self.stop_immediate.set(false); - self.phase.set(EventPhase::None); - self.current_target.set(None); - } - - #[inline] pub fn initialized(&self) -> bool { self.initialized.get() } @@ -468,98 +611,55 @@ impl TaskOnce for SimpleEventTask { } } -// See dispatch_event. -// https://dom.spec.whatwg.org/#concept-event-dispatch -fn dispatch_to_listeners(event: &Event, target: &EventTarget, event_path: &[&EventTarget]) { - assert!(!event.stop_propagation.get()); - assert!(!event.stop_immediate.get()); - - let window = match DomRoot::downcast::<Window>(target.global()) { - Some(window) => { - if window.need_emit_timeline_marker(TimelineMarkerType::DOMEvent) { - Some(window) - } else { - None - } - }, - _ => None, - }; - - // Step 5. - event.phase.set(EventPhase::Capturing); - - // Step 6. - for object in event_path.iter().rev() { - invoke( - window.as_deref(), - object, - event, - Some(ListenerPhase::Capturing), - ); - if event.stop_propagation.get() { - return; - } - } - assert!(!event.stop_propagation.get()); - assert!(!event.stop_immediate.get()); - - // Step 7. - event.phase.set(EventPhase::AtTarget); - - // Step 8. - invoke(window.as_deref(), target, event, None); - if event.stop_propagation.get() { - return; - } - assert!(!event.stop_propagation.get()); - assert!(!event.stop_immediate.get()); - - if !event.bubbles.get() { - return; - } - - // Step 9.1. - event.phase.set(EventPhase::Bubbling); - - // Step 9.2. - for object in event_path { - invoke( - window.as_deref(), - object, - event, - Some(ListenerPhase::Bubbling), - ); - if event.stop_propagation.get() { - return; - } - } -} - // https://dom.spec.whatwg.org/#concept-event-listener-invoke fn invoke( - window: Option<&Window>, + timeline_window: Option<&Window>, object: &EventTarget, event: &Event, - specific_listener_phase: Option<ListenerPhase>, + phase: Option<ListenerPhase>, + // TODO legacy_output_did_listeners_throw for indexeddb ) { - // Step 1. - assert!(!event.stop_propagation.get()); + // Step 1: Until shadow DOM puts the event path in the + // event itself, this is easier to do in dispatch before + // calling invoke. + + // Step 2 TODO: relatedTarget only matters for shadow DOM - // Steps 2-3. - let listeners = object.get_listeners_for(&event.type_(), specific_listener_phase); + // Step 3 TODO: touch target lists not implemented // Step 4. + if event.stop_propagation.get() { + return; + } + // Step 5. event.current_target.set(Some(object)); - // Step 5. - inner_invoke(window, object, event, &listeners); + // Step 6 + let listeners = object.get_listeners_for(&event.type_(), phase); - // TODO: step 6. + // Step 7. + let found = inner_invoke(timeline_window, object, event, &listeners); + + // Step 8 + if !found && event.trusted.get() { + if let Some(legacy_type) = match event.type_() { + atom!("animationend") => Some(atom!("webkitAnimationEnd")), + atom!("animationiteration") => Some(atom!("webkitAnimationIteration")), + atom!("animationstart") => Some(atom!("webkitAnimationStart")), + atom!("transitionend") => Some(atom!("webkitTransitionEnd")), + _ => None, + } { + let original_type = event.type_(); + *event.type_.borrow_mut() = legacy_type; + inner_invoke(timeline_window, object, event, &listeners); + *event.type_.borrow_mut() = original_type; + } + } } // https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke fn inner_invoke( - window: Option<&Window>, + timeline_window: Option<&Window>, object: &EventTarget, event: &Event, listeners: &[CompiledEventListener], @@ -569,6 +669,15 @@ fn inner_invoke( // Step 2. 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. @@ -580,17 +689,35 @@ fn inner_invoke( object.remove_listener_if_once(&event.type_(), &event_listener); } - // Step 2.6. + // Step 2.6-2.8 + // FIXME(#25478): we need to get the global that the event + // listener is going to be called on, then if it's a Window + // set its .event to the event, remembering the previous + // value of its .event. This allows events to just use + // the word "event" instead of taking the event as an argument. + + // Step 2.9 TODO: EventListener passive option not implemented + + // Step 2.10 let marker = TimelineMarker::start("DOMEvent".to_owned()); + + // Step 2.10 listener.call_or_handle_event(object, event, ExceptionHandling::Report); - if let Some(window) = window { + + if let Some(window) = timeline_window { window.emit_timeline_marker(marker.end()); } + + // Step 2.11 TODO: passive not implemented + + // Step 2.12 + // TODO This is where we put back the .event we + // had before step 2.6. + + // Step 2.13: short-circuit instead of going to next listener if event.stop_immediate.get() { return found; } - - // TODO: step 2.7. } // Step 3. |