Bug 1439315 - 6 - Move some utils into PlacesController or PlacesUIUtils. r=standard8 draft
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 28 Feb 2018 13:51:28 +0100
changeset 761874 9e9f885db98c72a857c941b2eb6b8a294d1b570e
parent 761873 a72bbdee84965d716b14d5ddbb020579cdc83f8c
child 761875 f3b8493ad943cf2a58be8e2c76d5c73c0d59132e
push id101023
push usermak77@bonardo.net
push dateThu, 01 Mar 2018 15:34:13 +0000
reviewersstandard8
bugs1439315
milestone60.0a1
Bug 1439315 - 6 - Move some utils into PlacesController or PlacesUIUtils. r=standard8 MozReview-Commit-ID: D7J6hY7Akro
browser/components/places/PlacesUIUtils.jsm
browser/components/places/content/browserPlacesViews.js
browser/components/places/content/controller.js
browser/components/places/content/menu.xml
browser/components/places/content/tree.xml
browser/components/places/content/treeView.js
browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -252,71 +252,16 @@ var PlacesUIUtils = {
     });
   },
 
   getString: function PUIU_getString(key) {
     return bundle.GetStringFromName(key);
   },
 
   /**
-   * Constructs a Places Transaction for the drop or paste of a blob of data
-   * into a container.
-   *
-   * @param   aData
-   *          The unwrapped data blob of dropped or pasted 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.
-   *
-   * @return  a Places Transaction that can be transacted for performing the
-   *          move/insert command.
-   */
-  getTransactionForData(aData, aNewParentGuid, aIndex, aCopy) {
-    if (!this.SUPPORTED_FLAVORS.includes(aData.type))
-      throw new Error(`Unsupported '${aData.type}' data type`);
-
-    if ("itemGuid" in aData && "instanceId" in aData &&
-        aData.instanceId == PlacesUtils.instanceId) {
-      if (!this.PLACES_FLAVORS.includes(aData.type))
-        throw new Error(`itemGuid unexpectedly set on ${aData.type} data`);
-
-      let info = { guid: aData.itemGuid,
-                   newParentGuid: aNewParentGuid,
-                   newIndex: aIndex };
-      if (aCopy) {
-        info.excludingAnnotation = "Places/SmartBookmark";
-        return PlacesTransactions.Copy(info);
-      }
-      return PlacesTransactions.Move(info);
-    }
-
-    // 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 });
-    }
-
-    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 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 bookmarkProperties.js
    * @param aWindow
    *        Owner window for the new dialog.
    *
@@ -1242,19 +1187,130 @@ var PlacesUIUtils = {
     try {
       await functionToWrap();
     } finally {
       if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) {
         resultNode.onEndUpdateBatch();
       }
     }
   },
