[DevTools] Support JQuery event listeners

This CL adds base support of JQuery event listeners. Listeners which was installed by JQuery are shown in Event Listeners sidebar.
API for frameworks are provided. Developers can add own event listeners provider to global devtoolsFrameworkEventListeners array (it should be created if undefined). Provider functions should return two arrays:
1. Array with framework event listeners for current object in eventListeners field in format {type: {string}, useCapture: {boolean}, handler: (function()|Object)}.
2. Array with internal framework event handlers for current object in internalHandlers field - one function per each handler. This array is used for filtering out native internal event handlers.
BUG=61643
R=paulirish@chromium.org, pfeldman@chromium.org, yurys@chromium.org

Review URL: https://codereview.chromium.org/1268353005 .

git-svn-id: svn://svn.chromium.org/blink/trunk@201767 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent 0107085a
...@@ -461,8 +461,10 @@ InspectorTest.expandAndDumpSelectedElementEventListeners = function(callback) ...@@ -461,8 +461,10 @@ InspectorTest.expandAndDumpSelectedElementEventListeners = function(callback)
InspectorTest.addResult(""); InspectorTest.addResult("");
InspectorTest.addResult("======== " + eventType + " ========"); InspectorTest.addResult("======== " + eventType + " ========");
var listenerItems = listenerTypes[i].children(); var listenerItems = listenerTypes[i].children();
for (var j = 0; j < listenerItems.length; ++j) for (var j = 0; j < listenerItems.length; ++j) {
InspectorTest.addResult("== " + listenerItems[j].eventListener().listenerType());
InspectorTest.dumpObjectPropertyTreeElement(listenerItems[j]); InspectorTest.dumpObjectPropertyTreeElement(listenerItems[j]);
}
} }
callback(); callback();
} }
......
...@@ -3,42 +3,53 @@ Tests event listeners output in the Elements sidebar panel. ...@@ -3,42 +3,53 @@ Tests event listeners output in the Elements sidebar panel.
Inspect Me Inspect Me
======== click ======== ======== click ========
== normal
[expanded] documentevent-listener-sidebar.html:6 [expanded] documentevent-listener-sidebar.html:6
useCapture: false useCapture: false
handler: function documentClickHandler(event) { console.log("click - document - attribute"); } handler: function documentClickHandler(event) { console.log("click - document - attribute"); }
== normal
[expanded] documentevent-listener-sidebar.html:31 [expanded] documentevent-listener-sidebar.html:31
useCapture: true useCapture: true
handler: function () { console.log("click - document - handleEvent"); } handler: function () { console.log("click - document - handleEvent"); }
== normal
[expanded] documentevent-listener-sidebar.html:25 [expanded] documentevent-listener-sidebar.html:25
useCapture: true useCapture: true
handler: function ObjectHandler() { document.addEventListener("click", this, true); } handler: function ObjectHandler() { document.addEventListener("click", this, true); }
== normal
[expanded] documentevent-listener-sidebar.html:19 [expanded] documentevent-listener-sidebar.html:19
useCapture: true useCapture: true
handler: function (event) { console.log("click - document - capturing"); } handler: function (event) { console.log("click - document - capturing"); }
== normal
[expanded] button#nodeevent-listener-sidebar.html:17 [expanded] button#nodeevent-listener-sidebar.html:17
useCapture: false useCapture: false
handler: function (event) { console.log("click - button - bubbling (registered after attribute)"); } handler: function (event) { console.log("click - button - bubbling (registered after attribute)"); }
== normal
[expanded] button#nodeevent-listener-sidebar.html:16 [expanded] button#nodeevent-listener-sidebar.html:16
useCapture: false useCapture: false
handler: function (event) { console.log("click - button - attribute"); } handler: function (event) { console.log("click - button - attribute"); }
== normal
[expanded] button#nodeevent-listener-sidebar.html:12 [expanded] button#nodeevent-listener-sidebar.html:12
useCapture: false useCapture: false
handler: function clickHandler(event) { console.log("click - button - bubbling (registered before attribute)"); } handler: function clickHandler(event) { console.log("click - button - bubbling (registered before attribute)"); }
== normal
[expanded] button#nodeevent-listener-sidebar.html:15 [expanded] button#nodeevent-listener-sidebar.html:15
useCapture: true useCapture: true
handler: function (event) { console.log("click - button - capturing"); } handler: function (event) { console.log("click - button - capturing"); }
======== custom event ======== ======== custom event ========
== normal
[expanded] bodyevent-listener-sidebar.html:10 [expanded] bodyevent-listener-sidebar.html:10
useCapture: true useCapture: true
handler: function f() {} handler: function f() {}
======== hover ======== ======== hover ========
== normal
[expanded] button#nodeevent-listener-sidebar.html:14 [expanded] button#nodeevent-listener-sidebar.html:14
useCapture: false useCapture: false
handler: function hoverHandler(event) { console.log("hover - button - bubbling"); } handler: function hoverHandler(event) { console.log("hover - button - bubbling"); }
======== load ======== ======== load ========
== normal
[expanded] Windowevent-listener-sidebar.html:73 [expanded] Windowevent-listener-sidebar.html:73
useCapture: false useCapture: false
handler: function onload(event) { handler: function onload(event) {
......
Tests event listeners output in the Elements sidebar panel.
Inspect Me
======== click ========
== frameworkUser
[expanded] button#nodeevent-listener-sidebar-jquery1.html:11
useCapture: true
handler: function (){ console.log("second jquery"); }
== frameworkUser
[expanded] button#nodeevent-listener-sidebar-jquery1.html:10
useCapture: true
handler: function (){ console.log("first jquery"); }
== normal
[expanded] button#nodeevent-listener-sidebar-jquery1.html:12
useCapture: false
handler: function () { console.log("addEventListener"); }
== frameworkInternal
[expanded] button#nodejquery-1.11.3.min.js:4
useCapture: false
handler: function (a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)}
======== load ========
== normal
[expanded] Windowevent-listener-sidebar-jquery1.html:36
useCapture: false
handler: function onload(event) {
onloadHandler()
}
<html>
<head>
<script src="../../http/tests/inspector/inspector-test.js"></script>
<script src="../../http/tests/inspector/elements-test.js"></script>
<script src="resources/jquery-1.11.3.min.js"></script>
<script>
function setupEventListeners()
{
var node = $("#node")[0];
$("#node").click(function(){ console.log("first jquery"); });
$("#node").click(function(){ console.log("second jquery"); });
node.addEventListener("click", function() { console.log("addEventListener"); });
}
function test()
{
var sidebarPane = WebInspector.panels.elements.sidebarPanes.eventListeners;
WebInspector.settingForTest("showEventListenersForAncestors").set(true);
InspectorTest.selectNodeWithId("node", step1);
function step1()
{
InspectorTest.expandAndDumpSelectedElementEventListeners(InspectorTest.completeTest.bind(this));
}
}
function onloadHandler()
{
setupEventListeners();
runTest();
}
</script>
</head>
<body onload="onloadHandler()">
<p>
Tests event listeners output in the Elements sidebar panel.
</p>
<button id="node">Inspect Me</button>
</body>
</html>
Tests event listeners output in the Elements sidebar panel.
Inspect Me
======== click ========
== frameworkUser
[expanded] button#nodeevent-listener-sidebar-jquery2.html:11
useCapture: true
handler: function (){ console.log("second jquery"); }
== frameworkUser
[expanded] button#nodeevent-listener-sidebar-jquery2.html:10
useCapture: true
handler: function (){ console.log("first jquery"); }
== normal
[expanded] button#nodeevent-listener-sidebar-jquery2.html:12
useCapture: false
handler: function () { console.log("addEventListener"); }
== frameworkInternal
[expanded] button#nodejquery-2.1.4.min.js:3
useCapture: false
handler: function (b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}
======== load ========
== normal
[expanded] Windowevent-listener-sidebar-jquery2.html:36
useCapture: false
handler: function onload(event) {
onloadHandler()
}
<html>
<head>
<script src="../../http/tests/inspector/inspector-test.js"></script>
<script src="../../http/tests/inspector/elements-test.js"></script>
<script src="resources/jquery-2.1.4.min.js"></script>
<script>
function setupEventListeners()
{
var node = $("#node")[0];
$("#node").click(function(){ console.log("first jquery"); });
$("#node").click(function(){ console.log("second jquery"); });
node.addEventListener("click", function() { console.log("addEventListener"); });
}
function test()
{
var sidebarPane = WebInspector.panels.elements.sidebarPanes.eventListeners;
WebInspector.settingForTest("showEventListenersForAncestors").set(true);
InspectorTest.selectNodeWithId("node", step1);
function step1()
{
InspectorTest.expandAndDumpSelectedElementEventListeners(InspectorTest.completeTest.bind(this));
}
}
function onloadHandler()
{
setupEventListeners();
runTest();
}
</script>
</head>
<body onload="onloadHandler()">
<p>
Tests event listeners output in the Elements sidebar panel.
</p>
<button id="node">Inspect Me</button>
</body>
</html>
...@@ -3,11 +3,13 @@ Tests event listeners output in the Elements sidebar panel when the listeners ar ...@@ -3,11 +3,13 @@ Tests event listeners output in the Elements sidebar panel when the listeners ar
======== click ======== ======== click ========
== normal
[expanded] bodyevent-listeners-about-blank.html:9 [expanded] bodyevent-listeners-about-blank.html:9
useCapture: true useCapture: true
handler: function f() {} handler: function f() {}
======== hover ======== ======== hover ========
== normal
[expanded] div#div-in-iframeevent-listeners-about-blank.html:9 [expanded] div#div-in-iframeevent-listeners-about-blank.html:9
useCapture: true useCapture: true
handler: function f() {} handler: function f() {}
......
...@@ -117,6 +117,7 @@ ...@@ -117,6 +117,7 @@
'front_end/components/ObjectPropertiesSection.js', 'front_end/components/ObjectPropertiesSection.js',
'front_end/components/RemoteObjectPreviewFormatter.js', 'front_end/components/RemoteObjectPreviewFormatter.js',
'front_end/components/ShortcutsScreen.js', 'front_end/components/ShortcutsScreen.js',
'front_end/components/EventListenersUtils.js',
'front_end/components/EventListenersView.js', 'front_end/components/EventListenersView.js',
], ],
'devtools_host_js_files': [ 'devtools_host_js_files': [
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
/**
* @typedef {Array<{object: !WebInspector.RemoteObject, eventListeners: ?Array<!WebInspector.EventListener>, frameworkEventListeners: ?{eventListeners: ?Array<!WebInspector.EventListener>, internalHandlers: ?WebInspector.RemoteArray}, isInternal: ?Array<boolean>}>}
*/
WebInspector.EventListenersResult;
/** /**
* @constructor * @constructor
* @param {!Element} element * @param {!Element} element
...@@ -29,21 +34,96 @@ WebInspector.EventListenersView.prototype = { ...@@ -29,21 +34,96 @@ WebInspector.EventListenersView.prototype = {
*/ */
addObjects: function(objects) addObjects: function(objects)
{ {
this.reset();
var promises = []; var promises = [];
for (var i = 0; i < objects.length; ++i) for (var object of objects)
promises.push(objects[i].eventListeners()); promises.push(this._addObject(object));
return Promise.all(promises).then(listenersCallback.bind(this)); return Promise.all(promises).then(this.addEmptyHolderIfNeeded.bind(this)).then(this._eventListenersArrivedForTest.bind(this));
},
/**
* @param {!WebInspector.RemoteObject} object
* @return {!Promise<undefined>}
*/
_addObject: function(object)
{
/** @type {?Array<!WebInspector.EventListener>} */
var eventListeners = null;
/** @type {?WebInspector.FrameworkEventListenersObject}*/
var frameworkEventListenersObject = null;
var promises = [];
promises.push(object.eventListeners().then(storeEventListeners));
promises.push(WebInspector.EventListener.frameworkEventListeners(object).then(storeFrameworkEventListenersObject));
return Promise.all(promises).then(markInternalEventListeners).then(addEventListeners.bind(this));
/**
* @param {?Array<!WebInspector.EventListener>} result
*/
function storeEventListeners(result)
{
eventListeners = result;
}
/**
* @param {?WebInspector.FrameworkEventListenersObject} result
*/
function storeFrameworkEventListenersObject(result)
{
frameworkEventListenersObject = result;
}
/**
* @return {!Promise<undefined>}
*/
function markInternalEventListeners()
{
if (!eventListeners || !frameworkEventListenersObject.internalHandlers)
return Promise.resolve(undefined);
return frameworkEventListenersObject.internalHandlers.object().callFunctionJSONPromise(isInternalEventListener, eventListeners.map(handlerArgument)).then(setIsInternal);
/**
* @param {!WebInspector.EventListener} listener
* @return {!RuntimeAgent.CallArgument}
*/
function handlerArgument(listener)
{
return WebInspector.RemoteObject.toCallArgument(listener.handler());
}
/**
* @suppressReceiverCheck
* @return {!Array<boolean>}
* @this {Array<*>}
*/
function isInternalEventListener()
{
var isInternal = [];
var internalHandlersSet = new Set(this);
for (var handler of arguments)
isInternal.push(internalHandlersSet.has(handler));
return isInternal;
}
/**
* @param {!Array<boolean>} isInternal
*/
function setIsInternal(isInternal)
{
for (var i = 0; i < eventListeners.length; ++i) {
if (isInternal[i])
eventListeners[i].setListenerType("frameworkInternal");
}
}
}
/** /**
* @param {!Array<?Array<!WebInspector.EventListener>>} listeners
* @this {WebInspector.EventListenersView} * @this {WebInspector.EventListenersView}
*/ */
function listenersCallback(listeners) function addEventListeners()
{ {
this.reset(); this._addObjectEventListeners(object, eventListeners);
for (var i = 0; i < listeners.length; ++i) this._addObjectEventListeners(object, frameworkEventListenersObject.eventListeners);
this._addObjectEventListeners(objects[i], listeners[i]);
this.addEmptyHolderIfNeeded();
this._eventListenersArrivedForTest();
} }
}, },
...@@ -61,6 +141,25 @@ WebInspector.EventListenersView.prototype = { ...@@ -61,6 +141,25 @@ WebInspector.EventListenersView.prototype = {
} }
}, },
/**
* @param {boolean} showFramework
*/
showFrameworkListeners: function(showFramework)
{
var eventTypes = this._treeOutline.rootElement().children();
for (var eventType of eventTypes) {
for (var listenerElement of eventType.children()) {
var listenerType = listenerElement.eventListener().listenerType();
var hidden = false;
if (listenerType === "frameworkUser" && !showFramework)
hidden = true;
if (listenerType === "frameworkInternal" && showFramework)
hidden = true;
listenerElement.hidden = hidden;
}
}
},
/** /**
* @param {string} type * @param {string} type
* @return {!WebInspector.EventListenersTreeElement} * @return {!WebInspector.EventListenersTreeElement}
...@@ -174,5 +273,13 @@ WebInspector.ObjectEventListenerBar.prototype = { ...@@ -174,5 +273,13 @@ WebInspector.ObjectEventListenerBar.prototype = {
title.appendChild(WebInspector.ObjectPropertiesSection.createValueElement(object, false)); title.appendChild(WebInspector.ObjectPropertiesSection.createValueElement(object, false));
}, },
/**
* @return {!WebInspector.EventListener}
*/
eventListener: function()
{
return this._eventListener;
},
__proto__: TreeElement.prototype __proto__: TreeElement.prototype
} }
\ No newline at end of file
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
"ObjectPropertiesSection.js", "ObjectPropertiesSection.js",
"RemoteObjectPreviewFormatter.js", "RemoteObjectPreviewFormatter.js",
"ShortcutsScreen.js", "ShortcutsScreen.js",
"EventListenersUtils.js",
"EventListenersView.js" "EventListenersView.js"
], ],
"resources": [ "resources": [
......
...@@ -38,6 +38,8 @@ WebInspector.EventListenersWidget = function() ...@@ -38,6 +38,8 @@ WebInspector.EventListenersWidget = function()
this._showForAncestorsSetting = WebInspector.settings.createSetting("showEventListenersForAncestors", true); this._showForAncestorsSetting = WebInspector.settings.createSetting("showEventListenersForAncestors", true);
this._showForAncestorsSetting.addChangeListener(this.update.bind(this)); this._showForAncestorsSetting.addChangeListener(this.update.bind(this));
this._showFrameworkListenersSetting = WebInspector.settings.createSetting("showFrameowkrListeners", true);
this._showFrameworkListenersSetting.addChangeListener(this._showFrameworkListenersChanged.bind(this));
this._eventListenersView = new WebInspector.EventListenersView(this.element); this._eventListenersView = new WebInspector.EventListenersView(this.element);
WebInspector.context.addFlavorChangeListener(WebInspector.DOMNode, this.update, this); WebInspector.context.addFlavorChangeListener(WebInspector.DOMNode, this.update, this);
} }
...@@ -53,6 +55,7 @@ WebInspector.EventListenersWidget.createSidebarWrapper = function() ...@@ -53,6 +55,7 @@ WebInspector.EventListenersWidget.createSidebarWrapper = function()
refreshButton.addEventListener("click", widget.update.bind(widget)); refreshButton.addEventListener("click", widget.update.bind(widget));
result.toolbar().appendToolbarItem(refreshButton); result.toolbar().appendToolbarItem(refreshButton);
result.toolbar().appendToolbarItem(new WebInspector.ToolbarCheckbox(WebInspector.UIString("Ancestors"), WebInspector.UIString("Show listeners on the ancestors"), widget._showForAncestorsSetting)); result.toolbar().appendToolbarItem(new WebInspector.ToolbarCheckbox(WebInspector.UIString("Ancestors"), WebInspector.UIString("Show listeners on the ancestors"), widget._showForAncestorsSetting));
result.toolbar().appendToolbarItem(new WebInspector.ToolbarCheckbox(WebInspector.UIString("Framework listeners"), WebInspector.UIString("Resolve event listeners bound with framework"), widget._showFrameworkListenersSetting));
return result; return result;
} }
...@@ -89,7 +92,13 @@ WebInspector.EventListenersWidget.prototype = { ...@@ -89,7 +92,13 @@ WebInspector.EventListenersWidget.prototype = {
} }
promises.push(this._windowObjectInNodeContext(node)); promises.push(this._windowObjectInNodeContext(node));
} }
return Promise.all(promises).then(this._eventListenersView.addObjects.bind(this._eventListenersView)); return Promise.all(promises).then(this._eventListenersView.addObjects.bind(this._eventListenersView)).then(this._showFrameworkListenersChanged.bind(this));
},
_showFrameworkListenersChanged: function()
{
this._eventListenersView.showFrameworkListeners(this._showFrameworkListenersSetting.get());
}, },
/** /**
......
...@@ -1499,16 +1499,3 @@ Promise.prototype.catchException = function(defaultValue) { ...@@ -1499,16 +1499,3 @@ Promise.prototype.catchException = function(defaultValue) {
return defaultValue; return defaultValue;
}); });
} }
/**
* @param {!Object} object
* @param {*} propertyName
* @param {T} result
* @return {T}
* @template T
*/
function storeResultTo(object, propertyName, result)
{
object[propertyName] = result;
return result;
}
...@@ -91,7 +91,7 @@ WebInspector.RemoteObject.prototype = { ...@@ -91,7 +91,7 @@ WebInspector.RemoteObject.prototype = {
}, },
/** /**
* @return {!Promise.<!{properties: ?Array.<!WebInspector.RemoteObjectProperty>, internalProperties: ?Array.<!WebInspector.RemoteObjectProperty>}>} * @return {!Promise<!{properties: ?Array.<!WebInspector.RemoteObjectProperty>, internalProperties: ?Array.<!WebInspector.RemoteObjectProperty>}>}
*/ */
getOwnPropertiesPromise: function() getOwnPropertiesPromise: function()
{ {
...@@ -230,9 +230,10 @@ WebInspector.RemoteObject.prototype = { ...@@ -230,9 +230,10 @@ WebInspector.RemoteObject.prototype = {
}, },
/** /**
* @param {function(this:Object, ...)} functionDeclaration * @param {function(this:Object, ...):T} functionDeclaration
* @param {!Array<!RuntimeAgent.CallArgument>|undefined} args * @param {!Array<!RuntimeAgent.CallArgument>|undefined} args
* @return {!Promise<*>} * @return {!Promise<T>}
* @template T
*/ */
callFunctionJSONPromise: function(functionDeclaration, args) callFunctionJSONPromise: function(functionDeclaration, args)
{ {
......
...@@ -495,8 +495,9 @@ WebInspector.ExecutionContext.prototype = { ...@@ -495,8 +495,9 @@ WebInspector.ExecutionContext.prototype = {
* @param {boolean} useCapture * @param {boolean} useCapture
* @param {?WebInspector.RemoteObject} handler * @param {?WebInspector.RemoteObject} handler
* @param {!WebInspector.DebuggerModel.Location} location * @param {!WebInspector.DebuggerModel.Location} location
* @param {string=} listenerType
*/ */
WebInspector.EventListener = function(target, type, useCapture, handler, location) WebInspector.EventListener = function(target, type, useCapture, handler, location, listenerType)
{ {
WebInspector.SDKObject.call(this, target); WebInspector.SDKObject.call(this, target);
this._type = type; this._type = type;
...@@ -504,6 +505,7 @@ WebInspector.EventListener = function(target, type, useCapture, handler, locatio ...@@ -504,6 +505,7 @@ WebInspector.EventListener = function(target, type, useCapture, handler, locatio
this._handler = handler; this._handler = handler;
this._location = location; this._location = location;
this._sourceURL = location.script().contentURL(); this._sourceURL = location.script().contentURL();
this._listenerType = listenerType || "normal";
} }
WebInspector.EventListener.prototype = { WebInspector.EventListener.prototype = {
...@@ -547,5 +549,21 @@ WebInspector.EventListener.prototype = { ...@@ -547,5 +549,21 @@ WebInspector.EventListener.prototype = {
return this._sourceURL; return this._sourceURL;
}, },
/**
* @return {string}
*/
listenerType: function()
{
return this._listenerType;
},
/**
* @param {string} listenerType
*/
setListenerType: function(listenerType)
{
this._listenerType = listenerType;
},
__proto__: WebInspector.SDKObject.prototype __proto__: WebInspector.SDKObject.prototype
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment