Bug 1266450 - part6: migrate EventDetails tooltip;r=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 31 May 2016 11:25:43 +0200
changeset 376622 fcee58928b2f281acc5da3e94cff8f02a1cf83bf
parent 376621 74de63608de290a5f947bdd1959f605457a60510
child 376623 26334504e7865e51915d2ec867d5d364b4f00ea7
push id20630
push userjdescottes@mozilla.com
push dateWed, 08 Jun 2016 11:49:53 +0000
reviewersbgrins
bugs1266450
milestone50.0a1
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 {