+
+  /**
+   * Constructs a Places Transaction for the drop or paste of a blob of data
+   * into a container.
+   *
+   * @param   aData
+   *          The unwrapped data blob of dropped or pasted 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.
+   *
+   * @return  a Places Transaction that can be transacted for performing the
+   *          move/insert command.
+   */
+  getTransactionForData(aData, aNewParentGuid, aIndex, aCopy) {
+    if (!this.SUPPORTED_FLAVORS.includes(aData.type))
+      throw new Error(`Unsupported '${aData.type}' data type`);
+
+    if ("itemGuid" in aData && "instanceId" in aData &&
+        aData.instanceId == PlacesUtils.instanceId) {
+      if (!this.PLACES_FLAVORS.includes(aData.type))
+        throw new Error(`itemGuid unexpectedly set on ${aData.type} data`);
+
+      let info = { guid: aData.itemGuid,
+                   newParentGuid: aNewParentGuid,
+                   newIndex: aIndex };
+      if (aCopy) {
+        info.excludingAnnotation = "Places/SmartBookmark";
+        return PlacesTransactions.Copy(info);
+      }
+      return PlacesTransactions.Move(info);
+    }
+
+    // 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 });
+    }
+
+    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 });
+  },
+
+  /**
+   * Processes a set of transfer items that have been dropped or pasted.
+   * Batching will be applied where necessary.
+   *
+   * @param {Array} items A list of unwrapped nodes to process.
+   * @param {Object} insertionPoint The requested point for insertion.
+   * @param {Boolean} doCopy Set to true to copy the items, false will move them
+   *                         if possible.
+   * @paramt {Object} view The view that should be used for batching.
+   * @return {Array} Returns an empty array when the insertion point is a tag, else
+   *                 returns an array of copied or moved guids.
+   */
+  async handleTransferItems(items, insertionPoint, doCopy, view) {
+    let transactions;
+    let itemsCount;
+    if (insertionPoint.isTag) {
+      let urls = items.filter(item => "uri" in item).map(item => item.uri);
+      itemsCount = urls.length;
+      transactions = [PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName })];
+    } else {
+      let insertionIndex = await insertionPoint.getIndex();
+      itemsCount = items.length;
+      transactions = await getTransactionsForTransferItems(
+        items, insertionIndex, insertionPoint.guid, doCopy);
+    }
+
+    // Check if we actually have something to add, if we don't it probably wasn't
+    // valid, or it was moving to the same location, so just ignore it.
+    if (!transactions.length) {
+      return [];
+    }
+
+    let guidsToSelect = [];
+    let resultForBatching = getResultForBatching(view);
+
+    // If we're inserting into a tag, we don't get the guid, so we'll just
+    // pass the transactions direct to the batch function.
+    let batchingItem = transactions;
+    if (!insertionPoint.isTag) {
+      // If we're not a tag, then we need to get the ids of the items to select.
+      batchingItem = async () => {
+        for (let transaction of transactions) {
+          let guid = await transaction.transact();
+          if (guid) {
+            guidsToSelect.push(guid);
+          }
+        }
+      };
+    }
+
+    await this.batchUpdatesForNode(resultForBatching, itemsCount, async () => {
+      await PlacesTransactions.batch(batchingItem);
+    });
+
+    return guidsToSelect;
+  },
 };
 
-
 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],
 
@@ -1267,8 +1323,122 @@ XPCOMUtils.defineLazyGetter(PlacesUIUtil
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "loadBookmarksInBackground",
                                       PREF_LOAD_BOOKMARKS_IN_BACKGROUND, false);
 XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "loadBookmarksInTabs",
                                       PREF_LOAD_BOOKMARKS_IN_TABS, false);
 XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "openInTabClosesMenu",
   "browser.bookmarks.openInTabClosesMenu", false);
