Bug 1266066 - Implement DOM support for the 'passive' event listener flag. r=smaug
authorKartikaya Gupta <kgupta@mozilla.com>
Thu, 12 May 2016 14:50:22 -0400
changeset 325890 f66aa2a433d88e2a46144bc0563501bd4f9a06c3
parent 325881 290bd460fc1644c804f8bd336abd0910298ce293
child 325891 e5880e371c16f2b4da10b645c7a54136a92a3b32
push idunknown
push userunknown
push dateunknown
reviewerssmaug
bugs1266066
milestone49.0a1
Bug 1266066 - Implement DOM support for the 'passive' event listener flag. r=smaug MozReview-Commit-ID: EvSCDxYC7g6
dom/events/Event.cpp
dom/events/EventListenerManager.cpp
dom/events/EventListenerManager.h
dom/events/test/mochitest.ini
dom/events/test/test_passive_listeners.html
widget/BasicEvents.h
--- a/dom/events/Event.cpp
+++ b/dom/events/Event.cpp
@@ -515,16 +515,19 @@ Event::PreventDefault(JSContext* aCx)
 }
 
 void
 Event::PreventDefaultInternal(bool aCalledByDefaultHandler)
 {
   if (!mEvent->mFlags.mCancelable) {
     return;
   }
+  if (mEvent->mFlags.mInPassiveListener) {
+    return;
+  }
 
   mEvent->PreventDefault(aCalledByDefaultHandler);
 
   if (!IsTrusted()) {
     return;
   }
 
   WidgetDragEvent* dragEvent = mEvent->AsDragEvent();
--- a/dom/events/EventListenerManager.cpp
+++ b/dom/events/EventListenerManager.cpp
@@ -259,17 +259,17 @@ EventListenerManager::AddEventListenerIn
   // to the listener.
 
   Listener* listener;
   uint32_t count = mListeners.Length();
   for (uint32_t i = 0; i < count; i++) {
     listener = &mListeners.ElementAt(i);
     // mListener == aListenerHolder is the last one, since it can be a bit slow.
     if (listener->mListenerIsHandler == aHandler &&
-        listener->mFlags == aFlags &&
+        listener->mFlags.EqualsForAddition(aFlags) &&
         EVENT_TYPE_EQUALS(listener, aEventMessage, aTypeAtom, aTypeString,
                           aAllEvents) &&
         listener->mListener == aListenerHolder) {
       return;
     }
   }
 
   mNoListenerForEvent = eVoidEvent;
@@ -612,17 +612,17 @@ EventListenerManager::RemoveEventListene
 #endif // MOZ_B2G
 
   for (uint32_t i = 0; i < count; ++i) {
     listener = &mListeners.ElementAt(i);
     if (EVENT_TYPE_EQUALS(listener, aEventMessage, aUserType, aTypeString,
                           aAllEvents)) {
       ++typeCount;
       if (listener->mListener == aListenerHolder &&
-          listener->mFlags.EqualsIgnoringTrustness(aFlags)) {
+          listener->mFlags.EqualsForRemoval(aFlags)) {
         RefPtr<EventListenerManager> kungFuDeathGrip(this);
         mListeners.RemoveElementAt(i);
         --count;
         mNoListenerForEvent = eVoidEvent;
         mNoListenerForEventAtom = nullptr;
         if (mTarget && aUserType) {
           mTarget->EventListenerRemoved(aUserType);
         }
@@ -1274,19 +1274,21 @@ EventListenerManager::HandleEventInterna
                   (*aDOMEvent)->GetEventPhase(&phase);
                   timelines->AddMarkerForDocShell(docShell, Move(
                     MakeUnique<EventTimelineMarker>(
                       typeStr, phase, MarkerTracingType::START)));
                 }
               }
             }
 
+            aEvent->mFlags.mInPassiveListener = listener->mFlags.mPassive;
             if (NS_FAILED(HandleEventSubType(listener, *aDOMEvent, aCurrentTarget))) {
               aEvent->mFlags.mExceptionWasRaised = true;
             }
+            aEvent->mFlags.mInPassiveListener = false;
 
             if (needsEndEventMarker) {
               timelines->AddMarkerForDocShell(
                 docShell, "DOMEvent", MarkerTracingType::END);
             }
           }
         }
       }
