forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1714 lines
		
	
	
	
		
			54 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1714 lines
		
	
	
	
		
			54 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- 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.defineESModuleGetters(this, {
 | |
|   PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs",
 | |
|   PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
 | |
|   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
 | |
| });
 | |
| 
 | |
| /* import-globals-from /browser/base/content/utilityOverlay.js */
 | |
| /* import-globals-from ./places.js */
 | |
| 
 | |
| /**
 | |
|  * Represents an insertion point within a container where we can insert
 | |
|  * items.
 | |
|  *
 | |
|  * @param {object} options an object containing the following properties:
 | |
|  * @param {string} options.parentGuid
 | |
|  *     The unique identifier of the parent container
 | |
|  * @param {number} [options.index]
 | |
|  *     The index within the container where to insert, defaults to appending
 | |
|  * @param {number} [options.orientation]
 | |
|  *     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! Defaults to DROP_ON.
 | |
|  * @param {string} [options.tagName]
 | |
|  *     The tag name if this IP is set to a tag, null otherwise.
 | |
|  * @param {*} [options.dropNearNode]
 | |
|  *     When defined index will be calculated based on this node
 | |
|  */
 | |
| function PlacesInsertionPoint({
 | |
|   parentGuid,
 | |
|   index = PlacesUtils.bookmarks.DEFAULT_INDEX,
 | |
|   orientation = Ci.nsITreeView.DROP_ON,
 | |
|   tagName = null,
 | |
|   dropNearNode = null,
 | |
| }) {
 | |
|   this.guid = parentGuid;
 | |
|   this._index = index;
 | |
|   this.orientation = orientation;
 | |
|   this.tagName = tagName;
 | |
|   this.dropNearNode = dropNearNode;
 | |
| }
 | |
| 
 | |