+
+/**
+ * Determines if an unwrapped node can be moved.
+ *
+ * @param unwrappedNode
+ *        A node unwrapped by PlacesUtils.unwrapNodes().
+ * @return True if the node can be moved, false otherwise.
+ */
+function canMoveUnwrappedNode(unwrappedNode) {
+  if ((unwrappedNode.concreteGuid && PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) ||
+      unwrappedNode.id <= 0 || PlacesUtils.isRootItem(unwrappedNode.id)) {
+    return false;
+  }
+
+  let parentGuid = unwrappedNode.parentGuid;
+  // If there's no parent Guid, this was likely a virtual query that returns
+  // bookmarks, such as a tags query.
+  if (!parentGuid ||
+      parentGuid == PlacesUtils.bookmarks.rootGuid) {
+    return false;
+  }
+  // leftPaneFolderId and allBookmarksFolderId are lazy getters running
+  // at least a synchronous DB query. Therefore we don't want to invoke
+  // them first, especially because isCommandEnabled may be called way
+  // before the left pane folder is even necessary.
+  if (typeof Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId").get != "function" &&
+      (unwrappedNode.parent == PlacesUIUtils.leftPaneFolderId)) {
+    return false;
+  }
+  return true;
+}
+
+/**
+ * This gets the most appropriate item for using for batching. In the case of multiple
+ * views being related, the method returns the most expensive result to batch.
+ * For example, if it detects the left-hand library pane, then it will look for
+ * and return the reference to the right-hand pane.
+ *
+ * @param {Object} viewOrElement The item to check.
+ * @return {Object} Will return the best result node to batch, or null
+ *                  if one could not be found.
+ */
+function getResultForBatching(viewOrElement) {
+  if (viewOrElement && viewOrElement instanceof Ci.nsIDOMElement &&
+      viewOrElement.id === "placesList") {
+    // Note: fall back to the existing item if we can't find the right-hane pane.
+    viewOrElement = viewOrElement.ownerDocument.getElementById("placeContent") || viewOrElement;
+  }
+
+  if (viewOrElement && viewOrElement.result) {
+    return viewOrElement.result;
+  }
+
+  return null;
+}
+
+/**
+ * Processes a set of transfer items and returns transactions to insert or
+ * move them.
+ *
+ * @param {Array} items A list of unwrapped nodes to get transactions for.
+ * @param {Integer} insertionIndex The requested index for insertion.
+ * @param {String} insertionParentGuid The guid of the parent folder to insert
+ *                                     or move the items to.
+ * @param {Boolean} doCopy Set to true to copy the items, false will move them
+ *                         if possible.
+ * @return {Array} Returns an array of created PlacesTransactions.
+ */
+async function getTransactionsForTransferItems(items, insertionIndex,
+                                               insertionParentGuid, doCopy) {
+  let transactions = [];
+  let index = insertionIndex;
+
+  for (let item of items) {
+    if (index != -1 && item.itemGuid) {
+      // Note: we use the parent from the existing bookmark as the sidebar
+      // gives us an unwrapped.parent that is actually a query and not the real
+      // parent.
+      let existingBookmark = await PlacesUtils.bookmarks.fetch(item.itemGuid);
+
+      // If we're dropping on the same folder, then we may need to adjust
+      // the index to insert at the correct place.
+      if (existingBookmark && insertionParentGuid == existingBookmark.parentGuid) {
+        if (index > existingBookmark.index) {
+          // If we're dragging down, we need to go one lower to insert at
+          // the real point as moving the element changes the index of
+          // everything below by 1.
+          index--;
+        } else if (index == existingBookmark.index) {
+          // This isn't moving so we skip it.
+          continue;
+        }
+      }
+    }
+
+    // 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 && !canMoveUnwrappedNode(item)) {
+      Components.utils.reportError("Tried to move an unmovable Places " +
+                                  "node, reverting to a copy operation.");
+      doCopy = true;
+    }
+    transactions.push(
+      PlacesUIUtils.getTransactionForData(item,
+                                          insertionParentGuid,
+                                          index,
+                                          doCopy));
+
+    if (index != -1 && item.itemGuid) {
+      index++;
+    }
+  }
+  return transactions;
+}
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -212,17 +212,17 @@ PlacesViewBase.prototype = {
           tagName = container.title;
           // TODO (Bug 1160193): properly support dropping on a tag root.
           if (!tagName)
             return null;
         }
       }
     }
 
-    if (PlacesControllerDragHelper.disallowInsertion(container, this))
+    if (this.controller.disallowInsertion(container))
       return null;
 
     return new PlacesInsertionPoint({
       parentId: PlacesUtils.getConcreteItemId(container),
       parentGuid: PlacesUtils.getConcreteItemGuid(container),
       index, orientation, tagName
     });
   },
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -1201,17 +1201,17 @@ PlacesController.prototype = {
       type = type.value;
       items = PlacesUtils.unwrapNodes(data, type);
     } catch (ex) {
       // No supported data exists or nodes unwrap failed, just bail out.
       return;
     }
 
     let doCopy = action == "copy";
-    let itemsToSelect = await handleTransferItems(items, ip, doCopy, this._view);
+    let itemsToSelect = await PlacesUIUtils.handleTransferItems(items, ip, doCopy, this._view);
 
     // Cut/past operations are not repeatable, so clear the clipboard.
     if (action == "cut") {
       this._clearClipboard();
     }
 
     if (itemsToSelect.length > 0)
       this._view.selectItems(itemsToSelect, false);
