Merge mozilla-central to inbound a=merge
authorCoroiu Cristina <ccoroiu@mozilla.com>
Fri, 28 Sep 2018 07:37:58 +0300
changeset 438690 f2b071db0a8a5fc8d93610d6f3225ea6e08ea48c
parent 438671 43ace919d4d6cca870a9d614d10b52ebd658eb86 (current diff)
parent 438650 95301d8052055fc5d5f131e123c0c35eef359978 (diff)
child 438691 238fb42f209eb4c03ca1af6210b9175891b40cc9
push id70086
push usertoros@mozilla.com
push dateFri, 28 Sep 2018 15:41:28 +0000
treeherderautoland@dbe2506cadd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to inbound a=merge
browser/base/content/test/siteIdentity/browser_navigation_failures.js
--- a/browser/actors/ContextMenuChild.jsm
+++ b/browser/actors/ContextMenuChild.jsm
@@ -599,16 +599,18 @@ class ContextMenuChild extends ActorChil
       customMenuItems,
       contentDisposition,
       frameOuterWindowID,
       popupNodeSelectors,
       disableSetDesktopBg,
       parentAllowsMixedContent,
     };
 
+    Services.obs.notifyObservers({wrappedJSObject: data}, "on-prepare-contextmenu");
+
     if (isRemote) {
       this.mm.sendAsyncMessage("contextmenu", data, {
         targetAsCPOW,
       });
     } else {
       let browser = this.docShell.chromeEventHandler;
       let mainWin = browser.ownerGlobal;
 
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -42,20 +42,16 @@
   min-width: 300px;
 %endif
 }
 
 #main-window[customize-entered] {
   min-width: -moz-fit-content;
 }
 
