Bug 736078 - Show which elements have listeners attached r=jwalker,bgrins
☠☠ backed out by 04fc2abf8ac6 ☠ ☠
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Fri, 18 Jul 2014 14:25:03 +0100
changeset 216995 89669f18bd2d3ce56382da9288c5d0eb44e9b4a5
parent 216994 489816b74790caab8353a6199553512632eff0fe
child 216996 cfe6339e0fcbbea0f3c8f44cd2b67a58a9f6ff52
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker, bgrins
bugs736078
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 736078 - Show which elements have listeners attached r=jwalker,bgrins
browser/devtools/markupview/markup-view.css
browser/devtools/markupview/markup-view.js
browser/devtools/markupview/markup-view.xhtml
browser/devtools/markupview/test/browser.ini
browser/devtools/markupview/test/browser_markupview_events.js
browser/devtools/markupview/test/doc_markup_events.html
browser/devtools/shared/widgets/Tooltip.js
browser/locales/en-US/chrome/browser/devtools/inspector.properties
browser/themes/shared/devtools/common.css
browser/themes/shared/devtools/inspector.css
browser/themes/shared/devtools/markup-view.css
browser/themes/shared/devtools/toolbars.inc.css
toolkit/devtools/server/actors/inspector.js
--- a/browser/devtools/markupview/markup-view.css
+++ b/browser/devtools/markupview/markup-view.css
@@ -126,8 +126,13 @@
 
 .tag-state.flash-out {
   transition: background .5s;
 }
 
 .tag-line .open, .tag-line .close, .comment {
   cursor: default;
 }
