Web Inspector: provide a way to enable/disable event listeners
authorwebkit@devinrousso.com <webkit@devinrousso.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 26 Oct 2017 04:07:59 +0000 (04:07 +0000)
committerwebkit@devinrousso.com <webkit@devinrousso.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 26 Oct 2017 04:07:59 +0000 (04:07 +0000)
https://bugs.webkit.org/show_bug.cgi?id=177451
<rdar://problem/34994925>

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

* inspector/protocol/DOM.json:
Add `setEventListenerDisabled` command that enables/disables a specific event listener
during event dispatch. When a disabled event listener is fired, the listener's callback will
not be called.

Source/WebCore:

Test: inspector/dom/setEventListenerDisabled.html

* dom/EventTarget.cpp:
(WebCore::EventTarget::fireEventListeners):
Add InspectorInstrumentation call to isEventListenerDisabled. If true, the event listener's
callback will not be called.

* inspector/InspectorDOMAgent.h:
* inspector/InspectorDOMAgent.cpp:
(WebCore::InspectorDOMAgent::discardBindings):
(WebCore::InspectorDOMAgent::getEventListenersForNode):
(WebCore::InspectorDOMAgent::setEventListenerDisabled):
(WebCore::InspectorDOMAgent::buildObjectForEventListener):
(WebCore::InspectorDOMAgent::willRemoveEventListener):
(WebCore::InspectorDOMAgent::isEventListenerDisabled):
Introduce a mapping of `EventListener*` to `InspectorEventListener`, a struct for uniquely
identifying event listeners so they can be referenced from the frontend. We only add items
to this mapping when `getEventListenersForNode` is called, as that is when EventListener
data is sent to the frontend. This allows us to defer creating an Inspector "mirror" object
for each EventListener until it is needed. Items are removed whenever an event listener is
removed or when the document changes.

* inspector/InspectorInstrumentation.h:
(WebCore::InspectorInstrumentation::isEventListenerDisabled):
* inspector/InspectorInstrumentation.cpp:
(WebCore::InspectorInstrumentation::willRemoveEventListenerImpl):
(WebCore::InspectorInstrumentation::isEventListenerDisabledImpl):
Pass additional parameters to InspectorDOMAgent so it can determine if the event listener
actually exists. If not, don't dispatch an event to the frontend as nothing will change.

Source/WebInspectorUI:

* Localizations/en.lproj/localizedStrings.js:

* UserInterface/Controllers/DOMTreeManager.js:
(WI.DOMTreeManager.prototype.setEventListenerDisabled):

* UserInterface/Views/DOMNodeDetailsSidebarPanel.js:
(WI.DOMNodeDetailsSidebarPanel.prototype.attached):
(WI.DOMNodeDetailsSidebarPanel.prototype.detached):
(WI.DOMNodeDetailsSidebarPanel.prototype._eventListenersChanged):
(WI.DOMNodeDetailsSidebarPanel.prototype.addEventListeners): Deleted.
(WI.DOMNodeDetailsSidebarPanel.prototype.removeEventListeners): Deleted.
Listen for `WI.DOMNode.Event.EventListenersChanged` on all instances of WI.DOMNode, since we
will still want to refresh the event listeners section in the event that an event listener
is removed from a parent node.

* UserInterface/Views/EventListenerSectionGroup.js:
(WI.EventListenerSectionGroup):
(WI.EventListenerSectionGroup.prototype._eventText):
(WI.EventListenerSectionGroup.prototype._nodeTextOrLink):
(WI.EventListenerSectionGroup.prototype._createDisabledToggleElement):
(WI.EventListenerSectionGroup.prototype._createDisabledToggleElement.updateTitle):
* UserInterface/Views/EventListenerSectionGroup.css:
(.event-listener-section > .content input[type="checkbox"]):

* UserInterface/Views/DetailsSectionSimpleRow.js:
(WI.DetailsSectionSimpleRow.prototype.get label):
(WI.DetailsSectionSimpleRow.prototype.set label):

LayoutTests:

* inspector/dom/setEventListenerDisabled-expected.txt: Added.
* inspector/dom/setEventListenerDisabled.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@224002 268f45cc-cd09-0410-ab3c-d52691b4dbfc

18 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/dom/setEventListenerDisabled-expected.txt [new file with mode: 0644]
LayoutTests/inspector/dom/setEventListenerDisabled.html [new file with mode: 0644]
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/protocol/DOM.json
Source/WebCore/ChangeLog
Source/WebCore/dom/EventTarget.cpp
Source/WebCore/inspector/InspectorDOMAgent.cpp
Source/WebCore/inspector/InspectorDOMAgent.h
Source/WebCore/inspector/InspectorInstrumentation.cpp
Source/WebCore/inspector/InspectorInstrumentation.h
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Controllers/DOMTreeManager.js
Source/WebInspectorUI/UserInterface/Views/DOMNodeDetailsSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/DetailsSectionSimpleRow.js
Source/WebInspectorUI/UserInterface/Views/EventListenerSectionGroup.css
Source/WebInspectorUI/UserInterface/Views/EventListenerSectionGroup.js