-searchbar {
-  -moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
-}
-
 .searchbar-textbox {
   -moz-binding: url("chrome://browser/content/search/search.xml#searchbar-textbox");
 }
 
 .search-one-offs {
   -moz-binding: url("chrome://browser/content/search/search.xml#search-one-offs");
 }
 
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -11,16 +11,17 @@
 #ifdef BROWSER_XHTML
 xmlns="http://www.w3.org/1999/xhtml"
 #endif
 >
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 for (let script of [
   "chrome://browser/content/browser.js",
+  "chrome://browser/content/search/searchbar.js",
 
   "chrome://browser/content/browser-captivePortal.js",
   "chrome://browser/content/browser-compacttheme.js",
   "chrome://browser/content/browser-contentblocking.js",
   "chrome://browser/content/browser-feeds.js",
   "chrome://browser/content/browser-media.js",
   "chrome://browser/content/browser-pageActions.js",
   "chrome://browser/content/browser-places.js",
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -60,16 +60,17 @@ function openContextMenu(aMessage) {
                               contentType: data.contentType,
                               contentDisposition: data.contentDisposition,
                               frameOuterWindowID: data.frameOuterWindowID,
                               selectionInfo: data.selectionInfo,
                               disableSetDesktopBackground: data.disableSetDesktopBg,
                               loginFillInfo: data.loginFillInfo,
                               parentAllowsMixedContent: data.parentAllowsMixedContent,
                               userContextId: data.userContextId,
+                              webExtContextData: data.webExtContextData,
                             };
 
   let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
   let context = gContextMenuContentData.context;
 
   // The event is a CPOW that can't be passed into the native openPopupAtScreen
   // function. Therefore we synthesize a new MouseEvent to propagate the
   // inputSource to the subsequently triggered popupshowing event.
@@ -123,16 +124,17 @@ nsContextMenu.prototype = {
         onPassword: this.onPassword,
         srcUrl: this.mediaURL,
         frameUrl: gContextMenuContentData ? gContextMenuContentData.docLocation : undefined,
         pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
         linkText: this.linkTextStr,
         linkUrl: this.linkURL,
         selectionText: this.isTextSelected ? this.selectionInfo.fullText : undefined,
         frameId: this.frameOuterWindowID,
+        webExtContextData: gContextMenuContentData ? gContextMenuContentData.webExtContextData : undefined,
       };
       subject.wrappedJSObject = subject;
       Services.obs.notifyObservers(subject, "on-build-contextmenu");
     }
 
     this.viewFrameSourceElement =
          document.getElementById("context-viewframesource");
     this.ellipsis = "\u2026";
@@ -162,17 +164,17 @@ nsContextMenu.prototype = {
       context = gContextMenuContentData.context;
       gContextMenuContentData.context = null;
       this.isRemote = gContextMenuContentData.isRemote;
     }
 
     this.shouldDisplay = context.shouldDisplay;
     this.timeStamp = context.timeStamp;
 
-    // Assign what's _possibly_ needed from `context` sent by ContextMenu.jsm
+    // Assign what's _possibly_ needed from `context` sent by ContextMenuChild.jsm
     // Keep this consistent with the similar code in ContextMenu's _setContext
     this.bgImageURL          = context.bgImageURL;
     this.imageDescURL        = context.imageDescURL;
     this.imageInfo           = context.imageInfo;
     this.mediaURL            = context.mediaURL;
     this.webExtBrowserType   = context.webExtBrowserType;
 
     this.canSpellCheck       = context.canSpellCheck;
--- a/browser/base/content/test/siteIdentity/browser.ini
+++ b/browser/base/content/test/siteIdentity/browser.ini
@@ -103,9 +103,9 @@ support-files =
 [browser_no_mcb_for_onions.js]
 tags = mcb
 support-files =
   test_no_mcb_for_onions.html
 [browser_check_identity_state.js]
 [browser_iframe_navigation.js]
 support-files =
   iframe_navigation.html
-[browser_navigation_failures.js]
+[browser_tls_handshake_failure.js]
rename from browser/base/content/test/siteIdentity/browser_navigation_failures.js
rename to browser/base/content/test/siteIdentity/browser_tls_handshake_failure.js
--- a/browser/base/content/test/siteIdentity/browser_navigation_failures.js
+++ b/browser/base/content/test/siteIdentity/browser_tls_handshake_failure.js
@@ -1,41 +1,25 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Tests that the site identity indicator is properly updated for navigations
-// that fail for various reasons. In particular, we currently test TLS handshake
-// failures and about: pages that don't actually exist.
-// See bug 1492424 and bug 1493427.
+// Tests that the site identity indicator is properly updated for connections
+// where the TLS handshake fails.
+// See bug 1492424.
 
-const kSecureURI = getRootDirectory(gTestPath).replace("chrome://mochitests/content",
-                                                       "https://example.com") + "dummy_page.html";
 add_task(async function() {
-  await BrowserTestUtils.withNewTab(kSecureURI, async (browser) => {
+  let rootURI = getRootDirectory(gTestPath).replace("chrome://mochitests/content",
+                                                    "https://example.com");
+  await BrowserTestUtils.withNewTab(rootURI + "dummy_page.html", async (browser) => {
     let identityMode = window.document.getElementById("identity-box").className;
     is(identityMode, "verifiedDomain", "identity should be secure before");
 
     const TLS_HANDSHAKE_FAILURE_URI = "https://ssl3.example.com/";
     // Try to connect to a server where the TLS handshake will fail.
     BrowserTestUtils.loadURI(browser, TLS_HANDSHAKE_FAILURE_URI);
     await BrowserTestUtils.browserLoaded(browser, false, TLS_HANDSHAKE_FAILURE_URI, true);
 
     let newIdentityMode = window.document.getElementById("identity-box").className;
     is(newIdentityMode, "unknownIdentity", "identity should be unknown (not secure) after");
   });
 });
-
-add_task(async function() {
-  await BrowserTestUtils.withNewTab(kSecureURI, async (browser) => {
-    let identityMode = window.document.getElementById("identity-box").className;
-    is(identityMode, "verifiedDomain", "identity should be secure before");
-
-    const BAD_ABOUT_PAGE_URI = "about:somethingthatdoesnotexist";
-    // Try to load an about: page that doesn't exist
-    BrowserTestUtils.loadURI(browser, BAD_ABOUT_PAGE_URI);
-    await BrowserTestUtils.browserLoaded(browser, false, BAD_ABOUT_PAGE_URI, true);
-
-    let newIdentityMode = window.document.getElementById("identity-box").className;
-    is(newIdentityMode, "unknownIdentity", "identity should be unknown (not secure) after");
-  });
-});
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
@@ -21,19 +21,18 @@ add_task(async function test_setup() {
 // it is not.
 async function test_opensearch(shouldWork) {
   let searchBar = BrowserSearch.searchBar;
 
   let rootDir = getRootDirectory(gTestPath);
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, rootDir + "opensearch.html");
   let searchPopup = document.getElementById("PopupSearchAutoComplete");
   let promiseSearchPopupShown = BrowserTestUtils.waitForEvent(searchPopup, "popupshown");
-  let searchBarButton = document.getAnonymousElementByAttribute(searchBar,
-                                                                "anonid",
-                                                                "searchbar-search-button");
+  let searchBarButton = searchBar.querySelector(".searchbar-search-button");
+
   searchBarButton.click();
   await promiseSearchPopupShown;
   let oneOffsContainer = document.getAnonymousElementByAttribute(searchPopup,
                                                                  "anonid",
                                                                  "search-one-off-buttons");
   let engineListElement = document.getAnonymousElementByAttribute(oneOffsContainer,
                                                                   "anonid",
                                                                   "add-engines");
--- a/browser/components/extensions/child/ext-menus.js
+++ b/browser/components/extensions/child/ext-menus.js
@@ -1,16 +1,23 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+ChromeUtils.defineModuleGetter(this, "Services",
+                               "resource://gre/modules/Services.jsm");
+
 var {
   withHandlingUserInput,
 } = ExtensionCommon;
 
+var {
+  ExtensionError,
+} = ExtensionUtils;
+
 // If id is not specified for an item we use an integer.
 // This ID need only be unique within a single addon. Since all addon code that
 // can use this API runs in the same process, this local variable suffices.
 var gNextMenuItemID = 0;
 
 // Map[Extension -> Map[string or id, ContextMenusClickPropHandler]]
 var gPropHandlers = new Map();
 
@@ -103,16 +110,17 @@ class ContextMenusClickPropHandler {
       this.unsetListener(id);
     }
   }
 }
 
 this.menusInternal = class extends ExtensionAPI {
   getAPI(context) {
     let onClickedProp = new ContextMenusClickPropHandler(context);
+    let pendingMenuEvent;
 
     let api = {
       menus: {
         create(createProperties, callback) {
           if (createProperties.id === null) {
             createProperties.id = ++gNextMenuItemID;
           }
           let {onclick} = createProperties;
@@ -160,16 +168,82 @@ this.menusInternal = class extends Exten
         },
 
         removeAll() {
           onClickedProp.deleteAllListenersFromExtension();
 
           return context.childManager.callParentAsyncFunction("menusInternal.removeAll", []);
         },
 
+        overrideContext(contextOptions) {
+          let {event} = context.contentWindow;
+          if (!event || event.type !== "contextmenu" || !event.isTrusted) {
+            throw new ExtensionError("overrideContext must be called during a \"contextmenu\" event");
+          }
+
+          let checkValidArg = (contextType, propKey) => {
+            if (contextOptions.context !== contextType) {
+              if (contextOptions[propKey]) {
+                throw new ExtensionError(`Property "${propKey}" can only be used with context "${contextType}"`);
+              }
+              return false;
+            }
+            if (contextOptions.showDefaults) {
+              throw new ExtensionError(`Property "showDefaults" cannot be used with context "${contextType}"`);
+            }
+            if (!contextOptions[propKey]) {
+              throw new ExtensionError(`Property "${propKey}" is required for context "${contextType}"`);
+            }
+            return true;
+          };
+          if (checkValidArg("tab", "tabId")) {
+            if (!context.extension.hasPermission("tabs")) {
+              throw new ExtensionError(`The "tab" context requires the "tabs" permission.`);
+            }
+          }
+          if (checkValidArg("bookmark", "bookmarkId")) {
+            if (!context.extension.hasPermission("bookmarks")) {
+              throw new ExtensionError(`The "bookmark" context requires the "bookmarks" permission.`);
+            }
+          }
+
+          let webExtContextData = {
+            extensionId: context.extension.id,
+            showDefaults: contextOptions.showDefaults,
+            overrideContext: contextOptions.context,
+            bookmarkId: contextOptions.bookmarkId,
+            tabId: contextOptions.tabId,
+          };
+
+          if (pendingMenuEvent) {
+            // overrideContext is called more than once during the same event.
+            pendingMenuEvent.webExtContextData = webExtContextData;
+            return;
+          }
+          pendingMenuEvent = {
+            webExtContextData,
+            observe(subject, topic, data) {
+              pendingMenuEvent = null;
+              Services.obs.removeObserver(this, "on-prepare-contextmenu");
+              subject.wrappedJSObject.webExtContextData = this.webExtContextData;
+            },
+            run() {
+              // "on-prepare-contextmenu" is expected to be observed before the
+              // end of the "contextmenu" event dispatch. This task is queued
+              // in case that does not happen, e.g. when the menu is not shown.
+              if (pendingMenuEvent === this) {
+                pendingMenuEvent = null;
+                Services.obs.removeObserver(this, "on-prepare-contextmenu");
+              }
+            },
+          };
+          Services.obs.addObserver(pendingMenuEvent, "on-prepare-contextmenu");
+          Services.tm.dispatchToMainThread(pendingMenuEvent);
+        },
+
         onClicked: new EventManager({
           context,
           name: "menus.onClicked",
           register: fire => {
             let listener = (info, tab) => {
               withHandlingUserInput(context.contentWindow,
                                     () => fire.sync(info, tab));
             };
--- a/browser/components/extensions/parent/ext-menus.js
+++ b/browser/components/extensions/parent/ext-menus.js
@@ -42,54 +42,113 @@ var gNextRadioGroupID = 0;
 var gMaxLabelLength = 64;
 
 var gMenuBuilder = {
   // When a new menu is opened, this function is called and
   // we populate the |xulMenu| with all the items from extensions
   // to be displayed. We always clear all the items again when
   // popuphidden fires.
   build(contextData) {
+    contextData = this.maybeOverrideContextData(contextData);
     let xulMenu = contextData.menu;
     xulMenu.addEventListener("popuphidden", this);
     this.xulMenu = xulMenu;
     for (let [, root] of gRootItems) {
-      let rootElement = this.createTopLevelElement(root, contextData);
-      if (rootElement) {
-        this.appendTopLevelElement(rootElement);
-      }
+      this.createAndInsertTopLevelElements(root, contextData, null);
     }
     this.afterBuildingMenu(contextData);
+
+    if (contextData.webExtContextData && !contextData.webExtContextData.showDefaults) {
+      // Wait until nsContextMenu.js has toggled the visibility of the default
+      // menu items before hiding the default items.
+      Promise.resolve().then(() => this.hideDefaultMenuItems());
+    }
+  },
+
+  maybeOverrideContextData(contextData) {
+    let {webExtContextData} = contextData;
+    if (!webExtContextData || !webExtContextData.overrideContext) {
+      return contextData;
+    }
+    if (webExtContextData.overrideContext === "bookmark") {
+      return {
+        menu: contextData.menu,
+        bookmarkId: webExtContextData.bookmarkId,
+        onBookmark: true,
+        webExtContextData,
+      };
+    }
+    if (webExtContextData.overrideContext === "tab") {
+      // TODO: Handle invalid tabs more gracefully (instead of throwing).
+      let tab = tabTracker.getTab(webExtContextData.tabId);
+      return {
+        menu: contextData.menu,
+        tab,
+        pageUrl: tab.linkedBrowser.currentURI.spec,
+        onTab: true,
+        webExtContextData,
+      };
+    }
+    throw new Error(`Unexpected overrideContext: ${webExtContextData.overrideContext}`);
   },
 
-  // Builds a context menu for browserAction and pageAction buttons.
-  buildActionContextMenu(contextData) {
-    const {menu} = contextData;
+  createAndInsertTopLevelElements(root, contextData, nextSibling) {
+    let rootElements;
+    if (contextData.onBrowserAction || contextData.onPageAction) {
+      if (contextData.extension.id !== root.extension.id) {
+        return;
+      }
+      rootElements = this.buildTopLevelElements(root, contextData, ACTION_MENU_TOP_LEVEL_LIMIT, false);
 
-    const root = gRootItems.get(contextData.extension);
-    if (!root) {
+      // Action menu items are prepended to the menu, followed by a separator.
+      nextSibling = nextSibling || this.xulMenu.firstElementChild;
+      if (rootElements.length && !this.itemsToCleanUp.has(nextSibling)) {
+        rootElements.push(this.xulMenu.ownerDocument.createXULElement("menuseparator"));
+      }
+    } else if (contextData.webExtContextData) {
+      let {
+        extensionId,
+        showDefaults,
+        overrideContext,
+      } = contextData.webExtContextData;
+      if (extensionId === root.extension.id) {
+        rootElements = this.buildTopLevelElements(root, contextData, Infinity, false);
+        // The extension menu should be rendered at the top, but after the navigation buttons.
+        nextSibling = nextSibling || this.xulMenu.querySelector(":scope > #context-sep-navigation + *");
+        if (rootElements.length && showDefaults && !this.itemsToCleanUp.has(nextSibling)) {
+          rootElements.push(this.xulMenu.ownerDocument.createXULElement("menuseparator"));
+        }
+      } else if (!showDefaults && !overrideContext) {
+        // When the default menu items should be hidden, menu items from other
+        // extensions should be hidden too.
+        return;
+      }
+      // Fall through to show default extension menu items.
+    }
+    if (!rootElements) {
+      rootElements = this.buildTopLevelElements(root, contextData, 1, true);
+      if (rootElements.length && !this.itemsToCleanUp.has(this.xulMenu.lastElementChild)) {
+        // All extension menu items are appended at the end.
+        // Prepend separator if this is the first extension menu item.
+        rootElements.unshift(this.xulMenu.ownerDocument.createXULElement("menuseparator"));
+      }
+    }
+
+    if (!rootElements.length) {
       return;
     }
 
-    const children = this.buildChildren(root, contextData);
-    const visible = children.slice(0, ACTION_MENU_TOP_LEVEL_LIMIT);
-
-    this.xulMenu = menu;
-    menu.addEventListener("popuphidden", this);
-
-    if (visible.length) {
-      const separator = menu.ownerDocument.createXULElement("menuseparator");
-      menu.insertBefore(separator, menu.firstElementChild);
-      this.itemsToCleanUp.add(separator);
-
-      for (const child of visible) {
-        this.itemsToCleanUp.add(child);
-        menu.insertBefore(child, separator);
-      }
+    if (nextSibling) {
+      nextSibling.before(...rootElements);
+    } else {
+      this.xulMenu.append(...rootElements);
     }
-    this.afterBuildingMenu(contextData);
+    for (let item of rootElements) {
+      this.itemsToCleanUp.add(item);
+    }
   },
 
   buildElementWithChildren(item, contextData) {
     const element = this.buildSingleElement(item, contextData);
     const children = this.buildChildren(item, contextData);
     if (children.length) {
       element.firstElementChild.append(...children);
     }
@@ -111,73 +170,68 @@ var gMenuBuilder = {
 
       if (child.enabledForContext(contextData)) {
         children.push(this.buildElementWithChildren(child, contextData));
       }
     }
     return children;
   },
 
-  createTopLevelElement(root, contextData) {
-    let rootElement = this.buildElementWithChildren(root, contextData);
-    if (!rootElement.firstElementChild || !rootElement.firstElementChild.children.length) {
-      // If the root has no visible children, there is no reason to show
-      // the root menu item itself either.
-      return null;
-    }
-    rootElement.setAttribute("ext-type", "top-level-menu");
-    rootElement = this.removeTopLevelMenuIfNeeded(rootElement);
+  buildTopLevelElements(root, contextData, maxCount, forceManifestIcons) {
+    let children = this.buildChildren(root, contextData);
 
-    // Display the extension icon on the root element.
-    if (root.extension.manifest.icons) {
-      this.setMenuItemIcon(rootElement, root.extension, contextData, root.extension.manifest.icons);
-    } else {
-      this.removeMenuItemIcon(rootElement);
-    }
-    return rootElement;
-  },
-
-  appendTopLevelElement(rootElement) {
-    if (this.itemsToCleanUp.size === 0) {
-      const separator = this.xulMenu.ownerDocument.createXULElement("menuseparator");
-      this.itemsToCleanUp.add(separator);
-      this.xulMenu.append(separator);
+    // TODO: Fix bug 1492969 and remove this whole if block.
+    if (children.length === 1 && maxCount === 1 && forceManifestIcons &&
+        AppConstants.platform === "linux" &&
+        children[0].getAttribute("type") === "checkbox") {
+      // Keep single checkbox items in the submenu on Linux since
+      // the extension icon overlaps the checkbox otherwise.
+      maxCount = 0;
     }
 
-    this.xulMenu.appendChild(rootElement);
-    this.itemsToCleanUp.add(rootElement);
+    if (children.length > maxCount) {
+      // Move excess items into submenu.
+      let rootElement = this.buildSingleElement(root, contextData);
+      rootElement.setAttribute("ext-type", "top-level-menu");
+      rootElement.firstElementChild.append(...children.splice(maxCount - 1));
+      children.push(rootElement);
+    }
+
+    if (forceManifestIcons) {
+      for (let rootElement of children) {
+        // Display the extension icon on the root element.
+        if (root.extension.manifest.icons) {
+          this.setMenuItemIcon(rootElement, root.extension, contextData, root.extension.manifest.icons);
+        } else {
+          this.removeMenuItemIcon(rootElement);
+        }
+      }
+    }
+    return children;
   },
 
   removeSeparatorIfNoTopLevelItems() {
-    if (this.itemsToCleanUp.size === 1) {
-      // Remove the separator if all extension menu items have disappeared.
-      const separator = this.itemsToCleanUp.values().next().value;
-      separator.remove();
-      this.itemsToCleanUp.clear();
-    }
-  },
+    // Extension menu items always have have a non-empty ID.
+    let isNonExtensionSeparator =
+      item => item.nodeName === "menuseparator" && !item.id;
+
+    // itemsToCleanUp contains all top-level menu items. A separator should
+    // only be kept if it is next to an extension menu item.
+    let isExtensionMenuItemSibling =
+      item => item && this.itemsToCleanUp.has(item) && !isNonExtensionSeparator(item);
 
-  removeTopLevelMenuIfNeeded(element) {
-    // If there is only one visible top level element we don't need the
-    // root menu element for the extension.
-    let menuPopup = element.firstElementChild;
-    if (menuPopup && menuPopup.children.length == 1) {
-      let onlyChild = menuPopup.firstElementChild;
-
-      // Keep single checkbox items in the submenu on Linux since
-      // the extension icon overlaps the checkbox otherwise.
-      if (AppConstants.platform === "linux" && onlyChild.getAttribute("type") === "checkbox") {
-        return element;
+    for (let item of this.itemsToCleanUp) {
+      if (isNonExtensionSeparator(item)) {
+        if (!isExtensionMenuItemSibling(item.previousElementSibling) &&
+            !isExtensionMenuItemSibling(item.nextElementSibling)) {
+          item.remove();
+          this.itemsToCleanUp.delete(item);
+        }
       }
-
-      onlyChild.remove();
-      return onlyChild;
     }
-
-    return element;
   },
 
   buildSingleElement(item, contextData) {
     let doc = contextData.menu.ownerDocument;
     let element;
     if (item.children.length > 0) {
       element = this.createMenuElement(doc, item);
     } else if (item.type == "separator") {
@@ -289,17 +343,21 @@ var gMenuBuilder = {
           if (child.type == "radio" && child.groupName == item.groupName) {
             child.checked = false;
           }
         }
         // Select the clicked radio item.
         item.checked = true;
       }
 
-      if (contextData.tab) {
+      let {webExtContextData} = contextData;
+      if (contextData.tab &&
+          // If the menu context was overridden by the extension, do not grant
+          // activeTab since the extension also controls the tabId.
+          (!webExtContextData || webExtContextData.extensionId !== item.extension.id)) {
         item.tabManager.addActiveTabPermission(contextData.tab);
       }
 
       let info = item.getClickInfo(contextData, wasChecked);
 
       const map = {shiftKey: "Shift", altKey: "Alt", metaKey: "Command", ctrlKey: "Ctrl"};
       info.modifiers = Object.keys(map).filter(key => event[key]).map(key => map[key]);
       if (event.ctrlKey && AppConstants.platform === "macosx") {
@@ -372,66 +430,37 @@ var gMenuBuilder = {
 
   rebuildMenu(extension) {
     let {contextData} = this;
     if (!contextData) {
       // This happens if the menu is not visible.
       return;
     }
 
-    if (contextData.onBrowserAction || contextData.onPageAction) {
-      if (contextData.extension.id !== extension.id) {
-        // The extension that just called refresh() is not the owner of the
-        // action whose context menu is showing, so it can't have any items in
-        // the menu anyway and nothing will change.
-        return;
-      }
-      // The action menu can only have items from one extension, so remove all
-      // items (including the separator) and rebuild the action menu (if any).
-      for (let item of this.itemsToCleanUp) {
+    // Find the group of existing top-level items (usually 0 or 1 items)
+    // and remember its position for when the new items are inserted.
+    let elementIdPrefix = `${makeWidgetId(extension.id)}-menuitem-`;
+    let nextSibling = null;
+    for (let item of this.itemsToCleanUp) {
+      if (item.id && item.id.startsWith(elementIdPrefix)) {
+        nextSibling = item.nextSibling;
         item.remove();
-      }
-      this.itemsToCleanUp.clear();
-      this.buildActionContextMenu(contextData);
-      return;
-    }
-
-    // First find the one and only top-level menu item for the extension.
-    let elementIdPrefix = `${makeWidgetId(extension.id)}-menuitem-`;
-    let oldRoot = null;
-    for (let item = this.xulMenu.lastElementChild; item !== null; item = item.previousElementSibling) {
-      if (item.id && item.id.startsWith(elementIdPrefix)) {
-        oldRoot = item;
-        this.itemsToCleanUp.delete(oldRoot);
-        break;
+        this.itemsToCleanUp.delete(item);
       }
     }
 
     let root = gRootItems.get(extension);
-    let newRoot = root && this.createTopLevelElement(root, contextData);
-    if (newRoot) {
-      this.itemsToCleanUp.add(newRoot);
-      if (oldRoot) {
-        oldRoot.replaceWith(newRoot);
-      } else {
-        this.appendTopLevelElement(newRoot);
-      }
-    } else if (oldRoot) {
-      oldRoot.remove();
-      this.removeSeparatorIfNoTopLevelItems();
+    if (root) {
+      this.createAndInsertTopLevelElements(root, contextData, nextSibling);
     }
+    this.removeSeparatorIfNoTopLevelItems();
   },
 
+  // This should be called once, after constructing the top-level menus, if any.
   afterBuildingMenu(contextData) {
-    if (this.contextData) {
-      // rebuildMenu can trigger us again, but the logic below should run only
-      // once per open menu.
-      return;
-    }
-
     function dispatchOnShownEvent(extension) {
       // Note: gShownMenuItems is a DefaultMap, so .get(extension) causes the
       // extension to be stored in the map even if there are currently no
       // shown menu items. This ensures that the onHidden event can be fired
       // when the menu is closed.
       let menuIds = gShownMenuItems.get(extension);
       extension.emit("webext-menu-shown", menuIds, contextData);
     }
@@ -440,16 +469,24 @@ var gMenuBuilder = {
       dispatchOnShownEvent(contextData.extension);
     } else {
       gOnShownSubscribers.forEach(dispatchOnShownEvent);
     }
 
     this.contextData = contextData;
   },
 
+  hideDefaultMenuItems() {
+    for (let item of this.xulMenu.children) {
+      if (!this.itemsToCleanUp.has(item)) {
+        item.hidden = true;
+      }
+    }
+  },
+
   handleEvent(event) {
     if (this.xulMenu != event.target || event.type != "popuphidden") {
       return;
     }
 
     delete this.xulMenu;
     delete this.contextData;
 
@@ -467,17 +504,17 @@ var gMenuBuilder = {
 
   itemsToCleanUp: new Set(),
 };
 
 // Called from pageAction or browserAction popup.
 global.actionContextMenu = function(contextData) {
   contextData.tab = tabTracker.activeTab;
   contextData.pageUrl = contextData.tab.linkedBrowser.currentURI.spec;
-  gMenuBuilder.buildActionContextMenu(contextData);
+  gMenuBuilder.build(contextData);
 };
 
 const contextsMap = {
   onAudio: "audio",
   onEditable: "editable",
   inFrame: "frame",
   onImage: "image",
   onLink: "link",
--- a/browser/components/extensions/schemas/menus.json
+++ b/browser/components/extensions/schemas/menus.json
@@ -10,16 +10,24 @@
         "$extend": "Permission",
         "choices": [{
           "type": "string",
           "enum": [
             "menus",
             "contextMenus"
           ]
         }]
+      }, {
+        "$extend": "OptionalPermission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "menus.overrideContext"
+          ]
+        }]
       }
     ]
   },
   {
     "namespace": "contextMenus",
     "permissions": ["contextMenus"],
     "description": "Use the browser.contextMenus API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
     "$import": "menus",
@@ -398,16 +406,54 @@
             "name": "callback",
             "optional": true,
             "parameters": [],
             "description": "Called when removal is complete."
           }
         ]
       },
       {
+        "name": "overrideContext",
+        "permissions": ["menus.overrideContext"],
+        "type": "function",
+        "description": "Show the matching menu items from this extension instead of the default menu. This should be called during a 'contextmenu' DOM event handler, and only applies to the menu that opens after this event.",
+        "parameters": [
+          {
+            "name": "contextOptions",
+            "type": "object",
+            "properties": {
+              "showDefaults": {
+                "type": "boolean",
+                "optional": true,
+                "default": false,
+                "description": "Whether to also include default menu items in the menu."
+              },
+              "context": {
+                "type": "string",
+                "enum": ["bookmark", "tab"],
+                "optional": true,
+                "description": "ContextType to override, to allow menu items from other extensions in the menu. Currently only 'bookmark' and 'tab' are supported. showDefaults cannot be used with this option."
+              },
+              "bookmarkId": {
+                "type": "string",
+                "minLength": 1,
+                "optional": true,
+                "description": "Required when context is 'bookmark'. Requires 'bookmark' permission."
+              },
+              "tabId": {
+                "type": "integer",
+                "minimum": 0,
+                "optional": true,
+                "description": "Required when context is 'tab'. Requires 'tabs' permission."
+              }
+            }
+          }
+        ]
+      },
+      {
         "name": "refresh",
         "type": "function",
         "description": "Updates the extension items in the shown menu, including changes that have been made since the menu was shown. Has no effect if the menu is hidden. Rebuilding a shown menu is an expensive operation, only invoke this method when necessary.",
         "async": true,
         "parameters": []
       }
     ],
     "events": [
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -111,16 +111,19 @@ skip-if = (verify && (os == 'linux' || o
 [browser_ext_menus.js]
 [browser_ext_menus_accesskey.js]
 [browser_ext_menus_activeTab.js]
 [browser_ext_menus_capture_secondary_click.js]
 [browser_ext_menus_errors.js]
 [browser_ext_menus_event_order.js]
 [browser_ext_menus_events.js]
 [browser_ext_menus_refresh.js]
+[browser_ext_menus_replace_menu.js]
+[browser_ext_menus_replace_menu_context.js]
+[browser_ext_menus_replace_menu_permissions.js]
 [browser_ext_menus_targetElement.js]
 [browser_ext_menus_targetElement_extension.js]
 [browser_ext_menus_targetElement_shadow.js]
 [browser_ext_menus_visible.js]
 [browser_ext_omnibox.js]
 [browser_ext_openPanel.js]
 skip-if = (verify && !debug && (os == 'linux' || os == 'mac'))
 [browser_ext_optionsPage_browser_style.js]
--- a/browser/components/extensions/test/browser/browser_ext_menus.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus.js
@@ -91,21 +91,28 @@ add_task(async function test_actionConte
     is(popup.children.length, 2, "Correct number of submenu items");
 
     let idPrefix = `${makeWidgetId(extension.id)}-menuitem-_`;
 
     is(second.tagName, "menuitem", "Second menu item type is correct");
     is(second.label, "click 1", "Second menu item title is correct");
     is(second.id, `${idPrefix}1`, "Second menu item id is correct");
 
-    is(last.label, "click 5", "Last menu item title is correct");
-    is(last.id, `${idPrefix}5`, "Last menu item id is correct");
+    is(last.tagName, "menu", "Last menu item type is correct");
+    is(last.label, "Generated extension", "Last menu item title is correct");
+    is(last.getAttribute("ext-type"), "top-level-menu", "Last menu ext-type is correct");
     is(separator.tagName, "menuseparator", "Separator after last menu item");
 
-    await closeActionContextMenu(popup.firstElementChild, kind);
+    // Verify that menu items exceeding ACTION_MENU_TOP_LEVEL_LIMIT are moved into a submenu.
+    let overflowPopup = await openSubmenu(last);
+    is(overflowPopup.children.length, 4, "Excess items should be moved into a submenu");
+    is(overflowPopup.firstElementChild.id, `${idPrefix}5`, "First submenu item ID is correct");
+    is(overflowPopup.lastElementChild.id, `${idPrefix}8`, "Last submenu item ID is correct");
+
+    await closeActionContextMenu(overflowPopup.firstElementChild, kind);
     const {info, tab} = await extension.awaitMessage("click");
     is(info.pageUrl, "http://example.com/", "Click info pageUrl is correct");
     is(tab.id, tabId, "Click event tab ID is correct");
   }
 
   BrowserTestUtils.removeTab(tab);
   await extension.unload();
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
@@ -0,0 +1,325 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+function getVisibleChildrenIds(menuElem) {
+  return Array.from(menuElem.children).filter(elem => !elem.hidden).map(elem => elem.id || elem.tagName);
+}
+
+function checkIsDefaultMenuItemVisible(visibleMenuItemIds) {
+  // In this whole test file, we open a menu on a link. Assume that all
+  // default menu items are shown if one link-specific menu item is shown.
+  ok(visibleMenuItemIds.includes("context-openlink"),
+     `The default 'Open Link in New Tab' menu item should be in ${visibleMenuItemIds}.`);
+}
+
+// Tests the following:
+// - Calling overrideContext({}) during oncontextmenu forces the menu to only
+//   show an extension's own items.
+// - These menu items all appear in the root menu.
+// - The usual extension filtering behavior (e.g. documentUrlPatterns and
+//   targetUrlPatterns) is still applied; some menu items are therefore hidden.
+// - Calling overrideContext({showDefaults:true}) causes the default menu items
+//   to be shown, but only after the extension's.
+// - overrideContext expires after the menu is opened once.
+add_task(async function overrideContext_in_extension_tab() {
+  function extensionTabScript() {
+    document.addEventListener("contextmenu", () => {
+      browser.menus.overrideContext({});
+      browser.test.sendMessage("oncontextmenu_in_dom_part_1");
+    }, {once: true});
+
+    browser.menus.create({
+      id: "tab_1",
+      title: "tab_1",
+      documentUrlPatterns: [document.URL],
+      onclick() {
+        document.addEventListener("contextmenu", () => {
+          // Verifies that last call takes precedence.
+          browser.menus.overrideContext({showDefaults: false});
+          browser.menus.overrideContext({showDefaults: true});
+          browser.test.sendMessage("oncontextmenu_in_dom_part_2");
+        }, {once: true});
+        browser.test.sendMessage("onClicked_tab_1");
+      },
+    });
+    browser.menus.create({
+      id: "tab_2",
+      title: "tab_2",
+      onclick() {
+        browser.test.sendMessage("onClicked_tab_2");
+      },
+    }, () => {
+      browser.test.sendMessage("menu-registered");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["menus", "menus.overrideContext"],
+    },
+    files: {
+      "tab.html": `
+        <!DOCTYPE html><meta charset="utf-8">
+        <a href="http://example.com/">Link</a>
+        <script src="tab.js"></script>
+      `,
+      "tab.js": extensionTabScript,
+    },
+    background() {
+      // Expected to match and thus be visible.
+      browser.menus.create({id: "bg_1", title: "bg_1"});
+      browser.menus.create({id: "bg_2", title: "bg_2", targetUrlPatterns: ["*://example.com/*"]});
+
+      // Expected to not match and be hidden.
+      browser.menus.create({id: "bg_3", title: "bg_3", targetUrlPatterns: ["*://nomatch/*"]});
+      browser.menus.create({id: "bg_4", title: "bg_4", documentUrlPatterns: [document.URL]});
+
+      browser.menus.onShown.addListener(info => {
+        browser.test.assertEq("bg_1,bg_2,tab_1,tab_2", info.menuIds.join(","), "Expected menu items.");
+        browser.test.sendMessage("onShown");
+      });
+
+      browser.tabs.create({url: "tab.html"});
+    },
+  });
+
+  let otherExtension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["menus"],
+    },
+    background() {
+      browser.menus.create({id: "other_extension_item", title: "other_extension_item"}, () => {
+        browser.test.sendMessage("other_extension_item_created");
+      });
+    },
+  });
+  await otherExtension.startup();
+  await otherExtension.awaitMessage("other_extension_item_created");
+
+  let extensionTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+  await extension.startup();
+  // Must wait for the tab to have loaded completely before calling openContextMenu.
+  await extensionTabPromise;
+  await extension.awaitMessage("menu-registered");
+
+  const EXPECTED_EXTENSION_MENU_IDS = [
+    `${makeWidgetId(extension.id)}-menuitem-_bg_1`,
+    `${makeWidgetId(extension.id)}-menuitem-_bg_2`,
+    `${makeWidgetId(extension.id)}-menuitem-_tab_1`,
+    `${makeWidgetId(extension.id)}-menuitem-_tab_2`,
+  ];
+  const OTHER_EXTENSION_MENU_ID =
+    `${makeWidgetId(otherExtension.id)}-menuitem-_other_extension_item`;
+
+  {
+    // Tests overrideContext({})
+    info("Expecting the menu to be replaced by overrideContext.");
+    let menu = await openContextMenu("a");
+    await extension.awaitMessage("oncontextmenu_in_dom_part_1");
+    await extension.awaitMessage("onShown");
+
+    Assert.deepEqual(
+      getVisibleChildrenIds(menu),
+      EXPECTED_EXTENSION_MENU_IDS,
+      "Expected only extension menu items");
+
+    let menuItems = menu.getElementsByAttribute("label", "tab_1");
+    await closeExtensionContextMenu(menuItems[0]);
+    await extension.awaitMessage("onClicked_tab_1");
+  }
+
+  {
+    // Tests overrideContext({showDefaults:true}))
+    info("Expecting the menu to be replaced by overrideContext, including default menu items.");
+    let menu = await openContextMenu("a");
+    await extension.awaitMessage("oncontextmenu_in_dom_part_2");
+    await extension.awaitMessage("onShown");
+
+    let visibleMenuItemIds = getVisibleChildrenIds(menu);
+    Assert.deepEqual(
+      visibleMenuItemIds.slice(0, EXPECTED_EXTENSION_MENU_IDS.length),
+      EXPECTED_EXTENSION_MENU_IDS,
+      "Expected extension menu items at the start.");
+
+    checkIsDefaultMenuItemVisible(visibleMenuItemIds);
+
+    is(visibleMenuItemIds[visibleMenuItemIds.length - 1], OTHER_EXTENSION_MENU_ID,
+       "Other extension menu item should be at the end.");
+
+    let menuItems = menu.getElementsByAttribute("label", "tab_2");
+    await closeExtensionContextMenu(menuItems[0]);
+    await extension.awaitMessage("onClicked_tab_2");
+  }
+
+  {
+    // Tests that previous overrideContext call has been forgotten,
+    // so the default behavior should occur (=move items into submenu).
+    info("Expecting the default menu to be used when overrideContext is not called.");
+    let menu = await openContextMenu("a");
+    await extension.awaitMessage("onShown");
+
+    checkIsDefaultMenuItemVisible(getVisibleChildrenIds(menu));
+
+    let menuItems = menu.getElementsByAttribute("ext-type", "top-level-menu");
+    is(menuItems.length, 1, "Expected top-level menu element for extension.");
+    let topLevelExtensionMenuItem = menuItems[0];
+    is(topLevelExtensionMenuItem.nextSibling, null, "Extension menu should be the last element.");
+
+    const submenu = await openSubmenu(topLevelExtensionMenuItem);
+    is(submenu, topLevelExtensionMenuItem.firstElementChild, "Correct submenu opened");
+
+    Assert.deepEqual(
+      getVisibleChildrenIds(submenu),
+      EXPECTED_EXTENSION_MENU_IDS,
+      "Extension menu items should be in the submenu by default.");
+
+    await closeContextMenu();
+  }
+
+  // Unloading the extension will automatically close the extension's tab.html
+  await extension.unload();
+  await otherExtension.unload();
+});
+
+// Tests some edge cases:
+// - overrideContext() is called without any menu registrations,
+//   followed by a menu registration + menus.refresh..
+// - overrideContext() is called and event.preventDefault() is also
+//   called to stop the menu from appearing.
+// - Open menu again and verify that the default menu behavior occurs.
+add_task(async function overrideContext_sidebar_edge_cases() {
+  function sidebarJs() {
+    const TIME_BEFORE_MENU_SHOWN = Date.now();
+    let count = 0;
+    // eslint-disable-next-line mozilla/balanced-listeners
+    document.addEventListener("contextmenu", event => {
+      ++count;
+      if (count === 1) {
+        browser.menus.overrideContext({});
+      } else if (count === 2) {
+        browser.menus.overrideContext({});
+        event.preventDefault(); // Prevent menu from being shown.
+
+        // We are not expecting a menu. Wait for the time it took to show and
+        // hide the previous menu, to check that no new menu appears.
+        // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+        setTimeout(() => {
+          browser.test.sendMessage("stop_waiting_for_menu_shown", "timer_reached");
+        }, Date.now() - TIME_BEFORE_MENU_SHOWN);
+      } else if (count === 3) {
+        // The overrideContext from the previous call should be forgotten.
+        // Use the default behavior, i.e. show the default menu.
+      } else {
+        browser.test.fail(`Unexpected menu count: ${count}`);
+      }
+
+      browser.test.sendMessage("oncontextmenu_in_dom");
+    });
+
+    browser.menus.onShown.addListener(info => {
+      if (count === 1) {
+        browser.test.assertEq("", info.menuIds.join(","), "Expected no items");
+        browser.menus.create({id: "some_item", title: "some_item"}, () => {
+          browser.test.sendMessage("onShown_1_and_menu_item_created");
+        });
+      } else if (count === 2) {
+        browser.test.fail("onShown should not have fired when the menu is not shown.");
+      } else if (count === 3) {
+        browser.test.assertEq("some_item", info.menuIds.join(","), "Expected menu item");
+        browser.test.sendMessage("onShown_3");
+      } else {
+        browser.test.fail(`Unexpected onShown at count: ${count}`);
+      }
+    });
+
+    browser.test.onMessage.addListener(async msg => {
+      browser.test.assertEq("refresh_menus", msg, "Expected message");
+      browser.test.assertEq(1, count, "Expected at first menu test");
+      await browser.menus.refresh();
+      browser.test.sendMessage("menus_refreshed");
+    });
+
+    browser.menus.onHidden.addListener(() => {
+      browser.test.sendMessage("onHidden", count);
+    });
+
+    browser.test.sendMessage("sidebar_ready");
+  }
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "temporary", // To automatically show sidebar on load.
+    manifest: {
+      permissions: ["menus", "menus.overrideContext"],
+      sidebar_action: {
+        default_panel: "sidebar.html",
+      },
+    },
+    files: {
+      "sidebar.html": `
+        <!DOCTYPE html><meta charset="utf-8">
+        <a href="http://example.com/">Link</a>
+        <script src="sidebar.js"></script>
+      `,
+      "sidebar.js": sidebarJs,
+    },
+    background() {
+      browser.test.assertThrows(
+        () => { browser.menus.overrideContext({someInvalidParameter: true}); },
+        /Unexpected property "someInvalidParameter"/,
+        "overrideContext should be available and the parameters be validated.");
+      browser.test.assertThrows(
+        () => { browser.menus.overrideContext({}); },
+        /overrideContext must be called during a "contextmenu" event/,
+        "overrideContext should fail outside of a 'contextmenu' event.");
+      browser.test.sendMessage("bg_test_done");
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitMessage("bg_test_done");
+  await extension.awaitMessage("sidebar_ready");
+
+  const EXPECTED_EXTENSION_MENU_ID =
+    `${makeWidgetId(extension.id)}-menuitem-_some_item`;
+
+  {
+    // Checks that a menu can initially be empty and be updated.
+    info("Expecting menu without items to appear and be updated after menus.refresh()");
+    let menu = await openContextMenuInSidebar("a");
+    await extension.awaitMessage("oncontextmenu_in_dom");
+    await extension.awaitMessage("onShown_1_and_menu_item_created");
+    Assert.deepEqual(getVisibleChildrenIds(menu), [], "Expected no items, initially");
+    extension.sendMessage("refresh_menus");
+    await extension.awaitMessage("menus_refreshed");
+    Assert.deepEqual(getVisibleChildrenIds(menu), [EXPECTED_EXTENSION_MENU_ID], "Expected updated menu");
+    await closeContextMenu(menu);
+    is(await extension.awaitMessage("onHidden"), 1, "Menu hidden");
+  }
+
+  {
+    // Trigger a context menu. The page has prevented the menu from being
+    // shown, so the promise should not resolve.
+    info("Expecting menu to not appear because of event.preventDefault()");
+    let popupShowingPromise = openContextMenuInSidebar("a");
+    await extension.awaitMessage("oncontextmenu_in_dom");
+    is(await Promise.race([
+      extension.awaitMessage("stop_waiting_for_menu_shown"),
+      popupShowingPromise.then(() => "popup_shown"),
+    ]), "timer_reached", "The menu should not be shown.");
+  }
+
+  {
+    info("Expecting default menu to be shown when the menu is reopened after event.preventDefault()");
+    let menu = await openContextMenuInSidebar("a");
+    await extension.awaitMessage("oncontextmenu_in_dom");
+    await extension.awaitMessage("onShown_3");
+    let visibleMenuItemIds = getVisibleChildrenIds(menu);
+    checkIsDefaultMenuItemVisible(visibleMenuItemIds);
+    ok(visibleMenuItemIds.includes(EXPECTED_EXTENSION_MENU_ID), "Expected extension menu item");
+    await closeContextMenu(menu);
+    is(await extension.awaitMessage("onHidden"), 3, "Menu hidden");
+  }
+
+  await extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
@@ -0,0 +1,245 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+function getVisibleChildrenIds(menuElem) {
+  return Array.from(menuElem.children).filter(elem => !elem.hidden).map(elem => elem.id || elem.tagName);
+}
+
+function checkIsDefaultMenuItemVisible(visibleMenuItemIds) {
+  // In this whole test file, we open a menu on a link. Assume that all
+  // default menu items are shown if one link-specific menu item is shown.
+  ok(visibleMenuItemIds.includes("context-openlink"),
+     `The default 'Open Link in New Tab' menu item should be in ${visibleMenuItemIds}.`);
+}
+
+// Tests that the context of an extension menu can be changed to:
+// - tab
+// - bookmark
+add_task(async function overrideContext_with_context() {
+  // Background script of the main test extension and the auxilary other extension.
+  function background() {
+    browser.test.onMessage.addListener(async (msg, tabId) => {
+      browser.test.assertEq("testTabAccess", msg, `Expected message in ${browser.runtime.id}`);
+      let tab = await browser.tabs.get(tabId);
+      if (!tab.url) { // tabs or activeTab not active.
+        browser.test.sendMessage("testTabAccessDone", "tab_no_url");
+        return;
+      }
+      try {
+        let [url] = await browser.tabs.executeScript(tabId, {
+          code: "document.URL",
+        });
+        browser.test.assertEq("http://example.com/?SomeTab", url, "Expected successful executeScript");
+        browser.test.sendMessage("testTabAccessDone", "executeScript_ok");
+        return;
+      } catch (e) {
+        browser.test.assertEq("Missing host permission for the tab", e.message, "Expected error message");
+        browser.test.sendMessage("testTabAccessDone", "executeScript_failed");
+      }
+    });
+    browser.menus.onShown.addListener((info, tab) => {
+      browser.test.sendMessage("onShown", {
+        menuIds: info.menuIds,
+        contexts: info.contexts,
+        bookmarkId: info.bookmarkId,
+        tabId: tab && tab.id,
+      });
+    });
+    browser.menus.onClicked.addListener((info, tab) => {
+      browser.test.sendMessage("onClicked", {
+        menuItemId: info.menuItemId,
+        bookmarkId: info.bookmarkId,
+        tabId: tab && tab.id,
+      });
+    });
+    browser.menus.create({id: "tab_context", title: "tab_context", contexts: ["tab"]});
+    browser.menus.create({id: "bookmark_context", title: "bookmark_context", contexts: ["bookmark"]});
+    browser.menus.create({id: "link_context", title: "link_context"}, () => {
+      browser.test.sendMessage("menu_items_registered");
+    });
+
+    if (browser.runtime.id === "@menu-test-extension") {
+      browser.tabs.create({url: "tab.html"});
+    }
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      applications: {gecko: {id: "@menu-test-extension"}},
+      permissions: ["menus", "menus.overrideContext", "tabs", "bookmarks"],
+    },
+    files: {
+      "tab.html": `
+        <!DOCTYPE html><meta charset="utf-8">
+        <a href="http://example.com/">Link</a>
+        <script src="tab.js"></script>
+      `,
+      "tab.js": async () => {
+        let [tab] = await browser.tabs.query({
+          url: "http://example.com/?SomeTab",
+        });
+        let bookmark = await browser.bookmarks.create({
+          title: "Bookmark for menu test",
+          url: "http://example.com/bookmark",
+        });
+        let testCases = [{
+          context: "tab",
+          tabId: tab.id,
+        }, {
+          context: "tab",
+          tabId: tab.id,
+        }, {
+          context: "bookmark",
+          bookmarkId: bookmark.id,
+        }, {
+          context: "tab",
+          tabId: 123456789, // Some invalid tabId.
+        }];
+
+        // eslint-disable-next-line mozilla/balanced-listeners
+        document.addEventListener("contextmenu", () => {
+          browser.menus.overrideContext(testCases.shift());
+          browser.test.sendMessage("oncontextmenu_in_dom");
+        });
+
+        browser.test.sendMessage("setup_ready", {
+          bookmarkId: bookmark.id,
+          tabId: tab.id,
+        });
+      },
+    },
+    background,
+  });
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/?SomeTab");
+
+  let otherExtension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      applications: {gecko: {id: "@other-test-extension"}},
+      permissions: ["menus", "bookmarks", "activeTab"],
+    },
+    background,
+  });
+  await otherExtension.startup();
+  await otherExtension.awaitMessage("menu_items_registered");
+
+  await extension.startup();
+  await extension.awaitMessage("menu_items_registered");
+
+  let {bookmarkId, tabId} = await extension.awaitMessage("setup_ready");
+  info(`Set up test with tabId=${tabId} and bookmarkId=${bookmarkId}.`);
+
+  {
+    // Test case 1: context=tab
+    let menu = await openContextMenu("a");
+    await extension.awaitMessage("oncontextmenu_in_dom");
+    for (let ext of [extension, otherExtension]) {
+      info(`Testing menu from ${ext.id} after changing context to tab`);
+      Assert.deepEqual(await ext.awaitMessage("onShown"), {
+        menuIds: ["tab_context"],
+        contexts: ["tab"],
+        bookmarkId: undefined,
+        tabId,
+      }, "Expected onShown details after changing context to tab");
+    }
+    Assert.deepEqual(getVisibleChildrenIds(menu), [
+      `${makeWidgetId(extension.id)}-menuitem-_tab_context`,
+      `menuseparator`,
+      `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context`,
+    ], "Expected menu items after changing context to tab");
+
+    extension.sendMessage("testTabAccess", tabId);
+    is(await extension.awaitMessage("testTabAccessDone"),
+       "executeScript_failed",
+       "executeScript should fail due to the lack of permissions.");
+
+    otherExtension.sendMessage("testTabAccess", tabId);
+    is(await otherExtension.awaitMessage("testTabAccessDone"),
+       "tab_no_url",
+       "Other extension should not have activeTab permissions yet.");
+
+    // Click on the menu item of the other extension to unlock host permissions.
+    let menuItems = menu.getElementsByAttribute("label", "tab_context");
+    is(menuItems.length, 2, "There are two menu items with label 'tab_context'");
+    await closeExtensionContextMenu(menuItems[1]);
+
+    Assert.deepEqual(await otherExtension.awaitMessage("onClicked"), {
+      menuItemId: "tab_context",
+      bookmarkId: undefined,
+      tabId,
+    }, "Expected onClicked details after changing context to tab");
+
+    extension.sendMessage("testTabAccess", tabId);
+    is(await extension.awaitMessage("testTabAccessDone"),
+       "executeScript_failed",
+       "executeScript of extension that created the menu should still fail.");
+
+    otherExtension.sendMessage("testTabAccess", tabId);
+    is(await otherExtension.awaitMessage("testTabAccessDone"),
+       "executeScript_ok",
+       "Other extension should have activeTab permissions.");
+  }
+
+  {
+    // Test case 2: context=tab, click on menu item of extension..
+    let menu = await openContextMenu("a");
+    await extension.awaitMessage("oncontextmenu_in_dom");
+
+    // The previous test has already verified the visible menu items,
+    // so we skip checking the onShown result and only test clicking.
+    await extension.awaitMessage("onShown");
+    await otherExtension.awaitMessage("onShown");
+    let menuItems = menu.getElementsByAttribute("label", "tab_context");
+    is(menuItems.length, 2, "There are two menu items with label 'tab_context'");
+    await closeExtensionContextMenu(menuItems[0]);
+
+    Assert.deepEqual(await extension.awaitMessage("onClicked"), {
+      menuItemId: "tab_context",
+      bookmarkId: undefined,
+      tabId,
+    }, "Expected onClicked details after changing context to tab");
+
+    extension.sendMessage("testTabAccess", tabId);
+    is(await extension.awaitMessage("testTabAccessDone"),
+       "executeScript_failed",
+       "activeTab permission should not be available to the extension that created the menu.");
+  }
+
+  {
+    // Test case 3: context=bookmark
+    let menu = await openContextMenu("a");
+    await extension.awaitMessage("oncontextmenu_in_dom");
+    for (let ext of [extension, otherExtension]) {
+      info(`Testing menu from ${ext.id} after changing context to bookmark`);
+      let shownInfo = await ext.awaitMessage("onShown");
+      Assert.deepEqual(shownInfo, {
+        menuIds: ["bookmark_context"],
+        contexts: ["bookmark"],
+        bookmarkId,
+        tabId: undefined,
+      }, "Expected onShown details after changing context to bookmark");
+    }
+    Assert.deepEqual(getVisibleChildrenIds(menu), [
+      `${makeWidgetId(extension.id)}-menuitem-_bookmark_context`,
+      `menuseparator`,
+      `${makeWidgetId(otherExtension.id)}-menuitem-_bookmark_context`,
+    ], "Expected menu items after changing context to bookmark");
+    await closeContextMenu(menu);
+  }
+
+  {
+    // Test case 4: context=tab, invalid tabId.
+    let menu = await openContextMenu("a");
+    await extension.awaitMessage("oncontextmenu_in_dom");
+    // When an invalid tabId is used, all extension menu logic is skipped and
+    // the default menu is shown.
+    checkIsDefaultMenuItemVisible(getVisibleChildrenIds(menu));
+    await closeContextMenu(menu);
+  }
+
+  await extension.unload();
+  await otherExtension.unload();
+  BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function auto_approve_optional_permissions() {
+  // Auto-approve optional permission requests, without UI.
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.webextOptionalPermissionPrompts", false]],
+  });
+  // TODO: Consider an observer for "webextension-optional-permission-prompt"
+  // once bug 1493396 is fixed.
+});
+
+add_task(async function overrideContext_permissions() {
+  function sidebarJs() {
+    // If the extension has the right permissions, calling
+    // menus.overrideContext with one of the following should not throw.
+    const CONTEXT_OPTIONS_TAB = {context: "tab", tabId: 1};
+    const CONTEXT_OPTIONS_BOOKMARK = {context: "bookmark", bookmarkId: "x"};
+
+    const E_PERM_TAB = /The "tab" context requires the "tabs" permission/;
+    const E_PERM_BOOKMARK = /The "bookmark" context requires the "bookmarks" permission/;
+
+    function assertAllowed(contextOptions) {
+      try {
+        let result = browser.menus.overrideContext(contextOptions);
+        browser.test.assertEq(undefined, result, `Allowed menu for context=${contextOptions.context}`);
+      } catch (e) {
+        browser.test.fail(`Unexpected error for context=${contextOptions.context}: ${e}`);
+      }
+    }
+
+    function assertNotAllowed(contextOptions, expectedError) {
+      browser.test.assertThrows(() => {
+        browser.menus.overrideContext(contextOptions);
+      }, expectedError, `Expected error for context=${contextOptions.context}`);
+    }
+
+    async function requestPermissions(permissions) {
+      try {
+        let permPromise;
+        window.withHandlingUserInputForPermissionRequestTest(() => {
+          permPromise = browser.permissions.request(permissions);
+        });
+        browser.test.assertTrue(await permPromise, `Should have granted ${JSON.stringify(permissions)}`);
+      } catch (e) {
+        browser.test.fail(`Failed to use permissions.request(${JSON.stringify(permissions)}): ${e}`);
+      }
+    }
+
+    // The menus.overrideContext method can only be called during a
+    // "contextmenu" event. So we use a generator to run tests, and yield
+    // before we call overrideContext after an asynchronous operation.
+    let testGenerator = (async function* () {
+      browser.test.assertEq(undefined, browser.menus.overrideContext,
+                            "menus.overrideContext requires the 'menus.overrideContext' permission");
+      await requestPermissions({permissions: ["menus.overrideContext"]});
+      yield;
+
+      // context without required property.
+      browser.test.assertThrows(
+        () => { browser.menus.overrideContext({context: "tab"}); },
+        /Property "tabId" is required for context "tab"/,
+        "Required property for context tab");
+      browser.test.assertThrows(
+        () => { browser.menus.overrideContext({context: "bookmark"}); },
+        /Property "bookmarkId" is required for context "bookmark"/,
+        "Required property for context bookmarks");
+
+      // context with too many properties.
+      browser.test.assertThrows(
+        () => { browser.menus.overrideContext({context: "bookmark", bookmarkId: "x", tabId: 1}); },
+        /Property "tabId" can only be used with context "tab"/,
+        "Invalid property for context bookmarks");
+      browser.test.assertThrows(
+        () => { browser.menus.overrideContext({context: "bookmark", bookmarkId: "x", showDefaults: true}); },
+        /Property "showDefaults" cannot be used with context "bookmark"/,
+        "showDefaults cannot be used with context bookmark");
+
+      // context with right properties, but missing permissions.
+      assertNotAllowed(CONTEXT_OPTIONS_BOOKMARK, E_PERM_BOOKMARK);
+      assertNotAllowed(CONTEXT_OPTIONS_TAB, E_PERM_TAB);
+
+      await requestPermissions({permissions: ["bookmarks"]});
+      browser.test.log("Active permissions: bookmarks");
+      yield;
+
+      assertAllowed(CONTEXT_OPTIONS_BOOKMARK);
+      assertNotAllowed(CONTEXT_OPTIONS_TAB, E_PERM_TAB);
+
+      await requestPermissions({permissions: ["tabs"]});
+      await browser.permissions.remove({permissions: ["bookmarks"]});
+      browser.test.log("Active permissions: tabs");
+      yield;
+
+      assertNotAllowed(CONTEXT_OPTIONS_BOOKMARK, E_PERM_BOOKMARK);
+      assertAllowed(CONTEXT_OPTIONS_TAB);
+      await browser.permissions.remove({permissions: ["tabs"]});
+      browser.test.log("Active permissions: none");
+      yield;
+
+      assertNotAllowed(CONTEXT_OPTIONS_TAB, E_PERM_TAB);
+
+      await browser.permissions.remove({permissions: ["menus.overrideContext"]});
+      browser.test.assertEq(undefined, browser.menus.overrideContext,
+                            "menus.overrideContext is unavailable after revoking the permission");
+    })();
+
+    // eslint-disable-next-line mozilla/balanced-listeners
+    document.addEventListener("contextmenu", async event => {
+      event.preventDefault();
+      try {
+        let {done} = await testGenerator.next();
+        browser.test.sendMessage("continue_test", !done);
+      } catch (e) {
+        browser.test.fail(`Unexpected error: ${e} :: ${e.stack}`);
+        browser.test.sendMessage("continue_test", false);
+      }
+    });
+    browser.test.sendMessage("sidebar_ready");
+  }
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "temporary", // To automatically show sidebar on load.
+    manifest: {
+      permissions: ["menus"],
+      optional_permissions: ["menus.overrideContext", "tabs", "bookmarks"],
+      sidebar_action: {
+        default_panel: "sidebar.html",
+      },
+    },
+    files: {
+      "sidebar.html": `
+        <!DOCTYPE html><meta charset="utf-8">
+        <a href="http://example.com/">Link</a>
+        <script src="sidebar.js"></script>
+      `,
+      "sidebar.js": sidebarJs,
+    },
+  });
+  await extension.startup();
+  await extension.awaitMessage("sidebar_ready");
+
+  // permissions.request requires user input, export helper.
+  await ContentTask.spawn(SidebarUI.browser.contentDocument.getElementById("webext-panels-browser"), null, () => {
+    let {withHandlingUserInput} = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm", {}).ExtensionCommon;
+    Cu.exportFunction((fn) => {
+      return withHandlingUserInput(content, fn);
+    }, content, {
+      defineAs: "withHandlingUserInputForPermissionRequestTest",
+    });
+  });
+
+  do {
+    info(`Going to trigger "contextmenu" event.`);
+    // The menu is never shown, so don't await the returned promise.
+    openContextMenuInSidebar("a");
+  } while (await extension.awaitMessage("continue_test"));
+
+  await extension.unload();
+});
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -11,496 +11,21 @@
 %browserDTD;
 ]>
 
 <bindings id="SearchBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
-  <binding id="searchbar">
-    <content>
-      <xul:stringbundle src="chrome://browser/locale/search.properties"
-                        anonid="searchbar-stringbundle"/>
-      <!--
-      There is a dependency between "maxrows" attribute and
-      "SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing
-      one of them requires changing the other one.
-      -->
-      <xul:textbox class="searchbar-textbox"
-                   anonid="searchbar-textbox"
-                   type="autocomplete"
-                   inputtype="search"
-                   placeholder="&searchInput.placeholder;"
-                   flex="1"
-                   autocompletepopup="PopupSearchAutoComplete"
-                   autocompletesearch="search-autocomplete"
-                   autocompletesearchparam="searchbar-history"
-                   maxrows="10"
-                   completeselectedindex="true"
-                   minresultsforpopup="0"
-                   xbl:inherits="disabled,disableautocomplete,searchengine,src,newlines">
-        <!--
-        Empty <box> to properly position the icon within the autocomplete
-        binding's anonymous children (the autocomplete binding positions <box>
-        children differently)
-        -->
-        <xul:box>
-          <xul:hbox class="searchbar-search-button"
-                    anonid="searchbar-search-button"
-                    xbl:inherits="addengines"
-                    tooltiptext="&searchIcon.tooltip;">
-            <xul:image class="searchbar-search-icon"/>
-            <xul:image class="searchbar-search-icon-overlay"/>
-          </xul:hbox>
-        </xul:box>
-        <xul:hbox class="search-go-container">
-          <xul:image class="search-go-button urlbar-icon" hidden="true"
-                     anonid="search-go-button"
-                     onclick="handleSearchCommand(event);"
-                     tooltiptext="&contentSearchSubmit.tooltip;"/>
-        </xul:hbox>
-      </xul:textbox>
-    </content>
-
-    <implementation implements="nsIObserver">
-      <constructor><![CDATA[
-        if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
-          return;
-
-        Services.obs.addObserver(this, "browser-search-engine-modified");
-        Services.obs.addObserver(this, "browser-search-service");
-
-        this._initialized = true;
-
-        (window.delayedStartupPromise || Promise.resolve()).then(() => {
-          window.requestIdleCallback(() => {
-            Services.search.init(aStatus => {
-              // Bail out if the binding's been destroyed
-              if (!this._initialized)
-                return;
-
-              if (Components.isSuccessCode(aStatus)) {
-                // Refresh the display (updating icon, etc)
-                this.updateDisplay();
-                BrowserSearch.updateOpenSearchBadge();
-              } else {
-                Cu.reportError("Cannot initialize search service, bailing out: " + aStatus);
-              }
-            });
-          });
-        });
-
-        // Wait until the popupshowing event to avoid forcing immediate
-        // attachment of the search-one-offs binding.
-        this.textbox.popup.addEventListener("popupshowing", () => {
-          let oneOffButtons = this.textbox.popup.oneOffButtons;
-          // Some accessibility tests create their own <searchbar> that doesn't
-          // use the popup binding below, so null-check oneOffButtons.
-          if (oneOffButtons) {
-            oneOffButtons.telemetryOrigin = "searchbar";
-            // Set .textbox first, since the popup setter will cause
-            // a _rebuild call that uses it.
-            oneOffButtons.textbox = this.textbox;
-            oneOffButtons.popup = this.textbox.popup;
-          }
-        }, {capture: true, once: true});
-      ]]></constructor>
-
-      <destructor><![CDATA[
-        this.destroy();
-      ]]></destructor>
-
-      <method name="destroy">
-        <body><![CDATA[
-        if (this._initialized) {
-          this._initialized = false;
-
-          Services.obs.removeObserver(this, "browser-search-engine-modified");
-          Services.obs.removeObserver(this, "browser-search-service");
-        }
-
-        // Make sure to break the cycle from _textbox to us. Otherwise we leak
-        // the world. But make sure it's actually pointing to us.
-        // Also make sure the textbox has ever been constructed, otherwise the
-        // _textbox getter will cause the textbox constructor to run, add an
-        // observer, and leak the world too.
-        if (this._textboxInitialized && this._textbox.mController.input == this)
-          this._textbox.mController.input = null;
-        ]]></body>
-      </method>
-
-      <field name="_ignoreFocus">false</field>
-      <field name="_clickClosedPopup">false</field>
-      <field name="_stringBundle">document.getAnonymousElementByAttribute(this,
-          "anonid", "searchbar-stringbundle");</field>
-      <field name="_textboxInitialized">false</field>
-      <field name="_textbox">document.getAnonymousElementByAttribute(this,
-          "anonid", "searchbar-textbox");</field>
-      <field name="_engines">null</field>
-      <field name="FormHistory" readonly="true">
-        (ChromeUtils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
-      </field>
-
-      <property name="engines" readonly="true">
-        <getter><![CDATA[
-          if (!this._engines)
-            this._engines = Services.search.getVisibleEngines();
-          return this._engines;
-        ]]></getter>
-      </property>
-
-      <property name="currentEngine">
-        <setter><![CDATA[
-          Services.search.currentEngine = val;
-          return val;
-        ]]></setter>
-        <getter><![CDATA[
-          var currentEngine = Services.search.currentEngine;
-          // Return a dummy engine if there is no currentEngine
-          return currentEngine || {name: "", uri: null};
-        ]]></getter>
-      </property>
-
-      <!-- textbox is used by sanitize.js to clear the undo history when
-           clearing form information. -->
-      <property name="textbox" readonly="true"
-                onget="return this._textbox;"/>
-
-      <property name="value" onget="return this._textbox.value;"
-                             onset="return this._textbox.value = val;"/>
-
-      <method name="focus">
-        <body><![CDATA[
-          this._textbox.focus();
-        ]]></body>
-      </method>
-
-      <method name="select">
-        <body><![CDATA[
-          this._textbox.select();
-        ]]></body>
-      </method>
-
-      <method name="observe">
-        <parameter name="aEngine"/>
-        <parameter name="aTopic"/>
-        <parameter name="aVerb"/>
-        <body><![CDATA[
-          if (aTopic == "browser-search-engine-modified" ||
-              (aTopic == "browser-search-service" && aVerb == "init-complete")) {
-            // Make sure the engine list is refetched next time it's needed
-            this._engines = null;
-
-            // Update the popup header and update the display after any modification.
-            this._textbox.popup.updateHeader();
-            this.updateDisplay();
-          }
-        ]]></body>
-      </method>
-
-      <method name="setIcon">
-        <parameter name="element"/>
-        <parameter name="uri"/>
-        <body><![CDATA[
-          element.setAttribute("src", uri);
-        ]]></body>
-      </method>
-
-      <method name="updateDisplay">
-        <body><![CDATA[
-          var uri = this.currentEngine.iconURI;
-          this.setIcon(this, uri ? uri.spec : "");
-
-          var name = this.currentEngine.name;
-          var text = this._stringBundle.getFormattedString("searchtip", [name]);
-          this._textbox.label = text;
-          this._textbox.tooltipText = text;
-        ]]></body>
-      </method>
-
-      <method name="updateGoButtonVisibility">
-        <body><![CDATA[
-          document.getAnonymousElementByAttribute(this, "anonid",
-                                                  "search-go-button")
-                  .hidden = !this._textbox.value;
-        ]]></body>
-      </method>
-
-      <method name="openSuggestionsPanel">
-        <parameter name="aShowOnlySettingsIfEmpty"/>
-        <body><![CDATA[
-          if (this._textbox.open)
-            return;
-
-          this._textbox.showHistoryPopup();
-
-          if (this._textbox.value) {
-            // showHistoryPopup does a startSearch("") call, ensure the
-            // controller handles the text from the input box instead:
-            this._textbox.mController.handleText();
-          } else if (aShowOnlySettingsIfEmpty) {
-            this.setAttribute("showonlysettings", "true");
-          }
-        ]]></body>
-      </method>
-
-      <method name="selectEngine">
-        <parameter name="aEvent"/>
-        <parameter name="isNextEngine"/>
-        <body><![CDATA[
-          // Find the new index
-          var newIndex = this.engines.indexOf(this.currentEngine);
-          newIndex += isNextEngine ? 1 : -1;
-
-          if (newIndex >= 0 && newIndex < this.engines.length) {
-            this.currentEngine = this.engines[newIndex];
-          }
-
-          aEvent.preventDefault();
-          aEvent.stopPropagation();
-
-          this.openSuggestionsPanel();
-        ]]></body>
-      </method>
-
-      <method name="handleSearchCommand">
-        <parameter name="aEvent"/>
-        <parameter name="aEngine"/>
-        <parameter name="aForceNewTab"/>
-        <body><![CDATA[
-          var where = "current";
-          let params;
-
-          // Open ctrl/cmd clicks on one-off buttons in a new background tab.
-          if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
-            if (aEvent.button == 2)
-              return;
-            where = whereToOpenLink(aEvent, false, true);
-          } else if (aForceNewTab) {
-            where = "tab";
-            if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
-              where += "-background";
-          } else {
-            var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
-            if (((aEvent instanceof KeyboardEvent && aEvent.altKey) ^ newTabPref) &&
-                !isTabEmpty(gBrowser.selectedTab)) {
-              where = "tab";
-            }
-            if ((aEvent instanceof MouseEvent) &&
-                (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
-              where = "tab";
-              params = {
-                inBackground: true,
-              };
-            }
-          }
-
-          this.handleSearchCommandWhere(aEvent, aEngine, where, params);
-        ]]></body>
-      </method>
-
-      <method name="handleSearchCommandWhere">
-        <parameter name="aEvent"/>
-        <parameter name="aEngine"/>
-        <parameter name="aWhere"/>
-        <parameter name="aParams"/>
-        <body><![CDATA[
-          var textBox = this._textbox;
-          var textValue = textBox.value;
-
-          let selection = this.telemetrySearchDetails;
-          let oneOffRecorded = false;
-
-          BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
-            aEvent,
-            selection ? selection.index : -1
-          );
-
-          if (!selection || (selection.index == -1)) {
-            oneOffRecorded = this.textbox.popup.oneOffButtons
-                                 .maybeRecordTelemetry(aEvent, aWhere, aParams);
-            if (!oneOffRecorded) {
-              let source = "unknown";
-              let type = "unknown";
-              let target = aEvent.originalTarget;
-              if (aEvent instanceof KeyboardEvent) {
-                type = "key";
-              } else if (aEvent instanceof MouseEvent) {
-                type = "mouse";
-                if (target.classList.contains("search-panel-header") ||
-                    target.parentNode.classList.contains("search-panel-header")) {
-                  source = "header";
-                }
-              } else if (aEvent instanceof XULCommandEvent) {
-                if (target.getAttribute("anonid") == "paste-and-search") {
-                  source = "paste";
-                }
-              }
-              if (!aEngine) {
-                aEngine = this.currentEngine;
-              }
-              BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
-                                                          aWhere);
-            }
-          }
-
-          // This is a one-off search only if oneOffRecorded is true.
-          this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded);
-
-          if (aWhere == "tab" && aParams && aParams.inBackground)
-            this.focus();
-        ]]></body>
-      </method>
-
-      <method name="doSearch">
-        <parameter name="aData"/>
-        <parameter name="aWhere"/>
-        <parameter name="aEngine"/>
-        <parameter name="aParams"/>
-        <parameter name="aOneOff"/>
-        <body><![CDATA[
-          var textBox = this._textbox;
-
-          // Save the current value in the form history
-          if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
-            this.FormHistory.update(
-              { op: "bump",
-                fieldname: textBox.getAttribute("autocompletesearchparam"),
-                value: aData },
-              { handleError(aError) {
-                  Cu.reportError("Saving search to form history failed: " + aError.message);
-              }});
-          }
-
-          let engine = aEngine || this.currentEngine;
-          var submission = engine.getSubmission(aData, null, "searchbar");
-          let telemetrySearchDetails = this.telemetrySearchDetails;
-          this.telemetrySearchDetails = null;
-          if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
-            telemetrySearchDetails = null;
-          }
-          // If we hit here, we come either from a one-off, a plain search or a suggestion.
-          const details = {
-            isOneOff: aOneOff,
-            isSuggestion: (!aOneOff && telemetrySearchDetails),
-            selection: telemetrySearchDetails,
-          };
-          BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
-          // null parameter below specifies HTML response for search
-          let params = {
-            postData: submission.postData,
-          };
-          if (aParams) {
-            for (let key in aParams) {
-              params[key] = aParams[key];
-            }
-          }
-          openTrustedLinkIn(submission.uri.spec, aWhere, params);
-        ]]></body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <handler event="command"><![CDATA[
-        const target = event.originalTarget;
-        if (target.engine) {
-          this.currentEngine = target.engine;
-        } else if (target.classList.contains("addengine-item")) {
-          // Select the installed engine if the installation succeeds
-          var installCallback = {
-            onSuccess: engine => this.currentEngine = engine,
-          };
-          Services.search.addEngine(target.getAttribute("uri"), null,
-                                    target.getAttribute("src"), false,
-                                    installCallback);
-        } else
-          return;
-
-        this.focus();
-        this.select();
-      ]]></handler>
-
-      <handler event="DOMMouseScroll"
-               phase="capturing"
-               modifiers="accel"
-               action="this.selectEngine(event, (event.detail > 0));"/>
-
-      <handler event="input" action="this.updateGoButtonVisibility();"/>
-      <handler event="drop" action="this.updateGoButtonVisibility();"/>
-
-      <handler event="blur">
-      <![CDATA[
-        // If the input field is still focused then a different window has
-        // received focus, ignore the next focus event.
-        this._ignoreFocus = (document.activeElement == this._textbox.inputField);
-      ]]></handler>
-
-      <handler event="focus">
-      <![CDATA[
-        // Speculatively connect to the current engine's search URI (and
-        // suggest URI, if different) to reduce request latency
-        this.currentEngine.speculativeConnect({window,
-                                               originAttributes: gBrowser.contentPrincipal
-                                                                         .originAttributes});
-
-        if (this._ignoreFocus) {
-          // This window has been re-focused, don't show the suggestions
-          this._ignoreFocus = false;
-          return;
-        }
-
-        // Don't open the suggestions if there is no text in the textbox.
-        if (!this._textbox.value)
-          return;
-
-        // Don't open the suggestions if the mouse was used to focus the
-        // textbox, that will be taken care of in the click handler.
-        if (Services.focus.getLastFocusMethod(window) & Services.focus.FLAG_BYMOUSE)
-          return;
-
-        this.openSuggestionsPanel();
-      ]]></handler>
-
-      <handler event="mousedown" phase="capturing">
-      <![CDATA[
-        if (event.originalTarget.getAttribute("anonid") == "searchbar-search-button") {
-          this._clickClosedPopup = this._textbox.popup._isHiding;
-        }
-      ]]></handler>
-
-      <handler event="mousedown" button="0">
-      <![CDATA[
-        // Ignore clicks on the search go button.
-        if (event.originalTarget.getAttribute("anonid") == "search-go-button") {
-          return;
-        }
-
-        let isIconClick = event.originalTarget.getAttribute("anonid") == "searchbar-search-button";
-
-        // Ignore clicks on the icon if they were made to close the popup
-        if (isIconClick && this._clickClosedPopup) {
-          return;
-        }
-
-        // Open the suggestions whenever clicking on the search icon or if there
-        // is text in the textbox.
-        if (isIconClick || this._textbox.value) {
-          this.openSuggestionsPanel(true);
-        }
-      ]]></handler>
-
-    </handlers>
-  </binding>
-
   <binding id="searchbar-textbox"
       extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
     <implementation>
       <constructor><![CDATA[
-        if (document.getBindingParent(this).parentNode.parentNode.localName ==
+        if (this.closest("searchbar").parentNode.parentNode.localName ==
             "toolbarpaletteitem")
           return;
 
         if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
           this.setAttribute("clickSelectsAll", true);
 
         var textBox = document.getAnonymousElementByAttribute(this,
                                               "anonid", "moz-input-box");
@@ -508,34 +33,34 @@
         // Force the Custom Element to upgrade until Bug 1470242 handles this:
         customElements.upgrade(textBox);
         var cxmenu = textBox.menupopup;
         cxmenu.addEventListener("popupshowing",
                                 () => { this.initContextMenu(cxmenu); },
                                 {capture: true, once: true});
 
         this.setAttribute("aria-owns", this.popup.id);
-        document.getBindingParent(this)._textboxInitialized = true;
+        this.closest("searchbar")._textboxInitialized = true;
       ]]></constructor>
 
       <destructor><![CDATA[
         // If the context menu has never been opened, there won't be anything
         // to remove here.
         // Also, XBL and the customize toolbar code sometimes interact poorly.
         try {
           this.controllers.removeController(this.searchbarController);
         } catch (ex) { }
       ]]></destructor>
 
       // Add items to context menu and attach controller to handle them the
       // first time the context menu is opened.
       <method name="initContextMenu">
         <parameter name="aMenu"/>
         <body><![CDATA[
-          let stringBundle = document.getBindingParent(this)._stringBundle;
+          let stringBundle = this.closest("searchbar")._stringBundle;
 
           let pasteAndSearch, suggestMenuItem;
           let element, label, akey;
 
           element = document.createXULElement("menuseparator");
           aMenu.appendChild(element);
 
           let insertLocation = aMenu.firstElementChild;
@@ -681,17 +206,17 @@
           }
         ]]></body>
       </method>
 
       <method name="openSearch">
         <body>
           <![CDATA[
             if (!this.popupOpen) {
-              document.getBindingParent(this).openSuggestionsPanel();
+              this.closest("searchbar").openSuggestionsPanel();
               return false;
             }
             return true;
           ]]>
         </body>
       </method>
 
       <method name="handleEnter">
@@ -724,17 +249,17 @@
               return;
             }
             engine = oneOff.engine;
           }
           if (this._selectionDetails) {
             BrowserSearch.searchBar.telemetrySearchDetails = this._selectionDetails;
             this._selectionDetails = null;
           }
-          document.getBindingParent(this).handleSearchCommand(aEvent, engine);
+          this.closest("searchbar").handleSearchCommand(aEvent, engine);
         ]]></body>
       </method>
 
       <property name="selectedButton">
         <getter><![CDATA[
           return this.popup.oneOffButtons.selectedButton;
         ]]></getter>
         <setter><![CDATA[
@@ -799,21 +324,21 @@
         this.popup.removeAttribute("showonlysettings");
       ]]></handler>
 
       <handler event="keypress" phase="capturing"
                action="return this.handleKeyboardNavigation(event);"/>
 
       <handler event="keypress" keycode="VK_UP" modifiers="accel"
                phase="capturing"
-               action="document.getBindingParent(this).selectEngine(event, false);"/>
+               action='this.closest("searchbar").selectEngine(event, false);'/>
 
       <handler event="keypress" keycode="VK_DOWN" modifiers="accel"
                phase="capturing"
-               action="document.getBindingParent(this).selectEngine(event, true);"/>
+               action='this.closest("searchbar").selectEngine(event, true);'/>
 
       <handler event="keypress" keycode="VK_DOWN" modifiers="alt"
                phase="capturing"
                action="return this.openSearch();"/>
 
       <handler event="keypress" keycode="VK_UP" modifiers="alt"
                phase="capturing"
                action="return this.openSearch();"/>
@@ -830,17 +355,17 @@
       <![CDATA[
         var dataTransfer = event.dataTransfer;
         var data = dataTransfer.getData("text/plain");
         if (!data)
           data = dataTransfer.getData("text/x-moz-text-internal");
         if (data) {
           event.preventDefault();
           this.value = data;
-          document.getBindingParent(this).openSuggestionsPanel();
+          this.closest("searchbar").openSuggestionsPanel();
         }
       ]]>
       </handler>
 
     </handlers>
   </binding>
 
   <binding id="browser-search-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
copy from browser/components/search/content/search.xml
copy to browser/components/search/content/searchbar.js
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/searchbar.js
@@ -1,2314 +1,482 @@
-<?xml version="1.0"?>
-<!-- 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/. -->
-
-<!-- XULCommandEvent is a specialised global. -->
-<!-- global XULCommandEvent -->
+/* 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/. */
 
-<!DOCTYPE bindings [
-<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
-%browserDTD;
-]>
-
-<bindings id="SearchBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
+"use strict";
 
-  <binding id="searchbar">
-    <content>
-      <xul:stringbundle src="chrome://browser/locale/search.properties"
-                        anonid="searchbar-stringbundle"/>
-      <!--
-      There is a dependency between "maxrows" attribute and
-      "SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing
-      one of them requires changing the other one.
-      -->
-      <xul:textbox class="searchbar-textbox"
-                   anonid="searchbar-textbox"
-                   type="autocomplete"
-                   inputtype="search"
-                   placeholder="&searchInput.placeholder;"
-                   flex="1"
-                   autocompletepopup="PopupSearchAutoComplete"
-                   autocompletesearch="search-autocomplete"
-                   autocompletesearchparam="searchbar-history"
-                   maxrows="10"
-                   completeselectedindex="true"
-                   minresultsforpopup="0"
-                   xbl:inherits="disabled,disableautocomplete,searchengine,src,newlines">
-        <!--
-        Empty <box> to properly position the icon within the autocomplete
-        binding's anonymous children (the autocomplete binding positions <box>
-        children differently)
-        -->
-        <xul:box>
-          <xul:hbox class="searchbar-search-button"
-                    anonid="searchbar-search-button"
-                    xbl:inherits="addengines"
-                    tooltiptext="&searchIcon.tooltip;">
-            <xul:image class="searchbar-search-icon"/>
-            <xul:image class="searchbar-search-icon-overlay"/>
-          </xul:hbox>
-        </xul:box>
-        <xul:hbox class="search-go-container">
-          <xul:image class="search-go-button urlbar-icon" hidden="true"
-                     anonid="search-go-button"
-                     onclick="handleSearchCommand(event);"
-                     tooltiptext="&contentSearchSubmit.tooltip;"/>
-        </xul:hbox>
-      </xul:textbox>
-    </content>
+/* eslint-env mozilla/browser-window */
+/* globals XULCommandEvent */
 
