Bug 1378089 - Part 3. Replace the Bookmarks Manager and the History viewer with the Firefox library. r=IanN a=IanN
authorFrank-Rainer Grahl <frgrahl@gmx.net>
Sat, 03 Feb 2018 19:49:47 +0100
changeset 31459 2516f9f3631a
parent 31458 1073c50c7d8c
child 31460 551982a6e2bd
push id383
push userclokep@gmail.com
push date2018-05-07 21:52 +0000
reviewersIanN, IanN
bugs1378089, 1383138, 371409
Bug 1378089 - Part 3. Replace the Bookmarks Manager and the History viewer with the Firefox library. r=IanN a=IanN Fx 56 up to Bug 1383138. Rev 371409 in m-c.
suite/FIXME-PLACES.TXT
suite/browser/browser-places.js
suite/browser/browser-prefs.js
suite/browser/content.js
suite/browser/jar.mn
suite/browser/navigator.js
suite/browser/navigator.xul
suite/browser/navigatorOverlay.xul
suite/browser/nsBrowserStatusHandler.js
suite/common/jar.mn
suite/common/mac/platformCommunicatorOverlay.xul
suite/common/moz.build
suite/common/places/PlacesUIUtils.jsm
suite/common/places/content/bookmarkProperties.js
suite/common/places/content/bookmarkProperties.xul
suite/common/places/content/bookmarksPanel.js
suite/common/places/content/bookmarksPanel.xul
suite/common/places/content/browserPlacesViews.js
suite/common/places/content/controller.js
suite/common/places/content/editBookmarkOverlay.js
suite/common/places/content/editBookmarkOverlay.xul
suite/common/places/content/history-panel.js
suite/common/places/content/history-panel.xul
suite/common/places/content/menu.xml
suite/common/places/content/moveBookmarks.js
suite/common/places/content/moveBookmarks.xul
suite/common/places/content/organizer.css
suite/common/places/content/places.css
suite/common/places/content/places.js
suite/common/places/content/places.xul
suite/common/places/content/placesOverlay.xul
suite/common/places/content/sidebarUtils.js
suite/common/places/content/tree.xml
suite/common/places/content/treeView.js
suite/common/places/jar.mn
suite/common/places/moz.build
suite/common/sidebar/sidebarOverlay.xul
suite/common/tasksOverlay.js
suite/common/utilityOverlay.js
suite/installer/allowed-dupes.mn
suite/locales/en-US/chrome/browser/navigator.dtd
suite/locales/en-US/chrome/common/help/cs_nav_prefs_appearance.xhtml
suite/locales/en-US/chrome/common/help/customize_help.xhtml
suite/locales/en-US/chrome/common/places/bookmarkProperties.properties
suite/locales/en-US/chrome/common/places/editBookmarkOverlay.dtd
suite/locales/en-US/chrome/common/places/moveBookmarks.dtd
suite/locales/en-US/chrome/common/places/places.dtd
suite/locales/en-US/chrome/common/places/places.properties
suite/locales/jar.mn
suite/modules/moz.build
suite/themes/classic/communicator/aboutSessionRestore.css
suite/themes/classic/communicator/communicator.css
suite/themes/classic/communicator/places/bookmarks.css
suite/themes/classic/communicator/places/bookmarksToolbar.css
suite/themes/classic/communicator/places/editBookmarkOverlay.css
suite/themes/classic/communicator/places/history.png
suite/themes/classic/communicator/places/organizer.css
suite/themes/classic/communicator/sidebar/customize.css
suite/themes/classic/jar.mn
suite/themes/classic/mac/communicator/aboutSessionRestore.css
suite/themes/classic/mac/communicator/places/bookmarks.css
suite/themes/classic/mac/communicator/places/bookmarksToolbar.css
suite/themes/classic/mac/communicator/places/editBookmarkOverlay.css
suite/themes/classic/mac/navigator/navigator.css
suite/themes/classic/mac/navigator/tabbrowser.css
suite/themes/classic/navigator/navigator.css
suite/themes/classic/navigator/tabbrowser.css
suite/themes/modern/communicator/aboutSessionRestore.css
suite/themes/modern/communicator/communicator.css
suite/themes/modern/communicator/places/bookmarks.css
suite/themes/modern/communicator/places/bookmarksToolbar.css
suite/themes/modern/communicator/places/editBookmarkOverlay.css
suite/themes/modern/communicator/places/history.png
suite/themes/modern/global/dirListing/dirListing.css
suite/themes/modern/jar.mn
suite/themes/modern/navigator/navigator.css
suite/themes/modern/navigator/tabbrowser.css
new file mode 100644
--- /dev/null
+++ b/suite/FIXME-PLACES.TXT
@@ -0,0 +1,5 @@
+Load in sidebar disabled because of:
+Timestamp: 1/29/2018, 9:56:54 PM
+Error: TypeError: browserWin.openWebPanel is not a function
+Source File: resource:///modules/PlacesUIUtils.jsm
+Line: 1053
--- a/suite/browser/browser-places.js
+++ b/suite/browser/browser-places.js
@@ -1,12 +1,18 @@
 /* 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/. */
 
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyScriptGetter(this, ["PlacesToolbar", "PlacesMenu",
+                                         "PlacesPanelview", "PlacesPanelMenuView"],
+                                  "chrome://communicator/content/places/browserPlacesViews.js");
+
 var StarUI = {
   _itemGuids: -1,
   uri: null,
   _batching: false,
 
   _element: function(aID) {
     return document.getElementById(aID);
   },
@@ -15,16 +21,25 @@ var StarUI = {
   get panel() {
     delete this.panel;
     var element = this._element("editBookmarkPanel");
     // initially the panel is hidden
     // to avoid impacting startup / new window performance
     element.hidden = false;
     element.addEventListener("popuphidden", this);
     element.addEventListener("keypress", this);
+    element.addEventListener("keypress", this);
+    element.addEventListener("mousedown", this);
+    element.addEventListener("mouseout", this);
+    element.addEventListener("mousemove", this);
+    element.addEventListener("compositionstart", this);
+    element.addEventListener("compositionend", this);
+    element.addEventListener("input", this);
+    element.addEventListener("popuphidden", this);
+    element.addEventListener("popupshown", this);
     return this.panel = element;
   },
 
   // Array of command elements to disable when the panel is opened.
   get _blockedCommands() {
     delete this._blockedCommands;
     return this._blockedCommands =
       ["cmd_close", "cmd_closeWindow"].map(id => this._element(id));
@@ -111,87 +126,110 @@ var StarUI = {
             break;
         }
         break;
     }
   },
 
   _overlayLoaded: false,
   _overlayLoading: false,
-  showEditBookmarkPopup:
-  function SU_showEditBookmarkPopup(aItemId, aAnchorElement, aPosition) {
+  async showEditBookmarkPopup(aNode, aAnchorElement, aPosition, aIsNewBookmark) {
+    // Slow double-clicks (not true double-clicks) shouldn't
+    // cause the panel to flicker.
+    if (this.panel.state == "showing" ||
+        this.panel.state == "open") {
+      return;
+    }
+
+    this._isNewBookmark = aIsNewBookmark;
+    this._uriForRemoval = "";
+    // TODO (bug 1131491): Deprecate this once async transactions are enabled
+    // and the legacy transactions code is gone.
+    if (typeof(aNode) == "number") {
+      let itemId = aNode;
+      let guid = await PlacesUtils.promiseItemGuid(itemId);
+      aNode = await PlacesUIUtils.fetchNodeLike(guid);
+    }
+
     // Performance: load the overlay the first time the panel is opened
     // (see bug 392443).
     if (this._overlayLoading)
       return;
 
     if (this._overlayLoaded) {
-      this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition);
+      this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
       return;
     }
 
     this._overlayLoading = true;
     document.loadOverlay(
-      "chrome://communicator/content/bookmarks/editBookmarkOverlay.xul",
-      (function (aSubject, aTopic, aData) {
+      "chrome://communicator/content/places/editBookmarkOverlay.xul",
+      (aSubject, aTopic, aData) => {
         // Move the header (star, title, button) into the grid,
         // so that it aligns nicely with the other items (bug 484022).
         let header = this._element("editBookmarkPanelHeader");
         let rows = this._element("editBookmarkPanelGrid").lastChild;
         rows.insertBefore(header, rows.firstChild);
         header.hidden = false;
 
         this._overlayLoading = false;
         this._overlayLoaded = true;
-        this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition);
-      }).bind(this)
+        this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
+      }
     );
   },
 
   _doShowEditBookmarkPanel:
-  function SU__doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition) {
+  function SU__doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition) {
     if (this.panel.state != "closed")
       return;
 
     this._blockCommands(); // un-done in the popuphiding handler
 
     // Set panel title:
     // if we are batching, i.e. the bookmark has been added now,
     // then show Page Bookmarked, else if the bookmark did already exist,
     // we are about editing it, then use Edit This Bookmark.
     this._element("editBookmarkPanelTitle").value =
-      this._batching ?
+      this._isNewBookmark ?
         gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
         gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle");
 
     this._element("editBookmarkPanelBottomButtons").hidden = false;
     this._element("editBookmarkPanelContent").hidden = false;
 
-    // The remove button is shown only if we're not already batching, i.e.
-    // if the cancel button/ESC does not remove the bookmark.
-    this._element("editBookmarkPanelRemoveButton").hidden = this._batching;
-
     // The label of the remove button differs if the URI is bookmarked
     // multiple times.
-    var bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
-    var forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
-    var label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length);
+    let bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
+    let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
+    let label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length);
     this._element("editBookmarkPanelRemoveButton").label = label;
 
-    this._itemId = aItemId !== undefined ? aItemId : this._itemId;
+    this._itemId = aNode.itemId;
     this.beginBatch();
 
-    // Consume dismiss clicks, see bug 400924
-    this.panel.popupBoxObject
-        .setConsumeRollupEvent(PopupBoxObject.ROLLUP_CONSUME);
+    let onPanelReady = fn => {
+      let target = this.panel;
+      if (target.parentNode) {
+        // By targeting the panel's parent and using a capturing listener, we
+        // can have our listener called before others waiting for the panel to
+        // be shown (which probably expect the panel to be fully initialized)
+        target = target.parentNode;
+      }
+      target.addEventListener("popupshown", function(event) {
+        fn();
+      }, {"capture": true, "once": true});
+    };
+    gEditItemOverlay.initPanel({ node: aNode,
+                                 onPanelReady,
+                                 hiddenRows: ["description", "location",
+                                              "loadInSidebar", "keyword"],
+                                 focusedElement: "preferred"});
+
     this.panel.openPopup(aAnchorElement, aPosition);
-
-    gEditItemOverlay.initPanel(this._itemId,
-                               { hiddenRows: ["description", "location",
-                                              "keyword"] });
   },
 
   panelShown:
   function SU_panelShown(aEvent) {
     if (aEvent.target == this.panel) {
       if (!this._element("editBookmarkPanelContent").hidden) {
         let fieldToFocus = "editBMPanel_" +
           Services.prefs.getCharPref("browser.bookmarks.editDialog.firstEditField");
@@ -232,81 +270,16 @@ var StarUI = {
     if (!this._batching) {
       PlacesUtils.transactionManager.beginBatch(null);
       this._batching = true;
     }
   }
 }
 
 var PlacesCommandHook = {
-  /**
-   * Adds a bookmark to the page loaded in the given browser.
-   *
-   * @param aBrowser
-   *        a <browser> element.
-   * @param [optional] aParent
-   *        The folder in which to create a new bookmark if the page loaded in
-   *        aBrowser isn't bookmarked yet, defaults to the unfiled root.
-   * @param [optional] aShowEditUI
-   *        whether or not to show the edit-bookmark UI for the bookmark item
-   */
-  bookmarkPage: function PCH_bookmarkPage(aBrowser, aParent, aShowEditUI) {
-    var uri = aBrowser.currentURI;
-    var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
-    if (itemId == -1) {
-      // Copied over from addBookmarkForBrowser:
-      // Bug 52536: We obtain the URL and title from the nsIWebNavigation
-      // associated with a <browser/> rather than from a DOMWindow.
-      // This is because when a full page plugin is loaded, there is
-      // no DOMWindow (?) but information about the loaded document
-      // may still be obtained from the webNavigation.
-      var webNav = aBrowser.webNavigation;
-      var url = webNav.currentURI;
-      var title;
-      var description;
-      var charset;
-      try {
-        title = webNav.document.title || url.spec;
-        description = PlacesUIUtils.getDescriptionFromDocument(webNav.document);
-        charset = webNav.document.characterSet;
-      }
-      catch (e) { }
-
-      if (aShowEditUI) {
-        // If we bookmark the page here (i.e. page was not "starred" already)
-        // but open right into the "edit" state, start batching here, so
-        // "Cancel" in that state removes the bookmark.
-        StarUI.beginBatch();
-      }
-
-      var parent = aParent || PlacesUtils.unfiledBookmarksFolderId;
-      var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
-      var txn = new PlacesCreateBookmarkTransaction(uri, parent,
-                                                    PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                                    title, null, [descAnno]);
-      PlacesUtils.transactionManager.doTransaction(txn);
-      // Set the character-set
-      if (charset)
-        PlacesUtils.setCharsetForURI(uri, charset);
-      itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
-    }
-
-    // dock the panel to the star icon when possible, otherwise dock
-    // it to the content area
-    if (aBrowser.contentWindow == window.content) {
-      let ubIcons = aBrowser.ownerDocument.getElementById("urlbar-icons");
-      if (ubIcons) {
-        if (aShowEditUI)
-          StarUI.showEditBookmarkPopup(itemId, ubIcons, "bottomcenter topright");
-        return;
-      }
-    }
-
-    StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap");
-  },
 
   /**
    * Adds a bookmark to the page loaded in the given browser using the
    * properties dialog.
    *
    * @param aBrowser
    *        a <browser> element.
    * @param [optional] aParent
@@ -330,130 +303,329 @@ var PlacesCommandHook = {
     var insertPoint = new InsertionPoint(parent,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX);
     var hiddenRows = [];
     PlacesUIUtils.showAddBookmarkUI(uri, title, description, insertPoint, true,
                                     null, null, null, null, hiddenRows);
   },
 
   /**
+   * Adds a bookmark to the page loaded in the given browser.
+   *
+   * @param aBrowser
+   *        a <browser> element.
+   * @param [optional] aParent
+   *        The folder in which to create a new bookmark if the page loaded in
+   *        aBrowser isn't bookmarked yet, defaults to the unfiled root.
+   * @param [optional] aShowEditUI
+   *        whether or not to show the edit-bookmark UI for the bookmark item
+   */
+  async bookmarkPage(aBrowser, aParent, aShowEditUI) {
+    if (PlacesUIUtils.useAsyncTransactions) {
+      await this._bookmarkPagePT(aBrowser, aParent, aShowEditUI);
+      return;
+    }
+
+    var uri = aBrowser.currentURI;
+    var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
+    let isNewBookmark = itemId == -1;
+    if (isNewBookmark) {
+      // Bug 1148838 - Make this code work for full page plugins.
+      var title;
+      var description;
+      var charset;
+
+      let docInfo = await this._getPageDetails(aBrowser);
+
+      try {
+        title = docInfo.isErrorPage ? PlacesUtils.history.getPageTitle(uri)
+                                    : aBrowser.contentTitle;
+        title = title || uri.spec;
+        description = docInfo.description;
+        charset = aBrowser.characterSet;
+      } catch (e) { }
+
+      if (aShowEditUI) {
+        // If we bookmark the page here but open right into a cancelable
+        // state (i.e. new bookmark in Library), start batching here so
+        // all of the actions can be undone in a single undo step.
+        StarUI.beginBatch();
+      }
+
+      var parent = aParent !== undefined ?
+                   aParent : PlacesUtils.unfiledBookmarksFolderId;
+      var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
+      var txn = new PlacesCreateBookmarkTransaction(uri, parent,
+                                                    PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                                    title, null, [descAnno]);
+      PlacesUtils.transactionManager.doTransaction(txn);
+      itemId = txn.item.id;
+      // Set the character-set.
+      if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
+        PlacesUtils.setCharsetForURI(uri, charset);
+    }
+
+    // If it was not requested to open directly in "edit" mode, we are done.
+    if (!aShowEditUI)
+      return;
+
+    // Dock the panel to the star icon when possible, otherwise dock
+    // it to the content area.
+    if (aBrowser.contentWindow == window.content) {
+      let ubIcons = aBrowser.ownerDocument.getElementById("urlbar-icons");
+      if (ubIcons) {
+        await StarUI.showEditBookmarkPopup(itemId, ubIcons,
+                                           "bottomcenter topright",
+                                           isNewBookmark);
+        return;
+      }
+    }
+
+    await StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap",
+                                       isNewBookmark);
+  },
+
+  // TODO: Replace bookmarkPage code with this function once legacy
+  // transactions are removed.
+  async _bookmarkPagePT(aBrowser, aParentId, aShowEditUI) {
+    let url = new URL(aBrowser.currentURI.spec);
+    let info = await PlacesUtils.bookmarks.fetch({ url });
+    let isNewBookmark = !info;
+    if (!info) {
+      let parentGuid = aParentId !== undefined ?
+                         await PlacesUtils.promiseItemGuid(aParentId) :
+                         PlacesUtils.bookmarks.unfiledGuid;
+      info = { url, parentGuid };
+      // Bug 1148838 - Make this code work for full page plugins.
+      let description = null;
+      let charset = null;
+
+      let docInfo = await this._getPageDetails(aBrowser);
+
+      try {
+        if (docInfo.isErrorPage) {
+          let entry = await PlacesUtils.history.fetch(aBrowser.currentURI);
+          if (entry) {
+            info.title = entry.title;
+          }
+        } else {
+          info.title = aBrowser.contentTitle;
+        }
+        info.title = info.title || url.href;
+        description = docInfo.description;
+        charset = aBrowser.characterSet;
+      } catch (e) {
+        Cu..reportError(e);
+      }
+
+      if (aShowEditUI && isNewBookmark) {
+        // If we bookmark the page here but open right into a cancelable
+        // state (i.e. new bookmark in Library), start batching here so
+        // all of the actions can be undone in a single undo step.
+        StarUI.beginBatch();
+      }
+
+      if (description) {
+        info.annotations = [{ name: PlacesUIUtils.DESCRIPTION_ANNO,
+                              value: description }];
+      }
+
+      info.guid = await PlacesTransactions.NewBookmark(info).transact();
+
+      // Set the character-set
+      if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
+         PlacesUtils.setCharsetForURI(makeURI(url.href), charset);
+    }
+
+    // If it was not requested to open directly in "edit" mode, we are done.
+    if (!aShowEditUI)
+      return;
+
+    let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(info);
+
+    // Dock the panel to the star icon when possible, otherwise dock
+    // it to the content area.
+    if (aBrowser.contentWindow == window.content) {
+      let ubIcons = aBrowser.ownerDocument.getElementById("urlbar-icons");
+      if (ubIcons) {
+        await StarUI.showEditBookmarkPopup(node, ubIcons,
+                                           "bottomcenter topright",
+                                           isNewBookmark);
+        return;
+      }
+    }
+
+    await StarUI.showEditBookmarkPopup(node, aBrowser, "overlap",
+                                       isNewBookmark);
+  },
+
+  _getPageDetails(browser) {
+    return new Promise(resolve => {
+      let mm = browser.messageManager;
+      mm.addMessageListener("Bookmarks:GetPageDetails:Result", function listener(msg) {
+        mm.removeMessageListener("Bookmarks:GetPageDetails:Result", listener);
+        resolve(msg.data);
+      });
+
+      mm.sendAsyncMessage("Bookmarks:GetPageDetails", { })
+    });
+  },
+
+  /**
    * Adds a bookmark to the page loaded in the current tab.
    */
   bookmarkCurrentPage: function PCH_bookmarkCurrentPage(aShowEditUI, aParent) {
-    this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI);
+    this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI)
+        .catch(Cu..reportError);
   },
 
   /**
    * Adds a bookmark to the page targeted by a link.
    * @param aParent
    *        The folder in which to create a new bookmark if aURL isn't
    *        bookmarked.
    * @param aURL (string)
    *        the address of the link target
    * @param aTitle
    *        The link text
+   * @param [optional] aDescription
+   *        The linked page description, if available
    */
-  bookmarkLink: function PCH_bookmarkLink(aParent, aURL, aTitle) {
-    var linkURI = makeURI(aURL);
-    var itemId = PlacesUtils.getMostRecentBookmarkForURI(linkURI);
-    if (itemId == -1)
-      PlacesUIUtils.showMinimalAddBookmarkUI(linkURI, aTitle);
-    else {
-      PlacesUIUtils.showItemProperties(itemId,
-                                       PlacesUtils.bookmarks.TYPE_BOOKMARK);
+  async bookmarkLink(aParentId, aURL, aTitle, aDescription = "") {
+    let node = await PlacesUIUtils.fetchNodeLike({ url: aURL });
+    if (node) {
+      PlacesUIUtils.showBookmarkDialog({ action: "edit",
+                                         node
+                                       }, window.top);
+      return;
     }
+
+    let ip = new InsertionPoint(aParentId,
+                                PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                Ci.nsITreeView.DROP_ON);
+    PlacesUIUtils.showBookmarkDialog({ action: "add",
+                                       type: "bookmark",
+                                       uri: makeURI(aURL),
+                                       title: aTitle,
+                                       description: aDescription,
+                                       defaultInsertionPoint: ip,
+                                       hiddenRows: [ "description",
+                                                     "location",
+                                                     "loadInSidebar",
+                                                     "keyword" ]
+                                     }, window.top);
   },
 
   /**
-   * This function returns a list of nsIURI objects characterizing the
-   * tabs currently open in the browser.  The URIs will appear in the
-   * list in the order in which their corresponding tabs appeared.  However,
-   * only the first instance of each URI will be returned.
-   *
-   * @returns a list of nsIURI objects representing unique locations open
+   * List of nsIURI objects characterizing the tabs currently open in the
+   * browser. The URIs will be in the order in which their
+   * corresponding tabs appeared and duplicates are discarded.
    */
-  _getUniqueTabInfo: function BATC__getUniqueTabInfo(aTitles) {
+  get uniqueCurrentPages() {
     var tabList = [];
     var seenURIs = {};
 
     var browsers = gBrowser.browsers;
     for (var i = 0; i < browsers.length; ++i) {
       let uri = browsers[i].currentURI;
 
       // skip redundant entries
       if (uri.spec in seenURIs)
         continue;
 
       // add to the set of seen URIs
       seenURIs[uri.spec] = null;
       tabList.push(uri);
-      if (aTitles)
-        aTitles.push(browsers[i].contentTitle);
     }
     return tabList;
   },
 
   /**
    * Adds a folder with bookmarks to all of the currently open tabs in this
    * window.
    */
   bookmarkCurrentPages: function PCH_bookmarkCurrentPages() {
-    var titles = [];
-    var tabURIs = this._getUniqueTabInfo(titles);
-    PlacesUIUtils.showMinimalAddMultiBookmarkUI(tabURIs, titles);
+    let pages = this.uniqueCurrentPages;
+    if (pages.length > 1) {
+    PlacesUIUtils.showBookmarkDialog({ action: "add",
+                                       type: "folder",
+                                       URIList: pages,
+                                       hiddenRows: [ "description" ]
+                                     }, window);
+    }
   },
 
+  /**
+   * Updates disabled state for the "Bookmark All Tabs" command.
+   */
+  updateBookmarkAllTabsCommand:
+  function PCH_updateBookmarkAllTabsCommand() {
+    // There's nothing to do in non-browser windows.
+    if (window.location.href != getBrowserURL())
+      return;
+
+    // Disable "Bookmark All Tabs" if there are less than two
+    // "unique current pages".
+    goSetCommandEnabled("Browser:BookmarkAllTabs",
+                        this.uniqueCurrentPages.length >= 2);
+  },
 
   /**
    * Adds a Live Bookmark to a feed associated with the current page.
    * @param     url
    *            The nsIURI of the page the feed was attached to
    * @title     title
    *            The title of the feed. Optional.
    * @subtitle  subtitle
    *            A short description of the feed. Optional.
    */
-  addLiveBookmark: function PCH_addLiveBookmark(url, feedTitle, feedSubtitle) {
-    var feedURI = makeURI(url);
+  async addLiveBookmark(url, feedTitle, feedSubtitle) {
+    let toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId,
+                                       PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                       Ci.nsITreeView.DROP_ON);
 
-    var doc = gBrowser.contentDocument;
-    var title = (arguments.length > 1) ? feedTitle : doc.title;
+    let feedURI = makeURI(url);
+    let title = feedTitle || gBrowser.contentTitle;
+    let description = feedSubtitle;
+    if (!description) {
+      description = (await this._getPageDetails(gBrowser.selectedBrowser)).description;
+    }
 
-    var description;
-    if (arguments.length > 2)
-      description = feedSubtitle;
-    else
-      description = PlacesUIUtils.getDescriptionFromDocument(doc);
-
-    var toolbarIP =
-      new InsertionPoint(PlacesUtils.bookmarks.toolbarFolder, -1);
-    PlacesUIUtils.showMinimalAddLivemarkUI(feedURI, gBrowser.currentURI,
-                                           title, description, toolbarIP, true);
+    PlacesUIUtils.showBookmarkDialog({ action: "add",
+                                       type: "livemark",
+                                       feedURI,
+                                       siteURI: gBrowser.currentURI,
+                                       title,
+                                       description,
+                                       defaultInsertionPoint: toolbarIP,
+                                       hiddenRows: [ "feedLocation",
+                                                     "siteLocation",
+                                                     "description" ]
+                                     }, window);
   },
 
   /**
-   * Opens the bookmarks manager.
+   * Opens the Places Organizer.
    * @param   aLeftPaneRoot
    *          The query to select in the organizer window - options
-   *          are: AllBookmarks, BookmarksMenu, BookmarksToolbar,
+   *          are: History, AllBookmarks, BookmarksMenu, BookmarksToolbar,
    *          UnfiledBookmarks and Tags.
    */
-  showBookmarksManager: function PCH_showBookmarksManager(aLeftPaneRoot) {
-    var manager = Services.wm.getMostRecentWindow("bookmarks:manager");
+  showPlacesOrganizer: function PCH_showPlacesOrganizer(aLeftPaneRoot) {
+    var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
     // Due to bug 528706, getMostRecentWindow can return closed windows.
-    if (!manager || manager.closed) {
+    if (!organizer || organizer.closed) {
       // No currently open places window, so open one with the specified mode.
-      openDialog("chrome://communicator/content/bookmarks/bookmarksManager.xul",
-                 "", "all,dialog=no", aLeftPaneRoot);
+      openDialog("chrome://communicator/content/places/places.xul",
+                 "", "chrome,toolbar=yes,dialog=no,resizable", aLeftPaneRoot);
+    } else {
+      organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
+      organizer.focus();
     }
-    else {
-      manager.PlacesOrganizer.selectLeftPaneQuery(aLeftPaneRoot);
-      manager.focus();
-    }
-  }
+  },
 };
 
 /**
  * Functions for handling events in the Bookmarks Toolbar and menu.
  */
 var BookmarksEventHandler = {
   /**
    * Handler for click event for an item in the bookmarks toolbar or menu.
@@ -673,18 +845,19 @@ var PlacesMenuDNDHandler = {
                                 PlacesUtils.bookmarks.DEFAULT_INDEX,
                                 Ci.nsITreeView.DROP_ON);
     PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
     event.stopPropagation();
   }
 };
 
 
-var PlacesStarButton = {
+var BookmarkingUI = {
   _hasBookmarksObserver: false,
+  _itemGuids: new Set(),
 
   uninit: function BUI_uninit()
   {
     if (this._hasBookmarksObserver) {
       PlacesUtils.bookmarks.removeObserver(this);
     }
 
     if (this._pendingUpdate) {
@@ -715,17 +888,17 @@ var PlacesStarButton = {
     this._itemGuids = [];
     let aItemGuids = [];
 
     // those objects are use to check if we are in the current iteration before
     // returning any result.
     let pendingUpdate = this._pendingUpdate = {};
 
     PlacesUtils.bookmarks.fetch({url: this._uri}, b => aItemGuids.push(b.guid))
-      .catch(Cu.reportError)
+      .catch(Cu..reportError)
       .then(() => {
          if (pendingUpdate != this._pendingUpdate) {
            return;
          }
 
          // It's possible that onItemAdded gets called before the async statement
          // calls back.  For such an edge case, retain all unique entries from the
          // array.
@@ -736,17 +909,17 @@ var PlacesStarButton = {
          this._updateStar();
 
          // Start observing bookmarks if needed.
          if (!this._hasBookmarksObserver) {
            try {
              PlacesUtils.bookmarks.addObserver(this);
              this._hasBookmarksObserver = true;
            } catch (ex) {
-             Cu.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex);
+             Cu..reportError("BookmarkingUI failed adding a bookmarks observer: " + ex);
            }
          }
 
          delete this._pendingUpdate;
        });
   },
 
   _updateStar: function BUI__updateStar()
--- a/suite/browser/browser-prefs.js
+++ b/suite/browser/browser-prefs.js
@@ -242,16 +242,19 @@ pref("browser.tabs.closeWindowWithLastTa
 pref("browser.tabs.warnOnClose", true);
 pref("browser.tabs.warnOnCloseOther", true);
 pref("browser.tabs.warnOnOpen", true);
 pref("browser.tabs.maxOpenBeforeWarn", 15);
 pref("browser.tabs.insertRelatedAfterCurrent", true);
 // 0 = append, 1 = replace
 pref("browser.tabs.loadGroup", 1);
 
+// For future use
+pref("browser.tabs.loadBookmarksInBackground", false);
+
 // No e10s in SeaMonkey for now.
 pref("browser.tabs.remote.autostart", false);
 
 // how many browsers can be saved in the DOM (by the tabbed browser)
 pref("browser.tabs.max_tabs_undo", 3);
 // should popups by saved in the DOM (by the tabbed browser)
 pref("browser.tabs.cache_popups", false);
 
--- a/suite/browser/content.js
+++ b/suite/browser/content.js
@@ -9,16 +9,18 @@
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(this, "LoginManagerContent",
   "resource://gre/modules/LoginManagerContent.jsm");
 ChromeUtils.defineModuleGetter(this, "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "LoginFormFactory",
   "resource://gre/modules/LoginManagerContent.jsm");
+ChromeUtils.defineModuleGetter(this, "PlacesUIUtils",
+  "resource:///modules/PlacesUIUtils.jsm");
 
 addMessageListener("RemoteLogins:fillForm", message => {
   LoginManagerContent.receiveMessage(message, content);
 });
 
 addEventListener("DOMFormHasPassword", event => {
   LoginManagerContent.onDOMFormHasPassword(event, content);
   let formLike = LoginFormFactory.createFromForm(event.target);
@@ -37,8 +39,16 @@ addEventListener("pageshow", event => {
 
 addEventListener("DOMAutoComplete", event => {
   LoginManagerContent.onUsernameInput(event);
 });
 
 addEventListener("blur", event => {
   LoginManagerContent.onUsernameInput(event);
 });
+
+addMessageListener("Bookmarks:GetPageDetails", (message) => {
+  let doc = content.document;
+  let isErrorPage = /^about:(neterror|certerror|blocked)/.test(doc.documentURI);
+  sendAsyncMessage("Bookmarks:GetPageDetails:Result",
+                   { isErrorPage,
+                     description: PlacesUIUtils.getDescriptionFromDocument(doc) });
+});
--- a/suite/browser/jar.mn
+++ b/suite/browser/jar.mn
@@ -1,16 +1,17 @@
 # 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/.
 
 comm.jar:
 % content navigator %content/navigator/
 % content navigator-region %content/navigator-region/
    content/navigator/browser.js
+   content/navigator/browser-places.js
    content/navigator/content.js
    content/navigator/fullScreen.js
    content/navigator/hiddenWindow.xul
    content/navigator/linkToolbarHandler.js
    content/navigator/linkToolbarItem.js
    content/navigator/linkToolbarOverlay.js
    content/navigator/linkToolbarOverlay.xul
    content/navigator/mailNavigatorOverlay.js
--- a/suite/browser/navigator.js
+++ b/suite/browser/navigator.js
@@ -14,16 +14,19 @@ this.__defineGetter__("PluralForm", func
 });
 this.__defineSetter__("PluralForm", function (val) {
   delete this.PluralForm;
   return this.PluralForm = val;
 });
 
 ChromeUtils.defineModuleGetter(this, "SafeBrowsing",
   "resource://gre/modules/SafeBrowsing.jsm");
+  
+XPCOMUtils.defineLazyScriptGetter(this, "gEditItemOverlay",
+                                  "chrome://communicator/content/places/editBookmarkOverlay.js");
 
 const REMOTESERVICE_CONTRACTID = "@mozilla.org/toolkit/remote-service;1";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 var gURLBar = null;
 var gProxyButton = null;
 var gProxyFavIcon = null;
 var gProxyDeck = null;
@@ -866,17 +869,17 @@ function WindowFocusTimerCallback(elemen
     }
   }
 }
 
 function Shutdown()
 {
   AeroPeek.onCloseWindow(window);
 
-  PlacesStarButton.uninit();
+  BookmarkingUI.uninit();
 
   // shut down browser access support
   window.browserDOMWindow = null;
 
   getBrowser().removeEventListener("DOMLinkAdded", BrowserSearch);
 
   try {
     getBrowser().removeProgressListener(window.XULBrowserWindow);
@@ -2592,21 +2595,16 @@ function StatusbarViewPopupManager()
 function popupBlockerMenuShowing(event)
 {
   var separator = document.getElementById("popupMenuSeparator");
 
   if (separator)
     separator.hidden = !createShowPopupsMenu(event.target, gBrowser.selectedBrowser);
 }
 
-function toHistory()
-{
-  toOpenWindowByType("history:manager", "chrome://communicator/content/history/history.xul");
-}
-
 // opener may not have been initialized by load time (chrome windows only)
 // so call this function some time later.
 function maybeInitPopupContext()
 {
   // it's not a popup with no opener
   if (!window.content.opener)
     return null;
 
--- a/suite/browser/navigator.xul
+++ b/suite/browser/navigator.xul
@@ -52,17 +52,18 @@
   <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
 
   <!-- Navigator -->
   <script type="application/javascript" src="chrome://navigator/content/fullScreen.js"/>
   <script type="application/javascript" src="chrome://navigator/content/navigatorDD.js"/>
   <script type="application/javascript" src="chrome://navigator/content/sessionHistoryUI.js"/>
 
   <!-- Places Bookmarks Utilities -->
-  <script type="application/javascript" src="chrome://communicator/content/bookmarks/editBookmarkOverlay.js"/>
+  <script type="application/javascript"
+          src="chrome://navigator/content/browser-places.js"/>
 
   <!-- hook for stringbundle overlays -->
   <stringbundleset id="stringbundleset">
     <stringbundle id="findBundle" src="chrome://global/locale/finddialog.properties"/>
   </stringbundleset>
 
   <commandset id="commands">
     <commandset id="findTypeMenuItems"/>
@@ -113,17 +114,48 @@
            noautofocus="true"
            hidden="true"/>
 
     <!-- for invalid form error message -->
     <panel id="invalid-form-popup" noautofocus="true" hidden="true" level="parent">
       <description/>
     </panel>
 
-    <panel id="editBookmarkPanel"/>
+    <panel id="editBookmarkPanel"
+           type="arrow"
+           animate="false"
+           orient="vertical"
+           ignorekeys="true"
+           hidden="true"
+           onpopupshown="StarUI.panelShown(event);"
+           aria-labelledby="editBookmarkPanelTitle">
+      <row id="editBookmarkPanelHeader" align="center" hidden="true">
+        <vbox align="center">
+          <image id="editBookmarkPanelStarIcon"/>
+        </vbox>
+        <label id="editBookmarkPanelTitle"/>
+      </row>
+      <vbox id="editBookmarkPanelContent" hidden="true"/>
+      <hbox id="editBookmarkPanelBottomButtons">
+        <button id="editBookmarkPanelRemoveButton"
+                class="editBookmarkPanelHeaderButton"
+                oncommand="StarUI.removeBookmarkButtonCommand();"
+                accesskey="&editBookmark.removeBookmark.accessKey;"/>
+        <spacer flex="1"/>
+        <button id="editBookmarkPanelDeleteButton"
+                class="editBookmarkPanelBottomButton"
+                label="&editBookmark.cancel.label;"
+                oncommand="StarUI.cancelButtonOnCommand();"/>
+        <button id="editBookmarkPanelDoneButton"
+                class="editBookmarkPanelBottomButton"
+                label="&editBookmark.done.label;"
+                default="true"
+                oncommand="StarUI.panel.hidePopup();"/>
+      </hbox>
+    </panel>
 
     <menupopup id="placesContext"/>
 
     <!-- Bookmarks and history tooltip -->
     <tooltip id="bhTooltip"/>
 
     <panel id="notification-popup"
            type="arrow"
@@ -327,17 +359,17 @@
           </deck>
           <hbox id="urlbar-icons" class="urlbar-icons"
                 onmousedown="event.stopPropagation();"
                 onclick="event.stopPropagation();">
             <image id="feedsButton" hidden="true" popup="feedsPopup"/>
             <image id="ev-button" hidden="true"
                    onclick="if (event.button == 0) BrowserPageInfo(null, 'securityTab');"/>
             <image id="star-button"
-                   onclick="PlacesStarButton.onClick(event);"/>
+                   onclick="BookmarkingUI.onClick(event);"/>
           </hbox>
           <menupopup id="ubhist-popup" class="autocomplete-history-popup"
                      popupalign="topleft" popupanchor="bottomleft"
                      onpopupshowing="createUBHistoryMenu(event.target);"
                      oncommand="executeUrlBarHistoryCommand(event.target);"/>
         </textbox>
       </toolbaritem>
 
--- a/suite/browser/navigatorOverlay.xul
+++ b/suite/browser/navigatorOverlay.xul
@@ -2,17 +2,17 @@
 <!-- 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/. -->
 
 <?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
 <?xul-overlay href="chrome://communicator/content/viewZoomOverlay.xul"?>
 <?xul-overlay href="chrome://communicator/content/viewApplyThemeOverlay.xul"?>
 <?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?>
-<?xul-overlay href="chrome://communicator/content/bookmarks/placesOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
 <?xul-overlay href="chrome://communicator/content/charsetOverlay.xul"?>
 <?xul-overlay href="chrome://navigator/content/mailNavigatorOverlay.xul"?>
 <?xul-overlay href="chrome://navigator/content/platformNavigationBindings.xul"?>
 
 <!DOCTYPE overlay [
 <!ENTITY % navigatorDTD SYSTEM "chrome://navigator/locale/navigator.dtd" >
 %navigatorDTD;
 <!ENTITY % navigatorOverlayDTD SYSTEM "chrome://navigator/locale/navigatorOverlay.dtd">
@@ -28,19 +28,17 @@
   <!-- Navigator -->
   <script type="application/javascript"
           src="chrome://navigator/content/browser.js"/>
   <script type="application/javascript"
           src="chrome://navigator/content/navigator.js"/>
 
   <!-- Places Bookmarks Utilities -->
   <script type="application/javascript"
-          src="chrome://communicator/content/places/browserPlacesViews.js"/>
-  <script type="application/javascript"
-          src="chrome://communicator/content/bookmarks/browser-places.js"/>
+          src="chrome://navigator/content/browser-places.js"/>
 
   <stringbundleset id="stringbundleset">
     <stringbundle id="bundle_navigator"
                   src="chrome://navigator/locale/navigator.properties"/>
     <stringbundle id="bundle_brand"
                   src="chrome://branding/locale/brand.properties"/>
     <stringbundle id="bundle_navigator_region"
                   src="chrome://navigator-region/locale/region.properties"/>
@@ -102,17 +100,20 @@
 
     <!-- Tools Menu -->
     <key id="searchInternetKb" key="&searchInternet.commandKey;" modifiers="accel,shift" command="Browser:SearchInternet"/>
 
     <!-- Misc -->
     <!-- the amazing fishcam, suppress warning by ',' at the beginning of modifiers, see bug 496322 -->
     <key key="f" modifiers=",control,alt" oncommand="loadURI('http://www.fishcam.com/');"/>
     <key id="goUpKb" keycode="VK_UP" command="Browser:Up" modifiers="alt"/>
-    <key id="key_gotoHistory"  key="&history.commandKey;"        oncommand="toHistory();" modifiers="accel"/>
+    <key id="key_gotoHistory"
+         key="&history.commandKey;"
+         oncommand="PlacesCommandHook.showPlacesOrganizer('History');"
+         modifiers="accel"/>
     <keyset id="viewZoomKeys"/>
     <keyset id="navigationKeys"/>
     <keyset id="tasksKeys"/>
     <key id="key_sanitize" command="Tools:Sanitize" keycode="VK_DELETE" modifiers="accel,shift"/>
   </keyset>
 
   <commandset id="commands">
     <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab();"/>
@@ -176,26 +177,26 @@
     <command id="cmd_findTypeLinks" observes="isImage"/>
 
     <!-- Bookmarks Menu -->
     <command id="Browser:AddBookmark"
              label="&addCurPageCmd.label;" accesskey="&addCurPageCmd.accesskey;"
              oncommand="PlacesCommandHook.bookmarkCurrentPage(false, PlacesUtils.bookmarksMenuFolderId);"/>
     <command id="Browser:AddBookmarkAs"
              label="&addCurPageAsCmd.label;" accesskey="&addCurPageAsCmd.accesskey;"
-             oncommand="PlacesCommandHook.bookmarkPageAs(gBrowser.selectedBrowser, PlacesUtils.bookmarksMenuFolderId);"/>
+             oncommand="PlacesCommandHook.bookmarkPage(gBrowser.selectedBrowser, PlacesUtils.bookmarksMenuFolderId, true);"/>
     <!-- The command is disabled for the hidden window. Otherwise its enabled
          state is handled by BookmarksEventHandler.onPopupShowing. -->
     <command id="Browser:BookmarkAllTabs"
              label="&addCurTabsAsCmd.label;" accesskey="&addCurTabsAsCmd.accesskey;"
              oncommand="PlacesCommandHook.bookmarkCurrentPages();"
              disabled="true"/>
     <command id="Browser:ManageBookmark"
              label="&manBookmarksCmd.label;" accesskey="&manBookmarksCmd.accesskey;"
-             oncommand="PlacesCommandHook.showBookmarksManager('AllBookmarks');"/>
+             oncommand="PlacesCommandHook.showPlacesOrganizer('AllBookmarks');"/>
     <command id="feedsMenu" disabled="true"/>
     <commandset id="placesCommands"/>
 
     <!-- Go Menu -->
     <command id="Browser:Home"    oncommand="BrowserHome(event);"/>
     <command id="Browser:Back"    oncommand="BrowserBack();"    observes="canGoBack"/>
     <command id="Browser:Forward" oncommand="BrowserForward();" observes="canGoForward"/>
     <command id="Browser:Up"      oncommand="BrowserUp();"      observes="canGoUp"/>
@@ -438,17 +439,17 @@
                   label="&historyRestoreLastSession.label;"
                   accesskey="&historyRestoreLastSession.accesskey;"
                   oncommand="restoreLastSession();"
                   disabled="true"/>
         <menuseparator/>
         <menuitem id="menu_showAllHistory"
                   label="&historyCmd.label;"
                   accesskey="&historyCmd.accesskey;"
-                  oncommand="toHistory()"
+                  oncommand="PlacesCommandHook.showPlacesOrganizer('History');"
                   key="key_gotoHistory"/>
         <menuseparator id="startHistorySeparator" hidden="true"/>
         <menuseparator id="endHistorySeparator" hidden="true"/>
         <!-- Dead for now.
         <menuitem id="sync-tabs-menuitem"
                   label="&syncTabsMenu.label;"
                   oncommand="BrowserOpenSyncTabs();"
                   disabled="true"/> -->
--- a/suite/browser/nsBrowserStatusHandler.js
+++ b/suite/browser/nsBrowserStatusHandler.js
@@ -328,17 +328,17 @@ nsBrowserStatusHandler.prototype =
       var userTypedValue = browser.userTypedValue;
       if (userTypedValue === null) {
         URLBarSetURI(aLocation, true);
       } else {
         this.urlBar.value = userTypedValue;
         SetPageProxyState("invalid", null);
       }
 
-      PlacesStarButton.updateStarState();
+      BookmarkingUI.updateStarState();
 
       this.feedsMenu.setAttribute("disabled", "true");
       this.feedsButton.hidden = true;
       this.feeds = [];
 
       // When background tab comes into foreground or loading a new page
       // (aRequest set), might want to update zoom.
       if (FullZoom.updateBackgroundTabs || aRequest)
--- a/suite/common/jar.mn
+++ b/suite/common/jar.mn
@@ -114,31 +114,16 @@ comm.jar:
    content/communicator/bindings/findbar.xml                        (bindings/findbar.xml)
    content/communicator/bindings/general.xml                        (bindings/general.xml)
    content/communicator/bindings/generalBindings.xml                (../../common/bindings/generalBindings.xml)
    content/communicator/bindings/notification.xml                   (bindings/notification.xml)
 *  content/communicator/bindings/preferences.xml                    (../../common/bindings/preferences.xml)
 *  content/communicator/bindings/toolbar.xml                        (../../common/bindings/toolbar.xml)   
    content/communicator/bindings/toolbar-xpfe.xml                   (bindings/toolbar-xpfe.xml)
 *  content/communicator/bindings/prefwindow.xml                     (bindings/prefwindow.xml)
-   content/communicator/bookmarks/bm-panel.js                       (bookmarks/bm-panel.js)
-   content/communicator/bookmarks/bm-panel.xul                      (bookmarks/bm-panel.xul)
-   content/communicator/bookmarks/bm-props.js                       (bookmarks/bm-props.js)
-   content/communicator/bookmarks/bm-props.xul                      (bookmarks/bm-props.xul)
-# Provide another URI for the bookmark properties dialog so we can persist the attributes separately
-% override chrome://communicator/content/bookmarks/bm-props2.xul chrome://communicator/content/bookmarks/bm-props.xul
-   content/communicator/bookmarks/bookmarksManager.css              (bookmarks/bookmarksManager.css)
-   content/communicator/bookmarks/bookmarksManager.js               (bookmarks/bookmarksManager.js)
-   content/communicator/bookmarks/bookmarksManager.xul              (bookmarks/bookmarksManager.xul)
-   content/communicator/bookmarks/browser-places.js                 (bookmarks/browser-places.js)
-   content/communicator/bookmarks/editBookmarkOverlay.js            (bookmarks/editBookmarkOverlay.js)
-   content/communicator/bookmarks/editBookmarkOverlay.xul           (bookmarks/editBookmarkOverlay.xul)
-   content/communicator/bookmarks/moveBookmarks.js                  (bookmarks/moveBookmarks.js)
-   content/communicator/bookmarks/moveBookmarks.xul                 (bookmarks/moveBookmarks.xul)
-   content/communicator/bookmarks/placesOverlay.xul                 (bookmarks/placesOverlay.xul)
    content/communicator/console/consoleBindings.xml                 (console/consoleBindings.xml)
    content/communicator/console/console.css                         (console/console.css)
    content/communicator/console/console.js                          (console/console.js)
    content/communicator/console/console.xul                         (console/console.xul)
    content/communicator/dataman/dataman.css                         (dataman/dataman.css)
    content/communicator/dataman/dataman.js                          (dataman/dataman.js)
    content/communicator/dataman/dataman.xml                         (dataman/dataman.xml)
    content/communicator/dataman/dataman.xul                         (dataman/dataman.xul)
@@ -153,36 +138,21 @@ comm.jar:
    content/communicator/downloads/progressDialog.xul                (downloads/progressDialog.xul)
    content/communicator/downloads/progressDialog.js                 (downloads/progressDialog.js)
    content/communicator/downloads/uploadProgress.xul                (downloads/uploadProgress.xul)
    content/communicator/downloads/uploadProgress.js                 (downloads/uploadProgress.js)
    content/communicator/downloads/treeView.js                       (downloads/treeView.js)
    content/communicator/feeds/subscribe.css                         (feeds/subscribe.css)
    content/communicator/feeds/subscribe.xhtml                       (feeds/subscribe.xhtml)
    content/communicator/feeds/subscribe.xml                         (feeds/subscribe.xml)
-   content/communicator/history/controller.js                       (history/controller.js)
-   content/communicator/history/history.js                          (history/history.js)
-   content/communicator/history/history.xul                         (history/history.xul)
-   content/communicator/history/history-panel.xul                   (history/history-panel.xul)
-   content/communicator/history/places.css                          (history/places.css)
-   content/communicator/history/placesOverlay.xul                   (history/placesOverlay.xul)
-   content/communicator/history/tree.xml                            (history/tree.xml)
-   content/communicator/history/treeView.js                         (history/treeView.js)
    content/communicator/permissions/cookieViewer.js                 (permissions/cookieViewer.js)
    content/communicator/permissions/cookieViewer.xul                (permissions/cookieViewer.xul)
    content/communicator/permissions/permissionsManager.js           (permissions/permissionsManager.js)
    content/communicator/permissions/permissionsManager.xul          (permissions/permissionsManager.xul)
    content/communicator/permissions/treeUtils.js                    (permissions/treeUtils.js)
-*  content/communicator/places/browserPlacesViews.js                (places/browserPlacesViews.js)
-   content/communicator/places/controller.js                        (places/controller.js)
-*  content/communicator/places/menu.xml                             (places/menu.xml)
-   content/communicator/places/places.css                           (places/places.css)
-   content/communicator/places/sidebarUtils.js                      (places/sidebarUtils.js)
-   content/communicator/places/tree.xml                             (places/tree.xml)
-   content/communicator/places/treeView.js                          (places/treeView.js)
    content/communicator/pref/preferences.xul                        (pref/preferences.xul)
    content/communicator/pref/preferences.js                         (pref/preferences.js)
    content/communicator/pref/prefpanels.css                         (pref/prefpanels.css)
    content/communicator/pref/prefpanels.xml                         (pref/prefpanels.xml)
    content/communicator/pref/pref-advanced.js                       (pref/pref-advanced.js)
    content/communicator/pref/pref-advanced.xul                      (pref/pref-advanced.xul)
    content/communicator/pref/pref-appearance.js                     (pref/pref-appearance.js)
    content/communicator/pref/pref-appearance.xul                    (pref/pref-appearance.xul)
--- a/suite/common/mac/platformCommunicatorOverlay.xul
+++ b/suite/common/mac/platformCommunicatorOverlay.xul
@@ -18,17 +18,20 @@
 
 <overlay id="platformCommunicatorOverlay"
      xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <!-- close -->
   <menuitem id="menu_close" label="&closeCmd.label;" key="key_close" command="cmd_close"/>
   <key id="key_close"  key="&closeCmd.key;" command="cmd_close" modifiers="accel"/>
   <key id="key_closeWindow"  key="&closeCmd.key;" command="cmd_closeWindow" modifiers="accel,shift"/>
-  <key id="key_gotoHistory"  key="&historyCmd.key;" oncommand="toHistory();" modifiers="accel,shift"/>
+  <key id="key_gotoHistory"
+       key="&historyCmd.key;"
+       oncommand="PlacesCommandHook.showPlacesOrganizer('History');"
+       modifiers="accel,shift"/>
 
   <menuitem id="menu_preferences" label="&preferencesCmd.label;"
             key="key_preferences"/>
 
   <menupopup id="menu_FilePopup">
     <!-- These 5 menuitems (and the menu_preferences menuitem above) are added
       to the Application menu. They just need to be in the DOM, widget code
       does the rest -->
--- a/suite/common/moz.build
+++ b/suite/common/moz.build
@@ -1,43 +1,31 @@
 # 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/.
 
 DIRS += [
     'console',
+    'places',
     'public',
     'src',
 ]
 
-EXTRA_COMPONENTS += [
-    'places/nsPlacesAutoComplete.js',
-    'places/nsPlacesAutoComplete.manifest',
-]
-
 BROWSER_CHROME_MANIFESTS += [
     'dataman/tests/browser.ini',
     'downloads/tests/browser/browser.ini',
-    'places/tests/browser/browser.ini',
-    'tests/browser/browser.ini',
     'tests/preferences/browser.ini',
 ]
 
 MOCHITEST_CHROME_MANIFESTS += [
     'downloads/tests/chrome/chrome.ini',
-    'places/tests/chrome/chrome.ini',
     'tests/chrome/chrome.ini',
 ]
 
-XPCSHELL_TESTS_MANIFESTS += [
-    'places/tests/autocomplete/xpcshell.ini',
-    'places/tests/unit/xpcshell.ini',
-]
-
 JAR_MANIFESTS += ['jar.mn']
 
 # DEFINES for preprocessing
 # Use suite/common/app-license.html as input when generating 
 # chrome://content/communicator/license.html to override 
 # chrome://global/content/license.html (about:license)
 DEFINES['APP_LICENSE_BLOCK'] = '%s/app-license.html' % SRCDIR
 
--- a/suite/common/places/PlacesUIUtils.jsm
+++ b/suite/common/places/PlacesUIUtils.jsm
@@ -1,227 +1,460 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* 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/. */
 
-var EXPORTED_SYMBOLS = ["PlacesUIUtils"];
+this.EXPORTED_SYMBOLS = ["PlacesUIUtils"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/Timer.jsm");
+// PlacesUtils exposes multiple symbols, so we can't use defineModuleGetter.
+ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() {
-  ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
-  return PlacesUtils;
-});
+ChromeUtils.defineModuleGetter(this, "PluralForm",
+                               "resource://gre/modules/PluralForm.jsm");
+ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
+                               "resource://gre/modules/PrivateBrowsingUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "NetUtil",
+                               "resource://gre/modules/NetUtil.jsm");
+ChromeUtils.defineModuleGetter(this, "RecentWindow",
+                               "resource:///modules/RecentWindow.jsm");
+ChromeUtils.defineModuleGetter(this, "PlacesTransactions",
+                               "resource://gre/modules/PlacesTransactions.jsm");
+
+ChromeUtils.defineModuleGetter(this, "Weave",
+                               "resource://services-sync/main.js");
+
+const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+const FAVICON_REQUEST_TIMEOUT = 60 * 1000;
+// Map from windows to arrays of data about pending favicon loads.
+let gFaviconLoadDataMap = new Map();
+
+// copied from utilityOverlay.js
+const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
 
 // This function isn't public both because it's synchronous and because it is
 // going to be removed in bug 1072833.
 function IsLivemark(aItemId) {
   // Since this check may be done on each dragover event, it's worth maintaining
   // a cache.
   let self = IsLivemark;
   if (!("ids" in self)) {
     const LIVEMARK_ANNO = PlacesUtils.LMANNO_FEEDURI;
 
     let idsVec = PlacesUtils.annotations.getItemsWithAnnotation(LIVEMARK_ANNO);
     self.ids = new Set(idsVec);
 
-    let obs = {
+    let obs = Object.freeze({
       QueryInterface: XPCOMUtils.generateQI(Ci.nsIAnnotationObserver),
 
       onItemAnnotationSet(itemId, annoName) {
         if (annoName == LIVEMARK_ANNO)
           self.ids.add(itemId);
       },
 
       onItemAnnotationRemoved(itemId, annoName) {
         // If annoName is set to an empty string, the item is gone.
         if (annoName == LIVEMARK_ANNO || annoName == "")
           self.ids.delete(itemId);
       },
 
       onPageAnnotationSet() { },
       onPageAnnotationRemoved() { },
-    };
+    });
     PlacesUtils.annotations.addObserver(obs);
     PlacesUtils.registerShutdownFunction(() => {
       PlacesUtils.annotations.removeObserver(obs);
     });
   }
   return self.ids.has(aItemId);
 }
 
-var PlacesUIUtils = {
-  ORGANIZER_LEFTPANE_VERSION: 6,
+let InternalFaviconLoader = {
+  /**
+   * This gets called for every inner window that is destroyed.
+   * In the parent process, we process the destruction ourselves. In the child process,
+   * we notify the parent which will then process it based on that message.
+   */
+  observe(subject, topic, data) {
+    let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+    this.removeRequestsForInner(innerWindowID);
+  },
+
+  /**
+   * Actually cancel the request, and clear the timeout for cancelling it.
+   */
+  _cancelRequest({uri, innerWindowID, timerID, callback}, reason) {
+    // Break cycle
+    let request = callback.request;
+    delete callback.request;
+    // Ensure we don't time out.
+    clearTimeout(timerID);
+    try {
+      request.cancel();
+    } catch (ex) {
+      Cu.reportError("When cancelling a request for " + uri.spec + " because " + reason + ", it was already canceled!");
+    }
+  },
+
+  /**
+   * Called for every inner that gets destroyed, only in the parent process.
+   */
+  removeRequestsForInner(innerID) {
+    for (let [window, loadDataForWindow] of gFaviconLoadDataMap) {
+      let newLoadDataForWindow = loadDataForWindow.filter(loadData => {
+        let innerWasDestroyed = loadData.innerWindowID == innerID;
+        if (innerWasDestroyed) {
+          this._cancelRequest(loadData, "the inner window was destroyed or a new favicon was loaded for it");
+        }
+        // Keep the items whose inner is still alive.
+        return !innerWasDestroyed;
+      });
+      // Map iteration with for...of is safe against modification, so
+      // now just replace the old value:
+      gFaviconLoadDataMap.set(window, newLoadDataForWindow);
+    }
+  },
+
+  /**
+   * Called when a toplevel chrome window unloads. We use this to tidy up after ourselves,
+   * avoid leaks, and cancel any remaining requests. The last part should in theory be
+   * handled by the inner-window-destroyed handlers. We clean up just to be on the safe side.
+   */
+  onUnload(win) {
+    let loadDataForWindow = gFaviconLoadDataMap.get(win);
+    if (loadDataForWindow) {
+      for (let loadData of loadDataForWindow) {
+        this._cancelRequest(loadData, "the chrome window went away");
+      }
+    }
+    gFaviconLoadDataMap.delete(win);
+  },
+
+  /**
+   * Remove a particular favicon load's loading data from our map tracking
+   * load data per chrome window.
+   *
+   * @param win
+   *        the chrome window in which we should look for this load
+   * @param filterData ({innerWindowID, uri, callback})
+   *        the data we should use to find this particular load to remove.
+   *
+   * @return the loadData object we removed, or null if we didn't find any.
+   */
+  _removeLoadDataFromWindowMap(win, {innerWindowID, uri, callback}) {
+    let loadDataForWindow = gFaviconLoadDataMap.get(win);
+    if (loadDataForWindow) {
+      let itemIndex = loadDataForWindow.findIndex(loadData => {
+        return loadData.innerWindowID == innerWindowID &&
+               loadData.uri.equals(uri) &&
+               loadData.callback.request == callback.request;
+      });
+      if (itemIndex != -1) {
+        let loadData = loadDataForWindow[itemIndex];
+        loadDataForWindow.splice(itemIndex, 1);
+        return loadData;
+      }
+    }
+    return null;
+  },
+
+  /**
+   * Create a function to use as a nsIFaviconDataCallback, so we can remove cancelling
+   * information when the request succeeds. Note that right now there are some edge-cases,
+   * such as about: URIs with chrome:// favicons where the success callback is not invoked.
+   * This is OK: we will 'cancel' the request after the timeout (or when the window goes
+   * away) but that will be a no-op in such cases.
+   */
+  _makeCompletionCallback(win, id) {
+    return {
+      onComplete(uri) {
+        let loadData = InternalFaviconLoader._removeLoadDataFromWindowMap(win, {
+          uri,
+          innerWindowID: id,
+          callback: this,
+        });
+        if (loadData) {
+          clearTimeout(loadData.timerID);
+        }
+        delete this.request;
+      },
+    };
+  },
+
+  ensureInitialized() {
+    if (this._initialized) {
+      return;
+    }
+    this._initialized = true;
+
+    Services.obs.addObserver(this, "inner-window-destroyed");
+    Services.ppmm.addMessageListener("Toolkit:inner-window-destroyed", msg => {
+      this.removeRequestsForInner(msg.data);
+    });
+  },
+
+  loadFavicon(browser, principal, uri) {
+    this.ensureInitialized();
+    let win = browser.ownerGlobal;
+    if (!gFaviconLoadDataMap.has(win)) {
+      gFaviconLoadDataMap.set(win, []);
+      let unloadHandler = event => {
+        let doc = event.target;
+        let eventWin = doc.defaultView;
+        if (eventWin == win) {
+          win.removeEventListener("unload", unloadHandler);
+          this.onUnload(win);
+        }
+      };
+      win.addEventListener("unload", unloadHandler, true);
+    }
+
+    let {innerWindowID, currentURI} = browser;
+
+    // Immediately cancel any earlier requests
+    this.removeRequestsForInner(innerWindowID);
+
+    // First we do the actual setAndFetch call:
+    let loadType = PrivateBrowsingUtils.isWindowPrivate(win)
+      ? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE
+      : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE;
+    let callback = this._makeCompletionCallback(win, innerWindowID);
+    let request = PlacesUtils.favicons.setAndFetchFaviconForPage(currentURI, uri, false,
+                                                                 loadType, callback, principal);
+
+    // Now register the result so we can cancel it if/when necessary.
+    if (!request) {
+      // The favicon service can return with success but no-op (and leave request
+      // as null) if the icon is the same as the page (e.g. for images) or if it is
+      // the favicon for an error page. In this case, we do not need to do anything else.
+      return;
+    }
+    callback.request = request;
+    let loadData = {innerWindowID, uri, callback};
+    loadData.timerID = setTimeout(() => {
+      this._cancelRequest(loadData, "it timed out");
+      this._removeLoadDataFromWindowMap(win, loadData);
+    }, FAVICON_REQUEST_TIMEOUT);
+    let loadDataForWindow = gFaviconLoadDataMap.get(win);
+    loadDataForWindow.push(loadData);
+  },
+};
+
+this.PlacesUIUtils = {
+  ORGANIZER_LEFTPANE_VERSION: 7,
   ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder",
   ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery",
 
   LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar",
   DESCRIPTION_ANNO: "bookmarkProperties/description",
 
-  TYPE_TAB_DROP: "application/x-moz-tabbrowser-tab",
-
   /**
    * Makes a URI from a spec, and do fixup
    * @param   aSpec
    *          The string spec of the URI
-   * @returns A URI object for the spec.
+   * @return A URI object for the spec.
    */
   createFixedURI: function PUIU_createFixedURI(aSpec) {
     return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
   },
 
-  /**
-   * Wraps a string in a nsISupportsString wrapper
-   * @param   aString
-   *          The string to wrap
-   * @returns A nsISupportsString object containing a string.
-   */
-  _wrapString: function PUIU__wrapString(aString) {
-    var s = Cc["@mozilla.org/supports-string;1"]
-              .createInstance(Ci.nsISupportsString);
-    s.data = aString;
-    return s;
-  },
-
   getFormattedString: function PUIU_getFormattedString(key, params) {
     return bundle.formatStringFromName(key, params, params.length);
   },
 
+  /**
+   * Get a localized plural string for the specified key name and numeric value
+   * substituting parameters.
+   *
+   * @param   aKey
+   *          String, key for looking up the localized string in the bundle
+   * @param   aNumber
+   *          Number based on which the final localized form is looked up
+   * @param   aParams
+   *          Array whose items will substitute #1, #2,... #n parameters
+   *          in the string.
+   *
+   * @see https://developer.mozilla.org/en/Localization_and_Plurals
+   * @return The localized plural string.
+   */
+  getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) {
+    let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey));
+
+    // Replace #1 with aParams[0], #2 with aParams[1], and so on.
+    return str.replace(/\#(\d+)/g, function(matchedId, matchedNumber) {
+      let param = aParams[parseInt(matchedNumber, 10) - 1];
+      return param !== undefined ? param : matchedId;
+    });
+  },
+
   getString: function PUIU_getString(key) {
     return bundle.GetStringFromName(key);
   },
 
+  get _copyableAnnotations() {
+    return [
+      this.DESCRIPTION_ANNO,
+      this.LOAD_IN_SIDEBAR_ANNO,
+      PlacesUtils.READ_ONLY_ANNO,
+    ];
+  },
+
   /**
-   * Get a transaction for copying a uri item (either a bookmark or a
-   * history entry) from one container to another.
+   * Get a transaction for copying a uri item (either a bookmark or a history
+   * entry) from one container to another.
    *
    * @param   aData
    *          JSON object of dropped or pasted item properties
    * @param   aContainer
    *          The container being copied into
    * @param   aIndex
    *          The index within the container the item is copied to
-   * @return  A nsITransaction object that performs the copy.
+   * @return A nsITransaction object that performs the copy.
    *
    * @note Since a copy creates a completely new item, only some internal
    *       annotations are synced from the old one.
    * @see this._copyableAnnotations for the list of copyable annotations.
    */
-   _getURIItemCopyTransaction:
-   function PUIU__getURIItemCopyTransaction(aData, aContainer, aIndex)
-   {
-     let transactions = [];
-     if (aData.dateAdded)
-       transactions.push(
-         new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
-       );
+  _getURIItemCopyTransaction:
+  function PUIU__getURIItemCopyTransaction(aData, aContainer, aIndex) {
+    let transactions = [];
+    if (aData.dateAdded) {
+      transactions.push(
+        new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
+      );
+    }
+    if (aData.lastModified) {
+      transactions.push(
+        new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
+      );
+    }
 
-     if (aData.lastModified)
-       transactions.push(
-         new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
-       );
+    let annos = [];
+    if (aData.annos) {
+      annos = aData.annos.filter(function(aAnno) {
+        return this._copyableAnnotations.includes(aAnno.name);
+      }, this);
+    }
 
-     let keyword = aData.keyword || null;
-     let annos = [];
-     if (aData.annos) {
-       annos = aData.annos.filter(function (aAnno) {
-         return this._copyableAnnotations.indexOf(aAnno.name) != -1;
-       }, this);
-     }
-
-     return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri),
-                                                aContainer, aIndex, aData.title,
-                                                keyword, annos, transactions);
+    // There's no need to copy the keyword since it's bound to the bookmark url.
+    return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri),
+                                               aContainer, aIndex, aData.title,
+                                               null, annos, transactions);
   },
 
   /**
    * Gets a transaction for copying (recursively nesting to include children)
    * a folder (or container) and its contents from one folder to another.
    *
    * @param   aData
    *          Unwrapped dropped folder data - Obj containing folder and children
    * @param   aContainer
    *          The container we are copying into
    * @param   aIndex
    *          The index in the destination container to insert the new items
-   * @return  A nsITransaction object that will perform the copy.
+   * @return A nsITransaction object that will perform the copy.
    *
    * @note Since a copy creates a completely new item, only some internal
    *       annotations are synced from the old one.
    * @see this._copyableAnnotations for the list of copyable annotations.
    */
-  _getFolderCopyTransaction:
-  function PUIU__getFolderCopyTransaction(aData, aContainer, aIndex) {
-    function getChildItemsTransactions(aChildren) {
+  _getFolderCopyTransaction(aData, aContainer, aIndex) {
+    function getChildItemsTransactions(aRoot) {
       let transactions = [];
       let index = aIndex;
-      aChildren.forEach(function (node, i) {
+      for (let i = 0; i < aRoot.childCount; ++i) {
+        let child = aRoot.getChild(i);
+        // Temporary hacks until we switch to PlacesTransactions.jsm.
+        let isLivemark =
+          PlacesUtils.annotations.itemHasAnnotation(child.itemId,
+                                                    PlacesUtils.LMANNO_FEEDURI);
+        let [node] = PlacesUtils.unwrapNodes(
+          PlacesUtils.wrapNode(child, PlacesUtils.TYPE_X_MOZ_PLACE, isLivemark),
+          PlacesUtils.TYPE_X_MOZ_PLACE
+        );
+
         // Make sure that items are given the correct index, this will be
         // passed by the transaction manager to the backend for the insertion.
         // Insertion behaves differently for DEFAULT_INDEX (append).
-        if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX)
+        if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX) {
           index = i;
+        }
 
-        if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
-          transactions.push(
-            PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index)
-          );
-        else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR)
+        if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
+          if (node.livemark && node.annos) {
+            transactions.push(
+              PlacesUIUtils._getLivemarkCopyTransaction(node, aContainer, index)
+            );
+          } else {
+            transactions.push(
+              PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index)
+            );
+          }
+        } else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
           transactions.push(new PlacesCreateSeparatorTransaction(-1, index));
-        else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE)
+        } else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) {
           transactions.push(
             PlacesUIUtils._getURIItemCopyTransaction(node, -1, index)
           );
-        else
+        } else {
           throw new Error("Unexpected item under a bookmarks folder");
-      });
+        }
+      }
       return transactions;
     }
 
-    if (aContainer == PlacesUtils.tagsFolderId) { // Copying a tag folder.
+    if (aContainer == PlacesUtils.tagsFolderId) { // Copying into a tag folder.
       let transactions = [];
-      if (aData.children) {
-        aData.children.forEach(function(aChild) {
+      if (!aData.livemark && aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
+        let {root} = PlacesUtils.getFolderContents(aData.id, false, false);
+        let urls = PlacesUtils.getURLsForContainerNode(root);
+        root.containerOpen = false;
+        for (let { uri } of urls) {
           transactions.push(
-            new PlacesTagURITransaction(PlacesUtils._uri(aChild.uri),
-                                        [aData.title])
+            new PlacesTagURITransaction(NetUtil.newURI(uri), [aData.title])
           );
-        });
+        }
       }
       return new PlacesAggregatedTransaction("addTags", transactions);
     }
 
-    if (aData.livemark && aData.annos) // Copying a livemark.
+    if (aData.livemark && aData.annos) { // Copying a livemark.
       return this._getLivemarkCopyTransaction(aData, aContainer, aIndex);
+    }
 
-    let transactions = getChildItemsTransactions(aData.children);
-    if (aData.dateAdded)
+    let {root} = PlacesUtils.getFolderContents(aData.id, false, false);
+    let transactions = getChildItemsTransactions(root);
+    root.containerOpen = false;
+
+    if (aData.dateAdded) {
       transactions.push(
         new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
       );
-    if (aData.lastModified)
+    }
+    if (aData.lastModified) {
       transactions.push(
         new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
-    );
+      );
+    }
 
     let annos = [];
     if (aData.annos) {
-      annos = aData.annos.filter(function (aAnno) {
-        return this._copyableAnnotations.indexOf(aAnno.name) != -1;
+      annos = aData.annos.filter(function(aAnno) {
+        return this._copyableAnnotations.includes(aAnno.name);
       }, this);
     }
 
-    return new PlacesCreateFolderTransaction(aData.title, aContainer,
-                                             aIndex, annos,
-                                             transactions);
+    return new PlacesCreateFolderTransaction(aData.title, aContainer, aIndex,
+                                             annos, transactions);
   },
 
-   /**
+  /**
    * Gets a transaction for copying a live bookmark item from one container to
    * another.
    *
    * @param   aData
    *          Unwrapped live bookmarkmark data
    * @param   aContainer
    *          The container we are copying into
    * @param   aIndex
@@ -229,30 +462,30 @@ var PlacesUIUtils = {
    * @return A nsITransaction object that will perform the copy.
    *
    * @note Since a copy creates a completely new item, only some internal
    *       annotations are synced from the old one.
    * @see this._copyableAnnotations for the list of copyable annotations.
    */
   _getLivemarkCopyTransaction:
   function PUIU__getLivemarkCopyTransaction(aData, aContainer, aIndex) {
-    if (!aData.livemark || !aData.annos)
-      throw("node is not a livemark");
+    if (!aData.livemark || !aData.annos) {
+      throw new Error("node is not a livemark");
+    }
 
-    let feedURI = null;
-    let siteURI = null;
+    let feedURI, siteURI;
+    let annos = [];
     if (aData.annos) {
       annos = aData.annos.filter(function(aAnno) {
         if (aAnno.name == PlacesUtils.LMANNO_FEEDURI) {
           feedURI = PlacesUtils._uri(aAnno.value);
-        }
-        else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) {
+        } else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) {
           siteURI = PlacesUtils._uri(aAnno.value);
         }
-        return this._copyableAnnotations.indexOf(aAnno.name) != -1
+        return this._copyableAnnotations.includes(aAnno.name)
       }, this);
     }
 
     return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title,
                                                aContainer, aIndex, annos);
   },
 
   /**
@@ -263,418 +496,175 @@ var PlacesUIUtils = {
    * @param   type
    *          The content type of the data
    * @param   container
    *          The container the data was dropped or pasted into
    * @param   index
    *          The index within the container the item was dropped or pasted at
    * @param   copy
    *          The drag action was copy, so don't move folders or links.
-   * @returns An object implementing nsITransaction that can perform
-   *          the move/insert.
+   * @return An object implementing nsITransaction that can perform
+   *         the move/insert.
    */
-  makeTransaction: function PUIU_makeTransaction(data, type, container,
-                                                 index, copy) {
+  makeTransaction:
+  function PUIU_makeTransaction(data, type, container, index, copy) {
     switch (data.type) {
       case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
-        if (copy)
+        if (copy) {
           return this._getFolderCopyTransaction(data, container, index);
-        // Otherwise move the item.
-        return new PlacesMoveItemTransaction(data.id, container, index);
-        break;
-      case PlacesUtils.TYPE_X_MOZ_PLACE:
-        if (copy || data.id == -1) // id is -1 if the place is not bookmarked
-          return this._getURIItemCopyTransaction(data, container, index);
+        }
 
         // Otherwise move the item.
         return new PlacesMoveItemTransaction(data.id, container, index);
-        break;
+      case PlacesUtils.TYPE_X_MOZ_PLACE:
+        if (copy || data.id == -1) { // Id is -1 if the place is not bookmarked.
+          return this._getURIItemCopyTransaction(data, container, index);
+        }
+
+        // Otherwise move the item.
+        return new PlacesMoveItemTransaction(data.id, container, index);
       case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
         if (copy) {
           // There is no data in a separator, so copying it just amounts to
           // inserting a new separator.
           return new PlacesCreateSeparatorTransaction(container, index);
         }
 
+        // Otherwise move the item.
         return new PlacesMoveItemTransaction(data.id, container, index);
-        break;
       default:
         if (type == PlacesUtils.TYPE_X_MOZ_URL ||
             type == PlacesUtils.TYPE_UNICODE ||
-            type == this.TYPE_TAB_DROP) {
-          let title = (type != PlacesUtils.TYPE_UNICODE) ? data.title :
-                                                             data.uri;
+            type == TAB_DROP_TYPE) {
+          let title = type != PlacesUtils.TYPE_UNICODE ? data.title
+                                                       : data.uri;
           return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri),
                                                      container, index, title);
         }
     }
     return null;
   },
 
   /**
-   * Methods to show the bookmarkProperties dialog in its various modes.
+   * ********* PlacesTransactions version of the function defined above ********
    *
-   * The showMinimalAdd* methods open the dialog by its alternative URI. Thus
-   * they persist the dialog dimensions separately from the showAdd* methods.
-   * Note these variants also do not return the dialog "performed" state since
-   * they may not open the dialog modally.
-   */
-
-  /**
-   * Shows the "Add Bookmark" dialog.
+   * Constructs a Places Transaction for the drop or paste of a blob of data
+   * into a container.
    *
-   * @param [optional] aURI
-   *        An nsIURI object for which the "add bookmark" dialog is
-   *        to be shown.
-   * @param [optional] aTitle
-   *        The default title for the new bookmark.
-   * @param [optional] aDescription
-            The default description for the new bookmark
-   * @param [optional] aDefaultInsertionPoint
-   *        The default insertion point for the new item. If set, the folder
-   *        picker would be hidden unless aShowPicker is set to true, in which
-   *        case the dialog only uses the folder identifier from the insertion
-   *        point as the initially selected item in the folder picker.
-   * @param [optional] aShowPicker
-   *        see above
-   * @param [optional] aKeyword
-   *        The default keyword for the new bookmark. The keyword field
-   *        will be shown in the dialog if this is used.
-   * @param [optional] aPostData
-   *        POST data for POST-style keywords.
-   * @param [optional] aCharSet
-   *        The character set for the bookmarked page.
-   * @param [optional] aHiddenRows
-   *        An array of rows to hide that is passed through to the
-            bookmark properties dialog.
-   * @return true if any transaction has been performed.
+   * @param   aData
+   *          The unwrapped data blob of dropped or pasted data.
+   * @param   aType
+   *          The content type of the data.
+   * @param   aNewParentGuid
+   *          GUID of the container the data was dropped or pasted into.
+   * @param   aIndex
+   *          The index within the container the item was dropped or pasted at.
+   * @param   aCopy
+   *          The drag action was copy, so don't move folders or links.
    *
-   * Notes:
-   *  - the location and description fields are
-   *    visible only if there is no initial URI (aURI is null).
-   *  - When aDefaultInsertionPoint is not set, the dialog defaults to the
-   *    bookmarks root folder.
+   * @return  a Places Transaction that can be transacted for performing the
+   *          move/insert command.
    */
-  showAddBookmarkUI: function PUIU_showAddBookmarkUI(aURI,
-                                                     aTitle,
-                                                     aDescription,
-                                                     aDefaultInsertionPoint,
-                                                     aShowPicker,
-                                                     aLoadInSidebar,
-                                                     aKeyword,
-                                                     aPostData,
-                                                     aCharSet,
-                                                     aHiddenRows) {
-    var info = {
-      action: "add",
-      type: "bookmark"
-    };
+  getTransactionForData(aData, aType, aNewParentGuid, aIndex, aCopy) {
+    if (!this.SUPPORTED_FLAVORS.includes(aData.type))
+      throw new Error(`Unsupported '${aData.type}' data type`);
+
+    if ("itemGuid" in aData) {
+      if (!this.PLACES_FLAVORS.includes(aData.type))
+        throw new Error(`itemGuid unexpectedly set on ${aData.type} data`);
 
-    if (aURI)
-      info.uri = aURI;
-
-    // allow default empty title
-    if (typeof(aTitle) == "string")
-      info.title = aTitle;
-
-    if (aDescription)
-      info.description = aDescription;
-
-    info.hiddenRows = aHiddenRows || [];
-
-    if (aDefaultInsertionPoint) {
-      info.defaultInsertionPoint = aDefaultInsertionPoint;
-      if (!aShowPicker)
-        info.hiddenRows.push("folderPicker");
+      let info = { guid: aData.itemGuid,
+                   newParentGuid: aNewParentGuid,
+                   newIndex: aIndex };
+      if (aCopy) {
+        info.excludingAnnotation = "Places/SmartBookmark";
+        return PlacesTransactions.Copy(info);
+      }
+      return PlacesTransactions.Move(info);
     }
 
-    if (typeof(aKeyword) == "string") {
-      info.keyword = aKeyword;
-      if (typeof(aPostData) == "string")
-        info.postData = aPostData;
-      if (typeof(aCharSet) == "string")
-        info.charSet = aCharSet;
+    // Since it's cheap and harmless, we allow the paste of separators and
+    // bookmarks from builds that use legacy transactions (i.e. when itemGuid
+    // was not set on PLACES_FLAVORS data). Containers are a different story,
+    // and thus disallowed.
+    if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
+      throw new Error("Can't copy a container from a legacy-transactions build");
+
+    if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
+      return PlacesTransactions.NewSeparator({ parentGuid: aNewParentGuid,
+                                               index: aIndex });
     }
 
-    return this._showBookmarkDialog(info);
-  },
-
-  /**
-   * @see showAddBookmarkUI
-   * This opens the dialog with only the name and folder pickers visible by
-   * default.
-   *
-   * You can still pass in the various paramaters as the default properties
-   * for the new bookmark.
-   *
-   * The keyword field will be visible only if the aKeyword parameter
-   * was used.
-   */
-  showMinimalAddBookmarkUI:
-  function PUIU_showMinimalAddBookmarkUI(aURI, aTitle, aDescription,
-                                         aDefaultInsertionPoint, aShowPicker,
-                                         aLoadInSidebar, aKeyword, aPostData,
-                                         aCharSet) {
-    var info = {
-      action: "add",
-      type: "bookmark",
-      hiddenRows: ["description"]
-    };
-    if (aURI)
-      info.uri = aURI;
-
-    // allow default empty title
-    if (typeof(aTitle) == "string")
-      info.title = aTitle;
-
-    if (aDescription)
-      info.description = aDescription;
-
-    if (aDefaultInsertionPoint) {
-      info.defaultInsertionPoint = aDefaultInsertionPoint;
-      if (!aShowPicker)
-        info.hiddenRows.push("folderPicker");
-    }
-
-    info.hiddenRows = info.hiddenRows.concat(["location"]);
-
-    if (typeof(aKeyword) == "string") {
-      info.keyword = aKeyword;
-      // Hide the Tags field if we are adding a keyword.
-      info.hiddenRows.push("tags");
-      // Keyword related params.
-      if (typeof(aPostData) == "string")
-        info.postData = aPostData;
-      if (typeof(aCharSet) == "string")
-        info.charSet = aCharSet;
-    }
-    else
-      info.hiddenRows.push("keyword");
-
-    return this._showBookmarkDialog(info);
+    let title = aData.type != PlacesUtils.TYPE_UNICODE ? aData.title
+                                                       : aData.uri;
+    return PlacesTransactions.NewBookmark({ url: Services.io.newURI(aData.uri),
+                                            title,
+                                            parentGuid: aNewParentGuid,
+                                            index: aIndex });
   },
 
   /**
-   * Shows the "Add Live Bookmark" dialog.
-   *
-   * @param [optional] aFeedURI
-   *        The feed URI for which the dialog is to be shown (nsIURI).
-   * @param [optional] aSiteURI
-   *        The site URI for the new live-bookmark (nsIURI).
-   * @param [optional] aDefaultInsertionPoint
-   *        The default insertion point for the new item. If set, the folder
-   *        picker would be hidden unless aShowPicker is set to true, in which
-   *        case the dialog only uses the folder identifier from the insertion
-   *        point as the initially selected item in the folder picker.
-   * @param [optional] aShowPicker
-   *        see above
-   * @return true if any transaction has been performed.
-   *
-   * Notes:
-   *  - the feedURI and description fields are visible only if there is no
-   *    initial feed URI (aFeedURI is null).
-   *  - When aDefaultInsertionPoint is not set, the dialog defaults to the
-   *    bookmarks root folder.
-   */
-  showAddLivemarkUI: function PUIU_showAddLivemarkURI(aFeedURI,
-                                                      aSiteURI,
-                                                      aTitle,
-                                                      aDescription,
-                                                      aDefaultInsertionPoint,
-                                                      aShowPicker) {
-    var info = {
-      action: "add",
-      type: "livemark"
-    };
-
-    if (aFeedURI)
-      info.feedURI = aFeedURI;
-    if (aSiteURI)
-      info.siteURI = aSiteURI;
-
-    // allow default empty title
-    if (typeof(aTitle) == "string")
-      info.title = aTitle;
-
-    if (aDescription)
-      info.description = aDescription;
-
-    if (aDefaultInsertionPoint) {
-      info.defaultInsertionPoint = aDefaultInsertionPoint;
-      if (!aShowPicker)
-        info.hiddenRows = ["folderPicker"];
-    }
-    return this._showBookmarkDialog(info);
-  },
-
-  /**
-   * @see showAddLivemarkUI
-   * This opens the dialog with only the name and folder pickers visible by
-   * default.
-   *
-   * You can still pass in the various paramaters as the default properties
-   * for the new live-bookmark.
-   */
-  showMinimalAddLivemarkUI:
-  function PUIU_showMinimalAddLivemarkURI(aFeedURI, aSiteURI, aTitle,
-                                          aDescription, aDefaultInsertionPoint,
-                                          aShowPicker) {
-    var info = {
-      action: "add",
-      type: "livemark",
-      hiddenRows: ["feedLocation", "siteLocation", "description"]
-    };
-
-    if (aFeedURI)
-      info.feedURI = aFeedURI;
-    if (aSiteURI)
-      info.siteURI = aSiteURI;
-
-    // allow default empty title
-    if (typeof(aTitle) == "string")
-      info.title = aTitle;
-
-    if (aDescription)
-      info.description = aDescription;
-
-    if (aDefaultInsertionPoint) {
-      info.defaultInsertionPoint = aDefaultInsertionPoint;
-      if (!aShowPicker)
-        info.hiddenRows.push("folderPicker");
-    }
-    return this._showBookmarkDialog(info);
-  },
-
-  /**
-   * Show an "Add Bookmarks" dialog to allow the adding of a folder full
-   * of bookmarks corresponding to the objects in the uriList.  This will
-   * be called most often as the result of a "Bookmark All Tabs..." command.
-   *
-   * @param aURIList  List of nsIURI objects representing the locations
-   *                  to be bookmarked.
-   * @param aTitleList  Optional list of strings giving the page titles.
-   * @return true if any transaction has been performed.
-   */
-  showMinimalAddMultiBookmarkUI: function PUIU_showAddMultiBookmarkUI(aURIList, aTitleList) {
-    if (aURIList.length == 0)
-      throw("showAddMultiBookmarkUI expects a list of nsIURI objects");
-    var info = {
-      action: "add",
-      type: "folder",
-      hiddenRows: ["description"],
-      URIList: aURIList
-    };
-
-    if (aTitleList)
-      info.titleList = aTitleList;
-
-    return this._showBookmarkDialog(info);
-  },
-
-  /**
-   * Opens the properties dialog for a given item identifier.
-   *
-   * @param aItemId
-   *        item identifier for which the properties are to be shown
-   * @param aType
-   *        item type, either "bookmark" or "folder"
-   * @param [optional] aReadOnly
-   *        states if properties dialog should be readonly
-   * @return true if any transaction has been performed.
-   */
-  showItemProperties: function PUIU_showItemProperties(aItemId, aType, aReadOnly) {
-    var info = {
-      action: "edit",
-      type: aType,
-      itemId: aItemId,
-      readOnly: aReadOnly
-    };
-    return this._showBookmarkDialog(info);
-  },
-
-  /**
-   * Shows the "New Folder" dialog.
-   *
-   * @param [optional] aTitle
-   *        The default title for the new bookmark.
-   * @param [optional] aDefaultInsertionPoint
-   *        The default insertion point for the new item. If set, the folder
-   *        picker would be hidden unless aShowPicker is set to true, in which
-   *        case the dialog only uses the folder identifier from the insertion
-   *        point as the initially selected item in the folder picker.
-   * @param [optional] aShowPicker
-   *        see above
-   * @return true if any transaction has been performed.
-   */
-  showAddFolderUI:
-  function PUIU_showAddFolderUI(aTitle, aDefaultInsertionPoint, aShowPicker) {
-    var info = {
-      action: "add",
-      type: "folder",
-      hiddenRows: []
-    };
-
-    // allow default empty title
-    if (typeof(aTitle) == "string")
-      info.title = aTitle;
-
-    if (aDefaultInsertionPoint) {
-      info.defaultInsertionPoint = aDefaultInsertionPoint;
-      if (!aShowPicker)
-        info.hiddenRows.push("folderPicker");
-    }
-    return this._showBookmarkDialog(info);
-  },
-
-  /**
-   * Shows the bookmark dialog corresponding to the specified info
+   * Shows the bookmark dialog corresponding to the specified info.
    *
    * @param aInfo
    *        Describes the item to be edited/added in the dialog.
-   *        See documentation at the top of bm-props.js
-   * @param aMinimalUI
-   *        [optional] if true, the dialog is opened by its alternative
-   *        chrome: uri.
+   *        See documentation at the top of bookmarkProperties.js
+   * @param aWindow
+   *        Owner window for the new dialog.
    *
+   * @see documentation at the top of bookmarkProperties.js
    * @return true if any transaction has been performed, false otherwise.
    */
-  _showBookmarkDialog: function PUIU__showBookmarkDialog(aInfo) {
+  showBookmarkDialog:
+  function PUIU_showBookmarkDialog(aInfo, aParentWindow) {
     // Preserve size attributes differently based on the fact the dialog has
     // a folder picker or not, since it needs more horizontal space than the
     // other controls.
     let hasFolderPicker = !("hiddenRows" in aInfo) ||
                           !aInfo.hiddenRows.includes("folderPicker");
     // Use a different chrome url to persist different sizes.
     let dialogURL = hasFolderPicker ?
-                    "chrome://communicator/content/bookmarks/bm-props2.xul" :
-                    "chrome://communicator/content/bookmarks/bm-props.xul";
+                    "chrome://communicator/content/places/bookmarkProperties2.xul" :
+                    "chrome://communicator/content/places/bookmarkProperties.xul";
 
     let features = "centerscreen,chrome,modal,resizable=yes";
-    this._getCurrentActiveWin().openDialog(dialogURL, "", features, aInfo);
+    aParentWindow.openDialog(dialogURL, "", features, aInfo);
     return ("performed" in aInfo && aInfo.performed);
   },
 
   _getTopBrowserWin: function PUIU__getTopBrowserWin() {
-    return this._getCurrentActiveWin().gPrivate ||
-           Services.wm.getMostRecentWindow("navigator:browser");
+    return RecentWindow.getMostRecentBrowserWindow();
   },
 
-  _getCurrentActiveWin: function PUIU__getCurrentActiveWin() {
-    return focusManager.activeWindow || Services.wm.getMostRecentWindow(null);
+  /**
+   * set and fetch a favicon. Can only be used from the parent process.
+   * @param browser   {Browser}   The XUL browser element for which we're fetching a favicon.
+   * @param principal {Principal} The loading principal to use for the fetch.
+   * @param uri       {URI}       The URI to fetch.
+   */
+  loadFavicon(browser, principal, uri) {
+    if (gInContentProcess) {
+      throw new Error("Can't track loads from within the child process!");
+    }
+    InternalFaviconLoader.loadFavicon(browser, principal, uri);
   },
 
   /**
    * Returns the closet ancestor places view for the given DOM node
    * @param aNode
    *        a DOM node
    * @return the closet ancestor places view if exists, null otherwsie.
    */
   getViewForNode: function PUIU_getViewForNode(aNode) {
     let node = aNode;
 
+    if (node.localName == "panelview" && node._placesView) {
+      return node._placesView;
+    }
+
     // The view for a <menu> of which its associated menupopup is a places
     // view, is the menupopup.
     if (node.localName == "menu" && !node._placesNode &&
         node.lastChild._placesView)
       return node.lastChild._placesView;
 
     while (node instanceof Ci.nsIDOMElement) {
       if (node._placesView)
@@ -735,70 +725,72 @@ var PlacesUIUtils = {
     if (PlacesUtils.nodeIsBookmark(aURINode))
       return true;
 
     var uri = PlacesUtils._uri(aURINode.uri);
     if (uri.schemeIs("javascript") || uri.schemeIs("data")) {
       const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
       var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]
                              .getService(Ci.nsIStringBundleService)
-                             .createBundle(BRANDING_BUNDLE_URI)
+                             .createBundle(BRANDING_BUNDLE_URI).
                              .GetStringFromName("brandShortName");
 
       var errorStr = this.getString("load-js-data-url-error");
       Services.prompt.alert(aWindow, brandShortName, errorStr);
       return false;
     }
     return true;
   },
 
   /**
    * Get the description associated with a document, as specified in a <META>
    * element.
    * @param   doc
    *          A DOM Document to get a description for
-   * @returns A description string if a META element was discovered with a
-   *          "description" or "httpequiv" attribute, empty string otherwise.
+   * @return A description string if a META element was discovered with a
+   *         "description" or "httpequiv" attribute, empty string otherwise.
    */
   getDescriptionFromDocument: function PUIU_getDescriptionFromDocument(doc) {
     var metaElements = doc.getElementsByTagName("META");
     for (var i = 0; i < metaElements.length; ++i) {
       if (metaElements[i].name.toLowerCase() == "description" ||
           metaElements[i].httpEquiv.toLowerCase() == "description") {
         return metaElements[i].content;
       }
     }
     return "";
   },
 
   /**
    * Retrieve the description of an item
    * @param aItemId
    *        item identifier
-   * @returns the description of the given item, or an empty string if it is
+   * @return the description of the given item, or an empty string if it is
    * not set.
    */
   getItemDescription: function PUIU_getItemDescription(aItemId) {
     if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO))
       return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO);
     return "";
   },
 
-   /**
+  /**
    * Check whether or not the given node represents a removable entry (either in
    * history or in bookmarks).
    *
    * @param aNode
    *        a node, except the root node of a query.
    * @return true if the aNode represents a removable entry, false otherwise.
    */
-  canUserRemove: function (aNode) {
+  canUserRemove(aNode) {
     let parentNode = aNode.parent;
-    if (!parentNode)
-      throw new Error("canUserRemove doesn't accept root nodes");
+    if (!parentNode) {
+      // canUserRemove doesn't accept root nodes.
+      return false;
+    }
 
     // If it's not a bookmark, we can remove it unless it's a child of a
     // livemark.
     if (aNode.itemId == -1) {
       // Rather than executing a db query, checking the existence of the feedURI
       // annotation, detect livemark children by the fact that they are the only
       // direct non-bookmark children of bookmark folders.
       return !PlacesUtils.nodeIsFolder(parentNode);
@@ -831,25 +823,23 @@ var PlacesUIUtils = {
    * Otherwise they are just treated as bookmarks (i.e. false is returned).
    *
    * @param aNodeOrItemId
    *        any item id or result node.
    * @throws if aNodeOrItemId is neither an item id nor a folder result node.
    * @note livemark "folders" are considered read-only (but see bug 1072833).
    * @return true if aItemId points to a read-only folder, false otherwise.
    */
-  isContentsReadOnly: function (aNodeOrItemId) {
+  isContentsReadOnly(aNodeOrItemId) {
     let itemId;
     if (typeof(aNodeOrItemId) == "number") {
       itemId = aNodeOrItemId;
-    }
-    else if (PlacesUtils.nodeIsFolder(aNodeOrItemId)) {
+    } else if (PlacesUtils.nodeIsFolder(aNodeOrItemId)) {
       itemId = PlacesUtils.getConcreteItemId(aNodeOrItemId);
-    }
-    else {
+    } else {
       throw new Error("invalid value for aNodeOrItemId");
     }
 
     if (itemId == PlacesUtils.placesRootId || IsLivemark(itemId))
       return true;
 
     // leftPaneFolderId, and as a result, allBookmarksFolderId, is a lazy getter
     // performing at least a synchronous DB query (and on its very first call
@@ -861,23 +851,23 @@ var PlacesUIUtils = {
     // toolbar for managing bookmarks).  To do so, we avoid comparing to those
     // special folder if the lazy getter is still in place.  This is safe merely
     // because the only way to access the left pane contents goes through
     // "resolving" the leftPaneFolderId getter.
     if ("get" in Object.getOwnPropertyDescriptor(this, "leftPaneFolderId"))
       return false;
 
     return itemId == this.leftPaneFolderId ||
-           itemId == this.allBookmarksFolderId;;
+           itemId == this.allBookmarksFolderId;
   },
 
   /**
    * Gives the user a chance to cancel loading lots of tabs at once
    */
-  _confirmOpenInTabs: function PUIU__confirmOpenInTabs(numTabsToOpen) {
+  confirmOpenInTabs(numTabsToOpen, aWindow) {
     const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen";
     var reallyOpen = true;
 
     if (Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) {
       if (numTabsToOpen >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
         // default to true: if it were false, we wouldn't get this far
         var warnOnOpen = { value: true };
 
@@ -885,17 +875,17 @@ var PlacesUIUtils = {
         var openKey = "tabs.openButtonMultiple";
         const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
         var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]
                                .getService(Ci.nsIStringBundleService)
                                .createBundle(BRANDING_BUNDLE_URI)
                                .GetStringFromName("brandShortName");
 
         var buttonPressed = Services.prompt.confirmEx(
-          this._getCurrentActiveWin(),
+          aWindow,
           this.getString("tabs.openWarningTitle"),
           this.getFormattedString(messageKey, [numTabsToOpen, brandShortName]),
           (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
             (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1),
           this.getString(openKey), null, null,
           this.getFormattedString("tabs.openWarningPromptMeBranded",
                                   [brandShortName]),
           warnOnOpen
@@ -909,98 +899,166 @@ var PlacesUIUtils = {
     }
 
     return reallyOpen;
   },
 
   /** aItemsToOpen needs to be an array of objects of the form:
     * {uri: string, isBookmark: boolean}
     */
-  _openTabset: function (aItemsToOpen, aEvent, aWhere) {
+  _openTabset: function PUIU__openTabset(aItemsToOpen, aEvent, aWindow) {
     if (!aItemsToOpen.length)
       return;
 
+    // Prefer the caller window if it's a browser window, otherwise use
+    // the top browser window.
+    var browserWindow = null;
+    browserWindow =
+      aWindow && aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser" ?
+      aWindow : this._getTopBrowserWin();
+
     var urls = [];
+    let skipMarking = browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow);
     for (let item of aItemsToOpen) {
+      urls.push(item.uri);
+      if (skipMarking) {
+        continue;
+      }
+
       if (item.isBookmark)
         this.markPageAsFollowedBookmark(item.uri);
       else
         this.markPageAsTyped(item.uri);
+    }
 
-      urls.push(item.uri);
+    // whereToOpenLink doesn't return "window" when there's no browser window
+    // open (Bug 630255).
+    var where = browserWindow ?
+                browserWindow.whereToOpenLink(aEvent, false, true) : "window";
+    if (where == "window") {
+      // There is no browser window open, thus open a new one.
+      var uriList = PlacesUtils.toISupportsString(urls.join("|"));
+      var args = Cc["@mozilla.org/array;1"]
+                   .createInstance(Ci.nsIMutableArray);
+      args.appendElement(uriList);
+      browserWindow = Services.ww.openWindow(aWindow,
+                                             "chrome://navigator/content/navigator.xul",
+                                             null, "chrome,dialog=no,all", args);
+      return;
     }
 
-    var browserWindow = this._getTopBrowserWin();
-    if (browserWindow) {
-      let where = aWhere || browserWindow.whereToOpenLink(aEvent, false, true);
-      browserWindow.openUILinkArrayIn(urls, where);
-    }
-    else {
-      let win = this._getCurrentActiveWin();
-      win.openDialog(win.getBrowserURL(), "_blank",
-                     "chrome,all,dialog=no", urls.join("\n"));
+    var loadInBackground = where == "tabshifted";
+    // For consistency, we want all the bookmarks to open in new tabs, instead
+    // of having one of them replace the currently focused tab.  Hence we call
+    // loadTabs with aReplace set to false.
+    browserWindow.gBrowser.loadTabs(urls, {
+      inBackground: loadInBackground,
+      replace: false,
+      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+    });
+  },
+
+  openLiveMarkNodesInTabs:
+  function PUIU_openLiveMarkNodesInTabs(aNode, aEvent, aView) {
+    let window = aView.ownerWindow;
+
+    PlacesUtils.livemarks.getLivemark({id: aNode.itemId})
+      .then(aLivemark => {
+        let urlsToOpen = [];
+
+        let nodes = aLivemark.getNodesForContainer(aNode);
+        for (let node of nodes) {
+          urlsToOpen.push({uri: node.uri, isBookmark: false});
+        }
+
+        if (this.confirmOpenInTabs(urlsToOpen.length, window)) {
+          this._openTabset(urlsToOpen, aEvent, window);
+        }
+      }, Cu.reportError);
+  },
+
+  openContainerNodeInTabs:
+  function PUIU_openContainerInTabs(aNode, aEvent, aView) {
+    let window = aView.ownerWindow;
+
+    let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode);
+    if (this.confirmOpenInTabs(urlsToOpen.length, window)) {
+      this._openTabset(urlsToOpen, aEvent, window);
     }
   },
 
-  openContainerNodeInTabs: function PUIU_openContainerInTabs(aNode, aEvent, aWhere) {
-    var urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode);
-    if (!this._confirmOpenInTabs(urlsToOpen.length))
-      return;
-
-    this._openTabset(urlsToOpen, aEvent, aWhere);
-  },
+  openURINodesInTabs: function PUIU_openURINodesInTabs(aNodes, aEvent, aView) {
+    let window = aView.ownerWindow;
 
-  openURINodesInTabs: function PUIU_openURINodesInTabs(aNodes, aEvent) {
-    this.openSelectionIn(aNodes, null, aEvent);
-  },
-
-  openSelectionIn: function (aNodes, aWhere, aEvent) {
-    var urlsToOpen = [];
-    for (let node of aNodes) {
-      // skip over separators and folders
-      if (PlacesUtils.nodeIsURI(node))
-        urlsToOpen.push({uri: node.uri, isBookmark: PlacesUtils.nodeIsBookmark(node)});
+    let urlsToOpen = [];
+    for (var i = 0; i < aNodes.length; i++) {
+      // Skip over separators and folders.
+      if (PlacesUtils.nodeIsURI(aNodes[i]))
+        urlsToOpen.push({uri: aNodes[i].uri, isBookmark: PlacesUtils.nodeIsBookmark(aNodes[i])});
     }
-    this._openTabset(urlsToOpen, aEvent, aWhere);
+    this._openTabset(urlsToOpen, aEvent, window);
   },
 
   /**
    * Loads the node's URL in the appropriate tab or window or as a web
    * panel given the user's preference specified by modifier keys tracked by a
    * DOM mouse/key event.
    * @param   aNode
    *          An uri result node.
    * @param   aEvent
    *          The DOM mouse/key event with modifier keys set that track the
    *          user's preferred destination window or tab.
    */
-  openNodeWithEvent: function PUIU_openNodeWithEvent(aNode, aEvent) {
-    this.openNodeIn(aNode, this._getCurrentActiveWin().whereToOpenLink(aEvent, false, true));
+  openNodeWithEvent:
+  function PUIU_openNodeWithEvent(aNode, aEvent) {
+    let window = aEvent.target.ownerGlobal;
+    this._openNodeIn(aNode, window.whereToOpenLink(aEvent, false, true), window);
   },
 
   /**
    * Loads the node's URL in the appropriate tab or window or as a
    * web panel.
    * see also openUILinkIn
    */
-  openNodeIn: function PUIU_openNodeIn(aNode, aWhere) {
-    if (!aNode || !aWhere)
-      return;
-    var win = this._getCurrentActiveWin();
-    if (PlacesUtils.nodeIsContainer(aNode) && aWhere != "current") {
-      this.openContainerNodeInTabs(aNode, null, aWhere);
-    } else if (PlacesUtils.nodeIsURI(aNode) && this.checkURLSecurity(aNode, win)) {
-      var isBookmark = PlacesUtils.nodeIsBookmark(aNode);
+  openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) {
+    let window = aView.ownerWindow;
+    this._openNodeIn(aNode, aWhere, window, aPrivate);
+  },
+
+  _openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aWindow, aPrivate = false) {
+    if (aNode && PlacesUtils.nodeIsURI(aNode) &&
+        this.checkURLSecurity(aNode, aWindow)) {
+      let isBookmark = PlacesUtils.nodeIsBookmark(aNode);
+
+      if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
+        if (isBookmark)
+          this.markPageAsFollowedBookmark(aNode.uri);
+        else
+          this.markPageAsTyped(aNode.uri);
+      }
 
-      if (isBookmark)
-        this.markPageAsFollowedBookmark(aNode.uri);
-      else
-        this.markPageAsTyped(aNode.uri);
+      // Check whether the node is a bookmark which should be opened as
+      // a web panel
+      // Currently not supported in SeaMonkey. Please stay tuned.
+      if (aWhere == "current-NOT_YET_SUPPORTED" && isBookmark) {
+        if (PlacesUtils.annotations
+                       .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) {
+          let browserWin = this._getTopBrowserWin();
+          if (browserWin) {
+            browserWin.openWebPanel(aNode.title, aNode.uri);
+            return;
+          }
+        }
+      }
 
-      win.openUILinkIn(aNode.uri, aWhere);
+      aWindow.openUILinkIn(aNode.uri, aWhere, {
+        allowPopups: aNode.uri.startsWith("javascript:"),
+        inBackground: Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground"),
+        private: aPrivate,
+      });
     }
   },
 
   /**
    * Helper for guessing scheme from an url string.
    * Used to avoid nsIURI overhead in frequently called UI functions.
    *
    * @param aUrlString the url to guess the scheme from.
@@ -1008,72 +1066,79 @@ var PlacesUIUtils = {
    * @return guessed scheme for this url string.
    *
    * @note this is not supposed be perfect, so use it only for UI purposes.
    */
   guessUrlSchemeForUI: function PUIU_guessUrlSchemeForUI(aUrlString) {
     return aUrlString.substr(0, aUrlString.indexOf(":"));
   },
 
-  getBestTitle: function PUIU_getBestTitle(aNode) {
+  getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) {
     var title;
     if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) {
       // if node title is empty, try to set the label using host and filename
       // PlacesUtils._uri() will throw if aNode.uri is not a valid URI
       try {
         var uri = PlacesUtils._uri(aNode.uri);
         var host = uri.host;
         var fileName = uri.QueryInterface(Ci.nsIURL).fileName;
         // if fileName is empty, use path to distinguish labels
-        title = host + (fileName ?
-                        (host ? "/" + this.ellipsis + "/" : "") + fileName :
-                        uri.pathQueryRef);
-      }
-      catch (e) {
+        if (aDoNotCutTitle) {
+          title = host + uri.pathQueryRef;
+        } else {
+          title = host + (fileName ?
+                           (host ? "/" + this.ellipsis + "/" : "") + fileName :
+                           uri.pathQueryRef);
+        }
+      } catch (e) {
         // Use (no title) for non-standard URIs (data:, javascript:, ...)
         title = "";
       }
-    }
-    else
+    } else
       title = aNode.title;
 
     return title || this.getString("noTitle");
   },
 
   get leftPaneQueries() {
     // build the map
     this.leftPaneFolderId;
     return this.leftPaneQueries;
   },
 
+  get leftPaneFolderId() {
+    delete this.leftPaneFolderId;
+    return this.leftPaneFolderId = this.maybeRebuildLeftPane();
+  },
+
   // Get the folder id for the organizer left-pane folder.
-  get leftPaneFolderId() {
+  maybeRebuildLeftPane() {
     let leftPaneRoot = -1;
     let allBookmarksId;
 
     // Shortcuts to services.
     let bs = PlacesUtils.bookmarks;
     let as = PlacesUtils.annotations;
 
     // This is the list of the left pane queries.
     let queries = {
       "PlacesRoot": { title: "" },
       "History": { title: this.getString("OrganizerQueryHistory") },
       "Tags": { title: this.getString("OrganizerQueryTags") },
       "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") },
       "BookmarksToolbar":
-        { title: null,
+        { title: "",
           concreteTitle: PlacesUtils.getString("BookmarksToolbarFolderTitle"),
           concreteId: PlacesUtils.toolbarFolderId },
       "BookmarksMenu":
-        { title: null,
+        { title: "",
           concreteTitle: PlacesUtils.getString("BookmarksMenuFolderTitle"),
           concreteId: PlacesUtils.bookmarksMenuFolderId },
       "UnfiledBookmarks":
-        { title: null,
+        { title: "",
           concreteTitle: PlacesUtils.getString("OtherBookmarksFolderTitle"),
           concreteId: PlacesUtils.unfiledBookmarksFolderId },
     };
     // All queries but PlacesRoot.
     const EXPECTED_QUERY_COUNT = 6;
 
     // Removes an item and associated annotations, ignoring eventual errors.
     function safeRemoveItem(aItemId) {
@@ -1085,39 +1150,36 @@ var PlacesUIUtils = {
           return;
         }
         // removeItemAnnotation does not check if item exists, nor the anno,
         // so this is safe to do.
         as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
         as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO);
         // This will throw if the annotation is an orphan.
         bs.removeItem(aItemId);
-      }
-      catch(e) { /* orphan anno */ }
+      } catch (e) { /* orphan anno */ }
     }
 
     // Returns true if item really exists, false otherwise.
     function itemExists(aItemId) {
       try {
-        bs.getItemIndex(aItemId);
-        return true;
-      }
-      catch(e) {
+        let index = bs.getItemIndex(aItemId);
+        return index > -1;
+      } catch (e) {
         return false;
       }
     }
 
     // Get all items marked as being the left pane folder.
     let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO);
     if (items.length > 1) {
       // Something went wrong, we cannot have more than one left pane folder,
       // remove all left pane folders and continue.  We will create a new one.
       items.forEach(safeRemoveItem);
-    }
-    else if (items.length == 1 && items[0] != -1) {
+    } else if (items.length == 1 && items[0] != -1) {
       leftPaneRoot = items[0];
       // Check that organizer left pane root is valid.
       let version = as.getItemAnnotation(leftPaneRoot, this.ORGANIZER_FOLDER_ANNO);
       if (version != this.ORGANIZER_LEFTPANE_VERSION ||
           !itemExists(leftPaneRoot)) {
         // Invalid root, we must rebuild the left pane.
         safeRemoveItem(leftPaneRoot);
         leftPaneRoot = -1;
@@ -1126,40 +1188,43 @@ var PlacesUIUtils = {
 
     if (leftPaneRoot != -1) {
       // A valid left pane folder has been found.
       // Build the leftPaneQueries Map.  This is used to quickly access them,
       // associating a mnemonic name to the real item ids.
       delete this.leftPaneQueries;
       this.leftPaneQueries = {};
 
-      let items = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO);
+      let queryItems = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO);
       // While looping through queries we will also check for their validity.
       let queriesCount = 0;
-      for (let i = 0; i < items.length; i++) {
-        let queryName = as.getItemAnnotation(items[i], this.ORGANIZER_QUERY_ANNO);
+      let corrupt = false;
+      for (let i = 0; i < queryItems.length; i++) {
+        let queryName = as.getItemAnnotation(queryItems[i], this.ORGANIZER_QUERY_ANNO);
 
         // Some extension did use our annotation to decorate their items
         // with icons, so we should check only our elements, to avoid dataloss.
         if (!(queryName in queries))
           continue;
 
         let query = queries[queryName];
-        query.itemId = items[i];
+        query.itemId = queryItems[i];
 
         if (!itemExists(query.itemId)) {
           // Orphan annotation, bail out and create a new left pane root.
+          corrupt = true;
           break;
         }
 
         // Check that all queries have valid parents.
         let parentId = bs.getFolderIdForItem(query.itemId);
-        if (items.indexOf(parentId) == -1 && parentId != leftPaneRoot) {
+        if (!queryItems.includes(parentId) && parentId != leftPaneRoot) {
           // The parent is not part of the left pane, bail out and create a new
           // left pane root.
+          corrupt = true;
           break;
         }
 
         // Titles could have been corrupted or the user could have changed his
         // locale.  Check title and eventually fix it.
         if (bs.getItemTitle(query.itemId) != query.title)
           bs.setItemTitle(query.itemId, query.title);
         if ("concreteId" in query) {
@@ -1167,27 +1232,29 @@ var PlacesUIUtils = {
             bs.setItemTitle(query.concreteId, query.concreteTitle);
         }
 
         // Add the query to our cache.
         this.leftPaneQueries[queryName] = query.itemId;
         queriesCount++;
       }
 
-      if (queriesCount != EXPECTED_QUERY_COUNT) {
+      // Note: it's not enough to just check for queriesCount, since we may
+      // find an invalid query just after accounting for a sufficient number of
+      // valid ones.  As well as we can't just rely on corrupt since we may find
+      // less valid queries than expected.
+      if (corrupt || queriesCount != EXPECTED_QUERY_COUNT) {
         // Queries number is wrong, so the left pane must be corrupt.
         // Note: we can't just remove the leftPaneRoot, because some query could
         // have a bad parent, so we have to remove all items one by one.
-        items.forEach(safeRemoveItem);
+        queryItems.forEach(safeRemoveItem);
         safeRemoveItem(leftPaneRoot);
-      }
-      else {
+      } else {
         // Everything is fine, return the current left pane folder.
-        delete this.leftPaneFolderId;
-        return this.leftPaneFolderId = leftPaneRoot;
+        return leftPaneRoot;
       }
     }
 
     // Create a new left pane folder.
     var callback = {
       // Helper to create an organizer special query.
       create_query: function CB_create_query(aQueryName, aParentId, aQueryUrl) {
         let itemId = bs.insertBookmark(aParentId,
@@ -1215,33 +1282,39 @@ var PlacesUIUtils = {
         as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1,
                              0, as.EXPIRE_NEVER);
 
         if (aIsRoot) {
           // Mark as special left pane root.
           as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
                                PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION,
                                0, as.EXPIRE_NEVER);
-        }
-        else {
+        } else {
           // Mark as special organizer folder.
-          as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO,
-                               aFolderName, 0, as.EXPIRE_NEVER);
+          as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aFolderName,
+                           0, as.EXPIRE_NEVER);
           PlacesUIUtils.leftPaneQueries[aFolderName] = folderId;
         }
         return folderId;
       },
 
       runBatched: function CB_runBatched(aUserData) {
         delete PlacesUIUtils.leftPaneQueries;
         PlacesUIUtils.leftPaneQueries = { };
 
         // Left Pane Root Folder.
         leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true);
 
+        // History Query.
+        this.create_query("History", leftPaneRoot,
+                          "place:type=" +
+                          Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY +
+                          "&sort=" +
+                          Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING);
+
         // Tags Query.
         this.create_query("Tags", leftPaneRoot,
                           "place:type=" +
                           Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY +
                           "&sort=" +
                           Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING);
 
         // All Bookmarks Folder.
@@ -1257,18 +1330,17 @@ var PlacesUIUtils = {
 
         // All Bookmarks->Unfiled Bookmarks Query.
         this.create_query("UnfiledBookmarks", allBookmarksId,
                           "place:folder=UNFILED_BOOKMARKS");
       }
     };
     bs.runInBatchMode(callback, null);
 
-    delete this.leftPaneFolderId;
-    return this.leftPaneFolderId = leftPaneRoot;
+    return leftPaneRoot;
   },
 
   /**
    * Get the folder id for the organizer left-pane folder.
    */
   get allBookmarksFolderId() {
     // ensure the left-pane root is initialized;
     this.leftPaneFolderId;
@@ -1276,252 +1348,405 @@ var PlacesUIUtils = {
     return this.allBookmarksFolderId = this.leftPaneQueries["AllBookmarks"];
   },
 
   /**
    * If an item is a left-pane query, returns the name of the query
    * or an empty string if not.
    *
    * @param aItemId id of a container
-   * @returns the name of the query, or empty string if not a left-pane query
+   * @return the name of the query, or empty string if not a left-pane query
    */
   getLeftPaneQueryNameFromId: function PUIU_getLeftPaneQueryNameFromId(aItemId) {
     var queryName = "";
     // If the let pane hasn't been built, use the annotation service
     // directly, to avoid building the left pane too early.
     if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) {
       try {
         queryName = PlacesUtils.annotations.
                                 getItemAnnotation(aItemId, this.ORGANIZER_QUERY_ANNO);
-      }
-      catch (ex) {
+      } catch (ex) {
         // doesn't have the annotation
         queryName = "";
       }
-    }
-    else {
+    } else {
       // If the left pane has already been built, use the name->id map
       // cached in PlacesUIUtils.
       for (let [name, id] of Object.entries(this.leftPaneQueries)) {
         if (aItemId == id)
           queryName = name;
       }
     }
     return queryName;
+  },
+
+  shouldShowTabsFromOtherComputersMenuitem() {
+    let weaveOK = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED &&
+                  Weave.Svc.Prefs.get("firstSync", "") != "notReady";
+    return weaveOK;
+  },
+
+  /**
+   * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT'S LIKELY TO BE REMOVED IN A
+   * FUTURE RELEASE.
+   *
+   * Checks if a place: href represents a folder shortcut.
+   *
+   * @param queryString
+   *        the query string to check (a place: href)
+   * @return whether or not queryString represents a folder shortcut.
+   * @throws if queryString is malformed.
+   */
+  isFolderShortcutQueryString(queryString) {
+    // Based on GetSimpleBookmarksQueryFolder in nsNavHistory.cpp.
+
+    let queriesParam = { }, optionsParam = { };
+    PlacesUtils.history.queryStringToQueries(queryString,
+                                             queriesParam,
+                                             { },
+                                             optionsParam);
+    let queries = queries.value;
+    if (queries.length == 0)
+      throw new Error(`Invalid place: uri: ${queryString}`);
+    return queries.length == 1 &&
+           queries[0].folderCount == 1 &&
+           !queries[0].hasBeginTime &&
+           !queries[0].hasEndTime &&
+           !queries[0].hasDomain &&
+           !queries[0].hasURI &&
+           !queries[0].hasSearchTerms &&
+           !queries[0].tags.length == 0 &&
+           optionsParam.value.maxResults == 0;
+  },
+
+  /**
+   * @see showAddBookmarkUI
+   * This opens the dialog with only the name and folder pickers visible by
+   * default.
+   *
+   * This is to be used only outside of the SeaMonkey browser part e.g. for
+   * bookmarking in mail and news windows.
+   *
+   * You can still pass in the various paramaters as the default properties
+   * for the new bookmark.
+   *
+   * The keyword field will be visible only if the aKeyword parameter
+   * was used.
+   */
+  showMinimalAddBookmarkUI:
+  function PUIU_showMinimalAddBookmarkUI(aURI, aTitle, aDescription,
+                                         aDefaultInsertionPoint, aShowPicker,
+                                         aLoadInSidebar, aKeyword, aPostData,
+                                         aCharSet) {
+    var info = {
+      action: "add",
+      type: "bookmark",
+      hiddenRows: ["description"]
+    };
+    if (aURI)
+      info.uri = aURI;
+
+    // allow default empty title
+    if (typeof(aTitle) == "string")
+      info.title = aTitle;
+
+    if (aDescription)
+      info.description = aDescription;
+
+    if (aDefaultInsertionPoint) {
+      info.defaultInsertionPoint = aDefaultInsertionPoint;
+      if (!aShowPicker)
+        info.hiddenRows.push("folderPicker");
+    }
+
+    info.hiddenRows = info.hiddenRows.concat(["location"]);
+
+    if (typeof(aKeyword) == "string") {
+      info.keyword = aKeyword;
+      // Hide the Tags field if we are adding a keyword.
+      info.hiddenRows.push("tags");
+      // Keyword related params.
+      if (typeof(aPostData) == "string")
+        info.postData = aPostData;
+      if (typeof(aCharSet) == "string")
+        info.charSet = aCharSet;
+    }
+    else
+      info.hiddenRows.push("keyword");
+
+    return this.showBookmarkDialog(info,
+                                   focusManager.activeWindow ||
+                                   Services.wm.getMostRecentWindow(null));
+  },
+
+  /**
+   * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
+   *
+   * Given a bookmark object for either a url bookmark or a folder, returned by
+   * Bookmarks.fetch (see Bookmark.jsm), this creates a node-like object suitable for
+   * initialising the edit overlay with it.
+   *
+   * @param aFetchInfo
+   *        a bookmark object returned by Bookmarks.fetch.
+   * @return a node-like object suitable for initialising editBookmarkOverlay.
+   * @throws if aFetchInfo is representing a separator.
+   */
+  async promiseNodeLikeFromFetchInfo(aFetchInfo) {
+    if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR)
+      throw new Error("promiseNodeLike doesn't support separators");
+
+    let parent = {
+      itemId: await PlacesUtils.promiseItemId(aFetchInfo.parentGuid),
+      bookmarkGuid: aFetchInfo.parentGuid,
+      type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
+    };
+
+    return Object.freeze({
+      itemId: await PlacesUtils.promiseItemId(aFetchInfo.guid),
+      bookmarkGuid: aFetchInfo.guid,
+      title: aFetchInfo.title,
+      uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "",
+
+      get type() {
+        if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_FOLDER)
+          return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
+
+        if (this.uri.length == 0)
+          throw new Error("Unexpected item type");
+
+        if (/^place:/.test(this.uri)) {
+          if (this.isFolderShortcutQueryString(this.uri))
+            return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+
+          return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
+        }
+
+        return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+      },
+
+      get parent() {
+        return parent;
+      }
+    });
+  },
+
+  /**
+   * Shortcut for calling promiseNodeLikeFromFetchInfo on the result of
+   * Bookmarks.fetch for the given guid/info object.
+   *
+   * @see promiseNodeLikeFromFetchInfo above and Bookmarks.fetch in Bookmarks.jsm.
+   */
+  async fetchNodeLike(aGuidOrInfo) {
+    let info = await PlacesUtils.bookmarks.fetch(aGuidOrInfo);
+    if (!info)
+      return null;
+    return this.promiseNodeLikeFromFetchInfo(info);
   }
 };
 
-XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "xulStore",
-                                   "@mozilla.org/xul/xulstore;1",
-                                   "nsIXULStore");
+
+PlacesUIUtils.PLACES_FLAVORS = [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
+                                PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
+                                PlacesUtils.TYPE_X_MOZ_PLACE];
+
+PlacesUIUtils.URI_FLAVORS = [PlacesUtils.TYPE_X_MOZ_URL,
+                             TAB_DROP_TYPE,
+                             PlacesUtils.TYPE_UNICODE],
+
+PlacesUIUtils.SUPPORTED_FLAVORS = [...PlacesUIUtils.PLACES_FLAVORS,
+                                   ...PlacesUIUtils.URI_FLAVORS];
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF",
+                                   "@mozilla.org/rdf/rdf-service;1",
+                                   "nsIRDFService");
 
 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() {
   return Services.prefs.getComplexValue("intl.ellipsis",
                                         Ci.nsIPrefLocalizedString).data;
 });
 
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "useAsyncTransactions", function() {
+  try {
+    return Services.prefs.getBoolPref("browser.places.useAsyncTransactions");
+  } catch (ex) { }
+  return false;
+});
+
 XPCOMUtils.defineLazyServiceGetter(this, "URIFixup",
                                    "@mozilla.org/docshell/urifixup;1",
                                    "nsIURIFixup");
 
 XPCOMUtils.defineLazyGetter(this, "bundle", function() {
   const PLACES_STRING_BUNDLE_URI =
     "chrome://communicator/locale/places/places.properties";
   return Cc["@mozilla.org/intl/stringbundle;1"]
            .getService(Ci.nsIStringBundleService)
            .createBundle(PLACES_STRING_BUNDLE_URI);
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "focusManager",
                                    "@mozilla.org/focus-manager;1",
                                    "nsIFocusManager");
-
 /**
  * This is a compatibility shim for old PUIU.ptm users.
  *
  * If you're looking for transactions and writing new code using them, directly
  * use the transactions objects exported by the PlacesUtils.jsm module.
  *
  * This object will be removed once enough users are converted to the new API.
  */
 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ptm", function() {
   // Ensure PlacesUtils is imported in scope.
   PlacesUtils;
 
   return {
-    aggregateTransactions: function(aName, aTransactions) {
-      return new PlacesAggregatedTransaction(aName, aTransactions);
-    },
+    aggregateTransactions: (aName, aTransactions) =>
+      new PlacesAggregatedTransaction(aName, aTransactions),
 
-    createFolder: function(aName, aContainer, aIndex, aAnnotations,
-                           aChildItemsTransactions) {
-      return new PlacesCreateFolderTransaction(aName, aContainer, aIndex, aAnnotations,
-                                               aChildItemsTransactions);
-    },
+    createFolder: (aName, aContainer, aIndex, aAnnotations,
+                   aChildItemsTransactions) =>
+      new PlacesCreateFolderTransaction(aName, aContainer, aIndex, aAnnotations,
+                                        aChildItemsTransactions),
 
-    createItem: function(aURI, aContainer, aIndex, aTitle, aKeyword,
-                         aAnnotations, aChildTransactions) {
-      return new PlacesCreateBookmarkTransaction(aURI, aContainer, aIndex, aTitle,
-                                                 aKeyword, aAnnotations,
-                                                 aChildTransactions);
-    },
+    createItem: (aURI, aContainer, aIndex, aTitle, aKeyword,
+                 aAnnotations, aChildTransactions) =>
+      new PlacesCreateBookmarkTransaction(aURI, aContainer, aIndex, aTitle,
+                                          aKeyword, aAnnotations,
+                                          aChildTransactions),
 
-    createSeparator: function(aContainer, aIndex) {
-      return new PlacesCreateSeparatorTransaction(aContainer, aIndex);
-    },
+    createSeparator: (aContainer, aIndex) =>
+      new PlacesCreateSeparatorTransaction(aContainer, aIndex),
 
-    createLivemark: function(aFeedURI, aSiteURI, aName, aContainer, aIndex,
-                             aAnnotations) {
-      return new PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName, aContainer,
-                                                 aIndex, aAnnotations);
-    },
+    createLivemark: (aFeedURI, aSiteURI, aName, aContainer, aIndex,
+                     aAnnotations) =>
+      new PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName, aContainer,
+                                          aIndex, aAnnotations),
 
-    moveItem: function(aItemId, aNewContainer, aNewIndex) {
-      return new PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex);
-    },
+    moveItem: (aItemId, aNewContainer, aNewIndex) =>
+      new PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex),
+
+    removeItem: (aItemId) =>
+      new PlacesRemoveItemTransaction(aItemId),
 
-    removeItem: function(aItemId) {
-      return new PlacesRemoveItemTransaction(aItemId);
-    },
+    editItemTitle: (aItemId, aNewTitle) =>
+      new PlacesEditItemTitleTransaction(aItemId, aNewTitle),
+
+    editBookmarkURI: (aItemId, aNewURI) =>
+      new PlacesEditBookmarkURITransaction(aItemId, aNewURI),
 
-    editItemTitle: function(aItemId, aNewTitle) {
-      return new PlacesEditItemTitleTransaction(aItemId, aNewTitle);
-    },
+    setItemAnnotation: (aItemId, aAnnotationObject) =>
+      new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject),
 
-    editBookmarkURI: function(aItemId, aNewURI) {
-      return new PlacesEditBookmarkURITransaction(aItemId, aNewURI);
-    },
+    setPageAnnotation: (aURI, aAnnotationObject) =>
+      new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject),
 
-    setItemAnnotation: function(aItemId, aAnnotationObject) {
-      return new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject);
-    },
+    editBookmarkKeyword: (aItemId, aNewKeyword) =>
+      new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword),
 
-    setPageAnnotation: function(aURI, aAnnotationObject) {
-      return new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject);
-    },
+    editLivemarkSiteURI: (aLivemarkId, aSiteURI) =>
+      new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI),
+
+    editLivemarkFeedURI: (aLivemarkId, aFeedURI) =>
+      new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI),
 
-    editBookmarkKeyword: function(aItemId, aNewKeyword) {
-      return new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword);
-    },
+    editItemDateAdded: (aItemId, aNewDateAdded) =>
+      new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded),
 
-    editBookmarkPostData: function(aItemId, aPostData) {
-      return new PlacesEditBookmarkPostDataTransaction(aItemId, aPostData);
-    },
+    editItemLastModified: (aItemId, aNewLastModified) =>
+      new PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified),
 
-    editLivemarkSiteURI: function(aLivemarkId, aSiteURI) {
-      return new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI);
-    },
+    sortFolderByName: (aFolderId) =>
+      new PlacesSortFolderByNameTransaction(aFolderId),
 
-    editLivemarkFeedURI: function(aLivemarkId, aFeedURI) {
-      return new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI);
-    },
+    tagURI: (aURI, aTags) =>
+      new PlacesTagURITransaction(aURI, aTags),
 
-    editItemDateAdded: function(aItemId, aNewDateAdded) {
-      return new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded);
-    },
+    untagURI: (aURI, aTags) =>
+      new PlacesUntagURITransaction(aURI, aTags),
 
-    editItemLastModified: function(aItemId, aNewLastModified) {
-      return new PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified);
-    },
-
-    sortFolderByName: function(aFolderId) {
-      return new PlacesSortFolderByNameTransaction(aFolderId);
-    },
-
-    tagURI: function(aURI, aTags) {
-      return new PlacesTagURITransaction(aURI, aTags);
-    },
-
-    untagURI: function(aURI, aTags) {
-      return new PlacesUntagURITransaction(aURI, aTags);
+    /**
+     * Transaction for setting/unsetting Load-in-sidebar annotation.
+     *
+     * @param aBookmarkId
+     *        id of the bookmark where to set Load-in-sidebar annotation.
+     * @param aLoadInSidebar
+     *        boolean value.
+     * @return nsITransaction object.
+     */
+    setLoadInSidebar(aItemId, aLoadInSidebar) {
+      let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
+                      type: Ci.nsIAnnotationService.TYPE_INT32,
+                      flags: 0,
+                      value: aLoadInSidebar,
+                      expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+      return new PlacesSetItemAnnotationTransaction(aItemId, annoObj);
     },
 
    /**
     * Transaction for editing the description of a bookmark or a folder.
     *
     * @param aItemId
     *        id of the item to edit.
     * @param aDescription
     *        new description.
-    * @returns nsITransaction object.
+    * @return nsITransaction object.
     */
-    editItemDescription: function(aItemId, aDescription)
-    {
+    editItemDescription(aItemId, aDescription) {
       let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO,
                       type: Ci.nsIAnnotationService.TYPE_STRING,
                       flags: 0,
                       value: aDescription,
                       expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
       return new PlacesSetItemAnnotationTransaction(aItemId, annoObj);
     },
 
-    ////////////////////////////////////////////////////////////////////////////
-    //// nsITransactionManager forwarders.
+    // nsITransactionManager forwarders.
 
-    beginBatch: function() {
-      PlacesUtils.transactionManager.beginBatch(null);
-    },
+    beginBatch: () =>
+      PlacesUtils.transactionManager.beginBatch(null),
 
-    endBatch: function() {
-      PlacesUtils.transactionManager.endBatch(false);
-    },
+    endBatch: () =>
+      PlacesUtils.transactionManager.endBatch(false),
 
-    doTransaction: function(txn) {
-      PlacesUtils.transactionManager.doTransaction(txn);
-    },
+    doTransaction: (txn) =>
+      PlacesUtils.transactionManager.doTransaction(txn),
 
-    undoTransaction: function() {
-      PlacesUtils.transactionManager.undoTransaction();
-    },
+    undoTransaction: () =>
+      PlacesUtils.transactionManager.undoTransaction(),
 
-    redoTransaction: function() {
-      PlacesUtils.transactionManager.redoTransaction();
-    },
+    redoTransaction: () =>
+      PlacesUtils.transactionManager.redoTransaction(),
 
     get numberOfUndoItems() {
       return PlacesUtils.transactionManager.numberOfUndoItems;
     },
     get numberOfRedoItems() {
       return PlacesUtils.transactionManager.numberOfRedoItems;
     },
     get maxTransactionCount() {
       return PlacesUtils.transactionManager.maxTransactionCount;
     },
     set maxTransactionCount(val) {
-      return PlacesUtils.transactionManager.maxTransactionCount = val;
-    },
-
-    clear: function() {
-      PlacesUtils.transactionManager.clear();
-    },
-
-    peekUndoStack: function() {
-      return PlacesUtils.transactionManager.peekUndoStack();
-    },
-
-    peekRedoStack: function() {
-      return PlacesUtils.transactionManager.peekRedoStack();
-    },
-
-    getUndoStack: function() {
-      return PlacesUtils.transactionManager.getUndoStack();
+      PlacesUtils.transactionManager.maxTransactionCount = val;
     },
 
-    getRedoStack: function() {
-      return PlacesUtils.transactionManager.getRedoStack();
-    },
+    clear: () =>
+      PlacesUtils.transactionManager.clear(),
+
+    peekUndoStack: () =>
+      PlacesUtils.transactionManager.peekUndoStack(),
+
+    peekRedoStack: () =>
+      PlacesUtils.transactionManager.peekRedoStack(),
 
-    AddListener: function(aListener) {
-      PlacesUtils.transactionManager.AddListener(aListener);
-    },
+    getUndoStack: () =>
+      PlacesUtils.transactionManager.getUndoStack(),
+
+    getRedoStack: () =>
+      PlacesUtils.transactionManager.getRedoStack(),
 
-    RemoveListener: function(aListener) {
-      PlacesUtils.transactionManager.RemoveListener(aListener);
-    }
+    AddListener: (aListener) =>
+      PlacesUtils.transactionManager.AddListener(aListener),
+
+    RemoveListener: (aListener) =>
+      PlacesUtils.transactionManager.RemoveListener(aListener)
   }
 });
-
-XPCOMUtils.defineLazyGetter(PlacesUIUtils, "_copyableAnnotations", function() {
-  return [this.DESCRIPTION_ANNO,
-          this.LOAD_IN_SIDEBAR_ANNO,
-          PlacesUtils.POST_DATA_ANNO,
-          PlacesUtils.READ_ONLY_ANNO]
-});
--- a/suite/common/places/content/bookmarkProperties.js
+++ b/suite/common/places/content/bookmarkProperties.js
@@ -1,26 +1,25 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* 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/. */
 
 /**
  * The panel is initialized based on data given in the js object passed
  * as window.arguments[0]. The object must have the following fields set:
  *   @ action (String). Possible values:
  *     - "add" - for adding a new item.
  *       @ type (String). Possible values:
  *         - "bookmark"
  *           @ loadBookmarkInSidebar - optional, the default state for the
  *             "Load this bookmark in the sidebar" field.
  *         - "folder"
  *           @ URIList (Array of nsIURI objects) - optional, list of uris to
  *             be bookmarked under the new folder.
- *           @ titleList (Array of String) - optional, list of titles.
  *         - "livemark"
  *       @ uri (nsIURI object) - optional, the default uri for the new item.
  *         The property is not used for the "folder with items" type.
  *       @ title (String) - optional, the default title for the new item.
  *       @ description (String) - optional, the default description for the new
  *         item.
  *       @ defaultInsertionPoint (InsertionPoint JS object) - optional, the
  *         default insertion point for the new item.
@@ -33,36 +32,44 @@
  *           with the given uri. If the dialog is set to adding a folder with
  *           bookmark items under it (see URIList), a default static title is
  *           used ("[Folder Name]").
  *        2) The index field of the default insertion point is ignored if
  *           the folder picker is shown.
  *     - "edit" - for editing a bookmark item or a folder.
  *       @ type (String). Possible values:
  *         - "bookmark"
- *           @ itemId (Integer) - the id of the bookmark item.
+ *           @ node (an nsINavHistoryResultNode object) - a node representing
+ *             the bookmark.
  *         - "folder" (also applies to livemarks)
- *           @ itemId (Integer) - the id of the folder.
+ *           @ node (an nsINavHistoryResultNode object) - a node representing
+ *             the folder.
  *   @ hiddenRows (Strings array) - optional, list of rows to be hidden
  *     regardless of the item edited or added by the dialog.
  *     Possible values:
  *     - "title"
  *     - "location"
  *     - "description"
  *     - "keyword"
  *     - "tags"
- *     - "feedLocation"
- *     - "siteLocation"
+ *     - "loadInSidebar"
  *     - "folderPicker" - hides both the tree and the menu.
- *   @ readOnly (Boolean) - optional, states if the panel should be read-only
  *
  * window.arguments[0].performed is set to true if any transaction has
  * been performed by the dialog.
  */
 
+/* import-globals-from editBookmarkOverlay.js */
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
+                               "resource://gre/modules/PrivateBrowsingUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "PromiseUtils",
+                               "resource://gre/modules/PromiseUtils.jsm");
+
 const BOOKMARK_ITEM = 0;
 const BOOKMARK_FOLDER = 1;
 const LIVEMARK_CONTAINER = 2;
 
 const ACTION_EDIT = 0;
 const ACTION_ADD = 1;
 
 var elementsHeight = new Map();
@@ -77,44 +84,43 @@ var BookmarkPropertiesPanel = {
     }
     return this.__strings;
   },
 
   _action: null,
   _itemType: null,
   _itemId: -1,
   _uri: null,
+  _loadInSidebar: false,
   _title: "",
   _description: "",
   _URIs: [],
-  _titles: [],
   _keyword: "",
   _postData: null,
   _charSet: "",
   _feedURI: null,
   _siteURI: null,
 
   _defaultInsertionPoint: null,
   _hiddenRows: [],
   _batching: false,
-  _readOnly: false,
 
   /**
    * This method returns the correct label for the dialog's "accept"
    * button based on the variant of the dialog.
    */
   _getAcceptLabel: function BPP__getAcceptLabel() {
     if (this._action == ACTION_ADD) {
       if (this._URIs.length)
         return this._strings.getString("dialogAcceptLabelAddMulti");
 
       if (this._itemType == LIVEMARK_CONTAINER)
         return this._strings.getString("dialogAcceptLabelAddLivemark");
 
-      if (this._dummyItem)
+      if (this._dummyItem || this._loadInSidebar)
         return this._strings.getString("dialogAcceptLabelAddItem");
 
       return this._strings.getString("dialogAcceptLabelSaveItem");
     }
     return this._strings.getString("dialogAcceptLabelEdit");
   },
 
   /**
@@ -139,137 +145,102 @@ var BookmarkPropertiesPanel = {
       return this._strings.getFormattedString("dialogTitleEdit", [this._title]);
     }
     return "";
   },
 
   /**
    * Determines the initial data for the item edited or added by this dialog
    */
-  _determineItemInfo: function BPP__determineItemInfo() {
-    var dialogInfo = window.arguments[0];
+  _determineItemInfo() {
+    let dialogInfo = window.arguments[0];
     this._action = dialogInfo.action == "add" ? ACTION_ADD : ACTION_EDIT;
     this._hiddenRows = dialogInfo.hiddenRows ? dialogInfo.hiddenRows : [];
     if (this._action == ACTION_ADD) {
       NS_ASSERT("type" in dialogInfo, "missing type property for add action");
 
       if ("title" in dialogInfo)
         this._title = dialogInfo.title;
 
       if ("defaultInsertionPoint" in dialogInfo) {
         this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint;
-      }
-      else
+      } else {
         this._defaultInsertionPoint =
           new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
                              PlacesUtils.bookmarks.DEFAULT_INDEX,
                              Ci.nsITreeView.DROP_ON);
+      }
 
       switch (dialogInfo.type) {
         case "bookmark":
           this._itemType = BOOKMARK_ITEM;
           if ("uri" in dialogInfo) {
             NS_ASSERT(dialogInfo.uri instanceof Ci.nsIURI,
                       "uri property should be a uri object");
             this._uri = dialogInfo.uri;
             if (typeof(this._title) != "string") {
               this._title = this._getURITitleFromHistory(this._uri) ||
                             this._uri.spec;
             }
-          }
-          else {
+          } else {
             this._uri = PlacesUtils._uri("about:blank");
             this._title = this._strings.getString("newBookmarkDefault");
             this._dummyItem = true;
           }
 
+          if ("loadBookmarkInSidebar" in dialogInfo)
+            this._loadInSidebar = dialogInfo.loadBookmarkInSidebar;
+
           if ("keyword" in dialogInfo) {
             this._keyword = dialogInfo.keyword;
             this._isAddKeywordDialog = true;
             if ("postData" in dialogInfo)
               this._postData = dialogInfo.postData;
             if ("charSet" in dialogInfo)
               this._charSet = dialogInfo.charSet;
           }
           break;
 
         case "folder":
           this._itemType = BOOKMARK_FOLDER;
           if (!this._title) {
             if ("URIList" in dialogInfo) {
               this._title = this._strings.getString("bookmarkAllTabsDefault");
               this._URIs = dialogInfo.URIList;
-              if ("titleList" in dialogInfo)
-                this._titles = dialogInfo.titleList;
-            }
-            else
+            } else
               this._title = this._strings.getString("newFolderDefault");
-              this._dummyItem = true;
+            this._dummyItem = true;
           }
           break;
 
         case "livemark":
           this._itemType = LIVEMARK_CONTAINER;
           if ("feedURI" in dialogInfo)
             this._feedURI = dialogInfo.feedURI;
           if ("siteURI" in dialogInfo)
             this._siteURI = dialogInfo.siteURI;
 
           if (!this._title) {
             if (this._feedURI) {
               this._title = this._getURITitleFromHistory(this._feedURI) ||
                             this._feedURI.spec;
-            }
-            else
+            } else
               this._title = this._strings.getString("newLivemarkDefault");
           }
       }
 
       if ("description" in dialogInfo)
         this._description = dialogInfo.description;
-    }
-    else { // edit
-      NS_ASSERT("itemId" in dialogInfo);
-      this._itemId = dialogInfo.itemId;
-      this._title = PlacesUtils.bookmarks.getItemTitle(this._itemId);
-      // Don't show folderPicker when editing
-      this._hiddenRows.push("folderPicker");
-      this._readOnly = !!dialogInfo.readOnly;
-
-      switch (dialogInfo.type) {
-        case "bookmark":
-          this._itemType = BOOKMARK_ITEM;
-
-          this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
-          // keyword
-          this._keyword = PlacesUtils.bookmarks
-                                     .getKeywordForBookmark(this._itemId);
-          break;
-
-        case "folder":
-          this._itemType = BOOKMARK_FOLDER;
-          PlacesUtils.livemarks.getLivemark({ id: this._itemId })
-                               .then(aLivemark => {
-            this._itemType = LIVEMARK_CONTAINER;
-            this._feedURI = aLivemark.feedURI;
-            this._siteURI = aLivemark.siteURI;
-            this._fillEditProperties();
-
-            document.documentElement
-                    .getButton("accept").disabled = !this._inputIsValid();
-          }, () => undefined);
-      }
-
-      // Description
-      if (PlacesUtils.annotations
-                     .itemHasAnnotation(this._itemId, PlacesUIUtils.DESCRIPTION_ANNO)) {
-        this._description = PlacesUtils.annotations
-                                       .getItemAnnotation(this._itemId,
-                                                          PlacesUIUtils.DESCRIPTION_ANNO);
-      }
+    } else { // edit
+      this._node = dialogInfo.node;
+      this._title = this._node.title;
+      if (PlacesUtils.nodeIsFolder(this._node))
+        this._itemType = BOOKMARK_FOLDER;
+      else if (PlacesUtils.nodeIsURI(this._node))
+        this._itemType = BOOKMARK_ITEM;
     }
   },
 
   /**
    * This method returns the title string corresponding to a given URI.
    * If none is available from the bookmark service (probably because
    * the given URI doesn't appear in bookmarks or history), we synthesize
    * a title from the first 100 characters of the URI.
@@ -285,17 +256,17 @@ var BookmarkPropertiesPanel = {
     // get the title from History
     return PlacesUtils.history.getPageTitle(aURI);
   },
 
   /**
    * This method should be called by the onload of the Bookmark Properties
    * dialog to initialize the state of the panel.
    */
-  onDialogLoad: function BPP_onDialogLoad() {
+  async onDialogLoad() {
     this._determineItemInfo();
 
     document.title = this._getDialogTitle();
     var acceptButton = document.documentElement.getButton("accept");
     acceptButton.label = this._getAcceptLabel();
 
     // Do not use sizeToContent, otherwise, due to bug 90276, the dialog will
     // grow at every opening.
@@ -304,186 +275,184 @@ var BookmarkPropertiesPanel = {
     this._height = window.outerHeight;
     this._mutationObserver = new MutationObserver(mutations => {
       for (let mutation of mutations) {
         let target = mutation.target;
         let id = target.id;
         if (!/^editBMPanel_.*(Row|Checkbox)$/.test(id))
           continue;
 
-        let collapsed = target.getAttribute("collapsed") == "true";
-        let wasCollapsed = mutation.oldValue == "true";
+        let collapsed = target.getAttribute("collapsed") === "true";
+        let wasCollapsed = mutation.oldValue === "true";
         if (collapsed == wasCollapsed)
           continue;
 
         if (collapsed) {
           this._height -= elementsHeight.get(id);
           elementsHeight.delete(id);
         } else {
           elementsHeight.set(id, target.boxObject.height);
           this._height += elementsHeight.get(id);
         }
+        window.resizeTo(window.outerWidth, this._height);
       }
-      window.resizeTo(window.outerWidth, this._height);
     });
 
     this._mutationObserver.observe(document,
-                                   { attributes: true,
-                                     subtree: true,
+                                   { subtree: true,
                                      attributeOldValue: true,
                                      attributeFilter: ["collapsed"] });
 
     // Some controls are flexible and we want to update their cached size when
     // the dialog is resized.
     window.addEventListener("resize", this);
 
     this._beginBatch();
 
     switch (this._action) {
       case ACTION_EDIT:
-        this._fillEditProperties();
-        acceptButton.disabled = this._readOnly;
+        gEditItemOverlay.initPanel({ node: this._node,
+                                     hiddenRows: this._hiddenRows,
+                                     focusedElement: "first" });
+        acceptButton.disabled = gEditItemOverlay.readOnly;
         break;
       case ACTION_ADD:
-        this._createNewItem().then(() => this._fillAddProperties());
+        this._node = await this._promiseNewItem();
+        // Edit the new item
+        gEditItemOverlay.initPanel({ node: this._node,
+                                     hiddenRows: this._hiddenRows,
+                                     postData: this._postData,
+                                     focusedElement: "first" });
+
+        // Empty location field if the uri is about:blank, this way inserting a new
+        // url will be easier for the user, Accept button will be automatically
+        // disabled by the input listener until the user fills the field.
+        let locationField = this._element("locationField");
+        if (locationField.value == "about:blank")
+          locationField.value = "";
+
         // if this is an uri related dialog disable accept button until
         // the user fills an uri value.
         if (this._itemType == BOOKMARK_ITEM)
           acceptButton.disabled = !this._inputIsValid();
         break;
     }
 
-    if (!this._readOnly) {
+    if (!gEditItemOverlay.readOnly) {
       // Listen on uri fields to enable accept button if input is valid
       if (this._itemType == BOOKMARK_ITEM) {
         this._element("locationField")
             .addEventListener("input", this);
         if (this._isAddKeywordDialog) {
           this._element("keywordField")
               .addEventListener("input", this);
         }
       }
-      else if (this._itemType == LIVEMARK_CONTAINER) {
-        this._element("feedLocationField")
-            .addEventListener("input", this);
-        this._element("siteLocationField")
-            .addEventListener("input", this);
-      }
     }
   },
 
   // nsIDOMEventListener
   handleEvent: function BPP_handleEvent(aEvent) {
     var target = aEvent.target;
     switch (aEvent.type) {
       case "input":
         if (target.id == "editBMPanel_locationField" ||
-            target.id == "editBMPanel_feedLocationField" ||
-            target.id == "editBMPanel_siteLocationField" ||
             target.id == "editBMPanel_keywordField") {
           // Check uri fields to enable accept button if input is valid
           document.documentElement
                   .getButton("accept").disabled = !this._inputIsValid();
         }
         break;
       case "resize":
         for (let [id, oldHeight] of elementsHeight) {
           let newHeight = document.getElementById(id).boxObject.height;
-          this._height += - oldHeight + newHeight;
+          this._height += -oldHeight + newHeight;
           elementsHeight.set(id, newHeight);
+        }
         break;
     }
-
-    }
   },
 
-  _beginBatch: function BPP__beginBatch() {
+  // Hack for implementing batched-Undo around the editBookmarkOverlay
+  // instant-apply code. For all the details see the comment above beginBatch
+  // in browser-places.js
+  _batchBlockingDeferred: null,
+  _beginBatch() {
     if (this._batching)
       return;
-
-    PlacesUtils.transactionManager.beginBatch(null);
+    if (PlacesUIUtils.useAsyncTransactions) {
+      this._batchBlockingDeferred = PromiseUtils.defer();
+      PlacesTransactions.batch(async () => {
+        await this._batchBlockingDeferred.promise;
+      });
+    } else {
+      PlacesUtils.transactionManager.beginBatch(null);
+    }
     this._batching = true;
   },
 
-  _endBatch: function BPP__endBatch() {
+  _endBatch() {
     if (!this._batching)
       return;
 
-    PlacesUtils.transactionManager.endBatch(false);
+    if (PlacesUIUtils.useAsyncTransactions) {
+      this._batchBlockingDeferred.resolve();
+      this._batchBlockingDeferred = null;
+    } else {
+      PlacesUtils.transactionManager.endBatch(false);
+    }
     this._batching = false;
   },
 
-  _fillEditProperties: function BPP__fillEditProperties() {
-    gEditItemOverlay.initPanel(this._itemId,
-                               { hiddenRows: this._hiddenRows,
-                                 forceReadOnly: this._readOnly });
-  },
-
-  _fillAddProperties: function BPP__fillAddProperties() {
-    // Edit the new item
-    gEditItemOverlay.initPanel(this._itemId,
-                               { hiddenRows: this._hiddenRows });
-    // Empty location field if the uri is about:blank, this way inserting a new
-    // url will be easier for the user, Accept button will be automatically
-    // disabled by the input listener until the user fills the field.
-    var locationField = this._element("locationField");
-    if (locationField.value == "about:blank")
-      locationField.value = "";
-  },
-
   // nsISupports
   QueryInterface: function BPP_QueryInterface(aIID) {
     if (aIID.equals(Ci.nsIDOMEventListener) ||
         aIID.equals(Ci.nsISupports))
       return this;
 
     throw Cr.NS_NOINTERFACE;
   },
 
   _element: function BPP__element(aID) {
     return document.getElementById("editBMPanel_" + aID);
   },
 
-  onDialogUnload: function BPP_onDialogUnload() {
-
-  // gEditItemOverlay does not exist anymore here, so don't rely on it.
+  onDialogUnload() {
+    // gEditItemOverlay does not exist anymore here, so don't rely on it.
     this._mutationObserver.disconnect();
     delete this._mutationObserver;
 
     window.removeEventListener("resize", this);
+
     // Calling removeEventListener with arguments which do not identify any
     // currently registered EventListener on the EventTarget has no effect.
     this._element("locationField")
         .removeEventListener("input", this);
-    this._element("feedLocationField")
-        .removeEventListener("input", this);
-    this._element("siteLocationField")
-        .removeEventListener("input", this);
-
   },
 
-  onDialogAccept: function BPP_onDialogAccept() {
+  onDialogAccept() {
     // We must blur current focused element to save its changes correctly
     document.commandDispatcher.focusedElement.blur();
     // The order here is important! We have to uninit the panel first, otherwise
     // late changes could force it to commit more transactions.
-    gEditItemOverlay.uninitPanel(false);
-    gEditItemOverlay = null;
+    gEditItemOverlay.uninitPanel(true);
     this._endBatch();
     window.arguments[0].performed = true;
   },
 
-  onDialogCancel: function BPP_onDialogCancel() {
+  onDialogCancel() {
     // The order here is important! We have to uninit the panel first, otherwise
     // changes done as part of Undo may change the panel contents and by
     // that force it to commit more transactions.
-    gEditItemOverlay.uninitPanel(false);
-    gEditItemOverlay = null;
+    gEditItemOverlay.uninitPanel(true);
     this._endBatch();
-    PlacesUtils.transactionManager.undoTransaction();
+    if (PlacesUIUtils.useAsyncTransactions)
+      PlacesTransactions.undo().catch(Cu.reportError);
+    else
+      PlacesUtils.transactionManager.undoTransaction();
     window.arguments[0].performed = false;
   },
 
   /**
    * This method checks to see if the input fields are in a valid state.
    *
    * @returns  true if the input is valid, false otherwise
    */
@@ -532,63 +501,70 @@ var BookmarkPropertiesPanel = {
   },
 
   /**
    * Returns a transaction for creating a new bookmark item representing the
    * various fields and opening arguments of the dialog.
    */
   _getCreateNewBookmarkTransaction:
   function BPP__getCreateNewBookmarkTransaction(aContainer, aIndex) {
-    const nsIAnnotationService = Ci.nsIAnnotationService;
     var annotations = [];
     var childTransactions = [];
 
     if (this._description) {
-      let annoObj = { name   : PlacesUIUtils.DESCRIPTION_ANNO,
-                      type   : nsIAnnotationService.TYPE_STRING,
-                      flags  : 0,
-                      value  : this._description,
-                      expires: nsIAnnotationService.EXPIRE_NEVER };
+      let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO,
+                      type: Ci.nsIAnnotationService.TYPE_STRING,
+                      flags: 0,
+                      value: this._description,
+                      expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
       let editItemTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj);
       childTransactions.push(editItemTxn);
     }
 
-    if (this._postData) {
-     let postDataTxn = new PlacesEditBookmarkPostDataTransaction(-1, this._postData);
-     childTransactions.push(postDataTxn);
+    if (this._loadInSidebar) {
+      let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
+                      value: true };
+      let setLoadTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj);
+      childTransactions.push(setLoadTxn);
     }
 
-    //XXX TODO: this should be in a transaction!
-    if (this._charSet)
+    // XXX TODO: this should be in a transaction!
+    if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window))
       PlacesUtils.setCharsetForURI(this._uri, this._charSet);
 
     let createTxn = new PlacesCreateBookmarkTransaction(this._uri,
                                                         aContainer,
                                                         aIndex,
                                                         this._title,
                                                         this._keyword,
                                                         annotations,
-                                                        childTransactions);
+                                                        childTransactions,
+                                                        this._postData);
 
     return new PlacesAggregatedTransaction(this._getDialogTitle(),
                                            [createTxn]);
   },
 
   /**
    * Returns a childItems-transactions array representing the URIList with
    * which the dialog has been opened.
    */
   _getTransactionsForURIList: function BPP__getTransactionsForURIList() {
     var transactions = [];
-    for (var i = 0; i < this._URIs.length; ++i) {
-      var uri = this._URIs[i];
-      var title = this._titles[i] || this._getURITitleFromHistory(uri);
-      var createTxn = new PlacesCreateBookmarkTransaction(uri, -1,
-                                                          PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                                          title);
+    for (let uri of this._URIs) {
+      // uri should be an object in the form { uri, title }. Though add-ons
+      // could still use the legacy form, where it's an nsIURI.
+      // TODO: Remove This from v57 on.
+      let [_uri, _title] = uri instanceof Ci.nsIURI ?
+        [uri, this._getURITitleFromHistory(uri)] : [uri.uri, uri.title];
+
+      let createTxn =
+        new PlacesCreateBookmarkTransaction(_uri, -1,
+                                            PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                            _title);
       transactions.push(createTxn);
     }
     return transactions;
   },
 
   /**
    * Returns a transaction for creating a new folder item representing the
    * various fields and opening arguments of the dialog.
@@ -603,48 +579,118 @@ var BookmarkPropertiesPanel = {
     if (this._description)
       annotations.push(this._getDescriptionAnnotation(this._description));
 
     return new PlacesCreateFolderTransaction(this._title, aContainer,
                                              aIndex, annotations,
                                              childItemsTransactions);
   },
 
-  /**
-   * Returns a transaction for creating a new live-bookmark item representing
-   * the various fields and opening arguments of the dialog.
-   */
-  _getCreateNewLivemarkTransaction:
-  function BPP__getCreateNewLivemarkTransaction(aContainer, aIndex) {
-    return new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI,
-                                               this._title,
-                                               aContainer, aIndex);
-  },
-
-  /**
-   * Dialog-accept code-path for creating a new item (any type)
-   */
-  _createNewItem: function BPP__getCreateItemTransaction() {
-    var [container, index] = this._getInsertionPointDetails();
-    var txn;
-
+  async _createNewItem() {
+    let [container, index] = this._getInsertionPointDetails();
+    let txn;
     switch (this._itemType) {
       case BOOKMARK_FOLDER:
         txn = this._getCreateNewFolderTransaction(container, index);
         break;
       case LIVEMARK_CONTAINER:
-        txn = this._getCreateNewLivemarkTransaction(container, index);
+        txn = new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI,
+                                                  this._title, container, index);
         break;
       default: // BOOKMARK_ITEM
         txn = this._getCreateNewBookmarkTransaction(container, index);
     }
 
     PlacesUtils.transactionManager.doTransaction(txn);
-    var promise = txn._promise || {
-      then: function(callback) {
-        callback();
-        return this;
+    // This is a temporary hack until we use PlacesTransactions.jsm
+    if (txn._promise) {
+      await txn._promise;
+    }
+
+    let folderGuid = await PlacesUtils.promiseItemGuid(container);
+    let bm = await PlacesUtils.bookmarks.fetch({
+      parentGuid: folderGuid,
+      index
+    });
+    this._itemId = await PlacesUtils.promiseItemId(bm.guid);
+
+    return Object.freeze({
+      itemId: this._itemId,
+      bookmarkGuid: bm.guid,
+      title: this._title,
+      uri: this._uri ? this._uri.spec : "",
+      type: this._itemType == BOOKMARK_ITEM ?
+              Ci.nsINavHistoryResultNode.RESULT_TYPE_URI :
+              Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+      parent: {
+        itemId: container,
+        bookmarkGuid: await PlacesUtils.promiseItemGuid(container),
+        type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
       }
-    };
-    return promise.then(() =>
-      this._itemId = PlacesUtils.bookmarks.getIdForItemAt(container, index));
+    });
+  },
+
+  async _promiseNewItem() {
+    if (!PlacesUIUtils.useAsyncTransactions)
+      return this._createNewItem();
+
+    let [containerId, index] = this._getInsertionPointDetails();
+    let parentGuid = await PlacesUtils.promiseItemGuid(containerId);
+    let annotations = [];
+    if (this._description) {
+      annotations.push({ name: PlacesUIUtils.DESCRIPTION_ANNO,
+                         value: this._description });
+    }
+    if (this._loadInSidebar) {
+      annotations.push({ name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
+                         value: true });
+    }
+
+    let itemGuid;
+    let info = { parentGuid, index, title: this._title, annotations };
+    if (this._itemType == BOOKMARK_ITEM) {
+      info.url = this._uri;
+      if (this._keyword)
+        info.keyword = this._keyword;
+      if (this._postData)
+        info.postData = this._postData;
+
+      if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window))
+        PlacesUtils.setCharsetForURI(this._uri, this._charSet);
+
+      itemGuid = await PlacesTransactions.NewBookmark(info).transact();
+    } else if (this._itemType == LIVEMARK_CONTAINER) {
+      info.feedUrl = this._feedURI;
+      if (this._siteURI)
+        info.siteUrl = this._siteURI;
+
+      itemGuid = await PlacesTransactions.NewLivemark(info).transact();
+    } else if (this._itemType == BOOKMARK_FOLDER) {
+      itemGuid = await PlacesTransactions.NewFolder(info).transact();
+      // URIs is an array of objects in the form { uri, title }.  It is still
+      // named URIs because for backwards compatibility it could also be an
+      // array of nsIURIs. TODO: Fix the property names from v57.
+      for (let { uri: url, title } of this._URIs) {
+        await PlacesTransactions.NewBookmark({ parentGuid: itemGuid, url, title })
+                                .transact();
+      }
+    } else {
+      throw new Error(`unexpected value for _itemType:  ${this._itemType}`);
+    }
+
+    this._itemGuid = itemGuid;
+    this._itemId = await PlacesUtils.promiseItemId(itemGuid);
+    return Object.freeze({
+      itemId: this._itemId,
+      bookmarkGuid: this._itemGuid,
+      title: this._title,
+      uri: this._uri ? this._uri.spec : "",
+      type: this._itemType == BOOKMARK_ITEM ?
+              Ci.nsINavHistoryResultNode.RESULT_TYPE_URI :
+              Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+      parent: {
+        itemId: containerId,
+        bookmarkGuid: parentGuid,
+        type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
+      }
+    });
   }
 };
--- a/suite/common/places/content/bookmarkProperties.xul
+++ b/suite/common/places/content/bookmarkProperties.xul
@@ -1,43 +1,43 @@
 <?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/. -->
 
 <?xml-stylesheet href="chrome://communicator/skin/"?>
 <?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
-<?xml-stylesheet href="chrome://communicator/skin/bookmarks/editBookmarkOverlay.css"?>
-<?xml-stylesheet href="chrome://communicator/skin/bookmarks/bookmarks.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/bookmarks.css"?>
 
-<?xul-overlay href="chrome://communicator/content/bookmarks/placesOverlay.xul"?>
-<?xul-overlay href="chrome://communicator/content/bookmarks/editBookmarkOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/places/editBookmarkOverlay.xul"?>
 
 <!DOCTYPE dialog [
-  <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://communicator/locale/bookmarks/editBookmarkOverlay.dtd">
+  <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://communicator/locale/places/editBookmarkOverlay.dtd">
   %editBookmarkOverlayDTD;
 ]>
 
 <dialog id="bookmarkproperties"
         buttons="accept, cancel"
         buttoniconaccept="save"
         ondialogaccept="BookmarkPropertiesPanel.onDialogAccept();"
         ondialogcancel="BookmarkPropertiesPanel.onDialogCancel();"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="BookmarkPropertiesPanel.onDialogLoad();"
         onunload="BookmarkPropertiesPanel.onDialogUnload();"
         style="min-width: 30em;"
-        persist="screenX screenY height width">
+        persist="screenX screenY width">
 
   <stringbundleset id="stringbundleset">
     <stringbundle id="stringBundle"
-                  src="chrome://communicator/locale/bookmarks/bm-props.properties"/>
+                  src="chrome://communicator/locale/places/bookmarkProperties.properties"/>
   </stringbundleset>
 
   <script type="application/javascript"
-          src="chrome://communicator/content/bookmarks/editBookmarkOverlay.js"/>
+          src="chrome://communicator/content/places/editBookmarkOverlay.js"/>
   <script type="application/javascript"
-          src="chrome://communicator/content/bookmarks/bm-props.js"/>
+          src="chrome://communicator/content/places/bookmarkProperties.js"/>
 
 <vbox id="editBookmarkPanelContent"/>
 
 </dialog>
--- a/suite/common/places/content/bookmarksPanel.js
+++ b/suite/common/places/content/bookmarksPanel.js
@@ -1,20 +1,24 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* 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 init() {
   document.getElementById("bookmarks-view").place =
-    "place:queryType=1&folder=" + PlacesUIUtils.allBookmarksFolderId;
+    "place:queryType=1&folder=" + window.top.PlacesUIUtils.allBookmarksFolderId;
 }
 
 function searchBookmarks(aSearchString) {
-  var tree = document.getElementById('bookmarks-view');
+  var tree = document.getElementById("bookmarks-view");
   if (!aSearchString)
     tree.place = tree.place;
   else
     tree.applyFilter(aSearchString,
                      [PlacesUtils.bookmarksMenuFolderId,
                       PlacesUtils.unfiledBookmarksFolderId,
-                      PlacesUtils.toolbarFolderId]);
+                      PlacesUtils.toolbarFolderId,
+                      PlacesUtils.mobileFolderId]);
 }
 
+window.addEventListener("SidebarFocused",
+                        () => document.getElementById("search-box").focus());
--- a/suite/common/places/content/bookmarksPanel.xul
+++ b/suite/common/places/content/bookmarksPanel.xul
@@ -1,48 +1,46 @@
-<?xml version="1.0"?>
-
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
 <!-- 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/. -->
 
 <?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sidebar/sidebarListView.css" type="text/css"?>
 <?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
-<?xml-stylesheet href="chrome://communicator/skin/sidebar/sidebarListView.css" type="text/css"?>
-<?xml-stylesheet href="chrome://communicator/skin/bookmarks/bookmarks.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/bookmarks.css"?>
 
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
-<?xul-overlay href="chrome://communicator/content/bookmarks/placesOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
 <?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
 
-<!DOCTYPE page SYSTEM "chrome://communicator/locale/bookmarks/places.dtd">
+<!DOCTYPE page SYSTEM "chrome://communicator/locale/places/places.dtd">
 
 <page id="bookmarksPanel"
       xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
       onload="init();"
-      onunload="SidebarUtils.setMouseoverURL('');"
-      elementtofocus="search-box">
+      onunload="SidebarUtils.setMouseoverURL('');">
 
   <script type="application/javascript"
-          src="chrome://communicator/content/places/sidebarUtils.js"/>
+          src="chrome://communicator/content/bookmarks/sidebarUtils.js"/>
   <script type="application/javascript"
-          src="chrome://communicator/content/bookmarks/bm-panel.js"/>
+          src="chrome://communicator/content/bookmarks/bookmarksPanel.js"/>
 
   <commandset id="placesCommands"/>
   <commandset id="editMenuCommands"/>
   <menupopup id="placesContext"/>
 
   <!-- Bookmarks and history tooltip -->
   <tooltip id="bhTooltip"/>
 
   <hbox id="sidebar-search-container" align="center">
-    <textbox id="search-box" flex="1" type="search" class="compact"
+    <textbox id="search-box" flex="1" type="search"
+             placeholder="&search.placeholder;"
              aria-controls="bookmarks-view"
-             placeholder="&search.placeholder;"
              oncommand="searchBookmarks(this.value);"/>
   </hbox>
 
   <tree id="bookmarks-view" class="sidebar-placesTree" type="places"
         flex="1"
         hidecolumnpicker="true"
         treelines="true"
         context="placesContext"
--- a/suite/common/places/content/browserPlacesViews.js
+++ b/suite/common/places/content/browserPlacesViews.js
@@ -1,32 +1,40 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
+ * 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/. */
 
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+/* eslint-env mozilla/browser-window */
 
 /**
  * The base view implements everything that's common to the toolbar and
  * menu views.
  */
-function PlacesViewBase(aPlace) {
+function PlacesViewBase(aPlace, aOptions = {}) {
+  if ("rootElt" in aOptions)
+    this._rootElt = aOptions.rootElt;
+  if ("viewElt" in aOptions)
+    this._viewElt = aOptions.viewElt;
+  this.options = aOptions;
+  this._controller = new PlacesController(this);
   this.place = aPlace;
-  this._controller = new PlacesController(this);
   this._viewElt.controllers.appendController(this._controller);
 }
 
 PlacesViewBase.prototype = {
   // The xul element that holds the entire view.
   _viewElt: null,
   get viewElt() {
     return this._viewElt;
   },
 
+  get associatedElement() {
+    return this._viewElt;
+  },
+
   get controllers() {
     return this._viewElt.controllers;
   },
 
   // The xul element that represents the root container.
   _rootElt: null,
 
   // Set to true for views that are represented by native widgets (i.e.
@@ -76,25 +84,39 @@ PlacesViewBase.prototype = {
     if (val) {
       this._resultNode = val.root;
       this._rootElt._placesNode = this._resultNode;
       this._domNodes = new Map();
       this._domNodes.set(this._resultNode, this._rootElt);
 
       // This calls _rebuild through invalidateContainer.
       this._resultNode.containerOpen = true;
-    }
-    else {
+    } else {
       this._resultNode = null;
       delete this._domNodes;
     }
 
     return val;
   },
 
+  _options: null,
+  get options() {
+    return this._options;
+  },
+  set options(val) {
+    if (!val)
+      val = {};
+
+    if (!("extraClasses" in val))
+      val.extraClasses = {};
+    this._options = val;
+
+    return val;
+  },
+
   /**
    * Gets the DOM node used for the given places node.
    *
    * @param aPlacesNode
    *        a places result node.
    * @throws if there is no DOM node set for aPlacesNode.
    */
   _getDOMNodeForPlacesNode:
@@ -109,23 +131,30 @@ PlacesViewBase.prototype = {
 
   get controller() {
     return this._controller;
   },
 
   get selType() {
     return "single";
   },
-  selectItems: function() { },
-  selectAll: function() { },
+  selectItems() { },
+  selectAll() { },
 
   get selectedNode() {
     if (this._contextMenuShown) {
-      let popup = document.popupNode;
-      return popup._placesNode || popup.parentNode._placesNode || null;
+      let anchor = this._contextMenuShown.triggerNode;
+      if (!anchor)
+        return null;
+
+      if (anchor._placesNode)
+        return this._rootElt == anchor ? null : anchor._placesNode;
+
+      anchor = anchor.parentNode;
+      return this._rootElt == anchor ? null : (anchor._placesNode || null);
     }
     return null;
   },
 
   get hasSelection() {
     return this.selectedNode != null;
   },
 
@@ -157,210 +186,209 @@ PlacesViewBase.prototype = {
         PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
           Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
       return null;
 
     // By default, the insertion point is at the top level, at the end.
     let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
     let container = this._resultNode;
     let orientation = Ci.nsITreeView.DROP_BEFORE;
-    let isTag = false;
+    let tagName = null;
 
     let selectedNode = this.selectedNode;
     if (selectedNode) {
       let popup = document.popupNode;
-      if (!popup._placesNode ||
-          popup._placesNode == this._resultNode ||
-          popup._placesNode.itemId == -1) {
+      if (!popup._placesNode || popup._placesNode == this._resultNode ||
+          popup._placesNode.itemId == -1 || !selectedNode.parent) {
         // If a static menuitem is selected, or if the root node is selected,
         // the insertion point is inside the folder, at the end.
         container = selectedNode;
         orientation = Ci.nsITreeView.DROP_ON;
-      }
-      else {
+      } else {
         // In all other cases the insertion point is before that node.
         container = selectedNode.parent;
         index = container.getChildIndex(selectedNode);
-        isTag = PlacesUtils.nodeIsTagQuery(container);
+        if (PlacesUtils.nodeIsTagQuery(container)) {
+          tagName = container.title;
+          // TODO (Bug 1160193): properly support dropping on a tag root.
+          if (!tagName)
+            return null;
+        }
       }
     }
 
     if (PlacesControllerDragHelper.disallowInsertion(container))
       return null;
 
     return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
-                              index, orientation, isTag);
+                              index, orientation, tagName);
   },
 
   buildContextMenu: function PVB_buildContextMenu(aPopup) {
-    this._contextMenuShown = true;
+    this._contextMenuShown = aPopup;
     window.updateCommands("places");
     return this.controller.buildContextMenu(aPopup);
   },
 
   destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
-    this._contextMenuShown = false;
+    this._contextMenuShown = null;
+  },
+
+  clearAllContents(aPopup) {
+    while (aPopup.firstChild) {
+      aPopup.firstChild.remove();
+    }
+    aPopup._emptyMenuitem = aPopup._startMarker = aPopup._endMarker = null;
   },
 
-  _cleanPopup: function PVB_cleanPopup(aPopup) {
-    // Remove places popup children and update markers to keep track of
-    // their indices.
-    let start = aPopup._startMarker != -1 ? aPopup._startMarker + 1 : 0;
-    let end = aPopup._endMarker != -1 ? aPopup._endMarker :
-                                        aPopup.childNodes.length;
-    let items = [];
-    let placesNodeFound = false;
-    for (let i = start; i < end; ++i) {
-      let item = aPopup.childNodes[i];
-      if (item.getAttribute("builder") == "end") {
-        // we need to do this for menus that have static content at the end but
-        // are initially empty, eg. the history menu, we need to know where to
-        // start inserting new items.
-        aPopup._endMarker = i;
-        break;
+  _cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
+    // Ensure markers are here when `invalidateContainer` is called before the
+    // popup is shown, which may the case for panelviews, for example.
+    this._ensureMarkers(aPopup);
+    // Remove Places nodes from the popup.
+    let child = aPopup._startMarker;
+    while (child.nextSibling != aPopup._endMarker) {
+      let sibling = child.nextSibling;
+      if (sibling._placesNode && !aDelay) {
+        aPopup.removeChild(sibling);
+      } else if (sibling._placesNode && aDelay) {
+        // HACK (bug 733419): the popups originating from the OS X native
+        // menubar don't live-update while open, thus we don't clean it
+        // until the next popupshowing, to avoid zombie menuitems.
+        if (!aPopup._delayedRemovals)
+          aPopup._delayedRemovals = [];
+        aPopup._delayedRemovals.push(sibling);
+        child = child.nextSibling;
+      } else {
+        child = child.nextSibling;
       }
-      if (item._placesNode) {
-        items.push(item);
-        placesNodeFound = true;
-      }
-      else {
-        // This is static content...
-        if (!placesNodeFound)
-          // ...at the start of the popup
-          // Initialized in menu.xml, in the base binding
-          aPopup._startMarker++;
-        else {
-          // ...after places nodes
-          aPopup._endMarker = i;
-          break;
-        }
-      }
-    }
-
-    for (let i = 0; i < items.length; ++i) {
-      items[i].remove();
-      if (aPopup._endMarker != -1)
-        aPopup._endMarker--;
     }
   },
 
   _rebuildPopup: function PVB__rebuildPopup(aPopup) {
-    this._cleanPopup(aPopup);
-
     let resultNode = aPopup._placesNode;
     if (!resultNode.containerOpen)
       return;
 
     if (this.controller.hasCachedLivemarkInfo(resultNode)) {
-      aPopup.removeAttribute("emptyplacesresult");
-      if (aPopup._emptyMenuItem)
-        aPopup._emptyMenuItem.hidden = true;
+      this._setEmptyPopupStatus(aPopup, false);
       aPopup._built = true;
       this._populateLivemarkPopup(aPopup);
       return;
     }
 
+    this._cleanPopup(aPopup);
+
     let cc = resultNode.childCount;
     if (cc > 0) {
-      aPopup.removeAttribute("emptyplacesresult");
-      if (aPopup._emptyMenuItem)
-        aPopup._emptyMenuItem.hidden = true;
+      this._setEmptyPopupStatus(aPopup, false);
 
       for (let i = 0; i < cc; ++i) {
         let child = resultNode.getChild(i);
         this._insertNewItemToPopup(child, aPopup, null);
       }
-    }
-    else {
-      aPopup.setAttribute("emptyplacesresult", "true");
-      // This menu is empty.  If there is no static content, add
-      // an element to show it is empty.
-      if (aPopup._startMarker == -1 && aPopup._endMarker == -1)
-        this._showEmptyMenuItem(aPopup);
+    } else {
+      this._setEmptyPopupStatus(aPopup, true);
     }
     aPopup._built = true;
   },
 
   _removeChild: function PVB__removeChild(aChild) {
     // If document.popupNode pointed to this child, null it out,
     // otherwise controller's command-updating may rely on the removed
     // item still being "selected".
     if (document.popupNode == aChild)
       document.popupNode = null;
 
     aChild.remove();
   },
 
-  _showEmptyMenuItem: function PVB__showEmptyMenuItem(aPopup) {
-    if (aPopup._emptyMenuItem) {
-      aPopup._emptyMenuItem.hidden = false;
-      return;
+  _setEmptyPopupStatus:
+  function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
+    if (!aPopup._emptyMenuitem) {
+      let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
+      aPopup._emptyMenuitem = document.createElement("menuitem");
+      aPopup._emptyMenuitem.setAttribute("label", label);
+      aPopup._emptyMenuitem.setAttribute("disabled", true);
+      aPopup._emptyMenuitem.className = "bookmark-item";
+      if (typeof this.options.extraClasses.entry == "string")
+        aPopup._emptyMenuitem.classList.add(this.options.extraClasses.entry);
     }
 
-    let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
-    aPopup._emptyMenuItem = document.createElement("menuitem");
-    aPopup._emptyMenuItem.setAttribute("label", label);
-    aPopup._emptyMenuItem.setAttribute("disabled", true);
-    aPopup.appendChild(aPopup._emptyMenuItem);
+    if (aEmpty) {
+      aPopup.setAttribute("emptyplacesresult", "true");
+      // Don't add the menuitem if there is static content.
+      if (!aPopup._startMarker.previousSibling &&
+          !aPopup._endMarker.nextSibling)
+        aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
+    } else {
+      aPopup.removeAttribute("emptyplacesresult");
+      try {
+        aPopup.removeChild(aPopup._emptyMenuitem);
+      } catch (ex) {}
+    }
   },
 
-  _createMenuItemForPlacesNode:
-  function PVB__createMenuItemForPlacesNode(aPlacesNode) {
+  _createDOMNodeForPlacesNode:
+  function PVB__createDOMNodeForPlacesNode(aPlacesNode) {
     this._domNodes.delete(aPlacesNode);
 
     let element;
     let type = aPlacesNode.type;
-    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR)
+    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
       element = document.createElement("menuseparator");
-    else {
+      element.setAttribute("class", "small-separator");
+    } else {
+      let itemId = aPlacesNode.itemId;
       if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
         element = document.createElement("menuitem");
         element.className = "menuitem-iconic bookmark-item menuitem-with-favicon";
         element.setAttribute("scheme",
                              PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
-      }
-      else if (PlacesUtils.containerTypes.indexOf(type) != -1) {
+      } else if (PlacesUtils.containerTypes.includes(type)) {
         element = document.createElement("menu");
         element.setAttribute("container", "true");
 
         if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
           element.setAttribute("query", "true");
           if (PlacesUtils.nodeIsTagQuery(aPlacesNode))
             element.setAttribute("tagContainer", "true");
           else if (PlacesUtils.nodeIsDay(aPlacesNode))
             element.setAttribute("dayContainer", "true");
           else if (PlacesUtils.nodeIsHost(aPlacesNode))
             element.setAttribute("hostContainer", "true");
-        }
-        else if (aPlacesNode.itemId != -1) {
-          PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
-                               .then(aLivemark => {
-            element.setAttribute("livemark", "true");
-            this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
-          }, () => undefined);
+        } else if (itemId != -1) {
+          PlacesUtils.livemarks.getLivemark({ id: itemId })
+            .then(aLivemark => {
+              element.setAttribute("livemark", "true");
+              if (AppConstants.platform === "macosx") {
+                // OS X native menubar doesn't track list-style-images since
+                // it doesn't have a frame (bug 733415).  Thus enforce updating.
+                element.setAttribute("image", "");
+                element.removeAttribute("image");
+              }
+              this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+            }, () => undefined);
         }
 
         let popup = document.createElement("menupopup");
         popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
-        if (this._nativeView) {
-          popup._startMarker = -1;
-          popup._endMarker = -1;
+
+        if (!this._nativeView) {
+          popup.setAttribute("placespopup", "true");
         }
-        else
-          popup.setAttribute("placespopup", "true");
-#ifdef XP_MACOSX
-        // No context menu on mac.
-        popup.setAttribute("context", "placesContext");
-#endif
+
         element.appendChild(popup);
         element.className = "menu-iconic bookmark-item";
+        if (typeof this.options.extraClasses.entry == "string") {
+          element.classList.add(this.options.extraClasses.entry);
+        }
 
         this._domNodes.set(aPlacesNode, popup);
-      }
-      else
+      } else
         throw "Unexpected node";
 
       element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
 
       let icon = aPlacesNode.icon;
       if (icon)
         element.setAttribute("image", icon);
     }
@@ -369,119 +397,111 @@ PlacesViewBase.prototype = {
     if (!this._domNodes.has(aPlacesNode))
       this._domNodes.set(aPlacesNode, element);
 
     return element;
   },
 
   _insertNewItemToPopup:
   function PVB__insertNewItemToPopup(aNewChild, aPopup, aBefore) {
-    let element = this._createMenuItemForPlacesNode(aNewChild);
+    let element = this._createDOMNodeForPlacesNode(aNewChild);
+    let before = aBefore || aPopup._endMarker;
 
-    if (aBefore) {
-      aPopup.insertBefore(element, aBefore);
-    }
-    else {
-      // Add the new element to the menu.  If there is static content at
-      // the end of the menu, add the element before that.  Otherwise,
-      // just add to the end.
-      if (aPopup._endMarker != -1) {
-        let lastElt = aPopup.childNodes[aPopup._endMarker];
-        aPopup.insertBefore(element, lastElt);
-      }
-      else {
-        aPopup.appendChild(element);
-      }
+    if (element.localName == "menuitem" || element.localName == "menu") {
+      if (typeof this.options.extraClasses.entry == "string")
+        element.classList.add(this.options.extraClasses.entry);
     }
 
-    if (aPopup._endMarker != -1)
-      aPopup._endMarker++;
-
+    aPopup.insertBefore(element, before);
     return element;
   },
 
   _setLivemarkSiteURIMenuItem:
   function PVB__setLivemarkSiteURIMenuItem(aPopup) {
     let livemarkInfo = this.controller.getCachedLivemarkInfo(aPopup._placesNode);
-    let siteUrl = livemarkInfo && livemarkInfo.siteURI && livemarkInfo.siteURI.spec;
+    let siteUrl = livemarkInfo && livemarkInfo.siteURI ?
+                  livemarkInfo.siteURI.spec : null;
     if (!siteUrl && aPopup._siteURIMenuitem) {
-      aPopup._siteURIMenuitem.remove();
+      aPopup.removeChild(aPopup._siteURIMenuitem);
       aPopup._siteURIMenuitem = null;
-      aPopup._startMarker--;
-      aPopup._siteURIMenuseparator.remove();
+      aPopup.removeChild(aPopup._siteURIMenuseparator);
       aPopup._siteURIMenuseparator = null;
-      aPopup._startMarker--;
-    }
-    else if (siteUrl && !aPopup._siteURIMenuitem) {
+    } else if (siteUrl && !aPopup._siteURIMenuitem) {
       // Add "Open (Feed Name)" menuitem.
       aPopup._siteURIMenuitem = document.createElement("menuitem");
       aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem";
+      if (typeof this.options.extraClasses.entry == "string") {
+        aPopup._siteURIMenuitem.classList.add(this.options.extraClasses.entry);
+      }
       aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl);
       aPopup._siteURIMenuitem.setAttribute("oncommand",
         "openUILink(this.getAttribute('targetURI'), event);");
 
       // If a user middle-clicks this item we serve the oncommand event.
       // We are using checkForMiddleClick because of Bug 246720.
       // Note: stopPropagation is needed to avoid serving middle-click
       // with BT_onClick that would open all items in tabs.
       aPopup._siteURIMenuitem.setAttribute("onclick",
         "checkForMiddleClick(this, event); event.stopPropagation();");
       let label =
         PlacesUIUtils.getFormattedString("menuOpenLivemarkOrigin.label",
                                          [aPopup.parentNode.getAttribute("label")])
       aPopup._siteURIMenuitem.setAttribute("label", label);
-      aPopup.insertBefore(aPopup._siteURIMenuitem,
-                          aPopup.childNodes.item(aPopup._startMarker + 1));
-      aPopup._startMarker++;
+      aPopup.insertBefore(aPopup._siteURIMenuitem, aPopup._startMarker);
 
       aPopup._siteURIMenuseparator = document.createElement("menuseparator");
-      aPopup.insertBefore(aPopup._siteURIMenuseparator,
-                         aPopup.childNodes.item(aPopup._startMarker + 1));
-      aPopup._startMarker++;
+      aPopup.insertBefore(aPopup._siteURIMenuseparator, aPopup._startMarker);
     }
   },
 
   /**
    * Add, update or remove the livemark status menuitem.
    * @param aPopup
    *        The livemark container popup
    * @param aStatus
    *        The livemark status
    */
   _setLivemarkStatusMenuItem:
   function PVB_setLivemarkStatusMenuItem(aPopup, aStatus) {
-    let itemId = aPopup._placesNode.itemId;
-    let lmStatus = null;
-    if (aStatus == Ci.mozILivemark.STATUS_LOADING)
-      lmStatus = "bookmarksLivemarkLoading";
-    else if (aStatus == Ci.mozILivemark.STATUS_FAILED)
-      lmStatus = "bookmarksLivemarkFailed";
-
-    let lmStatusElt = aPopup._lmStatusMenuItem;
-    if (lmStatus && !lmStatusElt) {
+    let statusMenuitem = aPopup._statusMenuitem;
+    if (!statusMenuitem) {
       // Create the status menuitem and cache it in the popup object.
-      lmStatusElt = document.createElement("menuitem");
-      lmStatusElt.setAttribute("lmStatus", lmStatus);
-      lmStatusElt.setAttribute("label", PlacesUIUtils.getString(lmStatus));
-      lmStatusElt.setAttribute("disabled", true);
-      aPopup.insertBefore(lmStatusElt,
-                          aPopup.childNodes.item(aPopup._startMarker + 1));
-      aPopup._lmStatusMenuItem = lmStatusElt;
-      aPopup._startMarker++;
+      statusMenuitem = document.createElement("menuitem");
+      statusMenuitem.className = "livemarkstatus-menuitem";
+      if (typeof this.options.extraClasses.entry == "string") {
+        statusMenuitem.classList.add(this.options.extraClasses.entry);
+      }
+      statusMenuitem.setAttribute("disabled", true);
+      aPopup._statusMenuitem = statusMenuitem;
     }
-    else if (lmStatus && lmStatusElt.getAttribute("lmStatus") != lmStatus) {
+
+    if (aStatus == Ci.mozILivemark.STATUS_LOADING ||
+        aStatus == Ci.mozILivemark.STATUS_FAILED) {
       // Status has changed, update the cached status menuitem.
-      lmStatusElt.setAttribute("label", PlacesUIUtils.getString(lmStatus));
+      let stringId = aStatus == Ci.mozILivemark.STATUS_LOADING ?
+                       "bookmarksLivemarkLoading" : "bookmarksLivemarkFailed";
+      statusMenuitem.setAttribute("label", PlacesUIUtils.getString(stringId));
+      if (aPopup._startMarker.nextSibling != statusMenuitem)
+        aPopup.insertBefore(statusMenuitem, aPopup._startMarker.nextSibling);
+    } else if (aPopup._statusMenuitem.parentNode == aPopup) {
+      // The livemark has finished loading.
+      aPopup.removeChild(aPopup._statusMenuitem);
     }
-    else if (!lmStatus && lmStatusElt) {
-      // The livemark has finished loading.
-      aPopup._lmStatusMenuItem.remove();
-      aPopup._lmStatusMenuItem = null;
-      aPopup._startMarker--;
-    }
+  },
+
+  toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) {
+    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+    // We may get the popup for menus, but we need the menu itself.
+    if (elt.localName == "menupopup")
+      elt = elt.parentNode;
+    if (aValue)
+      elt.setAttribute("cutting", "true");
+    else
+      elt.removeAttribute("cutting");
   },
 
   nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
 
     // Here we need the <menu>.
     if (elt.localName == "menupopup")
       elt = elt.parentNode;
@@ -493,40 +513,48 @@ PlacesViewBase.prototype = {
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
 
     // There's no UI representation for the root node, thus there's nothing to
     // be done when the icon changes.
     if (elt == this._rootElt)
       return;
 
     // Here we need the <menu>.
-    if (elt.localName == "menupopup")
+    if (elt.localName == "menupopup") {
       elt = elt.parentNode;
-
-    let icon = aPlacesNode.icon;
-    if (!icon)
-      elt.removeAttribute("image");
-    else if (icon != elt.getAttribute("image"))
-      elt.setAttribute("image", icon);
+    }
+    // We must remove and reset the attribute to force an update.
+    elt.removeAttribute("image");
+    elt.setAttribute("image", aPlacesNode.icon);
   },
 
   nodeAnnotationChanged:
   function PVB_nodeAnnotationChanged(aPlacesNode, aAnno) {
-    // All livemarks have a feedURI, so use it as our indicator.
+    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+    // All livemarks have a feedURI, so use it as our indicator of a livemark
+    // being modified.
     if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
-      let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
       let menu = elt.parentNode;
-      if (!menu.hasAttribute("livemark"))
+      if (!menu.hasAttribute("livemark")) {
         menu.setAttribute("livemark", "true");
+        if (AppConstants.platform === "macosx") {
+          // OS X native menubar doesn't track list-style-images since
+          // it doesn't have a frame (bug 733415).  Thus enforce updating.
+          menu.setAttribute("image", "");
+          menu.removeAttribute("image");
+        }
+      }
 
       PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
-                           .then(aLivemark => {
-        this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
-        this.invalidateContainer(aPlacesNode);
-      }, () => undefined);
+        .then(aLivemark => {
+          // Controller will use this to build the meta data for the node.
+          this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+          this.invalidateContainer(aPlacesNode);
+        }, () => undefined);
     }
   },
 
   nodeTitleChanged:
   function PVB_nodeTitleChanged(aPlacesNode, aNewTitle) {
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
 
     // There's no UI representation for the root node, thus there's
@@ -538,84 +566,79 @@ PlacesViewBase.prototype = {
     if (elt.localName == "menupopup")
       elt = elt.parentNode;
 
     if (!aNewTitle && elt.localName != "toolbarbutton") {
       // Many users consider toolbars as shortcuts containers, so explicitly
       // allow empty labels on toolbarbuttons.  For any other element try to be
       // smarter, guessing a title from the uri.
       elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
-    }
-    else {
+    } else {
       elt.setAttribute("label", aNewTitle);
     }
   },
 
   nodeRemoved:
   function PVB_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
     let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
 
     // Here we need the <menu>.
     if (elt.localName == "menupopup")
       elt = elt.parentNode;
 
     if (parentElt._built) {
-      elt.remove();
+      parentElt.removeChild(elt);
 
       // Figure out if we need to show the "<Empty>" menu-item.
       // TODO Bug 517701: This doesn't seem to handle the case of an empty
       // root.
-      if (!parentElt.hasChildNodes() ||
-          (parentElt.childNodes.length == 1 &&
-          parentElt.firstChild == parentElt._emptyMenuItem))
-        this._showEmptyMenuItem(parentElt);
-
-      if (parentElt._endMarker != -1)
-        parentElt._endMarker--;
+      if (parentElt._startMarker.nextSibling == parentElt._endMarker)
+        this._setEmptyPopupStatus(parentElt, true);
     }
   },
 
   nodeHistoryDetailsChanged:
   function PVB_nodeHistoryDetailsChanged(aPlacesNode, aTime, aCount) {
     if (aPlacesNode.parent &&
         this.controller.hasCachedLivemarkInfo(aPlacesNode.parent)) {
       // Find the node in the parent.
       let popup = this._getDOMNodeForPlacesNode(aPlacesNode.parent);
-      for (let i = popup._startMarker; i < popup.childNodes.length; i++) {
-        let child = popup.childNodes[i];
+      for (let child = popup._startMarker.nextSibling;
+           child != popup._endMarker;
+           child = child.nextSibling) {
         if (child._placesNode && child._placesNode.uri == aPlacesNode.uri) {
           if (aPlacesNode.accessCount)
             child.setAttribute("visited", "true");
           else
             child.removeAttribute("visited");
           break;
         }
       }
     }
   },
 
-  nodeTagsChanged: function() { },
-  nodeDateAddedChanged: function() { },
-  nodeLastModifiedChanged: function() { },
-  nodeKeywordChanged: function() { },
-  sortingChanged: function() { },
-  batching: function() { },
+  nodeTagsChanged() { },
+  nodeDateAddedChanged() { },
+  nodeLastModifiedChanged() { },
+  nodeKeywordChanged() { },
+  sortingChanged() { },
+  batching() { },
 
   nodeInserted:
   function PVB_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
     let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
     if (!parentElt._built)
       return;
 
-    let index = parentElt._startMarker + 1 + aIndex;
+    let index = Array.prototype.indexOf.call(parentElt.childNodes, parentElt._startMarker) +
+                aIndex + 1;
     this._insertNewItemToPopup(aPlacesNode, parentElt,
                                parentElt.childNodes[index]);
-    if (parentElt._emptyMenuItem)
-      parentElt._emptyMenuItem.hidden = true;
+    this._setEmptyPopupStatus(parentElt, false);
   },
 
   nodeMoved:
   function PBV_nodeMoved(aPlacesNode,
                          aOldParentPlacesNode, aOldIndex,
                          aNewParentPlacesNode, aNewIndex) {
     // Note: the current implementation of moveItem does not actually
     // use this notification when the item in question is moved from one
@@ -630,268 +653,389 @@ PlacesViewBase.prototype = {
     // If our root node is a folder, it might be moved. There's nothing
     // we need to do in that case.
     if (elt == this._rootElt)
       return;
 
     let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
     if (parentElt._built) {
       // Move the node.
-      elt.remove();
-      let index = parentElt._startMarker + 1 + aNewIndex;
+      parentElt.removeChild(elt);
+      let index = Array.prototype.indexOf.call(parentElt.childNodes, parentElt._startMarker) +
+                  aNewIndex + 1;
       parentElt.insertBefore(elt, parentElt.childNodes[index]);
     }
   },
 
   containerStateChanged:
   function PVB_containerStateChanged(aPlacesNode, aOldState, aNewState) {
     if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED ||
         aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) {
       this.invalidateContainer(aPlacesNode);
 
-      if (PlacesUtils.nodeIsFolder(aPlacesNode) &&
-          !PlacesUtils.asQuery(this._result.root).queryOptions.excludeItems) {
+      if (PlacesUtils.nodeIsFolder(aPlacesNode)) {
+        let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+        if (queryOptions.excludeItems) {
+          return;
+        }
+
         PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
-                             .then(aLivemark => {
-          let shouldInvalidate =
-            !this.controller.hasCachedLivemarkInfo(aPlacesNode);
-          this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
-          if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
-            aLivemark.registerForUpdates(aPlacesNode, this);
-            aLivemark.reload();
-            if (shouldInvalidate)
-              this.invalidateContainer(aPlacesNode);
-          }
-          else {
-            aLivemark.unregisterForUpdates(aPlacesNode);
-          }
-        }, () => undefined);
+          .then(aLivemark => {
+            let shouldInvalidate =
+              !this.controller.hasCachedLivemarkInfo(aPlacesNode);
+            this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+            if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
+              aLivemark.registerForUpdates(aPlacesNode, this);
+              // Prioritize the current livemark.
+              aLivemark.reload();
+              PlacesUtils.livemarks.reloadLivemarks();
+              if (shouldInvalidate)
+                this.invalidateContainer(aPlacesNode);
+            } else {
+              aLivemark.unregisterForUpdates(aPlacesNode);
+            }
+          }, () => undefined);
       }
     }
   },
 
-  _populateLivemarkPopup: function PVB__populateLivemarkPopup(aPopup)
-  {
+  _populateLivemarkPopup: function PVB__populateLivemarkPopup(aPopup) {
     this._setLivemarkSiteURIMenuItem(aPopup);
-    this._setLivemarkStatusMenuItem(aPopup, Ci.mozILivemark.STATUS_LOADING);
+    // Show the loading status only if there are no entries yet.
+    if (aPopup._startMarker.nextSibling == aPopup._endMarker)
+      this._setLivemarkStatusMenuItem(aPopup, Ci.mozILivemark.STATUS_LOADING);
 
     PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId })
-                         .then(aLivemark => {
-      let placesNode = aPopup._placesNode;
-      if (!placesNode.containerOpen)
-        return;
+      .then(aLivemark => {
+        let placesNode = aPopup._placesNode;
+        if (!placesNode.containerOpen)
+          return;
 
-      this._setLivemarkStatusMenuItem(aPopup, aLivemark.status);
-      this._cleanPopup(aPopup);
+        if (aLivemark.status != Ci.mozILivemark.STATUS_LOADING)
+          this._setLivemarkStatusMenuItem(aPopup, aLivemark.status);
+        this._cleanPopup(aPopup,
+          this._nativeView && aPopup.parentNode.hasAttribute("open"));
 
-      let children = aLivemark.getNodesForContainer(placesNode);
-      for (let i = 0; i < children.length; i++) {
-        let child = children[i];
-        this.nodeInserted(placesNode, child, i);
-        if (child.accessCount)
-          this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
-        else
-          this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
-      }
-    }, () => undefined);
+        let children = aLivemark.getNodesForContainer(placesNode);
+        for (let i = 0; i < children.length; i++) {
+          let child = children[i];
+          this.nodeInserted(placesNode, child, i);
+          if (child.accessCount)
+            this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
+          else
+            this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
+        }
+      }, Cu.reportError);
+  },
+
+  /**
+   * Checks whether the popup associated with the provided element is open.
+   * This method may be overridden by classes that extend this base class.
+   *
+   * @param  {nsIDOMElement} elt
+   * @return {Boolean}
+   */
+  _isPopupOpen(elt) {
+    return !!elt.parentNode.open;
   },
 
   invalidateContainer: function PVB_invalidateContainer(aPlacesNode) {
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
     elt._built = false;
 
     // If the menupopup is open we should live-update it.
-    if (elt.parentNode.open)
+    if (this._isPopupOpen(elt))
       this._rebuildPopup(elt);
   },
 
   uninit: function PVB_uninit() {
     if (this._result) {
       this._result.removeObserver(this);
       this._resultNode.containerOpen = false;
       this._resultNode = null;
       this._result = null;
     }
 
+    if (this._controller) {
+      this._controller.terminate();
+      // Removing the controller will fail if it is already no longer there.
+      // This can happen if the view element was removed/reinserted without
+      // our knowledge. There is no way to check for that having happened
+      // without the possibility of an exception. :-(
+      try {
+        this._viewElt.controllers.removeController(this._controller);
+      } catch (ex) {
+      } finally {
+        this._controller = null;
+      }
+    }
+
     delete this._viewElt._placesView;
   },
 
   get isRTL() {
     if ("_isRTL" in this)
       return this._isRTL;
 
-    return this._isRTL = document.defaultView.getComputedStyle(this, "")
-                                 .direction == "rtl"
+    return this._isRTL = document.defaultView
+                                 .getComputedStyle(this.viewElt)
+                                 .direction == "rtl";
+  },
+
+  get ownerWindow() {
+    return window;
   },
 
   /**
    * Adds an "Open All in Tabs" menuitem to the bottom of the popup.
    * @param aPopup
    *        a Places popup.
    */
   _mayAddCommandsItems: function PVB__mayAddCommandsItems(aPopup) {
     // The command items are never added to the root popup.
     if (aPopup == this._rootElt)
       return;
 
     let hasMultipleURIs = false;
 
     // Check if the popup contains at least 2 menuitems with places nodes.
-    // We don't currently support opening multiple uri nodes when
-    // they are not populated by the result.
+    // We don't currently support opening multiple uri nodes when they are not
+    // populated by the result.
     if (aPopup._placesNode.childCount > 0) {
       let currentChild = aPopup.firstChild;
       let numURINodes = 0;
       while (currentChild) {
         if (currentChild.localName == "menuitem" && currentChild._placesNode) {
           if (++numURINodes == 2)
             break;
         }
         currentChild = currentChild.nextSibling;
       }
       hasMultipleURIs = numURINodes > 1;
     }
 
+    let isLiveMark = false;
+    if (this.controller.hasCachedLivemarkInfo(aPopup._placesNode)) {
+      hasMultipleURIs = true;
+      isLiveMark = true;
+    }
+
+    if (!hasMultipleURIs) {
+      aPopup.setAttribute("singleitempopup", "true");
+    } else {
+      aPopup.removeAttribute("singleitempopup");
+    }
+
     if (!hasMultipleURIs) {
       // We don't have to show any option.
       if (aPopup._endOptOpenAllInTabs) {
-        aPopup._endOptOpenAllInTabs.remove();
+        aPopup.removeChild(aPopup._endOptOpenAllInTabs);
         aPopup._endOptOpenAllInTabs = null;
-        aPopup._endMarker--;
 
-        aPopup._endOptSeparator.remove();
+        aPopup.removeChild(aPopup._endOptSeparator);
         aPopup._endOptSeparator = null;
-        aPopup._endMarker--;
-        aPopup._endMarker -= aPopup._placesNode.childCount;
-        if (aPopup._endMarker < -1)
-          aPopup._endMarker = -1;
       }
-    }
-    else if (!aPopup._endOptOpenAllInTabs) {
+    } else if (!aPopup._endOptOpenAllInTabs) {
       // Create a separator before options.
       aPopup._endOptSeparator = document.createElement("menuseparator");
       aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator";
       aPopup.appendChild(aPopup._endOptSeparator);
-      aPopup._endMarker++;
 
       // Add the "Open All in Tabs" menuitem.
       aPopup._endOptOpenAllInTabs = document.createElement("menuitem");
       aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem";
-      aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
-        "PlacesUIUtils.openContainerNodeInTabs(this.parentNode._placesNode, event);");
+
+      if (typeof this.options.extraClasses.entry == "string")
+        aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.entry);
+      if (typeof this.options.extraClasses.footer == "string")
+        aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.footer);
+
+      if (isLiveMark) {
+        aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
+          "PlacesUIUtils.openLiveMarkNodesInTabs(this.parentNode._placesNode, event, " +
+                                                 "PlacesUIUtils.getViewForNode(this));");
+      } else {
+        aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
+          "PlacesUIUtils.openContainerNodeInTabs(this.parentNode._placesNode, event, " +
+                                                 "PlacesUIUtils.getViewForNode(this));");
+      }
       aPopup._endOptOpenAllInTabs.setAttribute("onclick",
         "checkForMiddleClick(this, event); event.stopPropagation();");
       aPopup._endOptOpenAllInTabs.setAttribute("label",
         gNavigatorBundle.getString("menuOpenAllInTabs.label"));
       aPopup.appendChild(aPopup._endOptOpenAllInTabs);
-      aPopup._endMarker++;
-      aPopup._endMarker += aPopup._placesNode.childCount;
+    }
+  },
+
+  _ensureMarkers: function PVB__ensureMarkers(aPopup) {
+    if (aPopup._startMarker)
+      return;
+
+    // _startMarker is an hidden menuseparator that lives before places nodes.
+    aPopup._startMarker = document.createElement("menuseparator");
+    aPopup._startMarker.hidden = true;
+    aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild);
+
+    // _endMarker is a DOM node that lives after places nodes, specified with
+    // the 'insertionPoint' option or will be a hidden menuseparator.
+    let node = this.options.insertionPoint ?
+               aPopup.querySelector(this.options.insertionPoint) : null;
+    if (node) {
+      aPopup._endMarker = node;
+    } else {
+      aPopup._endMarker = document.createElement("menuseparator");
+      aPopup._endMarker.hidden = true;
+    }
+    aPopup.appendChild(aPopup._endMarker);
+
+    // Move the markers to the right position.
+    let firstNonStaticNodeFound = false;
+    for (let i = 0; i < aPopup.childNodes.length; i++) {
+      let child = aPopup.childNodes[i];
+      // Menus that have static content at the end, but are initially empty,
+      // use a special "builder" attribute to figure out where to start
+      // inserting places nodes.
+      if (child.getAttribute("builder") == "end") {
+        aPopup.insertBefore(aPopup._endMarker, child);
+        break;
+      }
+
+      if (child._placesNode && !child.hasAttribute("simulated-places-node") &&
+          !firstNonStaticNodeFound) {
+        firstNonStaticNodeFound = true;
+        aPopup.insertBefore(aPopup._startMarker, child);
+      }
+    }
+    if (!firstNonStaticNodeFound) {
+      aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker);
     }
   },
 
   _onPopupShowing: function PVB__onPopupShowing(aEvent) {
     // Avoid handling popupshowing of inner views.
     let popup = aEvent.originalTarget;
+
+    this._ensureMarkers(popup);
+
+    // Remove any delayed element, see _cleanPopup for details.
+    if ("_delayedRemovals" in popup) {
+      while (popup._delayedRemovals.length > 0) {
+        popup.removeChild(popup._delayedRemovals.shift());
+      }
+    }
+
     if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
       if (!popup._placesNode.containerOpen)
         popup._placesNode.containerOpen = true;
       if (!popup._built)
         this._rebuildPopup(popup);
 
       this._mayAddCommandsItems(popup);
     }
   },
 
   _addEventListeners:
-  function PVB__addEventListeners(aObject, aEventNames, aCapturing) {
+  function PVB__addEventListeners(aObject, aEventNames, aCapturing = false) {
     for (let i = 0; i < aEventNames.length; i++) {
       aObject.addEventListener(aEventNames[i], this, aCapturing);
     }
   },
 
   _removeEventListeners:
-  function PVB__removeEventListeners(aObject, aEventNames, aCapturing) {
+  function PVB__removeEventListeners(aObject, aEventNames, aCapturing = false) {
     for (let i = 0; i < aEventNames.length; i++) {
       aObject.removeEventListener(aEventNames[i], this, aCapturing);
     }
   },
 };
 
 function PlacesToolbar(aPlace) {
+  let startTime = Date.now();
   // Add some smart getters for our elements.
   let thisView = this;
   [
     ["_viewElt",              "PlacesToolbar"],
     ["_rootElt",              "PlacesToolbarItems"],
     ["_dropIndicator",        "PlacesToolbarDropIndicator"],
     ["_chevron",              "PlacesChevron"],
     ["_chevronPopup",         "PlacesChevronPopup"]
-  ].forEach(function (elementGlobal) {
+  ].forEach(function(elementGlobal) {
     let [name, id] = elementGlobal;
-    thisView.__defineGetter__(name, function () {
+    thisView.__defineGetter__(name, function() {
       let element = document.getElementById(id);
       if (!element)
         return null;
 
       delete thisView[name];
       return thisView[name] = element;
     });
   });
 
   this._viewElt._placesView = this;
 
   this._addEventListeners(this._viewElt, this._cbEvents, false);
   this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
   this._addEventListeners(this._rootElt, ["overflow", "underflow"], true);
   this._addEventListeners(window, ["resize", "unload"], false);
 
+  // If personal-bookmarks has been dragged to the tabs toolbar,
+  // we have to track addition and removals of tabs, to properly
+  // recalculate the available space for bookmarks.
+  // TODO (bug 734730): Use a performant mutation listener when available.
+  if (this._viewElt.parentNode.parentNode == document.getElementById("TabsToolbar")) {
+    this._addEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
+  }
+
   PlacesViewBase.call(this, aPlace);
+
+  Services.telemetry.getHistogramById("FX_BOOKMARKS_TOOLBAR_INIT_MS")
+                    .add(Date.now() - startTime);
 }
 
 PlacesToolbar.prototype = {
   __proto__: PlacesViewBase.prototype,
 
   _cbEvents: ["dragstart", "dragover", "dragexit", "dragend", "drop",
-#ifdef XP_UNIX
-#ifndef XP_MACOSX
-              "mousedown", "mouseup",
-#endif
-#endif
               "mousemove", "mouseover", "mouseout"],
 
   QueryInterface: function PT_QueryInterface(aIID) {
     if (aIID.equals(Ci.nsIDOMEventListener) ||
         aIID.equals(Ci.nsITimerCallback))
       return this;
 
     return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
   },
 
   uninit: function PT_uninit() {
     this._removeEventListeners(this._viewElt, this._cbEvents, false);
     this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
                                true);
     this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
     this._removeEventListeners(window, ["resize", "unload"], false);
+    this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
+
+    if (this._chevron._placesView) {
+      this._chevron._placesView.uninit();
+    }
 
     PlacesViewBase.prototype.uninit.apply(this, arguments);
   },
 
   _openedMenuButton: null,
   _allowPopupShowing: true,
 
   _rebuild: function PT__rebuild() {
     // Clear out references to existing nodes, since they will be removed
     // and re-added.
     if (this._overFolder.elt)
       this._clearOverFolder();
 
     this._openedMenuButton = null;
     while (this._rootElt.hasChildNodes()) {
-      this._rootElt.lastChild.remove();
+      this._rootElt.firstChild.remove();
     }
 
     let cc = this._resultNode.childCount;
     for (let i = 0; i < cc; ++i) {
       this._insertNewItem(this._resultNode.getChild(i), null);
     }
 
     if (this._chevronPopup.hasAttribute("type")) {
@@ -905,53 +1049,48 @@ PlacesToolbar.prototype = {
   _insertNewItem:
   function PT__insertNewItem(aChild, aBefore) {
     this._domNodes.delete(aChild);
 
     let type = aChild.type;
     let button;
     if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
       button = document.createElement("toolbarseparator");
-    }
-    else {
+    } else {
       button = document.createElement("toolbarbutton");
       button.className = "bookmark-item";
-      button.setAttribute("label", aChild.title);
+      button.setAttribute("label", aChild.title || "");
       let icon = aChild.icon;
       if (icon)
         button.setAttribute("image", icon);
 
-      if (PlacesUtils.containerTypes.indexOf(type) != -1) {
+      if (PlacesUtils.containerTypes.includes(type)) {
         button.setAttribute("type", "menu");
         button.setAttribute("container", "true");
 
         if (PlacesUtils.nodeIsQuery(aChild)) {
           button.setAttribute("query", "true");
           if (PlacesUtils.nodeIsTagQuery(aChild))
             button.setAttribute("tagContainer", "true");
-        }
-        else if (PlacesUtils.nodeIsFolder(aChild)) {
+        } else if (PlacesUtils.nodeIsFolder(aChild)) {
           PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
-                               .then(aLivemark => {
-            button.setAttribute("livemark", "true");
-            this.controller.cacheLivemarkInfo(aChild, aLivemark);
-          }, () => undefined);
+            .then(aLivemark => {
+              button.setAttribute("livemark", "true");
+              this.controller.cacheLivemarkInfo(aChild, aLivemark);
+            }, () => undefined);
         }
 
         let popup = document.createElement("menupopup");
         popup.setAttribute("placespopup", "true");
         button.appendChild(popup);
         popup._placesNode = PlacesUtils.asContainer(aChild);
-#ifndef XP_MACOSX
         popup.setAttribute("context", "placesContext");
-#endif
 
         this._domNodes.set(aChild, popup);
-      }
-      else if (PlacesUtils.nodeIsURI(aChild)) {
+      } else if (PlacesUtils.nodeIsURI(aChild)) {
         button.setAttribute("scheme",
                             PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
       }
     }
 
     button._placesNode = aChild;
     if (!this._domNodes.has(aChild))
       this._domNodes.set(aChild, button);
@@ -959,19 +1098,20 @@ PlacesToolbar.prototype = {
     if (aBefore)
       this._rootElt.insertBefore(button, aBefore);
     else
       this._rootElt.appendChild(button);
   },
 
   _updateChevronPopupNodesVisibility:
   function PT__updateChevronPopupNodesVisibility() {
-    for (let i = 0; i < this._chevronPopup.childNodes.length; i++) {
-      this._chevronPopup.childNodes[i].hidden =
-        this._rootElt.childNodes[i].style.visibility != "hidden";
+    for (let i = 0, node = this._chevronPopup._startMarker.nextSibling;
+         node != this._chevronPopup._endMarker;
+         i++, node = node.nextSibling) {
+      node.hidden = this._rootElt.childNodes[i].style.visibility != "hidden";
     }
   },
 
   _onChevronPopupShowing:
   function PT__onChevronPopupShowing(aEvent) {
     // Handle popupshowing only for the chevron popup, not for nested ones.
     if (aEvent.target != this._chevronPopup)
       return;
@@ -989,41 +1129,27 @@ PlacesToolbar.prototype = {
         break;
       case "resize":
         // This handler updates nodes visibility in both the toolbar
         // and the chevron popup when a window resize does not change
         // the overflow status of the toolbar.
         this.updateChevron();
         break;
       case "overflow":
-        if (aEvent.target != aEvent.currentTarget)
-          return;
-
-        // Ignore purely vertical overflows.
-        if (aEvent.detail == 0)
+        if (!this._isOverflowStateEventRelevant(aEvent))
           return;
-
-        // Attach the popup binding to the chevron popup if it has not yet
-        // been initialized.
-        if (!this._chevronPopup.hasAttribute("type")) {
-          this._chevronPopup.setAttribute("place", this.place);
-          this._chevronPopup.setAttribute("type", "places");
-        }
-        this._chevron.collapsed = false;
-        this.updateChevron();
+        this._onOverflow();
         break;
       case "underflow":
-        if (aEvent.target != aEvent.currentTarget)
+        if (!this._isOverflowStateEventRelevant(aEvent))
           return;
-
-        // Ignore purely vertical underflows.
-        if (aEvent.detail == 0)
-          return;
-
-        this._chevron.collapsed = true;
+        this._onUnderflow();
+        break;
+      case "TabOpen":
+      case "TabClose":
         this.updateChevron();
         break;
       case "dragstart":
         this._onDragStart(aEvent);
         break;
       case "dragover":
         this._onDragOver(aEvent);
         break;
@@ -1040,37 +1166,56 @@ PlacesToolbar.prototype = {
         this._onMouseOver(aEvent);
         break;
       case "mousemove":
         this._onMouseMove(aEvent);
         break;
       case "mouseout":
         this._onMouseOut(aEvent);
         break;
-#ifdef XP_UNIX
-#ifndef XP_MACOSX
-      case "mouseup":
-        this._onMouseUp(aEvent);
-        break;
-      case "mousedown":
-        this._onMouseDown(aEvent);
-        break;
-#endif
-#endif
       case "popupshowing":
         this._onPopupShowing(aEvent);
         break;
       case "popuphidden":
         this._onPopupHidden(aEvent);
         break;
       default:
         throw "Trying to handle unexpected event.";
     }
   },
 
+  updateOverflowStatus() {
+    if (this._rootElt.scrollLeftMin != this._rootElt.scrollLeftMax) {
+      this._onOverflow();
+    } else {
+      this._onUnderflow();
+    }
+  },
+
+  _isOverflowStateEventRelevant: function PT_isOverflowStateEventRelevant(aEvent) {
+    // Ignore events not aimed at ourselves, as well as purely vertical ones:
+    return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
+  },
+
+  _onOverflow: function PT_onOverflow() {
+    // Attach the popup binding to the chevron popup if it has not yet
+    // been initialized.
+    if (!this._chevronPopup.hasAttribute("type")) {
+      this._chevronPopup.setAttribute("place", this.place);
+      this._chevronPopup.setAttribute("type", "places");
+    }
+    this._chevron.collapsed = false;
+    this.updateChevron();
+  },
+
+  _onUnderflow: function PT_onUnderflow() {
+    this.updateChevron();
+    this._chevron.collapsed = true;
+  },
+
   updateChevron: function PT_updateChevron() {
     // If the chevron is collapsed there's nothing to update.
     if (this._chevron.collapsed)
       return;
 
     // Update the chevron on a timer.  This will avoid repeated work when
     // lot of changes happen in a small timeframe.
     if (this._updateChevronTimer)
@@ -1082,18 +1227,19 @@ PlacesToolbar.prototype = {
   _updateChevronTimerCallback: function PT__updateChevronTimerCallback() {
     let scrollRect = this._rootElt.getBoundingClientRect();
     let childOverflowed = false;
     for (let i = 0; i < this._rootElt.childNodes.length; i++) {
       let child = this._rootElt.childNodes[i];
       // Once a child overflows, all the next ones will.
       if (!childOverflowed) {
         let childRect = child.getBoundingClientRect();
-        childOverflowed = this._isRTL ? (childRect.left < scrollRect.left)
-                                      : (childRect.right > scrollRect.right);
+        childOverflowed = this.isRTL ? (childRect.left < scrollRect.left)
+                                     : (childRect.right > scrollRect.right);
+
       }
       child.style.visibility = childOverflowed ? "hidden" : "visible";
     }
 
     // We rebuild the chevron on popupShowing, so if it is open
     // we must update it.
     if (this._chevron.open)
       this._updateChevronPopupNodesVisibility();
@@ -1144,24 +1290,23 @@ PlacesToolbar.prototype = {
 
       // Here we need the <menu>.
       if (elt.localName == "menupopup")
         elt = elt.parentNode;
 
       this._removeChild(elt);
       this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
 
-      // If the chevron popup is open, keep it in sync.
-      if (this._chevron.open) {
-        let chevronPopup = this._chevronPopup;
-        let menuitem = chevronPopup.childNodes[aOldIndex];
-        menuitem.remove();
-        chevronPopup.insertBefore(menuitem,
-                                  chevronPopup.childNodes[aNewIndex]);
-      }
+      // The chevron view may get nodeMoved after the toolbar.  In such a case,
+      // we should ensure (by manually swapping menuitems) that the actual nodes
+      // are in the final position before updateChevron tries to updates their
+      // visibility, or the chevron may go out of sync.
+      // Luckily updateChevron runs on a timer, so, by the time it updates
+      // nodes, the menu has already handled the notification.
+
       this.updateChevron();
       return;
     }
 
     PlacesViewBase.prototype.nodeMoved.apply(this, arguments);
   },
 
   nodeAnnotationChanged:
@@ -1171,29 +1316,31 @@ PlacesToolbar.prototype = {
       return;
 
     // We're notified for the menupopup, not the containing toolbarbutton.
     if (elt.localName == "menupopup")
       elt = elt.parentNode;
 
     if (elt.parentNode == this._rootElt) {
       // Node is on the toolbar.
+
       // All livemarks have a feedURI, so use it as our indicator.
       if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
         elt.setAttribute("livemark", true);
 
         PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
-                             .then(aLivemark => {
-          this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
-          this.invalidateContainer(aPlacesNode);
-        }, () => undefined);
+          .then(aLivemark => {
+            this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+            this.invalidateContainer(aPlacesNode);
+          }, Cu.reportError);
       }
+    } else {
+      // Node is in a submenu.
+      PlacesViewBase.prototype.nodeAnnotationChanged.apply(this, arguments);
     }
-
-    PlacesViewBase.prototype.nodeAnnotationChanged.apply(this, arguments);
   },
 
   nodeTitleChanged: function PT_nodeTitleChanged(aPlacesNode, aNewTitle) {
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
 
     // There's no UI representation for the root node, thus there's
     // nothing to be done when the title changes.
     if (elt == this._rootElt)
@@ -1251,132 +1398,117 @@ PlacesToolbar.prototype = {
   /**
    * This function returns information about where to drop when dragging over
    * the toolbar.  The returned object has the following properties:
    * - ip: the insertion point for the bookmarks service.
    * - beforeIndex: child index to drop before, for the drop indicator.
    * - folderElt: the folder to drop into, if applicable.
    */
   _getDropPoint: function PT__getDropPoint(aEvent) {
-    let result = this.result;
     if (!PlacesUtils.nodeIsFolder(this._resultNode))
       return null;
 
     let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
     let elt = aEvent.target;
     if (elt._placesNode && elt != this._rootElt &&
         elt.localName != "menupopup") {
       let eltRect = elt.getBoundingClientRect();
-      let eltIndex = Array.from(this._rootElt.childNodes).indexOf(elt);
+      let eltIndex = Array.prototype.indexOf.call(this._rootElt.childNodes, elt);
       if (PlacesUtils.nodeIsFolder(elt._placesNode) &&
           !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) {
         // This is a folder.
         // If we are in the middle of it, drop inside it.
         // Otherwise, drop before it, with regards to RTL mode.
         let threshold = eltRect.width * 0.25;
-        if (this._isRTL ? (aEvent.clientX > eltRect.right - threshold)
-                        : (aEvent.clientX < eltRect.left + threshold)) {
+        if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold)
+                       : (aEvent.clientX < eltRect.left + threshold)) {
           // Drop before this folder.
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-                               eltIndex,
-                               Ci.nsITreeView.DROP_BEFORE);
+                               eltIndex, Ci.nsITreeView.DROP_BEFORE);
           dropPoint.beforeIndex = eltIndex;
-        }
-        else if (this._isRTL ? (aEvent.clientX > eltRect.left + threshold)
-                             : (aEvent.clientX < eltRect.right - threshold)) {
+        } else if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
+                            : (aEvent.clientX < eltRect.right - threshold)) {
           // Drop inside this folder.
+          let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
+                        elt._placesNode.title : null;
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(elt._placesNode),
                                -1, Ci.nsITreeView.DROP_ON,
-                               PlacesUtils.nodeIsTagQuery(elt._placesNode));
+                               tagName);
           dropPoint.beforeIndex = eltIndex;
           dropPoint.folderElt = elt;
-        }
-        else {
+        } else {
           // Drop after this folder.
           let beforeIndex =
             (eltIndex == this._rootElt.childNodes.length - 1) ?
             -1 : eltIndex + 1;
 
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-                               beforeIndex,
-                               Ci.nsITreeView.DROP_BEFORE);
+                               beforeIndex, Ci.nsITreeView.DROP_BEFORE);
           dropPoint.beforeIndex = beforeIndex;
         }
-      }
-      else {
+      } else {
         // This is a non-folder node or a read-only folder.
         // Drop before it with regards to RTL mode.
         let threshold = eltRect.width * 0.5;
-        if (this._isRTL ? (aEvent.clientX > eltRect.left + threshold)
-                        : (aEvent.clientX < eltRect.left + threshold)) {
+        if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
+                       : (aEvent.clientX < eltRect.left + threshold)) {
           // Drop before this bookmark.
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-                               eltIndex,
-                               Ci.nsITreeView.DROP_BEFORE);
+                               eltIndex, Ci.nsITreeView.DROP_BEFORE);
           dropPoint.beforeIndex = eltIndex;
-        }
-        else {
+        } else {
           // Drop after this bookmark.
           let beforeIndex =
             eltIndex == this._rootElt.childNodes.length - 1 ?
             -1 : eltIndex + 1;
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-                               beforeIndex,
-                               Ci.nsITreeView.DROP_BEFORE);
+                               beforeIndex, Ci.nsITreeView.DROP_BEFORE);
           dropPoint.beforeIndex = beforeIndex;
         }
       }
-    }
-    else {
+    } else {
       // We are most likely dragging on the empty area of the
       // toolbar, we should drop after the last node.
       dropPoint.ip =
         new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
                            -1, Ci.nsITreeView.DROP_BEFORE);
       dropPoint.beforeIndex = -1;
     }
 
     return dropPoint;
   },
 
   _setTimer: function PT_setTimer(aTime) {
-    let timer = Cc["@mozilla.org/timer;1"]
-                  .createInstance(Ci.nsITimer);
+    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
     return timer;
   },
 
   notify: function PT_notify(aTimer) {
     if (aTimer == this._updateChevronTimer) {
       this._updateChevronTimer = null;
       this._updateChevronTimerCallback();
-    }
-
-    // * Timer to turn off indicator bar.
-    else if (aTimer == this._ibTimer) {
+    } else if (aTimer == this._ibTimer) {
+      // * Timer to turn off indicator bar.
       this._dropIndicator.collapsed = true;
       this._ibTimer = null;
-    }
-
-    // * Timer to open a menubutton that's being dragged over.
-    else if (aTimer == this._overFolder.openTimer) {
+    } else if (aTimer == this._overFolder.openTimer) {
+      // * Timer to open a menubutton that's being dragged over.
       // Set the autoopen attribute on the folder's menupopup so that
       // the menu will automatically close when the mouse drags off of it.
       this._overFolder.elt.lastChild.setAttribute("autoopened", "true");
       this._overFolder.elt.open = true;
       this._overFolder.openTimer = null;
-    }
-
-    // * Timer to close a menubutton that's been dragged off of.
-    else if (aTimer == this._overFolder.closeTimer) {
+    } else if (aTimer == this._overFolder.closeTimer) {
+      // * Timer to close a menubutton that's been dragged off of.
       // Close the menubutton if we are not dragging over it or one of
       // its children.  The autoopened attribute will let the menu know to
       // close later if the menu is still being dragged over.
       let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget;
       let inHierarchy = false;
       while (currentPlacesNode) {
         if (currentPlacesNode == this._rootElt) {
           inHierarchy = true;
@@ -1420,27 +1552,19 @@ PlacesToolbar.prototype = {
     let draggedElt = aEvent.target;
     if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
       return;
 
     if (draggedElt.localName == "toolbarbutton" &&
         draggedElt.getAttribute("type") == "menu") {
       // If the drag gesture on a container is toward down we open instead
       // of dragging.
-#ifdef XP_UNIX
-#ifndef XP_MACOSX
-      if (this._mouseDownTimer) {
-        this._mouseDownTimer.cancel();
-        this._mouseDownTimer = null;
-      }
-#endif
-#endif
       let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY;
       let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX;
-      if ((translateY) >= Math.abs(translateX/2)) {
+      if ((translateY) >= Math.abs(translateX / 2)) {
         // Don't start the drag.
         aEvent.preventDefault();
         // Open the menu.
         draggedElt.open = true;
         return;
       }
 
       // If the menu is open, close it.
@@ -1484,67 +1608,67 @@ PlacesToolbar.prototype = {
         this._clearOverFolder();
         this._overFolder.elt = overElt;
         this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime);
       }
       if (!this._overFolder.elt.hasAttribute("dragover"))
         this._overFolder.elt.setAttribute("dragover", "true");
 
       this._dropIndicator.collapsed = true;
-    }
-    else {
+    } else {
       // Dragging over a normal toolbarbutton,
       // show indicator bar and move it to the appropriate drop point.
       let ind = this._dropIndicator;
+      ind.parentNode.collapsed = false;
       let halfInd = ind.clientWidth / 2;
       let translateX;
-      if (this._isRTL) {
+      if (this.isRTL) {
         halfInd = Math.ceil(halfInd);
         translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd;
         if (this._rootElt.firstChild) {
           if (dropPoint.beforeIndex == -1)
             translateX += this._rootElt.lastChild.getBoundingClientRect().left;
           else {
             translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
                               .getBoundingClientRect().right;
           }
         }
-      }
-      else {
+      } else {
         halfInd = Math.floor(halfInd);
         translateX = 0 - this._rootElt.getBoundingClientRect().left +
                      halfInd;
         if (this._rootElt.firstChild) {
           if (dropPoint.beforeIndex == -1)
             translateX += this._rootElt.lastChild.getBoundingClientRect().right;
           else {
             translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
                               .getBoundingClientRect().left;
           }
         }
       }
 
       ind.style.transform = "translate(" + Math.round(translateX) + "px)";
-      ind.style.MozMarginStart = (-ind.clientWidth) + "px";
+      ind.style.marginInlineStart = (-ind.clientWidth) + "px";
       ind.collapsed = false;
 
       // Clear out old folder information.
       this._clearOverFolder();
     }
 
     aEvent.preventDefault();
     aEvent.stopPropagation();
   },
 
   _onDrop: function PT__onDrop(aEvent) {
     PlacesControllerDragHelper.currentDropTarget = aEvent.target;
 
     let dropPoint = this._getDropPoint(aEvent);
     if (dropPoint && dropPoint.ip) {
       PlacesControllerDragHelper.onDrop(dropPoint.ip, aEvent.dataTransfer)
+                                .catch(Cu.reportError);
       aEvent.preventDefault();
     }
 
     this._cleanupDragDetails();
     aEvent.stopPropagation();
   },
 
   _onDragExit: function PT__onDragExit(aEvent) {
@@ -1582,75 +1706,35 @@ PlacesToolbar.prototype = {
 
   _onPopupHidden: function PT__onPopupHidden(aEvent) {
     let popup = aEvent.target;
     let placesNode = popup._placesNode;
     // Avoid handling popuphidden of inner views
     if (placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
       // UI performance: folder queries are cheap, keep the resultnode open
       // so we don't rebuild its contents whenever the popup is reopened.
-      // Though, we want to always close feed containers so their
-      // expiration status will be checked at next opening.
+      // Though, we want to always close feed containers so their expiration
+      // status will be checked at next opening.
       if (!PlacesUtils.nodeIsFolder(placesNode) ||
-          this.controller.hasCachedLivemarkInfo(placesNode))
+          this.controller.hasCachedLivemarkInfo(placesNode)) {
         placesNode.containerOpen = false;
+      }
     }
 
     let parent = popup.parentNode;
     if (parent.localName == "toolbarbutton") {
       this._openedMenuButton = null;
       // Clear the dragover attribute if present, if we are dragging into a
       // folder in the hierachy of current opened popup we don't clear
       // this attribute on clearOverFolder.  See Notify for closeTimer.
       if (parent.hasAttribute("dragover"))
         parent.removeAttribute("dragover");
     }
   },
 
-#ifdef XP_UNIX
-#ifndef XP_MACOSX
-  _onMouseDown: function PT__onMouseDown(aEvent) {
-    let target = aEvent.target;
-    if (aEvent.button == 0 &&
-        target.localName == "toolbarbutton" &&
-        target.getAttribute("type") == "menu") {
-      this._allowPopupShowing = false;
-      // On Linux we can open the popup only after a delay.
-      // Indeed as soon as the menupopup opens we are unable to start a
-      // drag aEvent.  See bug 500081 for details.
-      this._mouseDownTimer = Cc["@mozilla.org/timer;1"].
-                             createInstance(Ci.nsITimer);
-      let callback = {
-        _self: this,
-        _target: target,
-        notify: function(timer) {
-          this._target.open = true;
-          this._mouseDownTimer = null;
-        }
-      };
-
-      this._mouseDownTimer.initWithCallback(callback, 300,
-                                            Ci.nsITimer.TYPE_ONE_SHOT);
-    }
-  },
-
-  _onMouseUp: function PT__onMouseUp(aEvent) {
-    if (aEvent.button != 0)
-      return;
-
-    if (this._mouseDownTimer) {
-      // On a click (down/up), we should open the menu popup.
-      this._mouseDownTimer.cancel();
-      this._mouseDownTimer = null;
-      aEvent.target.open = true;
-    }
-  },
-#endif
-#endif
-
   _onMouseMove: function PT__onMouseMove(aEvent) {
     // Used in dragStart to prevent dragging folders when dragging down.
     this._cachedMouseMoveEvent = aEvent;
 
     if (this._openedMenuButton == null ||
         PlacesControllerDragHelper.getSession())
       return;
 
@@ -1663,49 +1747,49 @@ PlacesToolbar.prototype = {
     }
   }
 };
 
 /**
  * View for Places menus.  This object should be created during the first
  * popupshowing that's dispatched on the menu.
  */
-function PlacesMenu(aPopupShowingEvent, aPlace) {
+function PlacesMenu(aPopupShowingEvent, aPlace, aOptions) {
   this._rootElt = aPopupShowingEvent.target; // <menupopup>
   this._viewElt = this._rootElt.parentNode;   // <menu>
   this._viewElt._placesView = this;
   this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
   this._addEventListeners(window, ["unload"], false);
 
-#ifdef XP_MACOSX
-  if (this._viewElt.parentNode.localName == "menubar") {
-    this._nativeView = true;
-    this._rootElt._startMarker = -1;
-    this._rootElt._endMarker = -1;
+  if (AppConstants.platform === "macosx") {
+    // Must walk up to support views in sub-menus, like Bookmarks Toolbar menu.
+    for (let elt = this._viewElt.parentNode; elt; elt = elt.parentNode) {
+      if (elt.localName == "menubar") {
+        this._nativeView = true;
+        break;
+      }
+    }
   }
-#endif
 
-  PlacesViewBase.call(this, aPlace);
+  PlacesViewBase.call(this, aPlace, aOptions);
   this._onPopupShowing(aPopupShowingEvent);
 }
 
 PlacesMenu.prototype = {
   __proto__: PlacesViewBase.prototype,
 
   QueryInterface: function PM_QueryInterface(aIID) {
     if (aIID.equals(Ci.nsIDOMEventListener))
       return this;
 
     return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
   },
 
   _removeChild: function PM_removeChild(aChild) {
     PlacesViewBase.prototype._removeChild.apply(this, arguments);
-    if (this._endMarker != -1)
-      this._endMarker--;
   },
 
   uninit: function PM_uninit() {
     this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
                                true);
     this._removeEventListeners(window, ["unload"], false);
 
     PlacesViewBase.prototype.uninit.apply(this, arguments);
@@ -1723,24 +1807,371 @@ PlacesMenu.prototype = {
         this._onPopupHidden(aEvent);
         break;
     }
   },
 
   _onPopupHidden: function PM__onPopupHidden(aEvent) {
     // Avoid handling popuphidden of inner views.
     let popup = aEvent.originalTarget;
-    if (!popup._placesNode || PlacesUIUtils.getViewForNode(popup) != this)
+    let placesNode = popup._placesNode;
+    if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this)
       return;
 
     // UI performance: folder queries are cheap, keep the resultnode open
     // so we don't rebuild its contents whenever the popup is reopened.
-    if (!PlacesUtils.nodeIsFolder(popup._placesNode))
-      popup._placesNode.containerOpen = false;
+    // Though, we want to always close feed containers so their expiration
+    // status will be checked at next opening.
+    if (!PlacesUtils.nodeIsFolder(placesNode) ||
+        this.controller.hasCachedLivemarkInfo(placesNode))
+      placesNode.containerOpen = false;
 
     // The autoopened attribute is set for folders which have been
     // automatically opened when dragged over.  Turn off this attribute
     // when the folder closes because it is no longer applicable.
     popup.removeAttribute("autoopened");
     popup.removeAttribute("dragstart");
   }
 };
 
+function PlacesPanelMenuView(aPlace, aViewId, aRootId, aOptions) {
+  this._viewElt = document.getElementById(aViewId);
+  this._rootElt = document.getElementById(aRootId);
+  this._viewElt._placesView = this;
+  this.options = aOptions;
+
+  PlacesViewBase.call(this, aPlace, aOptions);
+}
+
+PlacesPanelMenuView.prototype = {
+  __proto__: PlacesViewBase.prototype,
+
+  QueryInterface: function PAMV_QueryInterface(aIID) {
+    return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
+  },
+
+  uninit: function PAMV_uninit() {
+    PlacesViewBase.prototype.uninit.apply(this, arguments);
+  },
+
+  _insertNewItem:
+  function PAMV__insertNewItem(aChild, aBefore) {
+    this._domNodes.delete(aChild);
+
+    let type = aChild.type;
+    let button;
+    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+      button = document.createElement("toolbarseparator");
+      button.setAttribute("class", "small-separator");
+    } else {
+      button = document.createElement("toolbarbutton");
+      button.className = "bookmark-item";
+      if (typeof this.options.extraClasses.entry == "string")
+        button.classList.add(this.options.extraClasses.entry);
+      button.setAttribute("label", aChild.title || "");
+      let icon = aChild.icon;
+      if (icon)
+        button.setAttribute("image", icon);
+
+      if (PlacesUtils.containerTypes.includes(type)) {
+        button.setAttribute("container", "true");
+
+        if (PlacesUtils.nodeIsQuery(aChild)) {
+          button.setAttribute("query", "true");
+          if (PlacesUtils.nodeIsTagQuery(aChild))
+            button.setAttribute("tagContainer", "true");
+        } else if (PlacesUtils.nodeIsFolder(aChild)) {
+          PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
+            .then(aLivemark => {
+              button.setAttribute("livemark", "true");
+              this.controller.cacheLivemarkInfo(aChild, aLivemark);
+            }, () => undefined);
+        }
+      } else if (PlacesUtils.nodeIsURI(aChild)) {
+        button.setAttribute("scheme",
+                            PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
+      }
+    }
+
+    button._placesNode = aChild;
+    if (!this._domNodes.has(aChild))
+      this._domNodes.set(aChild, button);
+
+    this._rootElt.insertBefore(button, aBefore);
+  },
+
+  nodeInserted:
+  function PAMV_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
+    let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+    if (parentElt != this._rootElt)
+      return;
+
+    let children = this._rootElt.childNodes;
+    this._insertNewItem(aPlacesNode,
+      aIndex < children.length ? children[aIndex] : null);
+  },
+
+  nodeRemoved:
+  function PAMV_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
+    let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+    if (parentElt != this._rootElt)
+      return;
+
+    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+    this._removeChild(elt);
+  },
+
+  nodeMoved:
+  function PAMV_nodeMoved(aPlacesNode,
+                          aOldParentPlacesNode, aOldIndex,
+                          aNewParentPlacesNode, aNewIndex) {
+    let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+    if (parentElt != this._rootElt)
+      return;
+
+    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+    this._removeChild(elt);
+    this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
+  },
+
+  nodeAnnotationChanged:
+  function PAMV_nodeAnnotationChanged(aPlacesNode, aAnno) {
+    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+    // There's no UI representation for the root node.
+    if (elt == this._rootElt)
+      return;
+
+    if (elt.parentNode != this._rootElt)
+      return;
+
+    // All livemarks have a feedURI, so use it as our indicator.
+    if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
+      elt.setAttribute("livemark", true);
+
+      PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+        .then(aLivemark => {
+          this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+          this.invalidateContainer(aPlacesNode);
+        }, Cu.reportError);
+    }
+  },
+
+  nodeTitleChanged: function PAMV_nodeTitleChanged(aPlacesNode, aNewTitle) {
+    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+    // There's no UI representation for the root node.
+    if (elt == this._rootElt)
+      return;
+
+    PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
+  },
+
+  invalidateContainer: function PAMV_invalidateContainer(aPlacesNode) {
+    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+    if (elt != this._rootElt)
+      return;
+
+    // Container is the toolbar itself.
+    while (this._rootElt.hasChildNodes()) {
+      this._rootElt.firstChild.remove();
+    }
+
+    for (let i = 0; i < this._resultNode.childCount; ++i) {
+      this._insertNewItem(this._resultNode.getChild(i), null);
+    }
+  }
+};
+
+var PlacesPanelview = class extends PlacesViewBase {
+  constructor(container, panelview, place, options = {}) {
+    options.rootElt = container;
+    options.viewElt = panelview;
+    super(place, options);
+    this._viewElt._placesView = this;
+    // We're simulating a popup show, because a panelview may only be shown when
+    // its containing popup is already shown.
+    this._onPopupShowing({ originalTarget: this._viewElt });
+    this._addEventListeners(window, ["unload"]);
+    this._rootElt.setAttribute("context", "placesContext");
+  }
+
+  get events() {
+    if (this._events)
+      return this._events;
+    return this._events = ["command", "destructed", "dragend", "dragstart",
+      "ViewHiding", "ViewShowing", "ViewShown"];
+  }
+
+  get panel() {
+    return this.panelMultiView.parentNode;
+  }
+
+  get panelMultiView() {
+    return this._viewElt.panelMultiView;
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "command":
+        this._onCommand(event);
+        break;
+      case "destructed":
+        this._onDestructed(event);
+        break;
+      case "dragend":
+        this._onDragEnd(event);
+        break;
+      case "dragstart":
+        this._onDragStart(event);
+        break;
+      case "unload":
+        this.uninit(event);
+        break;
+      case "ViewHiding":
+        this._onPopupHidden(event);
+        break;
+      case "ViewShowing":
+        this._onPopupShowing(event);
+        break;
+      case "ViewShown":
+        this._onViewShown(event);
+        break;
+    }
+  }
+
+  _onCommand(event) {
+    let button = event.originalTarget;
+    if (!button._placesNode)
+      return;
+
+    PlacesUIUtils.openNodeWithEvent(button._placesNode, event);
+  }
+
+  _onDestructed(event) {
+    // The panelmultiview is ephemeral, so let's keep an eye out when the root
+    // element is showing again.
+    this._removeEventListeners(event.target, this.events);
+    this._addEventListeners(this._viewElt, ["ViewShowing"]);
+  }
+
+  _onDragEnd() {
+    this._draggedElt = null;
+  }
+
+  _onDragStart(event) {
+    let draggedElt = event.originalTarget;
+    if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
+      return;
+
+    // Activate the view and cache the dragged element.
+    this._draggedElt = draggedElt._placesNode;
+    this._rootElt.focus();
+
+    this._controller.setDataTransfer(event);
+    event.stopPropagation();
+  }
+
+  uninit(event) {
+    this._removeEventListeners(this.panelMultiView, this.events);
+    this._removeEventListeners(this._viewElt, ["ViewShowing"]);
+    this._removeEventListeners(window, ["unload"]);
+    super.uninit(event);
+  }
+
+  _createDOMNodeForPlacesNode(placesNode) {
+    this._domNodes.delete(placesNode);
+
+    let element;
+    let type = placesNode.type;
+    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+      element = document.createElement("toolbarseparator");
+    } else {
+      if (type != Ci.nsINavHistoryResultNode.RESULT_TYPE_URI)
+        throw "Unexpected node";
+
+      element = document.createElement("toolbarbutton");
+      element.classList.add("subviewbutton", "subviewbutton-iconic", "bookmark-item");
+      element.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(placesNode.uri));
+      element.setAttribute("label", PlacesUIUtils.getBestTitle(placesNode));
+
+      let icon = placesNode.icon;
+      if (icon)
+        element.setAttribute("image", icon);
+    }
+
+    element._placesNode = placesNode;
+    if (!this._domNodes.has(placesNode))
+      this._domNodes.set(placesNode, element);
+
+    return element;
+  }
+
+  _setEmptyPopupStatus(panelview, empty = false) {
+    if (!panelview._emptyMenuitem) {
+      let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
+      panelview._emptyMenuitem = document.createElement("toolbarbutton");
+      panelview._emptyMenuitem.setAttribute("label", label);
+      panelview._emptyMenuitem.setAttribute("disabled", true);
+      panelview._emptyMenuitem.className = "subviewbutton";
+      if (typeof this.options.extraClasses.entry == "string")
+        panelview._emptyMenuitem.classList.add(this.options.extraClasses.entry);
+    }
+
+    if (empty) {
+      panelview.setAttribute("emptyplacesresult", "true");
+      // Don't add the menuitem if there is static content.
+      // We also support external usage for custom crafted panels - which'll have
+      // no markers present.
+      if (!panelview._startMarker ||
+          (!panelview._startMarker.previousSibling && !panelview._endMarker.nextSibling)) {
+        panelview.insertBefore(panelview._emptyMenuitem, panelview._endMarker);
+      }
+    } else {
+      panelview.removeAttribute("emptyplacesresult");
+      try {
+        panelview.removeChild(panelview._emptyMenuitem);
+      } catch (ex) {}
+    }
+  }
+
+  _isPopupOpen() {
+    return this.panel.state == "open" && this.panelMultiView.current == this._viewElt;
+  }
+
+  _onPopupHidden(event) {
+    let panelview = event.originalTarget;
+    let placesNode = panelview._placesNode;
+    // Avoid handling ViewHiding of inner views
+    if (placesNode && PlacesUIUtils.getViewForNode(panelview) == this) {
+      // UI performance: folder queries are cheap, keep the resultnode open
+      // so we don't rebuild its contents whenever the popup is reopened.
+      // Though, we want to always close feed containers so their expiration
+      // status will be checked at next opening.
+      if (!PlacesUtils.nodeIsFolder(placesNode) ||
+          this.controller.hasCachedLivemarkInfo(placesNode)) {
+        placesNode.containerOpen = false;
+      }
+    }
+  }
+
+  _onPopupShowing(event) {
+    // If the event came from the root element, this is a sign that the panelmultiview
+    // was just instantiated (see `_onDestructed` above) or this is the first time
+    // we ever get here.
+    if (event.originalTarget == this._viewElt) {
+      this._removeEventListeners(this._viewElt, ["ViewShowing"]);
+      // Start listening for events from all panels inside the panelmultiview.
+      this._addEventListeners(this.panelMultiView, this.events);
+    }
+    super._onPopupShowing(event);
+  }
+
+  _onViewShown(event) {
+    if (event.originalTarget != this._viewElt)
+      return;
+
+    // Because PanelMultiView reparents the panelview internally, the controller
+    // may get lost. In that case we'll append it again, because we certainly
+    // need it later!
+    if (!this.controllers.getControllerCount() && this._controller)
+      this.controllers.appendController(this._controller);
+  }
+}
--- a/suite/common/places/content/controller.js
+++ b/suite/common/places/content/controller.js
@@ -1,106 +1,124 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* 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/. */
 
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ChromeUtils.defineModuleGetter(this, "ForgetAboutSite",
+                               "resource://gre/modules/ForgetAboutSite.jsm");
+ChromeUtils.defineModuleGetter(this, "NetUtil",
+                               "resource://gre/modules/NetUtil.jsm");
+ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
+                               "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 // XXXmano: we should move most/all of these constants to PlacesUtils
 const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
 
 // No change to the view, preserve current selection
 const RELOAD_ACTION_NOTHING = 0;
 // Inserting items new to the view, select the inserted rows
 const RELOAD_ACTION_INSERT = 1;
 // Removing items from the view, select the first item after the last selected
 const RELOAD_ACTION_REMOVE = 2;
 // Moving items within a view, don't treat the dropped items as additional
 // rows.
 const RELOAD_ACTION_MOVE = 3;
 
-// when removing a bunch of pages we split them in chunks to avoid passing
-// a too big array to RemovePages
-// 300 is the best choice with an history of about 150000 visits
-// smaller chunks could cause a Slow Script warning with a huge history
-const REMOVE_PAGES_CHUNKLEN = 300;
-// if we are removing less than this pages we will remove them one by one
-// since it will be reflected faster on the UI
-// 10 is a good compromise, since allows the user to delete a little amount of
-// urls for privacy reasons, but does not cause heavy disk access
-const REMOVE_PAGES_MAX_SINGLEREMOVES = 10;
-
-// XXX: should pull in from global definition, see bug 585500
-var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
-
 /**
  * Represents an insertion point within a container where we can insert
  * items.
  * @param   aItemId
  *          The identifier of the parent container
  * @param   aIndex
  *          The index within the container where we should insert
  * @param   aOrientation
  *          The orientation of the insertion. NOTE: the adjustments to the
  *          insertion point to accommodate the orientation should be done by
  *          the person who constructs the IP, not the user. The orientation
  *          is provided for informational purposes only!
- * @param   [optional] aIsTag
- *          Indicates if parent container is a tag
+ * @param   [optional] aTag
+ *          The tag name if this IP is set to a tag, null otherwise.
  * @param   [optional] aDropNearItemId
  *          When defined we will calculate index based on this itemId
  * @constructor
  */
-function InsertionPoint(aItemId, aIndex, aOrientation, aIsTag,
-                        aDropNearItemId) {
+function InsertionPoint(aItemId, aIndex, aOrientation, aTagName = null,
+                        aDropNearItemId = false) {
   this.itemId = aItemId;
   this._index = aIndex;
   this.orientation = aOrientation;
   this.isTag = aIsTag;
+  this.tagName = aTagName;
   this.dropNearItemId = aDropNearItemId;
 }
 
 InsertionPoint.prototype = {
   set index(val) {
     return this._index = val;
   },
 
+  promiseGuid() {
+    return PlacesUtils.promiseItemGuid(this.itemId);
+  },
+
   get index() {
     if (this.dropNearItemId > 0) {
       // If dropNearItemId is set up we must calculate the real index of
       // the item near which we will drop.
       var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId);
       return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
     }
     return this._index;
+  },
+
+  get isTag() {
+    return typeof(this.tagName) == "string";
   }
 };
 
 /**
  * Places Controller
  */
 
 function PlacesController(aView) {
   this._view = aView;
   XPCOMUtils.defineLazyServiceGetter(this, "clipboard",
                                      "@mozilla.org/widget/clipboard;1",
                                      "nsIClipboard");
+  XPCOMUtils.defineLazyGetter(this, "profileName", function() {
+    return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
+  });
+
   this._cachedLivemarkInfoObjects = new Map();
 }
 
 PlacesController.prototype = {
   /**
    * The places view.
    */
   _view: null,
 
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIClipboardOwner
+  ]),
+
+  // nsIClipboardOwner
+  LosingOwnership: function PC_LosingOwnership(aXferable) {
+    this.cutNodes = [];
+  },
+
+  terminate: function PC_terminate() {
+    this._releaseClipboardOwnership();
+  },
+
   supportsCommand: function PC_supportsCommand(aCommand) {
-    //LOG("supportsCommand: " + command);
     // Non-Places specific commands that we also support
     switch (aCommand) {
     case "cmd_undo":
     case "cmd_redo":
     case "cmd_cut":
     case "cmd_copy":
     case "cmd_paste":
     case "cmd_delete":
@@ -112,25 +130,30 @@ PlacesController.prototype = {
     // filters out other commands that we do _not_ support (see 329587).
     const CMD_PREFIX = "placesCmd_";
     return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
   },
 
   isCommandEnabled: function PC_isCommandEnabled(aCommand) {
     switch (aCommand) {
     case "cmd_undo":
-      return PlacesUtils.transactionManager.numberOfUndoItems > 0;
+      if (!PlacesUIUtils.useAsyncTransactions)
+        return PlacesUtils.transactionManager.numberOfUndoItems > 0;
+
+      return PlacesTransactions.topUndoEntry != null;
     case "cmd_redo":
-      return PlacesUtils.transactionManager.numberOfRedoItems > 0;
+      if (!PlacesUIUtils.useAsyncTransactions)
+        return PlacesUtils.transactionManager.numberOfRedoItems > 0;
+
+      return PlacesTransactions.topRedoEntry != null;
     case "cmd_cut":
     case "placesCmd_cut":
-    case "placesCmd_moveBookmarks":
       for (let node of this._view.selectedNodes) {
-        // If selection includes history nodes or tags-as-bookmark,
-        // disallow cutting.
+        // If selection includes history nodes or tags-as-bookmark, disallow
+        // cutting.
         if (node.itemId == -1 ||
             (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
           return false;
         }
       }
       // Otherwise fall through the cmd_delete check.
     case "cmd_delete":
     case "placesCmd_delete":
@@ -145,149 +168,158 @@ PlacesController.prototype = {
     case "cmd_selectAll":
       if (this._view.selType != "single") {
         let rootNode = this._view.result.root;
         if (rootNode.containerOpen && rootNode.childCount > 0)
           return true;
       }
       return false;
     case "placesCmd_open":
-    case "placesCmd_open:tab":
     case "placesCmd_open:window":
     case "placesCmd_open:privatewindow":
-      var selectedNode = this._view.selectedNode;
+    case "placesCmd_open:tab": {
+      let selectedNode = this._view.selectedNode;
       return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
+    }
     case "placesCmd_new:folder":
-    case "placesCmd_new:livemark":
       return this._canInsert();
     case "placesCmd_new:bookmark":
       return this._canInsert();
     case "placesCmd_new:separator":
       return this._canInsert() &&
              !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems &&
              this._view.result.sortingMode ==
                  Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
-    case "placesCmd_show:info":
-      var selectedNode = this._view.selectedNode;
-      return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1;
-    case "placesCmd_reload":
+    case "placesCmd_show:info": {
+      let selectedNode = this._view.selectedNode;
+      return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1
+    }
+    case "placesCmd_reload": {
       // Livemark containers
-      var selectedNode = this._view.selectedNode;
+      let selectedNode = this._view.selectedNode;
       return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
-    case "placesCmd_sortBy:name":
-      var selectedNode = this._view.selectedNode;
+    }
+    case "placesCmd_sortBy:name": {
+      let selectedNode = this._view.selectedNode;
       return selectedNode &&
              PlacesUtils.nodeIsFolder(selectedNode) &&
              !PlacesUIUtils.isContentsReadOnly(selectedNode) &&
              this._view.result.sortingMode ==
                  Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+    }
     case "placesCmd_createBookmark":
       var node = this._view.selectedNode;
       return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
     default:
       return false;
     }
   },
 
   doCommand: function PC_doCommand(aCommand) {
     switch (aCommand) {
     case "cmd_undo":
-      PlacesUtils.transactionManager.undoTransaction();
+      if (!PlacesUIUtils.useAsyncTransactions) {
+        PlacesUtils.transactionManager.undoTransaction();
+        return;
+      }
+      PlacesTransactions.undo().catch(Cu.reportError);
       break;
     case "cmd_redo":
-      PlacesUtils.transactionManager.redoTransaction();
+      if (!PlacesUIUtils.useAsyncTransactions) {
+        PlacesUtils.transactionManager.redoTransaction();
+        return;
+      }
+      PlacesTransactions.redo().catch(Cu.reportError);
       break;
     case "cmd_cut":
     case "placesCmd_cut":
       this.cut();
       break;
     case "cmd_copy":
     case "placesCmd_copy":
       this.copy();
       break;
     case "cmd_paste":
     case "placesCmd_paste":
-      this.paste();
+      this.paste().catch(Cu.reportError);
       break;
     case "cmd_delete":
     case "placesCmd_delete":
-      this.remove("Remove Selection");
+      this.remove("Remove Selection").catch(Cu.reportError);
       break;
     case "placesCmd_deleteDataHost":
       var host;
       if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
         var queries = this._view.selectedNode.getQueries();
         host = queries[0].domain;
-      }
-      else
-        host = PlacesUtils._uri(this._view.selectedNode.uri).host;
+      } else
+        host = NetUtil.newURI(this._view.selectedNode.uri).host;
+      ForgetAboutSite.removeDataFromDomain(host)
+                     .catch(Cu.reportError);
       break;
     case "cmd_selectAll":
       this.selectAll();
       break;
     case "placesCmd_open":
-      PlacesUIUtils.openNodeIn(this._view.selectedNode, "current");
+      PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
+      break;
+    case "placesCmd_open:window":
+      PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
+      break;
+    case "placesCmd_open:privatewindow":
+      PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view, true);
       break;
     case "placesCmd_open:tab":
-      PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab");
-      break;
-    case "placesCmd_open:window":
-      PlacesUIUtils.openNodeIn(this._view.selectedNode, "window");
-      break;
-    case "placesCmd_open:privatewindow":
-      PlacesUIUtils.openNodeIn(this._view.selectedNode, "private");
+      PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
       break;
     case "placesCmd_new:folder":
       this.newItem("folder");
       break;
     case "placesCmd_new:bookmark":
       this.newItem("bookmark");
       break;
-    case "placesCmd_new:livemark":
-      this.newItem("livemark");
-      break;
     case "placesCmd_new:separator":
-      this.newSeparator();
+      this.newSeparator().catch(Cu.reportError);
       break;
     case "placesCmd_show:info":
-      var dds = document.getElementById("detailsDeck-splitter");
-      if (dds) {
-        dds.removeAttribute("state");
-        document.getElementById("editBMPanel_namePicker").focus();
-      } else {
-        this.showBookmarkPropertiesForSelection();
-      }
-      break;
-    case "placesCmd_moveBookmarks":
-      this.moveSelectedBookmarks();
+      this.showBookmarkPropertiesForSelection();
       break;
     case "placesCmd_reload":
       this.reloadSelectedLivemark();
       break;
     case "placesCmd_sortBy:name":
-      this.sortFolderByName();
+      this.sortFolderByName().catch(Cu.reportError);
       break;
     case "placesCmd_createBookmark":
-      var node = this._view.selectedNode;
-      PlacesUIUtils.showMinimalAddBookmarkUI(PlacesUtils._uri(node.uri), node.title);
+      let node = this._view.selectedNode;
+      PlacesUIUtils.showBookmarkDialog({ action: "add",
+                                         type: "bookmark",
+                                         hiddenRows: [ "description",
+                                                        "keyword",
+                                                        "location",
+                                                        "loadInSidebar" ],
+                                         uri: NetUtil.newURI(node.uri),
+                                         title: node.title
+                                       }, window.top);
       break;
     }
   },
 
   onEvent: function PC_onEvent(eventName) { },
 
+
   /**
    * Determine whether or not the selection can be removed, either by the
    * delete or cut operations based on whether or not any of its contents
    * are non-removable. We don't need to worry about recursion here since it
    * is a policy decision that a removable item not be placed inside a non-
    * removable item.
    *
-   * @returns true if all nodes in the selection can be removed,
-   *          false otherwise.
+   * @return true if all nodes in the selection can be removed,
+   *         false otherwise.
    */
   _hasRemovableSelection() {
     var ranges = this._view.removableSelectionRanges;
     if (!ranges.length)
       return false;
 
     var root = this._view.result.root;
 
@@ -310,105 +342,85 @@ PlacesController.prototype = {
    * Determines whether or not nodes can be inserted relative to the selection.
    */
   _canInsert: function PC__canInsert(isPaste) {
     var ip = this._view.insertionPoint;
     return ip != null && (isPaste || ip.isTag != true);
   },
 
   /**
-   * Determines whether or not the root node for the view is selected
-   */
-  rootNodeIsSelected: function PC_rootNodeIsSelected() {
-    var nodes = this._view.selectedNodes;
-    var root = this._view.result.root;
-    for (var i = 0; i < nodes.length; ++i) {
-      if (nodes[i] == root)
-        return true;
-    }
-
-    return false;
-  },
-
-  /**
    * Looks at the data on the clipboard to see if it is paste-able.
    * Paste-able data is:
    *   - in a format that the view can receive
-   * @returns true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
-                       - clipboard data is of type TEXT_UNICODE and
-                         is a valid URI.
+   * @return true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
+   *                  - clipboard data is of type TEXT_UNICODE and
+   *                    is a valid URI.
    */
   _isClipboardDataPasteable: function PC__isClipboardDataPasteable() {
     // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
     // pasteable, with no need to unwrap all the nodes.
 
-    var flavors = PlacesControllerDragHelper.placesFlavors;
+    var flavors = PlacesUIUtils.PLACES_FLAVORS;
     var clipboard = this.clipboard;
     var hasPlacesData =
       clipboard.hasDataMatchingFlavors(flavors, flavors.length,
                                        Ci.nsIClipboard.kGlobalClipboard);
     if (hasPlacesData)
       return this._view.insertionPoint != null;
 
     // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
     // pasting of valid "text/unicode" and "text/x-moz-url" data
     var xferable = Cc["@mozilla.org/widget/transferable;1"]
                      .createInstance(Ci.nsITransferable);
+    xferable.init(null);
 
-    xferable.init(null);
     xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
     xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
     clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
 
     try {
       // getAnyTransferData will throw if no data is available.
       var data = { }, type = { };
       xferable.getAnyTransferData(type, data, { });
       data = data.value.QueryInterface(Ci.nsISupportsString).data;
       if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
           type.value != PlacesUtils.TYPE_UNICODE)
         return false;
 
       // unwrapNodes() will throw if the data blob is malformed.
-      var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value);
+      PlacesUtils.unwrapNodes(data, type.value);
       return this._view.insertionPoint != null;
-    }
-    catch (e) {
+    } catch (e) {
       // getAnyTransferData or unwrapNodes failed
       return false;
     }
   },
 
   /**
    * Gathers information about the selected nodes according to the following
    * rules:
    *    "link"              node is a URI
-   *    "bookmark"          node is a bookamrk
+   *    "bookmark"          node is a bookmark
    *    "livemarkChild"     node is a child of a livemark
    *    "tagChild"          node is a child of a tag
    *    "folder"            node is a folder
    *    "query"             node is a query
    *    "separator"         node is a separator line
    *    "host"              node is a host
    *
-   * @returns an array of objects corresponding the selected nodes. Each
-   *          object has each of the properties above set if its corresponding
-   *          node matches the rule. In addition, the annotations names for each
-   *          node are set on its corresponding object as properties.
+   * @return an array of objects corresponding the selected nodes. Each
+   *         object has each of the properties above set if its corresponding
+   *         node matches the rule. In addition, the annotations names for each
+   *         node are set on its corresponding object as properties.
    * Notes:
    *   1) This can be slow, so don't call it anywhere performance critical!
-   *   2) A single-object array corresponding the root node is returned if
-   *      there's no selection.
    */
   _buildSelectionMetadata: function PC__buildSelectionMetadata() {
     var metadata = [];
-    var root = this._view.result.root;
     var nodes = this._view.selectedNodes;
-    if (nodes.length == 0)
-      nodes.push(root); // See the second note above
 
     for (var i = 0; i < nodes.length; i++) {
       var nodeData = {};
       var node = nodes[i];
       var nodeType = node.type;
       var uri = null;
 
       // We don't use the nodeIs* methods here to avoid going through the type
@@ -432,21 +444,19 @@ PlacesController.prototype = {
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
           nodeData["folder"] = true;
           break;
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
           nodeData["separator"] = true;
           break;
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
           nodeData["link"] = true;
-          uri = PlacesUtils._uri(node.uri);
+          uri = NetUtil.newURI(node.uri);
           if (PlacesUtils.nodeIsBookmark(node)) {
             nodeData["bookmark"] = true;
-            PlacesUtils.nodeIsTagQuery(node.parent)
-
             var parentNode = node.parent;
             if (parentNode) {
               if (PlacesUtils.nodeIsTagQuery(parentNode))
                 nodeData["tagChild"] = true;
               else if (this.hasCachedLivemarkInfo(parentNode))
                 nodeData["livemarkChild"] = true;
             }
           }
@@ -474,376 +484,348 @@ PlacesController.prototype = {
   },
 
   /**
    * Determines if a context-menu item should be shown
    * @param   aMenuItem
    *          the context menu item
    * @param   aMetaData
    *          meta data about the selection
-   * @returns true if the conditions (see buildContextMenu) are satisfied
-   *          and the item can be displayed, false otherwise.
+   * @return true if the conditions (see buildContextMenu) are satisfied
+   *         and the item can be displayed, false otherwise.
    */
   _shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) {
     var selectiontype = aMenuItem.getAttribute("selectiontype");
-    if (selectiontype == "multiple" && aMetaData.length == 1)
+    if (!selectiontype) {
+      selectiontype = "single|multiple";
+    }
+    var selectionTypes = selectiontype.split("|");
+    if (selectionTypes.includes("any")) {
+      return true;
+    }
+    var count = aMetaData.length;
+    if (count > 1 && !selectionTypes.includes("multiple"))
       return false;
-    if (selectiontype == "single" && aMetaData.length != 1)
+    if (count == 1 && !selectionTypes.includes("single"))
       return false;
+    // NB: if there is no selection, we show the item if and only if
+    // the selectiontype includes 'none' - the metadata list will be
+    // empty so none of the other criteria will apply anyway.
+    if (count == 0)
+      return selectionTypes.includes("none");
 
     var forceHideAttr = aMenuItem.getAttribute("forcehideselection");
     if (forceHideAttr) {
       var forceHideRules = forceHideAttr.split("|");
       for (let i = 0; i < aMetaData.length; ++i) {
         for (let j = 0; j < forceHideRules.length; ++j) {
           if (forceHideRules[j] in aMetaData[i])
             return false;
         }
       }
     }
 
     var selectionAttr = aMenuItem.getAttribute("selection");
-    if (selectionAttr) {
-      if (selectionAttr == "any")
-        return true;
-
-      var showRules = selectionAttr.split("|");
-      var anyMatched = false;
-      function metaDataNodeMatches(metaDataNode, rules) {
-        for (var i=0; i < rules.length; i++) {
-          if (rules[i] in metaDataNode)
-            return true;
-        }
-
-        return false;
-      }
-      for (var i = 0; i < aMetaData.length; ++i) {
-        if (metaDataNodeMatches(aMetaData[i], showRules))
-          anyMatched = true;
-        else
-          return false;
-      }
-      return anyMatched;
+    if (!selectionAttr) {
+      return !aMenuItem.hidden;
     }
 
-    return !aMenuItem.hidden;
+    if (selectionAttr == "any")
+      return true;
+
+    var showRules = selectionAttr.split("|");
+    var anyMatched = false;
+    function metaDataNodeMatches(metaDataNode, rules) {
+      for (var i = 0; i < rules.length; i++) {
+        if (rules[i] in metaDataNode)
+          return true;
+      }
+      return false;
+    }
+
+    for (var i = 0; i < aMetaData.length; ++i) {
+      if (metaDataNodeMatches(aMetaData[i], showRules))
+        anyMatched = true;
+      else
+        return false;
+    }
+    return anyMatched;
   },
 
   /**
    * Detects information (meta-data rules) about the current selection in the
    * view (see _buildSelectionMetadata) and sets the visibility state for each
    * of the menu-items in the given popup with the following rules applied:
+   *  0) The "ignoreitem" attribute may be set to "true" for this code not to
+   *     handle that menuitem.
    *  1) The "selectiontype" attribute may be set on a menu-item to "single"
    *     if the menu-item should be visible only if there is a single node
    *     selected, or to "multiple" if the menu-item should be visible only if
-   *     multiple nodes are selected. If the attribute is not set or if it is
-   *     set to an invalid value, the menu-item may be visible for both types of
-   *     selection.
+   *     multiple nodes are selected, or to "none" if the menuitems should be
+   *     visible for if there are no selected nodes, or to a |-separated
+   *     combination of these.
+   *     If the attribute is not set or set to an invalid value, the menu-item
+   *     may be visible irrespective of the selection.
    *  2) The "selection" attribute may be set on a menu-item to the various
    *     meta-data rules for which it may be visible. The rules should be
    *     separated with the | character.
    *  3) A menu-item may be visible only if at least one of the rules set in
    *     its selection attribute apply to each of the selected nodes in the
    *     view.
    *  4) The "forcehideselection" attribute may be set on a menu-item to rules
    *     for which it should be hidden. This attribute takes priority over the
    *     selection attribute. A menu-item would be hidden if at least one of the
    *     given rules apply to one of the selected nodes. The rules should be
    *     separated with the | character.
    *  5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to
    *     true if it should be hidden when there's no insertion point
-   *  6) The "hideifprivatebrowsing" attribute may be set on a menu-item to
-   *     true if it should be hidden in a private window
-   *  7) The visibility state of a menu-item is unchanged if none of these
+   *  6) The visibility state of a menu-item is unchanged if none of these
    *     attribute are set.
-   *  8) These attributes should not be set on separators for which the
+   *  7) These attributes should not be set on separators for which the
    *     visibility state is "auto-detected."
+   *  8) The "hideifprivatebrowsing" attribute may be set on a menu-item to
+   *     true if it should be hidden inside the private browsing mode
    * @param   aPopup
    *          The menupopup to build children into.
    * @return true if at least one item is visible, false otherwise.
    */
   buildContextMenu: function PC_buildContextMenu(aPopup) {
     var metadata = this._buildSelectionMetadata();
     var ip = this._view.insertionPoint;
     var noIp = !ip || ip.isTag;
 
     var separator = null;
     var visibleItemsBeforeSep = false;
-    var anyVisible = false;
+    var usableItemCount = 0;
     for (var i = 0; i < aPopup.childNodes.length; ++i) {
       var item = aPopup.childNodes[i];
+      if (item.getAttribute("ignoreitem") == "true") {
+        continue;
+      }
       if (item.localName != "menuseparator") {
         // We allow pasting into tag containers, so special case that.
-        var hideIfNoIP = item.hasAttribute("hideifnoinsertionpoint") &&
+        var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
                          noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
-        var hideIfPrivate = item.hasAttribute("hideifprivatebrowsing") && top.gPrivate;
-        item.hidden = hideIfNoIP || hideIfPrivate ||
-                      !this._shouldShowMenuItem(item, metadata);
+        var hideIfPrivate = item.getAttribute("hideifprivatebrowsing") == "true" &&
+                            PrivateBrowsingUtils.isWindowPrivate(window);
+        var shouldHideItem = hideIfNoIP || hideIfPrivate ||
+                             !this._shouldShowMenuItem(item, metadata);
+        item.hidden = item.disabled = shouldHideItem;
 
         if (!item.hidden) {
           visibleItemsBeforeSep = true;
-          anyVisible = true;
+          usableItemCount++;
 
           // Show the separator above the menu-item if any
           if (separator) {
             separator.hidden = false;
             separator = null;
           }
         }
-      }
-      else { // menuseparator
+      } else { // menuseparator
         // Initially hide it. It will be unhidden if there will be at least one
         // visible menu-item above and below it.
         item.hidden = true;
 
         // We won't show the separator at all if no items are visible above it
         if (visibleItemsBeforeSep)
           separator = item;
 
         // New separator, count again:
         visibleItemsBeforeSep = false;
       }
     }
 
     // Set Open Folder/Links In Tabs items enabled state if they're visible
-    if (anyVisible) {
+    if (usableItemCount > 0) {
       var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
-      if (!openContainerInTabsItem.hidden && this._view.selectedNode &&
-          PlacesUtils.nodeIsContainer(this._view.selectedNode)) {
-        openContainerInTabsItem.disabled =
-          !PlacesUtils.hasChildURIs(this._view.selectedNode);
-      }
-      else {
-        // see selectiontype rule in the overlay
-        var openLinksInTabsItem = document.getElementById("placesContext_openLinks:tabs");
-        openLinksInTabsItem.disabled = openLinksInTabsItem.hidden;
+      if (!openContainerInTabsItem.hidden) {
+        var containerToUse = this._view.selectedNode || this._view.result.root;
+        if (PlacesUtils.nodeIsContainer(containerToUse)) {
+          if (!PlacesUtils.hasChildURIs(containerToUse)) {
+            openContainerInTabsItem.disabled = true;
+            // Ensure that we don't display the menu if nothing is enabled:
+            usableItemCount--;
+          }
+        }
       }
     }
 
-    return anyVisible;
+    // Make sure correct PluralForms are diplayed when multiple pages are selected.
+    var deleteHistoryItem = document.getElementById("placesContext_delete_history");
+    deleteHistoryItem.label = PlacesUIUtils.getPluralString("cmd.deletePages.label",
+                                                            metadata.length);
+    deleteHistoryItem.accessKey = PlacesUIUtils.getString("cmd.deletePages.accesskey");
+    var createBookmarkItem = document.getElementById("placesContext_createBookmark");
+    createBookmarkItem.label = PlacesUIUtils.getPluralString("cmd.bookmarkPages.label",
+                                                             metadata.length);
+    createBookmarkItem.accessKey = PlacesUIUtils.getString("cmd.bookmarkPages.accesskey");
+
+    return usableItemCount > 0;
   },
 
   /**
    * Select all links in the current view.
    */
   selectAll: function PC_selectAll() {
     this._view.selectAll();
   },
 
   /**
    * Opens the bookmark properties for the selected URI Node.
    */
-  showBookmarkPropertiesForSelection:
-  function PC_showBookmarkPropertiesForSelection() {
-    var node = this._view.selectedNode;
+  showBookmarkPropertiesForSelection() {
+    let node = this._view.selectedNode;
     if (!node)
       return;
 
-    var itemType = PlacesUtils.nodeIsFolder(node) ||
-                   PlacesUtils.nodeIsTagQuery(node) ? "folder" : "bookmark";
-    var concreteId = PlacesUtils.getConcreteItemId(node);
-    var isRootItem = PlacesUtils.isRootItem(concreteId);
-    var itemId = node.itemId;
-    if (isRootItem || PlacesUtils.nodeIsTagQuery(node)) {
-      // If this is a root or the Tags query we use the concrete itemId to catch
-      // the correct title for the node.
-      itemId = concreteId;
-    }
-
-    PlacesUIUtils.showItemProperties(itemId, itemType,
-                                     isRootItem /* read only */);
+    PlacesUIUtils.showBookmarkDialog({ action: "edit",
+                                       node,
+                                       hiddenRows: [ "folderPicker" ]
+                                     }, window.top);
   },
 
   /**
    * This method can be run on a URI parameter to ensure that it didn't
    * receive a string instead of an nsIURI object.
    */
   _assertURINotString: function PC__assertURINotString(value) {
     NS_ASSERT((typeof(value) == "object") && !(value instanceof String),
            "This method should be passed a URI as a nsIURI object, not as a string.");
   },
 
   /**
    * Reloads the selected livemark if any.
    */
   reloadSelectedLivemark: function PC_reloadSelectedLivemark() {
     var selectedNode = this._view.selectedNode;
-    if (selectedNode)
-      PlacesUtils.livemarks.getLivemark({ id: selectedNode.itemId })
-                           .then(aLivemark => aLivemark.reload(true));
-  },
-
-  /**
-   * Gives the user a chance to cancel loading lots of tabs at once
-   */
-  _confirmOpenTabs: function(numTabsToOpen) {
-    const kWarnOnOpenPref = "browser.tabs.warnOnOpen";
-    var reallyOpen = true;
-    if (Services.prefs.getBoolPref(kWarnOnOpenPref)) {
-      if (numTabsToOpen >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
-        var promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"]
-                              .getService(Ci.nsIPromptService);
-
-        // default to true: if it were false, we wouldn't get this far
-        var warnOnOpen = { value: true };
-
-        var messageKey = "tabs.openWarningMultipleBranded";
-        var openKey = "tabs.openButtonMultiple";
-        const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
-        var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]
-                               .getService(Ci.nsIStringBundleService)
-                               .createBundle(BRANDING_BUNDLE_URI)
-                               .GetStringFromName("brandShortName");
-
-        var buttonPressed = promptService.confirmEx(window,
-          PlacesUIUtils.getString("tabs.openWarningTitle"),
-          PlacesUIUtils.getFormattedString(messageKey,
-            [numTabsToOpen, brandShortName]),
-          (promptService.BUTTON_TITLE_IS_STRING * promptService.BUTTON_POS_0)
-          + (promptService.BUTTON_TITLE_CANCEL * promptService.BUTTON_POS_1),
-          PlacesUIUtils.getString(openKey),
-          null, null,
-          PlacesUIUtils.getFormattedString("tabs.openWarningPromptMeBranded",
-            [brandShortName]),
-          warnOnOpen);
-
-         reallyOpen = (buttonPressed == 0);
-         // don't set the pref unless they press OK and it's false
-         if (reallyOpen && !warnOnOpen.value)
-           Services.prefs.setBoolPref(kWarnOnOpenPref, false);
-      }
+    if (selectedNode) {
+      let itemId = selectedNode.itemId;
+      PlacesUtils.livemarks.getLivemark({ id: itemId })
+        .then(aLivemark => {
+          aLivemark.reload(true);
+        }, Cu.reportError);
     }
-    return reallyOpen;
   },
 
   /**
    * Opens the links in the selected folder, or the selected links in new tabs.
    */
   openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
     var node = this._view.selectedNode;
+    var nodes = this._view.selectedNodes;
+    // In the case of no selection, open the root node:
+    if (!node && !nodes.length) {
+      node = this._view.result.root;
+    }
     if (node && PlacesUtils.nodeIsContainer(node))
-      PlacesUIUtils.openContainerNodeInTabs(this._view.selectedNode, aEvent);
+      PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this._view);
     else
-      PlacesUIUtils.openURINodesInTabs(this._view.selectedNodes, aEvent);
+      PlacesUIUtils.openURINodesInTabs(nodes, aEvent, this._view);
   },
 
   /**
    * Shows the Add Bookmark UI for the current insertion point.
    *
    * @param aType
    *        the type of the new item (bookmark/livemark/folder)
    */
   newItem: function PC_newItem(aType) {
-    var ip = this._view.insertionPoint;
+    let ip = this._view.insertionPoint;
     if (!ip)
       throw Cr.NS_ERROR_NOT_AVAILABLE;
 
-    var performed = false;
-    if (aType == "bookmark")
-      performed = PlacesUIUtils.showAddBookmarkUI(null, null, null, ip);
-    else if (aType == "livemark")
-      performed = PlacesUIUtils.showAddLivemarkUI(null, null, null, null, ip);
-    else // folder
-      performed = PlacesUIUtils.showAddFolderUI(null, ip);
-
+    let performed =
+      PlacesUIUtils.showBookmarkDialog({ action: "add",
+                                         type: aType,
+                                         defaultInsertionPoint: ip,
+                                         hiddenRows: [ "folderPicker" ]
+                                       }, window.top);
     if (performed) {
-      // select the new item
-      var insertedNodeId = PlacesUtils.bookmarks
-                                      .getIdForItemAt(ip.itemId, ip.index);
-      this._view.selectItems([insertedNodeId], false);
-    }
-  },
-
-
-  /**
-   * Create a new Bookmark folder somewhere. Prompts the user for the name
-   * of the folder.
-   */
-  newFolder: function PC_newFolder() {
-    var ip = this._view.insertionPoint;
-    if (!ip)
-      throw Cr.NS_ERROR_NOT_AVAILABLE;
-
-    var performed = false;
-    performed = PlacesUIUtils.showAddFolderUI(null, ip);
-    if (performed) {
-      // select the new item
-      var insertedNodeId = PlacesUtils.bookmarks
+      // Select the new item.
+      let insertedNodeId = PlacesUtils.bookmarks
                                       .getIdForItemAt(ip.itemId, ip.index);
       this._view.selectItems([insertedNodeId], false);
     }
   },
 
   /**
    * Create a new Bookmark separator somewhere.
    */
-  newSeparator: function PC_newSeparator() {
+  async newSeparator() {
     var ip = this._view.insertionPoint;
     if (!ip)
       throw Cr.NS_ERROR_NOT_AVAILABLE;
-    var txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
-    PlacesUtils.transactionManager.doTransaction(txn);
-    // select the new item
-    var insertedNodeId = PlacesUtils.bookmarks
-                                    .getIdForItemAt(ip.itemId, ip.index);
-    this._view.selectItems([insertedNodeId], false);
-  },
 
-  /**
-   * Opens a dialog for moving the selected nodes.
-   */
-  moveSelectedBookmarks: function PC_moveBookmarks() {
-    window.openDialog("chrome://communicator/content/bookmarks/moveBookmarks.xul",
-                      "", "chrome,modal,resizable=yes",
-                      this._view.selectedNodes);
+    if (!PlacesUIUtils.useAsyncTransactions) {
+      let txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
+      PlacesUtils.transactionManager.doTransaction(txn);
+      // Select the new item.
+      let insertedNodeId = PlacesUtils.bookmarks
+                                      .getIdForItemAt(ip.itemId, ip.index);
+      this._view.selectItems([insertedNodeId], false);
+      return;
+    }
+
+    let txn = PlacesTransactions.NewSeparator({ parentGuid: await ip.promiseGuid(),
+                                                index: ip.index });
+    let guid = await txn.transact();
+    let itemId = await PlacesUtils.promiseItemId(guid);
+    // Select the new item.
+    this._view.selectItems([itemId], false);
   },
 
   /**
    * Sort the selected folder by name
    */
-  sortFolderByName: function PC_sortFolderByName() {
-    var itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
-    var txn = new PlacesSortFolderByNameTransaction(itemId);
-    PlacesUtils.transactionManager.doTransaction(txn);
+  async sortFolderByName() {
+    let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
+    if (!PlacesUIUtils.useAsyncTransactions) {
+      var txn = new PlacesSortFolderByNameTransaction(itemId);
+      PlacesUtils.transactionManager.doTransaction(txn);
+      return;
+    }
+    let guid = await PlacesUtils.promiseItemGuid(itemId);
+    await PlacesTransactions.SortByName(guid).transact();
   },
 
   /**
    * Walk the list of folders we're removing in this delete operation, and
    * see if the selected node specified is already implicitly being removed
    * because it is a child of that folder.
    * @param   node
    *          Node to check for containment.
    * @param   pastFolders
    *          List of folders the calling function has already traversed
-   * @returns true if the node should be skipped, false otherwise.
+   * @return true if the node should be skipped, false otherwise.
    */
   _shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
     /**
      * Determines if a node is contained by another node within a resultset.
      * @param   node
      *          The node to check for containment for
      * @param   parent
      *          The parent container to check for containment in
-     * @returns true if node is a member of parent's children, false otherwise.
+     * @return true if node is a member of parent's children, false otherwise.
      */
-    function isContainedBy(node, parent) {
+    function isNodeContainedBy(parent) {
       var cursor = node.parent;
       while (cursor) {
         if (cursor == parent)
           return true;
         cursor = cursor.parent;
       }
       return false;
     }
 
-      for (var j = 0; j < pastFolders.length; ++j) {
-        if (isContainedBy(node, pastFolders[j]))
-          return true;
-      }
-      return false;
+    for (var j = 0; j < pastFolders.length; ++j) {
+      if (isNodeContainedBy(pastFolders[j]))
+        return true;
+    }
+    return false;
   },
 
   /**
    * Creates a set of transactions for the removal of a range of items.
    * A range is an array of adjacent nodes in a view.
    * @param   [in] range
    *          An array of nodes to remove. Should all be adjacent.
    * @param   [out] transactions
@@ -860,422 +842,507 @@ PlacesController.prototype = {
       var node = range[i];
       if (this._shouldSkipNode(node, removedFolders))
         continue;
 
       if (PlacesUtils.nodeIsTagQuery(node.parent)) {
         // This is a uri node inside a tag container.  It needs a special
         // untag transaction.
         var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
-        var uri = PlacesUtils._uri(node.uri);
-        let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
-        transactions.push(txn);
-      }
-      else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
+        var uri = NetUtil.newURI(node.uri);
+        if (PlacesUIUtils.useAsyncTransactions) {
+          let tag = node.parent.title;
+          if (!tag)
+            tag = PlacesUtils.bookmarks.getItemTitle(tagItemId);
+          transactions.push(PlacesTransactions.Untag({ uri, tag }));
+        } else {
+          let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
+          transactions.push(txn);
+        }
+      } else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
                PlacesUtils.nodeIsQuery(node.parent) &&
                PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
                  Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
         // This is a tag container.
         // Untag all URIs tagged with this tag only if the tag container is
         // child of the "Tags" query in the library, in all other places we
         // must only remove the query node.
-        var tag = node.title;
-        var URIs = PlacesUtils.tagging.getURIsForTag(tag);
-        for (var j = 0; j < URIs.length; j++) {
-          let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
-          transactions.push(txn);
+        let tag = node.title;
+        let URIs = PlacesUtils.tagging.getURIsForTag(tag);
+        if (PlacesUIUtils.useAsyncTransactions) {
+          transactions.push(PlacesTransactions.Untag({ tag, urls: URIs }));
+        } else {
+          for (var j = 0; j < URIs.length; j++) {
+            let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
+            transactions.push(txn);
+          }
         }
-      }
-      else if (PlacesUtils.nodeIsURI(node) &&
+      } else if (PlacesUtils.nodeIsURI(node) &&
                PlacesUtils.nodeIsQuery(node.parent) &&
                PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
                  Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
         // This is a uri node inside an history query.
-        var bhist = PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory);
-        bhist.removePage(PlacesUtils._uri(node.uri));
+        PlacesUtils.history.remove(node.uri).catch(Cu.reportError);
         // History deletes are not undoable, so we don't have a transaction.
-      }
-      else if (node.itemId == -1 &&
+      } else if (node.itemId == -1 &&
                PlacesUtils.nodeIsQuery(node) &&
                PlacesUtils.asQuery(node).queryOptions.queryType ==
                  Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
         // This is a dynamically generated history query, like queries
         // grouped by site, time or both.  Dynamically generated queries don't
         // have an itemId even if they are descendants of a bookmark.
         this._removeHistoryContainer(node);
         // History deletes are not undoable, so we don't have a transaction.
-      }
-      else {
+      } else {
         // This is a common bookmark item.
         if (PlacesUtils.nodeIsFolder(node)) {
           // If this is a folder we add it to our array of folders, used
           // to skip nodes that are children of an already removed folder.
           removedFolders.push(node);
         }
-        let txn = new PlacesRemoveItemTransaction(node.itemId);
-        transactions.push(txn);
+        if (PlacesUIUtils.useAsyncTransactions) {
+          transactions.push(
+            PlacesTransactions.Remove({ guid: node.bookmarkGuid }));
+        } else {
+          let txn = new PlacesRemoveItemTransaction(node.itemId);
+          transactions.push(txn);
+        }
       }
     }
   },
 
   /**
    * Removes the set of selected ranges from bookmarks.
    * @param   txnName
    *          See |remove|.
    */
-  _removeRowsFromBookmarks: function PC__removeRowsFromBookmarks(txnName) {
+  async _removeRowsFromBookmarks(txnName) {
     var ranges = this._view.removableSelectionRanges;
     var transactions = [];
     var removedFolders = [];
 
     for (var i = 0; i < ranges.length; i++)
       this._removeRange(ranges[i], transactions, removedFolders);
 
     if (transactions.length > 0) {
-      var txn = new PlacesAggregatedTransaction(txnName, transactions);
-      PlacesUtils.transactionManager.doTransaction(txn);
+      if (PlacesUIUtils.useAsyncTransactions) {
+        await PlacesTransactions.batch(transactions);
+      } else {
+        var txn = new PlacesAggregatedTransaction(txnName, transactions);
+        PlacesUtils.transactionManager.doTransaction(txn);
+      }
     }
   },
 
   /**
-   * Removes the set of selected ranges from history.
+   * Removes the set of selected ranges from history, asynchronously.
+   *
+   * @note history deletes are not undoable.
    */
   _removeRowsFromHistory: function PC__removeRowsFromHistory() {
-    // Other containers are history queries, just delete from history
-    // history deletes are not undoable.
-    var nodes = this._view.selectedNodes;
-    var URIs = [];
-    var bhist = PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory);
-    var root = this._view.result.root;
-
-    for (var i = 0; i < nodes.length; ++i) {
-      var node = nodes[i];
+    let nodes = this._view.selectedNodes;
+    let URIs = new Set();
+    for (let i = 0; i < nodes.length; ++i) {
+      let node = nodes[i];
       if (PlacesUtils.nodeIsURI(node)) {
-        var uri = PlacesUtils._uri(node.uri);
-        // avoid trying to delete the same url twice
-        if (URIs.indexOf(uri) < 0) {
-          URIs.push(uri);
-        }
+        URIs.add(node.uri);
+      } else if (PlacesUtils.nodeIsQuery(node) &&
+               PlacesUtils.asQuery(node).queryOptions.queryType ==
+                 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+        this._removeHistoryContainer(node);
       }
-      else if (PlacesUtils.nodeIsQuery(node) &&
-               PlacesUtils.asQuery(node).queryOptions.queryType ==
-                 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
-        this._removeHistoryContainer(node);
     }
 
-    // if we have to delete a lot of urls RemovePage will be slow, it's better
-    // to delete them in bunch and rebuild the full treeView
-    if (URIs.length > REMOVE_PAGES_MAX_SINGLEREMOVES) {
-      // do removal in chunks to avoid passing a too big array to removePages
-      for (var i = 0; i < URIs.length; i += REMOVE_PAGES_CHUNKLEN) {
-        var URIslice = URIs.slice(i, i + REMOVE_PAGES_CHUNKLEN);
-        // set DoBatchNotify (third param) only on the last chunk, so we update
-        // the treeView when we are done.
-        bhist.removePages(URIslice, URIslice.length,
-                          (i + REMOVE_PAGES_CHUNKLEN) >= URIs.length);
-      }
-    }
-    else {
-      // if we have to delete fewer urls, removepage will allow us to avoid
-      // rebuilding the full treeView
-      for (var i = 0; i < URIs.length; ++i)
-        bhist.removePage(URIs[i]);
-    }
+    PlacesUtils.history.remove([...URIs]).catch(Cu.reportError);
   },
 
   /**
    * Removes history visits for an history container node.
    * @param   [in] aContainerNode
    *          The container node to remove.
+   *
+   * @note history deletes are not undoable.
    */
-  _removeHistoryContainer: function PC_removeHistoryContainer(aContainerNode) {
-    var bhist = PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory);
+  _removeHistoryContainer: function PC__removeHistoryContainer(aContainerNode) {
     if (PlacesUtils.nodeIsHost(aContainerNode)) {
       // Site container.
-      bhist.removePagesFromHost(aContainerNode.title, true);
-    }
-    else if (PlacesUtils.nodeIsDay(aContainerNode)) {
+      PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true);
+    } else if (PlacesUtils.nodeIsDay(aContainerNode)) {
       // Day container.
-      var query = aContainerNode.getQueries()[0];
-      var beginTime = query.beginTime;
-      var endTime = query.endTime;
+      let query = aContainerNode.getQueries()[0];
+      let beginTime = query.beginTime;
+      let endTime = query.endTime;
       NS_ASSERT(query && beginTime && endTime,
                 "A valid date container query should exist!");
       // We want to exclude beginTime from the removal because
       // removePagesByTimeframe includes both extremes, while date containers
       // exclude the lower extreme.  So, if we would not exclude it, we would
       // end up removing more history than requested.
-      bhist.removePagesByTimeframe(beginTime+1, endTime);
+      PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime);
     }
   },
 
   /**
    * Removes the selection
    * @param   aTxnName
    *          A name for the transaction if this is being performed
    *          as part of another operation.
    */
-  remove: function PC_remove(aTxnName) {
+  async remove(aTxnName) {
     if (!this._hasRemovableSelection())
       return;
 
     NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
 
     var root = this._view.result.root;
 
-    if (PlacesUtils.nodeIsFolder(root))
-      this._removeRowsFromBookmarks(aTxnName);
-    else if (PlacesUtils.nodeIsQuery(root)) {
-      var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
-      if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS)
+    if (PlacesUtils.nodeIsFolder(root)) {
+      if (PlacesUIUtils.useAsyncTransactions)
+        await this._removeRowsFromBookmarks(aTxnName);
+      else
         this._removeRowsFromBookmarks(aTxnName);
-      else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
+    } else if (PlacesUtils.nodeIsQuery(root)) {
+      var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
+      if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) {
+        if (PlacesUIUtils.useAsyncTransactions)
+          await this._removeRowsFromBookmarks(aTxnName);
+        else
+          this._removeRowsFromBookmarks(aTxnName);
+      } else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
         this._removeRowsFromHistory();
-      else
+      } else {
         NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
-    }
-    else
+      }
+    } else
       NS_ASSERT(false, "unexpected root");
   },
 
   /**
    * Fills a DataTransfer object with the content of the selection that can be
    * dropped elsewhere.
    * @param   aEvent
    *          The dragstart event.
    */
   setDataTransfer: function PC_setDataTransfer(aEvent) {
     let dt = aEvent.dataTransfer;
-    let doCopy = ["copyLink", "copy", "link"].indexOf(dt.effectAllowed) != -1;
 
     let result = this._view.result;
     let didSuppressNotifications = result.suppressNotifications;
     if (!didSuppressNotifications)
       result.suppressNotifications = true;
 
+    function addData(type, index, feedURI) {
+      let wrapNode = PlacesUtils.wrapNode(node, type, feedURI);
+      dt.mozSetDataAt(type, wrapNode, index);
+    }
+
+    function addURIData(index, feedURI) {
+      addData(PlacesUtils.TYPE_X_MOZ_URL, index, feedURI);
+      addData(PlacesUtils.TYPE_UNICODE, index, feedURI);
+      addData(PlacesUtils.TYPE_HTML, index, feedURI);
+    }
+
     try {
       let nodes = this._view.draggableSelection;
       for (let i = 0; i < nodes.length; ++i) {
         var node = nodes[i];
 
-        function addData(type, index, overrideURI) {
-          let wrapNode = PlacesUtils.wrapNode(node, type, overrideURI, doCopy);
-          dt.mozSetDataAt(type, wrapNode, index);
-        }
-
-        function addURIData(index, overrideURI) {
-          addData(PlacesUtils.TYPE_X_MOZ_URL, index, overrideURI);
-          addData(PlacesUtils.TYPE_UNICODE, index, overrideURI);
-          addData(PlacesUtils.TYPE_HTML, index, overrideURI);
-        }
-
         // This order is _important_! It controls how this and other
         // applications select data to be inserted based on type.
         addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
 
         // Drop the feed uri for livemark containers
         let livemarkInfo = this.getCachedLivemarkInfo(node);
-        if (livemarkInfo)
+        if (livemarkInfo) {
           addURIData(i, livemarkInfo.feedURI.spec);
-        else if (node.uri)
+        } else if (node.uri) {
           addURIData(i);
+        }
       }
-    }
-    finally {
+    } finally {
       if (!didSuppressNotifications)
         result.suppressNotifications = false;
     }
   },
 
+  get clipboardAction() {
+    let action = {};
+    let actionOwner;
+    try {
+      let xferable = Cc["@mozilla.org/widget/transferable;1"]
+                       .createInstance(Ci.nsITransferable);
+      xferable.init(null);
+      xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION)
+      this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+      xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {});
+      [action, actionOwner] =
+        action.value.QueryInterface(Ci.nsISupportsString).data.split(",");
+    } catch (ex) {
+      // Paste from external sources don't have any associated action, just
+      // fallback to a copy action.
+      return "copy";
+    }
+    // For cuts also check who inited the action, since cuts across different
+    // instances should instead be handled as copies (The sources are not
+    // available for this instance).
+    if (action == "cut" && actionOwner != this.profileName)
+      action = "copy";
+
+    return action;
+  },
+
+  _releaseClipboardOwnership: function PC__releaseClipboardOwnership() {
+    if (this.cutNodes.length > 0) {
+      // This clears the logical clipboard, doesn't remove data.
+      this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
+    }
+  },
+
+  _clearClipboard: function PC__clearClipboard() {
+    let xferable = Cc["@mozilla.org/widget/transferable;1"]
+                     .createInstance(Ci.nsITransferable);
+    xferable.init(null);
+    // Empty transferables may cause crashes, so just add an unknown type.
+    const TYPE = "text/x-moz-place-empty";
+    xferable.addDataFlavor(TYPE);
+    xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0);
+    this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
+  },
+
+  _populateClipboard: function PC__populateClipboard(aNodes, aAction) {
+    // This order is _important_! It controls how this and other applications
+    // select data to be inserted based on type.
+    let contents = [
+      { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
+      { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
+      { type: PlacesUtils.TYPE_HTML, entries: [] },
+      { type: PlacesUtils.TYPE_UNICODE, entries: [] },
+    ];
+
+    // Avoid handling descendants of a copied node, the transactions take care
+    // of them automatically.
+    let copiedFolders = [];
+    aNodes.forEach(function(node) {
+      if (this._shouldSkipNode(node, copiedFolders))
+        return;
+      if (PlacesUtils.nodeIsFolder(node))
+        copiedFolders.push(node);
+
+      let livemarkInfo = this.getCachedLivemarkInfo(node);
+      let feedURI = livemarkInfo && livemarkInfo.feedURI.spec;
+
+      contents.forEach(function(content) {
+        content.entries.push(
+          PlacesUtils.wrapNode(node, content.type, feedURI)
+        );
+      });
+    }, this);
+
+    function addData(type, data) {
+      xferable.addDataFlavor(type);
+      xferable.setTransferData(type, PlacesUtils.toISupportsString(data),
+                               data.length * 2);
+    }
+
+    let xferable = Cc["@mozilla.org/widget/transferable;1"]
+                     .createInstance(Ci.nsITransferable);
+    xferable.init(null);
+    let hasData = false;
+    // This order matters here!  It controls how this and other applications
+    // select data to be inserted based on type.
+    contents.forEach(function(content) {
+      if (content.entries.length > 0) {
+        hasData = true;
+        let glue =
+          content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
+        addData(content.type, content.entries.join(glue));
+      }
+    });
+
+    // Track the exected action in the xferable.  This must be the last flavor
+    // since it's the least preferred one.
+    // Enqueue a unique instance identifier to distinguish operations across
+    // concurrent instances of the application.
+    addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName);
+
+    if (hasData) {
+      this.clipboard.setData(xferable,
+                             this.cutNodes.length > 0 ? this : null,
+                             Ci.nsIClipboard.kGlobalClipboard);
+    }
+  },
+
+  _cutNodes: [],
+  get cutNodes() {
+    return this._cutNodes;
+  },
+  set cutNodes(aNodes) {
+    let self = this;
+    function updateCutNodes(aValue) {
+      self._cutNodes.forEach(function(aNode) {
+        self._view.toggleCutNode(aNode, aValue);
+      });
+    }
+
+    updateCutNodes(false);
+    this._cutNodes = aNodes;
+    updateCutNodes(true);
+    return aNodes;
+  },
+
   /**
    * Copy Bookmarks and Folders to the clipboard
    */
   copy: function PC_copy() {
     let result = this._view.result;
-
     let didSuppressNotifications = result.suppressNotifications;
     if (!didSuppressNotifications)
       result.suppressNotifications = true;
-
     try {
-      let nodes = this._view.selectedNodes;
-
-      let xferable = Cc["@mozilla.org/widget/transferable;1"]
-                       .createInstance(Ci.nsITransferable);
-      xferable.init(null);
-      let foundFolder = false, foundLink = false;
-      let copiedFolders = [];
-      let placeString, mozURLString, htmlString, unicodeString;
-      placeString = mozURLString = htmlString = unicodeString = "";
-
-      for (let i = 0; i < nodes.length; ++i) {
-        let node = nodes[i];
-        if (this._shouldSkipNode(node, copiedFolders))
-          continue;
-        if (PlacesUtils.nodeIsFolder(node))
-          copiedFolders.push(node);
-
-        let self = this;
-        function generateChunk(type, overrideURI) {
-          let suffix = i < (nodes.length - 1) ? PlacesUtils.endl : "";
-          let uri = overrideURI;
-
-          let livemarkInfo = self.getCachedLivemarkInfo(node);
-          if (livemarkInfo)
-            uri = livemarkInfo.feedURI.spec;
-
-          mozURLString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_X_MOZ_URL,
-                                                 uri) + suffix);
-          unicodeString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_UNICODE,
-                                                 uri) + suffix);
-          htmlString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_HTML,
-                                                 uri) + suffix);
-
-          let placeSuffix = i < (nodes.length - 1) ? "," : "";
-          let resolveShortcuts = !PlacesControllerDragHelper.canMoveNode(node);
-          return PlacesUtils.wrapNode(node, type, overrideURI, resolveShortcuts) + placeSuffix;
-        }
-
-        // all items wrapped as TYPE_X_MOZ_PLACE
-        placeString += generateChunk(PlacesUtils.TYPE_X_MOZ_PLACE);
-      }
-
-      function addData(type, data) {
-        xferable.addDataFlavor(type);
-        xferable.setTransferData(type, PlacesUIUtils._wrapString(data), data.length * 2);
-      }
-      // This order is _important_! It controls how this and other applications
-      // select data to be inserted based on type.
-      if (placeString)
-        addData(PlacesUtils.TYPE_X_MOZ_PLACE, placeString);
-      if (mozURLString)
-        addData(PlacesUtils.TYPE_X_MOZ_URL, mozURLString);
-      if (unicodeString)
-        addData(PlacesUtils.TYPE_UNICODE, unicodeString);
-      if (htmlString)
-        addData(PlacesUtils.TYPE_HTML, htmlString);
-
-      if (placeString || unicodeString || htmlString || mozURLString) {
-        this.clipboard.setData(xferable, null,
-                               Ci.nsIClipboard.kGlobalClipboard);
-      }
-    }
-    finally {
+      this._populateClipboard(this._view.selectedNodes, "copy");
+    } finally {
       if (!didSuppressNotifications)
         result.suppressNotifications = false;
     }
   },
 
   /**
    * Cut Bookmarks and Folders to the clipboard
    */
   cut: function PC_cut() {
-    this.copy();
-    this.remove("Cut Selection");
+    let result = this._view.result;
+    let didSuppressNotifications = result.suppressNotifications;
+    if (!didSuppressNotifications)
+      result.suppressNotifications = true;
+    try {
+      this._populateClipboard(this._view.selectedNodes, "cut");
+      this.cutNodes = this._view.selectedNodes;
+    } finally {
+      if (!didSuppressNotifications)
+        result.suppressNotifications = false;
+    }
   },
 
   /**
    * Paste Bookmarks and Folders from the clipboard
    */
-  paste: function PC_paste() {
-    // Strategy:
-    //
-    // There can be data of various types (folder, separator, link) on the
-    // clipboard. We need to get all of that data and build edit transactions
-    // for them. This means asking the clipboard once for each type and
-    // aggregating the results.
-
-    /**
-     * Constructs a transferable that can receive data of specific types.
-     * @param   types
-     *          The types of data the transferable can hold, in order of
-     *          preference.
-     * @returns The transferable.
-     */
-    function makeXferable(types) {
-      var xferable =
-          Cc["@mozilla.org/widget/transferable;1"]
-            .createInstance(Ci.nsITransferable);
-
-      xferable.init(null);
-      for (var i = 0; i < types.length; ++i)
-        xferable.addDataFlavor(types[i]);
-      return xferable;
-    }
-
-    var clipboard = this.clipboard;
-
-    var ip = this._view.insertionPoint;
+  async paste() {
+    // No reason to proceed if there isn't a valid insertion point.
+    let ip = this._view.insertionPoint;
     if (!ip)
       throw Cr.NS_ERROR_NOT_AVAILABLE;
 
-    /**
-     * Gets a list of transactions to perform the paste of specific types.
-     * @param   types
-     *          The types of data to form paste transactions for
-     * @returns An array of transactions that perform the paste.
-     */
-    function getTransactions(types) {
-      var xferable = makeXferable(types);
-      clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+    let action = this.clipboardAction;
+
+    let xferable = Cc["@mozilla.org/widget/transferable;1"]
+                     .createInstance(Ci.nsITransferable);
+    xferable.init(null);
+    // This order matters here!  It controls the preferred flavors for this
+    // paste operation.
+    [ PlacesUtils.TYPE_X_MOZ_PLACE,
+      PlacesUtils.TYPE_X_MOZ_URL,
+      PlacesUtils.TYPE_UNICODE,
+    ].forEach(type => xferable.addDataFlavor(type));
 
-      var data = { }, type = { };
-      try {
-        xferable.getAnyTransferData(type, data, { });
-        data = data.value.QueryInterface(Ci.nsISupportsString).data;
-        var items = PlacesUtils.unwrapNodes(data, type.value);
-        var transactions = [];
-        var index = ip.index;
-        for (var i = 0; i < items.length; ++i) {
-          var txn;
-          if (ip.isTag) {
-            var uri = PlacesUtils._uri(items[i].uri);
-            txn = new PlacesTagURITransaction(uri, [ip.itemId]);
-          }
-          else {
-            // adjusted to make sure that items are given the correct index
-            // transactions insert differently if index == -1
-            // transaction will enqueue the item.
-            if (ip.index > -1)
-              index = ip.index + i;
-            txn = PlacesUIUtils.makeTransaction(items[i], type.value,
-                                                ip.itemId, index, true);
-          }
-          transactions.push(txn);
-        }
-        return transactions;
-      }
-      catch (e) {
-        // getAnyTransferData will throw if there is no data of the specified
-        // type on the clipboard.
-        // unwrapNodes will throw if the data that is present is malformed in
-        // some way.
-        // In either case, don't fail horribly, just return no data.
-      }
-      return [];
+    this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+
+    // Now get the clipboard contents, in the best available flavor.
+    let data = {}, type = {}, items = [];
+    try {
+      xferable.getAnyTransferData(type, data, {});
+      data = data.value.QueryInterface(Ci.nsISupportsString).data;
+      type = type.value;
+      items = PlacesUtils.unwrapNodes(data, type);
+    } catch (ex) {
+      // No supported data exists or nodes unwrap failed, just bail out.
+      return;
     }
 
-    // Get transactions to paste any folders, separators or links that might
-    // be on the clipboard, aggregate them and execute them.
-    var transactions = getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE,
-                                        PlacesUtils.TYPE_X_MOZ_URL,
-                                        PlacesUtils.TYPE_UNICODE]);
-    let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
-    PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
+    let itemsToSelect = [];
+    if (PlacesUIUtils.useAsyncTransactions) {
+      if (ip.isTag) {
+        let urls = items.filter(item => "uri" in item).map(item => Services.io.newURI(item.uri));
+        await PlacesTransactions.Tag({ urls, tag: ip.tagName }).transact();
+      } else {
+        await PlacesTransactions.batch(async function() {
+          let insertionIndex = ip.index;
+          let parent = await ip.promiseGuid();
+
+          for (let item of items) {
+            let doCopy = action == "copy";
+
+            // If this is not a copy, check for safety that we can move the
+            // source, otherwise report an error and fallback to a copy.
+            if (!doCopy &&
+                !PlacesControllerDragHelper.canMoveUnwrappedNode(item)) {
+              Cu.reportError("Tried to move an unmovable " +
+                             "Places node, reverting to a copy operation.");
+              doCopy = true;
+            }
+            let guid = await PlacesUIUtils.getTransactionForData(
+              item, type, parent, insertionIndex, doCopy).transact();
+            itemsToSelect.push(await PlacesUtils.promiseItemId(guid));
 
-    // select the pasted items, they should be consecutive
-    var insertedNodeIds = [];
-    for (var i = 0; i < transactions.length; ++i)
-      insertedNodeIds.push(PlacesUtils.bookmarks
-                                      .getIdForItemAt(ip.itemId, ip.index + i));
-    if (insertedNodeIds.length > 0)
-      this._view.selectItems(insertedNodeIds, false);
+            // Adjust index to make sure items are pasted in the correct
+            // position.  If index is DEFAULT_INDEX, items are just appended.
+            if (insertionIndex != PlacesUtils.bookmarks.DEFAULT_INDEX)
+              insertionIndex++;
+          }
+        });
+      }
+    } else {
+      let transactions = [];
+      let insertionIndex = ip.index;
+      for (let i = 0; i < items.length; ++i) {
+        if (ip.isTag) {
+          // Pasting into a tag container means tagging the item, regardless of
+          // the requested action.
+          let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri),
+                                                   [ip.itemId]);
+          transactions.push(tagTxn);
+          continue;
+        }
+
+        // Adjust index to make sure items are pasted in the correct position.
+        // If index is DEFAULT_INDEX, items are just appended.
+        if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX)
+          insertionIndex = ip.index + i;
+
+        // If this is not a copy, check for safety that we can move the source,
+        // otherwise report an error and fallback to a copy.
+        if (action != "copy" && !PlacesControllerDragHelper.canMoveUnwrappedNode(items[i])) {
+          Cu.reportError("Tried to move an unmovable Places " +
+                                       "node, reverting to a copy operation.");
+          action = "copy";
+        }
+        transactions.push(
+          PlacesUIUtils.makeTransaction(items[i], type, ip.itemId,
+                                        insertionIndex, action == "copy")
+        );
+      }
+
+      let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
+      PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
+
+      for (let i = 0; i < transactions.length; ++i) {
+        itemsToSelect.push(
+          PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i)
+        );
+      }
+    }
+
+    // Cut/past operations are not repeatable, so clear the clipboard.
+    if (action == "cut") {
+      this._clearClipboard();
+    }
+
+    if (itemsToSelect.length > 0)
+      this._view.selectItems(itemsToSelect, false);
   },
 
-
   /**
    * Cache the livemark info for a node.  This allows the controller and the
    * views to treat the given node as a livemark.
    * @param aNode
    *        a places result node.
    * @param aLivemarkInfo
    *        a mozILivemarkInfo object.
    */
@@ -1283,17 +1350,17 @@ PlacesController.prototype = {
     this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo);
   },
 
   /**
    * Returns whether or not there's cached mozILivemarkInfo object for a node.
    * @param aNode
    *        a places result node.
    * @return true if there's a cached mozILivemarkInfo object for
-   * aNode, false otherwise.
+   *         aNode, false otherwise.
    */
   hasCachedLivemarkInfo: function PC_hasCachedLivemarkInfo(aNode) {
     return this._cachedLivemarkInfoObjects.has(aNode);
   },
 
   /**
    * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
    * null otherwise.
@@ -1319,46 +1386,54 @@ var PlacesControllerDragHelper = {
   currentDropTarget: null,
 
   /**
    * Determines if the mouse is currently being dragged over a child node of
    * this menu. This is necessary so that the menu doesn't close while the
    * mouse is dragging over one of its submenus
    * @param   node
    *          The container node
-   * @returns true if the user is dragging over a node within the hierarchy of
-   *          the container, false otherwise.
+   * @return true if the user is dragging over a node within the hierarchy of
+   *         the container, false otherwise.
    */
   draggingOverChildNode: function PCDH_draggingOverChildNode(node) {
     let currentNode = this.currentDropTarget;
     while (currentNode) {
       if (currentNode == node)
         return true;
       currentNode = currentNode.parentNode;
     }
     return false;
   },
 
   /**
-   * @returns The current active drag session. Returns null if there is none.
+   * @return The current active drag session. Returns null if there is none.
    */
   getSession: function PCDH__getSession() {
     return this.dragService.getCurrentSession();
   },
 
   /**
-   * Extract the first accepted flavor from a flavors array.
+   * Extract the first accepted flavor from a list of flavors.
    * @param aFlavors
-   *        The flavors array.
+   *        The flavors list of type DOMStringList.
    */
   getFirstValidFlavor: function PCDH_getFirstValidFlavor(aFlavors) {
     for (let i = 0; i < aFlavors.length; i++) {
-      if (this.GENERIC_VIEW_DROP_TYPES.indexOf(aFlavors[i]) != -1)
+      if (PlacesUIUtils.SUPPORTED_FLAVORS.includes(aFlavors[i]))
         return aFlavors[i];
     }
+
+    // If no supported flavor is found, check if data includes text/plain
+    // contents.  If so, request them as text/unicode, a conversion will happen
+    // automatically.
+    if (aFlavors.contains("text/plain")) {
+        return PlacesUtils.TYPE_UNICODE;
+    }
+
     return null;
   },
 
   /**
    * Determines whether or not the data currently being dragged can be dropped
    * on a places view.
    * @param ip
    *        The insertion point where the items should be dropped.
@@ -1381,209 +1456,255 @@ var PlacesControllerDragHelper = {
       // expected cases, which are either unsupported flavors, or items which
       // cannot be dropped in the current insertionpoint. The last case will
       // likely force us to use unwrapNodes for the private data types of
       // places.
       if (flavor == TAB_DROP_TYPE)
         continue;
 
       let data = dt.mozGetDataAt(flavor, i);
-      let dragged;
+      let nodes;
       try {
-        dragged = PlacesUtils.unwrapNodes(data, flavor)[0];
-      }
-      catch (e) {
+        nodes = PlacesUtils.unwrapNodes(data, flavor);
+      } catch (e) {
         return false;
       }
 
-      // Only bookmarks and urls can be dropped into tag containers.
-      if (ip.isTag && ip.orientation == Ci.nsITreeView.DROP_ON &&
-          dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
-          (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
-           /^place:/.test(dragged.uri)))
-        return false;
+      for (let dragged of nodes) {
+        // Only bookmarks and urls can be dropped into tag containers.
+        if (ip.isTag &&
+            dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
+            (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
+             (dragged.uri && dragged.uri.startsWith("place:")) ))
+          return false;
 
-      // The following loop disallows the dropping of a folder on itself or
-      // on any of its descendants.
-      if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
-          /^place:/.test(dragged.uri)) {
-        let parentId = ip.itemId;
-        while (parentId != PlacesUtils.placesRootId) {
-          if (dragged.concreteId == parentId || dragged.id == parentId)
-            return false;
-          parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
+        // The following loop disallows the dropping of a folder on itself or
+        // on any of its descendants.
+        if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
+            (dragged.uri && dragged.uri.startsWith("place:")) ) {
+          let parentId = ip.itemId;
+          while (parentId != PlacesUtils.placesRootId) {
+            if (dragged.concreteId == parentId || dragged.id == parentId)
+              return false;
+            parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
+          }
         }
       }
     }
     return true;
   },
 
+  /**
+   * Determines if an unwrapped node can be moved.
+   *
+   * @param   aUnwrappedNode
+   *          A node unwrapped by PlacesUtils.unwrapNodes().
+   * @return True if the node can be moved, false otherwise.
+   */
+  canMoveUnwrappedNode(aUnwrappedNode) {
+    return aUnwrappedNode.id > 0 &&
+           !PlacesUtils.isRootItem(aUnwrappedNode.id) &&
+           (!aUnwrappedNode.parent || !PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent)) &&
+           aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
+           aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId;
+  },
 
   /**
    * Determines if a node can be moved.
    *
    * @param   aNode
    *          A nsINavHistoryResultNode node.
-   * @returns True if the node can be moved, false otherwise.
+   * @param   [optional] aDOMNode
+   *          A XUL DOM node.
+   * @return True if the node can be moved, false otherwise.
    */
-  canMoveNode:
-  function PCDH_canMoveNode(aNode) {
+  canMoveNode(aNode, aDOMNode) {
     // Only bookmark items are movable.
     if (aNode.itemId == -1)
       return false;
+
+    let parentNode = aNode.parent;
+    if (!parentNode) {
+      // Normally parentless places nodes can not be moved,
+      // but simulated bookmarked URI nodes are special.
+      return !!aDOMNode &&
+             aDOMNode.hasAttribute("simulated-places-node") &&
+             PlacesUtils.nodeIsBookmark(aNode);
+    }
+
     // Once tags and bookmarked are divorced, the tag-query check should be
     // removed.
-    let parentNode = aNode.parent;
-    return parentNode &&
-           !(PlacesUtils.nodeIsFolder(parentNode) &&
+    return !(PlacesUtils.nodeIsFolder(parentNode) &&
              PlacesUIUtils.isContentsReadOnly(parentNode)) &&
            !PlacesUtils.nodeIsTagQuery(parentNode);
-    },
+  },
 
   /**
    * Handles the drop of one or more items onto a view.
    * @param   insertionPoint
    *          The insertion point where the items should be dropped
    */
-  onDrop: function PCDH_onDrop(insertionPoint, dt) {
-    let doCopy = ["copy", "link"].indexOf(dt.dropEffect) != -1;
+  async onDrop(insertionPoint, dt) {
+    let doCopy = ["copy", "link"].includes(dt.dropEffect);
 
     let transactions = [];
     let dropCount = dt.mozItemCount;
     let movedCount = 0;
+    let parentGuid = PlacesUIUtils.useAsyncTransactions ?
+                       (await insertionPoint.promiseGuid()) : null;
+    let tagName = insertionPoint.tagName;
+
+    // Following flavors may contain duplicated data.
+    let duplicable = new Map();
+    duplicable.set(PlacesUtils.TYPE_UNICODE, new Set());
+    duplicable.set(PlacesUtils.TYPE_X_MOZ_URL, new Set());
+
     for (let i = 0; i < dropCount; ++i) {
       let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
       if (!flavor)
         return;
 
       let data = dt.mozGetDataAt(flavor, i);
-      let unwrapped;
+      if (duplicable.has(flavor)) {
+        let handled = duplicable.get(flavor);
+        if (handled.has(data))
+          continue;
+        handled.add(data);
+      }
+
+      let nodes;
       if (flavor != TAB_DROP_TYPE) {
-        // There's only ever one in the D&D case.
-        unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
-      }
-      else if (data instanceof XULElement && data.localName == "tab" &&
-               data.ownerDocument.defaultView instanceof ChromeWindow) {
+        nodes = PlacesUtils.unwrapNodes(data, flavor);
+      } else if (data instanceof XULElement && data.localName == "tab" &&
+               data.ownerGlobal instanceof ChromeWindow) {
         let uri = data.linkedBrowser.currentURI;
         let spec = uri ? uri.spec : "about:blank";
-        let title = data.label;
-        unwrapped = { uri: spec,
-                      title: data.label,
-                      type: PlacesUtils.TYPE_X_MOZ_URL};
-      }
-      else
-        throw("bogus data was passed as a tab")
+        nodes = [{ uri: spec,
+                   title: data.label,
+                   type: PlacesUtils.TYPE_X_MOZ_URL}];
+      } else
+        throw new Error("bogus data was passed as a tab");
 
-      let index = insertionPoint.index;
+      for (let unwrapped of nodes) {
+        let index = insertionPoint.index;
+
+        // Adjust insertion index to prevent reversal of dragged items. When you
+        // drag multiple elts upward: need to increment index or each successive
+        // elt will be inserted at the same index, each above the previous.
+        let dragginUp = insertionPoint.itemId == unwrapped.parent &&
+                        index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
+        if (index != -1 && dragginUp)
+          index += movedCount++;
 
-      // Adjust insertion index to prevent reversal of dragged items. When you
-      // drag multiple elts upward: need to increment index or each successive
-      // elt will be inserted at the same index, each above the previous.
-      let dragginUp = insertionPoint.itemId == unwrapped.parent &&
-                      index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
-      if (index != -1 && dragginUp)
-        index+= movedCount++;
-
-      // If dragging over a tag container we should tag the item.
-      if (insertionPoint.isTag &&
-          insertionPoint.orientation == Ci.nsITreeView.DROP_ON) {
-        let uri = PlacesUtils._uri(unwrapped.uri);
-        let tagItemId = insertionPoint.itemId;
-        let tagTxn = new PlacesTagURITransaction(uri, [tagItemId]);
-        transactions.push(tagTxn);
-      }
-      else {
-        transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
-                          flavor, insertionPoint.itemId,
-                          index, doCopy));
+        // If dragging over a tag container we should tag the item.
+        if (insertionPoint.isTag) {
+          let uri = NetUtil.newURI(unwrapped.uri);
+          let tagItemId = insertionPoint.itemId;
+          if (PlacesUIUtils.useAsyncTransactions)
+            transactions.push(PlacesTransactions.Tag({ uri, tag: tagName }));
+          else
+            transactions.push(new PlacesTagURITransaction(uri, [tagItemId]));
+        } else {
+          // If this is not a copy, check for safety that we can move the
+          // source, otherwise report an error and fallback to a copy.
+          if (!doCopy && !PlacesControllerDragHelper.canMoveUnwrappedNode(unwrapped)) {
+            Cu.reportError("Tried to move an unmovable Places " +
+                                         "node, reverting to a copy operation.");
+            doCopy = true;
+          }
+          if (PlacesUIUtils.useAsyncTransactions) {
+            transactions.push(
+              PlacesUIUtils.getTransactionForData(unwrapped,
+                                                  flavor,
+                                                  parentGuid,
+                                                  index,
+                                                  doCopy));
+          } else {
+            transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
+                                flavor, insertionPoint.itemId,
+                                index, doCopy));
+          }
+        }
       }
     }
 
-    let txn = new PlacesAggregatedTransaction("DropItems", transactions);
-    PlacesUtils.transactionManager.doTransaction(txn);
+    if (PlacesUIUtils.useAsyncTransactions) {
+      await PlacesTransactions.batch(transactions);
+    } else {
+      let txn = new PlacesAggregatedTransaction("DropItems", transactions);
+      PlacesUtils.transactionManager.doTransaction(txn);
+    }
   },
 
   /**
    * Checks if we can insert into a container.
    * @param   aContainer
    *          The container were we are want to drop
    */
-  disallowInsertion: function(aContainer) {
+  disallowInsertion(aContainer) {
     NS_ASSERT(aContainer, "empty container");
     // Allow dropping into Tag containers and editable folders.
     return !PlacesUtils.nodeIsTagQuery(aContainer) &&
            (!PlacesUtils.nodeIsFolder(aContainer) ||
             PlacesUIUtils.isContentsReadOnly(aContainer));
-  },
-
-  placesFlavors: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
-                  PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
-                  PlacesUtils.TYPE_X_MOZ_PLACE],
-
-  // The order matters.
-  GENERIC_VIEW_DROP_TYPES: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
-                            PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
-                            PlacesUtils.TYPE_X_MOZ_PLACE,
-                            PlacesUtils.TYPE_X_MOZ_URL,
-                            TAB_DROP_TYPE,
-                            PlacesUtils.TYPE_UNICODE],
+  }
 };
 
 
 XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
                                    "@mozilla.org/widget/dragservice;1",
                                    "nsIDragService");
 
 function goUpdatePlacesCommands() {
   // Get the controller for one of the places commands.
   var placesController = doGetPlacesControllerForCommand("placesCmd_open");
-  if (!placesController)
-    return;
-
   function updatePlacesCommand(aCommand) {
-    goSetCommandEnabled(aCommand, placesController.isCommandEnabled(aCommand));
+    goSetCommandEnabled(aCommand, placesController &&
+                                  placesController.isCommandEnabled(aCommand));
   }
 
   updatePlacesCommand("placesCmd_open");
-  updatePlacesCommand("placesCmd_open:tab");
   updatePlacesCommand("placesCmd_open:window");
   updatePlacesCommand("placesCmd_open:privatewindow");
+  updatePlacesCommand("placesCmd_open:tab");
   updatePlacesCommand("placesCmd_new:folder");
   updatePlacesCommand("placesCmd_new:bookmark");
-  updatePlacesCommand("placesCmd_new:livemark");
   updatePlacesCommand("placesCmd_new:separator");
   updatePlacesCommand("placesCmd_show:info");
-  updatePlacesCommand("placesCmd_moveBookmarks");
   updatePlacesCommand("placesCmd_reload");
   updatePlacesCommand("placesCmd_sortBy:name");
   updatePlacesCommand("placesCmd_cut");
   updatePlacesCommand("placesCmd_copy");
   updatePlacesCommand("placesCmd_paste");
   updatePlacesCommand("placesCmd_delete");
 }
 
-function doGetPlacesControllerForCommand(aCommand)
-{
+function doGetPlacesControllerForCommand(aCommand) {
   // A context menu may be built for non-focusable views.  Thus, we first try
   // to look for a view associated with document.popupNode
-  let popupNode = document.popupNode;
+  let popupNode;
+  try {
+    popupNode = document.popupNode;
+  } catch (e) {
+    // The document went away (bug 797307).
+    return null;
+  }
   if (popupNode) {
     let view = PlacesUIUtils.getViewForNode(popupNode);
     if (view && view._contextMenuShown)
       return view.controllers.getControllerForCommand(aCommand);
   }
 
   // When we're not building a context menu, only focusable views
   // are possible.  Thus, we can safely use the command dispatcher.
   let controller = top.document.commandDispatcher
                       .getControllerForCommand(aCommand);
   if (controller)
     return controller;
 
   return null;
 }
 
-function goDoPlacesCommand(aCommand)
-{
+function goDoPlacesCommand(aCommand) {
   let controller = doGetPlacesControllerForCommand(aCommand);
   if (controller && controller.isCommandEnabled(aCommand))
     controller.doCommand(aCommand);
 }
--- a/suite/common/places/content/editBookmarkOverlay.js
+++ b/suite/common/places/content/editBookmarkOverlay.js
@@ -1,978 +1,1220 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
+ * 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/. */
 
-ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
 const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;
 
 var gEditItemOverlay = {
-  _uri: null,
-  _itemId: -1,
-  _itemIds: [],
-  _uris: [],
-  _tags: [],
-  _allTags: [],
-  _multiEdit: false,
-  _itemType: -1,
-  _readOnly: false,
-  _hiddenRows: [],
   _observersAdded: false,
   _staticFoldersListBuilt: false,
-  _initialized: false,
-  _titleOverride: null,
+
+  _paneInfo: null,
+  _setPaneInfo(aInitInfo) {
+    if (!aInitInfo)
+      return this._paneInfo = null;
+
+    if ("uris" in aInitInfo && "node" in aInitInfo)
+      throw new Error("ambiguous pane info");
+    if (!("uris" in aInitInfo) && !("node" in aInitInfo))
+      throw new Error("Neither node nor uris set for pane info");
+
+    // Once we stop supporting legacy add-ons the code should throw if a node is
+    // not passed.
+    let node = "node" in aInitInfo ? aInitInfo.node : null;
+
+    // Since there's no true UI for folder shortcuts (they show up just as their target
+    // folders), when the pane shows for them it's opened in read-only mode, showing the
+    // properties of the target folder.
+    let itemId = node ? node.itemId : -1;
+    let itemGuid = node ? PlacesUtils.getConcreteItemGuid(node) : null;
+    let isItem = itemId != -1;
+    let isFolderShortcut = isItem &&
+                           node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+    let isTag = node && PlacesUtils.nodeIsTagQuery(node);
+    if (isTag) {
+      itemId = PlacesUtils.getConcreteItemId(node);
+      // For now we don't have access to the item guid synchronously for tags,
+      // so we'll need to fetch it later.
+    }
+    let isURI = node && PlacesUtils.nodeIsURI(node);
+    let uri = isURI ? NetUtil.newURI(node.uri) : null;
+    let title = node ? node.title : null;
+    let isBookmark = isItem && isURI;
+    let bulkTagging = !node;
+    let uris = bulkTagging ? aInitInfo.uris : null;
+    let visibleRows = new Set();
+    let isParentReadOnly = false;
+    let postData = aInitInfo.postData;
+    let parentId = -1;
+    let parentGuid = null;
+
+    if (node && isItem) {
+      if (!node.parent || (node.parent.itemId > 0 && !node.parent.bookmarkGuid)) {
+        throw new Error("Cannot use an incomplete node to initialize the edit bookmark panel");
+      }
+      let parent = node.parent;
+      isParentReadOnly = !PlacesUtils.nodeIsFolder(parent) ||
+                          PlacesUIUtils.isContentsReadOnly(parent);
+      parentId = parent.itemId;
+      parentGuid = parent.bookmarkGuid;
+    }
+
+    let focusedElement = aInitInfo.focusedElement;
+    let onPanelReady = aInitInfo.onPanelReady;
+
+    return this._paneInfo = { itemId, itemGuid, parentId, parentGuid, isItem,
+                              isURI, uri, title,
+                              isBookmark, isFolderShortcut, isParentReadOnly,
+                              bulkTagging, uris,
+                              visibleRows, postData, isTag, focusedElement,
+                              onPanelReady };
+  },
+
+  get initialized() {
+    return this._paneInfo != null;
+  },
+
+  // Backwards-compatibility getters
+  get itemId() {
+    if (!this.initialized || this._paneInfo.bulkTagging)
+      return -1;
+    return this._paneInfo.itemId;
+  },
+
+  get uri() {
+    if (!this.initialized)
+      return null;
+    if (this._paneInfo.bulkTagging)
+      return this._paneInfo.uris[0];
+    return this._paneInfo.uri;
+  },
+
+  get multiEdit() {
+    return this.initialized && this._paneInfo.bulkTagging;
+  },
+
+  // Check if the pane is initialized to show only read-only fields.
+  get readOnly() {
+    // TODO (Bug 1120314): Folder shortcuts are currently read-only due to some
+    // quirky implementation details (the most important being the "smart"
+    // semantics of node.title that makes hard to edit the right entry).
+    // This pane is read-only if:
+    //  * the panel is not initialized
+    //  * the node is a folder shortcut
+    //  * the node is not bookmarked and not a tag container
+    //  * the node is child of a read-only container and is not a bookmarked
+    //    URI nor a tag container
+    return !this.initialized ||
+           this._paneInfo.isFolderShortcut ||
+           (!this._paneInfo.isItem && !this._paneInfo.isTag) ||
+           (this._paneInfo.isParentReadOnly && !this._paneInfo.isBookmark && !this._paneInfo.isTag);
+  },
 
   // the first field which was edited after this panel was initialized for
   // a certain item
   _firstEditedField: "",
 
-  get itemId() {
-    return this._itemId;
-  },
+  _initNamePicker() {
+    if (this._paneInfo.bulkTagging)
+      throw new Error("_initNamePicker called unexpectedly");
 
-  get uri() {
-    return this._uri;
-  },
-
-  get multiEdit() {
-    return this._multiEdit;
+    // title may by null, which, for us, is the same as an empty string.
+    this._initTextField(this._namePicker, this._paneInfo.title || "");
   },
 
-  /**
-   * Determines the initial data for the item edited or added by this dialog
-   */
-  _determineInfo: function EIO__determineInfo(aInfo) {
-    // hidden rows
-    if (aInfo && aInfo.hiddenRows)
-      this._hiddenRows = aInfo.hiddenRows;
-    else
-      this._hiddenRows.splice(0, this._hiddenRows.length);
-    // force-read-only
-    this._readOnly = aInfo && aInfo.forceReadOnly;
-    // override title
-    this._titleOverride = aInfo && aInfo.titleOverride;
+  _initLocationField() {
+    if (!this._paneInfo.isURI)
+      throw new Error("_initLocationField called unexpectedly");
+    this._initTextField(this._locationField, this._paneInfo.uri.spec);
+  },
+
+  _initDescriptionField() {
+    if (!this._paneInfo.isItem)
+      throw new Error("_initDescriptionField called unexpectedly");
+
+    this._initTextField(this._descriptionField,
+                        PlacesUIUtils.getItemDescription(this._paneInfo.itemId));
   },
 
-  _showHideRows: function EIO__showHideRows() {
-    var isBookmark = this._itemId != -1 &&
-                     this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK;
-    var isQuery = false;
-    if (this._uri)
-      isQuery = this._uri.schemeIs("place");
+  async _initKeywordField(newKeyword = "") {
+    if (!this._paneInfo.isBookmark) {
+      throw new Error("_initKeywordField called unexpectedly");
+    }
+
+    // Reset the field status synchronously now, eventually we'll reinit it
+    // later if we find an existing keyword. This way we can ensure to be in a
+    // consistent status when reusing the panel across different bookmarks.
+    this._keyword = newKeyword;
+    this._initTextField(this._keywordField, newKeyword);
 
-    this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1;
-    this._element("folderRow").collapsed =
-      this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly;
-    this._element("tagsRow").collapsed = !this._uri ||
-      this._hiddenRows.indexOf("tags") != -1 || isQuery;
-    // Collapse the tag selector if the item does not accept tags.
-    if (!this._element("tagsSelectorRow").collapsed &&
-        this._element("tagsRow").collapsed)
-      this.toggleTagsSelector();
-    this._element("descriptionRow").collapsed =
-      this._hiddenRows.indexOf("description") != -1 || this._readOnly;
-    this._element("keywordRow").collapsed = !isBookmark || this._readOnly ||
-      this._hiddenRows.indexOf("keyword") != -1 || isQuery;
-    this._element("locationRow").collapsed = !(this._uri && !isQuery) ||
-      this._hiddenRows.indexOf("location") != -1;
-    this._element("feedLocationRow").collapsed = !this._isLivemark ||
-      this._hiddenRows.indexOf("feedLocation") != -1;
-    this._element("siteLocationRow").collapsed = !this._isLivemark ||
-      this._hiddenRows.indexOf("siteLocation") != -1;
-    this._element("selectionCount").hidden = !this._multiEdit;
+    if (!newKeyword) {
+      let entries = [];
+      await PlacesUtils.keywords.fetch({ url: this._paneInfo.uri.spec },
+                                       e => entries.push(e));
+      if (entries.length > 0) {
+        // We show an existing keyword if either POST data was not provided, or
+        // if the POST data is the same.
+        let existingKeyword = entries[0].keyword;
+        let postData = this._paneInfo.postData;
+        if (postData) {
+          let sameEntry = entries.find(e => e.postData === postData);
+          existingKeyword = sameEntry ? sameEntry.keyword : "";
+        }
+        if (existingKeyword) {
+          this._keyword = existingKeyword;
+          // Update the text field to the existing keyword.
+          this._initTextField(this._keywordField, this._keyword);
+        }
+      }
+    }
+  },
+
+  _initLoadInSidebar() {
+    if (!this._paneInfo.isBookmark)
+      throw new Error("_initLoadInSidebar called unexpectedly");
+
+    this._loadInSidebarCheckbox.checked =
+      PlacesUtils.annotations.itemHasAnnotation(
+        this._paneInfo.itemId, PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
   },
 
   /**
-   * Initialize the panel
-   * @param aFor
-   *        Either a places-itemId (of a bookmark, folder or a live bookmark),
-   *        an array of itemIds (used for bulk tagging), or a URI object (in
-   *        which case, the panel would be initialized in read-only mode).
-   * @param [optional] aInfo
-   *        JS object which stores additional info for the panel
-   *        initialization. The following properties may bet set:
-   *        * hiddenRows (Strings array): list of rows to be hidden regardless
-   *          of the item edited. Possible values: "title", "location",
-   *          "description", "keyword", "feedLocation",
-   *          "siteLocation", folderPicker"
-   *        * forceReadOnly - set this flag to initialize the panel to its
-   *          read-only (view) mode even if the given item is editable.
+   * Initialize the panel.
+   *
+   * @param aInfo
+   *        An object having:
+   *        1. one of the following properties:
+   *        - node: either a result node or a node-like object representing the
+   *          item to be edited. A node-like object must have the following
+   *          properties (with values that match exactly those a result node
+   *          would have): itemId, bookmarkGuid, uri, title, type.
+   *        - uris: an array of uris for bulk tagging.
+   *
+   *        2. any of the following optional properties:
+   *          - hiddenRows (Strings array): list of rows to be hidden regardless
+   *            of the item edited. Possible values: "title", "location",
+   *            "description", "keyword", "loadInSidebar", "feedLocation",
+   *            "siteLocation", folderPicker"
    */
-  initPanel: function EIO_initPanel(aFor, aInfo) {
-    // For sanity ensure that the implementer has uninited the panel before
-    // trying to init it again, or we could end up leaking due to observers.
-    if (this._initialized)
-      this.uninitPanel(false);
-
-    var aItemIdList;
-    if (Array.isArray(aFor)) {
-      aItemIdList = aFor;
-      aFor = aItemIdList[0];
-    }
-    else if (this._multiEdit) {
-      this._multiEdit = false;
-      this._tags = [];
-      this._uris = [];
-      this._allTags = [];
-      this._itemIds = [];
-      this._element("selectionCount").hidden = true;
+  initPanel(aInfo) {
+    if (typeof(aInfo) != "object" || aInfo === null)
+      throw new Error("aInfo must be an object.");
+    if ("node" in aInfo) {
+      try {
+        aInfo.node.type;
+      } catch (e) {
+        // If the lazy loader for |type| generates an exception, it means that
+        // this bookmark could not be loaded. This sometimes happens when tests
+        // create a bookmark by clicking the bookmark star, then try to cleanup
+        // before the bookmark panel has finished opening. Either way, if we
+        // cannot retrieve the bookmark information, we cannot open the panel.
+        return;
+      }
     }
 
-    this._folderMenuList = this._element("folderMenuList");
-    this._folderTree = this._element("folderTree");
+    // For sanity ensure that the implementer has uninited the panel before
+    // trying to init it again, or we could end up leaking due to observers.
+    if (this.initialized)
+      this.uninitPanel(false);
+
+    let { parentId, isItem, isURI,
+          isBookmark, bulkTagging, uris,
+          visibleRows, focusedElement,
+          onPanelReady } = this._setPaneInfo(aInfo);
 
-    this._determineInfo(aInfo);
-    if (aFor instanceof Ci.nsIURI) {
-      this._itemId = -1;
-      this._uri = aFor;
-      this._readOnly = true;
+    let showOrCollapse =
+      (rowId, isAppropriateForInput, nameInHiddenRows = null) => {
+        let visible = isAppropriateForInput;
+        if (visible && "hiddenRows" in aInfo && nameInHiddenRows)
+          visible &= aInfo.hiddenRows.indexOf(nameInHiddenRows) == -1;
+        if (visible)
+          visibleRows.add(rowId);
+        return !(this._element(rowId).collapsed = !visible);
+      };
+
+    if (showOrCollapse("nameRow", !bulkTagging, "name")) {
+      this._initNamePicker();
+      this._namePicker.readOnly = this.readOnly;
     }
-    else {
-      this._itemId = aFor;
-      // We can't store information on livemarks.
-      if (aFor == -1)
-        this._readOnly = true;
-      var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
-      this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId);
-      if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
-        this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
-        this._initTextField("keywordField",
-                            PlacesUtils.bookmarks
-                                       .getKeywordForBookmark(this._itemId));
-      }
-      else {
-        this._uri = null;
-        this._isLivemark = false;
-        PlacesUtils.livemarks.getLivemark({ id: this._itemId })
-                             .then(aLivemark => {
-          this._isLivemark = true;
-          this._initTextField("feedLocationField", aLivemark.feedURI.spec, true);
-          this._initTextField("siteLocationField",
-                              aLivemark.siteURI ? aLivemark.siteURI.spec : "", true);
-          this._showHideRows();
-        }, () => undefined);
-      }
 
-      // folder picker
-      this._initFolderMenuList(containerId);
-
-      // description field
-      this._initTextField("descriptionField",
-                          PlacesUIUtils.getItemDescription(this._itemId));
+    // In some cases we want to hide the location field, since it's not
+    // human-readable, but we still want to initialize it.
+    showOrCollapse("locationRow", isURI, "location");
+    if (isURI) {
+      this._initLocationField();
+      this._locationField.readOnly = this.readOnly;
     }
 
-    if (this._itemId == -1 ||
-        this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
-      this._isLivemark = false;
+    // hide the description field for
+    if (showOrCollapse("descriptionRow", isItem && !this.readOnly,
+                       "description")) {
+      this._initDescriptionField();
+      this._descriptionField.readOnly = this.readOnly;
+    }
 
-      this._initTextField("locationField", this._uri.spec);
-      if (!aItemIdList) {
-        var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
-        this._initTextField("tagsField", tags, false);
-      }
-      else {
-        this._multiEdit = true;
-        this._allTags = [];
-        this._itemIds = aItemIdList;
-        for (var i = 0; i < aItemIdList.length; i++) {
-          if (aItemIdList[i] instanceof Ci.nsIURI) {
-            this._uris[i] = aItemIdList[i];
-            this._itemIds[i] = -1;
-          }
-          else
-            this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]);
-          this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]);
-        }
-        this._allTags = this._getCommonTags();
-        this._initTextField("tagsField", this._allTags.join(", "), false);
-        this._element("itemsCountText").value =
-          PlacesUIUtils.getFormattedString("detailsPane.multipleItems",
-                                           [this._itemIds.length]);
-      }
-
-      // tags selector
-      this._rebuildTagsSelectorList();
+    if (showOrCollapse("keywordRow", isBookmark, "keyword")) {
+      this._initKeywordField().catch(Cu.reportError);
+      this._keywordField.readOnly = this.readOnly;
     }
 
-    // name picker
-    this._initNamePicker();
+    // Collapse the tag selector if the item does not accept tags.
+    if (showOrCollapse("tagsRow", isURI || bulkTagging, "tags"))
+      this._initTagsField();
+    else if (!this._element("tagsSelectorRow").collapsed)
+      this.toggleTagsSelector();
 
-    this._showHideRows();
+    // Load in sidebar.
+    if (showOrCollapse("loadInSidebarCheckbox", isBookmark, "loadInSidebar")) {
+      this._initLoadInSidebar();
+    }
 
-    // observe changes
+    // Folder picker.
+    // Technically we should check that the item is not moveable, but that's
+    // not cheap (we don't always have the parent), and there's no use case for
+    // this (it's only the Star UI that shows the folderPicker)
+    if (showOrCollapse("folderRow", isItem, "folderPicker")) {
+      this._initFolderMenuList(parentId).catch(Cu.reportError);
+    }
+
+    // Selection count.
+    if (showOrCollapse("selectionCount", bulkTagging)) {
+      this._element("itemsCountText").value =
+        PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+                                      uris.length,
+                                      [uris.length]);
+    }
+
+    // Observe changes.
     if (!this._observersAdded) {
-      // Single bookmarks observe any change.  History entries and multiEdit
-      // observe only tags changes, through bookmarks.
-      if (this._itemId != -1 || this._uri || this._multiEdit)
-        PlacesUtils.bookmarks.addObserver(this);
+      PlacesUtils.bookmarks.addObserver(this);
       window.addEventListener("unload", this);
       this._observersAdded = true;
     }
 
-    this._initialized = true;
+    let focusElement = () => {
+      // The focusedElement possible values are:
+      //  * preferred: focus the field that the user touched first the last
+      //    time the pane was shown (either namePicker or tagsField)
+      //  * first: focus the first non collapsed textbox
+      // Note: since all controls are collapsed by default, we don't get the
+      // default XUL dialog behavior, that selects the first control, so we set
+      // the focus explicitly.
+      let elt;
+      if (focusedElement === "preferred") {
+        /* eslint-disable no-undef */
+        elt = this._element(Services.prefs.getCharPref("browser.bookmarks.editDialog.firstEditField"));
+        /* eslint-enable no-undef */
+      } else if (focusedElement === "first") {
+        elt = document.querySelector("textbox:not([collapsed=true])");
+      }
+      if (elt) {
+        elt.focus();
+        elt.select();
+      }
+    };
+
+    if (onPanelReady) {
+      onPanelReady(focusElement);
+    } else {
+      focusElement();
+    }
   },
 
   /**
-   * Finds tags that are in common among this._tags entries that track tags
-   * for each selected uri.
-   * The tags arrays should be kept up-to-date for this to work properly.
-   *
-   * @return array of common tags for the selected uris.
+   * Finds tags that are in common among this._currentInfo.uris;
    */
-  _getCommonTags: function() {
-    return this._tags[0].filter(
-      aTag => this._tags.every(
-        aTags => aTags.indexOf(aTag) != -1
-      )
-    );
+  _getCommonTags() {
+    if ("_cachedCommonTags" in this._paneInfo)
+      return this._paneInfo._cachedCommonTags;
+
+    let uris = [...this._paneInfo.uris];
+    let firstURI = uris.shift();
+    let commonTags = new Set(PlacesUtils.tagging.getTagsForURI(firstURI));
+    if (commonTags.size == 0)
+      return this._cachedCommonTags = [];
+
+    for (let uri of uris) {
+      let curentURITags = PlacesUtils.tagging.getTagsForURI(uri);
+      for (let tag of commonTags) {
+        if (!curentURITags.includes(tag)) {
+          commonTags.delete(tag)
+          if (commonTags.size == 0)
+            return this._paneInfo.cachedCommonTags = [];
+        }
+      }
+    }
+    return this._paneInfo._cachedCommonTags = [...commonTags];
   },
 
-  _initTextField: function(aTextFieldId, aValue, aReadOnly) {
-    var field = this._element(aTextFieldId);
-    field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly;
+  _initTextField(aElement, aValue) {
+    if (aElement.value != aValue) {
+      aElement.value = aValue;
 
-    if (fiel