Bug 1529364 - Move the markup context menu into a separate module. r=rcaliman
authorGabriel Luong <gabriel.luong@gmail.com>
Wed, 20 Feb 2019 15:03:38 -0500
changeset 460252 e92ff56d2be21676b447c6fbb87b4c4479539bc9
parent 460251 9c0bb955c19d90c1886b9af34d744eff5f110e52
child 460253 4a01452528cd75f9f41822da71ab011389502150
child 460447 af3314b6bcb1f9bb31700ad9a456661018e37686
push id112071
push usergabriel.luong@gmail.com
push dateThu, 21 Feb 2019 18:16:34 +0000
treeherdermozilla-inbound@e92ff56d2be2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcaliman
bugs1529364
milestone67.0a1
first release with
nightly linux32
e92ff56d2be2 / 67.0a1 / 20190221215439 / files
nightly linux64
e92ff56d2be2 / 67.0a1 / 20190221215439 / files
nightly mac
e92ff56d2be2 / 67.0a1 / 20190221215439 / files
nightly win32
e92ff56d2be2 / 67.0a1 / 20190221215439 / files
nightly win64
e92ff56d2be2 / 67.0a1 / 20190221215439 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1529364 - Move the markup context menu into a separate module. r=rcaliman Differential Revision: https://phabricator.services.mozilla.com/D20554
devtools/client/inspector/inspector.js
devtools/client/inspector/markup/markup-context-menu.js
devtools/client/inspector/markup/markup.js
devtools/client/inspector/markup/moz.build
devtools/client/inspector/markup/test/browser_markup_links_05.js
devtools/client/inspector/markup/test/browser_markup_links_06.js
devtools/client/inspector/markup/test/browser_markup_links_07.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js
devtools/client/inspector/markup/views/markup-container.js
devtools/client/inspector/shared/utils.js
devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js
devtools/client/inspector/test/browser_inspector_textbox-menu.js
devtools/client/inspector/test/shared-head.js
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -23,32 +23,24 @@ const Promise = require("Promise");
 
 loader.lazyRequireGetter(this, "initCssProperties", "devtools/shared/fronts/css-properties", true);
 loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true);
 loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts");
 loader.lazyRequireGetter(this, "InspectorSearch", "devtools/client/inspector/inspector-search", true);
 loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/inspector/toolsidebar", true);
 loader.lazyRequireGetter(this, "MarkupView", "devtools/client/inspector/markup/markup");
 loader.lazyRequireGetter(this, "HighlightersOverlay", "devtools/client/inspector/shared/highlighters-overlay");
-loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
-loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
-loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
 loader.lazyRequireGetter(this, "ExtensionSidebar", "devtools/client/inspector/extensions/extension-sidebar");
-loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
-loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 loader.lazyRequireGetter(this, "saveScreenshot", "devtools/shared/screenshot/save");
 
 loader.lazyImporter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
 
 const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
-loader.lazyGetter(this, "TOOLBOX_L10N", function() {
-  return new LocalizationHelper("devtools/client/locales/toolbox.properties");
-});
 
 // Sidebar dimensions
 const INITIAL_SIDEBAR_SIZE = 350;
 
 // How long we wait to debounce resize events
 const LAZY_RESIZE_INTERVAL_MS = 200;
 
 // If the toolbox's width is smaller than the given amount of pixels, the sidebar
@@ -120,22 +112,19 @@ function Inspector(toolbox) {
 
   this.reflowTracker = new ReflowTracker(this._target);
   this.styleChangeTracker = new InspectorStyleChangeTracker(this);
 
   // Store the URL of the target page prior to navigation in order to ensure
   // telemetry counts in the Grid Inspector are not double counted on reload.
   this.previousURL = this.target.url;
 
-  this.nodeMenuTriggerInfo = null;
-
   this._clearSearchResultsLabel = this._clearSearchResultsLabel.bind(this);
   this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
   this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
-  this._onContextMenu = this._onContextMenu.bind(this);
   this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
   this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
 
   this.onDetached = this.onDetached.bind(this);
   this.onHostChanged = this.onHostChanged.bind(this);
   this.onMarkupLoaded = this.onMarkupLoaded.bind(this);
   this.onNewSelection = this.onNewSelection.bind(this);
   this.onNewRoot = this.onNewRoot.bind(this);
@@ -1397,18 +1386,16 @@ Inspector.prototype = {
 
     if (this._highlighters) {
       this._highlighters.destroy();
       this._highlighters = null;
     }
 
     if (this._markupFrame) {
       this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
-      this._markupFrame.contentWindow.removeEventListener("contextmenu",
-                                                          this._onContextMenu);
     }
 
     if (this._search) {
       this._search.destroy();
       this._search = null;
     }
 
     const sidebarDestroyer = this.sidebar.destroy();
@@ -1444,459 +1431,16 @@ Inspector.prototype = {
       markupDestroyer,
       sidebarDestroyer,
       ruleViewSideBarDestroyer,
     ]);
 
     return this._panelDestroyer;
   },
 