@@ -1244,17 +1244,51 @@ PlacesController.prototype = {
    * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
    * null otherwise.
    * @param aNode
    *        a places result node.
    * @return the mozILivemarkInfo object for aNode, if set, null otherwise.
    */
   getCachedLivemarkInfo: function PC_getCachedLivemarkInfo(aNode) {
     return this._cachedLivemarkInfoObjects.get(aNode, null);
-  }
+  },
+
+  /**
+   * Checks if we can insert into a container.
+   * @param   container
+   *          The container were we are want to drop
+   */
+  disallowInsertion(container) {
+    NS_ASSERT(container, "empty container");
+    // Allow dropping into Tag containers and editable folders.
+    return !PlacesUtils.nodeIsTagQuery(container) &&
+           (!PlacesUtils.nodeIsFolder(container) ||
+            PlacesUIUtils.isFolderReadOnly(container, this._view));
+  },
+
+  /**
+   * Determines if a node can be moved.
+   *
+   * @param   aNode
+   *          A nsINavHistoryResultNode node.
+   * @return True if the node can be moved, false otherwise.
+   */
+  canMoveNode(node) {
+    // Only bookmark items are movable.
+    if (node.itemId == -1)
+      return false;
+
+    // Once tags and bookmarked are divorced, the tag-query check should be
+    // removed.
+    let parentNode = node.parent;
+    return parentNode != null &&
+           PlacesUtils.nodeIsFolder(parentNode) &&
+           !PlacesUIUtils.isFolderReadOnly(parentNode, this._view) &&
+           !PlacesUtils.nodeIsTagQuery(parentNode);
+  },
 };
 
 /**
  * Handles drag and drop operations for views. Note that this is view agnostic!
  * You should not use PlacesController._view within these methods, since
  * the view that the item(s) have been dropped on was not necessarily active.
  * Drop functions are passed the view that is being dropped on.
  */
@@ -1367,72 +1401,16 @@ var PlacesControllerDragHelper = {
           }
         }
       }
     }
     return true;
   },
 
   /**
-   * Determines if an unwrapped node can be moved.
-   *
-   * @param unwrappedNode
-   *        A node unwrapped by PlacesUtils.unwrapNodes().
-   * @return True if the node can be moved, false otherwise.
-   */
-  canMoveUnwrappedNode(unwrappedNode) {
-    if ((unwrappedNode.concreteGuid && PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) ||
-        unwrappedNode.id <= 0 || PlacesUtils.isRootItem(unwrappedNode.id)) {
-      return false;
-    }
-
-    let parentGuid = unwrappedNode.parentGuid;
-    // If there's no parent Guid, this was likely a virtual query that returns
-    // bookmarks, such as a tags query.
-    if (!parentGuid ||
-        parentGuid == PlacesUtils.bookmarks.rootGuid) {
-      return false;
-    }
-    // leftPaneFolderId and allBookmarksFolderId are lazy getters running
-    // at least a synchronous DB query. Therefore we don't want to invoke
-    // them first, especially because isCommandEnabled may be called way
-    // before the left pane folder is even necessary.
-    if (typeof Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId").get != "function" &&
-        (unwrappedNode.parent == PlacesUIUtils.leftPaneFolderId)) {
-      return false;
-    }
-    return true;
-  },
-
-  /**
-   * Determines if a node can be moved.
-   *
-   * @param   aNode
-   *          A nsINavHistoryResultNode node.
-   * @param   aView
-   *          The view originating the request
-   * @param   [optional] aDOMNode
-   *          A XUL DOM node.
-   * @return True if the node can be moved, false otherwise.
-   */
-  canMoveNode(aNode, aView) {
-    // Only bookmark items are movable.
-    if (aNode.itemId == -1)
-      return false;
-
-    // Once tags and bookmarked are divorced, the tag-query check should be
-    // removed.
-    let parentNode = aNode.parent;
-    return parentNode != null &&
-           PlacesUtils.nodeIsFolder(parentNode) &&
-           !PlacesUIUtils.isFolderReadOnly(parentNode, aView) &&
-           !PlacesUtils.nodeIsTagQuery(parentNode);
-  },
-
-  /**
    * Handles the drop of one or more items onto a view.
    *
    * @param {Object} insertionPoint The insertion point where the items should
    *                                be dropped.
    * @param {Object} dt             The dataTransfer information for the drop.
    * @param {Object} view           The view or the tree element. This allows
    *                                batching to take place.
    */
@@ -1474,170 +1452,15 @@ var PlacesControllerDragHelper = {
           title: data.label,
           type: PlacesUtils.TYPE_X_MOZ_URL
         });
       } else {
         throw new Error("bogus data was passed as a tab");
       }
     }
 
-    await handleTransferItems(nodes, insertionPoint, doCopy, view);
+    await PlacesUIUtils.handleTransferItems(nodes, insertionPoint, doCopy, view);
   },
-
-  /**
-   * Checks if we can insert into a container.
-   * @param   aContainer
-   *          The container were we are want to drop
-   * @param   aView
-   *          The view generating the request
-   */
-  disallowInsertion(aContainer, aView) {
-    NS_ASSERT(aContainer, "empty container");
-    // Allow dropping into Tag containers and editable folders.
-    return !PlacesUtils.nodeIsTagQuery(aContainer) &&
-           (!PlacesUtils.nodeIsFolder(aContainer) ||
-            PlacesUIUtils.isFolderReadOnly(aContainer, aView));
-  }
 };
 
