Bug 1266450 - part6: migrate EventDetails tooltip;r=bgrins
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 31 May 2016 11:25:43 +0200
changeset 301233 0109ec48bf5a2fc8a300deb52212c8855d0382da
parent 301232 42e38084205ef6aa7d70d212c6e0532c63311e21
child 301234 62ad4f6b5c3428e4b33e1e4ba75288c94c45ec92
push id78263
push usercbook@mozilla.com
push dateThu, 09 Jun 2016 10:13:31 +0000
treeherdermozilla-inbound@3d132a280ca0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1266450
milestone50.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 1266450 - part6: migrate EventDetails tooltip;r=bgrins For now this is a 1 to 1 migration of the existing Tooltip helper method from XUL to HTML. MozReview-Commit-ID: 9YiJLgibV9h
devtools/client/inspector/markup/markup.js
devtools/client/inspector/markup/test/browser_markup_events-overflow.js
devtools/client/inspector/markup/test/helper_events_test_runner.js
devtools/client/shared/widgets/HTMLTooltip.js
devtools/client/shared/widgets/Tooltip.js
devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
devtools/client/shared/widgets/tooltip/moz.build
devtools/client/themes/tooltips.css
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -30,21 +30,20 @@ const HTML_VOID_ELEMENTS = [ "area", "ba
   "track", "wbr" ];
 
 const {UndoStack} = require("devtools/client/shared/undo");
 const {editableField, InplaceEditor} =
       require("devtools/client/shared/inplace-editor");
 const {HTMLEditor} = require("devtools/client/inspector/markup/html-editor");
 const promise = require("promise");
 const Services = require("Services");
-const {Tooltip} = require("devtools/client/shared/widgets/Tooltip");
 const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
 const {setImageTooltip, setBrokenImageTooltip} =
       require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
-
+const {setEventTooltip} = require("devtools/client/shared/widgets/tooltip/EventTooltipHelper");
 const EventEmitter = require("devtools/shared/event-emitter");
 const Heritage = require("sdk/core/heritage");
 const {parseAttribute} =
       require("devtools/client/shared/node-attribute-parser");
 const {Task} = require("devtools/shared/task");
 const {scrollIntoViewIfNeeded} = require("devtools/shared/layout/utils");
 const {PrefObserver} = require("devtools/client/styleeditor/utils");
 const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