-    <implementation implements="nsIObserver">
-      <constructor><![CDATA[
-        if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
-          return;
-
-        Services.obs.addObserver(this, "browser-search-engine-modified");
-        Services.obs.addObserver(this, "browser-search-service");
-
-        this._initialized = true;
-
-        (window.delayedStartupPromise || Promise.resolve()).then(() => {
-          window.requestIdleCallback(() => {
-            Services.search.init(aStatus => {
-              // Bail out if the binding's been destroyed
-              if (!this._initialized)
-                return;
-
-              if (Components.isSuccessCode(aStatus)) {
-                // Refresh the display (updating icon, etc)
-                this.updateDisplay();
-                BrowserSearch.updateOpenSearchBadge();
-              } else {
-                Cu.reportError("Cannot initialize search service, bailing out: " + aStatus);
-              }
-            });
-          });
-        });
+// This is loaded into chrome windows with the subscript loader. Wrap in
+// a block to prevent accidentally leaking globals onto `window`.
+{
 
-        // Wait until the popupshowing event to avoid forcing immediate
-        // attachment of the search-one-offs binding.
-        this.textbox.popup.addEventListener("popupshowing", () => {
-          let oneOffButtons = this.textbox.popup.oneOffButtons;
-          // Some accessibility tests create their own <searchbar> that doesn't
-          // use the popup binding below, so null-check oneOffButtons.
-          if (oneOffButtons) {
-            oneOffButtons.telemetryOrigin = "searchbar";
-            // Set .textbox first, since the popup setter will cause
-            // a _rebuild call that uses it.
-            oneOffButtons.textbox = this.textbox;
-            oneOffButtons.popup = this.textbox.popup;
-          }
-        }, {capture: true, once: true});
-      ]]></constructor>
-
-      <destructor><![CDATA[
-        this.destroy();
-      ]]></destructor>
-
-      <method name="destroy">
-        <body><![CDATA[
-        if (this._initialized) {
-          this._initialized = false;
-
-          Services.obs.removeObserver(this, "browser-search-engine-modified");
-          Services.obs.removeObserver(this, "browser-search-service");
-        }
-
-        // Make sure to break the cycle from _textbox to us. Otherwise we leak
-        // the world. But make sure it's actually pointing to us.
-        // Also make sure the textbox has ever been constructed, otherwise the
-        // _textbox getter will cause the textbox constructor to run, add an
-        // observer, and leak the world too.
-        if (this._textboxInitialized && this._textbox.mController.input == this)
-          this._textbox.mController.input = null;
-        ]]></body>
-      </method>
-
-      <field name="_ignoreFocus">false</field>
-      <field name="_clickClosedPopup">false</field>
-      <field name="_stringBundle">document.getAnonymousElementByAttribute(this,
-          "anonid", "searchbar-stringbundle");</field>
-      <field name="_textboxInitialized">false</field>
-      <field name="_textbox">document.getAnonymousElementByAttribute(this,
-          "anonid", "searchbar-textbox");</field>
-      <field name="_engines">null</field>
-      <field name="FormHistory" readonly="true">
-        (ChromeUtils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
-      </field>
-
-      <property name="engines" readonly="true">
-        <getter><![CDATA[
-          if (!this._engines)
-            this._engines = Services.search.getVisibleEngines();
-          return this._engines;
-        ]]></getter>
-      </property>
+const inheritsMap = {
+  ".searchbar-textbox": ["disabled", "disableautocomplete", "searchengine", "src", "newlines"],
+  ".searchbar-search-button": ["addengines"],
+};
 
-      <property name="currentEngine">
-        <setter><![CDATA[
-          Services.search.currentEngine = val;
-          return val;
-        ]]></setter>
-        <getter><![CDATA[
-          var currentEngine = Services.search.currentEngine;
-          // Return a dummy engine if there is no currentEngine
-          return currentEngine || {name: "", uri: null};
-        ]]></getter>
-      </property>
-
-      <!-- textbox is used by sanitize.js to clear the undo history when
-           clearing form information. -->
-      <property name="textbox" readonly="true"
-                onget="return this._textbox;"/>
-
-      <property name="value" onget="return this._textbox.value;"
-                             onset="return this._textbox.value = val;"/>
-
-      <method name="focus">
-        <body><![CDATA[
-          this._textbox.focus();
-        ]]></body>
-      </method>
-
-      <method name="select">
-        <body><![CDATA[
-          this._textbox.select();
-        ]]></body>
-      </method>
-
-      <method name="observe">
-        <parameter name="aEngine"/>
-        <parameter name="aTopic"/>
-        <parameter name="aVerb"/>
-        <body><![CDATA[
-          if (aTopic == "browser-search-engine-modified" ||
-              (aTopic == "browser-search-service" && aVerb == "init-complete")) {
-            // Make sure the engine list is refetched next time it's needed
-            this._engines = null;
-
-            // Update the popup header and update the display after any modification.
-            this._textbox.popup.updateHeader();
-            this.updateDisplay();
-          }
-        ]]></body>
-      </method>
-
-      <method name="setIcon">
-        <parameter name="element"/>
-        <parameter name="uri"/>
-        <body><![CDATA[
-          element.setAttribute("src", uri);
-        ]]></body>
-      </method>
-
-      <method name="updateDisplay">
-        <body><![CDATA[
-          var uri = this.currentEngine.iconURI;
-          this.setIcon(this, uri ? uri.spec : "");
+function inheritAttribute(parent, child, attr) {
+  if (!parent.hasAttribute(attr)) {
+    child.removeAttribute(attr);
+  } else {
+    child.setAttribute(attr, parent.getAttribute(attr));
+  }
+}
 
-          var name = this.currentEngine.name;
-          var text = this._stringBundle.getFormattedString("searchtip", [name]);
-          this._textbox.label = text;
-          this._textbox.tooltipText = text;
-        ]]></body>
-      </method>
-
-      <method name="updateGoButtonVisibility">
-        <body><![CDATA[
-          document.getAnonymousElementByAttribute(this, "anonid",
-                                                  "search-go-button")
-                  .hidden = !this._textbox.value;
-        ]]></body>
-      </method>
-
-      <method name="openSuggestionsPanel">
-        <parameter name="aShowOnlySettingsIfEmpty"/>
-        <body><![CDATA[
-          if (this._textbox.open)
-            return;
-
-          this._textbox.showHistoryPopup();
-
-          if (this._textbox.value) {
-            // showHistoryPopup does a startSearch("") call, ensure the
-            // controller handles the text from the input box instead:
-            this._textbox.mController.handleText();
-          } else if (aShowOnlySettingsIfEmpty) {
-            this.setAttribute("showonlysettings", "true");
-          }
-        ]]></body>
-      </method>
-
-      <method name="selectEngine">
-        <parameter name="aEvent"/>
-        <parameter name="isNextEngine"/>
-        <body><![CDATA[
-          // Find the new index
-          var newIndex = this.engines.indexOf(this.currentEngine);
-          newIndex += isNextEngine ? 1 : -1;
-
-          if (newIndex >= 0 && newIndex < this.engines.length) {
-            this.currentEngine = this.engines[newIndex];
-          }
-
-          aEvent.preventDefault();
-          aEvent.stopPropagation();
-
-          this.openSuggestionsPanel();
-        ]]></body>
-      </method>
+class MozSearchbar extends MozXULElement {
 
-      <method name="handleSearchCommand">
-        <parameter name="aEvent"/>
-        <parameter name="aEngine"/>
-        <parameter name="aForceNewTab"/>
-        <body><![CDATA[
-          var where = "current";
-          let params;
-
-          // Open ctrl/cmd clicks on one-off buttons in a new background tab.
-          if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
-            if (aEvent.button == 2)
-              return;
-            where = whereToOpenLink(aEvent, false, true);
-          } else if (aForceNewTab) {
-            where = "tab";
-            if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
-              where += "-background";
-          } else {
-            var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
-            if (((aEvent instanceof KeyboardEvent && aEvent.altKey) ^ newTabPref) &&
-                !isTabEmpty(gBrowser.selectedTab)) {
-              where = "tab";
-            }
-            if ((aEvent instanceof MouseEvent) &&
-                (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
-              where = "tab";
-              params = {
-                inBackground: true,
-              };
-            }
-          }
-
-          this.handleSearchCommandWhere(aEvent, aEngine, where, params);
-        ]]></body>
-      </method>
-
-      <method name="handleSearchCommandWhere">
-        <parameter name="aEvent"/>
-        <parameter name="aEngine"/>
-        <parameter name="aWhere"/>
-        <parameter name="aParams"/>
-        <body><![CDATA[
-          var textBox = this._textbox;
-          var textValue = textBox.value;
-
-          let selection = this.telemetrySearchDetails;
-          let oneOffRecorded = false;
-
-          BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
-            aEvent,
-            selection ? selection.index : -1
-          );
+  static get observedAttributes() {
+    let unique = new Set();
+    for (var i in inheritsMap) {
+      inheritsMap[i].forEach(attr => unique.add(attr));
+    }
+    return Array.from(unique);
+  }
 
-          if (!selection || (selection.index == -1)) {
-            oneOffRecorded = this.textbox.popup.oneOffButtons
-                                 .maybeRecordTelemetry(aEvent, aWhere, aParams);
-            if (!oneOffRecorded) {
-              let source = "unknown";
-              let type = "unknown";
-              let target = aEvent.originalTarget;
-              if (aEvent instanceof KeyboardEvent) {
-                type = "key";
-              } else if (aEvent instanceof MouseEvent) {
-                type = "mouse";
-                if (target.classList.contains("search-panel-header") ||
-                    target.parentNode.classList.contains("search-panel-header")) {
-                  source = "header";
-                }
-              } else if (aEvent instanceof XULCommandEvent) {
-                if (target.getAttribute("anonid") == "paste-and-search") {
-                  source = "paste";
-                }
-              }
-              if (!aEngine) {
-                aEngine = this.currentEngine;
-              }
-              BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
-                                                          aWhere);
-            }
-          }
-
-          // This is a one-off search only if oneOffRecorded is true.
-          this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded);
-
-          if (aWhere == "tab" && aParams && aParams.inBackground)
-            this.focus();
-        ]]></body>
-      </method>
-
-      <method name="doSearch">
-        <parameter name="aData"/>
-        <parameter name="aWhere"/>
-        <parameter name="aEngine"/>
-        <parameter name="aParams"/>
-        <parameter name="aOneOff"/>
-        <body><![CDATA[
-          var textBox = this._textbox;
-
-          // Save the current value in the form history
-          if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
-            this.FormHistory.update(
-              { op: "bump",
-                fieldname: textBox.getAttribute("autocompletesearchparam"),
-                value: aData },
-              { handleError(aError) {
-                  Cu.reportError("Saving search to form history failed: " + aError.message);
-              }});
-          }
+  attributeChangedCallback() {
+    this.inheritAttributes();
+  }
 
-          let engine = aEngine || this.currentEngine;
-          var submission = engine.getSubmission(aData, null, "searchbar");
-          let telemetrySearchDetails = this.telemetrySearchDetails;
-          this.telemetrySearchDetails = null;
-          if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
-            telemetrySearchDetails = null;
-          }
-          // If we hit here, we come either from a one-off, a plain search or a suggestion.
-          const details = {
-            isOneOff: aOneOff,
-            isSuggestion: (!aOneOff && telemetrySearchDetails),
-            selection: telemetrySearchDetails,
-          };
-          BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
-          // null parameter below specifies HTML response for search
-          let params = {
-            postData: submission.postData,
-          };
-          if (aParams) {
-            for (let key in aParams) {
-              params[key] = aParams[key];
-            }
-          }
-          openTrustedLinkIn(submission.uri.spec, aWhere, params);
-        ]]></body>
-      </method>
-    </implementation>
+  inheritAttributes() {
+    if (!this.isConnected) {
+      return;
+    }
 
-    <handlers>
-      <handler event="command"><![CDATA[
-        const target = event.originalTarget;
-        if (target.engine) {
-          this.currentEngine = target.engine;
-        } else if (target.classList.contains("addengine-item")) {
-          // Select the installed engine if the installation succeeds
-          var installCallback = {
-            onSuccess: engine => this.currentEngine = engine,
-          };
-          Services.search.addEngine(target.getAttribute("uri"), null,
-                                    target.getAttribute("src"), false,
-                                    installCallback);
-        } else
-          return;
-
-        this.focus();
-        this.select();
-      ]]></handler>
-
-      <handler event="DOMMouseScroll"
-               phase="capturing"
-               modifiers="accel"
-               action="this.selectEngine(event, (event.detail > 0));"/>
-
-      <handler event="input" action="this.updateGoButtonVisibility();"/>
-      <handler event="drop" action="this.updateGoButtonVisibility();"/>
-
-      <handler event="blur">
-      <![CDATA[
-        // If the input field is still focused then a different window has
-        // received focus, ignore the next focus event.
-        this._ignoreFocus = (document.activeElement == this._textbox.inputField);
-      ]]></handler>
+    for (let sel in inheritsMap) {
+      let node = this.querySelector(sel);
+      for (let attr of inheritsMap[sel]) {
+        inheritAttribute(this, node, attr);
+      }
+    }
+  }
 
-      <handler event="focus">
-      <![CDATA[
-        // Speculatively connect to the current engine's search URI (and
-        // suggest URI, if different) to reduce request latency
-        this.currentEngine.speculativeConnect({window,
-                                               originAttributes: gBrowser.contentPrincipal
-                                                                         .originAttributes});
-
-        if (this._ignoreFocus) {
-          // This window has been re-focused, don't show the suggestions
-          this._ignoreFocus = false;
-          return;
-        }
-
-        // Don't open the suggestions if there is no text in the textbox.
-        if (!this._textbox.value)
-          return;
-
-        // Don't open the suggestions if the mouse was used to focus the
-        // textbox, that will be taken care of in the click handler.
-        if (Services.focus.getLastFocusMethod(window) & Services.focus.FLAG_BYMOUSE)
-          return;
-
-        this.openSuggestionsPanel();
-      ]]></handler>
-
-      <handler event="mousedown" phase="capturing">
-      <![CDATA[
-        if (event.originalTarget.getAttribute("anonid") == "searchbar-search-button") {
-          this._clickClosedPopup = this._textbox.popup._isHiding;
-        }
-      ]]></handler>
-
-      <handler event="mousedown" button="0">
-      <![CDATA[
-        // Ignore clicks on the search go button.
-        if (event.originalTarget.getAttribute("anonid") == "search-go-button") {
-          return;
-        }
-
-        let isIconClick = event.originalTarget.getAttribute("anonid") == "searchbar-search-button";
-
-        // Ignore clicks on the icon if they were made to close the popup
-        if (isIconClick && this._clickClosedPopup) {
-          return;
-        }
-
-        // Open the suggestions whenever clicking on the search icon or if there
-        // is text in the textbox.
-        if (isIconClick || this._textbox.value) {
-          this.openSuggestionsPanel(true);
-        }
-      ]]></handler>
-
-    </handlers>
-  </binding>
-
-  <binding id="searchbar-textbox"
-      extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
-    <implementation>
-      <constructor><![CDATA[
-        if (document.getBindingParent(this).parentNode.parentNode.localName ==
-            "toolbarpaletteitem")
-          return;
-
-        if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
-          this.setAttribute("clickSelectsAll", true);
-
-        var textBox = document.getAnonymousElementByAttribute(this,
-                                              "anonid", "moz-input-box");
-
-        // Force the Custom Element to upgrade until Bug 1470242 handles this:
-        customElements.upgrade(textBox);
-        var cxmenu = textBox.menupopup;
-        cxmenu.addEventListener("popupshowing",
-                                () => { this.initContextMenu(cxmenu); },
-                                {capture: true, once: true});
-
-        this.setAttribute("aria-owns", this.popup.id);
-        document.getBindingParent(this)._textboxInitialized = true;
-      ]]></constructor>
-
-      <destructor><![CDATA[
-        // If the context menu has never been opened, there won't be anything
-        // to remove here.
-        // Also, XBL and the customize toolbar code sometimes interact poorly.
-        try {
-          this.controllers.removeController(this.searchbarController);
-        } catch (ex) { }
-      ]]></destructor>
-
-      // Add items to context menu and attach controller to handle them the
-      // first time the context menu is opened.
-      <method name="initContextMenu">
-        <parameter name="aMenu"/>
-        <body><![CDATA[
-          let stringBundle = document.getBindingParent(this)._stringBundle;
-
-          let pasteAndSearch, suggestMenuItem;
-          let element, label, akey;
-
-          element = document.createXULElement("menuseparator");
-          aMenu.appendChild(element);
+  constructor() {
+    super();
+    this.destroy = this.destroy.bind(this);
+    this._setupEventListeners();
+    let searchbar = this;
+    this.observer = {
+      observe(aEngine, aTopic, aVerb) {
+        if (aTopic == "browser-search-engine-modified" ||
+          (aTopic == "browser-search-service" && aVerb == "init-complete")) {
+          // Make sure the engine list is refetched next time it's needed
+          searchbar._engines = null;
 
-          let insertLocation = aMenu.firstElementChild;
-          while (insertLocation.nextElementSibling &&
-                 insertLocation.getAttribute("cmd") != "cmd_paste")
-            insertLocation = insertLocation.nextElementSibling;
-          if (insertLocation) {
-            element = document.createXULElement("menuitem");
-            label = stringBundle.getString("cmd_pasteAndSearch");
-            element.setAttribute("label", label);
-            element.setAttribute("anonid", "paste-and-search");
-            element.setAttribute("oncommand", "BrowserSearch.pasteAndSearch(event)");
-            aMenu.insertBefore(element, insertLocation.nextElementSibling);
-            pasteAndSearch = element;
-          }
-
-          element = document.createXULElement("menuitem");
-          label = stringBundle.getString("cmd_clearHistory");
-          akey = stringBundle.getString("cmd_clearHistory_accesskey");
-          element.setAttribute("label", label);
-          element.setAttribute("accesskey", akey);
-          element.setAttribute("cmd", "cmd_clearhistory");
-          aMenu.appendChild(element);
-
-          element = document.createXULElement("menuitem");
-          label = stringBundle.getString("cmd_showSuggestions");
-          akey = stringBundle.getString("cmd_showSuggestions_accesskey");
-          element.setAttribute("anonid", "toggle-suggest-item");
-          element.setAttribute("label", label);
-          element.setAttribute("accesskey", akey);
-          element.setAttribute("cmd", "cmd_togglesuggest");
-          element.setAttribute("type", "checkbox");
-          element.setAttribute("autocheck", "false");
-          suggestMenuItem = element;
-          aMenu.appendChild(element);
-
-          if (AppConstants.platform == "macosx") {
-            this.addEventListener("keypress", aEvent => {
-              if (aEvent.keyCode == KeyEvent.DOM_VK_F4)
-                this.openSearch();
-            }, true);
-          }
-
-          this.controllers.appendController(this.searchbarController);
-
-          let onpopupshowing = function() {
-            BrowserSearch.searchBar._textbox.closePopup();
-            if (suggestMenuItem) {
-              let enabled =
-                Services.prefs.getBoolPref("browser.search.suggest.enabled");
-              suggestMenuItem.setAttribute("checked", enabled);
-            }
-
-            if (!pasteAndSearch)
-              return;
-            let controller = document.commandDispatcher.getControllerForCommand("cmd_paste");
-            let enabled = controller.isCommandEnabled("cmd_paste");
-            if (enabled)
-              pasteAndSearch.removeAttribute("disabled");
-            else
-              pasteAndSearch.setAttribute("disabled", "true");
-          };
-          aMenu.addEventListener("popupshowing", onpopupshowing);
-          onpopupshowing();
-        ]]></body>
-      </method>
-
-      <!--
-        This overrides the searchParam property in autocomplete.xml.  We're
-        hijacking this property as a vehicle for delivering the privacy
-        information about the window into the guts of nsSearchSuggestions.
-
-        Note that the setter is the same as the parent.  We were not sure whether
-        we can override just the getter.  If that proves to be the case, the setter
-        can be removed.
-      -->
-      <property name="searchParam"
-                onget="return this.getAttribute('autocompletesearchparam') +
-                       (PrivateBrowsingUtils.isWindowPrivate(window) ? '|private' : '');"
-                onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
-
-      <!-- This is implemented so that when textbox.value is set directly (e.g.,
-           by tests), the one-off query is updated. -->
-      <method name="onBeforeValueSet">
-        <parameter name="aValue"/>
-        <body><![CDATA[
-          this.popup.oneOffButtons.query = aValue;
-          return aValue;
-        ]]></body>
-      </method>
-
-      <!--
-        This method overrides the autocomplete binding's openPopup (essentially
-        duplicating the logic from the autocomplete popup binding's
-        openAutocompletePopup method), modifying it so that the popup is aligned with
-        the inner textbox, but sized to not extend beyond the search bar border.
-      -->
-      <method name="openPopup">
-        <body><![CDATA[
-          // Entering customization mode after the search bar had focus causes
-          // the popup to appear again, due to focus returning after the
-          // hamburger panel closes. Don't open in that spurious event.
-          if (document.documentElement.getAttribute("customizing") == "true") {
-            return;
-          }
-
-          var popup = this.popup;
-          if (!popup.mPopupOpen) {
-            // Initially the panel used for the searchbar (PopupSearchAutoComplete
-            // in browser.xul) is hidden to avoid impacting startup / new
-            // window performance. The base binding's openPopup would normally
-            // call the overriden openAutocompletePopup in
-            // browser-search-autocomplete-result-popup binding to unhide the popup,
-            // but since we're overriding openPopup we need to unhide the panel
-            // ourselves.
-            popup.hidden = false;
+          // Update the popup header and update the display after any modification.
+          searchbar._textbox.popup.updateHeader();
+          searchbar.updateDisplay();
+        }
+      },
+      QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
+    };
+    this.content = MozXULElement.parseXULToFragment(`
+      <stringbundle src="chrome://browser/locale/search.properties"></stringbundle>
+      <textbox class="searchbar-textbox" type="autocomplete" inputtype="search" placeholder="&searchInput.placeholder;" flex="1" autocompletepopup="PopupSearchAutoComplete" autocompletesearch="search-autocomplete" autocompletesearchparam="searchbar-history" maxrows="10" completeselectedindex="true" minresultsforpopup="0" inherits="disabled,disableautocomplete,searchengine,src,newlines">
+        <box>
+          <hbox class="searchbar-search-button" inherits="addengines" tooltiptext="&searchIcon.tooltip;">
+            <image class="searchbar-search-icon"></image>
+            <image class="searchbar-search-icon-overlay"></image>
+          </hbox>
+        </box>
+        <hbox class="search-go-container">
+          <image class="search-go-button urlbar-icon" hidden="true" onclick="handleSearchCommand(event);" tooltiptext="&contentSearchSubmit.tooltip;"></image>
+        </hbox>
+      </textbox>
+    `, ["chrome://browser/locale/browser.dtd"]);
+  }
 
-            // Don't roll up on mouse click in the anchor for the search UI.
-            if (popup.id == "PopupSearchAutoComplete") {
-              popup.setAttribute("norolluponanchor", "true");
-            }
-
-            popup.mInput = this;
-            // clear any previous selection, see bugs 400671 and 488357
-            popup.selectedIndex = -1;
-
-            document.popupNode = null;
-
-            const isRTL = getComputedStyle(this, "").direction == "rtl";
-
-            var outerRect = this.getBoundingClientRect();
-            var innerRect = this.inputField.getBoundingClientRect();
-            let width = isRTL ?
-                        innerRect.right - outerRect.left :
-                        outerRect.right - innerRect.left;
-            popup.setAttribute("width", width > 100 ? width : 100);
-
-            // invalidate() depends on the width attribute
-            popup._invalidate();
-
-            var yOffset = outerRect.bottom - innerRect.bottom;
-            popup.openPopup(this.inputField, "after_start", 0, yOffset, false, false);
-          }
-        ]]></body>
-      </method>
-
-      <method name="openSearch">
-        <body>
-          <![CDATA[
-            if (!this.popupOpen) {
-              document.getBindingParent(this).openSuggestionsPanel();
-              return false;
-            }
-            return true;
-          ]]>
-        </body>
-      </method>
+  connectedCallback() {
+    // Don't initialize if this isn't going to be visible
+    if (this.closest("#BrowserToolbarPalette")) {
+      return;
+    }
 
-      <method name="handleEnter">
-        <parameter name="event"/>
-        <body><![CDATA[
-          // Toggle the open state of the add-engine menu button if it's
-          // selected.  We're using handleEnter for this instead of listening
-          // for the command event because a command event isn't fired.
-          if (this.selectedButton &&
-              this.selectedButton.getAttribute("anonid") ==
-                "addengine-menu-button") {
-            this.selectedButton.open = !this.selectedButton.open;
-            return true;
-          }
-          // Otherwise, "call super": do what the autocomplete binding's
-          // handleEnter implementation does.
-          return this.mController.handleEnter(false, event || null);
-        ]]></body>
-      </method>
+    this.appendChild(document.importNode(this.content, true));
+    this.inheritAttributes();
+    window.addEventListener("unload", this.destroy);
+    this._ignoreFocus = false;
 
-      <!-- override |onTextEntered| in autocomplete.xml -->
-      <method name="onTextEntered">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          let engine;
-          let oneOff = this.selectedButton;
-          if (oneOff) {
-            if (!oneOff.engine) {
-              oneOff.doCommand();
-              return;
-            }
-            engine = oneOff.engine;
-          }
-          if (this._selectionDetails) {
-            BrowserSearch.searchBar.telemetrySearchDetails = this._selectionDetails;
-            this._selectionDetails = null;
-          }
-          document.getBindingParent(this).handleSearchCommand(aEvent, engine);
-        ]]></body>
-      </method>
+    this._clickClosedPopup = false;
 
-      <property name="selectedButton">
-        <getter><![CDATA[
-          return this.popup.oneOffButtons.selectedButton;
-        ]]></getter>
-        <setter><![CDATA[
-          return this.popup.oneOffButtons.selectedButton = val;
-        ]]></setter>
-      </property>
+    this._stringBundle = this.querySelector("stringbundle");
 
-      <method name="handleKeyboardNavigation">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          let popup = this.popup;
-          if (!popup.popupOpen)
-            return;
-
-          // accel + up/down changes the default engine and shouldn't affect
-          // the selection on the one-off buttons.
-          if (aEvent.getModifierState("Accel"))
-            return;
-
-          let suggestionsHidden =
-            popup.richlistbox.getAttribute("collapsed") == "true";
-          let numItems = suggestionsHidden ? 0 : this.popup.matchCount;
-          this.popup.oneOffButtons.handleKeyPress(aEvent, numItems, true);
-        ]]></body>
-      </method>
+    this._textboxInitialized = false;
 