-  /**
-   * Returns the clipboard content if it is appropriate for pasting
-   * into the current node's outer HTML, otherwise returns null.
-   */
-  _getClipboardContentForPaste: function() {
-    const content = clipboardHelper.getText();
-    if (content && content.trim().length > 0) {
-      return content;
-    }
-    return null;
-  },
-
-  _onContextMenu: function(e) {
-    if (!(e.originalTarget instanceof Element) ||
-        e.originalTarget.closest("input[type=text]") ||
-        e.originalTarget.closest("input:not([type])") ||
-        e.originalTarget.closest("textarea")) {
-      return;
-    }
-
-    e.stopPropagation();
-    e.preventDefault();
-
-    this._openMenu({
-      screenX: e.screenX,
-      screenY: e.screenY,
-      target: e.target,
-    });
-  },
-
-  _openMenu: function({ target, screenX = 0, screenY = 0 } = { }) {
-    if (this.selection.isSlotted()) {
-      // Slotted elements should not show any context menu.
-      return null;
-    }
-
-    const markupContainer = this.markup.getContainer(this.selection.nodeFront);
-
-    this.contextMenuTarget = target;
-    this.nodeMenuTriggerInfo = markupContainer &&
-      markupContainer.editor.getInfoAtNode(target);
-
-    const isSelectionElement = this.selection.isElementNode() &&
-                             !this.selection.isPseudoElementNode();
-    const isEditableElement = isSelectionElement &&
-                            !this.selection.isAnonymousNode();
-    const isDuplicatableElement = isSelectionElement &&
-                                !this.selection.isAnonymousNode() &&
-                                !this.selection.isRoot();
-    const isScreenshotable = isSelectionElement &&
-                           this.selection.nodeFront.isTreeDisplayed;
-
-    const menu = new Menu();
-    menu.append(new MenuItem({
-      id: "node-menu-edithtml",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
-      disabled: !isEditableElement,
-      click: () => this.editHTML(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-add",
-      label: INSPECTOR_L10N.getStr("inspectorAddNode.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"),
-      disabled: !this.canAddHTMLChild(),
-      click: () => this.addNode(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-duplicatenode",
-      label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"),
-      disabled: !isDuplicatableElement,
-      click: () => this.duplicateNode(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-delete",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"),
-      disabled: !this.isDeletable(this.selection.nodeFront),
-      click: () => this.deleteNode(),
-    }));
-
-    menu.append(new MenuItem({
-      label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"),
-      submenu: this._getAttributesSubmenu(isEditableElement),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-    }));
-
-    // Set the pseudo classes
-    for (const name of ["hover", "active", "focus", "focus-within"]) {
-      const menuitem = new MenuItem({
-        id: "node-menu-pseudo-" + name,
-        label: name,
-        type: "checkbox",
-        click: this.togglePseudoClass.bind(this, ":" + name),
-      });
-
-      if (isSelectionElement) {
-        const checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
-        menuitem.checked = checked;
-      } else {
-        menuitem.disabled = true;
-      }
-
-      menu.append(menuitem);
-    }
-
-    menu.append(new MenuItem({
-      type: "separator",
-    }));
-
-    menu.append(new MenuItem({
-      label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"),
-      submenu: this._getCopySubmenu(markupContainer, isSelectionElement),
-    }));
-
-    menu.append(new MenuItem({
-      label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"),
-      submenu: this._getPasteSubmenu(isEditableElement),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-    }));
-
-    const isNodeWithChildren = this.selection.isNode() &&
-                             markupContainer.hasChildren;
-    menu.append(new MenuItem({
-      id: "node-menu-expand",
-      label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"),
-      disabled: !isNodeWithChildren,
-      click: () => this.expandNode(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-collapse",
-      label: INSPECTOR_L10N.getStr("inspectorCollapseAll.label"),
-      disabled: !isNodeWithChildren || !markupContainer.expanded,
-      click: () => this.collapseAll(),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-    }));
-
-    menu.append(new MenuItem({
-      id: "node-menu-scrollnodeintoview",
-      label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.scrollNodeIntoView(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-screenshotnode",
-      label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"),
-      disabled: !isScreenshotable,
-      click: () => this.screenshotNode().catch(console.error),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-useinconsole",
-      label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"),
-      click: () => this.useInConsole(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-showdomproperties",
-      label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
-      click: () => this.showDOMProperties(),
-    }));
-
-    if (this.selection.nodeFront.customElementLocation) {
-      menu.append(new MenuItem({
-        type: "separator",
-      }));
-
-      menu.append(new MenuItem({
-        id: "node-menu-jumptodefinition",
-        label: INSPECTOR_L10N.getStr("inspectorCustomElementDefinition.label"),
-        click: () => this.jumpToCustomElementDefinition(),
-      }));
-    }
-
-    this.buildA11YMenuItem(menu);
-
-    const nodeLinkMenuItems = this._getNodeLinkMenuItems();
-    if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
-      menu.append(new MenuItem({
-        id: "node-menu-link-separator",
-        type: "separator",
-      }));
-    }
-
-    for (const menuitem of nodeLinkMenuItems) {
-      menu.append(menuitem);
-    }
-
-    menu.popup(screenX, screenY, this._toolbox);
-    return menu;
-  },
-
-  buildA11YMenuItem: function(menu) {
-    if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
-        !Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
-      return;
-    }
-
-    const showA11YPropsItem = new MenuItem({
-      id: "node-menu-showaccessibilityproperties",
-      label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
-      click: () => this.showAccessibilityProperties(),
-      disabled: true,
-    });
-    // Only attempt to determine if a11y props menu item needs to be enabled if
-    // AccessibilityFront is enabled.
-    if (this.accessibilityFront.enabled) {
-      this._updateA11YMenuItem(showA11YPropsItem);
-    }
-
-    menu.append(showA11YPropsItem);
-  },
-
-  _updateA11YMenuItem: async function(menuItem) {
-    const hasMethod = await this.target.actorHasMethod("domwalker",
-                                                       "hasAccessibilityProperties");
-    if (!hasMethod) {
-      return;
-    }
-
-    const hasA11YProps = await this.walker.hasAccessibilityProperties(
-      this.selection.nodeFront);
-    if (hasA11YProps) {
-      this._toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
-    }
-
-    this.emit("node-menu-updated");
-  },
-
-  _getCopySubmenu: function(markupContainer, isSelectionElement) {
-    const copySubmenu = new Menu();
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyinner",
-      label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyInnerHTML(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyouter",
-      label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyOuterHTML(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyuniqueselector",
-      label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyUniqueSelector(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copycsspath",
-      label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyCssPath(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyxpath",
-      label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyXPath(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyimagedatauri",
-      label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
-      disabled: !isSelectionElement || !markupContainer ||
-                !markupContainer.isPreviewable(),
-      click: () => this.copyImageDataUri(),
-    }));
-
-    return copySubmenu;
-  },
-
-  _getPasteSubmenu: function(isEditableElement) {
-    const isPasteable = isEditableElement && this._getClipboardContentForPaste();
-    const disableAdjacentPaste = !isPasteable || this.selection.isRoot() ||
-          this.selection.isBodyNode() || this.selection.isHeadNode();
-    const disableFirstLastPaste = !isPasteable ||
-          (this.selection.isHTMLNode() && this.selection.isRoot());
-
-    const pasteSubmenu = new Menu();
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteinnerhtml",
-      label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
-      disabled: !isPasteable,
-      click: () => this.pasteInnerHTML(),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteouterhtml",
-      label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
-      disabled: !isPasteable,
-      click: () => this.pasteOuterHTML(),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastebefore",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
-      disabled: disableAdjacentPaste,
-      click: () => this.pasteAdjacentHTML("beforeBegin"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteafter",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
-      disabled: disableAdjacentPaste,
-      click: () => this.pasteAdjacentHTML("afterEnd"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastefirstchild",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
-      disabled: disableFirstLastPaste,
-      click: () => this.pasteAdjacentHTML("afterBegin"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastelastchild",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
-      disabled: disableFirstLastPaste,
-      click: () => this.pasteAdjacentHTML("beforeEnd"),
-    }));
-
-    return pasteSubmenu;
-  },
-
-  _getAttributesSubmenu: function(isEditableElement) {
-    const attributesSubmenu = new Menu();
-    const nodeInfo = this.nodeMenuTriggerInfo;
-    const isAttributeClicked = isEditableElement && nodeInfo &&
-                              nodeInfo.type === "attribute";
-
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-add-attribute",
-      label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
-      disabled: !isEditableElement,
-      click: () => this.onAddAttribute(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-copy-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorCopyAttributeValue.label",
-                                        isAttributeClicked ? `${nodeInfo.value}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyAttributeValue.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onCopyAttributeValue(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-edit-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label",
-                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onEditAttribute(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-remove-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label",
-                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onRemoveAttribute(),
-    }));
-
-    return attributesSubmenu;
-  },
-
-  /**
-   * Link menu items can be shown or hidden depending on the context and
-   * selected node, and their labels can vary.
-   *
-   * @return {Array} list of visible menu items related to links.
-   */
-  _getNodeLinkMenuItems: function() {
-    const linkFollow = new MenuItem({
-      id: "node-menu-link-follow",
-      visible: false,
-      click: () => this.onFollowLink(),
-    });
-    const linkCopy = new MenuItem({
-      id: "node-menu-link-copy",
-      visible: false,
-      click: () => this.onCopyLink(),
-    });
-
-    // Get information about the right-clicked node.
-    const popupNode = this.contextMenuTarget;
-    if (!popupNode || !popupNode.classList.contains("link")) {
-      return [linkFollow, linkCopy];
-    }
-
-    const type = popupNode.dataset.type;
-    if ((type === "uri" || type === "cssresource" || type === "jsresource")) {
-      // Links can't be opened in new tabs in the browser toolbox.
-      if (type === "uri" && !this.target.chrome) {
-        linkFollow.visible = true;
-        linkFollow.label = INSPECTOR_L10N.getStr(
-          "inspector.menu.openUrlInNewTab.label");
-      } else if (type === "cssresource") {
-        linkFollow.visible = true;
-        linkFollow.label = TOOLBOX_L10N.getStr(
-          "toolbox.viewCssSourceInStyleEditor.label");
-      } else if (type === "jsresource") {
-        linkFollow.visible = true;
-        linkFollow.label = TOOLBOX_L10N.getStr(
-          "toolbox.viewJsSourceInDebugger.label");
-      }
-
-      linkCopy.visible = true;
-      linkCopy.label = INSPECTOR_L10N.getStr(
-        "inspector.menu.copyUrlToClipboard.label");
-    } else if (type === "idref") {
-      linkFollow.visible = true;
-      linkFollow.label = INSPECTOR_L10N.getFormatStr(
-        "inspector.menu.selectElement.label", popupNode.dataset.link);
-    }
-
-    return [linkFollow, linkCopy];
-  },
-
   _initMarkup: function() {
     if (!this._markupFrame) {
       this._markupFrame = this.panelDoc.createElement("iframe");
       this._markupFrame.setAttribute("aria-label",
         INSPECTOR_L10N.getStr("inspector.panelLabel.markupView"));
       this._markupFrame.setAttribute("flex", "1");
       // This is needed to enable tooltips inside the iframe document.
       this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
@@ -1908,17 +1452,16 @@ Inspector.prototype = {
       this._markupFrame.setAttribute("src", "markup/markup.xhtml");
     } else {
       this._onMarkupFrameLoad();
     }
   },
 
   _onMarkupFrameLoad: function() {
     this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
-    this._markupFrame.contentWindow.addEventListener("contextmenu", this._onContextMenu);
     this._markupFrame.contentWindow.focus();
     this._markupBox.style.visibility = "visible";
     this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
     this.emit("markuploaded");
   },
 
   _destroyMarkup: function() {
     let destroyPromise;
@@ -2024,251 +1567,16 @@ Inspector.prototype = {
 
       const hierarchical = pseudo == ":hover" || pseudo == ":active";
       return this.walker.addPseudoClassLock(node, pseudo, {parents: hierarchical});
     }
     return promise.resolve();
   },
 
   /**
-   * Show DOM properties
-   */
-  showDOMProperties: function() {
-    this._toolbox.openSplitConsole().then(() => {
-      const panel = this._toolbox.getPanel("webconsole");
-      const jsterm = panel.hud.jsterm;
-
-      jsterm.execute("inspect($0)");
-      jsterm.focus();
-    });
-  },
-
-  jumpToCustomElementDefinition: function() {
-    const node = this.selection.nodeFront;
-    const { url, line } = node.customElementLocation;
-    this._toolbox.viewSourceInDebugger(url, line, "show_custom_element");
-  },
-
-  /**
-   * Show Accessibility properties for currently selected node
-   */
-  async showAccessibilityProperties() {
-    const a11yPanel = await this._toolbox.selectTool("accessibility");
-    // Select the accessible object in the panel and wait for the event that
-    // tells us it has been done.
-    const onSelected = a11yPanel.once("new-accessible-front-selected");
-    a11yPanel.selectAccessibleForNode(this.selection.nodeFront,
-                                      "inspector-context-menu");
-    await onSelected;
-  },
-
-  /**
-   * Use in Console.
-   *
-   * Takes the currently selected node in the inspector and assigns it to a
-   * temp variable on the content window.  Also opens the split console and
-   * autofills it with the temp variable.
-   */
-  useInConsole: function() {
-    this._toolbox.openSplitConsole().then(() => {
-      const panel = this._toolbox.getPanel("webconsole");
-      const jsterm = panel.hud.jsterm;
-
-      const evalString = `{ let i = 0;
-        while (window.hasOwnProperty("temp" + i) && i < 1000) {
-          i++;
-        }
-        window["temp" + i] = $0;
-        "temp" + i;
-      }`;
-
-      const options = {
-        selectedNodeActor: this.selection.nodeFront.actorID,
-      };
-      jsterm.requestEvaluation(evalString, options).then((res) => {
-        jsterm.setInputValue(res.result);
-        this.emit("console-var-ready");
-      });
-    });
-  },
-
-  /**
-   * Edit the outerHTML of the selected Node.
-   */
-  editHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    if (this.markup) {
-      this.markup.beginEditingOuterHTML(this.selection.nodeFront);
-    }
-  },
-
-  /**
-   * Paste the contents of the clipboard into the selected Node's outer HTML.
-   */
-  pasteOuterHTML: function() {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.getNodeOuterHTML(node).then(oldContent => {
-      this.markup.updateNodeOuterHTML(node, content, oldContent);
-    });
-  },
-
-  /**
-   * Paste the contents of the clipboard into the selected Node's inner HTML.
-   */
-  pasteInnerHTML: function() {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.getNodeInnerHTML(node).then(oldContent => {
-      this.markup.updateNodeInnerHTML(node, content, oldContent);
-    });
-  },
-
-  /**
-   * Paste the contents of the clipboard as adjacent HTML to the selected Node.
-   * @param position
-   *        The position as specified for Element.insertAdjacentHTML
-   *        (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
-   */
-  pasteAdjacentHTML: function(position) {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.insertAdjacentHTMLToNode(node, position, content);
-  },
-
-  /**
-   * Copy the innerHTML of the selected Node to the clipboard.
-   */
-  copyInnerHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    this._copyLongString(this.walker.innerHTML(this.selection.nodeFront));
-  },
-
-  /**
-   * Copy the outerHTML of the selected Node to the clipboard.
-   */
-  copyOuterHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    const node = this.selection.nodeFront;
-
-    switch (node.nodeType) {
-      case nodeConstants.ELEMENT_NODE :
-        this._copyLongString(this.walker.outerHTML(node));
-        break;
-      case nodeConstants.COMMENT_NODE :
-        this._getLongString(node.getNodeValue()).then(comment => {
-          clipboardHelper.copyString("<!--" + comment + "-->");
-        });
-        break;
-      case nodeConstants.DOCUMENT_TYPE_NODE :
-        clipboardHelper.copyString(node.doctypeString);
-        break;
-    }
-  },
-
-  /**
-   * Copy the data-uri for the currently selected image in the clipboard.
-   */
-  copyImageDataUri: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    if (container && container.isPreviewable()) {
-      container.copyImageDataUri();
-    }
-  },
-
-  /**
-   * Copy the content of a longString (via a promise resolving a
-   * LongStringActor) to the clipboard
-   * @param  {Promise} longStringActorPromise
-   *         promise expected to resolve a LongStringActor instance
-   * @return {Promise} promise resolving (with no argument) when the
-   *         string is sent to the clipboard
-   */
-  _copyLongString: function(longStringActorPromise) {
-    return this._getLongString(longStringActorPromise).then(string => {
-      clipboardHelper.copyString(string);
-    }).catch(console.error);
-  },
-
-  /**
-   * Retrieve the content of a longString (via a promise resolving a LongStringActor)
-   * @param  {Promise} longStringActorPromise
-   *         promise expected to resolve a LongStringActor instance
-   * @return {Promise} promise resolving with the retrieved string as argument
-   */
-  _getLongString: function(longStringActorPromise) {
-    return longStringActorPromise.then(longStringActor => {
-      return longStringActor.string().then(string => {
-        longStringActor.release().catch(console.error);
-        return string;
-      });
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy a unique selector of the selected Node to the clipboard.
-   */
-  copyUniqueSelector: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.unique.css.selector.opened", 1);
-    this.selection.nodeFront.getUniqueSelector().then(selector => {
-      clipboardHelper.copyString(selector);
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy the full CSS Path of the selected Node to the clipboard.
-   */
-  copyCssPath: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.full.css.selector.opened", 1);
-    this.selection.nodeFront.getCssPath().then(path => {
-      clipboardHelper.copyString(path);
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy the XPath of the selected Node to the clipboard.
-   */
-  copyXPath: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.xpath.opened", 1);
-    this.selection.nodeFront.getXPath().then(path => {
-      clipboardHelper.copyString(path);
-    }).catch(console.error);
-  },
-
-  /**
    * Initiate screenshot command on selected node.
    */
   async screenshotNode() {
     // Bug 1332936 - it's possible to call `screenshotNode` while the BoxModel highlighter
     // is still visible, therefore showing it in the picture.
     // To avoid that, we have to hide it before taking the screenshot. The `hideBoxModel`
     // will do that, calling `hide` for the highlighter only if previously shown.
     await this.highlighter.hideBoxModel();
@@ -2281,170 +1589,16 @@ Inspector.prototype = {
       clipboard: clipboardEnabled,
     };
     const screenshotFront = await this.target.getFront("screenshot");
     const screenshot = await screenshotFront.capture(args);
     await saveScreenshot(this.panelWin, args, screenshot);
   },
 
   /**
-   * Scroll the node into view.
-   */
-  scrollNodeIntoView: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.selection.nodeFront.scrollIntoView();
-  },
-
-  /**
-   * Duplicate the selected node
-   */
-  duplicateNode: function() {
-    const selection = this.selection;
-    if (!selection.isElementNode() ||
-        selection.isRoot() ||
-        selection.isAnonymousNode() ||
-        selection.isPseudoElementNode()) {
-      return;
-    }
-    this.walker.duplicateNode(selection.nodeFront).catch(console.error);
-  },
-
-  /**
-   * Delete the selected node.
-   */
-  deleteNode: function() {
-    if (!this.selection.isNode() ||
-         this.selection.isRoot()) {
-      return;
-    }
-
-    // If the markup panel is active, use the markup panel to delete
-    // the node, making this an undoable action.
-    if (this.markup) {
-      this.markup.deleteNode(this.selection.nodeFront);
-    } else {
-      // remove the node from content
-      this.walker.removeNode(this.selection.nodeFront);
-    }
-  },
-
-  /**
-   * Add attribute to node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onAddAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.addAttribute();
-  },
-
-  /**
-   * Copy attribute value for node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onCopyAttributeValue: function() {
-    clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
-  },
-
-  /**
-   * Edit attribute for node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onEditAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.editAttribute(this.nodeMenuTriggerInfo.name);
-  },
-
-  /**
-   * Remove attribute from node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onRemoveAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.removeAttribute(this.nodeMenuTriggerInfo.name);
-  },
-
-  expandNode: function() {
-    this.markup.expandAll(this.selection.nodeFront);
-  },
-
-  collapseAll: function() {
-    this.markup.collapseAll(this.selection.nodeFront);
-  },
-
-  /**
-   * This method is here for the benefit of the node-menu-link-follow menu item
-   * in the inspector contextual-menu.
-   */
-  onFollowLink: function() {
-    const type = this.contextMenuTarget.dataset.type;
-    const link = this.contextMenuTarget.dataset.link;
-
-    this.followAttributeLink(type, link);
-  },
-
-  /**
-   * Given a type and link found in a node's attribute in the markup-view,
-   * attempt to follow that link (which may result in opening a new tab, the
-   * style editor or debugger).
-   */
-  followAttributeLink: function(type, link) {
-    if (!type || !link) {
-      return;
-    }
-
-    if (type === "uri" || type === "cssresource" || type === "jsresource") {
-      // Open link in a new tab.
-      this.inspector.resolveRelativeURL(
-        link, this.selection.nodeFront).then(url => {
-          if (type === "uri") {
-            openContentLink(url);
-          } else if (type === "cssresource") {
-            return this.toolbox.viewSourceInStyleEditor(url);
-          } else if (type === "jsresource") {
-            return this.toolbox.viewSourceInDebugger(url);
-          }
-          return null;
-        }).catch(console.error);
-    } else if (type == "idref") {
-      // Select the node in the same document.
-      this.walker.document(this.selection.nodeFront).then(doc => {
-        return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
-          if (!node) {
-            this.emit("idref-attribute-link-failed");
-            return;
-          }
-          this.selection.setNodeFront(node);
-        });
-      }).catch(console.error);
-    }
-  },
-
-  /**
-   * This method is here for the benefit of the node-menu-link-copy menu item
-   * in the inspector contextual-menu.
-   */
-  onCopyLink: function() {
-    const link = this.contextMenuTarget.dataset.link;
-
-    this.copyAttributeLink(link);
-  },
-
-  /**
-   * This method is here for the benefit of copying links.
-   */
-  copyAttributeLink: function(link) {
-    this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
-      clipboardHelper.copyString(url);
-    }, console.error);
-  },
-
-  /**
    * Returns an object containing the shared handler functions used in the box
    * model and grid React components.
    */
   getCommonComponentProps() {
     return {
       setSelectedNode: this.selection.setNodeFront,
       onShowBoxModelHighlighterForNode: this.onShowBoxModelHighlighterForNode,
     };
@@ -2459,28 +1613,16 @@ Inspector.prototype = {
    * @param  {Object} options
    *         Options passed to the highlighter actor.
    */
   onShowBoxModelHighlighterForNode(nodeFront, options) {
     const toolbox = this.toolbox;
     toolbox.highlighter.highlight(nodeFront, options);
   },
 
-  /**
-   * Returns a value indicating whether a node can be deleted.
-   *
-   * @param {NodeFront} nodeFront
-   *        The node to test for deletion
-   */
-  isDeletable(nodeFront) {
-    return !(nodeFront.isDocumentElement ||
-           nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
-           nodeFront.isAnonymous);
-  },
-
   async inspectNodeActor(nodeActor, inspectFromAnnotation) {
     const nodeFront = await this.walker.gripToNodeFront({ actor: nodeActor });
     if (!nodeFront) {
       console.error("The object cannot be linked to the inspector, the " +
                     "corresponding nodeFront could not be found.");
       return false;
     }
 
copy from devtools/client/inspector/inspector.js
copy to devtools/client/inspector/markup/markup-context-menu.js
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/markup/markup-context-menu.js
@@ -1,1490 +1,594 @@
 /* -*- 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/. */
 
-/* global window, BrowserLoader */
-
 "use strict";
 
 const Services = require("Services");
 const promise = require("promise");
-const EventEmitter = require("devtools/shared/event-emitter");
-const {executeSoon} = require("devtools/shared/DevToolsUtils");
-const {Toolbox} = require("devtools/client/framework/toolbox");
-const ReflowTracker = require("devtools/client/inspector/shared/reflow-tracker");
-const Store = require("devtools/client/inspector/store");
-const InspectorStyleChangeTracker = require("devtools/client/inspector/shared/style-change-tracker");
+const { LocalizationHelper } = require("devtools/shared/l10n");
 
-// Use privileged promise in panel documents to prevent having them to freeze
-// during toolbox destruction. See bug 1402779.
-const Promise = require("Promise");
-
-loader.lazyRequireGetter(this, "initCssProperties", "devtools/shared/fronts/css-properties", true);
-loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true);
-loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts");
-loader.lazyRequireGetter(this, "InspectorSearch", "devtools/client/inspector/inspector-search", true);
-loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/inspector/toolsidebar", true);
-loader.lazyRequireGetter(this, "MarkupView", "devtools/client/inspector/markup/markup");
-loader.lazyRequireGetter(this, "HighlightersOverlay", "devtools/client/inspector/shared/highlighters-overlay");
-loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
 loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
 loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
-loader.lazyRequireGetter(this, "ExtensionSidebar", "devtools/client/inspector/extensions/extension-sidebar");
+loader.lazyRequireGetter(this, "copyLongString", "devtools/client/inspector/shared/utils", true);
 loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
-loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
-loader.lazyRequireGetter(this, "saveScreenshot", "devtools/shared/screenshot/save");
 
-loader.lazyImporter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
-
-const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
-const INSPECTOR_L10N =
-  new LocalizationHelper("devtools/client/locales/inspector.properties");
 loader.lazyGetter(this, "TOOLBOX_L10N", function() {
   return new LocalizationHelper("devtools/client/locales/toolbox.properties");
 });
 
-// Sidebar dimensions
-const INITIAL_SIDEBAR_SIZE = 350;
-
-// How long we wait to debounce resize events
-const LAZY_RESIZE_INTERVAL_MS = 200;
-
-// If the toolbox's width is smaller than the given amount of pixels, the sidebar
-// automatically switches from 'landscape/horizontal' to 'portrait/vertical' mode.
-const PORTRAIT_MODE_WIDTH_THRESHOLD = 700;
-// If the toolbox's width docked to the side is smaller than the given amount of pixels,
-// the sidebar automatically switches from 'landscape/horizontal' to 'portrait/vertical'
-// mode.
-const SIDE_PORTAIT_MODE_WIDTH_THRESHOLD = 1000;
-
-const THREE_PANE_ENABLED_PREF = "devtools.inspector.three-pane-enabled";
-const THREE_PANE_ENABLED_SCALAR = "devtools.inspector.three_pane_enabled";
-const THREE_PANE_CHROME_ENABLED_PREF = "devtools.inspector.chrome.three-pane-enabled";
-const TELEMETRY_EYEDROPPER_OPENED = "devtools.toolbar.eyedropper.opened";
-const TRACK_CHANGES_PREF = "devtools.inspector.changes.enabled";
+const INSPECTOR_L10N =
+  new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 /**
- * Represents an open instance of the Inspector for a tab.
- * The inspector controls the breadcrumbs, the markup view, and the sidebar
- * (computed view, rule view, font view and animation inspector).
- *
- * Events:
- * - ready
- *      Fired when the inspector panel is opened for the first time and ready to
- *      use
- * - new-root
- *      Fired after a new root (navigation to a new page) event was fired by
- *      the walker, and taken into account by the inspector (after the markup
- *      view has been reloaded)
- * - markuploaded
- *      Fired when the markup-view frame has loaded
- * - breadcrumbs-updated
- *      Fired when the breadcrumb widget updates to a new node
- * - boxmodel-view-updated
- *      Fired when the box model updates to a new node
- * - markupmutation
- *      Fired after markup mutations have been processed by the markup-view
- * - computed-view-refreshed
- *      Fired when the computed rules view updates to a new node
- * - computed-view-property-expanded
- *      Fired when a property is expanded in the computed rules view
- * - computed-view-property-collapsed
- *      Fired when a property is collapsed in the computed rules view
- * - computed-view-sourcelinks-updated
- *      Fired when the stylesheet source links have been updated (when switching
- *      to source-mapped files)
- * - rule-view-refreshed
- *      Fired when the rule view updates to a new node
- * - rule-view-sourcelinks-updated
- *      Fired when the stylesheet source links have been updated (when switching
- *      to source-mapped files)
+ * Context menu for the Markup view.
  */
-function Inspector(toolbox) {
-  EventEmitter.decorate(this);
-
-  this._toolbox = toolbox;
-  this._target = toolbox.target;
-  this.panelDoc = window.document;
-  this.panelWin = window;
-  this.panelWin.inspector = this;
-  this.telemetry = toolbox.telemetry;
-  this.store = Store();
-
-  this._markupBox = this.panelDoc.getElementById("markup-box");
-
-  // Map [panel id => panel instance]
-  // Stores all the instances of sidebar panels like rule view, computed view, ...
-  this._panels = new Map();
-
-  this.reflowTracker = new ReflowTracker(this._target);
-  this.styleChangeTracker = new InspectorStyleChangeTracker(this);
-
-  // Store the URL of the target page prior to navigation in order to ensure
-  // telemetry counts in the Grid Inspector are not double counted on reload.
-  this.previousURL = this.target.url;
-
-  this.nodeMenuTriggerInfo = null;
-
-  this._clearSearchResultsLabel = this._clearSearchResultsLabel.bind(this);
-  this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
-  this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
-  this._onContextMenu = this._onContextMenu.bind(this);
-  this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
-  this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
-
-  this.onDetached = this.onDetached.bind(this);
-  this.onHostChanged = this.onHostChanged.bind(this);
-  this.onMarkupLoaded = this.onMarkupLoaded.bind(this);
-  this.onNewSelection = this.onNewSelection.bind(this);
-  this.onNewRoot = this.onNewRoot.bind(this);
-  this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
-  this.onShowBoxModelHighlighterForNode =
-    this.onShowBoxModelHighlighterForNode.bind(this);
-  this.onSidebarHidden = this.onSidebarHidden.bind(this);
-  this.onSidebarResized = this.onSidebarResized.bind(this);
-  this.onSidebarSelect = this.onSidebarSelect.bind(this);
-  this.onSidebarShown = this.onSidebarShown.bind(this);
-  this.onSidebarToggle = this.onSidebarToggle.bind(this);
-
-  this._target.on("will-navigate", this._onBeforeNavigate);
-}
-
-Inspector.prototype = {
-  /**
-   * open is effectively an asynchronous constructor
-   */
-  async init() {
-    // Localize all the nodes containing a data-localization attribute.
-    localizeMarkup(this.panelDoc);
-
-    await Promise.all([
-      this._getCssProperties(),
-      this._getPageStyle(),
-      this._getDefaultSelection(),
-      this._getAccessibilityFront(),
-    ]);
-
-    return this._deferredOpen();
-  },
-
-  get toolbox() {
-    return this._toolbox;
-  },
-
-  get inspector() {
-    return this.toolbox.inspector;
-  },
+class MarkupContextMenu {
+  constructor(markup) {
+    this.markup = markup;
+    this.inspector = markup.inspector;
+    this.selection = this.inspector.selection;
+    this.target = this.inspector.target;
+    this.telemetry = this.inspector.telemetry;
+    this.toolbox = this.inspector.toolbox;
+    this.walker = this.inspector.walker;
+  }
 
-  get walker() {
-    return this.toolbox.walker;
-  },
-
-  get selection() {
-    return this.toolbox.selection;
-  },
-
-  get highlighter() {
-    return this.toolbox.highlighter;
-  },
-
-  get highlighters() {
-    if (!this._highlighters) {
-      this._highlighters = new HighlightersOverlay(this);
-    }
-
-    return this._highlighters;
-  },
-
-  get isHighlighterReady() {
-    return !!this._highlighters;
-  },
-
-  get is3PaneModeEnabled() {
-    if (this.target.chrome) {
-      if (!this._is3PaneModeChromeEnabled) {
-        this._is3PaneModeChromeEnabled = Services.prefs.getBoolPref(
-          THREE_PANE_CHROME_ENABLED_PREF);
-      }
-
-      return this._is3PaneModeChromeEnabled;
-    }
-
-    if (!this._is3PaneModeEnabled) {
-      this._is3PaneModeEnabled = Services.prefs.getBoolPref(THREE_PANE_ENABLED_PREF);
-    }
-
-    return this._is3PaneModeEnabled;
-  },
-
-  set is3PaneModeEnabled(value) {
-    if (this.target.chrome) {
-      this._is3PaneModeChromeEnabled = value;
-      Services.prefs.setBoolPref(THREE_PANE_CHROME_ENABLED_PREF,
-        this._is3PaneModeChromeEnabled);
-    } else {
-      this._is3PaneModeEnabled = value;
-      Services.prefs.setBoolPref(THREE_PANE_ENABLED_PREF, this._is3PaneModeEnabled);
-    }
-  },
-
-  get notificationBox() {
-    if (!this._notificationBox) {
-      this._notificationBox = this.toolbox.getNotificationBox();
-    }
-
-    return this._notificationBox;
-  },
-
-  get search() {
-    if (!this._search) {
-      this._search = new InspectorSearch(this, this.searchBox, this.searchClearButton);
-    }
-
-    return this._search;
-  },
-
-  get cssProperties() {
-    return this._cssProperties.cssProperties;
-  },
-
-  /**
-   * Check if the changes panel is enabled and supported by the server.
-   */
-  _supportsChangesPanel() {
-    // The changes actor was introduced in Fx65, we are checking this for backward
-    // compatibility when connecting to an older server. Can be removed once Fx65 hit the
-    // release channel.
-    return this._target.hasActor("changes") &&
-      Services.prefs.getBoolPref(TRACK_CHANGES_PREF);
-  },
-
-  /**
-   * Handle promise rejections for various asynchronous actions, and only log errors if
-   * the inspector panel still exists.
-   * This is useful to silence useless errors that happen when the inspector is closed
-   * while still initializing (and making protocol requests).
-   */
-  _handleRejectionIfNotDestroyed: function(e) {
-    if (!this._panelDestroyer) {
-      console.error(e);
-    }
-  },
-
-  _deferredOpen: async function() {
-    this._initMarkup();
-    this.isReady = false;
-
-    // Set the node front so that the markup and sidebar panels will have the selected
-    // nodeFront ready when they're initialized.
-    if (this._defaultNode) {
-      this.selection.setNodeFront(this._defaultNode, { reason: "inspector-open" });
-    }
-
-    if (this._supportsChangesPanel()) {
-      // Get the Changes front, then call a method on it, which will instantiate
-      // the ChangesActor. We want the ChangesActor to be guaranteed available before
-      // the user makes any changes.
-      this.changesFront = await this.toolbox.target.getFront("changes");
-      this.changesFront.start();
+  show(event) {
+    if (!(event.originalTarget instanceof Element) ||
+        event.originalTarget.closest("input[type=text]") ||
+        event.originalTarget.closest("input:not([type])") ||
+        event.originalTarget.closest("textarea")) {
+      return;
     }
 
-    // Setup the splitter before the sidebar is displayed so, we don't miss any events.
-    this.setupSplitter();
-
-    // We can display right panel with: tab bar, markup view and breadbrumb. Right after
-    // the splitter set the right and left panel sizes, in order to avoid resizing it
-    // during load of the inspector.
-    this.panelDoc.getElementById("inspector-main-content").style.visibility = "visible";
-
-    // Setup the sidebar panels.
-    this.setupSidebar();
-
-    await this.once("markuploaded");
-    this.isReady = true;
-
-    // All the components are initialized. Take care of the remaining initialization
-    // and setup.
-    this.breadcrumbs = new HTMLBreadcrumbs(this);
-    this.setupExtensionSidebars();
-    this.setupSearchBox();
-    await this.setupToolbar();
-
-    this.onNewSelection();
-
-    this.walker.on("new-root", this.onNewRoot);
-    this.toolbox.on("host-changed", this.onHostChanged);
-    this.selection.on("new-node-front", this.onNewSelection);
-    this.selection.on("detached-front", this.onDetached);
-
-    // Log the 3 pane inspector setting on inspector open. The question we want to answer
-    // is:
-    // "What proportion of users use the 3 pane vs 2 pane inspector on inspector open?"
-    this.telemetry.keyedScalarAdd(THREE_PANE_ENABLED_SCALAR, this.is3PaneModeEnabled, 1);
+    event.stopPropagation();
+    event.preventDefault();
 
-    this.emit("ready");
-    return this;
-  },
-
-  _onBeforeNavigate: function() {
-    this._defaultNode = null;
-    this.selection.setNodeFront(null);
-    this._destroyMarkup();
-    this._pendingSelection = null;
-  },
-
-  _getCssProperties: function() {
-    return initCssProperties(this.toolbox).then(cssProperties => {
-      this._cssProperties = cssProperties;
-    }, this._handleRejectionIfNotDestroyed);
-  },
-
-  _getAccessibilityFront: async function() {
-    this.accessibilityFront = await this.target.getFront("accessibility");
-    return this.accessibilityFront;
-  },
-
-  _getDefaultSelection: function() {
-    // This may throw if the document is still loading and we are
-    // refering to a dead about:blank document
-    return this._getDefaultNodeForSelection().catch(this._handleRejectionIfNotDestroyed);
-  },
-
-  _getPageStyle: function() {
-    return this.inspector.getPageStyle().then(pageStyle => {
-      this.pageStyle = pageStyle;
-    }, this._handleRejectionIfNotDestroyed);
-  },
+    this._openMenu({
+      screenX: event.screenX,
+      screenY: event.screenY,
+      target: event.target,
+    });
+  }
 
   /**
-   * Return a promise that will resolve to the default node for selection.
+   * This method is here for the benefit of copying links.
    */
-  _getDefaultNodeForSelection: function() {
-    if (this._defaultNode) {
-      return this._defaultNode;
-    }
-    const walker = this.walker;
-    let rootNode = null;
-    const pendingSelection = this._pendingSelection;
-
-    // A helper to tell if the target has or is about to navigate.
-    // this._pendingSelection changes on "will-navigate" and "new-root" events.
-    const hasNavigated = () => pendingSelection !== this._pendingSelection;
-
-    // If available, set either the previously selected node or the body
-    // as default selected, else set documentElement
-    return walker.getRootNode().then(node => {
-      if (hasNavigated()) {
-        return promise.reject("navigated; resolution of _defaultNode aborted");
-      }
-
-      rootNode = node;
-      if (this.selectionCssSelector) {
-        return walker.querySelector(rootNode, this.selectionCssSelector);
-      }
-      return null;
-    }).then(front => {
-      if (hasNavigated()) {
-        return promise.reject("navigated; resolution of _defaultNode aborted");
-      }
-
-      if (front) {
-        return front;
-      }
-      return walker.querySelector(rootNode, "body");
-    }).then(front => {
-      if (hasNavigated()) {
-        return promise.reject("navigated; resolution of _defaultNode aborted");
-      }
-
-      if (front) {
-        return front;
-      }
-      return this.walker.documentElement();
-    }).then(node => {
-      if (hasNavigated()) {
-        return promise.reject("navigated; resolution of _defaultNode aborted");
-      }
-      this._defaultNode = node;
-      return node;
-    });
-  },
-
-  /**
-   * Target getter.
-   */
-  get target() {
-    return this._target;
-  },
-
-  /**
-   * Target setter.
-   */
-  set target(value) {
-    this._target = value;
-  },
+  _copyAttributeLink(link) {
+    this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
+      clipboardHelper.copyString(url);
+    }, console.error);
+  }
 
   /**
-   * Hooks the searchbar to show result and auto completion suggestions.
+   * Copy the full CSS Path of the selected Node to the clipboard.
    */
-  setupSearchBox: function() {
-    this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
-    this.searchClearButton = this.panelDoc.getElementById("inspector-searchinput-clear");
-    this.searchResultsLabel = this.panelDoc.getElementById("inspector-searchlabel");
-
-    this.searchBox.addEventListener("focus", () => {
-      this.search.on("search-cleared", this._clearSearchResultsLabel);
-      this.search.on("search-result", this._updateSearchResultsLabel);
-    }, { once: true });
-
-    const shortcuts = new KeyShortcuts({
-      window: this.panelDoc.defaultView,
-    });
-    const key = INSPECTOR_L10N.getStr("inspector.searchHTML.key");
-    shortcuts.on(key, event => {
-      // Prevent overriding same shortcut from the computed/rule views
-      if (event.target.closest("#sidebar-panel-ruleview") ||
-          event.target.closest("#sidebar-panel-computedview")) {
-        return;
-      }
-      event.preventDefault();
-      this.searchBox.focus();
-    });
-  },
-
-  get searchSuggestions() {
-    return this.search.autocompleter;
-  },
-
-  _clearSearchResultsLabel: function(result) {
-    return this._updateSearchResultsLabel(result, true);
-  },
-
-  _updateSearchResultsLabel: function(result, clear = false) {
-    let str = "";
-    if (!clear) {
-      if (result) {
-        str = INSPECTOR_L10N.getFormatStr(
-          "inspector.searchResultsCount2", result.resultsIndex + 1, result.resultsLength);
-      } else {
-        str = INSPECTOR_L10N.getStr("inspector.searchResultsNone");
-      }
+  _copyCssPath() {
+    if (!this.selection.isNode()) {
+      return;
     }
 
-    this.searchResultsLabel.textContent = str;
-  },
-
-  get React() {
-    return this._toolbox.React;
-  },
-
-  get ReactDOM() {
-    return this._toolbox.ReactDOM;
-  },
-
-  get ReactRedux() {
-    return this._toolbox.ReactRedux;
-  },
-
-  get browserRequire() {
-    return this._toolbox.browserRequire;
-  },
-
-  get InspectorTabPanel() {
-    if (!this._InspectorTabPanel) {
-      this._InspectorTabPanel =
-        this.React.createFactory(this.browserRequire(
-        "devtools/client/inspector/components/InspectorTabPanel"));
-    }
-    return this._InspectorTabPanel;
-  },
-
-  get InspectorSplitBox() {
-    if (!this._InspectorSplitBox) {
-      this._InspectorSplitBox = this.React.createFactory(this.browserRequire(
-        "devtools/client/shared/components/splitter/SplitBox"));
-    }
-    return this._InspectorSplitBox;
-  },
-
-  get TabBar() {
-    if (!this._TabBar) {
-      this._TabBar = this.React.createFactory(this.browserRequire(
-        "devtools/client/shared/components/tabs/TabBar"));
-    }
-    return this._TabBar;
-  },
+    this.telemetry.scalarSet("devtools.copy.full.css.selector.opened", 1);
+    this.selection.nodeFront.getCssPath().then(path => {
+      clipboardHelper.copyString(path);
+    }).catch(console.error);
+  }
 
   /**
-   * Check if the inspector should use the landscape mode.
-   *
-   * @return {Boolean} true if the inspector should be in landscape mode.
+   * Copy the data-uri for the currently selected image in the clipboard.
    */
-  useLandscapeMode: function() {
-    if (!this.panelDoc) {
-      return true;
+  _copyImageDataUri() {
+    const container = this.markup.getContainer(this.selection.nodeFront);
+    if (container && container.isPreviewable()) {
+      container.copyImageDataUri();
     }
-
-    const splitterBox = this.panelDoc.getElementById("inspector-splitter-box");
-    const { width } = window.windowUtils.getBoundsWithoutFlushing(splitterBox);
-
-    return this.is3PaneModeEnabled &&
-           (this.toolbox.hostType == Toolbox.HostType.LEFT ||
-            this.toolbox.hostType == Toolbox.HostType.RIGHT) ?
-      width > SIDE_PORTAIT_MODE_WIDTH_THRESHOLD :
-      width > PORTRAIT_MODE_WIDTH_THRESHOLD;
-  },
+  }
 
   /**
-   * Build Splitter located between the main and side area of
-   * the Inspector panel.
+   * Copy the innerHTML of the selected Node to the clipboard.
    */
-  setupSplitter: function() {
-    const { width, height, splitSidebarWidth } = this.getSidebarSize();
-
-    const splitter = this.InspectorSplitBox({
-      className: "inspector-sidebar-splitter",
-      initialWidth: width,
-      initialHeight: height,
-      minSize: "10%",
-      maxSize: "80%",
-      splitterSize: 1,
-      endPanelControl: true,
-      startPanel: this.InspectorTabPanel({
-        id: "inspector-main-content",
-      }),
-      endPanel: this.InspectorSplitBox({
-        initialWidth: splitSidebarWidth,
-        minSize: 10,
-        maxSize: "80%",
-        splitterSize: this.is3PaneModeEnabled ? 1 : 0,
-        endPanelControl: this.is3PaneModeEnabled,
-        startPanel: this.InspectorTabPanel({
-          id: "inspector-rules-container",
-        }),
-        endPanel: this.InspectorTabPanel({
-          id: "inspector-sidebar-container",
-        }),
-        ref: splitbox => {
-          this.sidebarSplitBox = splitbox;
-        },
-      }),
-      vert: this.useLandscapeMode(),
-      onControlledPanelResized: this.onSidebarResized,
-    });
-
-    this.splitBox = this.ReactDOM.render(splitter,
-      this.panelDoc.getElementById("inspector-splitter-box"));
-
-    this.panelWin.addEventListener("resize", this.onPanelWindowResize, true);
-  },
-
-  _onLazyPanelResize: async function() {
-    // We can be called on a closed window because of the deferred task.
-    if (window.closed) {
+  _copyInnerHTML() {
+    if (!this.selection.isNode()) {
       return;
     }
 
-    this.splitBox.setState({ vert: this.useLandscapeMode() });
-    this.emit("inspector-resize");
-  },
+    copyLongString(this.walker.innerHTML(this.selection.nodeFront));
+  }
+
+  /**
+   * Copy the outerHTML of the selected Node to the clipboard.
+   */
+  _copyOuterHTML() {
+    if (!this.selection.isNode()) {
+      return;
+    }
+
+    this.markup.copyOuterHTML();
+  }
+
+  /**
+   * Copy a unique selector of the selected Node to the clipboard.
+   */
+  _copyUniqueSelector() {
+    if (!this.selection.isNode()) {
+      return;
+    }
+
+    this.telemetry.scalarSet("devtools.copy.unique.css.selector.opened", 1);
+    this.selection.nodeFront.getUniqueSelector().then(selector => {
+      clipboardHelper.copyString(selector);
+    }).catch(console.error);
+  }
 
   /**
-   * If Toolbox width is less than 600 px, the splitter changes its mode
-   * to `horizontal` to support portrait view.
+   * Copy the XPath of the selected Node to the clipboard.
+   */
+  _copyXPath() {
+    if (!this.selection.isNode()) {
+      return;
+    }
+
+    this.telemetry.scalarSet("devtools.copy.xpath.opened", 1);
+    this.selection.nodeFront.getXPath().then(path => {
+      clipboardHelper.copyString(path);
+    }).catch(console.error);
+  }
+
+  /**
+   * Delete the selected node.
    */
-  onPanelWindowResize: function() {
-    if (this.toolbox.currentToolId !== "inspector") {
+  _deleteNode() {
+    if (!this.selection.isNode() ||
+         this.selection.isRoot()) {
+      return;
+    }
+
+    // If the markup panel is active, use the markup panel to delete
+    // the node, making this an undoable action.
+    if (this.markup) {
+      this.markup.deleteNode(this.selection.nodeFront);
+    } else {
+      // remove the node from content
+      this.walker.removeNode(this.selection.nodeFront);
+    }
+  }
+
+  /**
+   * Duplicate the selected node
+   */
+  _duplicateNode() {
+    if (!this.selection.isElementNode() ||
+        this.selection.isRoot() ||
+        this.selection.isAnonymousNode() ||
+        this.selection.isPseudoElementNode()) {
+      return;
+    }
+
+    this.walker.duplicateNode(this.selection.nodeFront).catch(console.error);
+  }
+
+  /**
+   * Edit the outerHTML of the selected Node.
+   */
+  _editHTML() {
+    if (!this.selection.isNode()) {
       return;
     }
 
-    if (!this._lazyResizeHandler) {
-      this._lazyResizeHandler = new DeferredTask(this._onLazyPanelResize.bind(this),
-                                                 LAZY_RESIZE_INTERVAL_MS, 0);
-    }
-    this._lazyResizeHandler.arm();
-  },
-
-  getSidebarSize: function() {
-    let width;
-    let height;
-    let splitSidebarWidth;
+    this.markup.beginEditingOuterHTML(this.selection.nodeFront);
+  }
 
-    // Initialize splitter size from preferences.
-    try {
-      width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector");
-      height = Services.prefs.getIntPref("devtools.toolsidebar-height.inspector");
-      splitSidebarWidth = Services.prefs.getIntPref(
-        "devtools.toolsidebar-width.inspector.splitsidebar");
-    } catch (e) {
-      // Set width and height of the splitter. Only one
-      // value is really useful at a time depending on the current
-      // orientation (vertical/horizontal).
-      // Having both is supported by the splitter component.
-      width = this.is3PaneModeEnabled ?
-        INITIAL_SIDEBAR_SIZE * 2 : INITIAL_SIDEBAR_SIZE;
-      height = INITIAL_SIDEBAR_SIZE;
-      splitSidebarWidth = INITIAL_SIDEBAR_SIZE;
-    }
-
-    return { width, height, splitSidebarWidth };
-  },
+  /**
+   * Jumps to the custom element definition in the debugger.
+   */
+  _jumpToCustomElementDefinition() {
+    const { url, line } = this.selection.nodeFront.customElementLocation;
+    this.toolbox.viewSourceInDebugger(url, line, "show_custom_element");
+  }
 
-  onSidebarHidden: function() {
-    // Store the current splitter size to preferences.
-    const state = this.splitBox.state;
-    Services.prefs.setIntPref("devtools.toolsidebar-width.inspector", state.width);
-    Services.prefs.setIntPref("devtools.toolsidebar-height.inspector", state.height);
-    Services.prefs.setIntPref("devtools.toolsidebar-width.inspector.splitsidebar",
-      this.sidebarSplitBox.state.width);
-  },
-
-  onSidebarResized: function(width, height) {
-    this.toolbox.emit("inspector-sidebar-resized", { width, height });
-  },
-
-  onSidebarSelect: function(toolId) {
-    // Save the currently selected sidebar panel
-    Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
+  /**
+   * Add attribute to node.
+   * Used for node context menu and shouldn't be called directly.
+   */
+  _onAddAttribute() {
+    const container = this.markup.getContainer(this.selection.nodeFront);
+    container.addAttribute();
+  }
 
-    // Then forces the panel creation by calling getPanel
-    // (This allows lazy loading the panels only once we select them)
-    this.getPanel(toolId);
-
-    this.toolbox.emit("inspector-sidebar-select", toolId);
-  },
+  /**
+   * Copy attribute value for node.
+   * Used for node context menu and shouldn't be called directly.
+   */
+  _onCopyAttributeValue() {
+    clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
+  }
 
-  onSidebarShown: function() {
-    const { width, height, splitSidebarWidth } = this.getSidebarSize();
-    this.splitBox.setState({ width, height });
-    this.sidebarSplitBox.setState({ width: splitSidebarWidth });
-  },
-
-  async onSidebarToggle() {
-    this.is3PaneModeEnabled = !this.is3PaneModeEnabled;
-    await this.setupToolbar();
-    await this.addRuleView({ skipQueue: true });
-  },
+  /**
+   * This method is here for the benefit of the node-menu-link-copy menu item
+   * in the inspector contextual-menu.
+   */
+  _onCopyLink() {
+    this.copyAttributeLink(this.contextMenuTarget.dataset.link);
+  }
 
   /**
-   * Sets the inspector sidebar split box state. Shows the splitter inside the sidebar
-   * split box, specifies the end panel control and resizes the split box width depending
-   * on the width of the toolbox.
+   * Edit attribute for node.
+   * Used for node context menu and shouldn't be called directly.
    */
-  setSidebarSplitBoxState() {
-    const toolboxWidth =
-      this.panelDoc.getElementById("inspector-splitter-box").clientWidth;
-
-    // Get the inspector sidebar's (right panel in horizontal mode or bottom panel in
-    // vertical mode) width.
-    const sidebarWidth = this.splitBox.state.width;
-    // This variable represents the width of the right panel in horizontal mode or
-    // bottom-right panel in vertical mode width in 3 pane mode.
-    let sidebarSplitboxWidth;
-
-    if (this.useLandscapeMode()) {
-      // Whether or not doubling the inspector sidebar's (right panel in horizontal mode
-      // or bottom panel in vertical mode) width will be bigger than half of the
-      // toolbox's width.
-      const canDoubleSidebarWidth = (sidebarWidth * 2) < (toolboxWidth / 2);
+  _onEditAttribute() {
+    const container = this.markup.getContainer(this.selection.nodeFront);
+    container.editAttribute(this.nodeMenuTriggerInfo.name);
+  }
 
-      // Resize the main split box's end panel that contains the middle and right panel.
-      // Attempts to resize the main split box's end panel to be double the size of the
-      // existing sidebar's width when switching to 3 pane mode. However, if the middle
-      // and right panel's width together is greater than half of the toolbox's width,
-      // split all 3 panels to be equally sized by resizing the end panel to be 2/3 of
-      // the current toolbox's width.
-      this.splitBox.setState({
-        width: canDoubleSidebarWidth ? sidebarWidth * 2 : toolboxWidth * 2 / 3,
-      });
-
-      // In landscape/horizontal mode, set the right panel back to its original
-      // inspector sidebar width if we can double the sidebar width. Otherwise, set
-      // the width of the right panel to be 1/3 of the toolbox's width since all 3
-      // panels will be equally sized.
-      sidebarSplitboxWidth = canDoubleSidebarWidth ? sidebarWidth : toolboxWidth / 3;
-    } else {
-      // In portrait/vertical mode, set the bottom-right panel to be 1/2 of the
-      // toolbox's width.
-      sidebarSplitboxWidth = toolboxWidth / 2;
-    }
-
-    // Show the splitter inside the sidebar split box. Sets the width of the inspector
-    // sidebar and specify that the end (right in horizontal or bottom-right in
-    // vertical) panel of the sidebar split box should be controlled when resizing.
-    this.sidebarSplitBox.setState({
-      endPanelControl: true,
-      splitterSize: 1,
-      width: sidebarSplitboxWidth,
-    });
-  },
+  /**
+   * This method is here for the benefit of the node-menu-link-follow menu item
+   * in the inspector contextual-menu.
+   */
+  _onFollowLink() {
+    const type = this.contextMenuTarget.dataset.type;
+    const link = this.contextMenuTarget.dataset.link;
+    this.markup.followAttributeLink(type, link);
+  }
 
   /**
-   * Adds the rule view to the middle (in landscape/horizontal mode) or bottom-left panel
-   * (in portrait/vertical mode) or inspector sidebar depending on whether or not it is 3
-   * pane mode. The default tab specifies whether or not the rule view should be selected.
-   * The defaultTab defaults to the rule view when reverting to the 2 pane mode and the
-   * rule view is being merged back into the inspector sidebar from middle/bottom-left
-   * panel. Otherwise, we specify the default tab when handling the sidebar setup.
-   *
-   * @params {String} defaultTab
-   *         Thie id of the default tab for the sidebar.
+   * Remove attribute from node.
+   * Used for node context menu and shouldn't be called directly.
    */
-  async addRuleView({ defaultTab = "ruleview", skipQueue = false } = {}) {
-    const ruleViewSidebar = this.sidebarSplitBox.startPanelContainer;
-
-    if (this.is3PaneModeEnabled) {
-      // Convert to 3 pane mode by removing the rule view from the inspector sidebar
-      // and adding the rule view to the middle (in landscape/horizontal mode) or
-      // bottom-left (in portrait/vertical mode) panel.
-      ruleViewSidebar.style.display = "block";
-
-      this.setSidebarSplitBoxState();
-
-      // Force the rule view panel creation by calling getPanel
-      this.getPanel("ruleview");
-
-      await this.sidebar.removeTab("ruleview");
-
-      this.ruleViewSideBar.addExistingTab(
-        "ruleview",
-        INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
-        true);
-
-      this.ruleViewSideBar.show();
-    } else {
-      // Removes the rule view from the 3 pane mode and adds the rule view to the main
-      // inspector sidebar.
-      ruleViewSidebar.style.display = "none";
-
-      // Set the width of the split box (right panel in horziontal mode and bottom panel
-      // in vertical mode) to be the width of the inspector sidebar.
-      const splitterBox = this.panelDoc.getElementById("inspector-splitter-box");
-      this.splitBox.setState({
-        width: this.useLandscapeMode() ?
-          this.sidebarSplitBox.state.width : splitterBox.clientWidth,
-      });
-
-      // Hide the splitter to prevent any drag events in the sidebar split box and
-      // specify that the end (right panel in horziontal mode or bottom panel in vertical
-      // mode) panel should be uncontrolled when resizing.
-      this.sidebarSplitBox.setState({
-        endPanelControl: false,
-        splitterSize: 0,
-      });
-
-      this.ruleViewSideBar.hide();
-      await this.ruleViewSideBar.removeTab("ruleview");
-
-      if (skipQueue) {
-        this.sidebar.addExistingTab(
-        "ruleview",
-        INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
-        defaultTab == "ruleview",
-        0);
-      } else {
-        this.sidebar.queueExistingTab(
-          "ruleview",
-          INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
-          defaultTab == "ruleview",
-          0);
-      }
-    }
-
-    this.emit("ruleview-added");
-  },
+  _onRemoveAttribute() {
+    const container = this.markup.getContainer(this.selection.nodeFront);
+    container.removeAttribute(this.nodeMenuTriggerInfo.name);
+  }
 
   /**
-   * Lazily get and create panel instances displayed in the sidebar
+   * Paste the contents of the clipboard as adjacent HTML to the selected Node.
+   *
+   * @param  {String} position
+   *         The position as specified for Element.insertAdjacentHTML
+   *         (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
    */
-  getPanel: function(id) {
-    if (this._panels.has(id)) {
-      return this._panels.get(id);
+  _pasteAdjacentHTML(position) {
+    const content = this._getClipboardContentForPaste();
+    if (!content) {
+      return promise.reject("No clipboard content for paste");
     }
 
-    let panel;
-    switch (id) {
-      case "animationinspector":
-        const AnimationInspector =
-          this.browserRequire("devtools/client/inspector/animation/animation");
-        panel = new AnimationInspector(this, this.panelWin);
-        break;
-      case "boxmodel":
-        // box-model isn't a panel on its own, it used to, now it is being used by
-        // the layout view which retrieves an instance via getPanel.
-        const BoxModel = require("devtools/client/inspector/boxmodel/box-model");
-        panel = new BoxModel(this, this.panelWin);
-        break;
-      case "changesview":
-        const ChangesView =
-          this.browserRequire("devtools/client/inspector/changes/ChangesView");
-        panel = new ChangesView(this, this.panelWin);
-        break;
-      case "computedview":
-        const {ComputedViewTool} =
-          this.browserRequire("devtools/client/inspector/computed/computed");
-        panel = new ComputedViewTool(this, this.panelWin);
-        break;
-      case "fontinspector":
-        const FontInspector =
-          this.browserRequire("devtools/client/inspector/fonts/fonts");
-        panel = new FontInspector(this, this.panelWin);
-        break;
-      case "layoutview":
-        const LayoutView =
-          this.browserRequire("devtools/client/inspector/layout/layout");
-        panel = new LayoutView(this, this.panelWin);
-        break;
-      case "newruleview":
-        const RulesView =
-          this.browserRequire("devtools/client/inspector/rules/new-rules");
-        panel = new RulesView(this, this.panelWin);
-        break;
-      case "ruleview":
-        const {RuleViewTool} = require("devtools/client/inspector/rules/rules");
-        panel = new RuleViewTool(this, this.panelWin);
-        break;
-      default:
-        // This is a custom panel or a non lazy-loaded one.
-        return null;
-    }
-
-    if (panel) {
-      this._panels.set(id, panel);
-    }
-
-    return panel;
-  },
+    const node = this.selection.nodeFront;
+    return this.markup.insertAdjacentHTMLToNode(node, position, content);
+  }
 
   /**
-   * Build the sidebar.
+   * Paste the contents of the clipboard into the selected Node's inner HTML.
    */
-  async setupSidebar() {
-    const sidebar = this.panelDoc.getElementById("inspector-sidebar");
-    const options = {
-      showAllTabsMenu: true,
-      sidebarToggleButton: {
-        collapsed: !this.is3PaneModeEnabled,
-        collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.hideThreePaneMode"),
-        expandPaneTitle: INSPECTOR_L10N.getStr("inspector.showThreePaneMode"),
-        onClick: this.onSidebarToggle,
-      },
-    };
-
-    this.sidebar = new ToolSidebar(sidebar, this, "inspector", options);
-    this.sidebar.on("select", this.onSidebarSelect);
-
-    const ruleSideBar = this.panelDoc.getElementById("inspector-rules-sidebar");
-    this.ruleViewSideBar = new ToolSidebar(ruleSideBar, this, "inspector", {
-      hideTabstripe: true,
-    });
-
-    // defaultTab may also be an empty string or a tab id that doesn't exist anymore
-    // (e.g. it was a tab registered by an addon that has been uninstalled).
-    let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
-
-    if (this.is3PaneModeEnabled && defaultTab === "ruleview") {
-      defaultTab = "layoutview";
+  _pasteInnerHTML() {
+    const content = this._getClipboardContentForPaste();
+    if (!content) {
+      return promise.reject("No clipboard content for paste");
     }
 
-    // Append all side panels
-
-    await this.addRuleView({ defaultTab });
+    const node = this.selection.nodeFront;
+    return this.markup.getNodeInnerHTML(node).then(oldContent => {
+      this.markup.updateNodeInnerHTML(node, content, oldContent);
+    });
+  }
 
-    // Inspector sidebar panels in order of appearance.
-    const sidebarPanels = [
-      {
-        id: "layoutview",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle2"),
-      },
-      {
-        id: "computedview",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
-      },
-      {
-        id: "fontinspector",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
-      },
-      {
-        id: "animationinspector",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"),
-      },
-    ];
-
-    if (this._supportsChangesPanel()) {
-      // Insert Changes as third tab, right after Computed.
-      // TODO: move this inline to `sidebarPanels` above when addressing Bug 1511877.
-      sidebarPanels.splice(2, 0, {
-        id: "changesview",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.changesViewTitle"),
-      });
-    }
-
-    if (Services.prefs.getBoolPref("devtools.inspector.new-rulesview.enabled")) {
-      sidebarPanels.push({
-        id: "newruleview",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
-      });
+  /**
+   * Paste the contents of the clipboard into the selected Node's outer HTML.
+   */
+  _pasteOuterHTML() {
+    const content = this._getClipboardContentForPaste();
+    if (!content) {
+      return promise.reject("No clipboard content for paste");
     }
 
-    for (const { id, title } of sidebarPanels) {
-      // The Computed panel is not a React-based panel. We pick its element container from
-      // the DOM and wrap it in a React component (InspectorTabPanel) so it behaves like
-      // other panels when using the Inspector's tool sidebar.
-      if (id === "computedview") {
-        this.sidebar.queueExistingTab(id, title, defaultTab === id);
-      } else {
-        // When `panel` is a function, it is called when the tab should render. It is
-        // expected to return a React component to populate the tab's content area.
-        // Calling this method on-demand allows us to lazy-load the requested panel.
-        this.sidebar.queueTab(id, title, {
-          props: {
-            id,
-            title,
-          },
-          panel: () => {
-            return this.getPanel(id).provider;
-          },
-        }, defaultTab === id);
-      }
-    }
-
-    this.sidebar.addAllQueuedTabs();
-
-    // Persist splitter state in preferences.
-    this.sidebar.on("show", this.onSidebarShown);
-    this.sidebar.on("hide", this.onSidebarHidden);
-    this.sidebar.on("destroy", this.onSidebarHidden);
-
-    this.sidebar.show();
-  },
+    const node = this.selection.nodeFront;
+    return this.markup.getNodeOuterHTML(node).then(oldContent => {
+      this.markup.updateNodeOuterHTML(node, content, oldContent);
+    });
+  }
 
   /**
-   * Setup any extension sidebar already registered to the toolbox when the inspector.
-   * has been created for the first time.
+   * Show Accessibility properties for currently selected node
    */
-  setupExtensionSidebars() {
-    for (const [sidebarId, {title}] of this.toolbox.inspectorExtensionSidebars) {
-      this.addExtensionSidebar(sidebarId, {title});
-    }
-  },
+  async _showAccessibilityProperties() {
+    const a11yPanel = await this.toolbox.selectTool("accessibility");
+    // Select the accessible object in the panel and wait for the event that
+    // tells us it has been done.
+    const onSelected = a11yPanel.once("new-accessible-front-selected");
+    a11yPanel.selectAccessibleForNode(this.selection.nodeFront, "inspector-context-menu");
+    await onSelected;
+  }
 
   /**
-   * Create a side-panel tab controlled by an extension
-   * using the devtools.panels.elements.createSidebarPane and sidebar object API
-   *
-   * @param {String} id
-   *        An unique id for the sidebar tab.
-   * @param {Object} options
-   * @param {String} options.title
-   *        The tab title
+   * Show DOM properties
    */
-  addExtensionSidebar: function(id, {title}) {
-    if (this._panels.has(id)) {
-      throw new Error(`Cannot create an extension sidebar for the existent id: ${id}`);
-    }
+  _showDOMProperties() {
+    this.toolbox.openSplitConsole().then(() => {
+      const panel = this.toolbox.getPanel("webconsole");
+      const jsterm = panel.hud.jsterm;
 
-    const extensionSidebar = new ExtensionSidebar(this, {id, title});
-
-    // TODO(rpl): pass some extension metadata (e.g. extension name and icon) to customize
-    // the render of the extension title (e.g. use the icon in the sidebar and show the
-    // extension name in a tooltip).
-    this.addSidebarTab(id, title, extensionSidebar.provider, false);
-
-    this._panels.set(id, extensionSidebar);
-
-    // Emit the created ExtensionSidebar instance to the listeners registered
-    // on the toolbox by the "devtools.panels.elements" WebExtensions API.
-    this.toolbox.emit(`extension-sidebar-created-${id}`, extensionSidebar);
-  },
+      jsterm.execute("inspect($0)");
+      jsterm.focus();
+    });
+  }
 
   /**
-   * Remove and destroy a side-panel tab controlled by an extension (e.g. when the
-   * extension has been disable/uninstalled while the toolbox and inspector were
-   * still open).
+   * Use in Console.
    *
-   * @param {String} id
-   *        The id of the sidebar tab to destroy.
+   * Takes the currently selected node in the inspector and assigns it to a
+   * temp variable on the content window.  Also opens the split console and
+   * autofills it with the temp variable.
    */
-  removeExtensionSidebar: function(id) {
-    if (!this._panels.has(id)) {
-      throw new Error(`Unable to find a sidebar panel with id "${id}"`);
-    }
-
-    const panel = this._panels.get(id);
-
-    if (!(panel instanceof ExtensionSidebar)) {
-      throw new Error(`The sidebar panel with id "${id}" is not an ExtensionSidebar`);
-    }
-
-    this._panels.delete(id);
-    this.sidebar.removeTab(id);
-    panel.destroy();
-  },
-
-  /**
-   * Register a side-panel tab. This API can be used outside of
-   * DevTools (e.g. from an extension) as well as by DevTools
-   * code base.
-   *
-   * @param {string} tab uniq id
-   * @param {string} title tab title
-   * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
-   * @param {boolean} selected true if the panel should be selected
-   */
-  addSidebarTab: function(id, title, panel, selected) {
-    this.sidebar.addTab(id, title, panel, selected);
-  },
+  _useInConsole() {
+    this.toolbox.openSplitConsole().then(() => {
+      const panel = this.toolbox.getPanel("webconsole");
+      const jsterm = panel.hud.jsterm;
 
-  /**
-   * Method to check whether the document is a HTML document and
-   * pickColorFromPage method is available or not.
-   *
-   * @return {Boolean} true if the eyedropper highlighter is supported by the current
-   *         document.
-   */
-  async supportsEyeDropper() {
-    try {
-      const hasSupportsHighlighters =
-        await this.target.actorHasMethod("inspector", "supportsHighlighters");
-
-      let supportsHighlighters;
-      if (hasSupportsHighlighters) {
-        supportsHighlighters = await this.inspector.supportsHighlighters();
-      } else {
-        // If the actor does not provide the supportsHighlighter method, fallback to
-        // check if the selected node's document is a HTML document.
-        const { nodeFront } = this.selection;
-        supportsHighlighters = nodeFront && nodeFront.isInHTMLDocument;
-      }
+      const evalString = `{ let i = 0;
+        while (window.hasOwnProperty("temp" + i) && i < 1000) {
+          i++;
+        }
+        window["temp" + i] = $0;
+        "temp" + i;
+      }`;
 
-      return supportsHighlighters;
-    } catch (e) {
-      console.error(e);
-      return false;
-    }
-  },
-
-  async setupToolbar() {
-    this.teardownToolbar();
+      const options = {
+        selectedNodeActor: this.selection.nodeFront.actorID,
+      };
+      jsterm.requestEvaluation(evalString, options).then((res) => {
+        jsterm.setInputValue(res.result);
+        this.inspector.emit("console-var-ready");
+      });
+    });
+  }
 
-    // Setup the add-node button.
-    this.addNode = this.addNode.bind(this);
-    this.addNodeButton = this.panelDoc.getElementById("inspector-element-add-button");
-    this.addNodeButton.addEventListener("click", this.addNode);
-
-    // Setup the eye-dropper icon if we're in an HTML document and we have actor support.
-    const canShowEyeDropper = await this.supportsEyeDropper();
-
-    // Bail out if the inspector was destroyed in the meantime and panelDoc is no longer
-    // available.
-    if (!this.panelDoc) {
+  _buildA11YMenuItem(menu) {
+    if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
+        !Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
       return;
     }
 
-    if (canShowEyeDropper) {
-      this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
-      this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this);
-      this.eyeDropperButton = this.panelDoc
-                                    .getElementById("inspector-eyedropper-toggle");
-      this.eyeDropperButton.disabled = false;
-      this.eyeDropperButton.title = INSPECTOR_L10N.getStr("inspector.eyedropper.label");
-      this.eyeDropperButton.addEventListener("click", this.onEyeDropperButtonClicked);
-    } else {
-      const eyeDropperButton =
-        this.panelDoc.getElementById("inspector-eyedropper-toggle");
-      eyeDropperButton.disabled = true;
-      eyeDropperButton.title = INSPECTOR_L10N.getStr("eyedropper.disabled.title");
-    }
-
-    this.emit("inspector-toolbar-updated");
-  },
-
-  teardownToolbar: function() {
-    if (this.addNodeButton) {
-      this.addNodeButton.removeEventListener("click", this.addNode);
-      this.addNodeButton = null;
-    }
-
-    if (this.eyeDropperButton) {
-      this.eyeDropperButton.removeEventListener("click", this.onEyeDropperButtonClicked);
-      this.eyeDropperButton = null;
-    }
-  },
-
-  /**
-   * Reset the inspector on new root mutation.
-   */
-  onNewRoot: function() {
-    // Record new-root timing for telemetry
-    this._newRootStart = this.panelWin.performance.now();
-
-    this._defaultNode = null;
-    this.selection.setNodeFront(null);
-    this._destroyMarkup();
-
-    const onNodeSelected = defaultNode => {
-      // Cancel this promise resolution as a new one had
-      // been queued up.
-      if (this._pendingSelection != onNodeSelected) {
-        return;
-      }
-      this._pendingSelection = null;
-      this.selection.setNodeFront(defaultNode, { reason: "navigateaway" });
-
-      this.once("markuploaded", this.onMarkupLoaded);
-      this._initMarkup();
+    const showA11YPropsItem = new MenuItem({
+      id: "node-menu-showaccessibilityproperties",
+      label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
+      click: () => this._showAccessibilityProperties(),
+      disabled: true,
+    });
 
-      // Setup the toolbar again, since its content may depend on the current document.
-      this.setupToolbar();
-    };
-    this._pendingSelection = onNodeSelected;
-    this._getDefaultNodeForSelection()
-        .then(onNodeSelected, this._handleRejectionIfNotDestroyed);
-  },
-
-  /**
-   * Handler for "markuploaded" event fired on a new root mutation and after the markup
-   * view is initialized. Expands the current selected node and restores the saved
-   * highlighter state.
-   */
-  async onMarkupLoaded() {
-    if (!this.markup) {
-      return;
-    }
-
-    const onExpand = this.markup.expandNode(this.selection.nodeFront);
-
-    // Restore the highlighter states prior to emitting "new-root".
-    if (this._highlighters) {
-      await Promise.all([
-        this.highlighters.restoreFlexboxState(),
-        this.highlighters.restoreGridState(),
-      ]);
-    }
-
-    this.emit("new-root");
-
-    // Wait for full expand of the selected node in order to ensure
-    // the markup view is fully emitted before firing 'reloaded'.
-    // 'reloaded' is used to know when the panel is fully updated
-    // after a page reload.
-    await onExpand;
-
-    this.emit("reloaded");
-
-    // Record the time between new-root event and inspector fully loaded.
-    if (this._newRootStart) {
-      // Only log the timing when inspector is not destroyed and is in foreground.
-      if (this.toolbox && this.toolbox.currentToolId == "inspector") {
-        const delay = this.panelWin.performance.now() - this._newRootStart;
-        const telemetryKey = "DEVTOOLS_INSPECTOR_NEW_ROOT_TO_RELOAD_DELAY_MS";
-        const histogram = this.telemetry.getHistogramById(telemetryKey);
-        histogram.add(delay);
-      }
-      delete this._newRootStart;
-    }
-  },
-
-  _selectionCssSelector: null,
-
-  /**
-   * Set the currently selected node unique css selector.
-   * Will store the current target url along with it to allow pre-selection at
-   * reload
-   */
-  set selectionCssSelector(cssSelector = null) {
-    if (this._panelDestroyer) {
-      return;
+    // Only attempt to determine if a11y props menu item needs to be enabled if
+    // AccessibilityFront is enabled.
+    if (this.inspector.accessibilityFront.enabled) {
+      this._updateA11YMenuItem(showA11YPropsItem);
     }
 
-    this._selectionCssSelector = {
-      selector: cssSelector,
-      url: this._target.url,
-    };
-  },
-
-  /**
-   * Get the current selection unique css selector if any, that is, if a node
-   * is actually selected and that node has been selected while on the same url
-   */
-  get selectionCssSelector() {
-    if (this._selectionCssSelector &&
-        this._selectionCssSelector.url === this._target.url) {
-      return this._selectionCssSelector.selector;
-    }
-    return null;
-  },
-
-  /**
-   * On any new selection made by the user, store the unique css selector
-   * of the selected node so it can be restored after reload of the same page
-   */
-  updateSelectionCssSelector() {
-    if (this.selection.isElementNode()) {
-      this.selection.nodeFront.getUniqueSelector().then(selector => {
-        this.selectionCssSelector = selector;
-      }, this._handleRejectionIfNotDestroyed);
-    }
-  },
-
-  /**
-   * Can a new HTML element be inserted into the currently selected element?
-   * @return {Boolean}
-   */
-  canAddHTMLChild: function() {
-    const selection = this.selection;
-
-    // Don't allow to insert an element into these elements. This should only
-    // contain elements where walker.insertAdjacentHTML has no effect.
-    const invalidTagNames = ["html", "iframe"];
-
-    return selection.isHTMLNode() &&
-           selection.isElementNode() &&
-           !selection.isPseudoElementNode() &&
-           !selection.isAnonymousNode() &&
-           !invalidTagNames.includes(
-            selection.nodeFront.nodeName.toLowerCase());
-  },
+    menu.append(showA11YPropsItem);
+  }
 
-  /**
-   * Update the state of the add button in the toolbar depending on the current selection.
-   */
-  updateAddElementButton() {
-    const btn = this.panelDoc.getElementById("inspector-element-add-button");
-    if (this.canAddHTMLChild()) {
-      btn.removeAttribute("disabled");
-    } else {
-      btn.setAttribute("disabled", "true");
-    }
-  },
-
-  /**
-   * Handler for the "host-changed" event from the toolbox. Resets the inspector
-   * sidebar sizes when the toolbox host type changes.
-   */
-  async onHostChanged() {
-    // Eagerly call our resize handling code to process the fact that we
-    // switched hosts. If we don't do this, we'll wait for resize events + 200ms
-    // to have passed, which causes the old layout to noticeably show up in the
-    // new host, followed by the updated one.
-    await this._onLazyPanelResize();
-    // Note that we may have been destroyed by now, especially in tests, so we
-    // need to check if that's happened before touching anything else.
-    if (!this.target || !this.is3PaneModeEnabled) {
-      return;
-    }
-
-    this.setSidebarSplitBoxState();
-  },
-
-  /**
-   * When a new node is selected.
-   */
-  onNewSelection: function(value, reason) {
-    if (reason === "selection-destroy") {
-      return;
-    }
-
-    this.updateAddElementButton();
-    this.updateSelectionCssSelector();
-
-    const selfUpdate = this.updating("inspector-panel");
-    executeSoon(() => {
-      try {
-        selfUpdate(this.selection.nodeFront);
-      } catch (ex) {
-        console.error(ex);
-      }
-    });
-  },
-
-  /**
-   * Delay the "inspector-updated" notification while a tool
-   * is updating itself.  Returns a function that must be
-   * invoked when the tool is done updating with the node
-   * that the tool is viewing.
-   */
-  updating: function(name) {
-    if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) {
-      this.cancelUpdate();
-    }
+  _getAttributesSubmenu(isEditableElement) {
+    const attributesSubmenu = new Menu();
+    const nodeInfo = this.nodeMenuTriggerInfo;
+    const isAttributeClicked = isEditableElement && nodeInfo &&
+                              nodeInfo.type === "attribute";
 
-    if (!this._updateProgress) {
-      // Start an update in progress.
-      const self = this;
-      this._updateProgress = {
-        node: this.selection.nodeFront,
-        outstanding: new Set(),
-        checkDone: function() {
-          if (this !== self._updateProgress) {
-            return;
-          }
-          // Cancel update if there is no `selection` anymore.
-          // It can happen if the inspector panel is already destroyed.
-          if (!self.selection || (this.node !== self.selection.nodeFront)) {
-            self.cancelUpdate();
-            return;
-          }
-          if (this.outstanding.size !== 0) {
-            return;
-          }
-
-          self._updateProgress = null;
-          self.emit("inspector-updated", name);
-        },
-      };
-    }
-
-    const progress = this._updateProgress;
-    const done = function() {
-      progress.outstanding.delete(done);
-      progress.checkDone();
-    };
-    progress.outstanding.add(done);
-    return done;
-  },
-
-  /**
-   * Cancel notification of inspector updates.
-   */
-  cancelUpdate: function() {
-    this._updateProgress = null;
-  },
-
-  /**
-   * When a node is deleted, select its parent node or the defaultNode if no
-   * parent is found (may happen when deleting an iframe inside which the
-   * node was selected).
-   */
-  onDetached: function(parentNode) {
-    this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
-    const nodeFront = parentNode ? parentNode : this._defaultNode;
-    this.selection.setNodeFront(nodeFront, { reason: "detached" });
-  },
-
-  /**
-   * Destroy the inspector.
-   */
-  destroy: function() {
-    if (this._panelDestroyer) {
-      return this._panelDestroyer;
-    }
-
-    if (this.walker) {
-      this.walker.off("new-root", this.onNewRoot);
-      this.pageStyle = null;
-    }
-
-    this.cancelUpdate();
+    attributesSubmenu.append(new MenuItem({
+      id: "node-menu-add-attribute",
+      label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
+      disabled: !isEditableElement,
+      click: () => this._onAddAttribute(),
+    }));
+    attributesSubmenu.append(new MenuItem({
+      id: "node-menu-copy-attribute",
+      label: INSPECTOR_L10N.getFormatStr("inspectorCopyAttributeValue.label",
+                                        isAttributeClicked ? `${nodeInfo.value}` : ""),
+      accesskey: INSPECTOR_L10N.getStr("inspectorCopyAttributeValue.accesskey"),
+      disabled: !isAttributeClicked,
+      click: () => this._onCopyAttributeValue(),
+    }));
+    attributesSubmenu.append(new MenuItem({
+      id: "node-menu-edit-attribute",
+      label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label",
+                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
+      accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
+      disabled: !isAttributeClicked,
+      click: () => this._onEditAttribute(),
+    }));
+    attributesSubmenu.append(new MenuItem({
+      id: "node-menu-remove-attribute",
+      label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label",
+                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
+      accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
+      disabled: !isAttributeClicked,
+      click: () => this._onRemoveAttribute(),
+    }));
 
-    this.sidebar.destroy();
-
-    this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true);
-    this.selection.off("new-node-front", this.onNewSelection);
-    this.selection.off("detached-front", this.onDetached);
-    this.sidebar.off("select", this.onSidebarSelect);
-    this.sidebar.off("show", this.onSidebarShown);
-    this.sidebar.off("hide", this.onSidebarHidden);
-    this.sidebar.off("destroy", this.onSidebarHidden);
-    this.target.off("will-navigate", this._onBeforeNavigate);
-
-    for (const [, panel] of this._panels) {
-      panel.destroy();
-    }
-    this._panels.clear();
-
-    if (this._highlighters) {
-      this._highlighters.destroy();
-      this._highlighters = null;
-    }
-
-    if (this._markupFrame) {
-      this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
-      this._markupFrame.contentWindow.removeEventListener("contextmenu",
-                                                          this._onContextMenu);
-    }
-
-    if (this._search) {
-      this._search.destroy();
-      this._search = null;
-    }
-
-    const sidebarDestroyer = this.sidebar.destroy();
-    const ruleViewSideBarDestroyer = this.ruleViewSideBar ?
-      this.ruleViewSideBar.destroy() : null;
-    const markupDestroyer = this._destroyMarkup();
-
-    this.teardownToolbar();
-
-    this.breadcrumbs.destroy();
-    this.reflowTracker.destroy();
-    this.styleChangeTracker.destroy();
-
-    this._is3PaneModeChromeEnabled = null;
-    this._is3PaneModeEnabled = null;
-    this._markupBox = null;
-    this._markupFrame = null;
-    this._notificationBox = null;
-    this._target = null;
-    this._toolbox = null;
-    this.breadcrumbs = null;
-    this.panelDoc = null;
-    this.panelWin.inspector = null;
-    this.panelWin = null;
-    this.resultsLength = null;
-    this.searchBox = null;
-    this.show3PaneTooltip = null;
-    this.sidebar = null;
-    this.store = null;
-    this.telemetry = null;
-
-    this._panelDestroyer = promise.all([
-      markupDestroyer,
-      sidebarDestroyer,
-      ruleViewSideBarDestroyer,
-    ]);
-
-    return this._panelDestroyer;
-  },
+    return attributesSubmenu;
+  }
 
   /**
    * Returns the clipboard content if it is appropriate for pasting
    * into the current node's outer HTML, otherwise returns null.
    */
-  _getClipboardContentForPaste: function() {
+  _getClipboardContentForPaste() {
     const content = clipboardHelper.getText();
     if (content && content.trim().length > 0) {
       return content;
     }
     return null;
-  },
+  }
 
-  _onContextMenu: function(e) {
-    if (!(e.originalTarget instanceof Element) ||
-        e.originalTarget.closest("input[type=text]") ||
-        e.originalTarget.closest("input:not([type])") ||
-        e.originalTarget.closest("textarea")) {
-      return;
+  _getCopySubmenu(markupContainer, isSelectionElement) {
+    const copySubmenu = new Menu();
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyinner",
+      label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyInnerHTML(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyouter",
+      label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyOuterHTML(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyuniqueselector",
+      label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyUniqueSelector(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copycsspath",
+      label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyCssPath(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyxpath",
+      label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyXPath(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyimagedatauri",
+      label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
+      disabled: !isSelectionElement || !markupContainer ||
+                !markupContainer.isPreviewable(),
+      click: () => this._copyImageDataUri(),
+    }));
+
+    return copySubmenu;
+  }
+
+  /**
+   * Link menu items can be shown or hidden depending on the context and
+   * selected node, and their labels can vary.
+   *
+   * @return {Array} list of visible menu items related to links.
+   */
+  _getNodeLinkMenuItems() {
+    const linkFollow = new MenuItem({
+      id: "node-menu-link-follow",
+      visible: false,
+      click: () => this._onFollowLink(),
+    });
+    const linkCopy = new MenuItem({
+      id: "node-menu-link-copy",
+      visible: false,
+      click: () => this._onCopyLink(),
+    });
+
+    // Get information about the right-clicked node.
+    const popupNode = this.contextMenuTarget;
+    if (!popupNode || !popupNode.classList.contains("link")) {
+      return [linkFollow, linkCopy];
     }
 
-    e.stopPropagation();
-    e.preventDefault();
+    const type = popupNode.dataset.type;
+    if ((type === "uri" || type === "cssresource" || type === "jsresource")) {
+      // Links can't be opened in new tabs in the browser toolbox.
+      if (type === "uri" && !this.target.chrome) {
+        linkFollow.visible = true;
+        linkFollow.label = INSPECTOR_L10N.getStr(
+          "inspector.menu.openUrlInNewTab.label");
+      } else if (type === "cssresource") {
+        linkFollow.visible = true;
+        linkFollow.label = TOOLBOX_L10N.getStr(
+          "toolbox.viewCssSourceInStyleEditor.label");
+      } else if (type === "jsresource") {
+        linkFollow.visible = true;
+        linkFollow.label = TOOLBOX_L10N.getStr(
+          "toolbox.viewJsSourceInDebugger.label");
+      }
+
+      linkCopy.visible = true;
+      linkCopy.label = INSPECTOR_L10N.getStr(
+        "inspector.menu.copyUrlToClipboard.label");
+    } else if (type === "idref") {
+      linkFollow.visible = true;
+      linkFollow.label = INSPECTOR_L10N.getFormatStr(
+        "inspector.menu.selectElement.label", popupNode.dataset.link);
+    }
+
+    return [linkFollow, linkCopy];
+  }
+
+  _getPasteSubmenu(isEditableElement) {
+    const isPasteable = isEditableElement && this._getClipboardContentForPaste();
+    const disableAdjacentPaste = !isPasteable ||
+                                 this.selection.isRoot() ||
+                                 this.selection.isBodyNode() ||
+                                 this.selection.isHeadNode();
+    const disableFirstLastPaste = !isPasteable ||
+      (this.selection.isHTMLNode() && this.selection.isRoot());
 
-    this._openMenu({
-      screenX: e.screenX,
-      screenY: e.screenY,
-      target: e.target,
-    });
-  },
+    const pasteSubmenu = new Menu();
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pasteinnerhtml",
+      label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
+      disabled: !isPasteable,
+      click: () => this._pasteInnerHTML(),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pasteouterhtml",
+      label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
+      disabled: !isPasteable,
+      click: () => this._pasteOuterHTML(),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pastebefore",
+      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
+      disabled: disableAdjacentPaste,
+      click: () => this._pasteAdjacentHTML("beforeBegin"),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pasteafter",
+      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
+      disabled: disableAdjacentPaste,
+      click: () => this._pasteAdjacentHTML("afterEnd"),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pastefirstchild",
+      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
+      disabled: disableFirstLastPaste,
+      click: () => this._pasteAdjacentHTML("afterBegin"),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pastelastchild",
+      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
+      disabled: disableFirstLastPaste,
+      click: () => this._pasteAdjacentHTML("beforeEnd"),
+    }));
 
-  _openMenu: function({ target, screenX = 0, screenY = 0 } = { }) {
+    return pasteSubmenu;
+  }
+
+  _openMenu({ target, screenX = 0, screenY = 0 } = {}) {
     if (this.selection.isSlotted()) {
       // Slotted elements should not show any context menu.
       return null;
     }
 
     const markupContainer = this.markup.getContainer(this.selection.nodeFront);
 
     this.contextMenuTarget = target;
@@ -1502,37 +606,37 @@ Inspector.prototype = {
                            this.selection.nodeFront.isTreeDisplayed;
 
     const menu = new Menu();
     menu.append(new MenuItem({
       id: "node-menu-edithtml",
       label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"),
       accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
       disabled: !isEditableElement,
-      click: () => this.editHTML(),
+      click: () => this._editHTML(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-add",
       label: INSPECTOR_L10N.getStr("inspectorAddNode.label"),
       accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"),
-      disabled: !this.canAddHTMLChild(),
-      click: () => this.addNode(),
+      disabled: !this.inspector.canAddHTMLChild(),
+      click: () => this.inspector.addNode(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-duplicatenode",
       label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"),
       disabled: !isDuplicatableElement,
-      click: () => this.duplicateNode(),
+      click: () => this._duplicateNode(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-delete",
       label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"),
       accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"),
-      disabled: !this.isDeletable(this.selection.nodeFront),
-      click: () => this.deleteNode(),
+      disabled: !this.markup.isDeletable(this.selection.nodeFront),
+      click: () => this._deleteNode(),
     }));
 
     menu.append(new MenuItem({
       label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"),
       accesskey:
         INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"),
       submenu: this._getAttributesSubmenu(isEditableElement),
     }));
@@ -1542,17 +646,17 @@ Inspector.prototype = {
     }));
 
     // Set the pseudo classes
     for (const name of ["hover", "active", "focus", "focus-within"]) {
       const menuitem = new MenuItem({
         id: "node-menu-pseudo-" + name,
         label: name,
         type: "checkbox",
-        click: this.togglePseudoClass.bind(this, ":" + name),
+        click: () => this.inspector.togglePseudoClass(":" + name),
       });
 
       if (isSelectionElement) {
         const checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
         menuitem.checked = checked;
       } else {
         menuitem.disabled = true;
       }
@@ -1579,920 +683,94 @@ Inspector.prototype = {
     }));
 
     const isNodeWithChildren = this.selection.isNode() &&
                              markupContainer.hasChildren;
     menu.append(new MenuItem({
       id: "node-menu-expand",
       label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"),
       disabled: !isNodeWithChildren,
-      click: () => this.expandNode(),
+      click: () => this.markup.expandAll(this.selection.nodeFront),
     }));
     menu.append(new MenuItem({
       id: "node-menu-collapse",
       label: INSPECTOR_L10N.getStr("inspectorCollapseAll.label"),
       disabled: !isNodeWithChildren || !markupContainer.expanded,
-      click: () => this.collapseAll(),
+      click: () => this.markup.collapseAll(this.selection.nodeFront),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
     }));
 
     menu.append(new MenuItem({
       id: "node-menu-scrollnodeintoview",
       label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
       accesskey:
         INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
       disabled: !isSelectionElement,
-      click: () => this.scrollNodeIntoView(),
+      click: () => this.markup.scrollNodeIntoView(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-screenshotnode",
       label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"),
       disabled: !isScreenshotable,
-      click: () => this.screenshotNode().catch(console.error),
+      click: () => this.inspector.screenshotNode().catch(console.error),
     }));
     menu.append(new MenuItem({
       id: "node-menu-useinconsole",
       label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"),
-      click: () => this.useInConsole(),
+      click: () => this._useInConsole(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-showdomproperties",
       label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
-      click: () => this.showDOMProperties(),
+      click: () => this._showDOMProperties(),
     }));
 
     if (this.selection.nodeFront.customElementLocation) {
       menu.append(new MenuItem({
         type: "separator",
       }));
 
       menu.append(new MenuItem({
         id: "node-menu-jumptodefinition",
         label: INSPECTOR_L10N.getStr("inspectorCustomElementDefinition.label"),
-        click: () => this.jumpToCustomElementDefinition(),
+        click: () => this._jumpToCustomElementDefinition(),
       }));
     }
 
-    this.buildA11YMenuItem(menu);
+    this._buildA11YMenuItem(menu);
 
     const nodeLinkMenuItems = this._getNodeLinkMenuItems();
     if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
       menu.append(new MenuItem({
         id: "node-menu-link-separator",
         type: "separator",
       }));
     }
 
     for (const menuitem of nodeLinkMenuItems) {
       menu.append(menuitem);
     }
 
-    menu.popup(screenX, screenY, this._toolbox);
+    menu.popup(screenX, screenY, this.toolbox);
     return menu;
-  },
-
-  buildA11YMenuItem: function(menu) {
-    if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
-        !Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
-      return;
-    }
+  }
 
-    const showA11YPropsItem = new MenuItem({
-      id: "node-menu-showaccessibilityproperties",
-      label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
-      click: () => this.showAccessibilityProperties(),
-      disabled: true,
-    });
-    // Only attempt to determine if a11y props menu item needs to be enabled if
-    // AccessibilityFront is enabled.
-    if (this.accessibilityFront.enabled) {
-      this._updateA11YMenuItem(showA11YPropsItem);
-    }
-
-    menu.append(showA11YPropsItem);
-  },
-
-  _updateA11YMenuItem: async function(menuItem) {
+  async _updateA11YMenuItem(menuItem) {
     const hasMethod = await this.target.actorHasMethod("domwalker",
                                                        "hasAccessibilityProperties");
     if (!hasMethod) {
       return;
     }
 
     const hasA11YProps = await this.walker.hasAccessibilityProperties(
       this.selection.nodeFront);
     if (hasA11YProps) {
-      this._toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
-    }
-
-    this.emit("node-menu-updated");
-  },
-
-  _getCopySubmenu: function(markupContainer, isSelectionElement) {
-    const copySubmenu = new Menu();
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyinner",
-      label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyInnerHTML(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyouter",
-      label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyOuterHTML(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyuniqueselector",
-      label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyUniqueSelector(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copycsspath",
-      label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyCssPath(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyxpath",
-      label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyXPath(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyimagedatauri",
-      label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
-      disabled: !isSelectionElement || !markupContainer ||
-                !markupContainer.isPreviewable(),
-      click: () => this.copyImageDataUri(),
-    }));
-
-    return copySubmenu;
-  },
-
-  _getPasteSubmenu: function(isEditableElement) {
-    const isPasteable = isEditableElement && this._getClipboardContentForPaste();
-    const disableAdjacentPaste = !isPasteable || this.selection.isRoot() ||
-          this.selection.isBodyNode() || this.selection.isHeadNode();
-    const disableFirstLastPaste = !isPasteable ||
-          (this.selection.isHTMLNode() && this.selection.isRoot());
-
-    const pasteSubmenu = new Menu();
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteinnerhtml",
-      label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
-      disabled: !isPasteable,
-      click: () => this.pasteInnerHTML(),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteouterhtml",
-      label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
-      disabled: !isPasteable,
-      click: () => this.pasteOuterHTML(),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastebefore",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
-      disabled: disableAdjacentPaste,
-      click: () => this.pasteAdjacentHTML("beforeBegin"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteafter",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
-      disabled: disableAdjacentPaste,
-      click: () => this.pasteAdjacentHTML("afterEnd"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastefirstchild",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
-      disabled: disableFirstLastPaste,
-      click: () => this.pasteAdjacentHTML("afterBegin"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastelastchild",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
-      disabled: disableFirstLastPaste,
-      click: () => this.pasteAdjacentHTML("beforeEnd"),
-    }));
-
-    return pasteSubmenu;
-  },
-
-  _getAttributesSubmenu: function(isEditableElement) {
-    const attributesSubmenu = new Menu();
-    const nodeInfo = this.nodeMenuTriggerInfo;
-    const isAttributeClicked = isEditableElement && nodeInfo &&
-                              nodeInfo.type === "attribute";
-
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-add-attribute",
-      label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
-      disabled: !isEditableElement,
-      click: () => this.onAddAttribute(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-copy-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorCopyAttributeValue.label",
-                                        isAttributeClicked ? `${nodeInfo.value}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyAttributeValue.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onCopyAttributeValue(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-edit-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label",
-                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onEditAttribute(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-remove-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label",
-                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onRemoveAttribute(),
-    }));
-
-    return attributesSubmenu;
-  },
-
-  /**
-   * Link menu items can be shown or hidden depending on the context and
-   * selected node, and their labels can vary.
-   *
-   * @return {Array} list of visible menu items related to links.
-   */
-  _getNodeLinkMenuItems: function() {
-    const linkFollow = new MenuItem({
-      id: "node-menu-link-follow",
-      visible: false,
-      click: () => this.onFollowLink(),
-    });
-    const linkCopy = new MenuItem({
-      id: "node-menu-link-copy",
-      visible: false,
-      click: () => this.onCopyLink(),
-    });
-
-    // Get information about the right-clicked node.
-    const popupNode = this.contextMenuTarget;
-    if (!popupNode || !popupNode.classList.contains("link")) {
-      return [linkFollow, linkCopy];
-    }
-
-    const type = popupNode.dataset.type;
-    if ((type === "uri" || type === "cssresource" || type === "jsresource")) {
-      // Links can't be opened in new tabs in the browser toolbox.
-      if (type === "uri" && !this.target.chrome) {
-        linkFollow.visible = true;
-        linkFollow.label = INSPECTOR_L10N.getStr(
-          "inspector.menu.openUrlInNewTab.label");
-      } else if (type === "cssresource") {
-        linkFollow.visible = true;
-        linkFollow.label = TOOLBOX_L10N.getStr(
-          "toolbox.viewCssSourceInStyleEditor.label");
-      } else if (type === "jsresource") {
-        linkFollow.visible = true;
-        linkFollow.label = TOOLBOX_L10N.getStr(
-          "toolbox.viewJsSourceInDebugger.label");
-      }
-
-      linkCopy.visible = true;
-      linkCopy.label = INSPECTOR_L10N.getStr(
-        "inspector.menu.copyUrlToClipboard.label");
-    } else if (type === "idref") {
-      linkFollow.visible = true;
-      linkFollow.label = INSPECTOR_L10N.getFormatStr(
-        "inspector.menu.selectElement.label", popupNode.dataset.link);
-    }
-
-    return [linkFollow, linkCopy];
-  },
-
-  _initMarkup: function() {
-    if (!this._markupFrame) {
-      this._markupFrame = this.panelDoc.createElement("iframe");
-      this._markupFrame.setAttribute("aria-label",
-        INSPECTOR_L10N.getStr("inspector.panelLabel.markupView"));
-      this._markupFrame.setAttribute("flex", "1");
-      // This is needed to enable tooltips inside the iframe document.
-      this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
-
-      this._markupBox.style.visibility = "hidden";
-      this._markupBox.appendChild(this._markupFrame);
-
-      this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
-      this._markupFrame.setAttribute("src", "markup/markup.xhtml");
-    } else {
-      this._onMarkupFrameLoad();
-    }
-  },
-
-  _onMarkupFrameLoad: function() {
-    this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
-    this._markupFrame.contentWindow.addEventListener("contextmenu", this._onContextMenu);
-    this._markupFrame.contentWindow.focus();
-    this._markupBox.style.visibility = "visible";
-    this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
-    this.emit("markuploaded");
-  },
-
-  _destroyMarkup: function() {
-    let destroyPromise;
-
-    if (this.markup) {
-      destroyPromise = this.markup.destroy();
-      this.markup = null;
-    } else {
-      destroyPromise = promise.resolve();
-    }
-
-    this._markupBox.style.visibility = "hidden";
-
-    return destroyPromise;
-  },
-
-  onEyeDropperButtonClicked: function() {
-    this.eyeDropperButton.classList.contains("checked")
-      ? this.hideEyeDropper()
-      : this.showEyeDropper();
-  },
-
-  startEyeDropperListeners: function() {
-    this.inspector.once("color-pick-canceled", this.onEyeDropperDone);
-    this.inspector.once("color-picked", this.onEyeDropperDone);
-    this.walker.once("new-root", this.onEyeDropperDone);
-  },
-
-  stopEyeDropperListeners: function() {
-    this.inspector.off("color-pick-canceled", this.onEyeDropperDone);
-    this.inspector.off("color-picked", this.onEyeDropperDone);
-    this.walker.off("new-root", this.onEyeDropperDone);
-  },
-
-  onEyeDropperDone: function() {
-    this.eyeDropperButton.classList.remove("checked");
-    this.stopEyeDropperListeners();
-  },
-
-  /**
-   * Show the eyedropper on the page.
-   * @return {Promise} resolves when the eyedropper is visible.
-   */
-  showEyeDropper: function() {
-    // The eyedropper button doesn't exist, most probably because the actor doesn't
-    // support the pickColorFromPage, or because the page isn't HTML.
-    if (!this.eyeDropperButton) {
-      return null;
-    }
-
-    this.telemetry.scalarSet(TELEMETRY_EYEDROPPER_OPENED, 1);
-    this.eyeDropperButton.classList.add("checked");
-    this.startEyeDropperListeners();
-    return this.inspector.pickColorFromPage({copyOnSelect: true})
-                         .catch(console.error);
-  },
-
-  /**
-   * Hide the eyedropper.
-   * @return {Promise} resolves when the eyedropper is hidden.
-   */
-  hideEyeDropper: function() {
-    // The eyedropper button doesn't exist, most probably  because the page isn't HTML.
-    if (!this.eyeDropperButton) {
-      return null;
-    }
-
-    this.eyeDropperButton.classList.remove("checked");
-    this.stopEyeDropperListeners();
-    return this.inspector.cancelPickColorFromPage()
-                         .catch(console.error);
-  },
-
-  /**
-   * Create a new node as the last child of the current selection, expand the
-   * parent and select the new node.
-   */
-  async addNode() {
-    if (!this.canAddHTMLChild()) {
-      return;
+      this.toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
     }
 
-    const html = "<div></div>";
-
-    // Insert the html and expect a childList markup mutation.
-    const onMutations = this.once("markupmutation");
-    await this.walker.insertAdjacentHTML(this.selection.nodeFront, "beforeEnd", html);
-    await onMutations;
-
-    // Expand the parent node.
-    this.markup.expandNode(this.selection.nodeFront);
-  },
-
-  /**
-   * Toggle a pseudo class.
-   */
-  togglePseudoClass: function(pseudo) {
-    if (this.selection.isElementNode()) {
-      const node = this.selection.nodeFront;
-      if (node.hasPseudoClassLock(pseudo)) {
-        return this.walker.removePseudoClassLock(node, pseudo, {parents: true});
-      }
-
-      const hierarchical = pseudo == ":hover" || pseudo == ":active";
-      return this.walker.addPseudoClassLock(node, pseudo, {parents: hierarchical});
-    }
-    return promise.resolve();
-  },
-
-  /**
-   * Show DOM properties
-   */
-  showDOMProperties: function() {
-    this._toolbox.openSplitConsole().then(() => {
-      const panel = this._toolbox.getPanel("webconsole");
-      const jsterm = panel.hud.jsterm;
-
-      jsterm.execute("inspect($0)");
-      jsterm.focus();
-    });
-  },
-
-  jumpToCustomElementDefinition: function() {
-    const node = this.selection.nodeFront;
-    const { url, line } = node.customElementLocation;
-    this._toolbox.viewSourceInDebugger(url, line, "show_custom_element");
-  },
-
-  /**
-   * Show Accessibility properties for currently selected node
-   */
-  async showAccessibilityProperties() {
-    const a11yPanel = await this._toolbox.selectTool("accessibility");
-    // Select the accessible object in the panel and wait for the event that
-    // tells us it has been done.
-    const onSelected = a11yPanel.once("new-accessible-front-selected");
-    a11yPanel.selectAccessibleForNode(this.selection.nodeFront,
-                                      "inspector-context-menu");
-    await onSelected;
-  },
-
-  /**
-   * Use in Console.
-   *
-   * Takes the currently selected node in the inspector and assigns it to a
-   * temp variable on the content window.  Also opens the split console and
-   * autofills it with the temp variable.
-   */
-  useInConsole: function() {
-    this._toolbox.openSplitConsole().then(() => {
-      const panel = this._toolbox.getPanel("webconsole");
-      const jsterm = panel.hud.jsterm;
-
-      const evalString = `{ let i = 0;
-        while (window.hasOwnProperty("temp" + i) && i < 1000) {
-          i++;
-        }
-        window["temp" + i] = $0;
-        "temp" + i;
-      }`;
-
-      const options = {
-        selectedNodeActor: this.selection.nodeFront.actorID,
-      };
-      jsterm.requestEvaluation(evalString, options).then((res) => {
-        jsterm.setInputValue(res.result);
-        this.emit("console-var-ready");
-      });
-    });
-  },
-
-  /**
-   * Edit the outerHTML of the selected Node.
-   */
-  editHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    if (this.markup) {
-      this.markup.beginEditingOuterHTML(this.selection.nodeFront);
-    }
-  },
-
-  /**
-   * Paste the contents of the clipboard into the selected Node's outer HTML.
-   */
-  pasteOuterHTML: function() {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.getNodeOuterHTML(node).then(oldContent => {
-      this.markup.updateNodeOuterHTML(node, content, oldContent);
-    });
-  },
-
-  /**
-   * Paste the contents of the clipboard into the selected Node's inner HTML.
-   */
-  pasteInnerHTML: function() {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.getNodeInnerHTML(node).then(oldContent => {
-      this.markup.updateNodeInnerHTML(node, content, oldContent);
-    });
-  },
-
-  /**
-   * Paste the contents of the clipboard as adjacent HTML to the selected Node.
-   * @param position
-   *        The position as specified for Element.insertAdjacentHTML
-   *        (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
-   */
-  pasteAdjacentHTML: function(position) {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.insertAdjacentHTMLToNode(node, position, content);
-  },
-
-  /**
-   * Copy the innerHTML of the selected Node to the clipboard.
-   */
-  copyInnerHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    this._copyLongString(this.walker.innerHTML(this.selection.nodeFront));
-  },
-
-  /**
-   * Copy the outerHTML of the selected Node to the clipboard.
-   */
-  copyOuterHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    const node = this.selection.nodeFront;
-
-    switch (node.nodeType) {
-      case nodeConstants.ELEMENT_NODE :
-        this._copyLongString(this.walker.outerHTML(node));
-        break;
-      case nodeConstants.COMMENT_NODE :
-        this._getLongString(node.getNodeValue()).then(comment => {
-          clipboardHelper.copyString("<!--" + comment + "-->");
-        });
-        break;
-      case nodeConstants.DOCUMENT_TYPE_NODE :
-        clipboardHelper.copyString(node.doctypeString);
-        break;
-    }
-  },
-
-  /**
-   * Copy the data-uri for the currently selected image in the clipboard.
-   */
-  copyImageDataUri: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    if (container && container.isPreviewable()) {
-      container.copyImageDataUri();
-    }
-  },
-
-  /**
-   * Copy the content of a longString (via a promise resolving a
-   * LongStringActor) to the clipboard
-   * @param  {Promise} longStringActorPromise
-   *         promise expected to resolve a LongStringActor instance
-   * @return {Promise} promise resolving (with no argument) when the
-   *         string is sent to the clipboard
-   */
-  _copyLongString: function(longStringActorPromise) {
-    return this._getLongString(longStringActorPromise).then(string => {
-      clipboardHelper.copyString(string);
-    }).catch(console.error);
-  },
-
-  /**
-   * Retrieve the content of a longString (via a promise resolving a LongStringActor)
-   * @param  {Promise} longStringActorPromise
-   *         promise expected to resolve a LongStringActor instance
-   * @return {Promise} promise resolving with the retrieved string as argument
-   */
-  _getLongString: function(longStringActorPromise) {
-    return longStringActorPromise.then(longStringActor => {
-      return longStringActor.string().then(string => {
-        longStringActor.release().catch(console.error);
-        return string;
-      });
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy a unique selector of the selected Node to the clipboard.
-   */
-  copyUniqueSelector: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.unique.css.selector.opened", 1);
-    this.selection.nodeFront.getUniqueSelector().then(selector => {
-      clipboardHelper.copyString(selector);
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy the full CSS Path of the selected Node to the clipboard.
-   */
-  copyCssPath: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.full.css.selector.opened", 1);
-    this.selection.nodeFront.getCssPath().then(path => {
-      clipboardHelper.copyString(path);
-    }).catch(console.error);
-  },
+    this.inspector.emit("node-menu-updated");
+  }
+}
 
-  /**
-   * Copy the XPath of the selected Node to the clipboard.
-   */
-  copyXPath: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.xpath.opened", 1);
-    this.selection.nodeFront.getXPath().then(path => {
-      clipboardHelper.copyString(path);
-    }).catch(console.error);
-  },
-
-  /**
-   * Initiate screenshot command on selected node.
-   */
-  async screenshotNode() {
-    // Bug 1332936 - it's possible to call `screenshotNode` while the BoxModel highlighter
-    // is still visible, therefore showing it in the picture.
-    // To avoid that, we have to hide it before taking the screenshot. The `hideBoxModel`
-    // will do that, calling `hide` for the highlighter only if previously shown.
-    await this.highlighter.hideBoxModel();
-
-    const clipboardEnabled = Services.prefs
-      .getBoolPref("devtools.screenshot.clipboard.enabled");
-    const args = {
-      file: true,
-      nodeActorID: this.selection.nodeFront.actorID,
-      clipboard: clipboardEnabled,
-    };
-    const screenshotFront = await this.target.getFront("screenshot");
-    const screenshot = await screenshotFront.capture(args);
-    await saveScreenshot(this.panelWin, args, screenshot);
-  },
-
-  /**
-   * Scroll the node into view.
-   */
-  scrollNodeIntoView: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.selection.nodeFront.scrollIntoView();
-  },
-
-  /**
-   * Duplicate the selected node
-   */
-  duplicateNode: function() {
-    const selection = this.selection;
-    if (!selection.isElementNode() ||
-        selection.isRoot() ||
-        selection.isAnonymousNode() ||
-        selection.isPseudoElementNode()) {
-      return;
-    }
-    this.walker.duplicateNode(selection.nodeFront).catch(console.error);
-  },
-
-  /**
-   * Delete the selected node.
-   */
-  deleteNode: function() {
-    if (!this.selection.isNode() ||
-         this.selection.isRoot()) {
-      return;
-    }
-
-    // If the markup panel is active, use the markup panel to delete
-    // the node, making this an undoable action.
-    if (this.markup) {
-      this.markup.deleteNode(this.selection.nodeFront);
-    } else {
-      // remove the node from content
-      this.walker.removeNode(this.selection.nodeFront);
-    }
-  },
-
-  /**
-   * Add attribute to node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onAddAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.addAttribute();
-  },
-
-  /**
-   * Copy attribute value for node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onCopyAttributeValue: function() {
-    clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
-  },
-
-  /**
-   * Edit attribute for node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onEditAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.editAttribute(this.nodeMenuTriggerInfo.name);
-  },
-
-  /**
-   * Remove attribute from node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onRemoveAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.removeAttribute(this.nodeMenuTriggerInfo.name);
-  },
-
-  expandNode: function() {
-    this.markup.expandAll(this.selection.nodeFront);
-  },
-
-  collapseAll: function() {
-    this.markup.collapseAll(this.selection.nodeFront);
-  },
-
-  /**
-   * This method is here for the benefit of the node-menu-link-follow menu item
-   * in the inspector contextual-menu.
-   */
-  onFollowLink: function() {
-    const type = this.contextMenuTarget.dataset.type;
-    const link = this.contextMenuTarget.dataset.link;
-
-    this.followAttributeLink(type, link);
-  },
-
-  /**
-   * Given a type and link found in a node's attribute in the markup-view,
-   * attempt to follow that link (which may result in opening a new tab, the
-   * style editor or debugger).
-   */
-  followAttributeLink: function(type, link) {
-    if (!type || !link) {
-      return;
-    }
-
-    if (type === "uri" || type === "cssresource" || type === "jsresource") {
-      // Open link in a new tab.
-      this.inspector.resolveRelativeURL(
-        link, this.selection.nodeFront).then(url => {
-          if (type === "uri") {
-            openContentLink(url);
-          } else if (type === "cssresource") {
-            return this.toolbox.viewSourceInStyleEditor(url);
-          } else if (type === "jsresource") {
-            return this.toolbox.viewSourceInDebugger(url);
-          }
-          return null;
-        }).catch(console.error);
-    } else if (type == "idref") {
-      // Select the node in the same document.
-      this.walker.document(this.selection.nodeFront).then(doc => {
-        return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
-          if (!node) {
-            this.emit("idref-attribute-link-failed");
-            return;
-          }
-          this.selection.setNodeFront(node);
-        });
-      }).catch(console.error);
-    }
-  },
-
-  /**
-   * This method is here for the benefit of the node-menu-link-copy menu item
-   * in the inspector contextual-menu.
-   */
-  onCopyLink: function() {
-    const link = this.contextMenuTarget.dataset.link;
-
-    this.copyAttributeLink(link);
-  },
-
-  /**
-   * This method is here for the benefit of copying links.
-   */
-  copyAttributeLink: function(link) {
-    this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
-      clipboardHelper.copyString(url);
-    }, console.error);
-  },
-
-  /**
-   * Returns an object containing the shared handler functions used in the box
-   * model and grid React components.
-   */
-  getCommonComponentProps() {
-    return {
-      setSelectedNode: this.selection.setNodeFront,
-      onShowBoxModelHighlighterForNode: this.onShowBoxModelHighlighterForNode,
-    };
-  },
-
-  /**
-   * Shows the box-model highlighter on the element corresponding to the provided
-   * NodeFront.
-   *
-   * @param  {NodeFront} nodeFront
-   *         The node to highlight.
-   * @param  {Object} options
-   *         Options passed to the highlighter actor.
-   */
-  onShowBoxModelHighlighterForNode(nodeFront, options) {
-    const toolbox = this.toolbox;
-    toolbox.highlighter.highlight(nodeFront, options);
-  },
-
-  /**
-   * Returns a value indicating whether a node can be deleted.
-   *
-   * @param {NodeFront} nodeFront
-   *        The node to test for deletion
-   */
-  isDeletable(nodeFront) {
-    return !(nodeFront.isDocumentElement ||
-           nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
-           nodeFront.isAnonymous);
-  },
-
-  async inspectNodeActor(nodeActor, inspectFromAnnotation) {
-    const nodeFront = await this.walker.gripToNodeFront({ actor: nodeActor });
-    if (!nodeFront) {
-      console.error("The object cannot be linked to the inspector, the " +
-                    "corresponding nodeFront could not be found.");
-      return false;
-    }
-
-    const isAttached = await this.walker.isInDOMTree(nodeFront);
-    if (!isAttached) {
-      console.error("Selected DOMNode is not attached to the document tree.");
-      return false;
-    }
-
-    await this.selection.setNodeFront(nodeFront, { reason: inspectFromAnnotation });
-    return true;
-  },
-};
-
-exports.Inspector = Inspector;
+module.exports = MarkupContextMenu;
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -16,19 +16,24 @@ const AutocompletePopup = require("devto
 const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
 const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
 const {PrefObserver} = require("devtools/client/shared/prefs");
 const MarkupElementContainer = require("devtools/client/inspector/markup/views/element-container");
 const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/read-only-container");
 const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container");
 const RootContainer = require("devtools/client/inspector/markup/views/root-container");
 
+loader.lazyRequireGetter(this, "MarkupContextMenu", "devtools/client/inspector/markup/markup-context-menu");
 loader.lazyRequireGetter(this, "SlottedNodeContainer", "devtools/client/inspector/markup/views/slotted-node-container");
+loader.lazyRequireGetter(this, "copyLongString", "devtools/client/inspector/shared/utils", true);
+loader.lazyRequireGetter(this, "getLongString", "devtools/client/inspector/shared/utils", true);
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 loader.lazyRequireGetter(this, "HTMLTooltip", "devtools/client/shared/widgets/tooltip/HTMLTooltip", true);
 loader.lazyRequireGetter(this, "UndoStack", "devtools/client/shared/undo", true);
+loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
 
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 // Page size for pageup/pagedown
 const PAGE_SIZE = 10;
 const DEFAULT_MAX_CHILDREN = 100;
 const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
@@ -93,31 +98,33 @@ function MarkupView(inspector, frame, co
   // slotted container for a given node front.
   this._slottedContainerKeys = new WeakMap();
 
   // Binding functions that need to be called in scope.
   this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
   this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this);
   this._mutationObserver = this._mutationObserver.bind(this);
   this._onBlur = this._onBlur.bind(this);
+  this._onContextMenu = this._onContextMenu.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onCollapseAttributesPrefChange = this._onCollapseAttributesPrefChange.bind(this);
   this._onWalkerNodeStatesChanged = this._onWalkerNodeStatesChanged.bind(this);
   this._onFocus = this._onFocus.bind(this);
   this._onMouseClick = this._onMouseClick.bind(this);
   this._onMouseMove = this._onMouseMove.bind(this);
   this._onMouseOut = this._onMouseOut.bind(this);
   this._onMouseUp = this._onMouseUp.bind(this);
   this._onNewSelection = this._onNewSelection.bind(this);
   this._onToolboxPickerCanceled = this._onToolboxPickerCanceled.bind(this);
   this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
 
   // Listening to various events.
   this._elt.addEventListener("blur", this._onBlur, true);
   this._elt.addEventListener("click", this._onMouseClick);
+  this._elt.addEventListener("contextmenu", this._onContextMenu);
   this._elt.addEventListener("mousemove", this._onMouseMove);
   this._elt.addEventListener("mouseout", this._onMouseOut);
   this._frame.addEventListener("focus", this._onFocus);
   this.inspector.selection.on("new-node-front", this._onNewSelection);
   this.walker.on("display-change", this._onWalkerNodeStatesChanged);
   this.walker.on("scrollable-change", this._onWalkerNodeStatesChanged);
   this.walker.on("mutations", this._mutationObserver);
   this.win.addEventListener("copy", this._onCopy);
@@ -151,16 +158,24 @@ function MarkupView(inspector, frame, co
 MarkupView.prototype = {
   /**
    * How long does a node flash when it mutates (in ms).
    */
   CONTAINER_FLASHING_DURATION: 500,
 
   _selectedContainer: null,
 
+  get contextMenu() {
+    if (!this._contextMenu) {
+      this._contextMenu = new MarkupContextMenu(this);
+    }
+
+    return this._contextMenu;
+  },
+
   get eventDetailsTooltip() {
     if (!this._eventDetailsTooltip) {
       // This tooltip will be attached to the toolbox document.
       this._eventDetailsTooltip = new HTMLTooltip(this.toolbox.doc, {
         type: "arrow",
         consumeOutsideClicks: false,
       });
     }
@@ -274,16 +289,20 @@ MarkupView.prototype = {
       return;
     }
 
     if (this._selectedContainer) {
       this._selectedContainer.clearFocus();
     }
   },
 
+  _onContextMenu: function(event) {
+    this.contextMenu.show(event);
+  },
+
   /**
    * Executed on each mouse-move while a node is being dragged in the view.
    * Auto-scrolls the view to reveal nodes below the fold to drop the dragged
    * node in.
    */
   _autoScroll: function(event) {
     const docEl = this.doc.documentElement;
 
@@ -761,23 +780,84 @@ MarkupView.prototype = {
   _onCopy: function(evt) {
     // Ignore copy events from editors
     if (this._isInputOrTextarea(evt.target)) {
       return;
     }
 
     const selection = this.inspector.selection;
     if (selection.isNode()) {
-      this.inspector.copyOuterHTML();
+      this.copyOuterHTML();
     }
     evt.stopPropagation();
     evt.preventDefault();
   },
 
   /**
+   * Copy the outerHTML of the selected Node to the clipboard.
+   */
+  copyOuterHTML: function() {
+    if (!this.inspector.selection.isNode()) {
+      return;
+    }
+    const node = this.inspector.selection.nodeFront;
+
+    switch (node.nodeType) {
+      case nodeConstants.ELEMENT_NODE :
+        copyLongString(this.walker.outerHTML(node));
+        break;
+      case nodeConstants.COMMENT_NODE :
+        getLongString(node.getNodeValue()).then(comment => {
+          clipboardHelper.copyString("<!--" + comment + "-->");
+        });
+        break;
+      case nodeConstants.DOCUMENT_TYPE_NODE :
+        clipboardHelper.copyString(node.doctypeString);
+        break;
+    }
+  },
+
+  /**
+   * Given a type and link found in a node's attribute in the markup-view,
+   * attempt to follow that link (which may result in opening a new tab, the
+   * style editor or debugger).
+   */
+  followAttributeLink: function(type, link) {
+    if (!type || !link) {
+      return;
+    }
+
+    if (type === "uri" || type === "cssresource" || type === "jsresource") {
+      // Open link in a new tab.
+      this.inspector.inspector.resolveRelativeURL(
+        link, this.inspector.selection.nodeFront).then(url => {
+          if (type === "uri") {
+            openContentLink(url);
+          } else if (type === "cssresource") {
+            return this.toolbox.viewSourceInStyleEditor(url);
+          } else if (type === "jsresource") {
+            return this.toolbox.viewSourceInDebugger(url);
+          }
+          return null;
+        }).catch(console.error);
+    } else if (type == "idref") {
+      // Select the node in the same document.
+      this.walker.document(this.inspector.selection.nodeFront).then(doc => {
+        return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
+          if (!node) {
+            this.emit("idref-attribute-link-failed");
+            return;
+          }
+          this.inspector.selection.setNodeFront(node);
+        });
+      }).catch(console.error);
+    }
+  },
+
+  /**
    * Register all key shortcuts.
    */
   _initShortcuts: function() {
     const shortcuts = new KeyShortcuts({
       window: this.win,
     });
 
     this._onShortcut = this._onShortcut.bind(this);
@@ -815,18 +895,17 @@ MarkupView.prototype = {
         }
         break;
       }
       case "markupView.edit.key": {
         this.beginEditingOuterHTML(this._selectedContainer.node);
         break;
       }
       case "markupView.scrollInto.key": {
-        const selection = this._selectedContainer.node;
-        this.inspector.scrollNodeIntoView(selection);
+        this.scrollNodeIntoView();
         break;
       }
       // Generic keys
       case "Delete": {
         this.deleteNodeOrAttribute();
         break;
       }
       case "Backspace": {
@@ -956,26 +1035,38 @@ MarkupView.prototype = {
       const container = focusedAttribute.closest("li.child").container;
       container.removeAttribute(focusedAttribute.dataset.attr);
     } else {
       this.deleteNode(this._selectedContainer.node, moveBackward);
     }
   },
 
   /**
+   * Returns a value indicating whether a node can be deleted.
+   *
+   * @param {NodeFront} nodeFront
+   *        The node to test for deletion
+   */
+  isDeletable(nodeFront) {
+    return !(nodeFront.isDocumentElement ||
+           nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
+           nodeFront.isAnonymous);
+  },
+
+  /**
    * Delete a node from the DOM.
    * This is an undoable action.
    *
    * @param  {NodeFront} node
    *         The node to remove.
    * @param  {Boolean} moveBackward
    *         If set to true, focus the previous sibling, otherwise the next one.
    */
   deleteNode: function(node, moveBackward) {
-    if (!this.inspector.isDeletable(node)) {
+    if (!this.isDeletable(node)) {
       return;
     }
 
     const container = this.getContainer(node);
 
     // Retain the node so we can undo this...
     this.walker.retainNode(node).then(() => {
       const parent = node.parentNode();
@@ -1013,16 +1104,27 @@ MarkupView.prototype = {
         const isValidSibling = nextSibling && !nextSibling.isPseudoElement;
         nextSibling = isValidSibling ? nextSibling : null;
         this.walker.insertBefore(node, parent, nextSibling);
       });
     }).catch(console.error);
   },
 
   /**
+   * Scroll the node into view.
+   */
+  scrollNodeIntoView() {
+    if (!this.inspector.selection.isNode()) {
+      return;
+    }
+
+    this.inspector.selection.nodeFront.scrollIntoView();
+  },
+
+  /**
    * If an editable item is focused, select its container.
    */
   _onFocus: function(event) {
     let parent = event.target;
     while (!parent.container) {
       parent = parent.parentNode;
     }
     if (parent) {
@@ -1947,16 +2049,17 @@ MarkupView.prototype = {
       this._undo = null;
     }
 
     this.popup.destroy();
     this.popup = null;
 
     this._elt.removeEventListener("blur", this._onBlur, true);
     this._elt.removeEventListener("click", this._onMouseClick);
+    this._elt.removeEventListener("contextmenu", this._onContextMenu);
     this._elt.removeEventListener("mousemove", this._onMouseMove);
     this._elt.removeEventListener("mouseout", this._onMouseOut);
     this._frame.removeEventListener("focus", this._onFocus);
     this.inspector.selection.off("new-node-front", this._onNewSelection);
     this.inspector.inspector.nodePicker.off(
       "picker-node-hovered", this._onToolboxPickerHover
     );
     this.walker.off("display-change", this._onWalkerNodeStatesChanged);
--- a/devtools/client/inspector/markup/moz.build
+++ b/devtools/client/inspector/markup/moz.build
@@ -4,13 +4,14 @@
 # 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/.
 
 DIRS += [
     'views',
 ]
 
 DevToolsModules(
+    'markup-context-menu.js',
     'markup.js',
     'utils.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/markup/test/browser_markup_links_05.js
+++ b/devtools/client/inspector/markup/test/browser_markup_links_05.js
@@ -18,17 +18,17 @@ add_task(async function() {
   info("Set the popupNode to the node that contains the uri");
   let {editor} = await getContainerForSelector("video", inspector);
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("poster").querySelector(".link"),
   });
 
   info("Follow the link and wait for the new tab to open");
   const onTabOpened = once(gBrowser.tabContainer, "TabOpen");
-  inspector.onFollowLink();
+  inspector.markup.contextMenu._onFollowLink();
   const {target: tab} = await onTabOpened;
   await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
   ok(true, "A new tab opened");
   is(tab.linkedBrowser.currentURI.spec, URL_ROOT + "doc_markup_tooltip.png",
     "The URL for the new tab is correct");
   gBrowser.removeTab(tab);
 
@@ -38,32 +38,32 @@ add_task(async function() {
   info("Set the popupNode to the node that contains the ref");
   ({editor} = await getContainerForSelector("label", inspector));
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("for").querySelector(".link"),
   });
 
   info("Follow the link and wait for the new node to be selected");
   const onSelection = inspector.selection.once("new-node-front");
-  inspector.onFollowLink();
+  inspector.markup.contextMenu._onFollowLink();
   await onSelection;
 
   ok(true, "A new node was selected");
   is(inspector.selection.nodeFront.id, "name", "The right node was selected");
 
   info("Select a node with an invalid IDREF attribute");
   await selectNode("output", inspector);
 
   info("Set the popupNode to the node that contains the ref");
   ({editor} = await getContainerForSelector("output", inspector));
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("for").querySelectorAll(".link")[2],
   });
 
   info("Try to follow the link and check that no new node were selected");
-  const onFailed = inspector.once("idref-attribute-link-failed");
-  inspector.onFollowLink();
+  const onFailed = inspector.markup.once("idref-attribute-link-failed");
+  inspector.markup.contextMenu._onFollowLink();
   await onFailed;
 
   ok(true, "The node selection failed");
   is(inspector.selection.nodeFront.tagName.toLowerCase(), "output",
     "The <output> node is still selected");
 });
--- a/devtools/client/inspector/markup/test/browser_markup_links_06.js
+++ b/devtools/client/inspector/markup/test/browser_markup_links_06.js
@@ -18,17 +18,17 @@ add_task(async function() {
   info("Set the popupNode to the node that contains the uri");
   let {editor} = await getContainerForSelector("link", inspector);
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("href").querySelector(".link"),
   });
 
   info("Follow the link and wait for the style-editor to open");
   const onStyleEditorReady = toolbox.once("styleeditor-ready");
-  inspector.onFollowLink();
+  inspector.markup.contextMenu._onFollowLink();
   await onStyleEditorReady;
 
   // No real need to test that the editor opened on the right file here as this
   // is already tested in /framework/test/browser_toolbox_view_source_*
   ok(true, "The style-editor was open");
 
   info("Switch back to the inspector");
   await toolbox.selectTool("inspector");
@@ -39,15 +39,15 @@ add_task(async function() {
   info("Set the popupNode to the node that contains the uri");
   ({editor} = await getContainerForSelector("script", inspector));
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("src").querySelector(".link"),
   });
 
   info("Follow the link and wait for the debugger to open");
   const onDebuggerReady = toolbox.once("jsdebugger-ready");
-  inspector.onFollowLink();
+  inspector.markup.contextMenu._onFollowLink();
   await onDebuggerReady;
 
   // No real need to test that the debugger opened on the right file here as
   // this is already tested in /framework/test/browser_toolbox_view_source_*
   ok(true, "The debugger was open");
 });