-
 XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
                                    "@mozilla.org/widget/dragservice;1",
                                    "nsIDragService");
-
-/**
- * This gets the most appropriate item for using for batching. In the case of multiple
- * views being related, the method returns the most expensive result to batch.
- * For example, if it detects the left-hand library pane, then it will look for
- * and return the reference to the right-hand pane.
- *
- * @param {Object} viewOrElement The item to check.
- * @return {Object} Will return the best result node to batch, or null
- *                  if one could not be found.
- */
-function getResultForBatching(viewOrElement) {
-  if (viewOrElement && viewOrElement instanceof Ci.nsIDOMElement &&
-      viewOrElement.id === "placesList") {
-    // Note: fall back to the existing item if we can't find the right-hane pane.
-    viewOrElement = document.getElementById("placeContent") || viewOrElement;
-  }
-
-  if (viewOrElement && viewOrElement.result) {
-    return viewOrElement.result;
-  }
-
-  return null;
-}
-
-/**
- * Processes a set of transfer items that have been dropped or pasted.
- * Batching will be applied where necessary.
- *
- * @param {Array} items A list of unwrapped nodes to process.
- * @param {Object} insertionPoint The requested point for insertion.
- * @param {Boolean} doCopy Set to true to copy the items, false will move them
- *                         if possible.
- * @return {Array} Returns an empty array when the insertion point is a tag, else
- *                 returns an array of copied or moved guids.
- */
-async function handleTransferItems(items, insertionPoint, doCopy, view) {
-  let transactions;
-  let itemsCount;
-  if (insertionPoint.isTag) {
-    let urls = items.filter(item => "uri" in item).map(item => item.uri);
-    itemsCount = urls.length;
-    transactions = [PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName })];
-  } else {
-    let insertionIndex = await insertionPoint.getIndex();
-    itemsCount = items.length;
-    transactions = await getTransactionsForTransferItems(
-      items, insertionIndex, insertionPoint.guid, doCopy);
-  }
-
-  // Check if we actually have something to add, if we don't it probably wasn't
-  // valid, or it was moving to the same location, so just ignore it.
-  if (!transactions.length) {
-    return [];
-  }
-
-  let guidsToSelect = [];
-  let resultForBatching = getResultForBatching(view);
-
-  // If we're inserting into a tag, we don't get the guid, so we'll just
-  // pass the transactions direct to the batch function.
-  let batchingItem = transactions;
-  if (!insertionPoint.isTag) {
-    // If we're not a tag, then we need to get the ids of the items to select.
-    batchingItem = async () => {
-      for (let transaction of transactions) {
-        let guid = await transaction.transact();
-        if (guid) {
-          guidsToSelect.push(guid);
-        }
-      }
-    };
-  }
-
-  await PlacesUIUtils.batchUpdatesForNode(resultForBatching, itemsCount, async () => {
-    await PlacesTransactions.batch(batchingItem);
-  });
-
-  return guidsToSelect;
-}
-
-/**
- * Processes a set of transfer items and returns transactions to insert or
- * move them.
- *
- * @param {Array} items A list of unwrapped nodes to get transactions for.
- * @param {Integer} insertionIndex The requested index for insertion.
- * @param {String} insertionParentGuid The guid of the parent folder to insert
- *                                     or move the items to.
- * @param {Boolean} doCopy Set to true to copy the items, false will move them
- *                         if possible.
- * @return {Array} Returns an array of created PlacesTransactions.
- */
-async function getTransactionsForTransferItems(items, insertionIndex,
-                                               insertionParentGuid, doCopy) {
-  let transactions = [];
-  let index = insertionIndex;
-
-  for (let item of items) {
-    if (index != -1 && item.itemGuid) {
-      // Note: we use the parent from the existing bookmark as the sidebar
-      // gives us an unwrapped.parent that is actually a query and not the real
-      // parent.
-      let existingBookmark = await PlacesUtils.bookmarks.fetch(item.itemGuid);
-
-      // If we're dropping on the same folder, then we may need to adjust
-      // the index to insert at the correct place.
-      if (existingBookmark && insertionParentGuid == existingBookmark.parentGuid) {
-        if (index > existingBookmark.index) {
-          // If we're dragging down, we need to go one lower to insert at
-          // the real point as moving the element changes the index of
-          // everything below by 1.
-          index--;
-        } else if (index == existingBookmark.index) {
-          // This isn't moving so we skip it.
-          continue;
-        }
-      }
-    }
-
-    // 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;
-    }
-    transactions.push(
-      PlacesUIUtils.getTransactionForData(item,
-                                          insertionParentGuid,
-                                          index,
-                                          doCopy));
-
-    if (index != -1 && item.itemGuid) {
-      index++;
-    }
-  }
-  return transactions;
-}
--- a/browser/components/places/content/menu.xml
+++ b/browser/components/places/content/menu.xml
@@ -345,17 +345,17 @@
         let elt = event.target;
         if (!elt._placesNode)
           return;
 
         let draggedElt = elt._placesNode;
 
         // Force a copy action if parent node is a query or we are dragging a
         // not-removable node.