-      <!-- nsIController -->
-      <field name="searchbarController" readonly="true"><![CDATA[({
-        _self: this,
-        supportsCommand(aCommand) {
-          return aCommand == "cmd_clearhistory" ||
-                 aCommand == "cmd_togglesuggest";
-        },
+    this._textbox = this.querySelector(".searchbar-textbox");
 
-        isCommandEnabled(aCommand) {
-          return true;
-        },
-
-        doCommand(aCommand) {
-          switch (aCommand) {
-            case "cmd_clearhistory":
-              var param = this._self.getAttribute("autocompletesearchparam");
+    this._engines = null;
 
-              BrowserSearch.searchBar.FormHistory.update({ op: "remove", fieldname: param }, null);
-              this._self.value = "";
-              break;
-            case "cmd_togglesuggest":
-              let enabled =
-                Services.prefs.getBoolPref("browser.search.suggest.enabled");
-              Services.prefs.setBoolPref("browser.search.suggest.enabled",
-                                         !enabled);
-              break;
-            default:
-              // do nothing with unrecognized command
-          }
-        },
-      })]]></field>
-    </implementation>
+    this.FormHistory = (ChromeUtils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
 
-    <handlers>
-      <handler event="input"><![CDATA[
-        this.popup.removeAttribute("showonlysettings");
-      ]]></handler>
-
-      <handler event="keypress" phase="capturing"
-               action="return this.handleKeyboardNavigation(event);"/>
-
-      <handler event="keypress" keycode="VK_UP" modifiers="accel"
-               phase="capturing"
-               action="document.getBindingParent(this).selectEngine(event, false);"/>
-
-      <handler event="keypress" keycode="VK_DOWN" modifiers="accel"
-               phase="capturing"
-               action="document.getBindingParent(this).selectEngine(event, true);"/>
-
-      <handler event="keypress" keycode="VK_DOWN" modifiers="alt"
-               phase="capturing"
-               action="return this.openSearch();"/>
+    if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
+      return;
 
-      <handler event="keypress" keycode="VK_UP" modifiers="alt"
-               phase="capturing"
-               action="return this.openSearch();"/>
-
-      <handler event="dragover">
-      <![CDATA[
-        var types = event.dataTransfer.types;
-        if (types.includes("text/plain") || types.includes("text/x-moz-text-internal"))
-          event.preventDefault();
-      ]]>
-      </handler>
+    Services.obs.addObserver(this.observer, "browser-search-engine-modified");
+    Services.obs.addObserver(this.observer, "browser-search-service");
 
-      <handler event="drop">
-      <![CDATA[
-        var dataTransfer = event.dataTransfer;
-        var data = dataTransfer.getData("text/plain");
-        if (!data)
-          data = dataTransfer.getData("text/x-moz-text-internal");
-        if (data) {
-          event.preventDefault();
-          this.value = data;
-          document.getBindingParent(this).openSuggestionsPanel();
-        }
-      ]]>
-      </handler>
-
-    </handlers>
-  </binding>
+    this._initialized = true;
 
-  <binding id="browser-search-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
-    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
-      <xul:hbox anonid="searchbar-engine" xbl:inherits="showonlysettings"
-                class="search-panel-header search-panel-current-engine">
-        <xul:image class="searchbar-engine-image" xbl:inherits="src"/>
-        <xul:label anonid="searchbar-engine-name" flex="1" crop="end"
-                   role="presentation"/>
-      </xul:hbox>
-      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox search-panel-tree" flex="1"/>
-      <xul:vbox anonid="search-one-off-buttons" class="search-one-offs"/>
-    </content>
-    <implementation>
-      <method name="openAutocompletePopup">
-        <parameter name="aInput"/>
-        <parameter name="aElement"/>
-        <body><![CDATA[
-          // initially the panel is hidden
-          // to avoid impacting startup / new window performance
-          aInput.popup.hidden = false;
-
-          // this method is defined on the base binding
-          this._openAutocompletePopup(aInput, aElement);
-        ]]></body>
-      </method>
-
-      <method name="onPopupClick">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          // Ignore all right-clicks
-          if (aEvent.button == 2)
+    (window.delayedStartupPromise || Promise.resolve()).then(() => {
+      window.requestIdleCallback(() => {
+        Services.search.init(aStatus => {
+          // Bail out if the binding's been destroyed
+          if (!this._initialized)
             return;
 
-          var searchBar = BrowserSearch.searchBar;
-          var popupForSearchBar = searchBar && searchBar.textbox == this.mInput;
-          if (popupForSearchBar) {
-            searchBar.telemetrySearchDetails = {
-              index: this.selectedIndex,
-              kind: "mouse",
-            };
-          }
-
-          // Check for unmodified left-click, and use default behavior
-          if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
-              !aEvent.altKey && !aEvent.metaKey) {
-            this.input.controller.handleEnter(true, aEvent);
-            return;
-          }
-
-          // Check for middle-click or modified clicks on the search bar
-          if (popupForSearchBar) {
-            BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
-              aEvent,
-              this.selectedIndex
-            );
-
-            // Handle search bar popup clicks
-            var search = this.input.controller.getValueAt(this.selectedIndex);
-
-            // open the search results according to the clicking subtlety
-            var where = whereToOpenLink(aEvent, false, true);
-            let params = {};
-
-            // But open ctrl/cmd clicks on autocomplete items in a new background tab.
-            let modifier = AppConstants.platform == "macosx" ?
-                           aEvent.metaKey :
-                           aEvent.ctrlKey;
-            if (where == "tab" && (aEvent instanceof MouseEvent) &&
-                (aEvent.button == 1 || modifier))
-              params.inBackground = true;
-
-            // leave the popup open for background tab loads
-            if (!(where == "tab" && params.inBackground)) {
-              // close the autocomplete popup and revert the entered search term
-              this.closePopup();
-              this.input.controller.handleEscape();
-            }
-
-            searchBar.doSearch(search, where, null, params);
-            if (where == "tab" && params.inBackground)
-              searchBar.focus();
-            else
-              searchBar.value = search;
-          }
-        ]]></body>
-      </method>
-
-      <!-- Popup rollup is triggered by native events before the mousedown event
-           reaches the DOM. The will be set to true by the popuphiding event and
-           false after the mousedown event has been triggered to detect what
-           caused rollup. -->
-      <field name="_isHiding">false</field>
-      <field name="_bundle">null</field>
-      <property name="bundle" readonly="true">
-        <getter>
-          <![CDATA[
-            if (!this._bundle) {
-              const kBundleURI = "chrome://browser/locale/search.properties";
-              this._bundle = Services.strings.createBundle(kBundleURI);
-            }
-            return this._bundle;
-          ]]>
-        </getter>
-      </property>
-
-      <field name="oneOffButtons" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid",
-                                                "search-one-off-buttons");
-      </field>
-
-      <method name="updateHeader">
-        <body><![CDATA[
-          let currentEngine = Services.search.currentEngine;
-          let uri = currentEngine.iconURI;
-          if (uri) {
-            this.setAttribute("src", uri.spec);
+          if (Components.isSuccessCode(aStatus)) {
+            // Refresh the display (updating icon, etc)
+            this.updateDisplay();
+            BrowserSearch.updateOpenSearchBadge();
           } else {
-            // If the default has just been changed to a provider without icon,
-            // avoid showing the icon of the previous default provider.
-            this.removeAttribute("src");
-          }
-
-          let headerText = this.bundle.formatStringFromName("searchHeader",
-                                                            [currentEngine.name], 1);
-          document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name")
-                  .setAttribute("value", headerText);
-          document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine")
-                  .engine = currentEngine;
-        ]]></body>
-      </method>
-
-      <!-- This is called when a one-off is clicked and when "search in new tab"
-           is selected from a one-off context menu. -->
-      <method name="handleOneOffSearch">
-        <parameter name="event"/>
-        <parameter name="engine"/>
-        <parameter name="where"/>
-        <parameter name="params"/>
-        <body><![CDATA[
-          let searchbar = document.getElementById("searchbar");
-          searchbar.handleSearchCommandWhere(event, engine, where, params);
-        ]]></body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <handler event="popupshowing"><![CDATA[
-        // Force the panel to have the width of the searchbar rather than
-        // the width of the textfield.
-        let DOMUtils = window.windowUtils;
-        let textboxRect = DOMUtils.getBoundsWithoutFlushing(this.mInput);
-        let inputRect = DOMUtils.getBoundsWithoutFlushing(this.mInput.inputField);
-
-        // Ensure the panel is wide enough to fit at least 3 engines.
-        let minWidth = Math.max(textboxRect.width,
-                                this.oneOffButtons.buttonWidth * 3);
-        this.style.minWidth = Math.round(minWidth) + "px";
-        // Alignment of the panel with the searchbar is obtained with negative
-        // margins.
-        this.style.marginLeft = (textboxRect.left - inputRect.left) + "px";
-        // This second margin is needed when the direction is reversed,
-        // eg. when using command+shift+X.
-        this.style.marginRight = (inputRect.right - textboxRect.right) + "px";
-
-        // First handle deciding if we are showing the reduced version of the
-        // popup containing only the preferences button. We do this if the
-        // glass icon has been clicked if the text field is empty.
-        let searchbar = document.getElementById("searchbar");
-        if (searchbar.hasAttribute("showonlysettings")) {
-          searchbar.removeAttribute("showonlysettings");
-          this.setAttribute("showonlysettings", "true");
-
-          // Setting this with an xbl-inherited attribute gets overridden the
-          // second time the user clicks the glass icon for some reason...
-          this.richlistbox.collapsed = true;
-        } else {
-          this.removeAttribute("showonlysettings");
-          // Uncollapse as long as we have a view which has >= 1 row.
-          // The autocomplete binding itself will take care of uncollapsing later,
-          // if we currently have no rows but end up having some in the future
-          // when the search string changes
-          this.richlistbox.collapsed = (this.matchCount == 0);
-        }
-
-        // Show the current default engine in the top header of the panel.
-        this.updateHeader();
-      ]]></handler>
-
-      <handler event="popuphiding"><![CDATA[
-        this._isHiding = true;
-        Services.tm.dispatchToMainThread(() => {
-          this._isHiding = false;
-        });
-      ]]></handler>
-
-      <!-- This handles clicks on the topmost "Foo Search" header in the
-           popup (hbox[anonid="searchbar-engine"]). -->
-      <handler event="click"><![CDATA[
-        if (event.button == 2) {
-          // Ignore right clicks.
-          return;
-        }
-        let button = event.originalTarget;
-        let engine = button.parentNode.engine;
-        if (!engine) {
-          return;
-        }
-        this.oneOffButtons.handleSearchCommand(event, engine);
-      ]]></handler>
-    </handlers>
-
-  </binding>
-
-  <binding id="search-one-offs">
-    <content context="_child">
-      <xul:deck anonid="search-panel-one-offs-header"
-                selectedIndex="0"
-                class="search-panel-header search-panel-current-input">
-        <xul:label anonid="searchbar-oneoffheader-search"
-                   value="&searchWithHeader.label;"/>
-        <xul:hbox anonid="search-panel-searchforwith"
-                  class="search-panel-current-input">
-          <xul:label anonid="searchbar-oneoffheader-before"
-                     value="&searchFor.label;"/>
-          <xul:label anonid="searchbar-oneoffheader-searchtext"
-                     class="search-panel-input-value"
-                     flex="1"
-                     crop="end"/>
-          <xul:label anonid="searchbar-oneoffheader-after"
-                     flex="10000"
-                     value="&searchWith.label;"/>
-        </xul:hbox>
-        <xul:hbox anonid="search-panel-searchonengine"
-                  class="search-panel-current-input">
-          <xul:label anonid="searchbar-oneoffheader-beforeengine"
-                     value="&search.label;"/>
-          <xul:label anonid="searchbar-oneoffheader-engine"
-                     class="search-panel-input-value"
-                     flex="1"
-                     crop="end"/>
-          <xul:label anonid="searchbar-oneoffheader-afterengine"
-                     flex="10000"
-                     value="&searchAfter.label;"/>
-        </xul:hbox>
-      </xul:deck>
-      <xul:description anonid="search-panel-one-offs"
-                       role="group"
-                       class="search-panel-one-offs"
-                       xbl:inherits="compact">
-        <xul:button anonid="search-settings-compact"
-                    oncommand="showSettings();"
-                    class="searchbar-engine-one-off-item search-setting-button-compact"
-                    tooltiptext="&changeSearchSettings.tooltip;"
-                    xbl:inherits="compact"/>
-      </xul:description>
-      <xul:vbox anonid="add-engines" class="search-add-engines"/>
-      <xul:button anonid="search-settings"
-                  oncommand="showSettings();"
-                  class="search-setting-button search-panel-header"
-                  label="&changeSearchSettings.button;"
-                  xbl:inherits="compact"/>
-      <xul:menupopup anonid="search-one-offs-context-menu">
-        <xul:menuitem anonid="search-one-offs-context-open-in-new-tab"
-                      label="&searchInNewTab.label;"
-                      accesskey="&searchInNewTab.accesskey;"/>
-        <xul:menuitem anonid="search-one-offs-context-set-default"
-                      label="&searchSetAsDefault.label;"
-                      accesskey="&searchSetAsDefault.accesskey;"/>
-      </xul:menupopup>
-    </content>
-
-    <implementation implements="nsIObserver,nsIWeakReference">
-
-      <!-- Width in pixels of the one-off buttons.  49px is the min-width of
-           each search engine button, adapt this const when changing the css.
-           It's actually 48px + 1px of right border. -->
-      <property name="buttonWidth" readonly="true" onget="return 49;"/>
-
-      <field name="_popup">null</field>
-
-      <!-- The popup that contains the one-offs.  This is required, so it should
-           never be null or undefined, except possibly before the one-offs are
-           used. -->
-      <property name="popup">
-        <getter><![CDATA[
-          return this._popup;
-        ]]></getter>
-        <setter><![CDATA[
-          let events = [
-            "popupshowing",
-            "popuphidden",
-          ];
-          if (this._popup) {
-            for (let event of events) {
-              this._popup.removeEventListener(event, this);
-            }
-          }
-          if (val) {
-            for (let event of events) {
-              val.addEventListener(event, this);
-            }
-          }
-          this._popup = val;
-
-          // If the popup is already open, rebuild the one-offs now.  The
-          // popup may be opening, so check that the state is not closed
-          // instead of checking popupOpen.
-          if (val && val.state != "closed") {
-            this._rebuild();
-          }
-          return val;
-        ]]></setter>
-      </property>
-
-      <field name="_textbox">null</field>
-      <field name="_textboxWidth">0</field>
-
-      <!-- The textbox associated with the one-offs.  Set this to a textbox to
-           automatically keep the related one-offs UI up to date.  Otherwise you
-           can leave it null/undefined, and in that case you should update the
-           query property manually. -->
-      <property name="textbox">
-        <getter><![CDATA[
-          return this._textbox;
-        ]]></getter>
-        <setter><![CDATA[
-          if (this._textbox) {
-            this._textbox.removeEventListener("input", this);
-          }
-          if (val) {
-            val.addEventListener("input", this);
-          }
-          return this._textbox = val;
-        ]]></setter>
-      </property>
-
-      <!-- Set this to a string that identifies your one-offs consumer.  It'll
-           be appended to telemetry recorded with maybeRecordTelemetry(). -->
-      <field name="telemetryOrigin">""</field>
-
-      <field name="_query">""</field>
-
-      <!-- The query string currently shown in the one-offs.  If the textbox
-           property is non-null, then this is automatically updated on
-           input. -->
-      <property name="query">
-        <getter><![CDATA[
-          return this._query;
-        ]]></getter>
-        <setter><![CDATA[
-          this._query = val;
-          if (this.popup && this.popup.popupOpen) {
-            this._updateAfterQueryChanged();
-          }
-          return val;
-        ]]></setter>
-      </property>
-
-      <field name="_selectedButton">null</field>
-
-      <!-- The selected one-off, a xul:button, including the add-engine button
-           and the search-settings button.  Null if no one-off is selected. -->
-      <property name="selectedButton">
-        <getter><![CDATA[
-          return this._selectedButton;
-        ]]></getter>
-        <setter><![CDATA[
-          if (val && val.classList.contains("dummy")) {
-            // Never select dummy buttons.
-            val = null;
-          }
-          if (this._selectedButton) {
-            this._selectedButton.removeAttribute("selected");
-          }
-          if (val) {
-            val.setAttribute("selected", "true");
-          }
-          this._selectedButton = val;
-          this._updateStateForButton(null);
-          if (val && !val.engine) {
-            // If the button doesn't have an engine, then clear the popup's
-            // selection to indicate that pressing Return while the button is
-            // selected will do the button's command, not search.
-            this.popup.selectedIndex = -1;
-          }
-          let event = document.createEvent("Events");
-          event.initEvent("SelectedOneOffButtonChanged", true, false);
-          this.dispatchEvent(event);
-          return val;
-        ]]></setter>
-      </property>
-
-      <!-- The index of the selected one-off, including the add-engine button
-           and the search-settings button.  -1 if no one-off is selected. -->
-      <property name="selectedButtonIndex">
-        <getter><![CDATA[
-          let buttons = this.getSelectableButtons(true);
-          for (let i = 0; i < buttons.length; i++) {
-            if (buttons[i] == this._selectedButton) {
-              return i;
-            }
+            Cu.reportError("Cannot initialize search service, bailing out: " + aStatus);
           }
-          return -1;
-        ]]></getter>
-        <setter><![CDATA[
-          let buttons = this.getSelectableButtons(true);
-          this.selectedButton = buttons[val];
-          return val;
-        ]]></setter>
-      </property>
-
-      <property name="compact" readonly="true">
-        <getter><![CDATA[
-          return this.getAttribute("compact") == "true";
-        ]]></getter>
-      </property>
+        });
+      });
+    });
 
-      <field name="buttons" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs");
-      </field>
-      <field name="header" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs-header");
-      </field>
-      <field name="addEngines" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
-      </field>
-      <field name="settingsButton" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
-      </field>
-      <field name="settingsButtonCompact" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact");
-      </field>
-
-      <field name="_bundle">null</field>
+    // Wait until the popupshowing event to avoid forcing immediate
+    // attachment of the search-one-offs binding.
+    this.textbox.popup.addEventListener("popupshowing", () => {
+      let oneOffButtons = this.textbox.popup.oneOffButtons;
+      // Some accessibility tests create their own <searchbar> that doesn't
+      // use the popup binding below, so null-check oneOffButtons.
+      if (oneOffButtons) {
+        oneOffButtons.telemetryOrigin = "searchbar";
+        // Set .textbox first, since the popup setter will cause
+        // a _rebuild call that uses it.
+        oneOffButtons.textbox = this.textbox;
+        oneOffButtons.popup = this.textbox.popup;
+      }
+    }, { capture: true, once: true });
+  }
 
-      <property name="bundle" readonly="true">
-        <getter><![CDATA[
-          if (!this._bundle) {
-            const kBundleURI = "chrome://browser/locale/search.properties";
-            this._bundle = Services.strings.createBundle(kBundleURI);
-          }
-          return this._bundle;
-        ]]></getter>
-      </property>
+  get engines() {
+    if (!this._engines)
+      this._engines = Services.search.getVisibleEngines();
+    return this._engines;
+  }
 
-      <!-- When a context menu is opened on a one-off button, this is set to the
-           engine of that button for use with the context menu actions. -->
-      <field name="_contextEngine">null</field>
-
-      <constructor><![CDATA[
-        // Force the <deck> Custom Element to be constructed. This can be removed
-        // once Bug 1470242 makes this happen behind the scenes.
-        customElements.upgrade(this.header);
+  set currentEngine(val) {
+    Services.search.currentEngine = val;
+    return val;
+  }
 
-        // Prevent popup events from the context menu from reaching the autocomplete
-        // binding (or other listeners).
-        let menu = document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-menu");
-        let listener = aEvent => aEvent.stopPropagation();
-        menu.addEventListener("popupshowing", listener);
-        menu.addEventListener("popuphiding", listener);
-        menu.addEventListener("popupshown", aEvent => {
-          this._ignoreMouseEvents = true;
-          aEvent.stopPropagation();
-        });
-        menu.addEventListener("popuphidden", aEvent => {
-          this._ignoreMouseEvents = false;
-          aEvent.stopPropagation();
-        });
-
-        // Add weak referenced observers to invalidate our cached list of engines.
-        Services.prefs.addObserver("browser.search.hiddenOneOffs", this, true);
-        Services.obs.addObserver(this, "browser-search-engine-modified", true);
-        Services.obs.addObserver(this, "browser-search-service", true);
-
-        // Rebuild the buttons when the theme changes.  See bug 1357800 for
-        // details.  Summary: On Linux, switching between themes can cause a row
-        // of buttons to disappear.
-        Services.obs.addObserver(this, "lightweight-theme-changed", true);
-      ]]></constructor>
+  get currentEngine() {
+    var currentEngine = Services.search.currentEngine;
+    // Return a dummy engine if there is no currentEngine
+    return currentEngine || { name: "", uri: null };
+  }
+  /**
+   * textbox is used by sanitize.js to clear the undo history when
+   * clearing form information.
+   */
+  get textbox() {
+    return this._textbox;
+  }
 
-      <!-- This handles events outside the one-off buttons, like on the popup
-           and textbox. -->
-      <method name="handleEvent">
-        <parameter name="event"/>
-        <body><![CDATA[
-          switch (event.type) {
-            case "input":
-              // Allow the consumer's input to override its value property with
-              // a oneOffSearchQuery property.  That way if the value is not
-              // actually what the user typed (e.g., it's autofilled, or it's a
-              // mozaction URI), the consumer has some way of providing it.
-              this.query = event.target.oneOffSearchQuery || event.target.value;
-              break;
-            case "popupshowing":
-              this._rebuild();
-              break;
-            case "popuphidden":
-              Services.tm.dispatchToMainThread(() => {
-                this.selectedButton = null;
-                this._contextEngine = null;
-              });
-              break;
-          }
-        ]]></body>
-      </method>
+  set value(val) {
+    return this._textbox.value = val;
+  }
+
+  get value() {
+    return this._textbox.value;
+  }
 
-      <method name="observe">
-        <parameter name="aEngine"/>
-        <parameter name="aTopic"/>
-        <parameter name="aData"/>
-        <body><![CDATA[
-          // Make sure the engine list is refetched next time it's needed.
-          this._engines = null;
-        ]]></body>
-      </method>
+  destroy() {
+    if (this._initialized) {
+      this._initialized = false;
+      window.removeEventListener("unload", this.destroy);
 
-      <method name="showSettings">
-        <body><![CDATA[
-          openPreferences("paneSearch", {origin: "contentSearch"});
-
-          // If the preference tab was already selected, the panel doesn't
-          // close itself automatically.
-          this.popup.hidePopup();
-        ]]></body>
-      </method>
+      Services.obs.removeObserver(this.observer, "browser-search-engine-modified");
+      Services.obs.removeObserver(this.observer, "browser-search-service");
+    }
 
-      <!-- Updates the parts of the UI that show the query string. -->
-      <method name="_updateAfterQueryChanged">
-        <body><![CDATA[
-          let headerSearchText =
-            document.getAnonymousElementByAttribute(this, "anonid",
-                                                    "searchbar-oneoffheader-searchtext");
-          headerSearchText.setAttribute("value", this.query);
-          let groupText;
-          let isOneOffSelected =
-            this.selectedButton &&
-            this.selectedButton.classList.contains("searchbar-engine-one-off-item");
-          // Typing de-selects the settings or opensearch buttons at the bottom
-          // of the search panel, as typing shows the user intends to search.
-          if (this.selectedButton && !isOneOffSelected)
-            this.selectedButton = null;
-          if (this.query) {
-            groupText = headerSearchText.previousElementSibling.value +
-                        '"' + headerSearchText.value + '"' +
-                        headerSearchText.nextElementSibling.value;
-            if (!isOneOffSelected)
-              this.header.selectedIndex = 1;
-          } else {
-            let noSearchHeader =
-              document.getAnonymousElementByAttribute(this, "anonid",
-                                                      "searchbar-oneoffheader-search");
-            groupText = noSearchHeader.value;
-            if (!isOneOffSelected)
-              this.header.selectedIndex = 0;
-          }
-          this.buttons.setAttribute("aria-label", groupText);
-        ]]></body>
-      </method>
+    // Make sure to break the cycle from _textbox to us. Otherwise we leak
+    // the world. But make sure it's actually pointing to us.
+    // Also make sure the textbox has ever been constructed, otherwise the
+    // _textbox getter will cause the textbox constructor to run, add an
+    // observer, and leak the world too.
+    if (this._textboxInitialized && this._textbox.mController.input == this)
+      this._textbox.mController.input = null;
+  }
+
+  focus() {
+    this._textbox.focus();
+  }
 
-      <field name="_engines">null</field>
-      <property name="engines" readonly="true">
-        <getter><![CDATA[
-          if (this._engines)
-            return this._engines;
-          let currentEngineNameToIgnore;
-          if (!this.getAttribute("includecurrentengine"))
-            currentEngineNameToIgnore = Services.search.currentEngine.name;
+  select() {
+    this._textbox.select();
+  }
 
-          let pref = Services.prefs.getStringPref("browser.search.hiddenOneOffs");
-          let hiddenList = pref ? pref.split(",") : [];
+  setIcon(element, uri) {
+    element.setAttribute("src", uri);
+  }
 
-          this._engines = Services.search.getVisibleEngines().filter(e => {
-            let name = e.name;
-            return (!currentEngineNameToIgnore ||
-                    name != currentEngineNameToIgnore) &&
-                   !hiddenList.includes(name);
-          });
+  updateDisplay() {
+    var uri = this.currentEngine.iconURI;
+    this.setIcon(this, uri ? uri.spec : "");
 
-          return this._engines;
-        ]]></getter>
-      </property>
-
-      <!-- Builds all the UI. -->
-      <method name="_rebuild">
-        <body><![CDATA[
-          // Update the 'Search for <keywords> with:" header.
-          this._updateAfterQueryChanged();
+    var name = this.currentEngine.name;
+    var text = this._stringBundle.getFormattedString("searchtip", [name]);
+    this._textbox.label = text;
+    this._textbox.tooltipText = text;
+  }
 
-          // Handle opensearch items. This needs to be done before building the
-          // list of one off providers, as that code will return early if all the
-          // alternative engines are hidden.
-          // Skip this in compact mode, ie. for the urlbar.
-          if (!this.compact)
-            this._rebuildAddEngineList();
+  updateGoButtonVisibility() {
+    this.querySelector(".search-go-button").hidden = !this._textbox.value;
+  }
+
+  openSuggestionsPanel(aShowOnlySettingsIfEmpty) {
+    if (this._textbox.open)
+      return;
 
-          // Check if the one-off buttons really need to be rebuilt.
-          if (this._textbox) {
-            // We can't get a reliable value for the popup width without flushing,
-            // but the popup width won't change if the textbox width doesn't.
-            let DOMUtils = window.windowUtils;
-            let textboxWidth =
-              DOMUtils.getBoundsWithoutFlushing(this._textbox).width;
-            // We can return early if neither the list of engines nor the panel
-            // width has changed.
-            if (this._engines && this._textboxWidth == textboxWidth) {
-              return;
-            }
-            this._textboxWidth = textboxWidth;
-          }
+    this._textbox.showHistoryPopup();
 
-          // Finally, build the list of one-off buttons.
-          while (this.buttons.firstElementChild != this.settingsButtonCompact)
-            this.buttons.firstElementChild.remove();
-          // Remove the trailing empty text node introduced by the binding's
-          // content markup above.
-          if (this.settingsButtonCompact.nextElementSibling)
-            this.settingsButtonCompact.nextElementSibling.remove();
-
-          let engines = this.engines;
-          let oneOffCount = engines.length;
-          let collapsed = !oneOffCount ||
-                          (oneOffCount == 1 && engines[0].name == Services.search.currentEngine.name);
-
-          // header is a xul:deck so collapsed doesn't work on it, see bug 589569.
-          this.header.hidden = this.buttons.collapsed = collapsed;
-
-          if (collapsed)
-            return;
-
-          let panelWidth = parseInt(this.popup.clientWidth);
+    if (this._textbox.value) {
+      // showHistoryPopup does a startSearch("") call, ensure the
+      // controller handles the text from the input box instead:
+      this._textbox.mController.handleText();
+    } else if (aShowOnlySettingsIfEmpty) {
+      this.setAttribute("showonlysettings", "true");
+    }
+  }
 
-          // There's one weird thing to guard against: when layout pixels
-          // aren't an integral multiple of device pixels, the last button
-          // of each row sometimes gets pushed to the next row, depending on the
-          // panel and button widths.
-          // This is likely because the clientWidth getter rounds the value, but
-          // the panel's border width is not an integer.
-          // As a workaround, decrement the width if the scale is not an integer.
-          let scale = window.windowUtils.screenPixelsPerCSSPixel;
-          if (Math.floor(scale) != scale) {
-            --panelWidth;
-          }
+  selectEngine(aEvent, isNextEngine) {
+    // Find the new index
+    var newIndex = this.engines.indexOf(this.currentEngine);
+    newIndex += isNextEngine ? 1 : -1;
 
-          // The + 1 is because the last button doesn't have a right border.
-          let enginesPerRow = Math.floor((panelWidth + 1) / this.buttonWidth);
-          let buttonWidth = Math.floor(panelWidth / enginesPerRow);
-          // There will be an emtpy area of:
-          //   panelWidth - enginesPerRow * buttonWidth  px
-          // at the end of each row.
+    if (newIndex >= 0 && newIndex < this.engines.length) {
+      this.currentEngine = this.engines[newIndex];
+    }
 
-          // If the <description> tag with the list of search engines doesn't have
-          // a fixed height, the panel will be sized incorrectly, causing the bottom
-          // of the suggestion <tree> to be hidden.
-          if (this.compact)
-            ++oneOffCount;
-          let rowCount = Math.ceil(oneOffCount / enginesPerRow);
-          let height = rowCount * 33; // 32px per row, 1px border.
-          this.buttons.setAttribute("height", height + "px");
-
-          // Ensure we can refer to the settings buttons by ID:
-          let origin = this.telemetryOrigin;
-          this.settingsButton.id = origin + "-anon-search-settings";
-          this.settingsButtonCompact.id = origin + "-anon-search-settings-compact";
+    aEvent.preventDefault();
+    aEvent.stopPropagation();
 
-          let dummyItems = enginesPerRow - (oneOffCount % enginesPerRow || enginesPerRow);
-          for (let i = 0; i < engines.length; ++i) {
-            let engine = engines[i];
-            let button = document.createXULElement("button");
-            button.id = this._buttonIDForEngine(engine);
-            let uri = "chrome://browser/skin/search-engine-placeholder.png";
-            if (engine.iconURI) {
-              uri = engine.iconURI.spec;
-            }
-            button.setAttribute("image", uri);
-            button.setAttribute("class", "searchbar-engine-one-off-item");
-            button.setAttribute("tooltiptext", engine.name);
-            button.setAttribute("width", buttonWidth);
-            button.engine = engine;
+    this.openSuggestionsPanel();
+  }
 
-            if ((i + 1) % enginesPerRow == 0)
-              button.classList.add("last-of-row");
-
-            if (i + 1 == engines.length)
-              button.classList.add("last-engine");
+  handleSearchCommand(aEvent, aEngine, aForceNewTab) {
+    var where = "current";
+    let params;
 
-            if (i >= oneOffCount + dummyItems - enginesPerRow)
-              button.classList.add("last-row");
-
-            this.buttons.insertBefore(button, this.settingsButtonCompact);
-          }
-
-          let hasDummyItems = !!dummyItems;
-          while (dummyItems) {
-            let button = document.createXULElement("button");
-            button.setAttribute("class", "searchbar-engine-one-off-item dummy last-row");
-            button.setAttribute("width", buttonWidth);
-
-            if (!--dummyItems)
-              button.classList.add("last-of-row");
-
-            this.buttons.insertBefore(button, this.settingsButtonCompact);
-          }
+    // Open ctrl/cmd clicks on one-off buttons in a new background tab.
+    if (aEvent && aEvent.originalTarget.classList.contains("search-go-button")) {
+      if (aEvent.button == 2)
+        return;
+      where = whereToOpenLink(aEvent, false, true);
+    } else if (aForceNewTab) {
+      where = "tab";
+      if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
+        where += "-background";
+    } else {
+      var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
+      if (((aEvent instanceof KeyboardEvent && aEvent.altKey) ^ newTabPref) &&
+        !isTabEmpty(gBrowser.selectedTab)) {
+        where = "tab";
+      }
+      if ((aEvent instanceof MouseEvent) &&
+        (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
+        where = "tab";
+        params = {
+          inBackground: true,
+        };
+      }
+    }
 
-          if (this.compact) {
-            this.settingsButtonCompact.setAttribute("width", buttonWidth);
-            if (rowCount == 1 && hasDummyItems) {
-              // When there's only one row, make the compact settings button
-              // hug the right edge of the panel.  It may not due to the panel's
-              // width not being an integral multiple of the button width.  (See
-              // the "There will be an emtpy area" comment above.)  Increase the
-              // width of the last dummy item by the remainder.
-              let remainder = panelWidth - (enginesPerRow * buttonWidth);
-              let width = remainder + buttonWidth;
-              let lastDummyItem = this.settingsButtonCompact.previousElementSibling;
-              lastDummyItem.setAttribute("width", width);
-            }
-          }
-        ]]></body>
-      </method>
-
-      <!-- If a page offers more than this number of engines, the add-engines
-           menu button is shown, instead of showing the engines directly in the
-           popup. -->
-      <field name="_addEngineMenuThreshold">5</field>
-
-      <method name="_rebuildAddEngineList">
-        <body><![CDATA[
-        let list = this.addEngines;
-        while (list.firstChild) {
-          list.firstChild.remove();
-        }
+    this.handleSearchCommandWhere(aEvent, aEngine, where, params);
+  }
 
-        // Add a button for each engine that the page in the selected browser
-        // offers, except when there are too many offered engines.
-        // The popup isn't designed to handle too many (by scrolling for
-        // example), so a page could break the popup by offering too many.
-        // Instead, add a single menu button with a submenu of all the engines.
-
-        if (!gBrowser.selectedBrowser.engines) {
-          return;
-        }
+  handleSearchCommandWhere(aEvent, aEngine, aWhere, aParams) {
+    var textBox = this._textbox;
+    var textValue = textBox.value;
 
-        let engines = gBrowser.selectedBrowser.engines;
-        let tooManyEngines = engines.length > this._addEngineMenuThreshold;
+    let selection = this.telemetrySearchDetails;
+    let oneOffRecorded = false;
 
-        if (tooManyEngines) {
-          // Make the top-level menu button.
-          let button = document.createXULElement("toolbarbutton");
-          list.appendChild(button);
-          button.classList.add("addengine-item", "badged-button");
-          button.setAttribute("anonid", "addengine-menu-button");
-          button.setAttribute("type", "menu");
-          button.setAttribute("label",
-            this.bundle.GetStringFromName("cmd_addFoundEngineMenu"));
-          button.setAttribute("crop", "end");
-          button.setAttribute("pack", "start");
+    BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
+      aEvent,
+      selection ? selection.index : -1
+    );
 
-          // Set the menu button's image to the image of the first engine.  The
-          // offered engines may have differing images, so there's no perfect
-          // choice here.
-          let engine = engines[0];
-          if (engine.icon) {
-            button.setAttribute("image", engine.icon);
+    if (!selection || (selection.index == -1)) {
+      oneOffRecorded = this.textbox.popup.oneOffButtons
+        .maybeRecordTelemetry(aEvent, aWhere, aParams);
+      if (!oneOffRecorded) {
+        let source = "unknown";
+        let type = "unknown";
+        let target = aEvent.originalTarget;
+        if (aEvent instanceof KeyboardEvent) {
+          type = "key";
+        } else if (aEvent instanceof MouseEvent) {
+          type = "mouse";
+          if (target.classList.contains("search-panel-header") ||
+            target.parentNode.classList.contains("search-panel-header")) {
+            source = "header";
           }
-
-          // Now make the button's child menupopup.
-          list = document.createXULElement("menupopup");
-          button.appendChild(list);
-          list.setAttribute("anonid", "addengine-menu");
-          list.setAttribute("position", "topright topleft");
-
-          // Events from child menupopups bubble up to the autocomplete binding,
-          // which breaks it, so prevent these events from propagating.
-          let suppressEventTypes = [
-            "popupshowing",
-            "popuphiding",
-            "popupshown",
-            "popuphidden",
-          ];
-          for (let type of suppressEventTypes) {
-            list.addEventListener(type, event => {
-              event.stopPropagation();
-            });
+        } else if (aEvent instanceof XULCommandEvent) {
+          if (target.getAttribute("anonid") == "paste-and-search") {
+            source = "paste";
           }
         }
-
-        // Finally, add the engines to the list.  If there aren't too many
-        // engines, the list is the add-engines vbox.  Otherwise it's the
-        // menupopup created earlier.  In the latter case, create menuitem
-        // elements instead of buttons, because buttons don't get keyboard
-        // handling for free inside menupopups.
-        let eltType = tooManyEngines ? "menuitem" : "toolbarbutton";
-        for (let engine of engines) {
-          let button = document.createXULElement(eltType);
-          button.classList.add("addengine-item");
-          if (!tooManyEngines) {
-            button.classList.add("badged-button");
-          }
-          button.id = this.telemetryOrigin + "-add-engine-" +
-                      this._fixUpEngineNameForID(engine.title);
-          let label = this.bundle.formatStringFromName("cmd_addFoundEngine",
-                                                       [engine.title], 1);
-          button.setAttribute("label", label);
-          button.setAttribute("crop", "end");
-          button.setAttribute("tooltiptext", engine.title + "\n" + engine.uri);
-          button.setAttribute("uri", engine.uri);
-          button.setAttribute("title", engine.title);
-          if (engine.icon) {
-            button.setAttribute("image", engine.icon);
-          }
-          if (tooManyEngines) {
-            button.classList.add("menuitem-iconic");
-          } else {
-            button.setAttribute("pack", "start");
-          }
-          list.appendChild(button);
+        if (!aEngine) {
+          aEngine = this.currentEngine;
         }
-        ]]></body>
-      </method>
-
-      <method name="_buttonIDForEngine">
-        <parameter name="engine"/>
-        <body><![CDATA[
-          return this.telemetryOrigin + "-engine-one-off-item-" +
-                 this._fixUpEngineNameForID(engine.name);
-        ]]></body>
-      </method>
-
-      <method name="_fixUpEngineNameForID">
-        <parameter name="name"/>
-        <body><![CDATA[
-          return name.replace(/ /g, "-");
-        ]]></body>
-      </method>
+        BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
+          aWhere);
+      }
+    }
 
-      <method name="_buttonForEngine">
-        <parameter name="engine"/>
-        <body><![CDATA[
-          return document.getElementById(this._buttonIDForEngine(engine));
-        ]]></body>
-      </method>
+    // This is a one-off search only if oneOffRecorded is true.
+    this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded);
 
-      <!--
-        Updates the popup and textbox for the currently selected or moused-over
-        button.
-
-        @param mousedOverButton
-               The currently moused-over button, or null if there isn't one.
-      -->
-      <method name="_updateStateForButton">
-        <parameter name="mousedOverButton"/>
-        <body><![CDATA[
-          let button = mousedOverButton;
+    if (aWhere == "tab" && aParams && aParams.inBackground)
+      this.focus();
+  }
 
-          // Ignore dummy buttons.
-          if (button && button.classList.contains("dummy")) {
-            button = null;
-          }
-
-          // If there's no moused-over button, then the one-offs should reflect
-          // the selected button, if any.
-          button = button || this.selectedButton;
-
-          if (!button) {
-            this.header.selectedIndex = this.query ? 1 : 0;
-            if (this.textbox) {
-              this.textbox.removeAttribute("aria-activedescendant");
-            }
-            return;
-          }
-
-          if (button.classList.contains("searchbar-engine-one-off-item") &&
-              button.engine) {
-            let headerEngineText =
-              document.getAnonymousElementByAttribute(this, "anonid",
-                                                      "searchbar-oneoffheader-engine");
-            this.header.selectedIndex = 2;
-            headerEngineText.value = button.engine.name;
-          } else {
-            this.header.selectedIndex = this.query ? 1 : 0;
-          }
-          if (this.textbox) {
-            this.textbox.setAttribute("aria-activedescendant", button.id);
-          }
-        ]]></body>
-      </method>
+  doSearch(aData, aWhere, aEngine, aParams, aOneOff) {
+    var textBox = this._textbox;
 
-      <method name="getSelectableButtons">
-        <parameter name="aIncludeNonEngineButtons"/>
-        <body><![CDATA[
-          let buttons = [];
-          for (let oneOff = this.buttons.firstElementChild; oneOff; oneOff = oneOff.nextElementSibling) {
-            // oneOff may be a text node since the list xul:description contains
-            // whitespace and the compact settings button.  See the markup
-            // above.  _rebuild removes text nodes, but it may not have been
-            // called yet (because e.g. the popup hasn't been opened yet).
-            if (oneOff.nodeType == Node.ELEMENT_NODE) {
-              if (oneOff.classList.contains("dummy") ||
-                  oneOff.classList.contains("search-setting-button-compact"))
-                break;
-              buttons.push(oneOff);
-            }
-          }
-
-          if (aIncludeNonEngineButtons) {
-            for (let addEngine = this.addEngines.firstElementChild; addEngine; addEngine = addEngine.nextElementSibling) {
-              buttons.push(addEngine);
-            }
-            buttons.push(this.compact ? this.settingsButtonCompact : this.settingsButton);
-          }
-
-          return buttons;
-        ]]></body>
-      </method>
-
-      <method name="handleSearchCommand">
-        <parameter name="aEvent"/>
-        <parameter name="aEngine"/>
-        <parameter name="aForceNewTab"/>
-        <body><![CDATA[
-          let where = "current";
-          let params;
+    // Save the current value in the form history
+    if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
+      this.FormHistory.update({
+        op: "bump",
+        fieldname: textBox.getAttribute("autocompletesearchparam"),
+        value: aData,
+      }, {
+        handleError(aError) {
+          Cu.reportError("Saving search to form history failed: " + aError.message);
+        },
+      });
+    }
 
-          // Open ctrl/cmd clicks on one-off buttons in a new background tab.
-          if (aForceNewTab) {
-            where = "tab";
-            if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) {
-              params = {
-                inBackground: true,
-              };
-            }
-          } else {
-            var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
-            if (((aEvent instanceof KeyboardEvent && aEvent.altKey) ^ newTabPref) &&
-                !isTabEmpty(gBrowser.selectedTab)) {
-              where = "tab";
-            }
-            if ((aEvent instanceof MouseEvent) &&
-                (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
-              where = "tab";
-              params = {
-                inBackground: true,
-              };
-            }
-          }
-
-          this.popup.handleOneOffSearch(aEvent, aEngine, where, params);
-        ]]></body>
-      </method>
-
-      <!--
-        Increments or decrements the index of the currently selected one-off.
-
-        @param aForward
-               If true, the index is incremented, and if false, the index is
-               decremented.
-        @param aIncludeNonEngineButtons
-               If true, non-dummy buttons that do not have engines are included.
-               These buttons include the OpenSearch and settings buttons.  For
-               example, if the currently selected button is an engine button,
-               the next button is the settings button, and you pass true for
-               aForward, then passing true for this value would cause the
-               settings to be selected.  Passing false for this value would
-               cause the selection to clear or wrap around, depending on what
-               value you passed for the aWrapAround parameter.
-        @param aWrapAround
-               If true, the selection wraps around between the first and last
-               buttons.
-        @return True if the selection can continue to advance after this method
-                returns and false if not.
-      -->
-      <method name="advanceSelection">
-        <parameter name="aForward"/>
-        <parameter name="aIncludeNonEngineButtons"/>
-        <parameter name="aWrapAround"/>
-        <body><![CDATA[
-          let buttons = this.getSelectableButtons(aIncludeNonEngineButtons);
-          let index;
-          if (this.selectedButton) {
-            let inc = aForward ? 1 : -1;
-            let oldIndex = buttons.indexOf(this.selectedButton);
-            index = ((oldIndex + inc) + buttons.length) % buttons.length;
-            if (!aWrapAround &&
-                ((aForward && index <= oldIndex) ||
-                 (!aForward && oldIndex <= index))) {
-              // The index has wrapped around, but wrapping around isn't
-              // allowed.
-              index = -1;
-            }
-          } else {
-            index = aForward ? 0 : buttons.length - 1;
-          }
-          this.selectedButton = index < 0 ? null : buttons[index];
-        ]]></body>
-      </method>
-
-      <!--
-        This handles key presses specific to the one-off buttons like Tab and
-        Alt+Up/Down, and Up/Down keys within the buttons.  Since one-off buttons
-        are always used in conjunction with a list of some sort (in this.popup),
-        it also handles Up/Down keys that cross the boundaries between list
-        items and the one-off buttons.
-
-        If this method handles the key press, then event.defaultPrevented will
-        be true when it returns.
+    let engine = aEngine || this.currentEngine;
+    var submission = engine.getSubmission(aData, null, "searchbar");
+    let telemetrySearchDetails = this.telemetrySearchDetails;
+    this.telemetrySearchDetails = null;
+    if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
+      telemetrySearchDetails = null;
+    }
+    // If we hit here, we come either from a one-off, a plain search or a suggestion.
+    const details = {
+      isOneOff: aOneOff,
+      isSuggestion: (!aOneOff && telemetrySearchDetails),
+      selection: telemetrySearchDetails,
+    };
+    BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
+    // null parameter below specifies HTML response for search
+    let params = {
+      postData: submission.postData,
+    };
+    if (aParams) {
+      for (let key in aParams) {
+        params[key] = aParams[key];
+      }
+    }
+    openTrustedLinkIn(submission.uri.spec, aWhere, params);
+  }
 