--- a/devtools/client/inspector/markup/test/browser_markup_links_07.js
+++ b/devtools/client/inspector/markup/test/browser_markup_links_07.js
@@ -94,16 +94,16 @@ async function followLinkWaitForNewNode(
   performMouseDown(linkEl, isMetaClick);
   await onSelection;
 
   ok(true, "A new node was selected");
   is(inspector.selection.nodeFront.id, "name", "The right node was selected");
 }
 
 async function followLinkNoNewNode(linkEl, isMetaClick, inspector) {
-  const onFailed = inspector.once("idref-attribute-link-failed");
+  const onFailed = inspector.markup.once("idref-attribute-link-failed");
   performMouseDown(linkEl, isMetaClick);
   await onFailed;
 
   ok(true, "The node selection failed");
   is(inspector.selection.nodeFront.tagName.toLowerCase(), "output",
     "The <output> node is still selected");
 }
--- a/devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js
@@ -40,25 +40,30 @@ add_task(async function() {
   const shadowRootContainer = hostContainer.getChildContainers()[0];
   await expandContainer(inspector, shadowRootContainer);
 
   info("Select the div under the shadow root");
   const divContainer = shadowRootContainer.getChildContainers()[0];
   await selectNode(divContainer.node, inspector);
 
   info("Check the copied values for the various copy*Path helpers");
-  await waitForClipboardPromise(() => inspector.copyXPath(), '//*[@id="el1"]');
-  await waitForClipboardPromise(() => inspector.copyCssPath(), "div#el1");
-  await waitForClipboardPromise(() => inspector.copyUniqueSelector(), "#el1");
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyXPath(),
+    '//*[@id="el1"]');
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyCssPath(),
+    "div#el1");
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyUniqueSelector(),
+    "#el1");
 
   info("Expand the div");
   await expandContainer(inspector, divContainer);
 
   info("Select the third span");
   const spanContainer = divContainer.getChildContainers()[2];
   await selectNode(spanContainer.node, inspector);
 
   info("Check the copied values for the various copy*Path helpers");