@@ -1347,19 +1349,22 @@ EventListenerManager::AddEventListener(
 void
 EventListenerManager::AddEventListener(
                         const nsAString& aType,
                         const EventListenerHolder& aListenerHolder,
                         const dom::AddEventListenerOptionsOrBoolean& aOptions,
                         bool aWantsUntrusted)
 {
   EventListenerFlags flags;
-  flags.mCapture =
-    aOptions.IsBoolean() ? aOptions.GetAsBoolean()
-                         : aOptions.GetAsAddEventListenerOptions().mCapture;
+  if (aOptions.IsBoolean()) {
+    flags.mCapture = aOptions.GetAsBoolean();
+  } else {
+    flags.mCapture = aOptions.GetAsAddEventListenerOptions().mCapture;
+    flags.mPassive = aOptions.GetAsAddEventListenerOptions().mPassive;
+  }
   flags.mAllowUntrustedEvents = aWantsUntrusted;
   return AddEventListenerByType(aListenerHolder, aType, flags);
 }
 
 void
 EventListenerManager::RemoveEventListener(
                         const nsAString& aType,
                         const EventListenerHolder& aListenerHolder,
--- a/dom/events/EventListenerManager.h
+++ b/dom/events/EventListenerManager.h
@@ -53,41 +53,42 @@ public:
   // it's listening at bubbling phase.
   bool mCapture : 1;
   // If mInSystemGroup is true, the listener is listening to the events in the
   // system group.
   bool mInSystemGroup : 1;
   // If mAllowUntrustedEvents is true, the listener is listening to the
   // untrusted events too.
   bool mAllowUntrustedEvents : 1;
+  // If mPassive is true, the listener will not be calling preventDefault on the
+  // event. (If it does call preventDefault, we should ignore it).
+  bool mPassive : 1;
 
   EventListenerFlags() :
     mListenerIsJSListener(false),
-    mCapture(false), mInSystemGroup(false), mAllowUntrustedEvents(false)
+    mCapture(false), mInSystemGroup(false), mAllowUntrustedEvents(false),
+    mPassive(false)
   {
   }
 
-  bool Equals(const EventListenerFlags& aOther) const
+  bool EqualsForAddition(const EventListenerFlags& aOther) const
   {
     return (mCapture == aOther.mCapture &&
             mInSystemGroup == aOther.mInSystemGroup &&
             mListenerIsJSListener == aOther.mListenerIsJSListener &&
             mAllowUntrustedEvents == aOther.mAllowUntrustedEvents);
+            // Don't compare mPassive
   }
 
-  bool EqualsIgnoringTrustness(const EventListenerFlags& aOther) const
+  bool EqualsForRemoval(const EventListenerFlags& aOther) const
   {
     return (mCapture == aOther.mCapture &&
             mInSystemGroup == aOther.mInSystemGroup &&
             mListenerIsJSListener == aOther.mListenerIsJSListener);
-  }
-
-  bool operator==(const EventListenerFlags& aOther) const
-  {
-    return Equals(aOther);
+            // Don't compare mAllowUntrustedEvents or mPassive
   }
 };
 
 inline EventListenerFlags TrustedEventsAtBubble()
 {
   EventListenerFlags flags;
   return flags;
 }
--- a/dom/events/test/mochitest.ini
+++ b/dom/events/test/mochitest.ini
@@ -194,8 +194,9 @@ support-files =
   bug1096146_embedded.html
 [test_offsetxy.html]
 [test_eventhandler_scoping.html]
 [test_bug1013412.html]
 skip-if = buildapp == 'b2g' # no wheel events on b2g
 [test_dom_activate_event.html]
 [test_bug1264380.html]
 run-if = (e10s && os != "win") # Bug 1270043, crash at windows platforms; Bug1264380 comment 20, nsDragService::InvokeDragSessionImpl behaves differently among platform implementations in non-e10s mode which prevents us to check the validity of nsIDragService::getCurrentSession() consistently via synthesize mouse clicks in non-e10s mode.
+[test_passive_listeners.html]
new file mode 100644
--- /dev/null
+++ b/dom/events/test/test_passive_listeners.html
@@ -0,0 +1,118 @@
+<html>
+<head>
+  <title>Tests for passive event listeners</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<body>
+<p id="display"></p>
+<div id="dummy">
+</div>
+
+<script>
+var listenerHitCount;
+var doPreventDefault;
+
+function listener(e)
+{
+  listenerHitCount++;
+  if (doPreventDefault) {
+    // When this function is registered as a passive listener, this
+    // call should be a no-op and might report a console warning.
+    e.preventDefault();
+  }
+}
+
+function listener2(e)
+{
+  if (doPreventDefault) {
+    e.preventDefault();
+  }
+}
+
+var elem = document.getElementById('dummy');
+
+function doTest(description, passiveArg)
+{
+  listenerHitCount = 0;
+
+  elem.addEventListener('test', listener, { passive: passiveArg });
+
+  // Test with a cancelable event
+  var e1 = new Event('test', { cancelable: true });
+  elem.dispatchEvent(e1);
+  is(listenerHitCount, 1, description + ' | hit count');
+  var expectedDefaultPrevented = (doPreventDefault && !passiveArg);
+  is(e1.defaultPrevented, expectedDefaultPrevented, description + ' | default prevented');
+
+  // Test with a non-cancelable event
+  var e2 = new Event('test', { cancelable: false });
+  elem.dispatchEvent(e2);
+  is(listenerHitCount, 2, description + ' | hit count after non-cancelable event');
+  is(e2.defaultPrevented, false, description + ' | default prevented on non-cancelable event');
+
+  // Test combining passive-enabled and "traditional" listeners
+  elem.addEventListener('test', listener2, false);
+  var e3 = new Event('test', { cancelable: true });
+  elem.dispatchEvent(e3);
+  is(listenerHitCount, 3, description + ' | hit count with second listener');
+  is(e3.defaultPrevented, doPreventDefault, description + ' | default prevented with second listener');
+  elem.removeEventListener('test', listener2, false);
+
+  elem.removeEventListener('test', listener, false);
+}
+
+function testAddListenerKey(passiveListenerFirst)
+{
+  listenerHitCount = 0;
+  doPreventDefault = true;
+
+  elem.addEventListener('test', listener, { capture: false, passive: passiveListenerFirst });
+  // This second listener should not be registered, because the "key" of
+  // { type, callback, capture } is the same, even though the 'passive' flag
+  // is different.
+  elem.addEventListener('test', listener, { capture: false, passive: !passiveListenerFirst });
+
+  var e1 = new Event('test', { cancelable: true });
+  elem.dispatchEvent(e1);
+
+  is(listenerHitCount, 1, 'Duplicate addEventListener was correctly ignored');
+  is(e1.defaultPrevented, !passiveListenerFirst, 'Prevent-default result based on first registered listener');
+
+  // Even though passive is the opposite of the first addEventListener call, it
+  // should remove the listener registered above.
+  elem.removeEventListener('test', listener, { capture: false, passive: !passiveListenerFirst });
+
+  var e2 = new Event('test', { cancelable: true });
+  elem.dispatchEvent(e2);
+
+  is(listenerHitCount, 1, 'non-passive listener was correctly unregistered');
+  is(e2.defaultPrevented, false, 'no listener was registered to preventDefault this event');
+}
+
+function test()
+{
+  doPreventDefault = false;
+
+  doTest('base case', undefined);
+  doTest('non-passive listener', false);
+  doTest('passive listener', true);
+
+  doPreventDefault = true;
+
+  doTest('base case', undefined);
+  doTest('non-passive listener', false);
+  doTest('passive listener', true);
+
+  testAddListenerKey(false);
+  testAddListenerKey(true);
+}
+
+test();
+
+</script>
+
+</body>
+</html>
+
+
--- a/widget/BasicEvents.h
+++ b/widget/BasicEvents.h
@@ -118,16 +118,19 @@ public:
   // in the parent process after the content process has handled it. Useful
   // for when the parent process need the know first how the event was used
   // by content before handling it itself.
   bool mWantReplyFromContentProcess : 1;
   // The event's action will be handled by APZ. The main thread should not
   // perform its associated action. This is currently only relevant for
   // wheel and touch events.
   bool mHandledByAPZ : 1;
+  // True if the event is currently being handled by an event listener that
+  // was registered as a passive listener.
+  bool mInPassiveListener: 1;
 
   // If the event is being handled in target phase, returns true.
   inline bool InTargetPhase() const
   {
     return (mInBubblingPhase && mInCapturePhase);
   }
 
   /**