-        @param event
-               The key event.
-        @param numListItems
-               The number of items in the list.  The reason that this is a
-               parameter at all is that the list may contain items at the end
-               that should be ignored, depending on the consumer.  That's true
-               for the urlbar for example.
-        @param allowEmptySelection
-               Pass true if it's OK that neither the list nor the one-off
-               buttons contains a selection.  Pass false if either the list or
-               the one-off buttons (or both) should always contain a selection.
-        @param textboxUserValue
-               When the last list item is selected and the user presses Down,
-               the first one-off becomes selected and the textbox value is
-               restored to the value that the user typed.  Pass that value here.
-               However, if you pass true for allowEmptySelection, you don't need
-               to pass anything for this parameter.  (Pass undefined or null.)
-      -->
-      <method name="handleKeyPress">
-        <parameter name="event"/>
-        <parameter name="numListItems"/>
-        <parameter name="allowEmptySelection"/>
-        <parameter name="textboxUserValue"/>
-        <body><![CDATA[
-          if (!this.popup) {
-            return;
-          }
-          let handled = this._handleKeyPress(event, numListItems,
-                                             allowEmptySelection,
-                                             textboxUserValue);
-          if (handled) {
-            event.preventDefault();
-            event.stopPropagation();
-          }
-        ]]></body>
-      </method>
+  disconnectedCallback() {
+    this.destroy();
+    while (this.firstChild) {
+      this.firstChild.remove();
+    }
+  }
 
-      <method name="_handleKeyPress">
-        <parameter name="event"/>
-        <parameter name="numListItems"/>
-        <parameter name="allowEmptySelection"/>
-        <parameter name="textboxUserValue"/>
-        <body><![CDATA[
-          if (this.compact && this.buttons.collapsed)
-            return false;
-          if (event.keyCode == KeyEvent.DOM_VK_RIGHT &&
-              this.selectedButton &&
-              this.selectedButton.getAttribute("anonid") ==
-                "addengine-menu-button") {
-            // If the add-engine overflow menu item is selected and the user
-            // presses the right arrow key, open the submenu.  Unfortunately
-            // handling the left arrow key -- to close the popup -- isn't
-            // straightforward.  Once the popup is open, it consumes all key
-            // events.  Setting ignorekeys=handled on it doesn't help, since the
-            // popup handles all arrow keys.  Setting ignorekeys=true on it does
-            // mean that the popup no longer consumes the left arrow key, but
-            // then it no longer handles up/down keys to select items in the
-            // popup.
-            this.selectedButton.open = true;
-            return true;
-          }
-
-          // Handle the Tab key, but only if non-Shift modifiers aren't also
-          // pressed to avoid clobbering other shortcuts (like the Alt+Tab
-          // browser tab switcher).  The reason this uses getModifierState() and
-          // checks for "AltGraph" is that when you press Shift-Alt-Tab,
-          // event.altKey is actually false for some reason, at least on macOS.
-          // getModifierState("Alt") is also false, but "AltGraph" is true.
-          if (event.keyCode == KeyEvent.DOM_VK_TAB &&
-              !event.getModifierState("Alt") &&
-              !event.getModifierState("AltGraph") &&
-              !event.getModifierState("Control") &&
-              !event.getModifierState("Meta")) {
-            if (this.getAttribute("disabletab") == "true" ||
-                (event.shiftKey &&
-                  this.selectedButtonIndex <= 0) ||
-                (!event.shiftKey &&
-                 this.selectedButtonIndex ==
-                   this.getSelectableButtons(true).length - 1)) {
-              this.selectedButton = null;
-              return false;
-            }
-            this.popup.selectedIndex = -1;
-            this.advanceSelection(!event.shiftKey, true, false);
-            return !!this.selectedButton;
-          }
+  _setupEventListeners() {
+    this.addEventListener("command", (event) => {
+      const target = event.originalTarget;
+      if (target.engine) {
+        this.currentEngine = target.engine;
+      } else if (target.classList.contains("addengine-item")) {
+        // Select the installed engine if the installation succeeds
+        var installCallback = {
+          onSuccess: engine => this.currentEngine = engine,
+        };
+        Services.search.addEngine(target.getAttribute("uri"), null,
+          target.getAttribute("src"), false,
+          installCallback);
+      } else
+        return;
 
-          if (event.keyCode == KeyboardEvent.DOM_VK_UP) {
-            if (event.altKey) {
-              // Keep the currently selected result in the list (if any) as a
-              // secondary "alt" selection and move the selection up within the
-              // buttons.
-              this.advanceSelection(false, false, false);
-              return true;
-            }
-            if (numListItems == 0) {
-              this.advanceSelection(false, true, false);
-              return true;
-            }
-            if (this.popup.selectedIndex > 0) {
-              // Moving up within the list.  The autocomplete controller should
-              // handle this case.  A button may be selected, so null it.
-              this.selectedButton = null;
-              return false;
-            }
-            if (this.popup.selectedIndex == 0) {
-              // Moving up from the top of the list.
-              if (allowEmptySelection) {
-                // Let the autocomplete controller remove selection in the list
-                // and revert the typed text in the textbox.
-                return false;
-              }
-              // Wrap selection around to the last button.
-              if (this.textbox && typeof(textboxUserValue) == "string") {
-                this.textbox.value = textboxUserValue;
-              }
-              this.advanceSelection(false, true, true);
-              return true;
-            }
-            if (!this.selectedButton) {
-              // Moving up from no selection in the list or the buttons, back
-              // down to the last button.
-              this.advanceSelection(false, true, true);
-              return true;
-            }
-            if (this.selectedButtonIndex == 0) {
-              // Moving up from the buttons to the bottom of the list.
-              this.selectedButton = null;
-              return false;
-            }
-            // Moving up/left within the buttons.
-            this.advanceSelection(false, true, false);
-            return true;
-          }
+      this.focus();
+      this.select();
+    });
+
+    this.addEventListener("DOMMouseScroll", (event) => { this.selectEngine(event, (event.detail > 0)); }, true);
+
+    this.addEventListener("input", (event) => { this.updateGoButtonVisibility(); });
+
+    this.addEventListener("drop", (event) => { this.updateGoButtonVisibility(); });
+
+    this.addEventListener("blur", (event) => {
+      // If the input field is still focused then a different window has
+      // received focus, ignore the next focus event.
+      this._ignoreFocus = (document.activeElement == this._textbox.inputField);
+    }, true);
 
-          if (event.keyCode == KeyboardEvent.DOM_VK_DOWN) {
-            if (event.altKey) {
-              // Keep the currently selected result in the list (if any) as a
-              // secondary "alt" selection and move the selection down within
-              // the buttons.
-              this.advanceSelection(true, false, false);
-              return true;
-            }
-            if (numListItems == 0) {
-              this.advanceSelection(true, true, false);
-              return true;
-            }
-            if (this.popup.selectedIndex >= 0 &&
-                this.popup.selectedIndex < numListItems - 1) {
-              // Moving down within the list.  The autocomplete controller
-              // should handle this case.  A button may be selected, so null it.
-              this.selectedButton = null;
-              return false;
-            }
-            if (this.popup.selectedIndex == numListItems - 1) {
-              // Moving down from the last item in the list to the buttons.
-              this.selectedButtonIndex = 0;
-              if (allowEmptySelection) {
-                // Let the autocomplete controller remove selection in the list
-                // and revert the typed text in the textbox.
-                return false;
-              }
-              if (this.textbox && typeof(textboxUserValue) == "string") {
-                this.textbox.value = textboxUserValue;
-              }
-              this.popup.selectedIndex = -1;
-              return true;
-            }
-            if (this.selectedButton) {
-              let buttons = this.getSelectableButtons(true);
-              if (this.selectedButtonIndex == buttons.length - 1) {
-                // Moving down from the buttons back up to the top of the list.
-                this.selectedButton = null;
-                if (allowEmptySelection) {
-                  // Prevent the selection from wrapping around to the top of
-                  // the list by returning true, since the list currently has no
-                  // selection.  Nothing should be selected after handling this
-                  // Down key.
-                  return true;
-                }
-                return false;
-              }
-              // Moving down/right within the buttons.
-              this.advanceSelection(true, true, false);
-              return true;
-            }
-            return false;
-          }
-
-          if (event.keyCode == KeyboardEvent.DOM_VK_LEFT) {
-            if (this.selectedButton &&
-                (this.compact || this.selectedButton.engine)) {
-              // Moving left within the buttons.
-              this.advanceSelection(false, this.compact, true);
-              return true;
-            }
-            return false;
-          }
+    this.addEventListener("focus", (event) => {
+      // Speculatively connect to the current engine's search URI (and
+      // suggest URI, if different) to reduce request latency
+      this.currentEngine.speculativeConnect({
+        window,
+        originAttributes: gBrowser.contentPrincipal
+          .originAttributes,
+      });
 
-          if (event.keyCode == KeyboardEvent.DOM_VK_RIGHT) {
-            if (this.selectedButton &&
-                (this.compact || this.selectedButton.engine)) {
-              // Moving right within the buttons.
-              this.advanceSelection(true, this.compact, true);
-              return true;
-            }
-            return false;
-          }
-
-          return false;
-        ]]></body>
-      </method>
-
-      <!--
-        If the given event is related to the one-offs, this method records
-        one-off telemetry for it.  this.telemetryOrigin will be appended to the
-        computed source, so make sure you set that first.
+      if (this._ignoreFocus) {
+        // This window has been re-focused, don't show the suggestions
+        this._ignoreFocus = false;
+        return;
+      }
 
-        @param aEvent
-               An event, like a click on a one-off button.
-        @param aOpenUILinkWhere
-               The "where" passed to openUILink.
-        @param aOpenUILinkParams
-               The "params" passed to openUILink.
-        @return True if telemetry was recorded and false if not.
-      -->
-      <method name="maybeRecordTelemetry">
-        <parameter name="aEvent"/>
-        <parameter name="aOpenUILinkWhere"/>
-        <parameter name="aOpenUILinkParams"/>
-        <body><![CDATA[
-          if (!aEvent) {
-            return false;
-          }
-
-          let source = null;
-          let type = "unknown";
-          let engine = null;
-          let target = aEvent.originalTarget;
+      // Don't open the suggestions if there is no text in the textbox.
+      if (!this._textbox.value)
+        return;
 
-          if (aEvent instanceof KeyboardEvent) {
-            type = "key";
-            if (this.selectedButton) {
-              source = "oneoff";
-              engine = this.selectedButton.engine;
-            }
-          } else if (aEvent instanceof MouseEvent) {
-            type = "mouse";
-            if (target.classList.contains("searchbar-engine-one-off-item")) {
-              source = "oneoff";
-              engine = target.engine;
-            }
-          } else if ((aEvent instanceof XULCommandEvent) &&
-                     target.getAttribute("anonid") ==
-                       "search-one-offs-context-open-in-new-tab") {
-            source = "oneoff-context";
-            engine = this._contextEngine;
-          }
-
-          if (!source) {
-            return false;
-          }
-
-          if (this.telemetryOrigin) {
-            source += "-" + this.telemetryOrigin;
-          }
+      // Don't open the suggestions if the mouse was used to focus the
+      // textbox, that will be taken care of in the click handler.
+      if (Services.focus.getLastFocusMethod(window) & Services.focus.FLAG_BYMOUSE)
+        return;
 
-          let tabBackground = aOpenUILinkWhere == "tab" &&
-                              aOpenUILinkParams &&
-                              aOpenUILinkParams.inBackground;
-          let where = tabBackground ? "tab-background" : aOpenUILinkWhere;
-          BrowserSearch.recordOneoffSearchInTelemetry(engine, source, type,
-                                                      where);
-          return true;
-        ]]></body>
-      </method>
-
-      <!-- All this stuff is to make the add-engines menu button behave like an
-           actual menu.  The add-engines menu button is shown when there are
-           many engines offered by the current site. -->
-      <field name="_addEngineMenuTimeoutMs">200</field>
-      <field name="_addEngineMenuTimeout">null</field>
-      <field name="_addEngineMenuShouldBeOpen">false</field>
+      this.openSuggestionsPanel();
+    }, true);
 
-      <method name="_resetAddEngineMenuTimeout">
-        <body><![CDATA[
-        if (this._addEngineMenuTimeout) {
-          clearTimeout(this._addEngineMenuTimeout);
-        }
-        this._addEngineMenuTimeout = setTimeout(() => {
-          delete this._addEngineMenuTimeout;
-          let button = document.getAnonymousElementByAttribute(
-            this, "anonid", "addengine-menu-button"
-          );
-          button.open = this._addEngineMenuShouldBeOpen;
-        }, this._addEngineMenuTimeoutMs);
-        ]]></body>
-      </method>
-
-    </implementation>
-
-    <handlers>
-
-      <handler event="mousedown"><![CDATA[
-        let target = event.originalTarget;
-        if (target.getAttribute("anonid") == "addengine-menu-button") {
-          return;
-        }
-        // Required to receive click events from the buttons on Linux.
-        event.preventDefault();
-      ]]></handler>
-
-      <handler event="mousemove"><![CDATA[
-        let target = event.originalTarget;
+    this.addEventListener("mousedown", (event) => {
+      if (event.originalTarget.classList.contains("searchbar-search-button")) {
+        this._clickClosedPopup = this._textbox.popup._isHiding;
+      }
+    }, true);
 
-        // Handle mouseover on the add-engine menu button and its popup items.
-        if (target.getAttribute("anonid") == "addengine-menu-button" ||
-            (target.localName == "menuitem" &&
-             target.classList.contains("addengine-item"))) {
-          let menuButton = document.getAnonymousElementByAttribute(
-            this, "anonid", "addengine-menu-button"
-          );
-          this._updateStateForButton(menuButton);
-          this._addEngineMenuShouldBeOpen = true;
-          this._resetAddEngineMenuTimeout();
-          return;
-        }
-
-        if (target.localName != "button")
-          return;
-
-        // Ignore mouse events when the context menu is open.
-         if (this._ignoreMouseEvents)
-           return;
+    this.addEventListener("mousedown", (event) => {
+      // Ignore right clicks
+      if (event.button != 0) {
+        return;
+      }
 
-        let isOneOff =
-          target.classList.contains("searchbar-engine-one-off-item") &&
-          !target.classList.contains("dummy");
-        if (isOneOff ||
-            target.classList.contains("addengine-item") ||
-            target.classList.contains("search-setting-button")) {
-          this._updateStateForButton(target);
-        }
-      ]]></handler>
-
-      <handler event="mouseout"><![CDATA[
-
-        let target = event.originalTarget;
-
-        // Handle mouseout on the add-engine menu button and its popup items.
-        if (target.getAttribute("anonid") == "addengine-menu-button" ||
-            (target.localName == "menuitem" &&
-             target.classList.contains("addengine-item"))) {
-          this._updateStateForButton(null);
-          this._addEngineMenuShouldBeOpen = false;
-          this._resetAddEngineMenuTimeout();
-          return;
-        }
-
-        if (target.localName != "button") {
-          return;
-        }
+      // Ignore clicks on the search go button.
+      if (event.originalTarget.classList.contains("search-go-button")) {
+        return;
+      }
 
-        // Don't update the mouseover state if the context menu is open.
-        if (this._ignoreMouseEvents)
-          return;
-
-        this._updateStateForButton(null);
-      ]]></handler>
-
-      <handler event="click"><![CDATA[
-        if (event.button == 2)
-          return; // ignore right clicks.
+      let isIconClick = event.originalTarget.classList.contains("searchbar-search-button");
 
-        let button = event.originalTarget;
-        let engine = button.engine;
-
-        if (!engine)
-          return;
-
-        // Select the clicked button so that consumers can easily tell which
-        // button was acted on.
-        this.selectedButton = button;
-        this.handleSearchCommand(event, engine);
-      ]]></handler>
+      // Ignore clicks on the icon if they were made to close the popup
+      if (isIconClick && this._clickClosedPopup) {
+        return;
+      }
 
-      <handler event="command"><![CDATA[
-        let target = event.originalTarget;
-        if (target.classList.contains("addengine-item")) {
-          // On success, hide the panel and tell event listeners to reshow it to
-          // show the new engine.
-          let installCallback = {
-            onSuccess: engine => {
-              this._rebuild();
-            },
-            onError(errorCode) {
-              if (errorCode != Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE) {
-                // Download error is shown by the search service
-                return;
-              }
-              const kSearchBundleURI = "chrome://global/locale/search/search.properties";
-              let searchBundle = Services.strings.createBundle(kSearchBundleURI);
-              let brandBundle = document.getElementById("bundle_brand");
-              let brandName = brandBundle.getString("brandShortName");
-              let title = searchBundle.GetStringFromName("error_invalid_engine_title");
-              let text = searchBundle.formatStringFromName("error_duplicate_engine_msg",
-                                                           [brandName, target.getAttribute("uri")], 2);
-              Services.prompt.QueryInterface(Ci.nsIPromptFactory);
-              let prompt = Services.prompt.getPrompt(gBrowser.contentWindow, Ci.nsIPrompt);
-              prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
-              prompt.setPropertyAsBool("allowTabModal", true);
-              prompt.alert(title, text);
-            },
-          };
-          Services.search.addEngine(target.getAttribute("uri"), null,
-                                    target.getAttribute("image"), false,
-                                    installCallback);
-        }
-        let anonid = target.getAttribute("anonid");
-        if (anonid == "search-one-offs-context-open-in-new-tab") {
-          // Select the context-clicked button so that consumers can easily
-          // tell which button was acted on.
-          this.selectedButton = this._buttonForEngine(this._contextEngine);
-          this.handleSearchCommand(event, this._contextEngine, true);
-        }
-        if (anonid == "search-one-offs-context-set-default") {
-          let currentEngine = Services.search.currentEngine;
+      // Open the suggestions whenever clicking on the search icon or if there
+      // is text in the textbox.
+      if (isIconClick || this._textbox.value) {
+        this.openSuggestionsPanel(true);
+      }
+    });
 
-          if (!this.getAttribute("includecurrentengine")) {
-            // Make the target button of the context menu reflect the current
-            // search engine first. Doing this as opposed to rebuilding all the
-            // one-off buttons avoids flicker.
-            let button = this._buttonForEngine(this._contextEngine);
-            button.id = this._buttonIDForEngine(currentEngine);
-            let uri = "chrome://browser/skin/search-engine-placeholder.png";
-            if (currentEngine.iconURI)
-              uri = currentEngine.iconURI.spec;
-            button.setAttribute("image", uri);
-            button.setAttribute("tooltiptext", currentEngine.name);
-            button.engine = currentEngine;
-          }
-
-          Services.search.currentEngine = this._contextEngine;
-        }
-      ]]></handler>
+  }
+}
 
-      <handler event="contextmenu"><![CDATA[
-        let target = event.originalTarget;
-        // Prevent the context menu from appearing except on the one off buttons.
-        if (!target.classList.contains("searchbar-engine-one-off-item") ||
-            target.classList.contains("dummy")) {
-          event.preventDefault();
-          return;
-        }
-        document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-set-default")
-                .setAttribute("disabled", target.engine == Services.search.currentEngine);
+customElements.define("searchbar", MozSearchbar);
 
-        this._contextEngine = target.engine;
-      ]]></handler>
-    </handlers>
-
-  </binding>
-
-</bindings>
+}
--- a/browser/components/search/jar.mn
+++ b/browser/components/search/jar.mn
@@ -1,12 +1,13 @@
 # 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/.
 
 browser.jar:
         content/browser/search/search.xml                           (content/search.xml)