+
+.markupview-events {
+  display: none;
+  cursor: pointer;
+}
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -83,59 +83,73 @@ function MarkupView(aInspector, aFrame, 
   this._containers = new Map();
 
   this._boundMutationObserver = this._mutationObserver.bind(this);
   this.walker.on("mutations", this._boundMutationObserver);
 
   this._boundOnDisplayChange = this._onDisplayChange.bind(this);
   this.walker.on("display-change", this._boundOnDisplayChange);
 
+  this._onMouseClick = this._onMouseClick.bind(this);
+
   this._boundOnNewSelection = this._onNewSelection.bind(this);
   this._inspector.selection.on("new-node-front", this._boundOnNewSelection);
   this._onNewSelection();
 
   this._boundKeyDown = this._onKeyDown.bind(this);
   this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
 
   this._boundFocus = this._onFocus.bind(this);
   this._frame.addEventListener("focus", this._boundFocus, false);
 
+  this._makeTooltipPersistent = this._makeTooltipPersistent.bind(this);
+
   this._initPreview();
   this._initTooltips();
   this._initHighlighter();
 
   EventEmitter.decorate(this);
 }
 
 exports.MarkupView = MarkupView;
 
 MarkupView.prototype = {
   _selectedContainer: null,
 
   _initTooltips: function() {
     this.tooltip = new Tooltip(this._inspector.panelDoc);
-    this.tooltip.startTogglingOnHover(this._elt,
-      this._isImagePreviewTarget.bind(this));
+    this._makeTooltipPersistent(false);
+
+    this._elt.addEventListener("click", this._onMouseClick, false);
   },
 
   _initHighlighter: function() {
     // Show the box model on markup-view mousemove
     this._onMouseMove = this._onMouseMove.bind(this);
     this._elt.addEventListener("mousemove", this._onMouseMove, false);
     this._onMouseLeave = this._onMouseLeave.bind(this);
     this._elt.addEventListener("mouseleave", this._onMouseLeave, false);
 
     // Show markup-containers as hovered on toolbox "picker-node-hovered" event
     // which happens when the "pick" button is pressed
     this._onToolboxPickerHover = (event, nodeFront) => {
       this.showNode(nodeFront, true).then(() => {
         this._showContainerAsHovered(nodeFront);
       });
+    };
+    this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
+  },
+
+  _makeTooltipPersistent: function(state) {
+    if (state) {
+      this.tooltip.stopTogglingOnHover();
+    } else {
+      this.tooltip.startTogglingOnHover(this._elt,
+        this._isImagePreviewTarget.bind(this));
     }
-    this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
   },
 
   _onMouseMove: function(event) {
     let target = event.target;
 
     // Search target for a markupContainer reference, if not found, walk up
     while (!target.container) {
       if (target.tagName.toLowerCase() === "body") {
@@ -150,16 +164,36 @@ MarkupView.prototype = {
         this._showBoxModel(container.node);
       } else {
         this._hideBoxModel();
       }
     }
     this._showContainerAsHovered(container.node);
   },
 
+  _onMouseClick: function(event) {
+    // From the target passed here, let's find the parent MarkupContainer
+    // and ask it if the tooltip should be shown
+    let parentNode = event.target;
+    let container;
+    while (parentNode !== this.doc.body) {
+      if (parentNode.container) {
+        container = parentNode.container;
+        break;
+      }
+      parentNode = parentNode.parentNode;
+    }
+
+    if (container) {
+      // With the newly found container, delegate the tooltip content creation
+      // and decision to show or not the tooltip
+      container._buildEventTooltipContent(event.target, this.tooltip);
+    }
+  },
+
   _hoveredNode: null,
 
   /**
    * Show a NodeFront's container as being hovered
    * @param {NodeFront} nodeFront The node to show as hovered
    */
   _showContainerAsHovered: function(nodeFront) {
     if (this._hoveredNode === nodeFront) {
@@ -1111,16 +1145,18 @@ MarkupView.prototype = {
     if (this._destroyer) {
       return this._destroyer;
     }
 
     // Note that if the toolbox is closed, this will work fine, but will fail
     // in case the browser is closed and will trigger a noSuchActor message.
     this._destroyer = this._hideBoxModel();
 
+    this._elt.removeEventListener("click", this._onMouseClick, false);
+
     this._hoveredNode = null;
     this._inspector.toolbox.off("picker-node-hovered", this._onToolboxPickerHover);
 
     this.htmlEditor.destroy();
     this.htmlEditor = null;
 
     this.undo.destroy();
     this.undo = null;
@@ -1410,16 +1446,37 @@ MarkupContainer.prototype = {
     // the tooltip, because we want the full-size image
     this.node.getImageData().then(data => {
       data.data.string().then(str => {
         clipboardHelper.copyString(str, this.markup.doc);
       });
     });
   },
 
+  _buildEventTooltipContent: function(target, tooltip) {
+    if (target.hasAttribute("data-event")) {
+      tooltip.hide(target);
+
+      this.node.getEventListenerInfo().then(listenerInfo => {
+        tooltip.setEventContent({
+          eventListenerInfos: listenerInfo,
+          toolbox: this._inspector.toolbox
+        });
+
+        this.markup._makeTooltipPersistent(true);
+        tooltip.once("hidden", () => {
+          this.markup._makeTooltipPersistent(false);
+        });
+
+        tooltip.show(target);
+      });
+      return true;
+    }
+  },
+
   /**
    * True if the current node has children.  The MarkupView
    * will set this attribute for the MarkupContainer.
    */
   _hasChildren: false,
 
   get hasChildren() {
     return this._hasChildren;
@@ -1855,16 +1912,17 @@ function ElementEditor(aContainer, aNode
         console.error(x);
       }
     }
   });
 
   let tagName = this.node.nodeName.toLowerCase();
   this.tag.textContent = tagName;
   this.closeTag.textContent = tagName;
+  this.eventNode.style.display = this.node.hasEventListeners ? "inline-block" : "none";
 
   this.update();
 }
 
 ElementEditor.prototype = {
   /**
    * Update the state of the editor from the node.
    */
--- a/browser/devtools/markupview/markup-view.xhtml
+++ b/browser/devtools/markupview/markup-view.xhtml
@@ -11,41 +11,80 @@
   <link rel="stylesheet" href="chrome://browser/skin/devtools/markup-view.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
 
   <script type="application/javascript;version=1.8"
           src="chrome://browser/content/devtools/theme-switching.js"/>
 
 </head>
 <body class="theme-body devtools-monospace" role="application">
+
+<!-- NOTE THAT WE MAKE EXTENSIVE USE OF HTML COMMENTS IN THIS FILE IN ORDER -->
+<!-- TO MAKE SPANS READABLE WHILST AVOIDING SIGNIFICANT WHITESPACE          -->
+
   <div id="root-wrapper">
     <div id="root"></div>
   </div>
   <div id="templates" style="display:none">
 
     <ul class="children">
       <li id="template-container" save="${elt}" class="child collapsed">
-        <div save="${tagLine}" class="tag-line"><span save="${tagState}" class="tag-state"></span><span save="${expander}" class="theme-twisty expander"></span></div>
+        <div save="${tagLine}" class="tag-line"><!--
+        --><span save="${tagState}" class="tag-state"></span><!--
+        --><span save="${expander}" class="theme-twisty expander"></span><!--
+     --></div>
         <ul save="${children}" class="children"></ul>
       </li>
 
-      <li id="template-more-nodes" class="more-nodes devtools-class-comment" save="${elt}"><span>${showing}</span> <button href="#" onclick="${allButtonClick}">${showAll}</button></li>
+      <li id="template-more-nodes"
+          class="more-nodes devtools-class-comment"
+          save="${elt}"><!--
+      --><span>${showing}</span> <!--
+      --><button href="#" onclick="${allButtonClick}">${showAll}</button>
+      </li>
     </ul>
 
-    <span id="template-element" save="${elt}" class="editor"><span class="open">&lt;<span save="${tag}" class="tag theme-fg-color3" tabindex="0"></span><span save="${attrList}"></span><span save="${newAttr}" class="newattr" tabindex="0"></span><span class="closing-bracket">&gt;</span></span><span class="close">&lt;/<span save="${closeTag}" class="tag theme-fg-color3"></span>&gt;</span></span>
+    <span id="template-element" save="${elt}" class="editor"><!--
+   --><span class="open">&lt;<!--
+     --><span save="${tag}" class="tag theme-fg-color3" tabindex="0"></span><!--
+     --><span save="${attrList}"></span><!--
+     --><span save="${newAttr}" class="newattr" tabindex="0"></span><!--
+     --><span class="closing-bracket">&gt;</span><!--
+   --></span><!--
+   --><span class="close">&lt;/<!--
+     --><span save="${closeTag}" class="tag theme-fg-color3"></span><!--
+     -->&gt;<!--
+   --></span><!--
+     --><div save="${eventNode}" class="markupview-events" data-event="true">ev</div><!--
+ --></span>
 
-    <span id="template-attribute" save="${attr}" data-attr="${attrName}" class="attreditor" style="display:none"> <span class="editable" save="${inner}" tabindex="0"><span save="${name}" class="attr-name theme-fg-color2"></span>=&quot;<span save="${val}" class="attr-value theme-fg-color6"></span>&quot;</span></span>
+    <span id="template-attribute"
+          save="${attr}"
+          data-attr="${attrName}"
+          class="attreditor"
+          style="display:none"> <!--
+   --><span class="editable" save="${inner}" tabindex="0"><!--
+     --><span save="${name}" class="attr-name theme-fg-color2"></span><!--
+     -->=&quot;<!--
+     --><span save="${val}" class="attr-value theme-fg-color6"></span><!--
+     -->&quot;<!--
+   --></span><!--
+ --></span>
 
     <span id="template-text" save="${elt}" class="editor text">
       <pre save="${value}" style="display:inline-block;" tabindex="0"></pre>
     </span>
 
-    <span id="template-comment" save="${elt}" class="editor comment theme-comment"><span>&lt;!--</span><pre save="${value}" style="display:inline-block;" tabindex="0"></pre><span>--&gt;</span></span>
-
-    <!-- span id="template-elementClose" save="${closeElt}">&lt;/<span save="${closeTag}" class="tagname theme-fg-color3"></span>&gt;</span -->
+    <span id="template-comment"
+          save="${elt}"
+          class="editor comment theme-comment"><!--
+   --><span>&lt;!--</span><!--
+   --><pre save="${value}" style="display:inline-block;" tabindex="0"></pre><!--
+   --><span>--&gt;</span><!--
+ --></span>
 
   </div>
   <div id="previewbar" class="disabled">
      <div id="preview"/>
      <div id="viewbox"/>
   </div>
 </body>
 </html>
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -1,27 +1,30 @@
 [DEFAULT]
 subsuite = devtools
 support-files =
   doc_markup_edit.html
+  doc_markup_events.html
   doc_markup_flashing.html
   doc_markup_mutation.html
   doc_markup_navigation.html
   doc_markup_not_displayed.html
   doc_markup_pagesize_01.html
   doc_markup_pagesize_02.html
   doc_markup_search.html
   doc_markup_toggle.html
   doc_markup_tooltip.png
   head.js
   helper_attributes_test_runner.js
   helper_outerhtml_test_runner.js
 
 [browser_markupview_copy_image_data.js]
 [browser_markupview_css_completion_style_attribute.js]
+[browser_markupview_events.js]
+skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_highlight_hover_01.js]
 skip-if = e10s # Bug 985597 - The XUL-based highlighter isn't e10s compatible
 [browser_markupview_highlight_hover_02.js]
 skip-if = e10s # Bug 985597 - The XUL-based highlighter isn't e10s compatible
 [browser_markupview_highlight_hover_03.js]
 skip-if = e10s # Bug 985597 - The XUL-based highlighter isn't e10s compatible
 [browser_markupview_html_edit_01.js]
 [browser_markupview_html_edit_02.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_events.js
@@ -0,0 +1,184 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "http://example.com/browser/browser/devtools/" +
+                 "markupview/test/doc_markup_events.html";
+
+let test = asyncTest(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  yield inspector.markup.expandAll();
+
+  yield checkEventsForNode("html", [
+    {
+      type: "load",
+      filename: TEST_URL,
+      bubbling: true,
+      dom0: true,
+      handler: "init();"
+    }
+  ]);
+
+  yield checkEventsForNode("#container", [
+    {
+      type: "mouseover",
+      filename: TEST_URL + ":62",
+      bubbling: false,
+      dom0: false,
+      handler: 'function mouseoverHandler(event) {\n' +
+               '  if (event.target.id !== "container") {\n' +
+               '    let output = document.getElementById("output");\n' +
+               '    output.textContent = event.target.textContent;\n' +
+               '  }\n' +
+               '}'
+    }
+  ]);
+
+  yield checkEventsForNode("#multiple", [
+    {
+      type: "click",
+      filename: TEST_URL + ":69",
+      bubbling: true,
+      dom0: false,
+      handler: 'function clickHandler(event) {\n' +
+               '  let output = document.getElementById("output");\n' +
+               '  output.textContent = "click";\n' +
+               '}'
+    },
+    {
+      type: "mouseup",
+      filename: TEST_URL + ":78",
+      bubbling: true,
+      dom0: false,
+      handler: 'function mouseupHandler(event) {\n' +
+               '  let output = document.getElementById("output");\n' +
+               '  output.textContent = "mouseup";\n' +
+               '}'
+    }
+  ]);
+
+  yield checkEventsForNode("#DOM0", [
+    {
+      type: "click",
+      filename: TEST_URL,
+      bubbling: true,
+      dom0: true,
+      handler: "alert('hi')"
+    }
+  ]);
+
+  yield checkEventsForNode("#handleevent", [
+    {
+      type: "click",
+      filename: TEST_URL + ":89",
+      bubbling: true,
+      dom0: false,
+      handler: 'handleEvent: function(blah) {\n' +
+               '  alert("handleEvent clicked");\n' +
+               '}'
+    }
+  ]);
+
+  yield checkEventsForNode("#fatarrow", [
+    {
+      type: "click",
+      filename: TEST_URL + ":57",
+      bubbling: true,
+      dom0: false,
+      handler: 'event => {\n' +
+               '  alert("Yay for the fat arrow!");\n' +
+               '}'
+    }
+  ]);
+
+  yield checkEventsForNode("#boundhe", [
+    {
+      type: "click",
+      filename: TEST_URL + ":101",
+      bubbling: true,
+      dom0: false,
+      handler: 'handleEvent: function() {\n' +
+               '  alert("boundHandleEvent clicked");\n' +
+               '}'
+    }
+  ]);
+
+  yield checkEventsForNode("#bound", [
+    {
+      type: "click",
+      filename: TEST_URL + ":74",
+      bubbling: true,
+      dom0: false,
+      handler: 'function boundClickHandler(event) {\n' +
+               '  alert("Bound event clicked");\n' +
+               '}'
+    }
+  ]);
+
+  gBrowser.removeCurrentTab();
+
+  // Wait for promises to avoid leaks when running this as a single test.
+  yield promiseNextTick();
+
+  function* checkEventsForNode(selector, expected) {
+    let container = yield getContainerForSelector(selector, inspector);
+    let evHolder = container.elt.querySelector(".markupview-events");
+    let tooltip = inspector.markup.tooltip;
+
+    evHolder.scrollIntoView();
+
+    // Wait for scrollIntoView to complete.
+    yield promiseNextTick();
+
+    // Click button to show tooltip
+    EventUtils.synthesizeMouseAtCenter(evHolder, {}, inspector.markup.doc.defaultView);
+    yield tooltip.once("shown");
+
+    // Check values
+    let content = tooltip.content;
+    let result = content.querySelectorAll("label,.event-tooltip-content-box");
+    let nodeFront = container.node;
+    let selector = nodeFront.nodeName + "#" + nodeFront.id;
+
+    let out = [];
+
+    for (let i = 0; i < result.length;) {
+      let type = result[i++];
+      let filename = result[i++];
+      let bubbling = result[i++];
+      let dom0 = result[i++];
+      let content = result[i++];
+
+      EventUtils.synthesizeMouseAtCenter(type, {}, type.ownerGlobal);
+
+      yield tooltip.once("event-tooltip-ready");
+
+      let editor = tooltip.eventEditors.get(content).editor;
+
+      out.push({
+        type: type.getAttribute("value"),
+        filename: filename.getAttribute("value"),
+        bubbling: bubbling.getAttribute("value") === "Bubbling",
+        dom0: dom0.getAttribute("value") === "DOM0",
+        handler: editor.getText()
+      });
+    }
+
+    for (let i = 0; i < out.length; i++) {
+      is(out[i].type, expected[i].type, "type matches for " + selector);
+      is(out[i].filename, expected[i].filename, "filename matches for " + selector);
+      is(out[i].bubbling, expected[i].bubbling, "bubbling matches for " + selector);
+      is(out[i].dom0, expected[i].dom0, "dom0 matches for " + selector);
+      is(out[i].handler, expected[i].handler, "handlers matches for " + selector);
+    }
+  }
+
+  function promiseNextTick() {
+    let deferred = promise.defer();
+    executeSoon(deferred.resolve);
+    return deferred.promise;
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/doc_markup_events.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <style>
+    #container {
+      border: 1px solid #000;
+      width: 200px;
+      height: 85px;
+    }
+
+    #container > div {
+      border: 1px solid #000;
+      display: inline-block;
+      margin: 2px;
+    }
+
+    #output,
+    #noevents,
+    #DOM0,
+    #handleevent,
+    #fatarrow,
+    #bound,
+    #boundhe {
+      border: 1px solid #000;
+      width: 200px;
+      min-height: 1em;
+      cursor: pointer;
+    }
+
+    #output,
+    #noevents {
+      cursor: auto;
+    }
+
+    #output {
+      min-height: 1.5em;
+    }
+    </style>
+    <script type="application/javascript;version=1.8">
+      function init() {
+        let container = document.getElementById("container");
+        let multiple = document.getElementById("multiple");
+        let fatarrow = document.getElementById("fatarrow");
+
+        container.addEventListener("mouseover", mouseoverHandler, true);
+        multiple.addEventListener("click", clickHandler, false);
+        multiple.addEventListener("mouseup", mouseupHandler, false);
+
+        new handleEventClick();
+        new boundHandleEventClick();
+
+        let bound = document.getElementById("bound");
+        boundClickHandler = boundClickHandler.bind(this);
+        bound.addEventListener("click", boundClickHandler);
+
+        fatarrow.addEventListener("click", event => {
+          alert("Yay for the fat arrow!");
+        });
+      }
+
+      function mouseoverHandler(event) {
+        if (event.target.id !== "container") {
+          let output = document.getElementById("output");
+          output.textContent = event.target.textContent;
+        }
+      }
+
+      function clickHandler(event) {
+        let output = document.getElementById("output");
+        output.textContent = "click";
+      }
+
+      function boundClickHandler(event) {
+        alert("Bound event clicked");
+      }
+
+      function mouseupHandler(event) {
+        let output = document.getElementById("output");
+        output.textContent = "mouseup";
+      }
+
+      function handleEventClick(hehe) {
+        let handleevent = document.getElementById("handleevent");
+        handleevent.addEventListener("click", this);
+      }
+
+      handleEventClick.prototype = {
+        handleEvent: function(blah) {
+          alert("handleEvent clicked");
+        }
+      };
+
+      function boundHandleEventClick() {
+        let boundhe = document.getElementById("boundhe");
+        this.handleEvent = this.handleEvent.bind(this);
+        boundhe.addEventListener("click", this);
+      }
+
+      boundHandleEventClick.prototype = {
+        handleEvent: function() {
+          alert("boundHandleEvent clicked");
+        }
+      };
+    </script>
+  </head>
+  <body onload="init();">
+    <div id="container">
+      <div>1</div>
+      <div>2</div>
+      <div>3</div>
+      <div>4</div>
+      <div>5</div>
+      <div>6</div>
+      <div>7</div>
+      <div>8</div>
+      <div>9</div>
+      <div>10</div>
+      <div>11</div>
+      <div>12</div>
+      <div>13</div>
+      <div>14</div>
+      <div>15</div>
+      <div>16</div>
+      <div id="multiple">multiple</div>
+    </div>
+    <div id="output"></div>
+    <div id="noevents">No events here</div>
+    <div id="DOM0" onclick="alert('hi')">DOM0 event here</div>
+    <div id="handleevent">handleEvent event here</div>
+    <div id="fatarrow">Fat arrow event</div>
+    <div id="boundhe">Bound handleEvent</div>
+    <div id="bound">Bound event</div>
+  </body>
+</html>
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -9,16 +9,20 @@ const {Promise: promise} = Cu.import("re
 const IOService = Cc["@mozilla.org/network/io-service;1"]
   .getService(Ci.nsIIOService);
 const {Spectrum} = require("devtools/shared/widgets/Spectrum");
 const {CubicBezierWidget} = require("devtools/shared/widgets/CubicBezierWidget");
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const {colorUtils} = require("devtools/css-color");
 const Heritage = require("sdk/core/heritage");
 const {Eyedropper} = require("devtools/eyedropper/eyedropper");
+const Editor = require("devtools/sourceeditor/editor");
+const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+devtools.lazyRequireGetter(this, "beautify", "devtools/jsbeautify");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "setNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "clearNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
@@ -361,16 +365,20 @@ Tooltip.prototype = {
    * @param {Number} showDelay
    *        An optional delay that will be observed before showing the tooltip.
    *        Defaults to this.defaultShowDelay.
    */
   startTogglingOnHover: function(baseNode, targetNodeCb, showDelay=this.defaultShowDelay) {
     if (this._basedNode) {
       this.stopTogglingOnHover();
     }
+    if (!baseNode) {
+      // Calling tool is in the process of being destroyed.
+      return;
+    }
 
     this._basedNode = baseNode;
     this._showDelay = showDelay;
     this._targetNodeCb = targetNodeCb || (() => true);
 
     this._onBaseNodeMouseMove = this._onBaseNodeMouseMove.bind(this);
     this._onBaseNodeMouseLeave = this._onBaseNodeMouseLeave.bind(this);
 
@@ -381,16 +389,20 @@ Tooltip.prototype = {
   /**
    * If the startTogglingOnHover function has been used previously, and you want
    * to get rid of this behavior, then call this function to remove the mouse
    * movement tracking
    */
   stopTogglingOnHover: function() {
     clearNamedTimeout(this.uid);
 
+    if (!this._basedNode) {
+      return;
+    }
+
     this._basedNode.removeEventListener("mousemove",
       this._onBaseNodeMouseMove, false);
     this._basedNode.removeEventListener("mouseleave",
       this._onBaseNodeMouseLeave, false);
 
     this._basedNode = null;
     this._targetNodeCb = null;
     this._lastHovered = null;
@@ -516,16 +528,29 @@ Tooltip.prototype = {
       hbox.appendChild(vbox);
       this.content = hbox;
     } else {
       this.content = vbox;
     }
   },
 
   /**
+   * Sets some event listener info as the content of this tooltip.
+   *
+   * @param {Object} (destructuring assignment)
+   *          @0 {array} eventListenerInfos
+   *             A list of event listeners.
+   *          @1 {toolbox} toolbox
+   *             Toolbox used to select debugger panel.
+   */
+  setEventContent: function({ eventListenerInfos, toolbox }) {
+    new EventTooltip(this, eventListenerInfos, toolbox);
+  },
+
+  /**
    * Fill the tooltip with a variables view, inspecting an object via its
    * corresponding object actor, as specified in the remote debugging protocol.
    *
    * @param {object} objectActor
    *        The value grip for the object actor.
    * @param {object} viewOptions [optional]
    *        Options for the variables view visualization.
    * @param {object} controllerOptions [optional]
@@ -674,17 +699,17 @@ Tooltip.prototype = {
         // If no dimensions were provided, load the image to get them
         label.textContent = l10n.strings.GetStringFromName("previewTooltip.image.brokenImage");
         let imgObj = new this.doc.defaultView.Image();
         imgObj.src = imageUrl;
         imgObj.onload = () => {
           imgObj.onload = null;
             label.textContent = this._getImageDimensionLabel(imgObj.naturalWidth,
               imgObj.naturalHeight);
-        }
+        };
       }
 
       vbox.appendChild(label);
     }
 
     this.content = vbox;
   },
 
@@ -1037,17 +1062,17 @@ SwatchColorPickerTooltip.prototype = Her
         toolboxWindow.focus();
       }
       this._selectColor(color);
     });
 
     dropper.once("destroy", () => {
       this.eyedropperOpen = false;
       this.activeSwatch = null;
-    })
+    });
 
     dropper.open();
     this.eyedropperOpen = true;
 
     // close the colorpicker tooltip so that only the eyedropper is open.
     this.hide();
 
     this.tooltip.emit("eyedropper-opened", dropper);
@@ -1064,16 +1089,257 @@ SwatchColorPickerTooltip.prototype = Her
     this.currentSwatchColor = null;
     this.spectrum.then(spectrum => {
       spectrum.off("changed", this._onSpectrumColorChange);
       spectrum.destroy();
     });
   }
 });
 
+function EventTooltip(tooltip, eventListenerInfos, toolbox) {
+  this._tooltip = tooltip;
+  this._eventListenerInfos = eventListenerInfos;
+  this._toolbox = toolbox;
+  this._tooltip.eventEditors = new WeakMap();
+
+  this._headerClicked = this._headerClicked.bind(this);
+  this._debugClicked = this._debugClicked.bind(this);
+  this.destroy = this.destroy.bind(this);
+
+  this._init();
+}
+
+EventTooltip.prototype = {
+  _init: function() {
+    let config = {
+      mode: Editor.modes.js,
+      lineNumbers: false,
+      lineWrapping: false,
+      readOnly: true,
+      styleActiveLine: true,
+      extraKeys: {},
+      theme: "mozilla markup-view"
+    };
+
+    let doc = this._tooltip.doc;
+    let container = doc.createElement("vbox");
+    container.setAttribute("id", "devtools-tooltip-events-container");
+
+    for (let listener of this._eventListenerInfos) {
+      let phase = listener.capturing ? "Capturing" : "Bubbling";
+      let level = listener.DOM0 ? "DOM0" : "DOM2";
+
+      // Header
+      let header = doc.createElement("hbox");
+      header.className = "event-header devtools-toolbar";
+      container.appendChild(header);
+
+      let debuggerIcon = doc.createElement("image");
+      debuggerIcon.className = "event-tooltip-debugger-icon";
+      debuggerIcon.setAttribute("src", "chrome://browser/skin/devtools/tool-debugger.svg");
+      let openInDebugger = l10n.strings.GetStringFromName("eventsTooltip.openInDebugger");
+      debuggerIcon.setAttribute("tooltiptext", openInDebugger);
+      header.appendChild(debuggerIcon);
+
+      let eventTypeLabel = doc.createElement("label");
+      eventTypeLabel.className = "event-tooltip-event-type";
+      eventTypeLabel.setAttribute("value", listener.type);
+      eventTypeLabel.setAttribute("tooltiptext", listener.type);
+      header.appendChild(eventTypeLabel);
+
+      let filename = doc.createElement("label");
+      filename.className = "event-tooltip-filename devtools-monospace";
+      filename.setAttribute("value", listener.origin);
+      filename.setAttribute("tooltiptext", listener.origin);
+      filename.setAttribute("crop", "left");
+      header.appendChild(filename);
+
+      let attributesBox = doc.createElement("box");
+      attributesBox.setAttribute("class", "event-tooltip-attributes-container");
+      header.appendChild(attributesBox);
+
+      let capturing = doc.createElement("label");
+      capturing.className = "event-tooltip-attributes";
+      capturing.setAttribute("value", phase);
+      capturing.setAttribute("tooltiptext", phase);
+      attributesBox.appendChild(capturing);
+
+      let attributesBox2 = attributesBox.cloneNode(false);
+      header.appendChild(attributesBox2);
+
+      let dom0 = doc.createElement("label");
+      dom0.className = "event-tooltip-attributes";
+      dom0.setAttribute("value", level);
+      dom0.setAttribute("tooltiptext", level);
+      attributesBox2.appendChild(dom0);
+
+      // Content
+      let content = doc.createElement("box");
+      let editor = new Editor(config);
+      this._tooltip.eventEditors.set(content, {
+        editor: editor,
+        handler: listener.handler,
+        searchString: listener.searchString,
+        uri: listener.origin,
+        dom0: listener.DOM0,
+        appended: false
+      });
+
+      content.className = "event-tooltip-content-box";
+      container.appendChild(content);
+
+      this._addContentListeners(header);
+    }
+
+    this._tooltip.content = container;
+    this._tooltip.panel.setAttribute("clamped-dimensions-no-min-height", "");
+
+    this._tooltip.panel.addEventListener("popuphiding", () => {
+      this.destroy(container);
+    }, false);
+  },
+
+  _addContentListeners: function(header) {
+    header.addEventListener("click", this._headerClicked);
+  },
+
+  _headerClicked: function(event) {
+    if (event.target.classList.contains("event-tooltip-debugger-icon")) {
+      this._debugClicked(event);
+    }
+
+    let doc = this._tooltip.doc;
+    let header = event.currentTarget;
+    let content = header.nextElementSibling;
+
+    if (content.hasAttribute("open")) {
+      content.removeAttribute("open");
+    } else {
+      let contentNodes = doc.querySelectorAll(".event-tooltip-content-box");
+
+      for (let node of contentNodes) {
+        if (node !== content) {
+          node.removeAttribute("open");
+        }
+      }
+
+      content.setAttribute("open", "");
+
+      let eventEditors = this._tooltip.eventEditors.get(content);
+
+      if (eventEditors.appended) {
+        return;
+      }
+
+      let {editor, handler} = eventEditors;
+
+      let iframe = doc.createElement("iframe");
+      iframe.setAttribute("style", "width:100%;");
+
+      editor.appendTo(content, iframe).then(() => {
+        let tidied = beautify.js(handler, { indent_size: 2 });
+
+        editor.setText(tidied);
+
+        eventEditors.appended = true;
+        this._tooltip.emit("event-tooltip-ready");
+      });
+    }
+  },
+
+  _debugClicked: function(event) {
+    let header = event.currentTarget;
+    let content = header.nextElementSibling;
+
+    let {uri, searchString, dom0} =
+      this._tooltip.eventEditors.get(content);
+
+    if (uri && uri !== "?") {
+      // Save a copy of toolbox as it will be set to null when we hide the
+      // tooltip.
+      let toolbox = this._toolbox;
+
+      this._tooltip.hide();
+
+      uri = uri.replace(/"/g, "");
+
+      let showSource = ({ DebuggerView }) => {
+        let matches = uri.match(/(.*):(\d+$)/);
+        let line = 1;
+
+        if (matches) {
+          uri = matches[1];
+          line = matches[2];
+        }
+
+        if (DebuggerView.Sources.containsValue(uri)) {
+          DebuggerView.setEditorLocation(uri, line, {noDebug: true}).then(() => {
+            if (dom0) {
+              let text = DebuggerView.editor.getText();
+              let index = text.indexOf(searchString);
+              let lastIndex = text.lastIndexOf(searchString);
+
+              // To avoid confusion we only search for DOM0 event handlers when
+              // there is only one possible match in the file.
+              if (index !== -1 && index === lastIndex) {
+                text = text.substr(0, index);
+                let matches = text.match(/\n/g);
+
+                if (matches) {
+                  DebuggerView.editor.setCursor({
+                    line: matches.length
+                  });
+                }
+              }
+            }
+          });
+        }
+      };
+
+      let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
+      toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
+        if (debuggerAlreadyOpen) {
+          showSource(dbg);
+        } else {
+          dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
+        }
+      });
+    }
+  },
+
+  destroy: function(container) {
+    if (this._tooltip) {
+      this._tooltip.panel.removeEventListener("popuphiding", this.destroy, false);
+
+      let boxes = container.querySelectorAll(".event-tooltip-content-box");
+
+      for (let box of boxes) {
+        let {editor} = this._tooltip.eventEditors.get(box);
+        editor.destroy();
+      }
+
+      this._tooltip.eventEditors.clear();
+      this._tooltip.eventEditors = null;
+    }
+
+    let headerNodes = container.querySelectorAll(".event-header");
+
+    for (let node of headerNodes) {
+      node.removeEventListener("click", this._headerClicked);
+    }
+
+    let sourceNodes = container.querySelectorAll(".event-tooltip-debugger-icon");
+    for (let node of sourceNodes) {
+      node.removeEventListener("click", this._debugClicked);
+    }
+
+    this._tooltip = this._eventListenerInfos =  this._toolbox = null;
+  }
+};
+
 /**
  * The swatch cubic-bezier tooltip class is a specific class meant to be used
  * along with rule-view's generated cubic-bezier swatches.
  * It extends the parent SwatchBasedEditorTooltip class.
  * It just wraps a standard Tooltip and sets its content with an instance of a
  * CubicBezierWidget.
  *
  * @param {XULDocument} doc
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.properties
@@ -42,8 +42,11 @@ inspector.panelLabel=Inspector Panel
 # When there are too many nodes to load at once, we will offer to
 # show all the nodes.
 markupView.more.showing=Some nodes were hidden.
 markupView.more.showAll=Show All %S Nodes
 inspector.tooltip=DOM and Style Inspector
 
 #LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded
 previewTooltip.image.brokenImage=Could not load the image
+
+#LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded
+eventsTooltip.openInDebugger=Open in Debugger
--- a/browser/themes/shared/devtools/common.css
+++ b/browser/themes/shared/devtools/common.css
@@ -158,17 +158,24 @@
 }
 
 .devtools-tooltip[clamped-dimensions] {
   min-height: 100px;
   max-height: 400px;
   min-width: 100px;
   max-width: 400px;
 }
-.devtools-tooltip[clamped-dimensions] .panel-arrowcontent {
+.devtools-tooltip[clamped-dimensions-no-min-height] {
+  min-height: 0;
+  max-height: 400px;
+  min-width: 100px;
+  max-width: 400px;
+}
+.devtools-tooltip[clamped-dimensions] .panel-arrowcontent,
+.devtools-tooltip[clamped-dimensions-no-min-height] .panel-arrowcontent {
   overflow: hidden;
 }
 
 /* Tooltip: Simple Text */
 
 .devtools-tooltip-simple-text {
   max-width: 400px;
   margin: 0 -4px; /* Compensate for the .panel-arrowcontent padding. */
--- a/browser/themes/shared/devtools/inspector.css
+++ b/browser/themes/shared/devtools/inspector.css
@@ -33,8 +33,98 @@
   -moz-padding-start: 22px;
   background-position: 8px center, top left, top left;
 }
 
 #inspector-searchbox[focused],
 #inspector-searchbox[filled] {
   max-width: 200px !important;
 }
+
+/* Tooltip: Events */
+
+#devtools-tooltip-events-container {
+  margin: -4px; /* Compensate for the .panel-arrowcontent padding. */
+  max-width: 390px;
+}
+
+.event-header {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+}
+
+.event-tooltip-event-type,
+.event-tooltip-filename,
+.event-tooltip-attributes {
+  -moz-margin-start: 0;
+  flex-shrink: 0;
+  cursor: pointer;
+}
+
+.event-tooltip-event-type {
+  font-weight: bold;
+  font-size: 13px;
+}
+
+.event-tooltip-filename {
+  -moz-margin-end: 0;
+  font-size: 100%;
+  flex-shrink: 1;
+}
+
+.event-tooltip-debugger-icon {
+  width: 16px;
+  height: 16px;
+  -moz-margin-end: 4px;
+  opacity: 0.6;
+  flex-shrink: 0;
+}
+
+.event-tooltip-debugger-icon:hover {
+  opacity: 1;
+}
+
+.event-tooltip-content-box {
+  display: none;
+  overflow: auto;
+  -moz-margin-end: 0;
+}
+
+.event-tooltip-content-box[open] {
+  display: block;
+}
+
+.event-tooltip-source-container {
+  margin-top: 5px;
+  margin-bottom: 10px;
+  -moz-margin-start: 5px;
+  -moz-margin-end: 0;
+}
+
+.event-tooltip-source {
+  margin-bottom: 0;
+}
+
+.theme-dark .event-tooltip-attributes-container {
+  background-color: #B6BABF;
+  color: #343C45;
+}
+
+.event-tooltip-attributes-container {
+  display: flex;
+  flex-shrink: 0;
+  align-items: center;
+  border-radius: 3px;
+  padding: 2px;
+  -moz-margin-start: 5px;
+}
+
+.event-tooltip-attributes {
+  margin: 0;
+  font-size: 9px;
+  padding-top: 2px;
+}
+
+.theme-light .event-tooltip-attributes-container {
+  background-color: #585959;
+  color: #F0F1F2;
+}
--- a/browser/themes/shared/devtools/markup-view.css
+++ b/browser/themes/shared/devtools/markup-view.css
@@ -81,9 +81,30 @@
 #viewbox {
   position: absolute;
   top: 0;
   right: 5px;
   width: 80px;
   border: 1px dashed #888;
   background: rgba(205,205,255,0.2);
   outline: 1px solid transparent;
-}
\ No newline at end of file
+}
+
+/* Events */
+.markupview-events {
+  font-size: 8px;
+  font-weight: bold;
+  line-height: 10px;
+  border-radius: 3px;
+  padding: 0px 2px;
+  -moz-margin-start: 5px;
+  -moz-user-select: none;
+}
+
+.theme-dark .markupview-events {
+  background-color: #b6babf;
+  color: #14171a;
+}
+
+.theme-light .markupview-events {
+  background-color: #585959;
+  color: #fcfcfc;
+}
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -822,17 +822,18 @@
 .theme-light .devtools-toolbarbutton > image,
 .theme-light .devtools-option-toolbarbutton > image,
 .theme-light #breadcrumb-separator-normal,
 .theme-light .scrollbutton-up > .toolbarbutton-icon,
 .theme-light .scrollbutton-down > .toolbarbutton-icon,
 .theme-light #black-boxed-message-button .button-icon,
 .theme-light #canvas-debugging-empty-notice-button .button-icon,
 .theme-light #requests-menu-perf-notice-button .button-icon,