-        if (!PlacesControllerDragHelper.canMoveNode(draggedElt, this._rootView))
+        if (!this._rootView.controller.canMoveNode(draggedElt))
           event.dataTransfer.effectAllowed = "copyLink";
 
         // Activate the view and cache the dragged element.
         this._rootView._draggedElt = draggedElt;
         this._rootView.controller.setDataTransfer(event);
         this.setAttribute("dragstart", "true");
         event.stopPropagation();
       ]]></handler>
--- a/browser/components/places/content/tree.xml
+++ b/browser/components/places/content/tree.xml
@@ -502,17 +502,17 @@
               container = lastSelected.parent;
 
               // See comment in the treeView.js's copy of this method
               if (!container || !container.containerOpen)
                 return null;
 
               // Avoid the potentially expensive call to getChildIndex
               // if we know this container doesn't allow insertion
-              if (PlacesControllerDragHelper.disallowInsertion(container, this))
+              if (this.controller.disallowInsertion(container))
                 return null;
 
               var queryOptions = PlacesUtils.asQuery(result.root).queryOptions;
               if (queryOptions.sortingMode !=
                     Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
                 // If we are within a sorted view, insert at the end
                 index = -1;
               } else if (queryOptions.excludeItems ||
@@ -525,17 +525,17 @@
                 dropNearNode = lastSelected;
               } else {
                 var lsi = container.getChildIndex(lastSelected);
                 index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
               }
             }
           }
 
-          if (PlacesControllerDragHelper.disallowInsertion(container, this))
+          if (this.controller.disallowInsertion(container))
             return null;
 
           // TODO (Bug 1160193): properly support dropping on a tag root.
           let tagName = null;
           if (PlacesUtils.nodeIsTagQuery(container)) {
             tagName = container.title;
             if (!tagName)
               return null;
@@ -757,17 +757,17 @@
           if (!node.parent) {
             event.preventDefault();
             event.stopPropagation();
             return;
           }
 
           // If this node is child of a readonly container (e.g. a livemark)
           // or cannot be moved, we must force a copy.
-          if (!PlacesControllerDragHelper.canMoveNode(node, this)) {
+          if (!this.controller.canMoveNode(node)) {
             event.dataTransfer.effectAllowed = "copyLink";
             break;
           }
         }
 
         this._controller.setDataTransfer(event);
         event.stopPropagation();
       ]]></handler>
--- a/browser/components/places/content/treeView.js
+++ b/browser/components/places/content/treeView.js
@@ -1431,17 +1431,17 @@ PlacesTreeView.prototype = {
         // container here.  However, we can simply bail out when this happens,
         // because we would then be back here in less than a millisecond, when
         // the container had been reopened.
         if (!container || !container.containerOpen)
           return null;
 
         // Avoid the potentially expensive call to getChildIndex
         // if we know this container doesn't allow insertion.
-        if (PlacesControllerDragHelper.disallowInsertion(container, this._tree.element))
+        if (this._controller.disallowInsertion(container))
           return null;
 
         let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
         if (queryOptions.sortingMode !=
               Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
           // If we are within a sorted view, insert at the end.
           index = -1;
         } else if (queryOptions.excludeItems ||
@@ -1454,17 +1454,17 @@ PlacesTreeView.prototype = {
           dropNearNode = lastSelected;
         } else {
           let lsi = container.getChildIndex(lastSelected);
           index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
         }
       }
     }
 