+        content/browser/search/searchbar.js                         (content/searchbar.js)
         content/browser/search/searchReset.xhtml                    (content/searchReset.xhtml)
         content/browser/search/searchReset.js                       (content/searchReset.js)
 
         searchplugins/                                              (searchplugins/**)
 
 % resource search-plugins %searchplugins/
--- a/browser/components/search/test/browser_426329.js
+++ b/browser/components/search/test/browser_426329.js
@@ -65,18 +65,17 @@ function promiseSetEngine() {
         case "engine-added":
           var engine = ss.getEngineByName("Bug 426329");
           ok(engine, "Engine was added.");
           ss.currentEngine = engine;
           break;
         case "engine-current":
           ok(ss.currentEngine.name == "Bug 426329", "currentEngine set");
           searchBar = BrowserSearch.searchBar;
-          searchButton = document.getAnonymousElementByAttribute(searchBar,
-                             "anonid", "search-go-button");
+          searchButton = searchBar.querySelector(".search-go-button");
           ok(searchButton, "got search-go-button");
           searchBar.value = "test";
 
           Services.obs.removeObserver(observer, "browser-search-engine-modified");
           resolve();
           break;
       }
     }
@@ -112,17 +111,17 @@ async function prepareTest() {
   preTabNo = gBrowser.tabs.length;
   searchBar = BrowserSearch.searchBar;
 
   await SimpleTest.promiseFocus();
 
   if (document.activeElement == searchBar)
     return;
 
-  let focusPromise = BrowserTestUtils.waitForEvent(searchBar, "focus");
+  let focusPromise = BrowserTestUtils.waitForEvent(searchBar.textbox, "focus");
   gURLBar.focus();
   searchBar.focus();
   await focusPromise;
 }
 
 add_task(async function testSetup() {
   await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
--- a/browser/components/search/test/browser_hiddenOneOffs_diacritics.js
+++ b/browser/components/search/test/browser_hiddenOneOffs_diacritics.js
@@ -12,19 +12,17 @@ var Preferences =
 
 let searchIcon;
 
 add_task(async function init() {
   let searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
-  searchIcon = document.getAnonymousElementByAttribute(
-    searchbar, "anonid", "searchbar-search-button"
-  );
+  searchIcon = searchbar.querySelector(".searchbar-search-button");
 
   let currentEngine = Services.search.currentEngine;
   await promiseNewEngine("testEngine_diacritics.xml", {setAsCurrent: false});
   registerCleanupFunction(() => {
     Services.search.currentEngine = currentEngine;
     Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
   });
 });
--- a/browser/components/search/test/browser_oneOffContextMenu.js
+++ b/browser/components/search/test/browser_oneOffContextMenu.js
@@ -20,19 +20,17 @@ const searchInNewTabMenuItem = document.
 let searchbar;
 let searchIcon;
 
 add_task(async function init() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
-  searchIcon = document.getAnonymousElementByAttribute(
-    searchbar, "anonid", "searchbar-search-button"
-  );
+  searchIcon = searchbar.querySelector(".searchbar-search-button");
 
   await promiseNewEngine(TEST_ENGINE_BASENAME, {
     setAsCurrent: false,
   });
 });
 
 add_task(async function telemetry() {
   // Open the popup.
--- a/browser/components/search/test/browser_oneOffContextMenu_setDefault.js
+++ b/browser/components/search/test/browser_oneOffContextMenu_setDefault.js
@@ -26,19 +26,17 @@ registerCleanupFunction(resetEngine);
 
 let searchIcon;
 
 add_task(async function init() {
   let searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
-  searchIcon = document.getAnonymousElementByAttribute(
-    searchbar, "anonid", "searchbar-search-button"
-  );
+  searchIcon = searchbar.querySelector(".searchbar-search-button");
 
   await promiseNewEngine(TEST_ENGINE_BASENAME, {
     setAsCurrent: false,
   });
 });
 
 add_task(async function test_searchBarChangeEngine() {
   let oneOffButton = await openPopupAndGetEngineButton(true, searchPopup,
--- a/browser/components/search/test/browser_oneOffHeader.js
+++ b/browser/components/search/test/browser_oneOffHeader.js
@@ -53,19 +53,17 @@ function synthesizeNativeMouseMove(aElem
 let searchbar;
 let searchIcon;
 
 add_task(async function init() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
-  searchIcon = document.getAnonymousElementByAttribute(
-    searchbar, "anonid", "searchbar-search-button"
-  );
+  searchIcon = searchbar.querySelector(".searchbar-search-button");
 
   await promiseNewEngine("testEngine.xml");
 });
 
 add_task(async function test_notext() {
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
   EventUtils.synthesizeMouseAtCenter(searchIcon, {});
@@ -92,17 +90,17 @@ add_task(async function test_notext() {
 
   promise = promiseEvent(searchPopup, "popuphidden");
   info("Closing search panel");
   EventUtils.synthesizeKey("KEY_Escape");
   await promise;
 });
 
 add_task(async function test_text() {
-  searchbar._textbox.value = "foo";
+  searchbar.textbox.value = "foo";
 
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
   SimpleTest.executeSoon(() => {
     EventUtils.synthesizeMouseAtCenter(searchIcon, {});
   });
   await promise;
 
@@ -131,18 +129,18 @@ add_task(async function test_text() {
                                             "searchbar-engine");
 
   await synthesizeNativeMouseMove(searchbarEngine);
   SimpleTest.executeSoon(() => {
     EventUtils.synthesizeMouseAtCenter(searchbarEngine, {});
   });
 
   let url = Services.search.currentEngine
-                           .getSubmission(searchbar._textbox.value).uri.spec;
+                           .getSubmission(searchbar.textbox.value).uri.spec;
   await promiseTabLoadEvent(gBrowser.selectedTab, url);
 
   // Move the cursor out of the panel area to avoid messing with other tests.
   await synthesizeNativeMouseMove(searchbar);
 });
 
 add_task(async function cleanup() {
-  searchbar._textbox.value = "";
+  searchbar.textbox.value = "";
 });
--- a/browser/components/search/test/browser_searchbar_keyboard_navigation.js
+++ b/browser/components/search/test/browser_searchbar_keyboard_navigation.js
@@ -23,17 +23,17 @@ function getOpenSearchItems() {
 let searchbar;
 let textbox;
 
 add_task(async function init() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
-  textbox = searchbar._textbox;
+  textbox = searchbar.textbox;
 
   await promiseNewEngine("testEngine.xml");
 
   // First cleanup the form history in case other tests left things there.
   await new Promise((resolve, reject) => {
     info("cleanup the search history");
     searchbar.FormHistory.update({op: "remove", fieldname: "searchbar-history"},
                                  {handleCompletion: resolve,
--- a/browser/components/search/test/browser_searchbar_openpopup.js
+++ b/browser/components/search/test/browser_searchbar_openpopup.js
@@ -58,23 +58,19 @@ let textbox;
 let searchIcon;
 let goButton;
 
 add_task(async function init() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
-  textbox = searchbar._textbox;
-  searchIcon = document.getAnonymousElementByAttribute(
-    searchbar, "anonid", "searchbar-search-button"
-  );
-  goButton = document.getAnonymousElementByAttribute(
-    searchbar, "anonid", "search-go-button"
-  );
+  textbox = searchbar.textbox;
+  searchIcon = searchbar.querySelector(".searchbar-search-button");
+  goButton = searchbar.querySelector(".search-go-button");
 
   await promiseNewEngine("testEngine.xml");
 
   // First cleanup the form history in case other tests left things there.
   await new Promise((resolve, reject) => {
     info("cleanup the search history");
     searchbar.FormHistory.update({op: "remove", fieldname: "searchbar-history"},
                                  {handleCompletion: resolve,
@@ -226,17 +222,17 @@ add_task(async function focus_change_clo
   await promise;
   isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
 
   is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
-  let promise2 = promiseEvent(searchbar, "blur");
+  let promise2 = promiseEvent(searchbar.textbox, "blur");
   EventUtils.synthesizeKey("KEY_Tab", {shiftKey: true});
   await promise;
   await promise2;
 
   textbox.value = "";
 });
 
 // Moving focus away from the search box should close the small popup
@@ -249,17 +245,17 @@ add_task(async function focus_change_clo
     EventUtils.synthesizeMouseAtCenter(searchIcon, {});
   });
   await promise;
   is(searchPopup.getAttribute("showonlysettings"), "true", "Should show the small popup");
 
   is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
 
   promise = promiseEvent(searchPopup, "popuphidden");
-  let promise2 = promiseEvent(searchbar, "blur");
+  let promise2 = promiseEvent(searchbar.textbox, "blur");
   EventUtils.synthesizeKey("KEY_Tab", {shiftKey: true});
   await promise;
   await promise2;
 });
 
 // Pressing escape should close the popup.
 add_task(async function escape_closes_popup() {
   gURLBar.focus();
@@ -364,17 +360,17 @@ add_task(async function refocus_window_d
   await new Promise(resolve => waitForFocus(resolve, newWin));
   await promise;
 
   function listener() {
     ok(false, "Should not have shown the popup.");
   }
   searchPopup.addEventListener("popupshowing", listener);
 
-  promise = promiseEvent(searchbar, "focus");
+  promise = promiseEvent(searchbar.textbox, "focus");
   newWin.close();
   await promise;
 
   // Wait a few ticks to allow any focus handlers to show the popup if they are going to.
   await new Promise(resolve => executeSoon(resolve));
   await new Promise(resolve => executeSoon(resolve));
   await new Promise(resolve => executeSoon(resolve));
 
@@ -401,17 +397,17 @@ add_task(async function refocus_window_d
   await new Promise(resolve => waitForFocus(resolve, newWin));
   await promise;
 
   function listener() {
     ok(false, "Should not have shown the popup.");
   }
   searchPopup.addEventListener("popupshowing", listener);
 
-  promise = promiseEvent(searchbar, "focus");
+  promise = promiseEvent(searchbar.textbox, "focus");
   newWin.close();
   await promise;
 
   // Wait a few ticks to allow any focus handlers to show the popup if they are going to.
   await new Promise(resolve => executeSoon(resolve));
   await new Promise(resolve => executeSoon(resolve));
   await new Promise(resolve => executeSoon(resolve));
 
--- a/browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
+++ b/browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
@@ -23,20 +23,18 @@ let searchbar;
 let textbox;
 let searchIcon;
 
 add_task(async function init() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
-  textbox = searchbar._textbox;
-  searchIcon = document.getAnonymousElementByAttribute(
-    searchbar, "anonid", "searchbar-search-button"
-  );
+  textbox = searchbar.textbox;
+  searchIcon = searchbar.querySelector(".searchbar-search-button");
 
   await promiseNewEngine("testEngine.xml");
 
   // First cleanup the form history in case other tests left things there.
   await new Promise((resolve, reject) => {
     info("cleanup the search history");
     searchbar.FormHistory.update({op: "remove", fieldname: "searchbar-history"},
                                  {handleCompletion: resolve,
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -148,19 +148,17 @@ var UITour = {
       infoPanelOffsetX: 18,
       infoPanelPosition: "after_start",
       query: "#searchbar",
       widgetName: "search-container",
     }],
     ["searchIcon", {
       query: (aDocument) => {
         let searchbar = aDocument.getElementById("searchbar");
-        return aDocument.getAnonymousElementByAttribute(searchbar,
-                                                        "anonid",
-                                                        "searchbar-search-button");
+        return searchbar.querySelector(".searchbar-search-button");
       },
       widgetName: "search-container",
     }],
     ["searchPrefsLink", {
       query: (aDocument) => {
         let element = null;
         let popup = aDocument.getElementById("PopupSearchAutoComplete");
         if (popup.state != "open")
--- a/build/moz.configure/rust.configure
+++ b/build/moz.configure/rust.configure
@@ -67,17 +67,17 @@ def rust_compiler(rustc_info, cargo_info
         die(dedent('''\
         Rust compiler not found.
         To compile rust language sources, you must have 'rustc' in your path.
         See https://www.rust-lang.org/ for more information.
 
         You can install rust by running './mach bootstrap'
         or by directly running the installer from https://rustup.rs/
         '''))
-    rustc_min_version = Version('1.28.0')
+    rustc_min_version = Version('1.29.0')
     cargo_min_version = rustc_min_version
 
     version = rustc_info.version
     if version < rustc_min_version:
         die(dedent('''\
         Rust compiler {} is too old.
 
         To compile Rust language sources please install at least
--- a/devtools/client/debugger/new/src/client/firefox/commands.js
+++ b/devtools/client/debugger/new/src/client/firefox/commands.js
@@ -30,16 +30,28 @@ function setupCommands(dependencies) {
   debuggerClient = dependencies.debuggerClient;
   supportsWasm = dependencies.supportsWasm;
   bpClients = {};
   return {
     bpClients
   };
 }
 
+function createObjectClient(grip) {
+  return debuggerClient.createObjectClient(grip);
+}
+
+function releaseActor(actor) {
+  if (!actor) {
+    return;
+  }
+
+  return debuggerClient.release(actor);
+}
+
 function sendPacket(packet, callback = r => r) {
   return debuggerClient.request(packet).then(callback);
 }
 
 function resume() {
   return new Promise(resolve => {
     threadClient.resume(resolve);
   });
@@ -401,16 +413,18 @@ async function fetchWorkers() {
   }
 
   return threadClient._parent.listWorkers();
 }
 
 const clientCommands = {
   autocomplete,
   blackBox,
+  createObjectClient,
+  releaseActor,
   interrupt,
   eventListeners,
   pauseGrip,
   resume,
   stepIn,
   stepOut,
   stepOver,
   rewind,
--- a/devtools/client/debugger/new/src/components/Editor/Preview/Popup.js
+++ b/devtools/client/debugger/new/src/components/Editor/Preview/Popup.js
@@ -42,29 +42,34 @@ function _interopRequireDefault(obj) { r
 /* 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/>. */
 const {
   REPS: {
     Rep
   },
   MODE,
-  ObjectInspector,
-  ObjectInspectorUtils
+  objectInspector
 } = _devtoolsReps2.default;
 const {
-  createNode,
-  getChildren,
-  getValue,
-  nodeIsPrimitive,
-  NODE_TYPES
-} = ObjectInspectorUtils.node;
+  ObjectInspector,
+  utils
+} = objectInspector;
 const {
-  loadItemProperties
-} = ObjectInspectorUtils.loadProperties;
+  node: {
+    createNode,
+    getChildren,
+    getValue,
+    nodeIsPrimitive,
+    NODE_TYPES
+  },
+  loadProperties: {
+    loadItemProperties
+  }
+} = utils;
 
 function inPreview(event) {
   const relatedTarget = event.relatedTarget;
 
   if (!relatedTarget || relatedTarget.classList && relatedTarget.classList.contains("preview-expression")) {
     return true;
   } // $FlowIgnore
 
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Expressions.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Expressions.js
@@ -31,16 +31,20 @@ var _firefox = require("../../client/fir
 var _Button = require("../shared/Button/index");
 
 var _lodash = require("devtools/client/shared/vendor/lodash");
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
+const {
+  ObjectInspector
+} = _devtoolsReps.objectInspector;
+
 class Expressions extends _react.Component {
   constructor(props) {
     super(props);
 
     this.clear = () => {
       this.setState(() => {
         this.props.clearExpressionError();
         return {
@@ -152,17 +156,17 @@ class Expressions extends _react.Compone
       };
       return _react2.default.createElement("li", {
         className: "expression-container",
         key: input,
         title: expression.input,
         onDoubleClick: (items, options) => this.editExpression(expression, index)
       }, _react2.default.createElement("div", {
         className: "expression-content"
-      }, _react2.default.createElement(_devtoolsReps.ObjectInspector, {
+      }, _react2.default.createElement(ObjectInspector, {
         roots: [root],
         autoExpandDepth: 0,
         disableWrap: true,
         focusable: false,
         openLink: openLink,
         createObjectClient: grip => (0, _firefox.createObjectClient)(grip)
       }), _react2.default.createElement("div", {
         className: "expression-container__close-btn"
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/FrameworkComponent.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/FrameworkComponent.js
@@ -23,22 +23,25 @@ var _devtoolsReps = require("devtools/cl
 var _preview = require("../../utils/preview");
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 /* 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/>. */
 const {
-  createNode,
-  getChildren
-} = _devtoolsReps.ObjectInspectorUtils.node;
-const {
-  loadItemProperties
-} = _devtoolsReps.ObjectInspectorUtils.loadProperties;
+  component: ObjectInspector,
+  utils: {
+    createNode,
+    getChildren,
+    loadProperties: {
+      loadItemProperties
+    }
+  }
+} = _devtoolsReps.objectInspector;
 
 class FrameworkComponent extends _react.PureComponent {
   async componentWillMount() {
     const expression = "this;";
     const {
       selectedFrame,
       setPopupObjectProperties
     } = this.props;
@@ -78,17 +81,17 @@ class FrameworkComponent extends _react.
 
     let roots = getChildren({
       item: root,
       loadedProperties: new Map([[root.path, loadedRootProperties]])
     });
     roots = roots.filter(r => ["state", "props"].includes(r.name));
     return _react2.default.createElement("div", {
       className: "pane framework-component"
-    }, _react2.default.createElement(_devtoolsReps.ObjectInspector, {
+    }, _react2.default.createElement(ObjectInspector, {
       roots: roots,
       autoExpandAll: false,
       autoExpandDepth: 0,
       disableWrap: true,
       focusable: false,
       dimTopLevelWindow: true,
       createObjectClient: grip => (0, _firefox.createObjectClient)(grip)
     }));
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Scopes.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Scopes.js
@@ -22,16 +22,20 @@ var _scopes = require("../../utils/pause
 
 var _devtoolsReps = require("devtools/client/shared/components/reps/reps.js");
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 /* 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/>. */
+const {
+  ObjectInspector
+} = _devtoolsReps.objectInspector;
+
 class Scopes extends _react.PureComponent {
   constructor(props, ...args) {
     const {
       why,
       selectedFrame,
       originalFrameScopes,
       generatedFrameScopes
     } = props;
@@ -74,17 +78,17 @@ class Scopes extends _react.PureComponen
       generatedScopes,
       showOriginal
     } = this.state;
     const scopes = showOriginal && originalScopes || generatedScopes;
 
     if (scopes && !isLoading) {
       return _react2.default.createElement("div", {
         className: "pane scopes-list"
-      }, _react2.default.createElement(_devtoolsReps.ObjectInspector, {
+      }, _react2.default.createElement(ObjectInspector, {
         roots: scopes,
         autoExpandAll: false,
         autoExpandDepth: 1,
         disableWrap: true,
         focusable: false,
         dimTopLevelWindow: true,
         openLink: openLink,
         createObjectClient: grip => (0, _firefox.createObjectClient)(grip)
--- a/devtools/client/debugger/new/src/reducers/index.js
+++ b/devtools/client/debugger/new/src/reducers/index.js
@@ -63,16 +63,18 @@ var _quickOpen2 = _interopRequireDefault
 var _sourceTree = require("./source-tree");
 
 var _sourceTree2 = _interopRequireDefault(_sourceTree);
 
 var _debuggee = require("./debuggee");
 
 var _debuggee2 = _interopRequireDefault(_debuggee);
 
+var _devtoolsReps = require("devtools/client/shared/components/reps/reps.js");
+
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 /* 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/>. */
 
 /**
  * Reducer index
@@ -89,10 +91,11 @@ exports.default = {
   pause: _pause2.default,
   ui: _ui2.default,
   fileSearch: _fileSearch2.default,
   ast: _ast2.default,
   coverage: _coverage2.default,
   projectTextSearch: _projectTextSearch2.default,
   quickOpen: _quickOpen2.default,
   sourceTree: _sourceTree2.default,
-  debuggee: _debuggee2.default
+  debuggee: _debuggee2.default,
+  objectInspector: _devtoolsReps.objectInspector.reducer.default
 };
\ No newline at end of file
--- a/devtools/client/debugger/new/src/selectors/index.js
+++ b/devtools/client/debugger/new/src/selectors/index.js
@@ -252,9 +252,25 @@ Object.defineProperty(exports, "getVisib
 
 var _breakpointSources = require("./breakpointSources");
 
 Object.defineProperty(exports, "getBreakpointSources", {
   enumerable: true,
   get: function () {
     return _breakpointSources.getBreakpointSources;
   }
+});
+
+var _devtoolsReps = require("devtools/client/shared/components/reps/reps.js");
+
+const {
+  reducer
+} = _devtoolsReps.objectInspector;
+Object.keys(reducer).forEach(function (key) {
+  if (key === "default" || key === "__esModule") {
+    return;
+  }
+
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: reducer[key]
+  });
 });
\ No newline at end of file
--- a/devtools/client/debugger/new/src/utils/pause/scopes/getScope.js
+++ b/devtools/client/debugger/new/src/utils/pause/scopes/getScope.js
@@ -11,16 +11,18 @@ var _getVariables = require("./getVariab
 
 var _utils = require("./utils");
 
 var _frames = require("../../pause/frames/index");
 
 /* 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/>. */
+const { utils: { node: { NODE_TYPES } } } = _devtoolsReps.objectInspector;
+
 function getScopeTitle(type, scope) {
   if (type === "block" && scope.block && scope.block.displayName) {
     return scope.block.displayName;
   }
 
   if (type === "function" && scope.function) {
     return scope.function.displayName ? (0, _frames.simplifyDisplayName)(scope.function.displayName) : L10N.getStr("anonymous");
   }
@@ -60,17 +62,17 @@ function getScope(scope, selectedFrame, 
 
     if (vars && vars.length) {
       const title = getScopeTitle(type, scope);
       vars.sort((a, b) => a.name.localeCompare(b.name));
       return {
         name: title,
         path: key,
         contents: vars,
-        type: _devtoolsReps.ObjectInspectorUtils.node.NODE_TYPES.BLOCK
+        type: NODE_TYPES.BLOCK
       };
     }
   } else if (type === "object" && scope.object) {
     let value = scope.object; // If this is the global window scope, mark it as such so that it will
     // preview Window: Global instead of Window: Window
 
     if (value.class === "Window") {
       value = { ...scope.object,
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions.js
@@ -82,15 +82,17 @@ add_task(async function() {
 
   await deleteExpression(dbg, "foo");
   await deleteExpression(dbg, "location");
   is(findAllElements(dbg, "expressionNodes").length, 0);
 
   // Test expanding properties when the debuggee is active
   await resume(dbg);
   await addExpression(dbg, "location");
-  await toggleExpressionNode(dbg, 1);
 
   is(findAllElements(dbg, "expressionNodes").length, 17);
 
+  await toggleExpressionNode(dbg, 1);
+  is(findAllElements(dbg, "expressionNodes").length, 1);
+
   await deleteExpression(dbg, "location");
   is(findAllElements(dbg, "expressionNodes").length, 0);
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-scopes.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-scopes.js
@@ -19,10 +19,10 @@ add_task(async function() {
   is(getLabel(dbg, 1), "secondCall");
   is(getLabel(dbg, 2), "<this>");
   is(getLabel(dbg, 4), "foo()");
   await toggleScopeNode(dbg, 4);
   is(getLabel(dbg, 5), "arguments");
 
   await stepOver(dbg);
   is(getLabel(dbg, 4), "foo()");
-  is(getLabel(dbg, 5), "Window");
+  is(getLabel(dbg, 11), "Window");
 });
--- a/devtools/client/inspector/extensions/components/ObjectTreeView.js
+++ b/devtools/client/inspector/extensions/components/ObjectTreeView.js
@@ -2,17 +2,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
-const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+// TODO: Upgrade to the current version reps https://bugzilla.mozilla.org/show_bug.cgi?id=1494680
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps-old");
 const { Rep } = REPS;
 const TreeViewClass = require("devtools/client/shared/components/tree/TreeView");
 const TreeView = createFactory(TreeViewClass);
 
 /**
  * The ObjectTreeView React Component is used in the ExtensionSidebar component to provide
  * a UI viewMode which shows a tree view of the passed JavaScript object.
  */
--- a/devtools/client/inspector/extensions/components/ObjectValueGripView.js
+++ b/devtools/client/inspector/extensions/components/ObjectValueGripView.js
@@ -3,17 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const Accordion = createFactory(require("devtools/client/inspector/layout/components/Accordion"));
-const reps = require("devtools/client/shared/components/reps/reps");
+
+// TODO: Upgrade to the current version reps https://bugzilla.mozilla.org/show_bug.cgi?id=1494680
+const reps = require("devtools/client/shared/components/reps/reps-old");
 const Types = require("../types");
 
 const { REPS, MODE } = reps;
 const { Grip } = REPS;
 
 const ObjectInspector = createFactory(reps.ObjectInspector);
 
 class ObjectValueGripView extends PureComponent {
--- a/devtools/client/shared/components/reps/moz.build
+++ b/devtools/client/shared/components/reps/moz.build
@@ -1,10 +1,11 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'reps-old.js',
     'reps.css',
     'reps.js',
 )
copy from devtools/client/shared/components/reps/reps.js
copy to devtools/client/shared/components/reps/reps-old.js
--- a/devtools/client/shared/components/reps/reps.js
+++ b/devtools/client/shared/components/reps/reps.js
@@ -1,18 +1,18 @@
 (function webpackUniversalModuleDefinition(root, factory) {
 	if(typeof exports === 'object' && typeof module === 'object')
-		module.exports = factory(require("devtools/client/shared/vendor/react"), require("Services"), require("devtools/client/shared/vendor/react-redux"), require("devtools/client/shared/vendor/redux"), require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react-dom-factories"));
+		module.exports = factory(require("devtools/client/shared/vendor/react"), require("Services"), require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/react-redux"));
 	else if(typeof define === 'function' && define.amd)
-		define(["devtools/client/shared/vendor/react", "Services", "devtools/client/shared/vendor/react-redux", "devtools/client/shared/vendor/redux", "devtools/client/shared/vendor/react-prop-types", "devtools/client/shared/vendor/react-dom-factories"], factory);
+		define(["devtools/client/shared/vendor/react", "Services", "devtools/client/shared/vendor/react-prop-types", "devtools/client/shared/vendor/react-dom-factories", "devtools/client/shared/vendor/react-redux"], factory);
 	else {
-		var a = typeof exports === 'object' ? factory(require("devtools/client/shared/vendor/react"), require("Services"), require("devtools/client/shared/vendor/react-redux"), require("devtools/client/shared/vendor/redux"), require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react-dom-factories")) : factory(root["devtools/client/shared/vendor/react"], root["Services"], root["devtools/client/shared/vendor/react-redux"], root["devtools/client/shared/vendor/redux"], root["devtools/client/shared/vendor/react-prop-types"], root["devtools/client/shared/vendor/react-dom-factories"]);
+		var a = typeof exports === 'object' ? factory(require("devtools/client/shared/vendor/react"), require("Services"), require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/react-redux")) : factory(root["devtools/client/shared/vendor/react"], root["Services"], root["devtools/client/shared/vendor/react-prop-types"], root["devtools/client/shared/vendor/react-dom-factories"], root["devtools/client/shared/vendor/react-redux"]);
 		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
 	}
-})(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE_0__, __WEBPACK_EXTERNAL_MODULE_22__, __WEBPACK_EXTERNAL_MODULE_3592__, __WEBPACK_EXTERNAL_MODULE_3593__, __WEBPACK_EXTERNAL_MODULE_3642__, __WEBPACK_EXTERNAL_MODULE_3643__) {
+})(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE_0__, __WEBPACK_EXTERNAL_MODULE_22__, __WEBPACK_EXTERNAL_MODULE_1758__, __WEBPACK_EXTERNAL_MODULE_1759__, __WEBPACK_EXTERNAL_MODULE_1763__) {
 return /******/ (function(modules) { // webpackBootstrap
 /******/ 	// The module cache
 /******/ 	var installedModules = {};
 /******/
 /******/ 	// The require function
 /******/ 	function __webpack_require__(moduleId) {
 /******/
 /******/ 		// Check if module is in cache
@@ -65,17 +65,17 @@ return /******/ (function(modules) { // 
 /******/
 /******/ 	// Object.prototype.hasOwnProperty.call
 /******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
 /******/
 /******/ 	// __webpack_public_path__
 /******/ 	__webpack_require__.p = "/assets/build";
 /******/
 /******/ 	// Load entry module and return exports
-/******/ 	return __webpack_require__(__webpack_require__.s = 3730);
+/******/ 	return __webpack_require__(__webpack_require__.s = 2082);
 /******/ })
 /************************************************************************/
 /******/ ({
 
 /***/ 0:
 /***/ (function(module, exports) {
 
 module.exports = __WEBPACK_EXTERNAL_MODULE_0__;
@@ -137,66 +137,45 @@ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBP
 	} else {
 		window.classNames = classNames;
 	}
 }());
 
 
 /***/ }),
 
-/***/ 22:
+/***/ 1758:
 /***/ (function(module, exports) {
 
-module.exports = __WEBPACK_EXTERNAL_MODULE_22__;
-
-/***/ }),
-
-/***/ 3592:
-/***/ (function(module, exports) {
-
-module.exports = __WEBPACK_EXTERNAL_MODULE_3592__;
+module.exports = __WEBPACK_EXTERNAL_MODULE_1758__;
 
 /***/ }),
 
-/***/ 3593:
+/***/ 1759:
 /***/ (function(module, exports) {
 
-module.exports = __WEBPACK_EXTERNAL_MODULE_3593__;
+module.exports = __WEBPACK_EXTERNAL_MODULE_1759__;
 
 /***/ }),
 
-/***/ 3642:
-/***/ (function(module, exports) {
-
-module.exports = __WEBPACK_EXTERNAL_MODULE_3642__;
-
-/***/ }),
-
-/***/ 3643:
-/***/ (function(module, exports) {
-
-module.exports = __WEBPACK_EXTERNAL_MODULE_3643__;
-
-/***/ }),
-
-/***/ 3644:
+/***/ 1760:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
 const validProtocols = /(http|https|ftp|data|resource|chrome):/i;
 const tokenSplitRegex = /(\s|\'|\"|\\)+/;
 const ELLIPSIS = "\u2026";
-const dom = __webpack_require__(3643);
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Returns true if the given object is a grip (see RDP protocol)
  */
 function isGrip(object) {
   return object && object.actor;
 }
@@ -605,17 +584,17 @@ module.exports = {
   getGripType,
   tokenSplitRegex,
   ellipsisElement,
   ELLIPSIS
 };
 
 /***/ }),
 
-/***/ 3645:
+/***/ 1762:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
@@ -625,62 +604,69 @@ module.exports = {
     TINY: Symbol("TINY"),
     SHORT: Symbol("SHORT"),
     LONG: Symbol("LONG")
   }
 };
 
 /***/ }),
 
-/***/ 3647:
+/***/ 1763:
+/***/ (function(module, exports) {
+
+module.exports = __WEBPACK_EXTERNAL_MODULE_1763__;
+
+/***/ }),
+
+/***/ 1768:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
-__webpack_require__(3672);
+__webpack_require__(1824);
 
 // Load all existing rep templates
-const Undefined = __webpack_require__(3673);
-const Null = __webpack_require__(3674);
-const StringRep = __webpack_require__(3648);
-const Number = __webpack_require__(3675);
-const ArrayRep = __webpack_require__(3649);
-const Obj = __webpack_require__(3676);
-const SymbolRep = __webpack_require__(3677);
-const InfinityRep = __webpack_require__(3678);
-const NaNRep = __webpack_require__(3679);
-const Accessor = __webpack_require__(3680);
+const Undefined = __webpack_require__(1825);
+const Null = __webpack_require__(1826);
+const StringRep = __webpack_require__(1769);
+const Number = __webpack_require__(1827);
+const ArrayRep = __webpack_require__(1773);
+const Obj = __webpack_require__(1828);
+const SymbolRep = __webpack_require__(1829);
+const InfinityRep = __webpack_require__(1830);
+const NaNRep = __webpack_require__(1831);
+const Accessor = __webpack_require__(1832);
 
 // DOM types (grips)
-const Accessible = __webpack_require__(3787);
-const Attribute = __webpack_require__(3681);
-const DateTime = __webpack_require__(3682);
-const Document = __webpack_require__(3683);
-const DocumentType = __webpack_require__(3684);
-const Event = __webpack_require__(3685);
-const Func = __webpack_require__(3658);
-const PromiseRep = __webpack_require__(3686);
-const RegExp = __webpack_require__(3687);
-const StyleSheet = __webpack_require__(3688);
-const CommentNode = __webpack_require__(3689);
-const ElementNode = __webpack_require__(3690);
-const TextNode = __webpack_require__(3691);
-const ErrorRep = __webpack_require__(3660);
-const Window = __webpack_require__(3692);
-const ObjectWithText = __webpack_require__(3693);
-const ObjectWithURL = __webpack_require__(3694);
-const GripArray = __webpack_require__(3661);
-const GripMap = __webpack_require__(3663);
-const GripMapEntry = __webpack_require__(3664);
-const Grip = __webpack_require__(3656);
+const Accessible = __webpack_require__(1833);
+const Attribute = __webpack_require__(1834);
+const DateTime = __webpack_require__(1835);
+const Document = __webpack_require__(1836);
+const DocumentType = __webpack_require__(1837);
+const Event = __webpack_require__(1838);
+const Func = __webpack_require__(1791);
+const PromiseRep = __webpack_require__(1839);
+const RegExp = __webpack_require__(1840);
+const StyleSheet = __webpack_require__(1841);
+const CommentNode = __webpack_require__(1842);
+const ElementNode = __webpack_require__(1843);
+const TextNode = __webpack_require__(1844);
+const ErrorRep = __webpack_require__(1793);
+const Window = __webpack_require__(1845);
+const ObjectWithText = __webpack_require__(1846);
+const ObjectWithURL = __webpack_require__(1847);
+const GripArray = __webpack_require__(1794);
+const GripMap = __webpack_require__(1796);
+const GripMapEntry = __webpack_require__(1797);
+const Grip = __webpack_require__(1782);
 
 // List of all registered template.
 // XXX there should be a way for extensions to register a new
 // or modify an existing rep.
 const reps = [RegExp, StyleSheet, Event, DateTime, CommentNode, Accessible, ElementNode, TextNode, Attribute, Func, PromiseRep, ArrayRep, Document, DocumentType, Window, ObjectWithText, ObjectWithURL, ErrorRep, GripArray, GripMap, GripMapEntry, Grip, Undefined, Null, StringRep, Number, SymbolRep, InfinityRep, NaNRep, Accessor, Obj];
 
 /**
  * Generic rep that is used for rendering native JS types or an object.
@@ -765,43 +751,43 @@ module.exports = {
     Window
   },
   // Exporting for tests
   getRep
 };
 
 /***/ }),
 
-/***/ 3648:
+/***/ 1769:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 
 const {
   containsURL,
   isURL,
   escapeString,
   getGripType,
   rawCropString,
   sanitizeString,
   wrapRender,
   isGrip,
   tokenSplitRegex,
   ELLIPSIS
-} = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+} = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { a, span } = dom;
 
 /**
  * Renders a string. String value is enclosed within quotes.
  */
 StringRep.propTypes = {
   useQuotes: PropTypes.bool,
   escapeWhitespace: PropTypes.bool,
@@ -1039,33 +1025,33 @@ function supportsObject(object, noGrip =
 module.exports = {
   rep: wrapRender(StringRep),
   supportsObject,
   isLongString
 };
 
 /***/ }),
 
-/***/ 3649:
+/***/ 1773:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
 
 /* 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/>. */
 
 // Dependencies
-const dom = __webpack_require__(3643);
-const PropTypes = __webpack_require__(3642);
-const { wrapRender } = __webpack_require__(3644);
-const { MODE } = __webpack_require__(3645);
+const dom = __webpack_require__(1759);
+const PropTypes = __webpack_require__(1758);
+const { wrapRender } = __webpack_require__(1760);
+const { MODE } = __webpack_require__(1762);
 const { span } = dom;
 
 const ModePropType = PropTypes.oneOf(
 // @TODO Change this to Object.values when supported in Node's version of V8
 Object.keys(MODE).map(key => MODE[key]));
 
 /**
  * Renders an array. The array is enclosed by left and right bracket
@@ -1147,17 +1133,17 @@ function arrayIterator(props, array, max
  */
 ItemRep.propTypes = {
   object: PropTypes.any.isRequired,
   delim: PropTypes.string.isRequired,
   mode: ModePropType
 };
 
 function ItemRep(props) {
-  const { Rep } = __webpack_require__(3647);
+  const { Rep } = __webpack_require__(1768);
 
   const { object, delim, mode } = props;
   return span({}, Rep(_extends({}, props, {
     object: object,
     mode: mode
   })), delim);
 }
 
@@ -1179,34 +1165,34 @@ module.exports = {
   supportsObject,
   maxLengthMap,
   getLength,
   ModePropType
 };
 
 /***/ }),
 
-/***/ 3650:
+/***/ 1774:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
 
 /* 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/>. */
 
 // Dependencies
-const PropTypes = __webpack_require__(3642);
-const { maybeEscapePropertyName, wrapRender } = __webpack_require__(3644);
-const { MODE } = __webpack_require__(3645);
-
-const { span } = __webpack_require__(3643);
+const PropTypes = __webpack_require__(1758);
+const { maybeEscapePropertyName, wrapRender } = __webpack_require__(1760);
+const { MODE } = __webpack_require__(1762);
+
+const { span } = __webpack_require__(1759);
 
 /**
  * Property for Obj (local JS objects), Grip (remote JS objects)
  * and GripMap (remote JS maps and weakmaps) reps.
  * It's used to render object properties.
  */
 PropRep.propTypes = {
   // Property name.
@@ -1227,18 +1213,18 @@ PropRep.propTypes = {
 /**
  * Function that given a name, a delimiter and an object returns an array
  * of React elements representing an object property (e.g. `name: value`)
  *
  * @param {Object} props
  * @return {Array} Array of React elements.
  */
 function PropRep(props) {
-  const Grip = __webpack_require__(3656);
-  const { Rep } = __webpack_require__(3647);
+  const Grip = __webpack_require__(1782);
+  const { Rep } = __webpack_require__(1768);
 
   let { name, mode, equal, suppressQuotes } = props;
 
   let key;
   // The key can be a simple string, for plain objects,
   // or another object for maps and weakmaps.
   if (typeof name === "string") {
     if (!suppressQuotes) {
@@ -1259,73 +1245,71 @@ function PropRep(props) {
   }, equal), Rep(_extends({}, props))];
 }
 
 // Exports from this module
 module.exports = wrapRender(PropRep);
 
 /***/ }),
 
-/***/ 3655:
+/***/ 1778:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
-const { MODE } = __webpack_require__(3645);
-const { REPS, getRep } = __webpack_require__(3647);
-const ObjectInspector = __webpack_require__(3695);
-const ObjectInspectorUtils = __webpack_require__(3657);
+const { MODE } = __webpack_require__(1762);
+const { REPS, getRep } = __webpack_require__(1768);
+const objectInspector = __webpack_require__(1848);
 
 const {
   parseURLEncodedText,
   parseURLParams,
   maybeEscapePropertyName,
   getGripPreviewItems
-} = __webpack_require__(3644);
+} = __webpack_require__(1760);
 
 module.exports = {
   REPS,
   getRep,
   MODE,
   maybeEscapePropertyName,
   parseURLEncodedText,
   parseURLParams,
   getGripPreviewItems,
-  ObjectInspector,
-  ObjectInspectorUtils
+  objectInspector
 };
 
 /***/ }),
 
-/***/ 3656:
+/***/ 1782:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
 
 /* 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/>. */
 
 // ReactJS
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 
 // Dependencies
-const { interleave, isGrip, wrapRender } = __webpack_require__(3644);
-const PropRep = __webpack_require__(3650);
-const { MODE } = __webpack_require__(3645);
-
-const dom = __webpack_require__(3643);
+const { interleave, isGrip, wrapRender } = __webpack_require__(1760);
+const PropRep = __webpack_require__(1774);
+const { MODE } = __webpack_require__(1762);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders generic grip. Grip is client representation
  * of remote JS object and is used as an input object
  * for this rep component.
  */
 GripRep.propTypes = {
@@ -1411,17 +1395,17 @@ function safePropIterator(props, object,
   } catch (err) {
     console.error(err);
   }
   return [];
 }
 
 function propIterator(props, object, max) {
   if (object.preview && Object.keys(object.preview).includes("wrappedValue")) {
-    const { Rep } = __webpack_require__(3647);
+    const { Rep } = __webpack_require__(1768);
 
     return [Rep({
       object: object.preview.wrappedValue,
       mode: props.mode || MODE.TINY,
       defaultRep: Grip
     })];
   }
 
@@ -1600,1293 +1584,35 @@ const Grip = {
   maxLengthMap
 };
 
 // Exports from this module
 module.exports = Grip;
 
 /***/ }),
 
-/***/ 3657:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-/* 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/>. */
-
-const client = __webpack_require__(3665);
-const loadProperties = __webpack_require__(3666);
-const node = __webpack_require__(3667);
-const { nodeIsError, nodeIsPrimitive } = node;
-const selection = __webpack_require__(3698);
-
-const { MODE } = __webpack_require__(3645);
-const {
-  REPS: { Rep, Grip }
-} = __webpack_require__(3647);
-
-
-function shouldRenderRootsInReps(roots) {
-  if (roots.length > 1) {
-    return false;
-  }
-
-  const root = roots[0];
-  const name = root && root.name;
-  return (name === null || typeof name === "undefined") && (nodeIsPrimitive(root) || nodeIsError(root));
-}
-
-function renderRep(item, props) {
-  return Rep(_extends({}, props, {
-    object: node.getValue(item),
-    mode: props.mode || MODE.TINY,
-    defaultRep: Grip
-  }));
-}
-
-module.exports = {
-  client,
-  loadProperties,
-  node,
-  renderRep,
-  selection,
-  shouldRenderRootsInReps
-};
-
-/***/ }),
-
-/***/ 3658:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-/* 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/>. */
-
-// ReactJS
-const PropTypes = __webpack_require__(3642);
-
-// Reps
-const { getGripType, isGrip, cropString, wrapRender } = __webpack_require__(3644);
-const { MODE } = __webpack_require__(3645);
-
-const dom = __webpack_require__(3643);
-const { span } = dom;
-
-const IGNORED_SOURCE_URLS = ["debugger eval code"];
-
-/**
- * This component represents a template for Function objects.
- */
-FunctionRep.propTypes = {
-  object: PropTypes.object.isRequired,
-  parameterNames: PropTypes.array,
-  onViewSourceInDebugger: PropTypes.func
-};
-
-function FunctionRep(props) {
-  const { object: grip, onViewSourceInDebugger, recordTelemetryEvent } = props;
-
-  let jumpToDefinitionButton;
-  if (onViewSourceInDebugger && grip.location && grip.location.url && !IGNORED_SOURCE_URLS.includes(grip.location.url)) {
-    jumpToDefinitionButton = dom.button({
-      className: "jump-definition",
-      draggable: false,
-      title: "Jump to definition",
-      onClick: e => {
-        // Stop the event propagation so we don't trigger ObjectInspector
-        // expand/collapse.
-        e.stopPropagation();
-        if (recordTelemetryEvent) {
-          recordTelemetryEvent("jump_to_definition");
-        }
-        onViewSourceInDebugger(grip.location);
-      }
-    });
-  }
-
-  return span({
-    "data-link-actor-id": grip.actor,
-    className: "objectBox objectBox-function",
-    // Set dir="ltr" to prevent function parentheses from
-    // appearing in the wrong direction
-    dir: "ltr"
-  }, getTitle(grip, props), getFunctionName(grip, props), "(", ...renderParams(props), ")", jumpToDefinitionButton);
-}
-
-function getTitle(grip, props) {
-  const { mode } = props;
-
-  if (mode === MODE.TINY && !grip.isGenerator && !grip.isAsync) {
-    return null;
-  }
-
-  let title = mode === MODE.TINY ? "" : "function ";
-
-  if (grip.isGenerator) {
-    title = mode === MODE.TINY ? "* " : "function* ";
-  }
-
-  if (grip.isAsync) {
-    title = `${"async" + " "}${title}`;
-  }
-
-  return span({
-    className: "objectTitle"
-  }, title);
-}
-
-/**
- * Returns a ReactElement representing the function name.
- *
- * @param {Object} grip : Function grip
- * @param {Object} props: Function rep props
- */
-function getFunctionName(grip, props = {}) {
-  let { functionName } = props;
-  let name;
-
-  if (functionName) {
-    const end = functionName.length - 1;
-    functionName = functionName.startsWith('"') && functionName.endsWith('"') ? functionName.substring(1, end) : functionName;
-  }
-
-  if (grip.displayName != undefined && functionName != undefined && grip.displayName != functionName) {
-    name = `${functionName}:${grip.displayName}`;
-  } else {
-    name = cleanFunctionName(grip.userDisplayName || grip.displayName || grip.name || props.functionName || "");
-  }
-
-  return cropString(name, 100);
-}
-
-const objectProperty = /([\w\d]+)$/;
-const arrayProperty = /\[(.*?)\]$/;
-const functionProperty = /([\w\d]+)[\/\.<]*?$/;
-const annonymousProperty = /([\w\d]+)\(\^\)$/;
-
-/**
- * Decodes an anonymous naming scheme that
- * spider monkey implements based on "Naming Anonymous JavaScript Functions"
- * http://johnjbarton.github.io/nonymous/index.html
- *
- * @param {String} name : Function name to clean up
- * @returns String
- */
-function cleanFunctionName(name) {
-  for (const reg of [objectProperty, arrayProperty, functionProperty, annonymousProperty]) {
-    const match = reg.exec(name);
-    if (match) {
-      return match[1];
-    }
-  }
-
-  return name;
-}
-
-function renderParams(props) {
-  const { parameterNames = [] } = props;
-
-  return parameterNames.filter(param => param).reduce((res, param, index, arr) => {
-    res.push(span({ className: "param" }, param));
-    if (index < arr.length - 1) {
-      res.push(span({ className: "delimiter" }, ", "));
-    }
-    return res;
-  }, []);
-}
-
-// Registration
-function supportsObject(grip, noGrip = false) {
-  const type = getGripType(grip, noGrip);
-  if (noGrip === true || !isGrip(grip)) {
-    return type == "function";
-  }
-
-  return type == "Function";
-}
-
-// Exports from this module
-
-module.exports = {
-  rep: wrapRender(FunctionRep),
-  supportsObject,
-  cleanFunctionName,
-  // exported for testing purpose.
-  getFunctionName
-};
-
-/***/ }),
-
-/***/ 3659:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-/* 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/>. */
-
-module.exports = {
-  ELEMENT_NODE: 1,
-  ATTRIBUTE_NODE: 2,
-  TEXT_NODE: 3,
-  CDATA_SECTION_NODE: 4,
-  ENTITY_REFERENCE_NODE: 5,
-  ENTITY_NODE: 6,
-  PROCESSING_INSTRUCTION_NODE: 7,
-  COMMENT_NODE: 8,
-  DOCUMENT_NODE: 9,
-  DOCUMENT_TYPE_NODE: 10,
-  DOCUMENT_FRAGMENT_NODE: 11,
-  NOTATION_NODE: 12,
-
-  // DocumentPosition
-  DOCUMENT_POSITION_DISCONNECTED: 0x01,
-  DOCUMENT_POSITION_PRECEDING: 0x02,
-  DOCUMENT_POSITION_FOLLOWING: 0x04,
-  DOCUMENT_POSITION_CONTAINS: 0x08,
-  DOCUMENT_POSITION_CONTAINED_BY: 0x10,
-  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 0x20
-};
-
-/***/ }),
-
-/***/ 3660:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-/* 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/>. */
-
-// ReactJS
-const PropTypes = __webpack_require__(3642);
-// Utils
-const { getGripType, isGrip, wrapRender } = __webpack_require__(3644);
-const { cleanFunctionName } = __webpack_require__(3658);
-const { isLongString } = __webpack_require__(3648);
-const { MODE } = __webpack_require__(3645);
-
-const dom = __webpack_require__(3643);
-const { span } = dom;
-const IGNORED_SOURCE_URLS = ["debugger eval code"];
-
-/**
- * Renders Error objects.
- */
-ErrorRep.propTypes = {
-  object: PropTypes.object.isRequired,
-  // @TODO Change this to Object.values when supported in Node's version of V8
-  mode: PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key]))
-};
-
-function ErrorRep(props) {
-  const object = props.object;
-  const preview = object.preview;
-
-  let name;
-  if (preview && preview.name && preview.kind) {
-    switch (preview.kind) {
-      case "Error":
-        name = preview.name;
-        break;
-      case "DOMException":
-        name = preview.kind;
-        break;
-      default:
-        throw new Error("Unknown preview kind for the Error rep.");
-    }
-  } else {
-    name = "Error";
-  }
-
-  const content = [];
-
-  if (props.mode === MODE.TINY) {
-    content.push(name);
-  } else {
-    content.push(`${name}: "${preview.message}"`);
-  }
-
-  if (preview.stack && props.mode !== MODE.TINY) {
-    content.push("\n", getStacktraceElements(props, preview));
-  }
-
-  return span({
-    "data-link-actor-id": object.actor,
-    className: "objectBox-stackTrace"
-  }, content);
-}
-
-/**
- * Returns a React element reprensenting the Error stacktrace, i.e.
- * transform error.stack from:
- *
- * semicolon@debugger eval code:1:109
- * jkl@debugger eval code:1:63
- * asdf@debugger eval code:1:28
- * @debugger eval code:1:227
- *
- * Into a column layout:
- *
- * semicolon  (<anonymous>:8:10)
- * jkl        (<anonymous>:5:10)
- * asdf       (<anonymous>:2:10)
- *            (<anonymous>:11:1)
- */
-function getStacktraceElements(props, preview) {
-  const stack = [];
-  if (!preview.stack) {
-    return stack;
-  }
-
-  const isStacktraceALongString = isLongString(preview.stack);
-  const stackString = isStacktraceALongString ? preview.stack.initial : preview.stack;
-
-  stackString.split("\n").forEach((frame, index, frames) => {
-    if (!frame) {
-      // Skip any blank lines
-      return;
-    }
-
-    // If the stacktrace is a longString, don't include the last frame in the
-    // array, since it is certainly incomplete.
-    // Can be removed when https://bugzilla.mozilla.org/show_bug.cgi?id=1448833
-    // is fixed.
-    if (isStacktraceALongString && index === frames.length - 1) {
-      return;
-    }
-
-    let functionName;
-    let location;
-
-    // Given the input: "functionName@scriptLocation:2:100"
-    // Result: [
-    //   "functionName@scriptLocation:2:100",
-    //   "functionName",
-    //   "scriptLocation:2:100"
-    // ]
-    const result = frame.match(/^(.*)@(.*)$/);
-    if (result && result.length === 3) {
-      functionName = result[1];
-
-      // If the resource was loaded by base-loader.js, the location looks like:
-      // resource://devtools/shared/base-loader.js -> resource://path/to/file.js .
-      // What's needed is only the last part after " -> ".
-      location = result[2].split(" -> ").pop();
-    }
-
-    if (!functionName) {
-      functionName = "<anonymous>";
-    }
-
-    let onLocationClick;
-    // Given the input: "scriptLocation:2:100"
-    // Result:
-    // ["scriptLocation:2:100", "scriptLocation", "2", "100"]
-    const locationParts = location.match(/^(.*):(\d+):(\d+)$/);
-
-    if (props.onViewSourceInDebugger && location && locationParts && !IGNORED_SOURCE_URLS.includes(locationParts[1])) {
-      const [, url, line, column] = locationParts;
-      onLocationClick = e => {
-        // Don't trigger ObjectInspector expand/collapse.
-        e.stopPropagation();
-        props.onViewSourceInDebugger({
-          url,
-          line: Number(line),
-          column: Number(column)
-        });
-      };
-    }
-
-    stack.push("\t", span({
-      key: `fn${index}`,
-      className: "objectBox-stackTrace-fn"
-    }, cleanFunctionName(functionName)), " ", span({
-      key: `location${index}`,
-      className: "objectBox-stackTrace-location",
-      onClick: onLocationClick,
-      title: onLocationClick ? `View source in debugger → ${location}` : undefined
-    }, location), "\n");
-  });
-
-  return span({
-    key: "stack",
-    className: "objectBox-stackTrace-grid"
-  }, stack);
-}
-
-// Registration
-function supportsObject(object, noGrip = false) {
-  if (noGrip === true || !isGrip(object)) {
-    return false;
-  }
-  return object.preview && getGripType(object, noGrip) === "Error" || object.class === "DOMException";
-}
-
-// Exports from this module
-module.exports = {
-  rep: wrapRender(ErrorRep),
-  supportsObject
-};
-
-/***/ }),
-
-/***/ 3661:
+/***/ 1783:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
 
 /* 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/>. */
 
-// Dependencies
-const PropTypes = __webpack_require__(3642);
-
-const { lengthBubble } = __webpack_require__(3662);
-const {
-  interleave,
-  getGripType,
-  isGrip,
-  wrapRender,
-  ellipsisElement
-} = __webpack_require__(3644);
-const { MODE } = __webpack_require__(3645);
-
-const dom = __webpack_require__(3643);
-const { span } = dom;
-const { ModePropType } = __webpack_require__(3649);
-const DEFAULT_TITLE = "Array";
-
-/**
- * Renders an array. The array is enclosed by left and right bracket
- * and the max number of rendered items depends on the current mode.
- */
-GripArray.propTypes = {
-  object: PropTypes.object.isRequired,
-  // @TODO Change this to Object.values when supported in Node's version of V8
-  mode: ModePropType,
-  provider: PropTypes.object,
-  onDOMNodeMouseOver: PropTypes.func,
-  onDOMNodeMouseOut: PropTypes.func,
-  onInspectIconClick: PropTypes.func
-};
-
-function GripArray(props) {
-  const { object, mode = MODE.SHORT } = props;
-
-  let brackets;
-  const needSpace = function (space) {
-    return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
-  };
-
-  const config = {
-    "data-link-actor-id": object.actor,
-    className: "objectBox objectBox-array"
-  };
-
-  const title = getTitle(props, object);
-
-  if (mode === MODE.TINY) {
-    const isEmpty = getLength(object) === 0;
-
-    // Omit bracketed ellipsis for non-empty non-Array arraylikes (f.e: Sets).
-    if (!isEmpty && object.class !== "Array") {
-      return span(config, title);
-    }
-
-    brackets = needSpace(false);
-    return span(config, title, span({
-      className: "arrayLeftBracket"
-    }, brackets.left), isEmpty ? null : ellipsisElement, span({
-      className: "arrayRightBracket"
-    }, brackets.right));
-  }
-
-  const max = maxLengthMap.get(mode);
-  const items = arrayIterator(props, object, max);
-  brackets = needSpace(items.length > 0);
-
-  return span({
-    "data-link-actor-id": object.actor,
-    className: "objectBox objectBox-array"
-  }, title, span({
-    className: "arrayLeftBracket"
-  }, brackets.left), ...interleave(items, ", "), span({
-    className: "arrayRightBracket"
-  }, brackets.right), span({
-    className: "arrayProperties",
-    role: "group"
-  }));
-}
-
-function getLength(grip) {
-  if (!grip.preview) {
-    return 0;
-  }
-
-  return grip.preview.length || grip.preview.childNodesLength || 0;
-}
-
-function getTitle(props, object) {
-  const objectLength = getLength(object);
-  const isEmpty = objectLength === 0;
-
-  let title = props.title || object.class || DEFAULT_TITLE;
-
-  const length = lengthBubble({
-    object,
-    mode: props.mode,
-    maxLengthMap,
-    getLength
-  });
-
-  if (props.mode === MODE.TINY) {
-    if (isEmpty) {
-      if (object.class === DEFAULT_TITLE) {
-        return null;
-      }
-
-      return span({ className: "objectTitle" }, `${title} `);
-    }
-
-    let trailingSpace;
-    if (object.class === DEFAULT_TITLE) {
-      title = null;
-      trailingSpace = " ";
-    }
-
-    return span({ className: "objectTitle" }, title, length, trailingSpace);
-  }
-
-  return span({ className: "objectTitle" }, title, length, " ");
-}
-
-function getPreviewItems(grip) {
-  if (!grip.preview) {
-    return null;
-  }
-
-  return grip.preview.items || grip.preview.childNodes || [];
-}
-
-function arrayIterator(props, grip, max) {
-  const { Rep } = __webpack_require__(3647);
-
-  let items = [];
-  const gripLength = getLength(grip);
-
-  if (!gripLength) {
-    return items;
-  }
-
-  const previewItems = getPreviewItems(grip);
-  const provider = props.provider;
-
-  let emptySlots = 0;
-  let foldedEmptySlots = 0;
-  items = previewItems.reduce((res, itemGrip) => {
-    if (res.length >= max) {
-      return res;
-    }
-
-    let object;
-    try {
-      if (!provider && itemGrip === null) {
-        emptySlots++;
-        return res;
-      }
-
-      object = provider ? provider.getValue(itemGrip) : itemGrip;
-    } catch (exc) {
-      object = exc;
-    }
-
-    if (emptySlots > 0) {
-      res.push(getEmptySlotsElement(emptySlots));
-      foldedEmptySlots = foldedEmptySlots + emptySlots - 1;
-      emptySlots = 0;
-    }
-
-    if (res.length < max) {
-      res.push(Rep(_extends({}, props, {
-        object,
-        mode: MODE.TINY,
-        // Do not propagate title to array items reps
-        title: undefined
-      })));
-    }
-
-    return res;
-  }, []);
-
-  // Handle trailing empty slots if there are some.
-  if (items.length < max && emptySlots > 0) {
-    items.push(getEmptySlotsElement(emptySlots));
-    foldedEmptySlots = foldedEmptySlots + emptySlots - 1;
-  }
-
-  const itemsShown = items.length + foldedEmptySlots;
-  if (gripLength > itemsShown) {
-    items.push(ellipsisElement);
-  }
-
-  return items;
-}
-
-function getEmptySlotsElement(number) {
-  // TODO: Use l10N - See https://github.com/devtools-html/reps/issues/141
-  return `<${number} empty slot${number > 1 ? "s" : ""}>`;
-}
-
-function supportsObject(grip, noGrip = false) {
-  if (noGrip === true || !isGrip(grip)) {
-    return false;
-  }
-
-  return grip.preview && (grip.preview.kind == "ArrayLike" || getGripType(grip, noGrip) === "DocumentFragment");
-}
-
-const maxLengthMap = new Map();
-maxLengthMap.set(MODE.SHORT, 3);
-maxLengthMap.set(MODE.LONG, 10);
-
-// Exports from this module
-module.exports = {
-  rep: wrapRender(GripArray),
-  supportsObject,
-  maxLengthMap,
-  getLength
-};
-
-/***/ }),
-
-/***/ 3662:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-/* 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/>. */
-
-const PropTypes = __webpack_require__(3642);
-
-const { wrapRender } = __webpack_require__(3644);
-const { MODE } = __webpack_require__(3645);
-const { ModePropType } = __webpack_require__(3649);
-
-const dom = __webpack_require__(3643);
-const { span } = dom;
-
-GripLengthBubble.propTypes = {
-  object: PropTypes.object.isRequired,
-  maxLengthMap: PropTypes.instanceOf(Map).isRequired,
-  getLength: PropTypes.func.isRequired,
-  mode: ModePropType,
-  visibilityThreshold: PropTypes.number
-};
-
-function GripLengthBubble(props) {
-  const {
-    object,
-    mode = MODE.SHORT,
-    visibilityThreshold = 2,
-    maxLengthMap,
-    getLength,
-    showZeroLength = false
-  } = props;
-
-  const length = getLength(object);
-  const isEmpty = length === 0;
-  const isObvious = [MODE.SHORT, MODE.LONG].includes(mode) && length > 0 && length <= maxLengthMap.get(mode) && length <= visibilityThreshold;
-  if (isEmpty && !showZeroLength || isObvious) {
-    return "";
-  }
-
-  return span({
-    className: "objectLengthBubble"
-  }, `(${length})`);
-}
-
-module.exports = {
-  lengthBubble: wrapRender(GripLengthBubble)
-};
-
-/***/ }),
-
-/***/ 3663:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-/* 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/>. */
-
-// Dependencies
-
-const { lengthBubble } = __webpack_require__(3662);
-const PropTypes = __webpack_require__(3642);
-const {
-  interleave,
-  isGrip,
-  wrapRender,
-  ellipsisElement
-} = __webpack_require__(3644);
-const PropRep = __webpack_require__(3650);
-const { MODE } = __webpack_require__(3645);
-const { ModePropType } = __webpack_require__(3649);
-
-const { span } = __webpack_require__(3643);
-
-/**
- * Renders an map. A map is represented by a list of its
- * entries enclosed in curly brackets.
- */
-GripMap.propTypes = {
-  object: PropTypes.object,
-  // @TODO Change this to Object.values when supported in Node's version of V8
-  mode: ModePropType,
-  isInterestingEntry: PropTypes.func,
-  onDOMNodeMouseOver: PropTypes.func,
-  onDOMNodeMouseOut: PropTypes.func,
-  onInspectIconClick: PropTypes.func,
-  title: PropTypes.string
-};
-
-function GripMap(props) {
-  const { mode, object } = props;
-
-  const config = {
-    "data-link-actor-id": object.actor,
-    className: "objectBox objectBox-object"
-  };
-
-  const title = getTitle(props, object);
-  const isEmpty = getLength(object) === 0;
-
-  if (isEmpty || mode === MODE.TINY) {
-    return span(config, title);
-  }
-
-  const propsArray = safeEntriesIterator(props, object, maxLengthMap.get(mode));
-
-  return span(config, title, span({
-    className: "objectLeftBrace"
-  }, " { "), ...interleave(propsArray, ", "), span({
-    className: "objectRightBrace"
-  }, " }"));
-}
-
-function getTitle(props, object) {
-  const title = props.title || (object && object.class ? object.class : "Map");
-  return span({
-    className: "objectTitle"
-  }, title, lengthBubble({
-    object,
-    mode: props.mode,
-    maxLengthMap,
-    getLength,
-    showZeroLength: true
-  }));
-}
-
-function safeEntriesIterator(props, object, max) {
-  max = typeof max === "undefined" ? 3 : max;
-  try {
-    return entriesIterator(props, object, max);
-  } catch (err) {
-    console.error(err);
-  }
-  return [];
-}
-
-function entriesIterator(props, object, max) {
-  // Entry filter. Show only interesting entries to the user.
-  const isInterestingEntry = props.isInterestingEntry || ((type, value) => {
-    return type == "boolean" || type == "number" || type == "string" && value.length != 0;
-  });
-
-  const mapEntries = object.preview && object.preview.entries ? object.preview.entries : [];
-
-  let indexes = getEntriesIndexes(mapEntries, max, isInterestingEntry);
-  if (indexes.length < max && indexes.length < mapEntries.length) {
-    // There are not enough entries yet, so we add uninteresting entries.
-    indexes = indexes.concat(getEntriesIndexes(mapEntries, max - indexes.length, (t, value, name) => {
-      return !isInterestingEntry(t, value, name);
-    }));
-  }
-
-  const entries = getEntries(props, mapEntries, indexes);
-  if (entries.length < getLength(object)) {
-    // There are some undisplayed entries. Then display "…".
-    entries.push(ellipsisElement);
-  }
-
-  return entries;
-}
-
-/**
- * Get entries ordered by index.
- *
- * @param {Object} props Component props.
- * @param {Array} entries Entries array.
- * @param {Array} indexes Indexes of entries.
- * @return {Array} Array of PropRep.
- */
-function getEntries(props, entries, indexes) {
-  const { onDOMNodeMouseOver, onDOMNodeMouseOut, onInspectIconClick } = props;
-
-  // Make indexes ordered by ascending.
-  indexes.sort(function (a, b) {
-    return a - b;
-  });
-
-  return indexes.map((index, i) => {
-    const [key, entryValue] = entries[index];
-    const value = entryValue.value !== undefined ? entryValue.value : entryValue;
-
-    return PropRep({
-      name: key,
-      equal: " \u2192 ",
-      object: value,
-      mode: MODE.TINY,
-      onDOMNodeMouseOver,
-      onDOMNodeMouseOut,
-      onInspectIconClick
-    });
-  });
-}
-
-/**
- * Get the indexes of entries in the map.
- *
- * @param {Array} entries Entries array.
- * @param {Number} max The maximum length of indexes array.
- * @param {Function} filter Filter the entry you want.
- * @return {Array} Indexes of filtered entries in the map.
- */
-function getEntriesIndexes(entries, max, filter) {
-  return entries.reduce((indexes, [key, entry], i) => {
-    if (indexes.length < max) {
-      const value = entry && entry.value !== undefined ? entry.value : entry;
-      // Type is specified in grip's "class" field and for primitive
-      // values use typeof.
-      const type = (value && value.class ? value.class : typeof value).toLowerCase();
-
-      if (filter(type, value, key)) {
-        indexes.push(i);
-      }
-    }
-
-    return indexes;
-  }, []);
-}
-
-function getLength(grip) {
-  return grip.preview.size || 0;
-}
-
-function supportsObject(grip, noGrip = false) {
-  if (noGrip === true || !isGrip(grip)) {
-    return false;
-  }
-  return grip.preview && grip.preview.kind == "MapLike";
-}
-
-const maxLengthMap = new Map();
-maxLengthMap.set(MODE.SHORT, 3);
-maxLengthMap.set(MODE.LONG, 10);
-
-// Exports from this module
-module.exports = {
-  rep: wrapRender(GripMap),
-  supportsObject,
-  maxLengthMap,
-  getLength
-};
-
-/***/ }),
-
-/***/ 3664:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-/* 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/>. */
-
-// Dependencies
-const PropTypes = __webpack_require__(3642);
-// Shortcuts
-const dom = __webpack_require__(3643);
-const { span } = dom;
-const { wrapRender } = __webpack_require__(3644);
-const PropRep = __webpack_require__(3650);
-const { MODE } = __webpack_require__(3645);
-/**
- * Renders an map entry. A map entry is represented by its key,
- * a column and its value.
- */
-GripMapEntry.propTypes = {
-  object: PropTypes.object,
-  // @TODO Change this to Object.values when supported in Node's version of V8
-  mode: PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-  onDOMNodeMouseOver: PropTypes.func,
-  onDOMNodeMouseOut: PropTypes.func,
-  onInspectIconClick: PropTypes.func
-};
-
-function GripMapEntry(props) {
-  const { object } = props;
-
-  const { key, value } = object.preview;
-
-  return span({
-    className: "objectBox objectBox-map-entry"
-  }, PropRep(_extends({}, props, {
-    name: key,
-    object: value,
-    equal: " \u2192 ",
-    title: null,
-    suppressQuotes: false
-  })));
-}
-
-function supportsObject(grip, noGrip = false) {
-  if (noGrip === true) {
-    return false;
-  }
-  return grip && (grip.type === "mapEntry" || grip.type === "storageEntry") && grip.preview;
-}
-
-function createGripMapEntry(key, value) {
-  return {
-    type: "mapEntry",
-    preview: {
-      key,
-      value
-    }
-  };
-}
-
-// Exports from this module
-module.exports = {
-  rep: wrapRender(GripMapEntry),
-  createGripMapEntry,
-  supportsObject
-};
-
-/***/ }),
-
-/***/ 3665:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-const { getValue, nodeHasFullText } = __webpack_require__(3667); /* 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/>. */
-
-async function enumIndexedProperties(objectClient, start, end) {
-  try {
-    const { iterator } = await objectClient.enumProperties({
-      ignoreNonIndexedProperties: true
-    });
-    const response = await iteratorSlice(iterator, start, end);
-    return response;
-  } catch (e) {
-    console.error("Error in enumIndexedProperties", e);
-    return {};
-  }
-}
-
-async function enumNonIndexedProperties(objectClient, start, end) {
-  try {
-    const { iterator } = await objectClient.enumProperties({
-      ignoreIndexedProperties: true
-    });
-    const response = await iteratorSlice(iterator, start, end);
-    return response;
-  } catch (e) {
-    console.error("Error in enumNonIndexedProperties", e);
-    return {};
-  }
-}
-
-async function enumEntries(objectClient, start, end) {
-  try {
-    const { iterator } = await objectClient.enumEntries();
-    const response = await iteratorSlice(iterator, start, end);
-    return response;
-  } catch (e) {
-    console.error("Error in enumEntries", e);
-    return {};
-  }
-}
-
-async function enumSymbols(objectClient, start, end) {
-  try {
-    const { iterator } = await objectClient.enumSymbols();
-    const response = await iteratorSlice(iterator, start, end);
-    return response;
-  } catch (e) {
-    console.error("Error in enumSymbols", e);
-    return {};
-  }
-}
-
-async function getPrototype(objectClient) {
-  if (typeof objectClient.getPrototype !== "function") {
-    console.error("objectClient.getPrototype is not a function");
-    return Promise.resolve({});
-  }
-  return objectClient.getPrototype();
-}
-
-async function getFullText(longStringClient, item) {
-  const { initial, fullText, length } = getValue(item);
-
-  // Return fullText property if it exists so that it can be added to the
-  // loadedProperties map.
-  if (nodeHasFullText(item)) {
-    return Promise.resolve({ fullText });
-  }
-
-  return new Promise((resolve, reject) => {
-    longStringClient.substring(initial.length, length, response => {
-      if (response.error) {
-        console.error("LongStringClient.substring", `${response.error}: ${response.message}`);
-        reject({});
-        return;
-      }
-
-      resolve({
-        fullText: initial + response.substring
-      });
-    });
-  });
-}
-
-function iteratorSlice(iterator, start, end) {
-  start = start || 0;
-  const count = end ? end - start + 1 : iterator.count;
-
-  if (count === 0) {
-    return Promise.resolve({});
-  }
-  return iterator.slice(start, count);
-}
-
-module.exports = {
-  enumEntries,
-  enumIndexedProperties,
-  enumNonIndexedProperties,
-  enumSymbols,
-  getPrototype,
-  getFullText
-};
-
-/***/ }),
-
-/***/ 3666:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-/* 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/>. */
-
-const {
-  enumEntries,
-  enumIndexedProperties,
-  enumNonIndexedProperties,
-  getPrototype,
-  enumSymbols,
-  getFullText
-} = __webpack_require__(3665);
-
-const {
-  getClosestGripNode,
-  getClosestNonBucketNode,
-  getValue,
-  nodeHasAccessors,
-  nodeHasAllEntriesInPreview,
-  nodeHasProperties,
-  nodeIsBucket,
-  nodeIsDefaultProperties,
-  nodeIsEntries,
-  nodeIsMapEntry,
-  nodeIsPrimitive,
-  nodeIsProxy,
-  nodeNeedsNumericalBuckets,
-  nodeIsLongString
-} = __webpack_require__(3667);
-
-function loadItemProperties(item, createObjectClient, createLongStringClient, loadedProperties) {
-  const gripItem = getClosestGripNode(item);
-  const value = getValue(gripItem);
-
-  const [start, end] = item.meta ? [item.meta.startIndex, item.meta.endIndex] : [];
-
-  const promises = [];
-  let objectClient;
-  const getObjectClient = () => objectClient || createObjectClient(value);
-
-  if (shouldLoadItemIndexedProperties(item, loadedProperties)) {
-    promises.push(enumIndexedProperties(getObjectClient(), start, end));
-  }
-
-  if (shouldLoadItemNonIndexedProperties(item, loadedProperties)) {
-    promises.push(enumNonIndexedProperties(getObjectClient(), start, end));
-  }
-
-  if (shouldLoadItemEntries(item, loadedProperties)) {
-    promises.push(enumEntries(getObjectClient(), start, end));
-  }
-
-  if (shouldLoadItemPrototype(item, loadedProperties)) {
-    promises.push(getPrototype(getObjectClient()));
-  }
-
-  if (shouldLoadItemSymbols(item, loadedProperties)) {
-    promises.push(enumSymbols(getObjectClient(), start, end));
-  }
-
-  if (shouldLoadItemFullText(item, loadedProperties)) {
-    promises.push(getFullText(createLongStringClient(value), item));
-  }
-
-  return Promise.all(promises).then(mergeResponses);
-}
-
-function mergeResponses(responses) {
-  const data = {};
-
-  for (const response of responses) {
-    if (response.hasOwnProperty("ownProperties")) {
-      data.ownProperties = _extends({}, data.ownProperties, response.ownProperties);
-    }
-
-    if (response.ownSymbols && response.ownSymbols.length > 0) {
-      data.ownSymbols = response.ownSymbols;
-    }
-
-    if (response.prototype) {
-      data.prototype = response.prototype;
-    }
-
-    if (response.fullText) {
-      data.fullText = response.fullText;
-    }
-  }
-
-  return data;
-}
-
-function shouldLoadItemIndexedProperties(item, loadedProperties = new Map()) {
-  const gripItem = getClosestGripNode(item);
-  const value = getValue(gripItem);
-
-  return value && nodeHasProperties(gripItem) && !loadedProperties.has(item.path) && !nodeIsProxy(item) && !nodeNeedsNumericalBuckets(item) && !nodeIsEntries(getClosestNonBucketNode(item)) &&
-  // The data is loaded when expanding the window node.
-  !nodeIsDefaultProperties(item);
-}
-
-function shouldLoadItemNonIndexedProperties(item, loadedProperties = new Map()) {
-  const gripItem = getClosestGripNode(item);
-  const value = getValue(gripItem);
-
-  return value && nodeHasProperties(gripItem) && !loadedProperties.has(item.path) && !nodeIsProxy(item) && !nodeIsEntries(getClosestNonBucketNode(item)) && !nodeIsBucket(item) &&
-  // The data is loaded when expanding the window node.
-  !nodeIsDefaultProperties(item);
-}
-
-function shouldLoadItemEntries(item, loadedProperties = new Map()) {
-  const gripItem = getClosestGripNode(item);
-  const value = getValue(gripItem);
-
-  return value && nodeIsEntries(getClosestNonBucketNode(item)) && !nodeHasAllEntriesInPreview(gripItem) && !loadedProperties.has(item.path) && !nodeNeedsNumericalBuckets(item);
-}
-
-function shouldLoadItemPrototype(item, loadedProperties = new Map()) {
-  const value = getValue(item);
-
-  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item);
-}
-
-function shouldLoadItemSymbols(item, loadedProperties = new Map()) {
-  const value = getValue(item);
-
-  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
-}
-
-function shouldLoadItemFullText(item, loadedProperties = new Map()) {
-  return !loadedProperties.has(item.path) && nodeIsLongString(item);
-}
-
-module.exports = {
-  loadItemProperties,
-  mergeResponses,
-  shouldLoadItemEntries,
-  shouldLoadItemIndexedProperties,
-  shouldLoadItemNonIndexedProperties,
-  shouldLoadItemPrototype,
-  shouldLoadItemSymbols,
-  shouldLoadItemFullText
-};
-
-/***/ }),
-
-/***/ 3667:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-/* 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/>. */
-
-const { maybeEscapePropertyName } = __webpack_require__(3644);
-const ArrayRep = __webpack_require__(3649);
-const GripArrayRep = __webpack_require__(3661);
-const GripMap = __webpack_require__(3663);
-const GripMapEntryRep = __webpack_require__(3664);
-const ErrorRep = __webpack_require__(3660);
-const { isLongString } = __webpack_require__(3648);
+const { maybeEscapePropertyName } = __webpack_require__(1760);
+const ArrayRep = __webpack_require__(1773);
+const GripArrayRep = __webpack_require__(1794);
+const GripMap = __webpack_require__(1796);
+const GripMapEntryRep = __webpack_require__(1797);
+const ErrorRep = __webpack_require__(1793);
+const { isLongString } = __webpack_require__(1769);
 
 const MAX_NUMERICAL_PROPERTIES = 100;
 
 const NODE_TYPES = {
   BUCKET: Symbol("[n…m]"),
   DEFAULT_PROPERTIES: Symbol("<default properties>"),
   ENTRIES: Symbol("<entries>"),
   GET: Symbol("<get>"),
@@ -2924,16 +1650,32 @@ function getValue(item) {
 
   if (nodeHasAccessors(item)) {
     return item.contents;
   }
 
   return undefined;
 }
 
+function getActor(item, roots) {
+  const isRoot = isNodeRoot(item, roots);
+  const value = getValue(item);
+  return isRoot || !value ? null : value.actor;
+}
+
+function isNodeRoot(item, roots) {
+  const gripItem = getClosestGripNode(item);
+  const value = getValue(gripItem);
+
+  return value && roots.some(root => {
+    const rootValue = getValue(root);
+    return rootValue && rootValue.actor === value.actor;
+  });
+}
+
 function nodeIsBucket(item) {
   return getType(item) === NODE_TYPES.BUCKET;
 }
 
 function nodeIsEntries(item) {
   return getType(item) === NODE_TYPES.ENTRIES;
 }
 
@@ -3591,16 +2333,17 @@ function getClosestNonBucketNode(item) {
     return null;
   }
 
   return getClosestNonBucketNode(parent);
 }
 
 module.exports = {
   createNode,
+  getActor,
   getChildren,
   getClosestGripNode,
   getClosestNonBucketNode,
   getParent,
   getNumericalPropertiesCount,
   getValue,
   makeNodesForEntries,
   makeNodesForPromiseProperties,
@@ -3637,65 +2380,1096 @@ module.exports = {
   nodeSupportsNumericalBucketing,
   setNodeChildren,
   sortProperties,
   NODE_TYPES
 };
 
 /***/ }),
 