| PlacesInsertionPoint.prototype = {
 | |
|   set index(val) {
 | |
|     this._index = val;
 | |
|   },
 | |
| 
 | |
|   async getIndex() {
 | |
|     if (this.dropNearNode) {
 | |
|       // If dropNearNode is set up we must calculate the index of the item near
 | |
|       // which we will drop.
 | |
|       let index = (
 | |
|         await PlacesUtils.bookmarks.fetch(this.dropNearNode.bookmarkGuid)
 | |
|       ).index;
 | |
|       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;
 | |
|   ChromeUtils.defineLazyGetter(this, "profileName", function () {
 | |
|     return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
 | |
|   });
 | |
| 
 | |
|   XPCOMUtils.defineLazyPreferenceGetter(
 | |
|     this,
 | |
|     "forgetSiteClearByBaseDomain",
 | |
|     "places.forgetThisSite.clearByBaseDomain",
 | |
|     false
 | |
|   );
 | |
|   ChromeUtils.defineESModuleGetters(this, {
 | |
|     ForgetAboutSite: "resource://gre/modules/ForgetAboutSite.sys.mjs",
 | |
|   });
 | |
| }
 | |
| 
 | |
| PlacesController.prototype = {
 | |
|   /**
 | |
|    * The places view.
 | |
|    */
 | |
|   _view: null,
 | |
| 
 | |
|   // This is used in certain views to disable user actions on the places tree
 | |
|   // views. This avoids accidental deletion/modification when the user is not
 | |
|   // actually organising the trees.
 | |
|   disableUserActions: false,
 | |
| 
 | |
|   QueryInterface: ChromeUtils.generateQI(["nsIClipboardOwner"]),
 | |
| 
 | |
|   // nsIClipboardOwner
 | |
|   LosingOwnership: function PC_LosingOwnership(aXferable) {
 | |
|     this.cutNodes = [];
 | |
|   },
 | |
| 
 | |
|   terminate: function PC_terminate() {
 | |
|     this._releaseClipboardOwnership();
 | |
|   },
 | |
| 
 | |
|   supportsCommand: function PC_supportsCommand(aCommand) {
 | |
|     if (this.disableUserActions) {
 | |
|       return false;
 | |
|     }
 | |
|     // 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":
 | |
|       case "cmd_selectAll":
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     // All other Places Commands are prefixed with "placesCmd_" ... this
 | |
|     // 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) {
 | |
|     // Determine whether or not nodes can be inserted.
 | |
|     let ip = this._view.insertionPoint;
 | |
|     let canInsert = ip && (aCommand.endsWith("_paste") || !ip.isTag);
 | |
| 
 | |
|     switch (aCommand) {
 | |
|       case "cmd_undo":
 | |
|         return PlacesTransactions.topUndoEntry != null;
 | |
|       case "cmd_redo":
 | |
|         return PlacesTransactions.topRedoEntry != null;
 | |
|       case "cmd_cut":
 | |
|       case "placesCmd_cut":
 | |
|         for (let node of this._view.selectedNodes) {
 | |
|           // 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":
 | |
|       case "placesCmd_deleteDataHost":
 | |
|         return this._hasRemovableSelection();
 | |
|       case "cmd_copy":
 | |
|       case "placesCmd_copy":
 | |
|       case "placesCmd_showInFolder":
 | |
|         return this._view.hasSelection;
 | |
|       case "cmd_paste":
 | |
|       case "placesCmd_paste":
 | |
|         // If the clipboard contains a Places flavor it is definitely pasteable,
 | |
|         // otherwise we also allow pasting "text/plain" and "text/x-moz-url" data.
 | |
|         // We don't check if the data is valid here, because the clipboard may
 | |
|         // contain very large blobs that would largely slowdown commands updating.
 | |
|         // Of course later paste() should ignore any invalid data.
 | |
|         return (
 | |
|           canInsert &&
 | |
|           Services.clipboard.hasDataMatchingFlavors(
 | |
|             [
 | |
|               ...PlacesUIUtils.PLACES_FLAVORS,
 | |
|               PlacesUtils.TYPE_X_MOZ_URL,
 | |
|               PlacesUtils.TYPE_PLAINTEXT,
 | |
|             ],
 | |
|             Ci.nsIClipboard.kGlobalClipboard
 | |
|           )
 | |
|         );
 | |
|       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:window":
 | |
|       case "placesCmd_open:privatewindow":
 | |
|       case "placesCmd_open:tab": {
 | |
|         let selectedNode = this._view.selectedNode;
 | |
|         return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
 | |
|       }
 | |
|       case "placesCmd_new:folder":
 | |
|         return canInsert;
 | |
|       case "placesCmd_new:bookmark":
 | |
|         return canInsert;
 | |
|       case "placesCmd_new:separator":
 | |
|         return (
 | |
|           canInsert &&
 | |
|           !PlacesUtils.asQuery(this._view.result.root).queryOptions
 | |
|             .excludeItems &&
 | |
|           this._view.result.sortingMode ==
 | |
|             Ci.nsINavHistoryQueryOptions.SORT_BY_NONE
 | |
|         );
 | |
|       case "placesCmd_show:info": {
 | |
|         let selectedNode = this._view.selectedNode;
 | |
|         return (
 | |
|           selectedNode &&
 | |
|           !PlacesUtils.isRootItem(
 | |
|             PlacesUtils.getConcreteItemGuid(selectedNode)
 | |
|           ) &&
 | |
|           (PlacesUtils.nodeIsTagQuery(selectedNode) ||
 | |
|             PlacesUtils.nodeIsBookmark(selectedNode) ||
 | |
|             (PlacesUtils.nodeIsFolder(selectedNode) &&
 | |
|               !PlacesUtils.isQueryGeneratedFolder(selectedNode)))
 | |
|         );
 | |
|       }
 | |
|       case "placesCmd_sortBy:name": {
 | |
|         let selectedNode = this._view.selectedNode;
 | |
|         return (
 | |
|           selectedNode &&
 | |
|           PlacesUtils.nodeIsFolder(selectedNode) &&
 | |
|           !PlacesUIUtils.isFolderReadOnly(selectedNode) &&
 | |
|           this._view.result.sortingMode ==
 | |
|             Ci.nsINavHistoryQueryOptions.SORT_BY_NONE
 | |
|         );
 | |
|       }
 | |
|       case "placesCmd_createBookmark": {
 | |
|         return !this._view.selectedNodes.some(
 | |
|           node => !PlacesUtils.nodeIsURI(node) || node.itemId != -1
 | |
|         );
 | |
|       }
 | |
|       default:
 | |
|         return false;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   doCommand: function PC_doCommand(aCommand) {
 | |
|     switch (aCommand) {
 | |
|       case "cmd_undo":
 | |
|         PlacesTransactions.undo().catch(console.error);
 | |
|         break;
 | |
|       case "cmd_redo":
 | |
|         PlacesTransactions.redo().catch(console.error);
 | |
|         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().catch(console.error);
 | |
|         break;
 | |
|       case "cmd_delete":
 | |
|       case "placesCmd_delete":
 | |
|         this.remove("Remove Selection").catch(console.error);
 | |
|         break;
 | |
|       case "placesCmd_deleteDataHost":
 | |
|         this.forgetAboutThisSite().catch(console.error);
 | |
|         break;
 | |
|       case "cmd_selectAll":
 | |
|         this.selectAll();
 | |
|         break;
 | |
|       case "placesCmd_open":
 | |
|         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", this._view);
 | |
|         break;
 | |
|       case "placesCmd_new:folder":
 | |
|         this.newItem("folder").catch(console.error);
 | |
|         break;
 | |
|       case "placesCmd_new:bookmark":
 | |
|         this.newItem("bookmark").catch(console.error);
 | |
|         break;
 | |
|       case "placesCmd_new:separator":
 | |
|         this.newSeparator().catch(console.error);
 | |
|         break;
 | |
|       case "placesCmd_show:info":
 | |
|         this.showBookmarkPropertiesForSelection();
 | |
|         break;
 | |
|       case "placesCmd_sortBy:name":
 | |
|         this.sortFolderByName().catch(console.error);
 | |
|         break;
 | |
|       case "placesCmd_createBookmark": {
 | |
|         const nodes = this._view.selectedNodes.map(node => {
 | |
|           return {
 | |
|             uri: Services.io.newURI(node.uri),
 | |
|             title: node.title,
 | |
|           };
 | |
|         });
 | |
|         PlacesUIUtils.showBookmarkPagesDialog(
 | |
|           nodes,
 | |
|           ["keyword", "location"],
 | |
|           window.top
 | |
|         );
 | |
|         break;
 | |
|       }
 | |
|       case "placesCmd_showInFolder":
 | |
|         this.showInFolder(this._view.selectedNode.bookmarkGuid);
 | |
|         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 {boolean} 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;
 | |
| 
 | |
|     for (var j = 0; j < ranges.length; j++) {
 | |
|       var nodes = ranges[j];
 | |
|       for (var i = 0; i < nodes.length; ++i) {
 | |
|         // Disallow removing the view's root node
 | |
|         if (nodes[i] == root) {
 | |
|           return false;
 | |
|         }
 | |
| 
 | |
|         if (!PlacesUIUtils.canUserRemove(nodes[i])) {
 | |
|           return false;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gathers information about the selected nodes according to the following
 | |
|    * rules:
 | |
|    *    "link"              node is a URI
 | |
|    *    "bookmark"          node is a bookmark
 | |
|    *    "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 {Array} 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!
 | |
|    */
 | |
|   _buildSelectionMetadata() {
 | |
|     return this._view.selectedNodes.map(n => this._selectionMetadataForNode(n));
 | |
|   },
 | |
| 
 | |
|   _selectionMetadataForNode(node) {
 | |
|     let nodeData = {};
 | |
|     // We don't use the nodeIs* methods here to avoid going through the type
 | |
|     // property way too often
 | |
|     switch (node.type) {
 | |
|       case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
 | |
|         nodeData.query = true;
 | |
|         if (node.parent) {
 | |
|           switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
 | |
|             case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
 | |
|               nodeData.query_host = true;
 | |
|               break;
 | |
|             case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
 | |
|             case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
 | |
|               nodeData.query_day = true;
 | |
|               break;
 | |
|             case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT:
 | |
|               nodeData.query_tag = true;
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
|       case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
 | |
|       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;
 | |
|         if (PlacesUtils.nodeIsBookmark(node)) {
 | |
|           nodeData.link_bookmark = true;
 | |
|           var parentNode = node.parent;
 | |
|           if (parentNode && PlacesUtils.nodeIsTagQuery(parentNode)) {
 | |
|             nodeData.link_bookmark_tag = true;
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|     return nodeData;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determines if a context-menu item should be shown
 | |
|    *
 | |
|    * @param {object} aMenuItem
 | |
|    *        the context menu item
 | |
|    * @param {object} aMetaData
 | |
|    *        meta data about the selection
 | |
|    * @returns {boolean} true if the conditions (see buildContextMenu) are satisfied
 | |
|    *          and the item can be displayed, false otherwise.
 | |
|    */
 | |
|   _shouldShowMenuItem(aMenuItem, aMetaData) {
 | |
|     if (
 | |
|       aMenuItem.hasAttribute("hide-if-private-browsing") &&
 | |
|       !PrivateBrowsingUtils.enabled
 | |
|     ) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       aMenuItem.hasAttribute("hide-if-usercontext-disabled") &&
 | |
|       !Services.prefs.getBoolPref("privacy.userContext.enabled", false)
 | |
|     ) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     let selectiontype =
 | |
|       aMenuItem.getAttribute("selection-type") || "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 (count == 1 && !selectionTypes.includes("single")) {
 | |
|       return false;
 | |
|     }
 | |
|     // If there is no selection and selectionType doesn't include `none`
 | |
|     // hide the item, otherwise try to use the root node to extract valid
 | |
|     // metadata to compare against.
 | |
|     if (count == 0) {
 | |
|       if (!selectionTypes.includes("none")) {
 | |
|         return false;
 | |
|       }
 | |
|       aMetaData = [this._selectionMetadataForNode(this._view.result.root)];
 | |
|     }
 | |
| 
 | |
|     let attr = aMenuItem.getAttribute("hide-if-node-type");
 | |
|     if (attr) {
 | |
|       let rules = attr.split("|");
 | |
|       if (aMetaData.some(d => rules.some(r => r in d))) {
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     attr = aMenuItem.getAttribute("hide-if-node-type-is-only");
 | |
|     if (attr) {
 | |
|       let rules = attr.split("|");
 | |
|       if (rules.some(r => aMetaData.every(d => r in d))) {
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     attr = aMenuItem.getAttribute("node-type");
 | |
|     if (!attr) {
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     let anyMatched = false;
 | |
|     let rules = attr.split("|");
 | |
|     for (let metaData of aMetaData) {
 | |
|       if (rules.some(r => r in metaData)) {
 | |
|         anyMatched = true;
 | |
|       } else {
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
|     return anyMatched;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Uses meta-data rules set as attributes on the menuitems, representing the
 | |
|    * current selection in the view (see `_buildSelectionMetadata`) and sets the
 | |
|    * visibility state for each menuitem according to the following rules:
 | |
|    *  1) The visibility state is unchanged if none of the attributes are set.
 | |
|    *  2) Attributes should not be set on menuseparators.
 | |
|    *  3) The boolean `ignore-item` attribute may be set when this code should
 | |
|    *     not handle that menuitem.
 | |
|    *  4) The `selection-type` attribute may be set to:
 | |
|    *      - `single` if it should be visible only when there is a single node
 | |
|    *         selected
 | |
|    *      - `multiple` if it should be visible only when multiple nodes are
 | |
|    *         selected
 | |
|    *      - `none` if it should be visible when there are no selected nodes
 | |
|    *      - `any` if it should be visible for any kind of selection
 | |
|    *      - a `|` separated combination of the above.
 | |
|    *  5) The `node-type` attribute may be set to values representing the
 | |
|    *     type of the node triggering the context menu. The menuitem will be
 | |
|    *     visible when one of the rules (separated by `|`) matches.
 | |
|    *     In case of multiple selection, the menuitem is visible only if all of
 | |
|    *     the selected nodes match one of the rule.
 | |
|    *  6) The `hide-if-node-type` accepts the same rules as `node-type`, but
 | |
|    *     hides the menuitem if the nodes match at least one of the rules.
 | |
|    *     It takes priority over `nodetype`.
 | |
|    *  7) The `hide-if-node-type-is-only` accepts the same rules as `node-type`, but
 | |
|    *     hides the menuitem if any of the rules match all of the nodes.
 | |
|    *  8) The boolean `hide-if-no-insertion-point` attribute may be set to hide a
 | |
|    *     menuitem when there's no insertion point. An insertion point represents
 | |
|    *     a point in the view where a new item can be inserted.
 | |
|    *  9) The boolean `hide-if-private-browsing` attribute may be set to hide a
 | |
|    *     menuitem in private browsing mode
 | |
|    * 10) The boolean `hide-if-single-click-opens` attribute may be set to hide a
 | |
|    *     menuitem in views opening entries with a single click.
 | |
|    *
 | |
|    * @param {object} aPopup
 | |
|    *        The menupopup to build children into.
 | |
|    * @returns {boolean} true if at least one item is visible, false otherwise.
 | |
|    */
 | |
|   buildContextMenu(aPopup) {
 | |
|     var metadata = this._buildSelectionMetadata();
 | |
|     var ip = this._view.insertionPoint;
 | |
|     var noIp = !ip || ip.isTag;
 | |
| 
 | |
|     var separator = null;
 | |
|     var visibleItemsBeforeSep = false;
 | |
|     var usableItemCount = 0;
 | |
|     for (var i = 0; i < aPopup.children.length; ++i) {
 | |
|       var item = aPopup.children[i];
 | |
|       if (item.getAttribute("ignore-item") == "true") {
 | |
|         continue;
 | |
|       }
 | |
|       if (item.localName != "menuseparator") {
 | |
|         // We allow pasting into tag containers, so special case that.
 | |
|         let hideIfNoIP =
 | |
|           item.getAttribute("hide-if-no-insertion-point") == "true" &&
 | |
|           noIp &&
 | |
|           !(ip && ip.isTag && item.id == "placesContext_paste");
 | |
|         let hideIfPrivate =
 | |
|           item.getAttribute("hide-if-private-browsing") == "true" &&
 | |
|           PrivateBrowsingUtils.isWindowPrivate(window);
 | |
|         // Hide `Open` if the primary action on click is opening.
 | |
|         let hideIfSingleClickOpens =
 | |
|           item.getAttribute("hide-if-single-click-opens") == "true" &&
 | |
|           !PlacesUIUtils.loadBookmarksInBackground &&
 | |
|           !PlacesUIUtils.loadBookmarksInTabs &&
 | |
|           this._view.singleClickOpens;
 | |
|         let hideIfNotSearch =
 | |
|           item.getAttribute("hide-if-not-search") == "true" &&
 | |
|           (!this._view.selectedNode ||
 | |
|             !this._view.selectedNode.parent ||
 | |
|             !PlacesUtils.nodeIsQuery(this._view.selectedNode.parent));
 | |
| 
 | |
|         let shouldHideItem =
 | |
|           hideIfNoIP ||
 | |
|           hideIfPrivate ||
 | |
|           hideIfSingleClickOpens ||
 | |
|           hideIfNotSearch ||
 | |
|           !this._shouldShowMenuItem(item, metadata);
 | |
|         item.hidden = shouldHideItem;
 | |
|         item.disabled =
 | |
|           shouldHideItem || item.getAttribute("start-disabled") == "true";
 | |
| 
 | |
|         if (!item.hidden) {
 | |
|           visibleItemsBeforeSep = true;
 | |
|           usableItemCount++;
 | |
| 
 | |
|           // Show the separator above the menu-item if any
 | |
|           if (separator) {
 | |
|             separator.hidden = false;
 | |
|             separator = null;
 | |
|           }
 | |
|         }
 | |
|       } 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;
 | |
|       }
 | |
| 
 | |
|       if (item.id === "placesContext_deleteBookmark") {
 | |
|         document.l10n.setAttributes(item, "places-delete-bookmark", {
 | |
|           count: metadata.length,
 | |
|         });
 | |
|       }
 | |
|       if (item.id === "placesContext_deleteFolder") {
 | |
|         document.l10n.setAttributes(item, "places-delete-folder", {
 | |
|           count: metadata.length,
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Set Open Folder/Links In Tabs or Open Bookmark item's enabled state if they're visible
 | |
|     if (usableItemCount > 0) {
 | |
|       let openContainerInTabsItem = document.getElementById(
 | |
|         "placesContext_openContainer:tabs"
 | |
|       );
 | |
|       let openBookmarksItem = document.getElementById(
 | |
|         "placesContext_openBookmarkContainer:tabs"
 | |
|       );
 | |
|       for (let menuItem of [openContainerInTabsItem, openBookmarksItem]) {
 | |
|         if (!menuItem.hidden) {
 | |
|           var containerToUse =
 | |
|             this._view.selectedNode || this._view.result.root;
 | |
|           if (PlacesUtils.nodeIsContainer(containerToUse)) {
 | |
|             if (!PlacesUtils.hasChildURIs(containerToUse)) {
 | |
|               menuItem.disabled = true;
 | |
|               // Ensure that we don't display the menu if nothing is enabled:
 | |
|               usableItemCount--;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const deleteHistoryItem = document.getElementById(
 | |
|       "placesContext_delete_history"
 | |
|     );
 | |
|     document.l10n.setAttributes(deleteHistoryItem, "places-delete-page", {
 | |
|       count: metadata.length,
 | |
|     });
 | |
| 
 | |
|     const createBookmarkItem = document.getElementById(
 | |
|       "placesContext_createBookmark"
 | |
|     );
 | |
|     document.l10n.setAttributes(createBookmarkItem, "places-create-bookmark", {
 | |
|       count: metadata.length,
 | |
|     });
 | |
| 
 | |
|     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() {
 | |
|     let node = this._view.selectedNode;
 | |
|     if (!node) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     PlacesUIUtils.showBookmarkDialog(
 | |
|       { action: "edit", node, hiddenRows: ["folderPicker"] },
 | |
|       window.top
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens the links in the selected folder, or the selected links in new tabs.
 | |
|    *
 | |
|    * @param {object} aEvent
 | |
|    *   The associated event.
 | |
|    */
 | |
|   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;
 | |
|     }
 | |
|     PlacesUIUtils.openMultipleLinksInTabs(
 | |
|       node ? node : nodes,
 | |
|       aEvent,
 | |
|       this._view
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Shows the Add Bookmark UI for the current insertion point.
 | |
|    *
 | |
|    * @param {string} aType
 | |
|    *        the type of the new item (bookmark/folder)
 | |
|    */
 | |
|   async newItem(aType) {
 | |
|     let ip = this._view.insertionPoint;
 | |
|     if (!ip) {
 | |
|       throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
 | |
|     }
 | |
| 
 | |
|     let bookmarkGuid = await PlacesUIUtils.showBookmarkDialog(
 | |
|       {
 | |
|         action: "add",
 | |
|         type: aType,
 | |
|         defaultInsertionPoint: ip,
 | |
|         hiddenRows: ["folderPicker"],
 | |
|       },
 | |
|       window.top
 | |
|     );
 | |
|     if (bookmarkGuid) {
 | |
|       this._view.selectItems([bookmarkGuid], false);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create a new Bookmark separator somewhere.
 | |
|    */
 | |
|   async newSeparator() {
 | |
|     var ip = this._view.insertionPoint;
 | |
|     if (!ip) {
 | |
|       throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
 | |
|     }
 | |
| 
 | |
|     let index = await ip.getIndex();
 | |
|     let txn = PlacesTransactions.NewSeparator({ parentGuid: ip.guid, index });
 | |
|     let guid = await txn.transact();
 | |
|     // Select the new item.
 | |
|     this._view.selectItems([guid], false);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sort the selected folder by name
 | |
|    */
 | |
|   async sortFolderByName() {
 | |
|     let guid = PlacesUtils.getConcreteItemGuid(this._view.selectedNode);
 | |
|     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 {object} node
 | |
|    *        Node to check for containment.
 | |
|    * @param {Array} pastFolders
 | |
|    *        List of folders the calling function has already traversed
 | |
|    * @returns {boolean} 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 {object} parent
 | |
|      *        The parent container to check for containment in
 | |
|      * @returns {boolean} true if node is a member of parent's children, false otherwise.
 | |
|      */
 | |
|     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 (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 {Array} range
 | |
|    *          An array of nodes to remove. Should all be adjacent.
 | |
|    * @param {Array} transactions
 | |
|    *          An array of transactions (returned)
 | |
|    * @param  {Array} [removedFolders]
 | |
|    *          An array of folder nodes that have already been removed.
 | |
|    * @returns {number} The total number of items affected.
 | |
|    */
 | |
|   async _removeRange(range, transactions, removedFolders) {
 | |
|     if (!(transactions instanceof Array)) {
 | |
|       throw new Error("Must pass a transactions array");
 | |
|     }
 | |
|     if (!removedFolders) {
 | |
|       removedFolders = [];
 | |
|     }
 | |
| 
 | |
|     let bmGuidsToRemove = [];
 | |
|     let totalItems = 0;
 | |
| 
 | |
|     for (var i = 0; i < range.length; ++i) {
 | |
|       var node = range[i];
 | |
|       if (this._shouldSkipNode(node, removedFolders)) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       totalItems++;
 | |
| 
 | |
|       if (PlacesUtils.nodeIsTagQuery(node.parent)) {
 | |
|         // This is a uri node inside a tag container.  It needs a special
 | |
|         // untag transaction.
 | |
|         let tag = node.parent.title || "";
 | |
|         if (!tag) {
 | |
|           // The parent may be the root node, that doesn't have a title.
 | |
|           tag = node.parent.query.tags[0];
 | |
|         }
 | |
|         transactions.push(PlacesTransactions.Untag({ urls: [node.uri], tag }));
 | |
|       } else if (
 | |
|         PlacesUtils.nodeIsTagQuery(node) &&
 | |
|         node.parent &&
 | |
|         PlacesUtils.nodeIsQuery(node.parent) &&
 | |
|         PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
 | |
|           Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT
 | |
|       ) {
 | |
|         // 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.
 | |
|         let tag = node.title;
 | |
|         let urls = new Set();
 | |
|         await PlacesUtils.bookmarks.fetch({ tags: [tag] }, b =>
 | |
|           urls.add(b.url)
 | |
|         );
 | |
|         transactions.push(
 | |
|           PlacesTransactions.Untag({ tag, urls: Array.from(urls) })
 | |
|         );
 | |
|       } 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.
 | |
|         await PlacesUtils.history.remove(node.uri).catch(console.error);
 | |
|         // History deletes are not undoable, so we don't have a transaction.
 | |
|       } 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.
 | |
|         await this._removeHistoryContainer(node).catch(console.error);
 | |
|         // History deletes are not undoable, so we don't have a transaction.
 | |
|       } 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);
 | |
|         }
 | |
|         bmGuidsToRemove.push(node.bookmarkGuid);
 | |
|       }
 | |
|     }
 | |
|     if (bmGuidsToRemove.length) {
 | |
|       transactions.push(PlacesTransactions.Remove({ guids: bmGuidsToRemove }));
 | |
|     }
 | |
|     return totalItems;
 | |
|   },
 | |
| 
 | |
|   async _removeRowsFromBookmarks() {
 | |
|     let ranges = this._view.removableSelectionRanges;
 | |
|     let transactions = [];
 | |
|     let removedFolders = [];
 | |
|     let totalItems = 0;
 | |
| 
 | |
|     for (let range of ranges) {
 | |
|       totalItems += await this._removeRange(
 | |
|         range,
 | |
|         transactions,
 | |
|         removedFolders
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (transactions.length) {
 | |
|       await PlacesUIUtils.batchUpdatesForNode(
 | |
|         this._view.result,
 | |
|         totalItems,
 | |
|         async () => {
 | |
|           await PlacesTransactions.batch(
 | |
|             transactions,
 | |
|             "PlacesController::removeRowsFromBookmarks"
 | |
|           );
 | |
|         }
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes the set of selected ranges from history, asynchronously. History
 | |
|    * deletes are not undoable.
 | |
|    */
 | |
|   async _removeRowsFromHistory() {
 | |
|     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)) {
 | |
|         URIs.add(node.uri);
 | |
|       } else if (
 | |
|         PlacesUtils.nodeIsQuery(node) &&
 | |
|         PlacesUtils.asQuery(node).queryOptions.queryType ==
 | |
|           Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
 | |
|       ) {
 | |
|         await this._removeHistoryContainer(node).catch(console.error);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (URIs.size) {
 | |
|       await PlacesUIUtils.batchUpdatesForNode(
 | |
|         this._view.result,
 | |
|         URIs.size,
 | |
|         async () => {
 | |
|           await PlacesUtils.history.remove([...URIs]);
 | |
|         }
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes history visits for an history container node. History deletes are
 | |
|    * not undoable.
 | |
|    *
 | |
|    * @param {object} aContainerNode
 | |
|    *        The container node to remove.
 | |
|    */
 | |
|   async _removeHistoryContainer(aContainerNode) {
 | |
|     if (PlacesUtils.nodeIsHost(aContainerNode)) {
 | |
|       // This is a site container.
 | |
|       // Check if it's the container for local files (don't be fooled by the
 | |
|       // bogus string name, this is "(local files)").
 | |
|       let host =
 | |
|         "." +
 | |
|         (aContainerNode.title == PlacesUtils.getString("localhost")
 | |
|           ? ""
 | |
|           : aContainerNode.title);
 | |
|       // Will update faster if all children hidden before removing
 | |
|       aContainerNode.containerOpen = false;
 | |
|       await PlacesUtils.history.removeByFilter({ host });
 | |
|     } else if (PlacesUtils.nodeIsDay(aContainerNode)) {
 | |
|       // This is a day container.
 | |
|       let query = aContainerNode.query;
 | |
|       let beginTime = query.beginTime;
 | |
|       let endTime = query.endTime;
 | |
|       if (!query || !beginTime || !endTime) {
 | |
|         throw new Error("A valid date container query should exist!");
 | |
|       }
 | |
|       // Will update faster if all children hidden before removing
 | |
|       aContainerNode.containerOpen = false;
 | |
|       // 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.
 | |
|       await PlacesUtils.history.removeByFilter({
 | |
|         beginDate: PlacesUtils.toDate(beginTime + 1000),
 | |
|         endDate: PlacesUtils.toDate(endTime),
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes the selection
 | |
|    */
 | |
|   async remove() {
 | |
|     if (!this._hasRemovableSelection()) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var root = this._view.result.root;
 | |
| 
 | |
|     if (PlacesUtils.nodeIsFolder(root)) {
 | |
|       await this._removeRowsFromBookmarks();
 | |
|     } else if (PlacesUtils.nodeIsQuery(root)) {
 | |
|       var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
 | |
|       if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) {
 | |
|         await this._removeRowsFromBookmarks();
 | |
|       } else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
 | |
|         await this._removeRowsFromHistory();
 | |
|       } else {
 | |
|         throw new Error("Unknown query type");
 | |
|       }
 | |
|     } else {
 | |
|       throw new Error("unexpected root");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Fills a DataTransfer object with the content of the selection that can be
 | |
|    * dropped elsewhere.
 | |
|    *
 | |
|    * @param {object} aEvent
 | |
|    *        The dragstart event.
 | |
|    */
 | |
|   setDataTransfer: function PC_setDataTransfer(aEvent) {
 | |
|     let dt = aEvent.dataTransfer;
 | |
| 
 | |
|     let result = this._view.result;
 | |
|     let didSuppressNotifications = result.suppressNotifications;
 | |
|     if (!didSuppressNotifications) {
 | |
|       result.suppressNotifications = true;
 | |
|     }
 | |
| 
 | |
|     function addData(type, index) {
 | |
|       let wrapNode = PlacesUtils.wrapNode(node, type);
 | |
|       dt.mozSetDataAt(type, wrapNode, index);
 | |
|     }
 | |
| 
 | |
|     function addURIData(index) {
 | |
|       addData(PlacesUtils.TYPE_X_MOZ_URL, index);
 | |
|       addData(PlacesUtils.TYPE_PLAINTEXT, index);
 | |
|       addData(PlacesUtils.TYPE_HTML, index);
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       let nodes = this._view.draggableSelection;
 | |
|       for (let i = 0; i < nodes.length; ++i) {
 | |
|         var node = nodes[i];
 | |
| 
 | |
|         // 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);
 | |
|         if (node.uri) {
 | |
|           addURIData(i);
 | |
|         }
 | |
|       }
 | |
|     } 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);
 | |
|       Services.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) {
 | |
|       // This clears the logical clipboard, doesn't remove data.
 | |
|       Services.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(""));
 | |
|     Services.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_PLAINTEXT, 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);
 | |
|       }
 | |
| 
 | |
|       contents.forEach(function (content) {
 | |
|         content.entries.push(PlacesUtils.wrapNode(node, content.type));
 | |
|       });
 | |
|     }, this);
 | |
| 
 | |
|     function addData(type, data) {
 | |
|       xferable.addDataFlavor(type);
 | |
|       xferable.setTransferData(type, PlacesUtils.toISupportsString(data));
 | |
|     }
 | |
| 
 | |
|     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) {
 | |
|         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) {
 | |
|       Services.clipboard.setData(
 | |
|         xferable,
 | |
|         aAction == "cut" ? 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);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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 {
 | |
|       this._populateClipboard(this._view.selectedNodes, "copy");
 | |
|     } finally {
 | |
|       if (!didSuppressNotifications) {
 | |
|         result.suppressNotifications = false;
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Cut Bookmarks and Folders to the clipboard
 | |
|    */
 | |
|   cut: function PC_cut() {
 | |
|     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
 | |
|    */
 | |
|   async paste() {
 | |
|     // No reason to proceed if there isn't a valid insertion point.
 | |
|     let ip = this._view.insertionPoint;
 | |
|     if (!ip) {
 | |
|       throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
 | |
|     }
 | |
| 
 | |
|     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_PLAINTEXT,
 | |
|     ].forEach(type => xferable.addDataFlavor(type));
 | |
| 
 | |
|     Services.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;
 | |
|     }
 | |
| 
 | |
|     let doCopy = action == "copy";
 | |
|     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) {
 | |
|       this._view.selectItems(itemsToSelect, false);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks if we can insert into a container.
 | |
|    *
 | |
|    * @param {object} container
 | |
|    *          The container were we are want to drop
 | |
|    * @returns {boolean}
 | |
|    */
 | |
|   disallowInsertion(container) {
 | |
|     if (!container) {
 | |
|       throw new Error("empty container");
 | |
|     }
 | |
|     // Allow dropping into Tag containers and editable folders.
 | |
|     return (
 | |
|       !PlacesUtils.nodeIsTagQuery(container) &&
 | |
|       (!PlacesUtils.nodeIsFolder(container) ||
 | |
|         PlacesUIUtils.isFolderReadOnly(container))
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determines if a node can be moved.
 | |
|    *
 | |
|    * @param {object} node
 | |
|    *        A nsINavHistoryResultNode node.
 | |
|    * @returns {boolean} 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;
 | |
|     if (!parentNode) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // Once tags and bookmarked are divorced, the tag-query check should be
 | |
|     // removed.
 | |
|     if (PlacesUtils.nodeIsTagQuery(parentNode)) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     return (
 | |
|       (PlacesUtils.nodeIsFolder(parentNode) &&
 | |
|         !PlacesUIUtils.isFolderReadOnly(parentNode)) ||
 | |
|       PlacesUtils.nodeIsQuery(parentNode)
 | |
|     );
 | |
|   },
 | |
|   async forgetAboutThisSite() {
 | |
|     let host;
 | |
|     if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
 | |
|       host = this._view.selectedNode.query.domain;
 | |
|     } else {
 | |
|       host = Services.io.newURI(this._view.selectedNode.uri).host;
 | |
|     }
 | |
|     let baseDomain;
 | |
|     try {
 | |
|       baseDomain = Services.eTLD.getBaseDomainFromHost(host);
 | |
|     } catch (e) {
 | |
|       // If there is no baseDomain we fall back to host
 | |
|     }
 | |
|     const [title, body, forget] = await document.l10n.formatValues([
 | |
|       { id: "places-forget-about-this-site-confirmation-title" },
 | |
|       {
 | |
|         id: "places-forget-about-this-site-confirmation-msg",
 | |
|         args: { hostOrBaseDomain: baseDomain ?? host },
 | |
|       },
 | |
|       { id: "places-forget-about-this-site-forget" },
 | |
|     ]);
 | |
| 
 | |
|     const flags =
 | |
|       Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
 | |
|       Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
 | |
|       Services.prompt.BUTTON_POS_1_DEFAULT;
 | |
| 
 | |
|     let bag = await Services.prompt.asyncConfirmEx(
 | |
|       window.browsingContext,
 | |
|       Services.prompt.MODAL_TYPE_INTERNAL_WINDOW,
 | |
|       title,
 | |
|       body,
 | |
|       flags,
 | |
|       forget,
 | |
|       null,
 | |
|       null,
 | |
|       null,
 | |
|       false
 | |
|     );
 | |
|     if (bag.getProperty("buttonNumClicked") !== 0) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (this.forgetSiteClearByBaseDomain) {
 | |
|       await this.ForgetAboutSite.removeDataFromBaseDomain(host);
 | |
|     } else {
 | |
|       await this.ForgetAboutSite.removeDataFromDomain(host);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   showInFolder(aBookmarkGuid) {
 | |
|     // Open containing folder in left pane/sidebar bookmark tree
 | |
|     let documentUrl = document.documentURI.toLowerCase();
 | |
|     if (documentUrl.endsWith("browser.xhtml")) {
 | |
|       // We're in a menu or a panel.
 | |
|       window.SidebarUI._show("viewBookmarksSidebar").then(() => {
 | |
|         let theSidebar = document.getElementById("sidebar");
 | |
|         theSidebar.contentDocument
 | |
|           .getElementById("bookmarks-view")
 | |
|           .selectItems([aBookmarkGuid]);
 | |
|       });
 | |
|     } else if (documentUrl.includes("sidebar")) {
 | |
|       // We're in the sidebar - clear the search box first
 | |
|       let searchBox = document.getElementById("search-box");
 | |
|       searchBox.value = "";
 | |
|       searchBox.doCommand();
 | |
| 
 | |
|       // And go to the node
 | |
|       this._view.selectItems([aBookmarkGuid], true);
 | |
|     } else {
 | |
|       // We're in the bookmark library/manager
 | |
|       PlacesUtils.bookmarks
 | |
|         .fetch(aBookmarkGuid, null, { includePath: true })
 | |
|         .then(b => {
 | |
|           let containers = b.path.map(obj => {
 | |
|             return obj.guid;
 | |
|           });
 | |
|           // selectLeftPane looks for literal "AllBookmarks" as a "built-in"
 | |
|           containers.splice(0, 0, "AllBookmarks");
 | |
|           PlacesOrganizer.selectLeftPaneContainerByHierarchy(containers);
 | |
|           this._view.selectItems([aBookmarkGuid], false);
 | |
|         });
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * 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.
 | |
|  */
 | |
| var PlacesControllerDragHelper = {
 | |
|   /**
 | |
|    * For views using DOM nodes like toolbars, menus and panels, this is the DOM
 | |
|    * element currently being dragged over. For other views not handling DOM
 | |
|    * nodes, like trees, it is a Places result node instead.
 | |
|    */
 | |
|   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 {object} node
 | |
|    *        The container node
 | |
|    * @returns {boolean} 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 {object|null} The current active drag session. Returns null if there is none.
 | |
|    */
 | |
|   getSession: function PCDH__getSession() {
 | |
|     return this.dragService.getCurrentSession();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Extract the most relevant flavor from a list of flavors.
 | |
|    *
 | |
|    * @param {DOMStringList} flavors The flavors list.
 | |
|    * @returns {string} The most relevant flavor, or undefined.
 | |
|    */
 | |
|   getMostRelevantFlavor(flavors) {
 | |
|     // The DnD API returns a DOMStringList, but tests may pass an Array.
 | |
|     flavors = Array.from(flavors);
 | |
|     return PlacesUIUtils.SUPPORTED_FLAVORS.find(f => flavors.includes(f));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determines whether or not the data currently being dragged can be dropped
 | |
|    * on a places view.
 | |
|    *
 | |
|    * @param {object} ip
 | |
|    *        The insertion point where the items should be dropped.
 | |
|    * @param {object} dt
 | |
|    *        The data transfer object.
 | |
|    * @returns {boolean}
 | |
|    */
 | |
|   canDrop: function PCDH_canDrop(ip, dt) {
 | |
|     let dropCount = dt.mozItemCount;
 | |
| 
 | |
|     // Check every dragged item.
 | |
|     for (let i = 0; i < dropCount; i++) {
 | |
|       let flavor = this.getMostRelevantFlavor(dt.mozTypesAt(i));
 | |
|       if (!flavor) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       // Urls can be dropped on any insertionpoint.
 | |
|       // XXXmano: remember that this method is called for each dragover event!
 | |
|       // Thus we shouldn't use unwrapNodes here at all if possible.
 | |
|       // I think it would be OK to accept bogus data here (e.g. text which was
 | |
|       // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
 | |
|       // will just case the actual drop to be a no-op), and only rule out valid
 | |
|       // 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 nodes;
 | |
|       try {
 | |
|         nodes = PlacesUtils.unwrapNodes(data, flavor);
 | |
|       } catch (e) {
 | |
|         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;
 | |
|         }
 | |
| 
 | |
|         // Disallow dropping of a folder on itself or any of its descendants.
 | |
|         // This check is done to show an appropriate drop indicator, a stricter
 | |
|         // check is done later by the bookmarks API.
 | |
|         if (
 | |
|           dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
 | |
|           (dragged.uri && dragged.uri.startsWith("place:"))
 | |
|         ) {
 | |
|           let dragOverPlacesNode = this.currentDropTarget;
 | |
|           if (!(dragOverPlacesNode instanceof Ci.nsINavHistoryResultNode)) {
 | |
|             // If it's a DOM node, it should have a _placesNode expando, or it
 | |
|             // may be a static element in a places container, like the [empty]
 | |
|             // menuitem.
 | |
|             dragOverPlacesNode =
 | |
|               dragOverPlacesNode._placesNode ??
 | |
|               dragOverPlacesNode.parentNode?._placesNode;
 | |
|           }
 | |
| 
 | |
|           // If we couldn't get a target Places result node then we can't check
 | |
|           // whether the drag is allowed, just let it go through.
 | |
|           if (dragOverPlacesNode) {
 | |
|             let guid = dragged.concreteGuid ?? dragged.itemGuid;
 | |
|             // Dragging over itself.
 | |
|             if (PlacesUtils.getConcreteItemGuid(dragOverPlacesNode) == guid) {
 | |
|               return false;
 | |
|             }
 | |
|             // Dragging over a descendant.
 | |
|             for (let ancestor of PlacesUtils.nodeAncestors(
 | |
|               dragOverPlacesNode
 | |
|             )) {
 | |
|               if (PlacesUtils.getConcreteItemGuid(ancestor) == guid) {
 | |
|                 return false;
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         // Disallow the dropping of multiple bookmarks if they include
 | |
|         // a javascript: bookmarklet
 | |
|         if (
 | |
|           !flavor.startsWith("text/x-moz-place") &&
 | |
|           (nodes.length > 1 || dropCount > 1) &&
 | |
|           nodes.some(n => n.uri?.startsWith("javascript:"))
 | |
|         ) {
 | |
|           return false;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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.
 | |
|    */
 | |
|   async onDrop(insertionPoint, dt, view) {
 | |
|     let doCopy = ["copy", "link"].includes(dt.dropEffect);
 | |
| 
 | |
|     let dropCount = dt.mozItemCount;
 | |
| 
 | |
|     // Following flavors may contain duplicated data.
 | |
|     let duplicable = new Map();
 | |
|     duplicable.set(PlacesUtils.TYPE_PLAINTEXT, new Set());
 | |
|     duplicable.set(PlacesUtils.TYPE_X_MOZ_URL, new Set());
 | |
| 
 | |
|     // Collect all data from the DataTransfer before processing it, as the
 | |
|     // DataTransfer is only valid during the synchronous handling of the `drop`
 | |
|     // event handler callback.
 | |
|     let nodes = [];
 | |
|     let externalDrag = false;
 | |
|     for (let i = 0; i < dropCount; ++i) {
 | |
|       let flavor = this.getMostRelevantFlavor(dt.mozTypesAt(i));
 | |
|       if (!flavor) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let data = dt.mozGetDataAt(flavor, i);
 | |
|       if (duplicable.has(flavor)) {
 | |
|         let handled = duplicable.get(flavor);
 | |
|         if (handled.has(data)) {
 | |
|           continue;
 | |
|         }
 | |
|         handled.add(data);
 | |
|       }
 | |
| 
 | |
|       // Check that the drag/drop is not internal
 | |
|       if (i == 0 && !flavor.startsWith("text/x-moz-place")) {
 | |
|         externalDrag = true;
 | |
|       }
 | |
| 
 | |
|       if (flavor != TAB_DROP_TYPE) {
 | |
|         nodes = [...nodes, ...PlacesUtils.unwrapNodes(data, flavor)];
 | |
|       } else if (
 | |
|         XULElement.isInstance(data) &&
 | |
|         data.localName == "tab" &&
 | |
|         data.ownerGlobal.isChromeWindow
 | |
|       ) {
 | |
|         let uri = data.linkedBrowser.currentURI;
 | |
|         let spec = uri ? uri.spec : "about:blank";
 | |
|         nodes.push({
 | |
|           uri: spec,
 | |
|           title: data.label,
 | |
|           type: PlacesUtils.TYPE_X_MOZ_URL,
 | |
|         });
 | |
|       } else {
 | |
|         throw new Error("bogus data was passed as a tab");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // If a multiple urls are being dropped from the urlbar or an external source,
 | |
|     // and they include javascript url, not bookmark any of them
 | |
|     if (
 | |
|       externalDrag &&
 | |
|       (nodes.length > 1 || dropCount > 1) &&
 | |
|       nodes.some(n => n.uri?.startsWith("javascript:"))
 | |
|     ) {
 | |
|       throw new Error("Javascript bookmarklet passed with uris");
 | |
|     }
 | |
| 
 | |
|     // If a single javascript url is being dropped from the urlbar or an external source,
 | |
|     // show the bookmark dialog as a speedbump protection against malicious cases.
 | |
|     if (
 | |
|       nodes.length == 1 &&
 | |
|       externalDrag &&
 | |
|       nodes[0].uri?.startsWith("javascript")
 | |
|     ) {
 | |
|       let uri;
 | |
|       try {
 | |
|         uri = Services.io.newURI(nodes[0].uri);
 | |
|       } catch (ex) {
 | |
|         // Invalid uri, we skip this code and the entry will be discarded later.
 | |
|       }
 | |
| 
 | |
|       if (uri) {
 | |
|         let bookmarkGuid = await PlacesUIUtils.showBookmarkDialog(
 | |
|           {
 | |
|             action: "add",
 | |
|             type: "bookmark",
 | |
|             defaultInsertionPoint: insertionPoint,
 | |
|             hiddenRows: ["folderPicker"],
 | |
|             title: nodes[0].title,
 | |
|             uri,
 | |
|           },
 | |
|           BrowserWindowTracker.getTopWindow() // `window` may be the Library.
 | |
|         );
 | |
| 
 | |
|         if (bookmarkGuid && view) {
 | |
|           view.selectItems([bookmarkGuid], false);
 | |
|         }
 | |
| 
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     await PlacesUIUtils.handleTransferItems(
 | |
|       nodes,
 | |
|       insertionPoint,
 | |
|       doCopy,
 | |
|       view
 | |
|     );
 | |
|   },
 | |
| };
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(
 | |
|   PlacesControllerDragHelper,
 | |
|   "dragService",
 | |
|   "@mozilla.org/widget/dragservice;1",
 | |
|   "nsIDragService"
 | |
| );
 | 