-  await waitForClipboardPromise(() => inspector.copyXPath(), "/div/span[3]");
-  await waitForClipboardPromise(() => inspector.copyCssPath(), "div#el1 span");
-  await waitForClipboardPromise(() => inspector.copyUniqueSelector(),
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyXPath(),
+    "/div/span[3]");
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyCssPath(),
+    "div#el1 span");
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyUniqueSelector(),
     "#el1 > span:nth-child(3)");
 });
--- a/devtools/client/inspector/markup/views/markup-container.js
+++ b/devtools/client/inspector/markup/views/markup-container.js
@@ -559,17 +559,17 @@ MarkupContainer.prototype = {
     }
 
     // Follow attribute links if middle or meta click.
     if (isMiddleClick || isMetaClick) {
       const link = target.dataset.link;
       const type = target.dataset.type;
       // Make container tabbable descendants not tabbable (by default).
       this.canFocus = false;
-      this.markup.inspector.followAttributeLink(type, link);
+      this.markup.followAttributeLink(type, link);
       return;
     }
 
     // Start node drag & drop (if the mouse moved, see _onMouseMove).
     if (isLeftClick && this.isDraggable()) {
       this._isPreDragging = true;
       this._dragStartY = event.pageY;
       this.markup._draggedContainer = this;
--- a/devtools/client/inspector/shared/utils.js
+++ b/devtools/client/inspector/shared/utils.js
@@ -6,16 +6,17 @@
 
 "use strict";
 
 const promise = require("promise");
 
 loader.lazyRequireGetter(this, "KeyCodes", "devtools/client/shared/keycodes", true);
 loader.lazyRequireGetter(this, "getCSSLexer", "devtools/shared/css/lexer", true);
 loader.lazyRequireGetter(this, "parseDeclarations", "devtools/shared/css/parsing-utils", true);
+loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 /**
  * Called when a character is typed in a value editor.  This decides
  * whether to advance or not, first by checking to see if ";" was
  * typed, and then by lexing the input and seeing whether the ";"
  * would be a terminator at this point.
@@ -78,16 +79,31 @@ function blurOnMultipleProperties(cssPro
       if (props.length > 1) {
         e.target.blur();
       }
     }, 0);
   };
 }
 
 /**
+ * Copy the content of a longString (via a promise resolving a
+ * LongStringActor) to the clipboard.
+ *
+ * @param  {Promise} longStringActorPromise
+ *         promise expected to resolve a LongStringActor instance
+ * @return {Promise} promise resolving (with no argument) when the
+ *         string is sent to the clipboard
+ */
+function copyLongString(longStringActorPromise) {
+  return getLongString(longStringActorPromise).then(string => {
+    clipboardHelper.copyString(string);
+  }).catch(console.error);
+}
+
+/**
  * Create a child element with a set of attributes.
  *
  * @param {Element} parent
  *        The parent node.
  * @param {string} tagName
  *        The tag name.
  * @param {object} attributes
  *        A set of attributes to set on the node.
@@ -105,16 +121,32 @@ function createChild(parent, tagName, at
       }
     }
   }
   parent.appendChild(elt);
   return elt;
 }
 
 /**
+ * Retrieve the content of a longString (via a promise resolving a LongStringActor).
+ *
+ * @param  {Promise} longStringActorPromise
+ *         promise expected to resolve a LongStringActor instance
+ * @return {Promise} promise resolving with the retrieved string as argument
+ */
+function getLongString(longStringActorPromise) {
+  return longStringActorPromise.then(longStringActor => {
+    return longStringActor.string().then(string => {
+      longStringActor.release().catch(console.error);
+      return string;
+    });
+  }).catch(console.error);
+}
+
+/**
  * Returns a selector of the Element Rep from the grip. This is based on the
  * getElements() function in our devtools-reps component for a ElementNode.
  *
  * @param  {Object} grip
  *         Grip-like object that can be used with Reps.
  * @return {String} selector of the element node.
  */
 function getSelectorFromGrip(grip) {
@@ -191,12 +223,14 @@ function translateNodeFrontToGrip(nodeFr
       nodeType: nodeFront.nodeType,
     },
   };
 }
 
 exports.advanceValidate = advanceValidate;
 exports.appendText = appendText;
 exports.blurOnMultipleProperties = blurOnMultipleProperties;