-/***/ 3669:
+/***/ 1784:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-var _tree = __webpack_require__(3670);
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+function initialState() {
+  return {
+    expandedPaths: new Set(),
+    loadedProperties: new Map(),
+    actors: new Set()
+  };
+} /* 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/>. */
+
+
+function reducer(state = initialState(), action = {}) {
+  const { type, data } = action;
+
+  const cloneState = overrides => _extends({}, state, overrides);
+
+  if (type === "NODE_EXPAND") {
+    return cloneState({
+      expandedPaths: new Set(state.expandedPaths).add(data.node.path)
+    });
+  }
+
+  if (type === "NODE_COLLAPSE") {
+    const expandedPaths = new Set(state.expandedPaths);
+    expandedPaths.delete(data.node.path);
+    return cloneState({ expandedPaths });
+  }
+
+  if (type === "NODE_PROPERTIES_LOADED") {
+    return cloneState({
+      actors: data.actor ? new Set(state.actors || []).add(data.actor) : state.actors,
+      loadedProperties: new Map(state.loadedProperties).set(data.node.path, action.data.properties)
+    });
+  }
+
+  if (type === "ROOTS_CHANGED") {
+    return cloneState();
+  }
+
+  return state;
+}
+
+function getObjectInspectorState(state) {
+  return state.objectInspector;
+}
+
+function getExpandedPaths(state) {
+  return getObjectInspectorState(state).expandedPaths;
+}
+
+function getExpandedPathKeys(state) {
+  return [...getExpandedPaths(state).keys()];
+}
+
+function getActors(state) {
+  return getObjectInspectorState(state).actors;
+}
+
+function getLoadedProperties(state) {
+  return getObjectInspectorState(state).loadedProperties;
+}
+
+function getLoadedPropertyKeys(state) {
+  return [...getLoadedProperties(state).keys()];
+}
+
+const selectors = {
+  getExpandedPaths,
+  getExpandedPathKeys,
+  getActors,
+  getLoadedProperties,
+  getLoadedPropertyKeys
+};
+
+Object.defineProperty(module.exports, "__esModule", {
+  value: true
+});
+module.exports = selectors;
+module.exports.default = reducer;
+
+/***/ }),
+
+/***/ 1788:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var _tree = __webpack_require__(1798);
 
 var _tree2 = _interopRequireDefault(_tree);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 module.exports = {
   Tree: _tree2.default
 }; /* 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/. */
 
 /***/ }),
 