@@ -169,17 +168,18 @@ MarkupView.prototype = {
   /**
    * How long does a node flash when it mutates (in ms).
    */
   CONTAINER_FLASHING_DURATION: 500,
 
   _selectedContainer: null,
 
   _initTooltips: function () {
-    this.eventDetailsTooltip = new Tooltip(this._inspector.panelDoc);
+    this.eventDetailsTooltip = new HTMLTooltip(this._inspector.toolbox,
+      {type: "arrow"});
     this.imagePreviewTooltip = new HTMLTooltip(this._inspector.toolbox,
       {type: "arrow"});
     this._enableImagePreviewTooltip();
   },
 
   _enableImagePreviewTooltip: function () {
     this.imagePreviewTooltip.startTogglingOnHover(this._elt,
       this._isImagePreviewTarget);
@@ -2610,21 +2610,18 @@ function MarkupElementContainer(markupVi
 }
 
 MarkupElementContainer.prototype = Heritage.extend(MarkupContainer.prototype, {
   _buildEventTooltipContent: function (target, tooltip) {
     if (target.hasAttribute("data-event")) {
       tooltip.hide(target);
 
       this.node.getEventListenerInfo().then(listenerInfo => {
-        tooltip.setEventContent({
-          eventListenerInfos: listenerInfo,
-          toolbox: this.markup._inspector.toolbox
-        });
-
+        let toolbox = this.markup._inspector.toolbox;
+        setEventTooltip(tooltip, listenerInfo, toolbox);
         // Disable the image preview tooltip while we display the event details
         this.markup._disableImagePreviewTooltip();
         tooltip.once("hidden", () => {
           // Enable the image preview tooltip after closing the event details
           this.markup._enableImagePreviewTooltip();
         });
         tooltip.show(target);
       });
--- a/devtools/client/inspector/markup/test/browser_markup_events-overflow.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events-overflow.js
@@ -38,17 +38,17 @@ add_task(function* () {
   let tooltip = inspector.markup.eventDetailsTooltip;
 
   info("Clicking to open event tooltip.");
   EventUtils.synthesizeMouseAtCenter(evHolder, {},
     inspector.markup.doc.defaultView);
   yield tooltip.once("shown");
   info("EventTooltip visible.");
 
-  let container = tooltip.content;
+  let container = tooltip.panel;
   let containerRect = container.getBoundingClientRect();
   let headers = container.querySelectorAll(".event-header");
 
   for (let data of TEST_DATA) {
     info("Testing scrolling when " + data.desc);
 
     if (data.initialScrollTop < 0) {
       info("Scrolling container to the bottom.");
--- a/devtools/client/inspector/markup/test/helper_events_test_runner.js
+++ b/devtools/client/inspector/markup/test/helper_events_test_runner.js
@@ -65,42 +65,45 @@ function* checkEventsForNode(test, inspe
   // Click button to show tooltip
   info("Clicking evHolder");
   EventUtils.synthesizeMouseAtCenter(evHolder, {},
     inspector.markup.doc.defaultView);
   yield tooltip.once("shown");
   info("tooltip shown");
 
   // Check values
-  let headers = tooltip.content.querySelectorAll(".event-header");
+  let headers = tooltip.panel.querySelectorAll(".event-header");
   let nodeFront = container.node;
   let cssSelector = nodeFront.nodeName + "#" + nodeFront.id;
 
   for (let i = 0; i < headers.length; i++) {
     info("Processing header[" + i + "] for " + cssSelector);
 
     let header = headers[i];
     let type = header.querySelector(".event-tooltip-event-type");
     let filename = header.querySelector(".event-tooltip-filename");
     let attributes = header.querySelectorAll(".event-tooltip-attributes");
     let contentBox = header.nextElementSibling;
 
-    is(type.getAttribute("value"), expected[i].type,
+    is(type.textContent, expected[i].type,
        "type matches for " + cssSelector);
-    is(filename.getAttribute("value"), expected[i].filename,
+    is(filename.textContent, expected[i].filename,
        "filename matches for " + cssSelector);
 
     is(attributes.length, expected[i].attributes.length,
        "we have the correct number of attributes");
 
     for (let j = 0; j < expected[i].attributes.length; j++) {
-      is(attributes[j].getAttribute("value"), expected[i].attributes[j],
+      is(attributes[j].textContent, expected[i].attributes[j],
          "attribute[" + j + "] matches for " + cssSelector);
     }
 
+    // Make sure the header is not hidden by scrollbars before clicking.
+    header.scrollIntoView();
+
     EventUtils.synthesizeMouseAtCenter(header, {}, type.ownerGlobal);
     yield tooltip.once("event-tooltip-ready");
 
     let editor = tooltip.eventEditors.get(contentBox).editor;
     is(editor.getText(), expected[i].handler,
        "handler matches for " + cssSelector);
   }
 
--- a/devtools/client/shared/widgets/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/HTMLTooltip.js
@@ -105,24 +105,25 @@ HTMLTooltip.prototype = {
   /**
    * Set the tooltip content element. The preferred width/height should also be
    * specified here.
    *
    * @param {Element} content
    *        The tooltip content, should be a HTML element.
    * @param {Number} width
    *        Preferred width for the tooltip container
-   * @param {Number} height
+   * @param {Number} height (optional)
    *        Preferred height for the tooltip container. If the content height is
    *        smaller than the container's height, the tooltip will automatically
-   *        shrink around the content.
+   *        shrink around the content. If not specified, will use all the height
+   *        available.
    * @return {Promise} a promise that will resolve when the content has been
    *         added in the tooltip container.
    */
-  setContent: function (content, width, height) {
+  setContent: function (content, width, height = Infinity) {
     let themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type];
     let themeWidth = 2 * EXTRA_BORDER[this.type];
 
     this.preferredWidth = width + themeWidth;
     this.preferredHeight = height + themeHeight;
 
     this.panel.innerHTML = "";
     this.panel.appendChild(content);
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -11,17 +11,16 @@ const {CubicBezierWidget} =
       require("devtools/client/shared/widgets/CubicBezierWidget");
 const {MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
 const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
 const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {colorUtils} = require("devtools/client/shared/css-color");
 const Heritage = require("sdk/core/heritage");
 const {Eyedropper} = require("devtools/client/eyedropper/eyedropper");
-const Editor = require("devtools/client/sourceeditor/editor");
 const Services = require("Services");
 
 loader.lazyRequireGetter(this, "beautify", "devtools/shared/jsbeautify/beautify");
 loader.lazyRequireGetter(this, "setNamedTimeout", "devtools/client/shared/widgets/view-helpers", true);
 loader.lazyRequireGetter(this, "clearNamedTimeout", "devtools/client/shared/widgets/view-helpers", true);
 loader.lazyRequireGetter(this, "setNamedTimeout", "devtools/client/shared/widgets/view-helpers", true);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -430,29 +429,16 @@ 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]
@@ -1061,308 +1047,16 @@ Heritage.extend(SwatchBasedEditorTooltip
     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);
-
-      if (!listener.hide.debugger) {
-        let debuggerIcon = doc.createElement("image");
-        debuggerIcon.className = "event-tooltip-debugger-icon";
-        debuggerIcon.setAttribute("src", "chrome://devtools/skin/images/tool-debugger.svg");
-        let openInDebugger =
-            l10n.strings.GetStringFromName("eventsTooltip.openInDebugger");
-        debuggerIcon.setAttribute("tooltiptext", openInDebugger);
-        header.appendChild(debuggerIcon);
-      }
-
-      if (!listener.hide.type) {
-        let eventTypeLabel = doc.createElement("label");
-        eventTypeLabel.className = "event-tooltip-event-type";
-        eventTypeLabel.setAttribute("value", listener.type);
-        eventTypeLabel.setAttribute("tooltiptext", listener.type);
-        header.appendChild(eventTypeLabel);
-      }
-
-      if (!listener.hide.filename) {
-        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 attributesContainer = doc.createElement("hbox");
-      attributesContainer.setAttribute("class",
-                                       "event-tooltip-attributes-container");
-      header.appendChild(attributesContainer);
-
-      if (!listener.hide.capturing) {
-        let attributesBox = doc.createElement("box");
-        attributesBox.setAttribute("class", "event-tooltip-attributes-box");
-        attributesContainer.appendChild(attributesBox);
-
-        let capturing = doc.createElement("label");
-        capturing.className = "event-tooltip-attributes";
-        capturing.setAttribute("value", phase);
-        capturing.setAttribute("tooltiptext", phase);
-        attributesBox.appendChild(capturing);
-      }
-
-      if (listener.tags) {
-        for (let tag of listener.tags.split(",")) {
-          let attributesBox = doc.createElement("box");
-          attributesBox.setAttribute("class", "event-tooltip-attributes-box");
-          attributesContainer.appendChild(attributesBox);
-
-          let tagBox = doc.createElement("label");
-          tagBox.className = "event-tooltip-attributes";
-          tagBox.setAttribute("value", tag);
-          tagBox.setAttribute("tooltiptext", tag);
-          attributesBox.appendChild(tagBox);
-        }
-      }
-
-      if (!listener.hide.dom0) {
-        let attributesBox = doc.createElement("box");
-        attributesBox.setAttribute("class", "event-tooltip-attributes-box");
-        attributesContainer.appendChild(attributesBox);
-
-        let dom0 = doc.createElement("label");
-        dom0.className = "event-tooltip-attributes";
-        dom0.setAttribute("value", level);
-        dom0.setAttribute("tooltiptext", level);
-        attributesBox.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-max-or-min-height",
-                                     "");
-    this._tooltip.panel.setAttribute("wide", "");
-
-    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);
-      event.stopPropagation();
-      return;
-    }
-
-    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(() => {
-        /* eslint-disable camelcase */
-        let tidied = beautify.js(handler, { indent_size: 2 });
-        /* eslint-enable */
-        editor.setText(tidied);
-
-        eventEditors.appended = true;
-
-        let container = header.parentElement.getBoundingClientRect();
-        if (header.getBoundingClientRect().top < container.top) {
-          header.scrollIntoView(true);
-        } else if (content.getBoundingClientRect().bottom > container.bottom) {
-          content.scrollIntoView(false);
-        }
-
-        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];
-        }
-
-        let item = DebuggerView.Sources.getItemForAttachment(
-          a => a.source.url === uri
-        );
-        if (item) {
-          let actor = item.attachment.source.actor;
-          DebuggerView.setEditorLocation(
-            actor, 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 newlineMatches = text.match(/\n/g);
-
-                if (newlineMatches) {
-                  DebuggerView.editor.setCursor({
-                    line: newlineMatches.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 = 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._eventListenerInfos = this._toolbox = this._tooltip = 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
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
@@ -0,0 +1,315 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+loader.lazyGetter(this, "GetStringFromName", () => {
+  let bundle = Services.strings.createBundle(
+    "chrome://devtools/locale/inspector.properties");
+  return key => {
+    return bundle.GetStringFromName(key);
+  };
+});
+
+loader.lazyRequireGetter(this, "Editor", "devtools/client/sourceeditor/editor");
+loader.lazyRequireGetter(this, "beautify", "devtools/shared/jsbeautify/beautify");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const CONTAINER_WIDTH = 500;
+
+/**
+ * Set the content of a provided HTMLTooltip instance to display a list of event
+ * listeners, with their event type, capturing argument and a link to the code
+ * of the event handler.
+ *
+ * @param {HTMLTooltip} tooltip
+ *        The tooltip instance on which the event details content should be set
+ * @param {Array} eventListenerInfos
+ *        A list of event listeners
+ * @param {Toolbox} toolbox
+ *        Toolbox used to select debugger panel
+ */
+function setEventTooltip(tooltip, eventListenerInfos, toolbox) {
+  let eventTooltip = new EventTooltip(tooltip, eventListenerInfos, toolbox);
+  eventTooltip.init();
+}
+
+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);
+}
+
+EventTooltip.prototype = {
+  init: function () {
+    let config = {
+      mode: Editor.modes.js,
+      lineNumbers: false,
+      lineWrapping: true,
+      readOnly: true,
+      styleActiveLine: true,
+      extraKeys: {},
+      theme: "mozilla markup-view"
+    };
+
+    let doc = this._tooltip.doc;
+    this.container = doc.createElementNS(XHTML_NS, "div");
+    this.container.className = "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.createElementNS(XHTML_NS, "div");
+      header.className = "event-header devtools-toolbar";
+      this.container.appendChild(header);
+
+      if (!listener.hide.debugger) {
+        let debuggerIcon = doc.createElementNS(XHTML_NS, "img");
+        debuggerIcon.className = "event-tooltip-debugger-icon";
+        debuggerIcon.setAttribute("src",
+          "chrome://devtools/skin/images/tool-debugger.svg");
+        let openInDebugger = GetStringFromName("eventsTooltip.openInDebugger");
+        debuggerIcon.setAttribute("title", openInDebugger);
+        header.appendChild(debuggerIcon);
+      }
+
+      if (!listener.hide.type) {
+        let eventTypeLabel = doc.createElementNS(XHTML_NS, "span");
+        eventTypeLabel.className = "event-tooltip-event-type";
+        eventTypeLabel.textContent = listener.type;
+        eventTypeLabel.setAttribute("title", listener.type);
+        header.appendChild(eventTypeLabel);
+      }
+
+      if (!listener.hide.filename) {
+        let filename = doc.createElementNS(XHTML_NS, "span");
+        filename.className = "event-tooltip-filename devtools-monospace";
+        filename.textContent = listener.origin;
+        filename.setAttribute("title", listener.origin);
+        header.appendChild(filename);
+      }
+
+      let attributesContainer = doc.createElementNS(XHTML_NS, "div");
+      attributesContainer.className = "event-tooltip-attributes-container";
+      header.appendChild(attributesContainer);
+
+      if (!listener.hide.capturing) {
+        let attributesBox = doc.createElementNS(XHTML_NS, "div");
+        attributesBox.className = "event-tooltip-attributes-box";
+        attributesContainer.appendChild(attributesBox);
+
+        let capturing = doc.createElementNS(XHTML_NS, "span");
+        capturing.className = "event-tooltip-attributes";
+        capturing.textContent = phase;
+        capturing.setAttribute("title", phase);
+        attributesBox.appendChild(capturing);
+      }
+
+      if (listener.tags) {
+        for (let tag of listener.tags.split(",")) {
+          let attributesBox = doc.createElementNS(XHTML_NS, "div");
+          attributesBox.className = "event-tooltip-attributes-box";
+          attributesContainer.appendChild(attributesBox);
+
+          let tagBox = doc.createElementNS(XHTML_NS, "span");
+          tagBox.className = "event-tooltip-attributes";
+          tagBox.textContent = tag;
+          tagBox.setAttribute("title", tag);
+          attributesBox.appendChild(tagBox);
+        }
+      }
+
+      if (!listener.hide.dom0) {
+        let attributesBox = doc.createElementNS(XHTML_NS, "div");
+        attributesBox.className = "event-tooltip-attributes-box";
+        attributesContainer.appendChild(attributesBox);
+
+        let dom0 = doc.createElementNS(XHTML_NS, "span");
+        dom0.className = "event-tooltip-attributes";
+        dom0.textContent = level;
+        dom0.setAttribute("title", level);
+        attributesBox.appendChild(dom0);
+      }
+
+      // Content
+      let content = doc.createElementNS(XHTML_NS, "div");
+      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";
+      this.container.appendChild(content);
+
+      this._addContentListeners(header);
+    }
+
+    this._tooltip.setContent(this.container, CONTAINER_WIDTH);
+    this._tooltip.on("hidden", this.destroy);
+  },
+
+  _addContentListeners: function (header) {
+    header.addEventListener("click", this._headerClicked);
+  },
+
+  _headerClicked: function (event) {
+    if (event.target.classList.contains("event-tooltip-debugger-icon")) {
+      this._debugClicked(event);
+      event.stopPropagation();
+      return;
+    }
+
+    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.createElementNS(XHTML_NS, "iframe");
+      iframe.setAttribute("style", "width: 100%; height: 100%; border-style: none;");
+
+      editor.appendTo(content, iframe).then(() => {
+        let tidied = beautify.js(handler, { "indent_size": 2 });
+        editor.setText(tidied);
+
+        eventEditors.appended = true;
+
+        let container = header.parentElement.getBoundingClientRect();
+        if (header.getBoundingClientRect().top < container.top) {
+          header.scrollIntoView(true);
+        } else if (content.getBoundingClientRect().bottom > container.bottom) {
+          content.scrollIntoView(false);
+        }
+
+        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];
+        }
+
+        let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === uri);
+        if (item) {
+          let actor = item.attachment.source.actor;
+          DebuggerView.setEditorLocation(
+            actor, 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 newlineMatches = text.match(/\n/g);
+
+                if (newlineMatches) {
+                  DebuggerView.editor.setCursor({
+                    line: newlineMatches.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 () {
+    if (this._tooltip) {
+      this._tooltip.off("hidden", this.destroy);
+
+      let boxes = this.container.querySelectorAll(".event-tooltip-content-box");
+
+      for (let box of boxes) {
+        let {editor} = this._tooltip.eventEditors.get(box);
+        editor.destroy();
+      }
+
+      this._tooltip.eventEditors = null;
+    }
+
+    let headerNodes = this.container.querySelectorAll(".event-header");
+
+    for (let node of headerNodes) {
+      node.removeEventListener("click", this._headerClicked);
+    }
+
+    let sourceNodes = this.container.querySelectorAll(".event-tooltip-debugger-icon");
+    for (let node of sourceNodes) {
+      node.removeEventListener("click", this._debugClicked);
+    }
+
+    this._eventListenerInfos = this._toolbox = this._tooltip = null;
+  }
+};
+
+module.exports.setEventTooltip = setEventTooltip;
--- a/devtools/client/shared/widgets/tooltip/moz.build
+++ b/devtools/client/shared/widgets/tooltip/moz.build
@@ -1,10 +1,11 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'EventTooltipHelper.js',
     'ImageTooltipHelper.js',
     'TooltipToggle.js',
 )
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -194,77 +194,87 @@
 .tooltip-top .tooltip-arrow:before {
   margin-top: -12px;
   transform: rotate(45deg);
 }
 
 /* Tooltip: Events */
 
 #devtools-tooltip-events-container {
-  margin: -4px; /* Compensate for the .panel-arrowcontent padding. */
-  max-width: 590px;
   overflow-y: auto;
 }
 
 .event-header {
   display: flex;
   align-items: center;
   cursor: pointer;
+  overflow: hidden;
 }
 
 .event-header:first-child {
   border-width: 0;
 }
 
 .event-header:not(:first-child) {
   border-width: 1px 0 0 0;
 }
 
+.devtools-tooltip-events-container {
+  height: 100%;
+  overflow-y: auto;
+}
+
 .event-tooltip-event-type,
 .event-tooltip-filename,
 .event-tooltip-attributes {
   margin-inline-start: 0;
   flex-shrink: 0;
   cursor: pointer;
 }
 
 .event-tooltip-event-type {
   font-weight: bold;
   font-size: 13px;
 }
 
 .event-tooltip-filename {
-  margin-inline-end: 0;
+  margin: 0 5px;
   font-size: 100%;
   flex-shrink: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  /* Force ellipsis to be displayed on the left */
+  direction: rtl;
 }
 
 .event-tooltip-debugger-icon {
   width: 16px;
   height: 16px;
   margin-inline-end: 4px;
   opacity: 0.6;
   flex-shrink: 0;
 }
 
 .event-tooltip-debugger-icon:hover {
   opacity: 1;
 }
 
 .event-tooltip-content-box {
   display: none;
-  height: 100px;
+  height: 54px;
   overflow: hidden;
   margin-inline-end: 0;
   border: 1px solid var(--theme-splitter-color);
   border-width: 1px 0 0 0;
 }
 
 .event-toolbox-content-box iframe {
   height: 100%;
+  border-style: none;
 }
 
 .event-tooltip-content-box[open] {
   display: block;
 }
 
 .event-tooltip-source-container {
   margin-top: 5px;
@@ -283,16 +293,17 @@
   flex-grow: 1;
   justify-content: flex-end;
 }
 
 .event-tooltip-attributes-box {
   display: flex;
   flex-shrink: 0;
   align-items: center;
+  height: 14px;
   border-radius: 3px;
   padding: 2px;
   margin-inline-start: 5px;
   background-color: var(--theme-body-color-alt);
   color: var(--theme-toolbar-background);
 }
 
 .event-tooltip-attributes {