-.theme-light #requests-menu-network-summary-button .button-icon {
+.theme-light #requests-menu-network-summary-button .button-icon,
+.theme-light .event-tooltip-debugger-icon {
   filter: url(filters.svg#invert);
 }
 
 /* Since selected backgrounds are blue, we want to use the normal
  * (light) icons. */
 .theme-light .command-button-invertable[checked=true]:not(:active) > image,
 .theme-light .devtools-tab[icon-invertable][selected] > image,
 .theme-light .devtools-tab[icon-invertable][highlighted] > image,
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -114,16 +114,30 @@ HELPER_SHEET += ":-moz-devtools-highligh
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 
 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
   return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
 });
 
+loader.lazyGetter(this, "Debugger", function() {
+  let JsDebugger = require("resource://gre/modules/jsdebugger.jsm");
+
+  let global = Cu.getGlobalForObject({});
+  JsDebugger.addDebuggerToGlobal(global);
+
+  return global.Debugger;
+});
+
+loader.lazyGetter(this, "eventListenerService", function() {
+  return Cc["@mozilla.org/eventlistenerservice;1"]
+           .getService(Ci.nsIEventListenerService);
+});
+
 exports.register = function(handle) {
   handle.addGlobalActor(InspectorActor, "inspectorActor");
   handle.addTabActor(InspectorActor, "inspectorActor");
 };
 
 exports.unregister = function(handle) {
   handle.removeGlobalActor(InspectorActor);
   handle.removeTabActor(InspectorActor);
@@ -234,16 +248,18 @@ var NodeActor = exports.NodeActor = prot
       publicId: this.rawNode.publicId,
       systemId: this.rawNode.systemId,
 
       attrs: this.writeAttrs(),
 
       pseudoClassLocks: this.writePseudoClassLocks(),
 
       isDisplayed: this.isDisplayed,
+
+      hasEventListeners: this._hasEventListeners,
     };
 
     if (this.isDocumentElement()) {
       form.isDocumentElement = true;
     }
 
     if (this.rawNode.nodeValue) {
       // We only include a short version of the value if it's longer than
@@ -277,16 +293,37 @@ var NodeActor = exports.NodeActor = prot
     if (!style) {
       // Consider all non-element nodes as displayed
       return true;
     } else {
       return style.display !== "none";
     }
   },
 