-/***/ 3670:
+/***/ 1791:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* 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/>. */
+
+// ReactJS
+const PropTypes = __webpack_require__(1758);
+
+// Reps
+const { getGripType, isGrip, cropString, wrapRender } = __webpack_require__(1760);
+const { MODE } = __webpack_require__(1762);
+
+const dom = __webpack_require__(1759);
+const { span } = dom;
+
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+/**
+ * This component represents a template for Function objects.
+ */
+FunctionRep.propTypes = {
+  object: PropTypes.object.isRequired,
+  parameterNames: PropTypes.array,
+  onViewSourceInDebugger: PropTypes.func
+};
+
+function FunctionRep(props) {
+  const { object: grip, onViewSourceInDebugger, recordTelemetryEvent } = props;
+
+  let jumpToDefinitionButton;
+  if (onViewSourceInDebugger && grip.location && grip.location.url && !IGNORED_SOURCE_URLS.includes(grip.location.url)) {
+    jumpToDefinitionButton = dom.button({
+      className: "jump-definition",
+      draggable: false,
+      title: "Jump to definition",
+      onClick: e => {
+        // Stop the event propagation so we don't trigger ObjectInspector
+        // expand/collapse.
+        e.stopPropagation();
+        if (recordTelemetryEvent) {
+          recordTelemetryEvent("jump_to_definition");
+        }
+        onViewSourceInDebugger(grip.location);
+      }
+    });
+  }
+
+  return span({
+    "data-link-actor-id": grip.actor,
+    className: "objectBox objectBox-function",
+    // Set dir="ltr" to prevent function parentheses from
+    // appearing in the wrong direction
+    dir: "ltr"
+  }, getTitle(grip, props), getFunctionName(grip, props), "(", ...renderParams(props), ")", jumpToDefinitionButton);
+}
+
+function getTitle(grip, props) {
+  const { mode } = props;
+
+  if (mode === MODE.TINY && !grip.isGenerator && !grip.isAsync) {
+    return null;
+  }
+
+  let title = mode === MODE.TINY ? "" : "function ";
+
+  if (grip.isGenerator) {
+    title = mode === MODE.TINY ? "* " : "function* ";
+  }
+
+  if (grip.isAsync) {
+    title = `${"async" + " "}${title}`;
+  }
+
+  return span({
+    className: "objectTitle"
+  }, title);
+}
+
+/**
+ * Returns a ReactElement representing the function name.
+ *
+ * @param {Object} grip : Function grip
+ * @param {Object} props: Function rep props
+ */
+function getFunctionName(grip, props = {}) {
+  let { functionName } = props;
+  let name;
+
+  if (functionName) {
+    const end = functionName.length - 1;
+    functionName = functionName.startsWith('"') && functionName.endsWith('"') ? functionName.substring(1, end) : functionName;
+  }
+
+  if (grip.displayName != undefined && functionName != undefined && grip.displayName != functionName) {
+    name = `${functionName}:${grip.displayName}`;
+  } else {
+    name = cleanFunctionName(grip.userDisplayName || grip.displayName || grip.name || props.functionName || "");
+  }
+
+  return cropString(name, 100);
+}
+
+const objectProperty = /([\w\d]+)$/;
+const arrayProperty = /\[(.*?)\]$/;
+const functionProperty = /([\w\d]+)[\/\.<]*?$/;
+const annonymousProperty = /([\w\d]+)\(\^\)$/;
+
+/**
+ * Decodes an anonymous naming scheme that
+ * spider monkey implements based on "Naming Anonymous JavaScript Functions"
+ * http://johnjbarton.github.io/nonymous/index.html
+ *
+ * @param {String} name : Function name to clean up
+ * @returns String
+ */
+function cleanFunctionName(name) {
+  for (const reg of [objectProperty, arrayProperty, functionProperty, annonymousProperty]) {
+    const match = reg.exec(name);
+    if (match) {
+      return match[1];
+    }
+  }
+
+  return name;
+}
+
+function renderParams(props) {
+  const { parameterNames = [] } = props;
+
+  return parameterNames.filter(param => param).reduce((res, param, index, arr) => {
+    res.push(span({ className: "param" }, param));
+    if (index < arr.length - 1) {
+      res.push(span({ className: "delimiter" }, ", "));
+    }
+    return res;
+  }, []);
+}
+
+// Registration
+function supportsObject(grip, noGrip = false) {
+  const type = getGripType(grip, noGrip);
+  if (noGrip === true || !isGrip(grip)) {
+    return type == "function";
+  }
+
+  return type == "Function";
+}
+
+// Exports from this module
+
+module.exports = {
+  rep: wrapRender(FunctionRep),
+  supportsObject,
+  cleanFunctionName,
+  // exported for testing purpose.
+  getFunctionName
+};
+
+/***/ }),
+
+/***/ 1792:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* 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/>. */
+
+module.exports = {
+  ELEMENT_NODE: 1,
+  ATTRIBUTE_NODE: 2,
+  TEXT_NODE: 3,
+  CDATA_SECTION_NODE: 4,
+  ENTITY_REFERENCE_NODE: 5,
+  ENTITY_NODE: 6,
+  PROCESSING_INSTRUCTION_NODE: 7,
+  COMMENT_NODE: 8,
+  DOCUMENT_NODE: 9,
+  DOCUMENT_TYPE_NODE: 10,
+  DOCUMENT_FRAGMENT_NODE: 11,
+  NOTATION_NODE: 12,
+
+  // DocumentPosition
+  DOCUMENT_POSITION_DISCONNECTED: 0x01,
+  DOCUMENT_POSITION_PRECEDING: 0x02,
+  DOCUMENT_POSITION_FOLLOWING: 0x04,
+  DOCUMENT_POSITION_CONTAINS: 0x08,
+  DOCUMENT_POSITION_CONTAINED_BY: 0x10,
+  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 0x20
+};
+
+/***/ }),
+
+/***/ 1793:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* 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/>. */
+
+// ReactJS
+const PropTypes = __webpack_require__(1758);
+// Utils
+const { getGripType, isGrip, wrapRender } = __webpack_require__(1760);
+const { cleanFunctionName } = __webpack_require__(1791);
+const { isLongString } = __webpack_require__(1769);
+const { MODE } = __webpack_require__(1762);
+
+const dom = __webpack_require__(1759);
+const { span } = dom;
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+/**
+ * Renders Error objects.
+ */
+ErrorRep.propTypes = {
+  object: PropTypes.object.isRequired,
+  // @TODO Change this to Object.values when supported in Node's version of V8
+  mode: PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key]))
+};
+
+function ErrorRep(props) {
+  const object = props.object;
+  const preview = object.preview;
+
+  let name;
+  if (preview && preview.name && preview.kind) {
+    switch (preview.kind) {
+      case "Error":
+        name = preview.name;
+        break;
+      case "DOMException":
+        name = preview.kind;
+        break;
+      default:
+        throw new Error("Unknown preview kind for the Error rep.");
+    }
+  } else {
+    name = "Error";
+  }
+
+  const content = [];
+
+  if (props.mode === MODE.TINY) {
+    content.push(name);
+  } else {
+    content.push(`${name}: "${preview.message}"`);
+  }
+
+  if (preview.stack && props.mode !== MODE.TINY) {
+    content.push("\n", getStacktraceElements(props, preview));
+  }
+
+  return span({
+    "data-link-actor-id": object.actor,
+    className: "objectBox-stackTrace"
+  }, content);
+}
+
+/**
+ * Returns a React element reprensenting the Error stacktrace, i.e.
+ * transform error.stack from:
+ *
+ * semicolon@debugger eval code:1:109
+ * jkl@debugger eval code:1:63
+ * asdf@debugger eval code:1:28
+ * @debugger eval code:1:227
+ *
+ * Into a column layout:
+ *
+ * semicolon  (<anonymous>:8:10)
+ * jkl        (<anonymous>:5:10)
+ * asdf       (<anonymous>:2:10)
+ *            (<anonymous>:11:1)
+ */
+function getStacktraceElements(props, preview) {
+  const stack = [];
+  if (!preview.stack) {
+    return stack;
+  }
+
+  const isStacktraceALongString = isLongString(preview.stack);
+  const stackString = isStacktraceALongString ? preview.stack.initial : preview.stack;
+
+  stackString.split("\n").forEach((frame, index, frames) => {
+    if (!frame) {
+      // Skip any blank lines
+      return;
+    }
+
+    // If the stacktrace is a longString, don't include the last frame in the
+    // array, since it is certainly incomplete.
+    // Can be removed when https://bugzilla.mozilla.org/show_bug.cgi?id=1448833
+    // is fixed.
+    if (isStacktraceALongString && index === frames.length - 1) {
+      return;
+    }
+
+    let functionName;
+    let location;
+
+    // Given the input: "functionName@scriptLocation:2:100"
+    // Result: [
+    //   "functionName@scriptLocation:2:100",
+    //   "functionName",
+    //   "scriptLocation:2:100"
+    // ]
+    const result = frame.match(/^(.*)@(.*)$/);
+    if (result && result.length === 3) {
+      functionName = result[1];
+
+      // If the resource was loaded by base-loader.js, the location looks like:
+      // resource://devtools/shared/base-loader.js -> resource://path/to/file.js .
+      // What's needed is only the last part after " -> ".
+      location = result[2].split(" -> ").pop();
+    }
+
+    if (!functionName) {
+      functionName = "<anonymous>";
+    }
+
+    let onLocationClick;
+    // Given the input: "scriptLocation:2:100"
+    // Result:
+    // ["scriptLocation:2:100", "scriptLocation", "2", "100"]
+    const locationParts = location.match(/^(.*):(\d+):(\d+)$/);
+
+    if (props.onViewSourceInDebugger && location && locationParts && !IGNORED_SOURCE_URLS.includes(locationParts[1])) {
+      const [, url, line, column] = locationParts;
+      onLocationClick = e => {
+        // Don't trigger ObjectInspector expand/collapse.
+        e.stopPropagation();
+        props.onViewSourceInDebugger({
+          url,
+          line: Number(line),
+          column: Number(column)
+        });
+      };
+    }
+
+    stack.push("\t", span({
+      key: `fn${index}`,
+      className: "objectBox-stackTrace-fn"
+    }, cleanFunctionName(functionName)), " ", span({
+      key: `location${index}`,
+      className: "objectBox-stackTrace-location",
+      onClick: onLocationClick,
+      title: onLocationClick ? `View source in debugger → ${location}` : undefined
+    }, location), "\n");
+  });
+
+  return span({
+    key: "stack",
+    className: "objectBox-stackTrace-grid"
+  }, stack);
+}
+
+// Registration
+function supportsObject(object, noGrip = false) {
+  if (noGrip === true || !isGrip(object)) {
+    return false;
+  }
+  return object.preview && getGripType(object, noGrip) === "Error" || object.class === "DOMException";
+}
+
+// Exports from this module
+module.exports = {
+  rep: wrapRender(ErrorRep),
+  supportsObject
+};
+
+/***/ }),
+
+/***/ 1794:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+/* 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/>. */
+
+// Dependencies
+const PropTypes = __webpack_require__(1758);
+
+const { lengthBubble } = __webpack_require__(1795);
+const {
+  interleave,
+  getGripType,
+  isGrip,
+  wrapRender,
+  ellipsisElement
+} = __webpack_require__(1760);
+const { MODE } = __webpack_require__(1762);
+
+const dom = __webpack_require__(1759);
+const { span } = dom;
+const { ModePropType } = __webpack_require__(1773);
+const DEFAULT_TITLE = "Array";
+
+/**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+GripArray.propTypes = {
+  object: PropTypes.object.isRequired,
+  // @TODO Change this to Object.values when supported in Node's version of V8
+  mode: ModePropType,
+  provider: PropTypes.object,
+  onDOMNodeMouseOver: PropTypes.func,
+  onDOMNodeMouseOut: PropTypes.func,
+  onInspectIconClick: PropTypes.func
+};
+
+function GripArray(props) {
+  const { object, mode = MODE.SHORT } = props;
+
+  let brackets;
+  const needSpace = function (space) {
+    return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
+  };
+
+  const config = {
+    "data-link-actor-id": object.actor,
+    className: "objectBox objectBox-array"
+  };
+
+  const title = getTitle(props, object);
+
+  if (mode === MODE.TINY) {
+    const isEmpty = getLength(object) === 0;
+
+    // Omit bracketed ellipsis for non-empty non-Array arraylikes (f.e: Sets).
+    if (!isEmpty && object.class !== "Array") {
+      return span(config, title);
+    }
+
+    brackets = needSpace(false);
+    return span(config, title, span({
+      className: "arrayLeftBracket"
+    }, brackets.left), isEmpty ? null : ellipsisElement, span({
+      className: "arrayRightBracket"
+    }, brackets.right));
+  }
+
+  const max = maxLengthMap.get(mode);
+  const items = arrayIterator(props, object, max);
+  brackets = needSpace(items.length > 0);
+
+  return span({
+    "data-link-actor-id": object.actor,
+    className: "objectBox objectBox-array"
+  }, title, span({
+    className: "arrayLeftBracket"
+  }, brackets.left), ...interleave(items, ", "), span({
+    className: "arrayRightBracket"
+  }, brackets.right), span({
+    className: "arrayProperties",
+    role: "group"
+  }));
+}
+
+function getLength(grip) {
+  if (!grip.preview) {
+    return 0;
+  }
+
+  return grip.preview.length || grip.preview.childNodesLength || 0;
+}
+
+function getTitle(props, object) {
+  const objectLength = getLength(object);
+  const isEmpty = objectLength === 0;
+
+  let title = props.title || object.class || DEFAULT_TITLE;
+
+  const length = lengthBubble({
+    object,
+    mode: props.mode,
+    maxLengthMap,
+    getLength
+  });
+
+  if (props.mode === MODE.TINY) {
+    if (isEmpty) {
+      if (object.class === DEFAULT_TITLE) {
+        return null;
+      }
+
+      return span({ className: "objectTitle" }, `${title} `);
+    }
+
+    let trailingSpace;
+    if (object.class === DEFAULT_TITLE) {
+      title = null;
+      trailingSpace = " ";
+    }
+
+    return span({ className: "objectTitle" }, title, length, trailingSpace);
+  }
+
+  return span({ className: "objectTitle" }, title, length, " ");
+}
+
+function getPreviewItems(grip) {
+  if (!grip.preview) {
+    return null;
+  }
+
+  return grip.preview.items || grip.preview.childNodes || [];
+}
+
+function arrayIterator(props, grip, max) {
+  const { Rep } = __webpack_require__(1768);
+
+  let items = [];
+  const gripLength = getLength(grip);
+
+  if (!gripLength) {
+    return items;
+  }
+
+  const previewItems = getPreviewItems(grip);
+  const provider = props.provider;
+
+  let emptySlots = 0;
+  let foldedEmptySlots = 0;
+  items = previewItems.reduce((res, itemGrip) => {
+    if (res.length >= max) {
+      return res;
+    }
+
+    let object;
+    try {
+      if (!provider && itemGrip === null) {
+        emptySlots++;
+        return res;
+      }
+
+      object = provider ? provider.getValue(itemGrip) : itemGrip;
+    } catch (exc) {
+      object = exc;
+    }
+
+    if (emptySlots > 0) {
+      res.push(getEmptySlotsElement(emptySlots));
+      foldedEmptySlots = foldedEmptySlots + emptySlots - 1;
+      emptySlots = 0;
+    }
+
+    if (res.length < max) {
+      res.push(Rep(_extends({}, props, {
+        object,
+        mode: MODE.TINY,
+        // Do not propagate title to array items reps
+        title: undefined
+      })));
+    }
+
+    return res;
+  }, []);
+
+  // Handle trailing empty slots if there are some.
+  if (items.length < max && emptySlots > 0) {
+    items.push(getEmptySlotsElement(emptySlots));
+    foldedEmptySlots = foldedEmptySlots + emptySlots - 1;
+  }
+
+  const itemsShown = items.length + foldedEmptySlots;
+  if (gripLength > itemsShown) {
+    items.push(ellipsisElement);
+  }
+
+  return items;
+}
+
+function getEmptySlotsElement(number) {
+  // TODO: Use l10N - See https://github.com/devtools-html/reps/issues/141
+  return `<${number} empty slot${number > 1 ? "s" : ""}>`;
+}
+
+function supportsObject(grip, noGrip = false) {
+  if (noGrip === true || !isGrip(grip)) {
+    return false;
+  }
+
+  return grip.preview && (grip.preview.kind == "ArrayLike" || getGripType(grip, noGrip) === "DocumentFragment");
+}
+
+const maxLengthMap = new Map();
+maxLengthMap.set(MODE.SHORT, 3);
+maxLengthMap.set(MODE.LONG, 10);
+
+// Exports from this module
+module.exports = {
+  rep: wrapRender(GripArray),
+  supportsObject,
+  maxLengthMap,
+  getLength
+};
+
+/***/ }),
+
+/***/ 1795:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* 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/>. */
+
+const PropTypes = __webpack_require__(1758);
+
+const { wrapRender } = __webpack_require__(1760);
+const { MODE } = __webpack_require__(1762);
+const { ModePropType } = __webpack_require__(1773);
+
+const dom = __webpack_require__(1759);
+const { span } = dom;
+
+GripLengthBubble.propTypes = {
+  object: PropTypes.object.isRequired,
+  maxLengthMap: PropTypes.instanceOf(Map).isRequired,
+  getLength: PropTypes.func.isRequired,
+  mode: ModePropType,
+  visibilityThreshold: PropTypes.number
+};
+
+function GripLengthBubble(props) {
+  const {
+    object,
+    mode = MODE.SHORT,
+    visibilityThreshold = 2,
+    maxLengthMap,
+    getLength,
+    showZeroLength = false
+  } = props;
+
+  const length = getLength(object);
+  const isEmpty = length === 0;
+  const isObvious = [MODE.SHORT, MODE.LONG].includes(mode) && length > 0 && length <= maxLengthMap.get(mode) && length <= visibilityThreshold;
+  if (isEmpty && !showZeroLength || isObvious) {
+    return "";
+  }
+
+  return span({
+    className: "objectLengthBubble"
+  }, `(${length})`);
+}
+
+module.exports = {
+  lengthBubble: wrapRender(GripLengthBubble)
+};
+
+/***/ }),
+
+/***/ 1796:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* 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/>. */
+
+// Dependencies
+
+const { lengthBubble } = __webpack_require__(1795);
+const PropTypes = __webpack_require__(1758);
+const {
+  interleave,
+  isGrip,
+  wrapRender,
+  ellipsisElement
+} = __webpack_require__(1760);
+const PropRep = __webpack_require__(1774);
+const { MODE } = __webpack_require__(1762);
+const { ModePropType } = __webpack_require__(1773);
+
+const { span } = __webpack_require__(1759);
+
+/**
+ * Renders an map. A map is represented by a list of its
+ * entries enclosed in curly brackets.
+ */
+GripMap.propTypes = {
+  object: PropTypes.object,
+  // @TODO Change this to Object.values when supported in Node's version of V8
+  mode: ModePropType,
+  isInterestingEntry: PropTypes.func,
+  onDOMNodeMouseOver: PropTypes.func,
+  onDOMNodeMouseOut: PropTypes.func,
+  onInspectIconClick: PropTypes.func,
+  title: PropTypes.string
+};
+
+function GripMap(props) {
+  const { mode, object } = props;
+
+  const config = {
+    "data-link-actor-id": object.actor,
+    className: "objectBox objectBox-object"
+  };
+
+  const title = getTitle(props, object);
+  const isEmpty = getLength(object) === 0;
+
+  if (isEmpty || mode === MODE.TINY) {
+    return span(config, title);
+  }
+
+  const propsArray = safeEntriesIterator(props, object, maxLengthMap.get(mode));
+
+  return span(config, title, span({
+    className: "objectLeftBrace"
+  }, " { "), ...interleave(propsArray, ", "), span({
+    className: "objectRightBrace"
+  }, " }"));
+}
+
+function getTitle(props, object) {
+  const title = props.title || (object && object.class ? object.class : "Map");
+  return span({
+    className: "objectTitle"
+  }, title, lengthBubble({
+    object,
+    mode: props.mode,
+    maxLengthMap,
+    getLength,
+    showZeroLength: true
+  }));
+}
+
+function safeEntriesIterator(props, object, max) {
+  max = typeof max === "undefined" ? 3 : max;
+  try {
+    return entriesIterator(props, object, max);
+  } catch (err) {
+    console.error(err);
+  }
+  return [];
+}
+
+function entriesIterator(props, object, max) {
+  // Entry filter. Show only interesting entries to the user.
+  const isInterestingEntry = props.isInterestingEntry || ((type, value) => {
+    return type == "boolean" || type == "number" || type == "string" && value.length != 0;
+  });
+
+  const mapEntries = object.preview && object.preview.entries ? object.preview.entries : [];
+
+  let indexes = getEntriesIndexes(mapEntries, max, isInterestingEntry);
+  if (indexes.length < max && indexes.length < mapEntries.length) {
+    // There are not enough entries yet, so we add uninteresting entries.
+    indexes = indexes.concat(getEntriesIndexes(mapEntries, max - indexes.length, (t, value, name) => {
+      return !isInterestingEntry(t, value, name);
+    }));
+  }
+
+  const entries = getEntries(props, mapEntries, indexes);
+  if (entries.length < getLength(object)) {
+    // There are some undisplayed entries. Then display "…".
+    entries.push(ellipsisElement);
+  }
+
+  return entries;
+}
+
+/**
+ * Get entries ordered by index.
+ *
+ * @param {Object} props Component props.
+ * @param {Array} entries Entries array.
+ * @param {Array} indexes Indexes of entries.
+ * @return {Array} Array of PropRep.
+ */
+function getEntries(props, entries, indexes) {
+  const { onDOMNodeMouseOver, onDOMNodeMouseOut, onInspectIconClick } = props;
+
+  // Make indexes ordered by ascending.
+  indexes.sort(function (a, b) {
+    return a - b;
+  });
+
+  return indexes.map((index, i) => {
+    const [key, entryValue] = entries[index];
+    const value = entryValue.value !== undefined ? entryValue.value : entryValue;
+
+    return PropRep({
+      name: key,
+      equal: " \u2192 ",
+      object: value,
+      mode: MODE.TINY,
+      onDOMNodeMouseOver,
+      onDOMNodeMouseOut,
+      onInspectIconClick
+    });
+  });
+}
+
+/**
+ * Get the indexes of entries in the map.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the entry you want.
+ * @return {Array} Indexes of filtered entries in the map.
+ */
+function getEntriesIndexes(entries, max, filter) {
+  return entries.reduce((indexes, [key, entry], i) => {
+    if (indexes.length < max) {
+      const value = entry && entry.value !== undefined ? entry.value : entry;
+      // Type is specified in grip's "class" field and for primitive
+      // values use typeof.
+      const type = (value && value.class ? value.class : typeof value).toLowerCase();
+
+      if (filter(type, value, key)) {
+        indexes.push(i);
+      }
+    }
+
+    return indexes;
+  }, []);
+}
+
+function getLength(grip) {
+  return grip.preview.size || 0;
+}
+
+function supportsObject(grip, noGrip = false) {
+  if (noGrip === true || !isGrip(grip)) {
+    return false;
+  }
+  return grip.preview && grip.preview.kind == "MapLike";
+}
+
+const maxLengthMap = new Map();
+maxLengthMap.set(MODE.SHORT, 3);
+maxLengthMap.set(MODE.LONG, 10);
+
+// Exports from this module
+module.exports = {
+  rep: wrapRender(GripMap),
+  supportsObject,
+  maxLengthMap,
+  getLength
+};
+
+/***/ }),
+
+/***/ 1797:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+/* 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/>. */
+
+// Dependencies
+const PropTypes = __webpack_require__(1758);
+// Shortcuts
+const dom = __webpack_require__(1759);
+const { span } = dom;
+const { wrapRender } = __webpack_require__(1760);
+const PropRep = __webpack_require__(1774);
+const { MODE } = __webpack_require__(1762);
+/**
+ * Renders an map entry. A map entry is represented by its key,
+ * a column and its value.
+ */
+GripMapEntry.propTypes = {
+  object: PropTypes.object,
+  // @TODO Change this to Object.values when supported in Node's version of V8
+  mode: PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+  onDOMNodeMouseOver: PropTypes.func,
+  onDOMNodeMouseOut: PropTypes.func,
+  onInspectIconClick: PropTypes.func
+};
+
+function GripMapEntry(props) {
+  const { object } = props;
+
+  const { key, value } = object.preview;
+
+  return span({
+    className: "objectBox objectBox-map-entry"
+  }, PropRep(_extends({}, props, {
+    name: key,
+    object: value,
+    equal: " \u2192 ",
+    title: null,
+    suppressQuotes: false
+  })));
+}
+
+function supportsObject(grip, noGrip = false) {
+  if (noGrip === true) {
+    return false;
+  }
+  return grip && (grip.type === "mapEntry" || grip.type === "storageEntry") && grip.preview;
+}
+
+function createGripMapEntry(key, value) {
+  return {
+    type: "mapEntry",
+    preview: {
+      key,
+      value
+    }
+  };
+}
+
+// Exports from this module
+module.exports = {
+  rep: wrapRender(GripMapEntry),
+  createGripMapEntry,
+  supportsObject
+};
+
+/***/ }),
+
+/***/ 1798:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
-var _reactDomFactories = __webpack_require__(3643);
+var _reactDomFactories = __webpack_require__(1759);
 
 var _reactDomFactories2 = _interopRequireDefault(_reactDomFactories);
 
-var _propTypes = __webpack_require__(3642);
+var _propTypes = __webpack_require__(1758);
 
 var _propTypes2 = _interopRequireDefault(_propTypes);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 const { Component, createFactory } = _react2.default; /* 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/>. */
 
-__webpack_require__(3671);
+__webpack_require__(1799);
 
 // depth
 const AUTO_EXPAND_DEPTH = 0;
 
 /**
  * An arrow that displays whether its node is expanded (▼) or collapsed
  * (▶). When its node has no children, it is hidden.
  */
@@ -4483,44 +4257,361 @@ class Tree extends Component {
     }, nodes);
   }
 }
 
 exports.default = Tree;
 
 /***/ }),
 
-/***/ 3671:
+/***/ 1799:
 /***/ (function(module, exports) {
 
 // removed by extract-text-webpack-plugin
 
 /***/ }),
 
-/***/ 3672:
+/***/ 1800:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+/* 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/>. */
+
+const {
+  enumEntries,
+  enumIndexedProperties,
+  enumNonIndexedProperties,
+  getPrototype,
+  enumSymbols,
+  getFullText
+} = __webpack_require__(1801);
+
+const {
+  getClosestGripNode,
+  getClosestNonBucketNode,
+  getValue,
+  nodeHasAccessors,
+  nodeHasAllEntriesInPreview,
+  nodeHasProperties,
+  nodeIsBucket,
+  nodeIsDefaultProperties,
+  nodeIsEntries,
+  nodeIsMapEntry,
+  nodeIsPrimitive,
+  nodeIsProxy,
+  nodeNeedsNumericalBuckets,
+  nodeIsLongString
+} = __webpack_require__(1783);
+
+function loadItemProperties(item, createObjectClient, createLongStringClient, loadedProperties) {
+  const gripItem = getClosestGripNode(item);
+  const value = getValue(gripItem);
+
+  const [start, end] = item.meta ? [item.meta.startIndex, item.meta.endIndex] : [];
+
+  const promises = [];
+  let objectClient;
+  const getObjectClient = () => objectClient || createObjectClient(value);
+
+  if (shouldLoadItemIndexedProperties(item, loadedProperties)) {
+    promises.push(enumIndexedProperties(getObjectClient(), start, end));
+  }
+
+  if (shouldLoadItemNonIndexedProperties(item, loadedProperties)) {
+    promises.push(enumNonIndexedProperties(getObjectClient(), start, end));
+  }
+
+  if (shouldLoadItemEntries(item, loadedProperties)) {
+    promises.push(enumEntries(getObjectClient(), start, end));
+  }
+
+  if (shouldLoadItemPrototype(item, loadedProperties)) {
+    promises.push(getPrototype(getObjectClient()));
+  }
+
+  if (shouldLoadItemSymbols(item, loadedProperties)) {
+    promises.push(enumSymbols(getObjectClient(), start, end));
+  }
+
+  if (shouldLoadItemFullText(item, loadedProperties)) {
+    promises.push(getFullText(createLongStringClient(value), item));
+  }
+
+  return Promise.all(promises).then(mergeResponses);
+}
+
+function mergeResponses(responses) {
+  const data = {};
+
+  for (const response of responses) {
+    if (response.hasOwnProperty("ownProperties")) {
+      data.ownProperties = _extends({}, data.ownProperties, response.ownProperties);
+    }
+
+    if (response.ownSymbols && response.ownSymbols.length > 0) {
+      data.ownSymbols = response.ownSymbols;
+    }
+
+    if (response.prototype) {
+      data.prototype = response.prototype;
+    }
+
+    if (response.fullText) {
+      data.fullText = response.fullText;
+    }
+  }
+
+  return data;
+}
+
+function shouldLoadItemIndexedProperties(item, loadedProperties = new Map()) {
+  const gripItem = getClosestGripNode(item);
+  const value = getValue(gripItem);
+
+  return value && nodeHasProperties(gripItem) && !loadedProperties.has(item.path) && !nodeIsProxy(item) && !nodeNeedsNumericalBuckets(item) && !nodeIsEntries(getClosestNonBucketNode(item)) &&
+  // The data is loaded when expanding the window node.
+  !nodeIsDefaultProperties(item);
+}
+
+function shouldLoadItemNonIndexedProperties(item, loadedProperties = new Map()) {
+  const gripItem = getClosestGripNode(item);
+  const value = getValue(gripItem);
+
+  return value && nodeHasProperties(gripItem) && !loadedProperties.has(item.path) && !nodeIsProxy(item) && !nodeIsEntries(getClosestNonBucketNode(item)) && !nodeIsBucket(item) &&
+  // The data is loaded when expanding the window node.
+  !nodeIsDefaultProperties(item);
+}
+
+function shouldLoadItemEntries(item, loadedProperties = new Map()) {
+  const gripItem = getClosestGripNode(item);
+  const value = getValue(gripItem);
+
+  return value && nodeIsEntries(getClosestNonBucketNode(item)) && !nodeHasAllEntriesInPreview(gripItem) && !loadedProperties.has(item.path) && !nodeNeedsNumericalBuckets(item);
+}
+
+function shouldLoadItemPrototype(item, loadedProperties = new Map()) {
+  const value = getValue(item);
+
+  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item);
+}
+
+function shouldLoadItemSymbols(item, loadedProperties = new Map()) {
+  const value = getValue(item);
+
+  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
+}
+
+function shouldLoadItemFullText(item, loadedProperties = new Map()) {
+  return !loadedProperties.has(item.path) && nodeIsLongString(item);
+}
+
+module.exports = {
+  loadItemProperties,
+  mergeResponses,
+  shouldLoadItemEntries,
+  shouldLoadItemIndexedProperties,
+  shouldLoadItemNonIndexedProperties,
+  shouldLoadItemPrototype,
+  shouldLoadItemSymbols,
+  shouldLoadItemFullText
+};
+
+/***/ }),
+
+/***/ 1801:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const { getValue, nodeHasFullText } = __webpack_require__(1783); /* 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/>. */
+
+async function enumIndexedProperties(objectClient, start, end) {
+  try {
+    const { iterator } = await objectClient.enumProperties({
+      ignoreNonIndexedProperties: true
+    });
+    const response = await iteratorSlice(iterator, start, end);
+    return response;
+  } catch (e) {
+    console.error("Error in enumIndexedProperties", e);
+    return {};
+  }
+}
+
+async function enumNonIndexedProperties(objectClient, start, end) {
+  try {
+    const { iterator } = await objectClient.enumProperties({
+      ignoreIndexedProperties: true
+    });
+    const response = await iteratorSlice(iterator, start, end);
+    return response;
+  } catch (e) {
+    console.error("Error in enumNonIndexedProperties", e);
+    return {};
+  }
+}
+
+async function enumEntries(objectClient, start, end) {
+  try {
+    const { iterator } = await objectClient.enumEntries();
+    const response = await iteratorSlice(iterator, start, end);
+    return response;
+  } catch (e) {
+    console.error("Error in enumEntries", e);
+    return {};
+  }
+}
+
+async function enumSymbols(objectClient, start, end) {
+  try {
+    const { iterator } = await objectClient.enumSymbols();
+    const response = await iteratorSlice(iterator, start, end);
+    return response;
+  } catch (e) {
+    console.error("Error in enumSymbols", e);
+    return {};
+  }
+}
+
+async function getPrototype(objectClient) {
+  if (typeof objectClient.getPrototype !== "function") {
+    console.error("objectClient.getPrototype is not a function");
+    return Promise.resolve({});
+  }
+  return objectClient.getPrototype();
+}
+
+async function getFullText(longStringClient, item) {
+  const { initial, fullText, length } = getValue(item);
+
+  // Return fullText property if it exists so that it can be added to the
+  // loadedProperties map.
+  if (nodeHasFullText(item)) {
+    return Promise.resolve({ fullText });
+  }
+
+  return new Promise((resolve, reject) => {
+    longStringClient.substring(initial.length, length, response => {
+      if (response.error) {
+        console.error("LongStringClient.substring", `${response.error}: ${response.message}`);
+        reject({});
+        return;
+      }
+
+      resolve({
+        fullText: initial + response.substring
+      });
+    });
+  });
+}
+
+function iteratorSlice(iterator, start, end) {
+  start = start || 0;
+  const count = end ? end - start + 1 : iterator.count;
+
+  if (count === 0) {
+    return Promise.resolve({});
+  }
+  return iterator.slice(start, count);
+}
+
+module.exports = {
+  enumEntries,
+  enumIndexedProperties,
+  enumNonIndexedProperties,
+  enumSymbols,
+  getPrototype,
+  getFullText
+};
+
+/***/ }),
+
+/***/ 1802:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+/* 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/>. */
+
+const client = __webpack_require__(1801);
+const loadProperties = __webpack_require__(1800);
+const node = __webpack_require__(1783);
+const { nodeIsError, nodeIsPrimitive } = node;
+const selection = __webpack_require__(1852);
+
+const { MODE } = __webpack_require__(1762);
+const {
+  REPS: { Rep, Grip }
+} = __webpack_require__(1768);
+
+
+function shouldRenderRootsInReps(roots) {
+  if (roots.length > 1) {
+    return false;
+  }
+
+  const root = roots[0];
+  const name = root && root.name;
+  return (name === null || typeof name === "undefined") && (nodeIsPrimitive(root) || nodeIsError(root));
+}
+
+function renderRep(item, props) {
+  return Rep(_extends({}, props, {
+    object: node.getValue(item),
+    mode: props.mode || MODE.TINY,
+    defaultRep: Grip
+  }));
+}
+
+module.exports = {
+  client,
+  loadProperties,
+  node,
+  renderRep,
+  selection,
+  shouldRenderRootsInReps
+};
+
+/***/ }),
+
+/***/ 1824:
 /***/ (function(module, exports) {
 
 // removed by extract-text-webpack-plugin
 
 /***/ }),
 
-/***/ 3673:
+/***/ 1825:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
-const { getGripType, wrapRender } = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+const { getGripType, wrapRender } = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders undefined value
  */
 const Undefined = function () {
   return span({ className: "objectBox objectBox-undefined" }, "undefined");
 };
@@ -4537,29 +4628,29 @@ function supportsObject(object, noGrip =
 
 module.exports = {
   rep: wrapRender(Undefined),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3674:
+/***/ 1826:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
-const { wrapRender } = __webpack_require__(3644);
-const dom = __webpack_require__(3643);
+const { wrapRender } = __webpack_require__(1760);
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders null value
  */
 function Null(props) {
   return span({ className: "objectBox objectBox-null" }, "null");
 }
@@ -4580,32 +4671,32 @@ function supportsObject(object, noGrip =
 
 module.exports = {
   rep: wrapRender(Null),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3675:
+/***/ 1827:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
-const PropTypes = __webpack_require__(3642);
-
-const { getGripType, wrapRender } = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+const PropTypes = __webpack_require__(1758);
+
+const { getGripType, wrapRender } = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders a number
  */
 Number.propTypes = {
   object: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.bool]).isRequired
 };
@@ -4630,35 +4721,35 @@ function supportsObject(object, noGrip =
 
 module.exports = {
   rep: wrapRender(Number),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3676:
+/***/ 1828:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
 
 /* 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/>. */
 
 // Dependencies
-const PropTypes = __webpack_require__(3642);
-const { wrapRender, ellipsisElement } = __webpack_require__(3644);
-const PropRep = __webpack_require__(3650);
-const { MODE } = __webpack_require__(3645);
-
-const dom = __webpack_require__(3643);
+const PropTypes = __webpack_require__(1758);
+const { wrapRender, ellipsisElement } = __webpack_require__(1760);
+const PropRep = __webpack_require__(1774);
+const { MODE } = __webpack_require__(1762);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 const DEFAULT_TITLE = "Object";
 
 /**
  * Renders an object. An object is represented by a list of its
  * properties enclosed in curly brackets.
  */
@@ -4802,32 +4893,32 @@ function supportsObject(object, noGrip =
 // Exports from this module
 module.exports = {
   rep: wrapRender(ObjectRep),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3677:
+/***/ 1829:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
-const PropTypes = __webpack_require__(3642);
-
-const { getGripType, wrapRender } = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+const PropTypes = __webpack_require__(1758);
+
+const { getGripType, wrapRender } = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders a symbol.
  */
 SymbolRep.propTypes = {
   object: PropTypes.object.isRequired
 };
@@ -4849,32 +4940,32 @@ function supportsObject(object, noGrip =
 // Exports from this module
 module.exports = {
   rep: wrapRender(SymbolRep),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3678:
+/***/ 1830:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
-const PropTypes = __webpack_require__(3642);
-
-const { getGripType, wrapRender } = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+const PropTypes = __webpack_require__(1758);
+
+const { getGripType, wrapRender } = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders a Infinity object
  */
 InfinityRep.propTypes = {
   object: PropTypes.object.isRequired
 };
@@ -4893,30 +4984,30 @@ function supportsObject(object, noGrip =
 // Exports from this module
 module.exports = {
   rep: wrapRender(InfinityRep),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3679:
+/***/ 1831:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
-const { getGripType, wrapRender } = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+const { getGripType, wrapRender } = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders a NaN object
  */
 function NaNRep(props) {
   return span({ className: "objectBox objectBox-nan" }, "NaN");
 }
@@ -4928,31 +5019,31 @@ function supportsObject(object, noGrip =
 // Exports from this module
 module.exports = {
   rep: wrapRender(NaNRep),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3680:
+/***/ 1832:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
-const dom = __webpack_require__(3643);
-const PropTypes = __webpack_require__(3642);
-const { wrapRender } = __webpack_require__(3644);
-const { MODE } = __webpack_require__(3645);
+const dom = __webpack_require__(1759);
+const PropTypes = __webpack_require__(1758);
+const { wrapRender } = __webpack_require__(1760);
+const { MODE } = __webpack_require__(1762);
 const { span } = dom;
 
 /**
  * Renders an object. An object is represented by a list of its
  * properties enclosed in curly brackets.
  */
 Accessor.propTypes = {
   object: PropTypes.object.isRequired,
@@ -4993,34 +5084,156 @@ function supportsObject(object, noGrip =
 // Exports from this module
 module.exports = {
   rep: wrapRender(Accessor),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3681:
+/***/ 1833:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // ReactJS
-const PropTypes = __webpack_require__(3642);
-const dom = __webpack_require__(3643);
+const PropTypes = __webpack_require__(1758);
+const { button, span } = __webpack_require__(1759);
+
+// Utils
+const { isGrip, wrapRender } = __webpack_require__(1760);
+const { rep: StringRep } = __webpack_require__(1769);
+
+/**
+ * Renders Accessible object.
+ */
+Accessible.propTypes = {
+  object: PropTypes.object.isRequired,
+  inspectIconTitle: PropTypes.string,
+  nameMaxLength: PropTypes.number,
+  onAccessibleClick: PropTypes.func,
+  onAccessibleMouseOver: PropTypes.func,
+  onAccessibleMouseOut: PropTypes.func,
+  onInspectIconClick: PropTypes.func,
+  separatorText: PropTypes.string
+};
+
+function Accessible(props) {
+  const {
+    object,
+    inspectIconTitle,
+    nameMaxLength,
+    onAccessibleClick,
+    onAccessibleMouseOver,
+    onAccessibleMouseOut,
+    onInspectIconClick,
+    separatorText
+  } = props;
+  const elements = getElements(object, nameMaxLength, separatorText);
+  const isInTree = object.preview && object.preview.isConnected === true;
+  const baseConfig = {
+    "data-link-actor-id": object.actor,
+    className: "objectBox objectBox-accessible"
+  };
+
+  let inspectIcon;
+  if (isInTree) {
+    if (onAccessibleClick) {
+      Object.assign(baseConfig, {
+        onClick: _ => onAccessibleClick(object),
+        className: `${baseConfig.className} clickable`
+      });
+    }
+
+    if (onAccessibleMouseOver) {
+      Object.assign(baseConfig, {
+        onMouseOver: _ => onAccessibleMouseOver(object)
+      });
+    }
+
+    if (onAccessibleMouseOut) {
+      Object.assign(baseConfig, {
+        onMouseOut: onAccessibleMouseOut
+      });
+    }
+
+    if (onInspectIconClick) {
+      inspectIcon = button({
+        className: "open-accessibility-inspector",
+        title: inspectIconTitle,
+        onClick: e => {
+          if (onAccessibleClick) {
+            e.stopPropagation();
+          }
+
+          onInspectIconClick(object, e);
+        }
+      });
+    }
+  }
+
+  return span(baseConfig, ...elements, inspectIcon);
+}
+
+function getElements(grip, nameMaxLength, separatorText = ": ") {
+  const { name, role } = grip.preview;
+  const elements = [];
+
+  elements.push(span({ className: "accessible-role" }, role));
+  if (name) {
+    elements.push(span({ className: "separator" }, separatorText), StringRep({
+      className: "accessible-name",
+      object: name,
+      cropLimit: nameMaxLength
+    }));
+  }
+
+  return elements;
+}
+
+// Registration
+function supportsObject(object, noGrip = false) {
+  if (noGrip === true || !isGrip(object)) {
+    return false;
+  }
+
+  return object.preview && object.typeName && object.typeName === "accessible";
+}
+
+// Exports from this module
+module.exports = {
+  rep: wrapRender(Accessible),
+  supportsObject
+};
+
+/***/ }),
+
+/***/ 1834:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* 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/>. */
+
+// ReactJS
+const PropTypes = __webpack_require__(1758);
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 // Reps
-const { getGripType, isGrip, wrapRender } = __webpack_require__(3644);
-const { rep: StringRep } = __webpack_require__(3648);
+const { getGripType, isGrip, wrapRender } = __webpack_require__(1760);
+const { rep: StringRep } = __webpack_require__(1769);
 
 /**
  * Renders DOM attribute
  */
 Attribute.propTypes = {
   object: PropTypes.object.isRequired
 };
 
@@ -5049,33 +5262,33 @@ function supportsObject(grip, noGrip = f
 
 module.exports = {
   rep: wrapRender(Attribute),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3682:
+/***/ 1835:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // ReactJS
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 
 // Reps
-const { getGripType, isGrip, wrapRender } = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+const { getGripType, isGrip, wrapRender } = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Used to render JS built-in Date() object.
  */
 DateTime.propTypes = {
   object: PropTypes.object.isRequired
 };
@@ -5113,38 +5326,38 @@ function supportsObject(grip, noGrip = f
 // Exports from this module
 module.exports = {
   rep: wrapRender(DateTime),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3683:
+/***/ 1836:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // ReactJS
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 
 // Reps
 const {
   getGripType,
   isGrip,
   getURLDisplayString,
   wrapRender
-} = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+} = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders DOM document object.
  */
 Document.propTypes = {
   object: PropTypes.object.isRequired
 };
@@ -5182,32 +5395,32 @@ function supportsObject(object, noGrip =
 // Exports from this module
 module.exports = {
   rep: wrapRender(Document),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3684:
+/***/ 1837:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // ReactJS
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 
 // Reps
-const { getGripType, isGrip, wrapRender } = __webpack_require__(3644);
-const dom = __webpack_require__(3643);
+const { getGripType, isGrip, wrapRender } = __webpack_require__(1760);
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders DOM documentType object.
  */
 DocumentType.propTypes = {
   object: PropTypes.object.isRequired
 };
@@ -5234,36 +5447,36 @@ function supportsObject(object, noGrip =
 // Exports from this module
 module.exports = {
   rep: wrapRender(DocumentType),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3685:
+/***/ 1838:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
 
 /* 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/>. */
 
 // ReactJS
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 
 // Reps
-const { isGrip, wrapRender } = __webpack_require__(3644);
-
-const { MODE } = __webpack_require__(3645);
-const { rep } = __webpack_require__(3656);
+const { isGrip, wrapRender } = __webpack_require__(1760);
+
+const { MODE } = __webpack_require__(1762);
+const { rep } = __webpack_require__(1782);
 
 /**
  * Renders DOM event objects.
  */
 Event.propTypes = {
   object: PropTypes.object.isRequired,
   // @TODO Change this to Object.values when supported in Node's version of V8
   mode: PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
@@ -5340,37 +5553,37 @@ function supportsObject(grip, noGrip = f
 // Exports from this module
 module.exports = {
   rep: wrapRender(Event),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3686:
+/***/ 1839:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
 
 /* 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/>. */
 
 // ReactJS
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 // Dependencies
-const { getGripType, isGrip, wrapRender } = __webpack_require__(3644);
-
-const PropRep = __webpack_require__(3650);
-const { MODE } = __webpack_require__(3645);
-
-const dom = __webpack_require__(3643);
+const { getGripType, isGrip, wrapRender } = __webpack_require__(1760);
+
+const PropRep = __webpack_require__(1774);
+const { MODE } = __webpack_require__(1762);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders a DOM Promise object.
  */
 PromiseRep.propTypes = {
   object: PropTypes.object.isRequired,
   // @TODO Change this to Object.values when supported in Node's version of V8
@@ -5385,17 +5598,17 @@ function PromiseRep(props) {
   const { promiseState } = object;
 
   const config = {
     "data-link-actor-id": object.actor,
     className: "objectBox objectBox-object"
   };
 
   if (props.mode === MODE.TINY) {
-    const { Rep } = __webpack_require__(3647);
+    const { Rep } = __webpack_require__(1768);
 
     return span(config, getTitle(object), span({
       className: "objectLeftBrace"
     }, " { "), Rep({ object: promiseState.state }), span({
       className: "objectRightBrace"
     }, " }"));
   }
 
@@ -5449,33 +5662,33 @@ function supportsObject(object, noGrip =
 // Exports from this module
 module.exports = {
   rep: wrapRender(PromiseRep),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3687:
+/***/ 1840:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // ReactJS
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 
 // Reps
-const { getGripType, isGrip, wrapRender } = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+const { getGripType, isGrip, wrapRender } = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders a grip object with regular expression.
  */
 RegExp.propTypes = {
   object: PropTypes.object.isRequired
 };
@@ -5505,38 +5718,38 @@ function supportsObject(object, noGrip =
 // Exports from this module
 module.exports = {
   rep: wrapRender(RegExp),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3688:
+/***/ 1841:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // ReactJS
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 
 // Reps
 const {
   getGripType,
   isGrip,
   getURLDisplayString,
   wrapRender
-} = __webpack_require__(3644);
-
-const dom = __webpack_require__(3643);
+} = __webpack_require__(1760);
+
+const dom = __webpack_require__(1759);
 const { span } = dom;
 
 /**
  * Renders a grip representing CSSStyleSheet
  */
 StyleSheet.propTypes = {
   object: PropTypes.object.isRequired
 };
@@ -5574,37 +5787,37 @@ function supportsObject(object, noGrip =
 
 module.exports = {
   rep: wrapRender(StyleSheet),
   supportsObject
 };
 
 /***/ }),
 
-/***/ 3689:
+/***/ 1842:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/>. */
 
 // Dependencies
-const PropTypes = __webpack_require__(3642);
+const PropTypes = __webpack_require__(1758);
 const {
   isGrip,
   cropString,
   cropMultipleLines,
   wrapRender
-} = __webpack_require__(3644);
-const { MODE } = __webpack_require__(3645);
-const nodeConstants = __webpack_require__(3659);
-const dom = __webpack_require__(3643);
+} = __webpack_require__(1760);
+const { MODE } = __webpack_require__(1762);
+const nodeConstants = __webpack_require__(1792);
+const dom = __webpack_require__(1759);