-    if (PlacesControllerDragHelper.disallowInsertion(container, this._tree.element))
+    if (this._controller.disallowInsertion(container))
       return null;
 
     // TODO (Bug 1160193): properly support dropping on a tag root.
     let tagName = null;
     if (PlacesUtils.nodeIsTagQuery(container)) {
       tagName = container.title;
       if (!tagName)
         return null;
--- a/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
+++ b/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
@@ -23,32 +23,32 @@ add_task(async function() {
       title: "",
       type: PlacesUtils.bookmarks.TYPE_FOLDER,
     });
     let folderId = await PlacesUtils.promiseItemId(folder.guid);
     tree.selectItems([folder.guid]);
     Assert.equal(tree.selectedNode.bookmarkGuid, folder.guid,
                  "Selected the expected node");
     Assert.equal(tree.selectedNode.type, 6, "node is a folder");
-    Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+    Assert.ok(tree.controller.canMoveNode(tree.selectedNode),
               "can move regular folder node");
 
     info("Test a folder shortcut");
     let shortcut = await PlacesUtils.bookmarks.insert({
       parentGuid: root.guid,
       title: "bar",
       url: `place:folder=${folderId}`
     });
     tree.selectItems([shortcut.guid]);
     Assert.equal(tree.selectedNode.bookmarkGuid, shortcut.guid,
                  "Selected the expected node");
     Assert.equal(tree.selectedNode.type, 9, "node is a folder shortcut");
     Assert.equal(PlacesUtils.getConcreteItemGuid(tree.selectedNode),
                  folder.guid, "shortcut node guid and concrete guid match");
-    Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+    Assert.ok(tree.controller.canMoveNode(tree.selectedNode),
               "can move folder shortcut node");
 
     info("Test a query");
     let bookmark = await PlacesUtils.bookmarks.insert({
       parentGuid: root.guid,
       title: "",
       url: "http://foo.com",
     });
@@ -58,53 +58,53 @@ add_task(async function() {
     let query = await PlacesUtils.bookmarks.insert({
       parentGuid: root.guid,
       title: "bar",
       url: `place:terms=foo`
     });
     tree.selectItems([query.guid]);
     Assert.equal(tree.selectedNode.bookmarkGuid, query.guid,
                  "Selected the expected node");
-    Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+    Assert.ok(tree.controller.canMoveNode(tree.selectedNode),
               "can move query node");
 
 
     info("Test a tag container");
     PlacesUtils.tagging.tagURI(Services.io.newURI(bookmark.url.href), ["bar"]);
     // Add the tags root query.
     let tagsQuery = await PlacesUtils.bookmarks.insert({
       parentGuid: root.guid,
       title: "",
       url: "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY,
     });
     tree.selectItems([tagsQuery.guid]);
     PlacesUtils.asQuery(tree.selectedNode).containerOpen = true;
     Assert.equal(tree.selectedNode.childCount, 1, "has tags");
     let tagNode = tree.selectedNode.getChild(0);
-    Assert.ok(!PlacesControllerDragHelper.canMoveNode(tagNode, tree),
+    Assert.ok(!tree.controller.canMoveNode(tagNode),
               "should not be able to move tag container node");
     tree.selectedNode.containerOpen = false;
 
     info("Test that special folders and cannot be moved but other shortcuts can.");
     let roots = [
       PlacesUtils.bookmarks.menuGuid,
       PlacesUtils.bookmarks.unfiledGuid,
       PlacesUtils.bookmarks.toolbarGuid,
     ];
 
     for (let guid of roots) {
       tree.selectItems([guid]);
-      Assert.ok(!PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+      Assert.ok(!tree.controller.canMoveNode(tree.selectedNode),
                 "shouldn't be able to move default shortcuts to roots");
       let id = await PlacesUtils.promiseItemId(guid);
       let s = await PlacesUtils.bookmarks.insert({
         parentGuid: root.guid,
         title: "bar",
         url: `place:folder=${id}`,
       });
       tree.selectItems([s.guid]);
       Assert.equal(tree.selectedNode.bookmarkGuid, s.guid,
                    "Selected the expected node");
-      Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+      Assert.ok(tree.controller.canMoveNode(tree.selectedNode),
                 "should be able to move user-created shortcuts to roots");
     }
   });
 });