+  /**
+   * Are event listeners that are listening on this node?
+   */
+  get _hasEventListeners() {
+    let listeners;
+
+    if (this.rawNode.nodeName.toLowerCase() === "html") {
+      listeners = eventListenerService.getListenerInfoFor(this.rawNode.ownerGlobal);
+    } else {
+      listeners = eventListenerService.getListenerInfoFor(this.rawNode) || [];
+    }
+
+    listeners = listeners.filter(listener => {
+      return listener.listenerObject && listener.type && listener.listenerObject;
+    });
+
+    let hasListeners = listeners.length > 0;
+
+    return hasListeners;
+  },
+
   writeAttrs: function() {
     if (!this.rawNode.attributes) {
       return undefined;
     }
     return [{namespace: attr.namespace, name: attr.name, value: attr.value }
             for (attr of this.rawNode.attributes)];
   },
 
@@ -300,16 +337,133 @@ var NodeActor = exports.NodeActor = prot
         ret = ret || [];
         ret.push(pseudo);
       }
     }
     return ret;
   },
 
   /**
+   * Get event listeners attached to a node.
+   *
+   * @param  {Node} node
+   *         Node for which we are to get listeners.
+   * @return {Array}
+   *         An array of objects where a typical object looks like this:
+   *           {
+   *             type: "click",
+   *             DOM0: true,
+   *             capturing: true,
+   *             handler: "function() { doSomething() }",
+   *             origin: "http://www.mozilla.com",
+   *             searchString: 'onclick="doSomething()"'
+   *           }
+   */
+  getEventListeners: function(node) {
+    let dbg = new Debugger();
+    let handlers = eventListenerService.getListenerInfoFor(node);
+    let events = [];
+
+    for (let handler of handlers) {
+      let listener = handler.listenerObject;
+
+      // If there is no JS event listener skip this.
+      if (!listener) {
+        continue;
+      }
+
+      let global = Cu.getGlobalForObject(listener);
+      let globalDO = dbg.addDebuggee(global);
+      let listenerDO = globalDO.makeDebuggeeValue(listener);
+
+      // If the listener is an object with a 'handleEvent' method, use that.
+      if (listenerDO.class === "Object" || listenerDO.class === "XULElement") {
+        let desc;
+
+        while (!desc && listenerDO) {
+          desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
+          listenerDO = listenerDO.proto;
+        }
+
+        if (desc && desc.value) {
+          listenerDO = desc.value;
+        }
+      }
+
+      if (listenerDO.isBoundFunction) {
+        listenerDO = listenerDO.boundTargetFunction;
+      }
+
+      let script = listenerDO.script;
+      let scriptSource = script.source.text;
+      let functionSource =
+        scriptSource.substr(script.sourceStart, script.sourceLength);
+
+      /*
+      The script returned is the whole script and
+      scriptSource.substr(script.sourceStart, script.sourceLength) returns
+      something like:
+        () { doSomething(); }
+
+      So we need to work back to the preceeding \n, ; or } so we can get the
+        appropriate function info e.g.:
+        () => { doSomething(); }
+        function doit() { doSomething(); }
+        doit: function() { doSomething(); }
+      */
+      let scriptBeforeFunc = scriptSource.substr(0, script.sourceStart);
+      let lastEnding = Math.max(
+        scriptBeforeFunc.lastIndexOf(";"),
+        scriptBeforeFunc.lastIndexOf("}"),
+        scriptBeforeFunc.lastIndexOf("{"),
+        scriptBeforeFunc.lastIndexOf("("),
+        scriptBeforeFunc.lastIndexOf(","),
+        scriptBeforeFunc.lastIndexOf("!")
+      );
+
+      if (lastEnding !== -1) {
+        let functionPrefix = scriptBeforeFunc.substr(lastEnding + 1);
+        functionSource = functionPrefix + functionSource;
+      }
+
+      let type = handler.type;
+      let dom0 = false;
+
+      if (typeof node.hasAttribute !== "undefined") {
+        dom0 = !!node.hasAttribute("on" + type);
+      } else {
+        dom0 = !!node["on" + type];
+      }
+
+      let line = script.startLine;
+      let url = script.url;
+      let origin = url + (dom0 ? "" : ":" + line);
+      let searchString;
+
+      if (dom0) {
+        searchString = "on" + type + "=\"" + script.source.text + "\"";
+      } else {
+        scriptSource = "    " + scriptSource;
+      }
+
+      events.push({
+        type: type,
+        DOM0: dom0,
+        capturing: handler.capturing,
+        handler: functionSource.trim(),
+        origin: origin,
+        searchString: searchString
+      });
+
+      dbg.removeDebuggee(globalDO);
+    }
+    return events;
+  },
+
+  /**
    * Returns a LongStringActor with the node's value.
    */
   getNodeValue: method(function() {
     return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
   }, {
     request: {},
     response: {
       value: RetVal("longstring")
@@ -349,16 +503,31 @@ var NodeActor = exports.NodeActor = prot
       return promise.reject(new Error("Image not available"));
     }
   }, {
     request: {maxDim: Arg(0, "nullable:number")},
     response: RetVal("imageData")
   }),
 
   /**
+   * Get all event listeners that are listening on this node.
+   */
+  getEventListenerInfo: method(function() {
+    if (this.rawNode.nodeName.toLowerCase() === "html") {
+      return this.getEventListeners(this.rawNode.ownerGlobal);
+    }
+    return this.getEventListeners(this.rawNode);
+  }, {
+    request: {},
+    response: {
+      events: RetVal("json")
+    }
+  }),
+
+  /**
    * Modify a node's attributes.  Passed an array of modifications
    * similar in format to "attributes" mutations.
    * {
    *   attributeName: <string>
    *   attributeNamespace: <optional string>
    *   newValue: <optional string> - If null or undefined, the attribute
    *     will be removed.
    * }
@@ -546,16 +715,18 @@ let NodeFront = protocol.FrontClass(Node
 
   get className() {
     return this.getAttribute("class") || '';
   },
 
   get hasChildren() this._form.numChildren > 0,
   get numChildren() this._form.numChildren,
 
+  get hasEventListeners() this._form.hasEventListeners,
+
   get tagName() this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null,
   get shortValue() this._form.shortValue,
   get incompleteValue() !!this._form.incompleteValue,
 
   get isDocumentElement() !!this._form.isDocumentElement,
 
   // doctype properties
   get name() this._form.name,