index 28a200d..8a9da2e 100644 (file)
@@ -1,5 +1,16 @@
 2017-10-25  Devin Rousso  <webkit@devinrousso.com>
 
+        Web Inspector: provide a way to enable/disable event listeners
+        https://bugs.webkit.org/show_bug.cgi?id=177451
+        <rdar://problem/34994925>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/dom/setEventListenerDisabled-expected.txt: Added.
+        * inspector/dom/setEventListenerDisabled.html: Added.
+
+2017-10-25  Devin Rousso  <webkit@devinrousso.com>
+
         Web Inspector: Canvas Tab: starting a second recording doesn't show red titlebar if the first recording was empty
         https://bugs.webkit.org/show_bug.cgi?id=178805
         <rdar://problem/35176303>
diff --git a/LayoutTests/inspector/dom/setEventListenerDisabled-expected.txt b/LayoutTests/inspector/dom/setEventListenerDisabled-expected.txt
new file mode 100644 (file)
index 0000000..e911640
--- /dev/null
@@ -0,0 +1,22 @@
+Testing DOMAgent.setEventListenerDisabled.
+
+
+== Running test suite: DOM.setEventListenerDisabled
+-- Running test case: DOM.setEventListenerDisabled.DisabledClickEvent
+Click event listener is enabled.
+Disabling event listener...
+<body> clicked.
+PASS: Click event listener did not fire.
+Click event listener is disabled.
+
+-- Running test case: DOM.setEventListenerDisabled.ReenabledClickEvent
+Click event listener is disabled.
+Enabling event listener...
+<body> clicked.
+PASS: Click event listener fired.
+Click event listener is enabled.
+
+-- Running test case: DOM.setEventListenerDisabled.Invalid
+PASS: Should produce an error.
+Error: No event listener for given identifier.
+
diff --git a/LayoutTests/inspector/dom/setEventListenerDisabled.html b/LayoutTests/inspector/dom/setEventListenerDisabled.html
new file mode 100644 (file)
index 0000000..c665dc5
--- /dev/null
@@ -0,0 +1,129 @@
+<!doctype html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function clickBody() {
+    document.body.click();
+
+    TestPage.dispatchEventToFrontend("TestPageAfterClick");
+}
+
+function test() {
+    let clickEventListener = null;
+
+    let suite = InspectorTest.createAsyncSuite("DOM.setEventListenerDisabled");
+
+    function logListener() {
+        return DOMAgent.getEventListenersForNode(clickEventListener.nodeId).then(({listeners}) => {
+            InspectorTest.assert(listeners.length === 1, "There should only be one event listener.");
+            InspectorTest.assert(listeners[0].type === "click", `There event listener should be for "click".`);
+            InspectorTest.log("Click event listener is " + (listeners[0].disabled ? "disabled" : "enabled") + ".");
+        });
+    }
+
+    suite.addTestCase({
+        name: "DOM.setEventListenerDisabled.DisabledClickEvent",
+        test(resolve, reject) {
+            let listener = InspectorTest.singleFireEventListener("TestPageDocumentClicked", () => {
+                reject("Click event listener should not be called");
+            });
+
+            InspectorTest.singleFireEventListener("TestPageAfterClick", () => {
+                InspectorTest.pass("Click event listener did not fire.");
+
+                InspectorTest.removeEventListener(listener);
+
+                logListener().then(resolve, reject);
+            });
+
+            logListener().then(() => {
+                InspectorTest.log("Disabling event listener...");
+
+                const disabled = true;
+                DOMAgent.setEventListenerDisabled(clickEventListener.eventListenerId, disabled, (error) => {
+                    if (error) {
+                        reject(error);
+                        return;
+                    }
+
+                    InspectorTest.evaluateInPage(`clickBody()`, () => {
+                        InspectorTest.log("<body> clicked.");
+                    });
+                });
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "DOM.setEventListenerDisabled.ReenabledClickEvent",
+        test(resolve, reject) {
+            InspectorTest.singleFireEventListener("TestPageDocumentClicked", () => {
+                InspectorTest.pass("Click event listener fired.");
+
+                logListener().then(resolve, reject);
+            });
+
+            logListener().then(() => {
+                InspectorTest.log("Enabling event listener...");
+
+                const disabled = false;
+                DOMAgent.setEventListenerDisabled(clickEventListener.eventListenerId, disabled, (error) => {
+                    if (error) {
+                        reject(error);
+                        return;
+                    }
+
+                    InspectorTest.evaluateInPage(`clickBody()`, () => {
+                        InspectorTest.log("<body> clicked.");
+                    });
+                });
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "DOM.setEventListenerDisabled.Invalid",
+        description: "Invalid event listener identifiers should cause an error.",
+        test(resolve, reject) {
+            const invalidEventListenerId = 9999999;
+            const disabled = false;
+            DOMAgent.setEventListenerDisabled(invalidEventListenerId, disabled, (error) => {
+                InspectorTest.expectThat(error, "Should produce an error.");
+                InspectorTest.log("Error: " + error);
+                resolve();
+            });
+        }
+    });
+
+    WI.domTreeManager.requestDocument((documentNode) => {
+        DOMAgent.getEventListenersForNode(documentNode.id, (error, eventListeners) => {
+            if (error) {
+                InspectorTest.fail("Unable to retrieve event listeners.");
+                InspectorTest.completeTest();
+                return;
+            }
+
+            clickEventListener = eventListeners[0];
+            if (!clickEventListener || clickEventListener.type !== "click") {
+                InspectorTest.fail("Missing click event listener.");
+                InspectorTest.completeTest();
+                return;
+            }
+
+            suite.runTestCasesAndFinish();
+        });
+    });
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing DOMAgent.setEventListenerDisabled.</p>
+
+    <script>
+        document.addEventListener("click", (event) => {
+            TestPage.dispatchEventToFrontend("TestPageDocumentClicked");
+        });
+    </script>
+</body>
+</html>
index 8f95f16..527b6b8 100644 (file)
@@ -1,3 +1,16 @@
+2017-10-25  Devin Rousso  <webkit@devinrousso.com>
+
+        Web Inspector: provide a way to enable/disable event listeners
+        https://bugs.webkit.org/show_bug.cgi?id=177451
+        <rdar://problem/34994925>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/protocol/DOM.json:
+        Add `setEventListenerDisabled` command that enables/disables a specific event listener
+        during event dispatch. When a disabled event listener is fired, the listener's callback will
+        not be called.
+
 2017-10-25  Commit Queue  <commit-queue@webkit.org>
 
         Unreviewed, rolling out r223691 and r223729.
index f9df66d..038f309 100644 (file)
             "description": "Unique DOM node identifier used to reference a node that may not have been pushed to the front-end."
         },
         {
+            "id": "EventListenerId",
+            "type": "integer",
+            "description": "Unique event listener identifier."
+        },
+        {
             "id": "PseudoType",
             "type": "string",
             "enum": ["before", "after"],
@@ -74,6 +79,7 @@
             "type": "object",
             "description": "A structure holding event listener properties.",
             "properties": [
+                { "name": "eventListenerId", "$ref": "EventListenerId" },
                 { "name": "type", "type": "string", "description": "<code>EventListener</code>'s type." },
                 { "name": "useCapture", "type": "boolean", "description": "<code>EventListener</code>'s useCapture." },
                 { "name": "isAttribute", "type": "boolean", "description": "<code>EventListener</code>'s isAttribute." },
@@ -83,7 +89,8 @@
                 { "name": "sourceName", "type": "string", "optional": true, "description": "Source script URL." },
                 { "name": "handler", "$ref": "Runtime.RemoteObject", "optional": true, "description": "Event handler function value." },
                 { "name": "passive", "type": "boolean", "optional": true, "description": "<code>EventListener</code>'s passive." },
-                { "name": "once", "type": "boolean", "optional": true, "description": "<code>EventListener</code>'s once." }
+                { "name": "once", "type": "boolean", "optional": true, "description": "<code>EventListener</code>'s once." },
+                { "name": "disabled", "type": "boolean", "optional": true }
             ]
         },
         {
             ]
         },
         {
+            "name": "setEventListenerDisabled",
+            "description": "Enable/disable the given event listener. A disabled event listener will not fire.",
+            "parameters": [
+                { "name": "eventListenerId", "$ref": "EventListenerId" },
+                { "name": "disabled", "type": "boolean" }
+            ]
+        },
+        {
             "name": "getAccessibilityPropertiesForNode",
             "description": "Returns a dictionary of accessibility properties for the node.",
             "parameters": [
index d52f4a7..3cc95ff 100644 (file)
@@ -1,3 +1,41 @@
+2017-10-25  Devin Rousso  <webkit@devinrousso.com>
+
+        Web Inspector: provide a way to enable/disable event listeners
+        https://bugs.webkit.org/show_bug.cgi?id=177451
+        <rdar://problem/34994925>
+
+        Reviewed by Joseph Pecoraro.
+
+        Test: inspector/dom/setEventListenerDisabled.html
+
+        * dom/EventTarget.cpp:
+        (WebCore::EventTarget::fireEventListeners):
+        Add InspectorInstrumentation call to isEventListenerDisabled. If true, the event listener's
+        callback will not be called.
+
+        * inspector/InspectorDOMAgent.h:
+        * inspector/InspectorDOMAgent.cpp:
+        (WebCore::InspectorDOMAgent::discardBindings):
+        (WebCore::InspectorDOMAgent::getEventListenersForNode):
+        (WebCore::InspectorDOMAgent::setEventListenerDisabled):
+        (WebCore::InspectorDOMAgent::buildObjectForEventListener):
+        (WebCore::InspectorDOMAgent::willRemoveEventListener):
+        (WebCore::InspectorDOMAgent::isEventListenerDisabled):
+        Introduce a mapping of `EventListener*` to `InspectorEventListener`, a struct for uniquely
+        identifying event listeners so they can be referenced from the frontend. We only add items
+        to this mapping when `getEventListenersForNode` is called, as that is when EventListener
+        data is sent to the frontend. This allows us to defer creating an Inspector "mirror" object
+        for each EventListener until it is needed. Items are removed whenever an event listener is
+        removed or when the document changes.
+
+        * inspector/InspectorInstrumentation.h:
+        (WebCore::InspectorInstrumentation::isEventListenerDisabled):
+        * inspector/InspectorInstrumentation.cpp:
+        (WebCore::InspectorInstrumentation::willRemoveEventListenerImpl):
+        (WebCore::InspectorInstrumentation::isEventListenerDisabledImpl):
+        Pass additional parameters to InspectorDOMAgent so it can determine if the event listener
+        actually exists. If not, don't dispatch an event to the frontend as nothing will change.
+
 2017-10-25  Keith Miller  <keith_miller@apple.com>
 
         Move Platform to unified sources.
index 7668cc7..a749e3e 100644 (file)
@@ -278,6 +278,9 @@ void EventTarget::fireEventListeners(Event& event, EventListenerVector listeners
         if (event.eventPhase() == Event::BUBBLING_PHASE && registeredListener->useCapture())
             continue;
 
+        if (InspectorInstrumentation::isEventListenerDisabled(*this, event.type(), registeredListener->callback(), registeredListener->useCapture()))
+            continue;
+
         // If stopImmediatePropagation has been called, we just break out immediately, without
         // handling any more events on this target.
         if (event.immediatePropagationStopped())
index 27a9bcd..468ba9c 100644 (file)
@@ -486,6 +486,7 @@ void InspectorDOMAgent::discardBindings()
 {
     m_documentNodeToIdMap.clear();
     m_idToNode.clear();
+    m_eventListenerEntries.clear();
     releaseDanglingNodes();
     m_childrenRequested.clear();
     m_backendIdToNode.clear();
@@ -830,12 +831,31 @@ void InspectorDOMAgent::getEventListenersForNode(ErrorString& errorString, int n
     Vector<EventListenerInfo> eventInformation;
     getEventListeners(node, eventInformation, true);
 
+    auto addListener = [&] (RegisteredEventListener& listener, const EventListenerInfo& info) {
+        int identifier = 0;
+        bool disabled = false;
+
+        auto it = m_eventListenerEntries.find(&listener.callback());
+        if (it == m_eventListenerEntries.end()) {
+            InspectorEventListener inspectorEventListener(m_lastEventListenerId++, *info.node, info.eventType, listener.useCapture());
+            m_eventListenerEntries.add(&listener.callback(), inspectorEventListener);
+
+            identifier = inspectorEventListener.identifier;
+            disabled = inspectorEventListener.disabled;
+        } else {
+            identifier = it->value.identifier;
+            disabled = it->value.disabled;
+        }
+
+        listenersArray->addItem(buildObjectForEventListener(listener, identifier, info.eventType, info.node, objectGroup, disabled));
+    };
+
     // Get Capturing Listeners (in this order)
     size_t eventInformationLength = eventInformation.size();
     for (auto& info : eventInformation) {
         for (auto& listener : info.eventListenerVector) {
             if (listener->useCapture())
-                listenersArray->addItem(buildObjectForEventListener(*listener, info.eventType, info.node, objectGroup));
+                addListener(*listener, info);
         }
     }
 
@@ -844,7 +864,7 @@ void InspectorDOMAgent::getEventListenersForNode(ErrorString& errorString, int n
         const EventListenerInfo& info = eventInformation[i - 1];
         for (auto& listener : info.eventListenerVector) {
             if (!listener->useCapture())
-                listenersArray->addItem(buildObjectForEventListener(*listener, info.eventType, info.node, objectGroup));
+                addListener(*listener, info);
         }
     }
 }
@@ -881,6 +901,18 @@ void InspectorDOMAgent::getEventListeners(Node* node, Vector<EventListenerInfo>&
     }
 }
 
+void InspectorDOMAgent::setEventListenerDisabled(ErrorString& errorString, int eventListenerId, bool disabled)
+{
+    for (InspectorEventListener& inspectorEventListener : m_eventListenerEntries.values()) {
+        if (inspectorEventListener.identifier == eventListenerId) {
+            inspectorEventListener.disabled = disabled;
+            return;
+        }
+    }
+
+    errorString = ASCIILiteral("No event listener for given identifier.");
+}
+
 void InspectorDOMAgent::getAccessibilityPropertiesForNode(ErrorString& errorString, int nodeId, RefPtr<Inspector::Protocol::DOM::AccessibilityProperties>& axProperties)
 {
     Node* node = assertNode(errorString, nodeId);
@@ -1554,7 +1586,7 @@ RefPtr<Inspector::Protocol::Array<Inspector::Protocol::DOM::Node>> InspectorDOMA
     return WTFMove(pseudoElements);
 }
 
-Ref<Inspector::Protocol::DOM::EventListener> InspectorDOMAgent::buildObjectForEventListener(const RegisteredEventListener& registeredEventListener, const AtomicString& eventType, Node* node, const String* objectGroupId)
+Ref<Inspector::Protocol::DOM::EventListener> InspectorDOMAgent::buildObjectForEventListener(const RegisteredEventListener& registeredEventListener, int identifier, const AtomicString& eventType, Node* node, const String* objectGroupId, bool disabled)
 {
     Ref<EventListener> eventListener = registeredEventListener.callback();
 
@@ -1585,6 +1617,7 @@ Ref<Inspector::Protocol::DOM::EventListener> InspectorDOMAgent::buildObjectForEv
     }
 
     auto value = Inspector::Protocol::DOM::EventListener::create()
+        .setEventListenerId(identifier)
         .setType(eventType)
         .setUseCapture(registeredEventListener.useCapture())
         .setIsAttribute(eventListener->isAttribute())
@@ -1610,6 +1643,8 @@ Ref<Inspector::Protocol::DOM::EventListener> InspectorDOMAgent::buildObjectForEv
         value->setPassive(true);
     if (registeredEventListener.isOnce())
         value->setOnce(true);
+    if (disabled)
+        value->setDisabled(disabled);
     return value;
 }
     
@@ -2220,7 +2255,7 @@ void InspectorDOMAgent::didAddEventListener(EventTarget& target)
     m_frontendDispatcher->didAddEventListener(nodeId);
 }
 
-void InspectorDOMAgent::willRemoveEventListener(EventTarget& target)
+void InspectorDOMAgent::willRemoveEventListener(EventTarget& target, const AtomicString& eventType, EventListener& listener, bool capture)
 {
     auto node = target.toNode();
     if (!node)
@@ -2230,9 +2265,34 @@ void InspectorDOMAgent::willRemoveEventListener(EventTarget& target)
     if (!nodeId)
         return;
 
+    bool listenerExists = false;
+    for (const RefPtr<RegisteredEventListener>& item : node->eventListeners(eventType)) {
+        if (item->callback() == listener && item->useCapture() == capture) {
+            listenerExists = true;
+            break;
+        }
+    }
+
+    if (!listenerExists)
+        return;
+
+    m_eventListenerEntries.remove(&listener);
+
     m_frontendDispatcher->willRemoveEventListener(nodeId);
 }
 
+bool InspectorDOMAgent::isEventListenerDisabled(EventTarget& target, const AtomicString& eventType, EventListener& listener, bool capture)
+{
+    auto it = m_eventListenerEntries.find(&listener);
+    if (it == m_eventListenerEntries.end())
+        return false;
+
+    if (!it->value.disabled)
+        return false;
+
+    return it->value.eventTarget.get() == &target && it->value.eventType == eventType && it->value.useCapture == capture;
+}
+
 Node* InspectorDOMAgent::nodeForPath(const String& path)
 {
     // The path is of form "1,HTML,2,BODY,1,DIV"
index a216f56..d942eff 100644 (file)
@@ -123,6 +123,7 @@ public:
     void setOuterHTML(ErrorString&, int nodeId, const String& outerHTML) override;
     void setNodeValue(ErrorString&, int nodeId, const String& value) override;
     void getEventListenersForNode(ErrorString&, int nodeId, const WTF::String* const objectGroup, RefPtr<Inspector::Protocol::Array<Inspector::Protocol::DOM::EventListener>>& listenersArray) override;
+    void setEventListenerDisabled(ErrorString&, int eventListenerId, bool disabled) override;
     void getAccessibilityPropertiesForNode(ErrorString&, int nodeId, RefPtr<Inspector::Protocol::DOM::AccessibilityProperties>& axProperties) override;
     void performSearch(ErrorString&, const String& whitespaceTrimmedQuery, const Inspector::InspectorArray* nodeIds, String* searchId, int* resultCount) override;
     void getSearchResults(ErrorString&, const String& searchId, int fromIndex, int toIndex, RefPtr<Inspector::Protocol::Array<int>>&) override;
@@ -168,7 +169,8 @@ public:
     void pseudoElementCreated(PseudoElement&);
     void pseudoElementDestroyed(PseudoElement&);
     void didAddEventListener(EventTarget&);
-    void willRemoveEventListener(EventTarget&);
+    void willRemoveEventListener(EventTarget&, const AtomicString& eventType, EventListener&, bool capture);
+    bool isEventListenerDisabled(EventTarget&, const AtomicString& eventType, EventListener&, bool capture);
 
     // Callbacks that don't directly correspond to an instrumentation entry point.
     void setDocument(Document*);
@@ -232,7 +234,7 @@ private:
     Ref<Inspector::Protocol::Array<String>> buildArrayForElementAttributes(Element*);
     Ref<Inspector::Protocol::Array<Inspector::Protocol::DOM::Node>> buildArrayForContainerChildren(Node* container, int depth, NodeToIdMap* nodesMap);
     RefPtr<Inspector::Protocol::Array<Inspector::Protocol::DOM::Node>> buildArrayForPseudoElements(const Element&, NodeToIdMap* nodesMap);
-    Ref<Inspector::Protocol::DOM::EventListener> buildObjectForEventListener(const RegisteredEventListener&, const AtomicString& eventType, Node*, const String* objectGroupId);
+    Ref<Inspector::Protocol::DOM::EventListener> buildObjectForEventListener(const RegisteredEventListener&, int identifier, const AtomicString& eventType, Node*, const String* objectGroupId, bool disabled = false);
     RefPtr<Inspector::Protocol::DOM::AccessibilityProperties> buildObjectForAccessibilityProperties(Node*);
     void processAccessibilityChildren(RefPtr<AccessibilityObject>&&, RefPtr<Inspector::Protocol::Array<int>>&&);
     
@@ -273,6 +275,27 @@ private:
     bool m_searchingForNode { false };
     bool m_suppressAttributeModifiedEvent { false };
     bool m_documentRequested { false };
+
+    struct InspectorEventListener {
+        int identifier { 1 };
+        RefPtr<EventTarget> eventTarget;
+        AtomicString eventType;
+        bool useCapture { false };
+        bool disabled { false };
+
+        InspectorEventListener() { }
+
+        InspectorEventListener(int identifier, EventTarget& eventTarget, const AtomicString& eventType, bool useCapture)
+            : identifier(identifier)
+            , eventTarget(&eventTarget)
+            , eventType(eventType)
+            , useCapture(useCapture)
+        {
+        }
+    };
+
+    HashMap<EventListener*, InspectorEventListener> m_eventListenerEntries;
+    int m_lastEventListenerId { 1 };
 };
 
 } // namespace WebCore
index 9f44232..321ceab 100644 (file)
@@ -327,7 +327,14 @@ void InspectorInstrumentation::willRemoveEventListenerImpl(InstrumentingAgents&
     if (PageDebuggerAgent* pageDebuggerAgent = instrumentingAgents.pageDebuggerAgent())
         pageDebuggerAgent->willRemoveEventListener(target, eventType, listener, capture);
     if (InspectorDOMAgent* domAgent = instrumentingAgents.inspectorDOMAgent())
-        domAgent->willRemoveEventListener(target);
+        domAgent->willRemoveEventListener(target, eventType, listener, capture);
+}
+
+bool InspectorInstrumentation::isEventListenerDisabledImpl(InstrumentingAgents& instrumentingAgents, EventTarget& target, const AtomicString& eventType, EventListener& listener, bool capture)
+{
+    if (InspectorDOMAgent* domAgent = instrumentingAgents.inspectorDOMAgent())
+        return domAgent->isEventListenerDisabled(target, eventType, listener, capture);
+    return false;
 }
 
 void InspectorInstrumentation::didPostMessageImpl(InstrumentingAgents& instrumentingAgents, const TimerBase& timer, JSC::ExecState& state)
index da40e6a..c636cfc 100644 (file)
@@ -146,6 +146,7 @@ public:
     static void didCallFunction(const InspectorInstrumentationCookie&, ScriptExecutionContext*);
     static void didAddEventListener(EventTarget&, const AtomicString& eventType);
     static void willRemoveEventListener(EventTarget&, const AtomicString& eventType, EventListener&, bool capture);
+    static bool isEventListenerDisabled(EventTarget&, const AtomicString& eventType, EventListener&, bool capture);
     static InspectorInstrumentationCookie willDispatchEvent(Document&, const Event&, bool hasEventListeners);
     static void didDispatchEvent(const InspectorInstrumentationCookie&);
     static void willHandleEvent(ScriptExecutionContext&, const Event&, const RegisteredEventListener&);
@@ -323,6 +324,7 @@ private:
     static void didCallFunctionImpl(const InspectorInstrumentationCookie&, ScriptExecutionContext*);
     static void didAddEventListenerImpl(InstrumentingAgents&, EventTarget&, const AtomicString& eventType);
     static void willRemoveEventListenerImpl(InstrumentingAgents&, EventTarget&, const AtomicString& eventType, EventListener&, bool capture);
+    static bool isEventListenerDisabledImpl(InstrumentingAgents&, EventTarget&, const AtomicString& eventType, EventListener&, bool capture);
     static InspectorInstrumentationCookie willDispatchEventImpl(InstrumentingAgents&, Document&, const Event&, bool hasEventListeners);
     static void willHandleEventImpl(InstrumentingAgents&, const Event&, const RegisteredEventListener&);
     static void didHandleEventImpl(InstrumentingAgents&);
@@ -687,6 +689,14 @@ inline void InspectorInstrumentation::willRemoveEventListener(EventTarget& targe
         willRemoveEventListenerImpl(*instrumentingAgents, target, eventType, listener, capture);
 }
 
+inline bool InspectorInstrumentation::isEventListenerDisabled(EventTarget& target, const AtomicString& eventType, EventListener& listener, bool capture)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(false);
+    if (InstrumentingAgents* instrumentingAgents = instrumentingAgentsForContext(target.scriptExecutionContext()))
+        return isEventListenerDisabledImpl(*instrumentingAgents, target, eventType, listener, capture);
+    return false;
+}
+
 inline void InspectorInstrumentation::didPostMessage(Frame& frame, TimerBase& timer, JSC::ExecState& state)
 {
     FAST_RETURN_IF_NO_FRONTENDS(void());
index 076dba7..ccf1e68 100644 (file)
@@ -1,5 +1,41 @@
 2017-10-25  Devin Rousso  <webkit@devinrousso.com>
 
+        Web Inspector: provide a way to enable/disable event listeners
+        https://bugs.webkit.org/show_bug.cgi?id=177451
+        <rdar://problem/34994925>
+
+        Reviewed by Joseph Pecoraro.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
+        * UserInterface/Controllers/DOMTreeManager.js:
+        (WI.DOMTreeManager.prototype.setEventListenerDisabled):
+
+        * UserInterface/Views/DOMNodeDetailsSidebarPanel.js:
+        (WI.DOMNodeDetailsSidebarPanel.prototype.attached):
+        (WI.DOMNodeDetailsSidebarPanel.prototype.detached):
+        (WI.DOMNodeDetailsSidebarPanel.prototype._eventListenersChanged):
+        (WI.DOMNodeDetailsSidebarPanel.prototype.addEventListeners): Deleted.
+        (WI.DOMNodeDetailsSidebarPanel.prototype.removeEventListeners): Deleted.
+        Listen for `WI.DOMNode.Event.EventListenersChanged` on all instances of WI.DOMNode, since we
+        will still want to refresh the event listeners section in the event that an event listener
+        is removed from a parent node.
+
+        * UserInterface/Views/EventListenerSectionGroup.js:
+        (WI.EventListenerSectionGroup):
+        (WI.EventListenerSectionGroup.prototype._eventText):
+        (WI.EventListenerSectionGroup.prototype._nodeTextOrLink):
+        (WI.EventListenerSectionGroup.prototype._createDisabledToggleElement):
+        (WI.EventListenerSectionGroup.prototype._createDisabledToggleElement.updateTitle):
+        * UserInterface/Views/EventListenerSectionGroup.css:
+        (.event-listener-section > .content input[type="checkbox"]):
+
+        * UserInterface/Views/DetailsSectionSimpleRow.js:
+        (WI.DetailsSectionSimpleRow.prototype.get label):
+        (WI.DetailsSectionSimpleRow.prototype.set label):
+
+2017-10-25  Devin Rousso  <webkit@devinrousso.com>
+
         Web Inspector: replace TypeVerifier with subclasses of WI.Collection
         https://bugs.webkit.org/show_bug.cgi?id=178045
         <rdar://problem/35174307>
index 29c2aa2..901486a 100644 (file)
@@ -297,6 +297,7 @@ localizedStrings["Dimensions"] = "Dimensions";
 localizedStrings["Direction"] = "Direction";
 localizedStrings["Disable Breakpoint"] = "Disable Breakpoint";
 localizedStrings["Disable Breakpoints"] = "Disable Breakpoints";
+localizedStrings["Disable Event Listener"] = "Disable Event Listener";
 localizedStrings["Disable Program"] = "Disable Program";
 localizedStrings["Disable all breakpoints (%s)"] = "Disable all breakpoints (%s)";
 localizedStrings["Disable paint flashing"] = "Disable paint flashing";
@@ -361,11 +362,13 @@ localizedStrings["Element overlaps other compositing element"] = "Element overla
 localizedStrings["Elements"] = "Elements";
 localizedStrings["Enable Breakpoint"] = "Enable Breakpoint";
 localizedStrings["Enable Breakpoints"] = "Enable Breakpoints";
+localizedStrings["Enable Event Listener"] = "Enable Event Listener";
 localizedStrings["Enable Layers Tab"] = "Enable Layers Tab";
 localizedStrings["Enable Program"] = "Enable Program";
 localizedStrings["Enable all breakpoints (%s)"] = "Enable all breakpoints (%s)";
 localizedStrings["Enable breakpoints"] = "Enable breakpoints";
 localizedStrings["Enable paint flashing"] = "Enable paint flashing";
+localizedStrings["Enabled"] = "Enabled";
 localizedStrings["Encoded"] = "Encoded";
 localizedStrings["Encoding"] = "Encoding";
 localizedStrings["Enter Tag"] = "Enter Tag";
index 82782fe..67f3be9 100644 (file)
@@ -526,6 +526,11 @@ WI.DOMTreeManager = class DOMTreeManager extends WI.Object
         DOMAgent.setInspectedNode(node.id, callback);
     }
 
+    setEventListenerDisabled(eventListenerId, disabled)
+    {
+        DOMAgent.setEventListenerDisabled(eventListenerId, disabled);
+    }
+
     _buildHighlightConfig(mode = "all")
     {
         let highlightConfig = {showInfo: mode === "all"};
index 82c4ccf..9a8190e 100644 (file)
@@ -45,16 +45,6 @@ WI.DOMNodeDetailsSidebarPanel = class DOMNodeDetailsSidebarPanel extends WI.DOMD
         super.closed();
     }
 
-    addEventListeners()
-    {
-        this.domNode.addEventListener(WI.DOMNode.Event.EventListenersChanged, this._eventListenersChanged, this);
-    }
-
-    removeEventListeners()
-    {
-        this.domNode.removeEventListener(WI.DOMNode.Event.EventListenersChanged, this._eventListenersChanged, this);
-    }
-
     // Protected
 
     initialLayout()
@@ -169,6 +159,20 @@ WI.DOMNodeDetailsSidebarPanel = class DOMNodeDetailsSidebarPanel extends WI.DOMD
         this._attributesDataGridRow.sizeDidChange();
     }
 
+    attached()
+    {
+        super.attached();
+
+        WI.DOMNode.addEventListener(WI.DOMNode.Event.EventListenersChanged, this._eventListenersChanged, this);
+    }
+
+    detached()
+    {
+        WI.DOMNode.removeEventListener(WI.DOMNode.Event.EventListenersChanged, this._eventListenersChanged, this);
+
+        super.detached();
+    }
+
     // Private
 
     _accessibilitySupported()
@@ -753,7 +757,8 @@ WI.DOMNodeDetailsSidebarPanel = class DOMNodeDetailsSidebarPanel extends WI.DOMD
 
     _eventListenersChanged(event)
     {
-        this._refreshEventListeners();
+        if (event.target === this.domNode || event.target.isAncestor(this.domNode))
+            this._refreshEventListeners();
     }
 
     _attributesChanged(event)
index 8e4c34c..e5a5fec 100644 (file)
@@ -73,12 +73,18 @@ WI.DetailsSectionSimpleRow = class DetailsSectionSimpleRow extends WI.DetailsSec
 
     get label()
     {
-        return this._labelElement.textContent;
+        return this._label;
     }
 
     set label(label)
     {
-        this._labelElement.textContent = label;
+        this._label = label || "";
+
+        if (this._label instanceof Node) {
+            this._labelElement.removeChildren();
+            this._labelElement.appendChild(this._label);
+        } else
+            this._labelElement.textContent = this._label;
     }
 
     get value()
index 00b1889..4765752 100644 (file)
@@ -28,3 +28,9 @@
     overflow: hidden;
     text-overflow: ellipsis;
 }
+
+.event-listener-section > .content input[type="checkbox"] {
+    width: 12px;
+    height: 12px;
+    margin: 0;
+}
index cdf987e..67e7e31 100644 (file)
@@ -52,6 +52,9 @@ WI.EventListenerSectionGroup = class EventListenerSectionGroup extends WI.Detail
         if (this._eventListener.once)
             rows.push(new WI.DetailsSectionSimpleRow(WI.UIString("Once"), WI.UIString("Yes")));
 
+        if (DOMAgent.setEventListenerDisabled && this._eventListener.eventListenerId)
+            rows.push(this._createDisabledToggleRow());
+
         this.rows = rows;
     }
 
@@ -104,4 +107,34 @@ WI.EventListenerSectionGroup = class EventListenerSectionGroup extends WI.Detail
         fragment.append(linkElement, functionName);
         return fragment;
     }
+
+    _createDisabledToggleRow()
+    {
+        let toggleElement = document.createElement("input");
+        toggleElement.type = "checkbox";
+        toggleElement.checked = !this._eventListener.disabled;
+
+        let updateTitle = () => {
+            if (this._eventListener.disabled)
+                toggleElement.title = WI.UIString("Enable Event Listener");
+            else
+                toggleElement.title = WI.UIString("Disable Event Listener");
+        };
+
+        updateTitle();
+
+        toggleElement.addEventListener("change", (event) => {
+            this._eventListener.disabled = !toggleElement.checked;
+            WI.domTreeManager.setEventListenerDisabled(this._eventListener.eventListenerId, this._eventListener.disabled);
+            updateTitle();
+        });
+
+        let toggleLabel = document.createElement("span");
+        toggleLabel.textContent = WI.UIString("Enabled");
+        toggleLabel.addEventListener("click", (event) => {
+            toggleElement.click();
+        });
+
+        return new WI.DetailsSectionSimpleRow(toggleLabel, toggleElement);
+    }
 };