+exports.copyLongString = copyLongString;
 exports.createChild = createChild;
+exports.getLongString = getLongString;
 exports.getSelectorFromGrip = getSelectorFromGrip;
 exports.promiseWarn = promiseWarn;
 exports.translateNodeFrontToGrip = translateNodeFrontToGrip;
--- a/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js
@@ -31,48 +31,48 @@ add_task(async function() {
     ok(hasAttribute, "attribute was successfully added");
   }
 
   async function testCopyAttributeValue() {
     info("Testing 'Copy Attribute Value' and waiting for clipboard promise to resolve");
     const copyAttributeValue = getMenuItem("node-menu-copy-attribute");
 
     info("Triggering 'Copy Attribute Value' and waiting for clipboard to copy the value");
-    inspector.nodeMenuTriggerInfo = {
+    inspector.markup.contextMenu.nodeMenuTriggerInfo = {
       type: "attribute",
       name: "data-edit",
       value: "the",
     };
 
     await waitForClipboardPromise(() => copyAttributeValue.click(), "the");
   }
 
   async function testCopyLongAttributeValue() {
     info("Testing 'Copy Attribute Value' copies very long attribute values");
     const copyAttributeValue = getMenuItem("node-menu-copy-attribute");
     const longAttribute = "#01234567890123456789012345678901234567890123456789" +
     "12345678901234567890123456789012345678901234567890123456789012345678901" +
     "23456789012345678901234567890123456789012345678901234567890123456789012" +
     "34567890123456789012345678901234567890123456789012345678901234567890123";
 
-    inspector.nodeMenuTriggerInfo = {
+    inspector.markup.contextMenu.nodeMenuTriggerInfo = {
       type: "attribute",
       name: "data-edit",
       value: longAttribute,
     };
 
     await waitForClipboardPromise(() => copyAttributeValue.click(), longAttribute);
   }
 
   async function testEditAttribute() {
     info("Testing 'Edit Attribute' menu item");
     const editAttribute = getMenuItem("node-menu-edit-attribute");
 
     info("Triggering 'Edit Attribute' and waiting for mutation to occur");
-    inspector.nodeMenuTriggerInfo = {
+    inspector.markup.contextMenu.nodeMenuTriggerInfo = {
       type: "attribute",
       name: "data-edit",
     };
     editAttribute.click();
     EventUtils.sendString("data-edit='edited'");
     const onMutation = inspector.once("markupmutation");
     EventUtils.synthesizeKey("KEY_Enter");
     await onMutation;
@@ -82,17 +82,17 @@ add_task(async function() {
     ok(isAttributeChanged, "attribute was successfully edited");
   }
 
   async function testRemoveAttribute() {
     info("Testing 'Remove Attribute' menu item");
     const removeAttribute = getMenuItem("node-menu-remove-attribute");
 
     info("Triggering 'Remove Attribute' and waiting for mutation to occur");
-    inspector.nodeMenuTriggerInfo = {
+    inspector.markup.contextMenu.nodeMenuTriggerInfo = {
       type: "attribute",
       name: "data-remove",
     };
     const onMutation = inspector.once("markupmutation");
     removeAttribute.click();
     await onMutation;
 
     const hasAttribute = await testActor.hasNode("#attributes[data-remove]");
--- a/devtools/client/inspector/test/browser_inspector_textbox-menu.js
+++ b/devtools/client/inspector/test/browser_inspector_textbox-menu.js
@@ -77,17 +77,17 @@ add_task(async function() {
 
   // Move the mouse out of the box-model region to avoid triggering the box model
   // highlighter.
   EventUtils.synthesizeMouseAtCenter(tag, {}, inspector.panelWin);
 });
 
 async function checkTextBox(textBox, toolbox) {
   let textboxContextMenu = toolbox.doc.getElementById("toolbox-menu");
-  ok(!textboxContextMenu, "The menu is  closed");
+  ok(!textboxContextMenu, "The menu is closed");
 
   info("Simulating context click on the textbox and expecting the menu to open");
   const onContextMenu = toolbox.once("menu-open");
   synthesizeContextMenuEvent(textBox);
   await onContextMenu;
 
   textboxContextMenu = toolbox.doc.getElementById("toolbox-menu");
   ok(textboxContextMenu, "The menu is now visible");
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -655,11 +655,11 @@ function openStyleContextMenuAndGetAllIt
 
 /**
  * Open the inspector menu and return all of it's items in a flat array
  * @param {InspectorPanel} inspector
  * @param {Object} options to pass into openMenu
  * @return An array of MenuItems
  */
 function openContextMenuAndGetAllItems(inspector, options) {
-  const menu = inspector._openMenu(options);
+  const menu = inspector.markup.contextMenu._openMenu(options);
   return buildContextMenuItems(menu);
 }