Mypal/application/basilisk/components/places/content/controller.js

1748 lines
61 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/. */
XPCOMUtils.defineLazyModuleGetter(this, "ForgetAboutSite",
"resource://gre/modules/ForgetAboutSite.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
// XXXmano: we should move most/all of these constants to PlacesUtils
const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
// No change to the view, preserve current selection
const RELOAD_ACTION_NOTHING = 0;
// Inserting items new to the view, select the inserted rows
const RELOAD_ACTION_INSERT = 1;
// Removing items from the view, select the first item after the last selected
const RELOAD_ACTION_REMOVE = 2;
// Moving items within a view, don't treat the dropped items as additional
// rows.
const RELOAD_ACTION_MOVE = 3;
// When removing a bunch of pages we split them in chunks to give some breath
// to the main-thread.
const REMOVE_PAGES_CHUNKLEN = 300;
/**
* Represents an insertion point within a container where we can insert
* items.
* @param aItemId
* The identifier of the parent container
* @param aIndex
* The index within the container where we should insert
* @param aOrientation
* The orientation of the insertion. NOTE: the adjustments to the
* insertion point to accommodate the orientation should be done by
* the person who constructs the IP, not the user. The orientation
* is provided for informational purposes only!
* @param [optional] aTag
* The tag name if this IP is set to a tag, null otherwise.
* @param [optional] aDropNearItemId
* When defined we will calculate index based on this itemId
* @constructor
*/
function InsertionPoint(aItemId, aIndex, aOrientation, aTagName = null,
aDropNearItemId = false) {
this.itemId = aItemId;
this._index = aIndex;
this.orientation = aOrientation;
this.tagName = aTagName;
this.dropNearItemId = aDropNearItemId;
}
InsertionPoint.prototype = {
set index(val) {
return this._index = val;
},
promiseGuid: function () {
return PlacesUtils.promiseItemGuid(this.itemId);
},
get index() {
if (this.dropNearItemId > 0) {
// If dropNearItemId is set up we must calculate the real index of
// the item near which we will drop.
var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId);
return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
}
return this._index;
},
get isTag() {
return typeof(this.tagName) == "string";
}
};
/**
* Places Controller
*/
function PlacesController(aView) {
this._view = aView;
XPCOMUtils.defineLazyServiceGetter(this, "clipboard",
"@mozilla.org/widget/clipboard;1",
"nsIClipboard");
XPCOMUtils.defineLazyGetter(this, "profileName", function () {
return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
});
this._cachedLivemarkInfoObjects = new Map();
}
PlacesController.prototype = {
/**
* The places view.
*/
_view: null,
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIClipboardOwner
]),
// nsIClipboardOwner
LosingOwnership: function(aXferable) {
this.cutNodes = [];
},
terminate: function() {
this._releaseClipboardOwnership();
},
supportsCommand: function(aCommand) {
// 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(aCommand) {
switch (aCommand) {
case "cmd_undo":
if (!PlacesUIUtils.useAsyncTransactions)
return PlacesUtils.transactionManager.numberOfUndoItems > 0;
return PlacesTransactions.topUndoEntry != null;
case "cmd_redo":
if (!PlacesUIUtils.useAsyncTransactions)
return PlacesUtils.transactionManager.numberOfRedoItems > 0;
return PlacesTransactions.topRedoEntry != null;
case "cmd_cut":
case "placesCmd_cut":
case "placesCmd_moveBookmarks":
for (let node of this._view.selectedNodes) {
// If selection includes history nodes or tags-as-bookmark, disallow
// cutting.
if (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":
return this._view.hasSelection;
case "cmd_paste":
case "placesCmd_paste":
return this._canInsert(true) && this._isClipboardDataPasteable();
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":
var selectedNode = this._view.selectedNode;
return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
case "placesCmd_new:folder":
return this._canInsert();
case "placesCmd_new:bookmark":
return this._canInsert();
case "placesCmd_new:separator":
return this._canInsert() &&
!PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems &&
this._view.result.sortingMode ==
Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
case "placesCmd_show:info": {
let selectedNode = this._view.selectedNode;
return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1
}
case "placesCmd_reload": {
// Livemark containers
let selectedNode = this._view.selectedNode;
return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
}
case "placesCmd_sortBy:name": {
let selectedNode = this._view.selectedNode;
return selectedNode &&
PlacesUtils.nodeIsFolder(selectedNode) &&
!PlacesUIUtils.isContentsReadOnly(selectedNode) &&
this._view.result.sortingMode ==
Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
}
case "placesCmd_createBookmark":
var node = this._view.selectedNode;
return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
default:
return false;
}
},
doCommand: function(aCommand) {
switch (aCommand) {
case "cmd_undo":
if (!PlacesUIUtils.useAsyncTransactions) {
PlacesUtils.transactionManager.undoTransaction();
return;
}
PlacesTransactions.undo().then(null, Components.utils.reportError);
break;
case "cmd_redo":
if (!PlacesUIUtils.useAsyncTransactions) {
PlacesUtils.transactionManager.redoTransaction();
return;
}
PlacesTransactions.redo().then(null, Components.utils.reportError);
break;
case "cmd_cut":
case "placesCmd_cut":
this.cut();
break;
case "cmd_copy":
case "placesCmd_copy":
this.copy();
break;
case "cmd_paste":
case "placesCmd_paste":
this.paste().then(null, Components.utils.reportError);
break;
case "cmd_delete":
case "placesCmd_delete":
this.remove("Remove Selection").then(null, Components.utils.reportError);
break;
case "placesCmd_deleteDataHost":
var host;
if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
var queries = this._view.selectedNode.getQueries();
host = queries[0].domain;
}
else
host = NetUtil.newURI(this._view.selectedNode.uri).host;
ForgetAboutSite.removeDataFromDomain(host)
.catch(Components.utils.reportError);
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");
break;
case "placesCmd_new:bookmark":
this.newItem("bookmark");
break;
case "placesCmd_new:separator":
this.newSeparator().catch(Components.utils.reportError);
break;
case "placesCmd_show:info":
this.showBookmarkPropertiesForSelection();
break;
case "placesCmd_moveBookmarks":
this.moveSelectedBookmarks();
break;
case "placesCmd_reload":
this.reloadSelectedLivemark();
break;
case "placesCmd_sortBy:name":
this.sortFolderByName().then(null, Components.utils.reportError);
break;
case "placesCmd_createBookmark":
let node = this._view.selectedNode;
PlacesUIUtils.showBookmarkDialog({ action: "add"
, type: "bookmark"
, hiddenRows: [ "description"
, "keyword"
, "location"
, "loadInSidebar" ]
, uri: NetUtil.newURI(node.uri)
, title: node.title
}, window.top);
break;
}
},
onEvent: function(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.
*
* @return true if all nodes in the selection can be removed,
* false otherwise.
*/
_hasRemovableSelection() {
var ranges = this._view.removableSelectionRanges;
if (!ranges.length)
return false;
var root = this._view.result.root;
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;
},
/**
* Determines whether or not nodes can be inserted relative to the selection.
*/
_canInsert: function(isPaste) {
var ip = this._view.insertionPoint;
return ip != null && (isPaste || ip.isTag != true);
},
/**
* Looks at the data on the clipboard to see if it is paste-able.
* Paste-able data is:
* - in a format that the view can receive
* @return true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
* - clipboard data is of type TEXT_UNICODE and
* is a valid URI.
*/
_isClipboardDataPasteable: function() {
// if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
// pasteable, with no need to unwrap all the nodes.
var flavors = PlacesUIUtils.PLACES_FLAVORS;
var clipboard = this.clipboard;
var hasPlacesData =
clipboard.hasDataMatchingFlavors(flavors, flavors.length,
Ci.nsIClipboard.kGlobalClipboard);
if (hasPlacesData)
return this._view.insertionPoint != null;
// if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
// pasting of valid "text/unicode" and "text/x-moz-url" data
var xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
xferable.init(null);
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
try {
// getAnyTransferData will throw if no data is available.
var data = { }, type = { };
xferable.getAnyTransferData(type, data, { });
data = data.value.QueryInterface(Ci.nsISupportsString).data;
if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
type.value != PlacesUtils.TYPE_UNICODE)
return false;
// unwrapNodes() will throw if the data blob is malformed.
PlacesUtils.unwrapNodes(data, type.value);
return this._view.insertionPoint != null;
}
catch (e) {
// getAnyTransferData or unwrapNodes failed
return false;
}
},
/**
* Gathers information about the selected nodes according to the following
* rules:
* "link" node is a URI
* "bookmark" node is a bookmark
* "livemarkChild" node is a child of a livemark
* "tagChild" node is a child of a tag
* "folder" node is a folder
* "query" node is a query
* "separator" node is a separator line
* "host" node is a host
*
* @return an array of objects corresponding the selected nodes. Each
* object has each of the properties above set if its corresponding
* node matches the rule. In addition, the annotations names for each
* node are set on its corresponding object as properties.
* Notes:
* 1) This can be slow, so don't call it anywhere performance critical!
*/
_buildSelectionMetadata: function() {
var metadata = [];
var nodes = this._view.selectedNodes;
for (var i = 0; i < nodes.length; i++) {
var nodeData = {};
var node = nodes[i];
var nodeType = node.type;
var uri = null;
// We don't use the nodeIs* methods here to avoid going through the type
// property way too often
switch (nodeType) {
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["host"] = true;
break;
case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
nodeData["day"] = true;
break;
}
}
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;
uri = NetUtil.newURI(node.uri);
if (PlacesUtils.nodeIsBookmark(node)) {
nodeData["bookmark"] = true;
var parentNode = node.parent;
if (parentNode) {
if (PlacesUtils.nodeIsTagQuery(parentNode))
nodeData["tagChild"] = true;
}
} else {
var parentNode = node.parent;
if (parentNode) {
if (this.hasCachedLivemarkInfo(parentNode))
nodeData["livemarkChild"] = true;
}
}
break;
}
// annotations
if (uri) {
let names = PlacesUtils.annotations.getPageAnnotationNames(uri);
for (let j = 0; j < names.length; ++j)
nodeData[names[j]] = true;
}
// For items also include the item-specific annotations
if (node.itemId != -1) {
let names = PlacesUtils.annotations
.getItemAnnotationNames(node.itemId);
for (let j = 0; j < names.length; ++j)
nodeData[names[j]] = true;
}
metadata.push(nodeData);
}
return metadata;
},
/**
* Determines if a context-menu item should be shown
* @param aMenuItem
* the context menu item
* @param aMetaData
* meta data about the selection
* @return true if the conditions (see buildContextMenu) are satisfied
* and the item can be displayed, false otherwise.
*/
_shouldShowMenuItem: function(aMenuItem, aMetaData) {
var selectiontype = aMenuItem.getAttribute("selectiontype");
if (!selectiontype) {
selectiontype = "single|multiple";
}
var selectionTypes = selectiontype.split("|");
if (selectionTypes.includes("any")) {
return true;
}
var count = aMetaData.length;
if (count > 1 && !selectionTypes.includes("multiple"))
return false;
if (count == 1 && !selectionTypes.includes("single"))
return false;
// NB: if there is no selection, we show the item if and only if
// the selectiontype includes 'none' - the metadata list will be
// empty so none of the other criteria will apply anyway.
if (count == 0)
return selectionTypes.includes("none");
var forceHideAttr = aMenuItem.getAttribute("forcehideselection");
if (forceHideAttr) {
var forceHideRules = forceHideAttr.split("|");
for (let i = 0; i < aMetaData.length; ++i) {
for (let j = 0; j < forceHideRules.length; ++j) {
if (forceHideRules[j] in aMetaData[i])
return false;
}
}
}
var selectionAttr = aMenuItem.getAttribute("selection");
if (!selectionAttr) {
return !aMenuItem.hidden;
}
if (selectionAttr == "any")
return true;
var showRules = selectionAttr.split("|");
var anyMatched = false;
function metaDataNodeMatches(metaDataNode, rules) {
for (var i = 0; i < rules.length; i++) {
if (rules[i] in metaDataNode)
return true;
}
return false;
}
for (var i = 0; i < aMetaData.length; ++i) {
if (metaDataNodeMatches(aMetaData[i], showRules))
anyMatched = true;
else
return false;
}
return anyMatched;
},
/**
* Detects information (meta-data rules) about the current selection in the
* view (see _buildSelectionMetadata) and sets the visibility state for each
* of the menu-items in the given popup with the following rules applied:
* 0) The "ignoreitem" attribute may be set to "true" for this code not to
* handle that menuitem.
* 1) The "selectiontype" attribute may be set on a menu-item to "single"
* if the menu-item should be visible only if there is a single node
* selected, or to "multiple" if the menu-item should be visible only if
* multiple nodes are selected, or to "none" if the menuitems should be
* visible for if there are no selected nodes, or to a |-separated
* combination of these.
* If the attribute is not set or set to an invalid value, the menu-item
* may be visible irrespective of the selection.
* 2) The "selection" attribute may be set on a menu-item to the various
* meta-data rules for which it may be visible. The rules should be
* separated with the | character.
* 3) A menu-item may be visible only if at least one of the rules set in
* its selection attribute apply to each of the selected nodes in the
* view.
* 4) The "forcehideselection" attribute may be set on a menu-item to rules
* for which it should be hidden. This attribute takes priority over the
* selection attribute. A menu-item would be hidden if at least one of the
* given rules apply to one of the selected nodes. The rules should be
* separated with the | character.
* 5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to
* true if it should be hidden when there's no insertion point
* 6) The visibility state of a menu-item is unchanged if none of these
* attribute are set.
* 7) These attributes should not be set on separators for which the
* visibility state is "auto-detected."
* 8) The "hideifprivatebrowsing" attribute may be set on a menu-item to
* true if it should be hidden inside the private browsing mode
* @param aPopup
* The menupopup to build children into.
* @return true if at least one item is visible, false otherwise.
*/
buildContextMenu: function(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.childNodes.length; ++i) {
var item = aPopup.childNodes[i];
if (item.getAttribute("ignoreitem") == "true") {
continue;
}
if (item.localName != "menuseparator") {
// We allow pasting into tag containers, so special case that.
var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
var hideIfPrivate = item.getAttribute("hideifprivatebrowsing") == "true" &&
PrivateBrowsingUtils.isWindowPrivate(window);
var shouldHideItem = hideIfNoIP || hideIfPrivate ||
!this._shouldShowMenuItem(item, metadata);
item.hidden = item.disabled = shouldHideItem;
if (!item.hidden) {
visibleItemsBeforeSep = true;
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;
}
}
// Set Open Folder/Links In Tabs items enabled state if they're visible
if (usableItemCount > 0) {
var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
if (!openContainerInTabsItem.hidden) {
var containerToUse = this._view.selectedNode || this._view.result.root;
if (PlacesUtils.nodeIsContainer(containerToUse)) {
if (!PlacesUtils.hasChildURIs(containerToUse)) {
openContainerInTabsItem.disabled = true;
// Ensure that we don't display the menu if nothing is enabled:
usableItemCount--;
}
}
}
}
return usableItemCount > 0;
},
/**
* Select all links in the current view.
*/
selectAll: function() {
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);
},
/**
* This method can be run on a URI parameter to ensure that it didn't
* receive a string instead of an nsIURI object.
*/
_assertURINotString: function(value) {
NS_ASSERT((typeof(value) == "object") && !(value instanceof String),
"This method should be passed a URI as a nsIURI object, not as a string.");
},
/**
* Reloads the selected livemark if any.
*/
reloadSelectedLivemark: function() {
var selectedNode = this._view.selectedNode;
if (selectedNode) {
let itemId = selectedNode.itemId;
PlacesUtils.livemarks.getLivemark({ id: itemId })
.then(aLivemark => {
aLivemark.reload(true);
}, Components.utils.reportError);
}
},
/**
* Opens the links in the selected folder, or the selected links in new tabs.
*/
openSelectionInTabs: function(aEvent) {
var node = this._view.selectedNode;
var nodes = this._view.selectedNodes;
// In the case of no selection, open the root node:
if (!node && !nodes.length) {
node = this._view.result.root;
}
if (node && PlacesUtils.nodeIsContainer(node))
PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this._view);
else
PlacesUIUtils.openURINodesInTabs(nodes, aEvent, this._view);
},
/**
* Shows the Add Bookmark UI for the current insertion point.
*
* @param aType
* the type of the new item (bookmark/livemark/folder)
*/
newItem: function(aType) {
let ip = this._view.insertionPoint;
if (!ip)
throw Cr.NS_ERROR_NOT_AVAILABLE;
let performed =
PlacesUIUtils.showBookmarkDialog({ action: "add"
, type: aType
, defaultInsertionPoint: ip
, hiddenRows: [ "folderPicker" ]
}, window.top);
if (performed) {
// Select the new item.
let insertedNodeId = PlacesUtils.bookmarks
.getIdForItemAt(ip.itemId, ip.index);
this._view.selectItems([insertedNodeId], false);
}
},
/**
* Create a new Bookmark separator somewhere.
*/
newSeparator: Task.async(function* () {
var ip = this._view.insertionPoint;
if (!ip)
throw Cr.NS_ERROR_NOT_AVAILABLE;
if (!PlacesUIUtils.useAsyncTransactions) {
let txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
PlacesUtils.transactionManager.doTransaction(txn);
// Select the new item.
let insertedNodeId = PlacesUtils.bookmarks
.getIdForItemAt(ip.itemId, ip.index);
this._view.selectItems([insertedNodeId], false);
return;
}
let txn = PlacesTransactions.NewSeparator({ parentGuid: yield ip.promiseGuid()
, index: ip.index });
let guid = yield txn.transact();
let itemId = yield PlacesUtils.promiseItemId(guid);
// Select the new item.
this._view.selectItems([itemId], false);
}),
/**
* Opens a dialog for moving the selected nodes.
*/
moveSelectedBookmarks: function() {
window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
"", "chrome, modal",
this._view.selectedNodes);
},
/**
* Sort the selected folder by name
*/
sortFolderByName: Task.async(function* () {
let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
if (!PlacesUIUtils.useAsyncTransactions) {
var txn = new PlacesSortFolderByNameTransaction(itemId);
PlacesUtils.transactionManager.doTransaction(txn);
return;
}
let guid = yield PlacesUtils.promiseItemGuid(itemId);
yield PlacesTransactions.SortByName(guid).transact();
}),
/**
* Walk the list of folders we're removing in this delete operation, and
* see if the selected node specified is already implicitly being removed
* because it is a child of that folder.
* @param node
* Node to check for containment.
* @param pastFolders
* List of folders the calling function has already traversed
* @return true if the node should be skipped, false otherwise.
*/
_shouldSkipNode: function(node, pastFolders) {
/**
* Determines if a node is contained by another node within a resultset.
* @param node
* The node to check for containment for
* @param parent
* The parent container to check for containment in
* @return true if node is a member of parent's children, false otherwise.
*/
function isContainedBy(node, parent) {
var cursor = node.parent;
while (cursor) {
if (cursor == parent)
return true;
cursor = cursor.parent;
}
return false;
}
for (var j = 0; j < pastFolders.length; ++j) {
if (isContainedBy(node, pastFolders[j]))
return true;
}
return false;
},
/**
* Creates a set of transactions for the removal of a range of items.
* A range is an array of adjacent nodes in a view.
* @param [in] range
* An array of nodes to remove. Should all be adjacent.
* @param [out] transactions
* An array of transactions.
* @param [optional] removedFolders
* An array of folder nodes that have already been removed.
*/
_removeRange: function(range, transactions, removedFolders) {
NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
if (!removedFolders)
removedFolders = [];
for (var i = 0; i < range.length; ++i) {
var node = range[i];
if (this._shouldSkipNode(node, removedFolders))
continue;
if (PlacesUtils.nodeIsTagQuery(node.parent)) {
// This is a uri node inside a tag container. It needs a special
// untag transaction.
var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
var uri = NetUtil.newURI(node.uri);
if (PlacesUIUtils.useAsyncTransactions) {
let tag = node.parent.title;
if (!tag)
tag = PlacesUtils.bookmarks.getItemTitle(tagItemId);
transactions.push(PlacesTransactions.Untag({ uri: uri, tag: tag }));
}
else {
let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
transactions.push(txn);
}
}
else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
PlacesUtils.nodeIsQuery(node.parent) &&
PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
// This is a tag container.
// Untag all URIs tagged with this tag only if the tag container is
// child of the "Tags" query in the library, in all other places we
// must only remove the query node.
let tag = node.title;
let URIs = PlacesUtils.tagging.getURIsForTag(tag);
if (PlacesUIUtils.useAsyncTransactions) {
transactions.push(PlacesTransactions.Untag({ tag: tag, uris: URIs }));
}
else {
for (var j = 0; j < URIs.length; j++) {
let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
transactions.push(txn);
}
}
}
else if (PlacesUtils.nodeIsURI(node) &&
PlacesUtils.nodeIsQuery(node.parent) &&
PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
// This is a uri node inside an history query.
PlacesUtils.bhistory.removePage(NetUtil.newURI(node.uri));
// 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.
this._removeHistoryContainer(node);
// 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);
}
if (PlacesUIUtils.useAsyncTransactions) {
transactions.push(
PlacesTransactions.Remove({ guid: node.bookmarkGuid }));
}
else {
let txn = new PlacesRemoveItemTransaction(node.itemId);
transactions.push(txn);
}
}
}
},
/**
* Removes the set of selected ranges from bookmarks.
* @param txnName
* See |remove|.
*/
_removeRowsFromBookmarks: Task.async(function* (txnName) {
var ranges = this._view.removableSelectionRanges;
var transactions = [];
var removedFolders = [];
for (var i = 0; i < ranges.length; i++)
this._removeRange(ranges[i], transactions, removedFolders);
if (transactions.length > 0) {
if (PlacesUIUtils.useAsyncTransactions) {
yield PlacesTransactions.batch(transactions);
}
else {
var txn = new PlacesAggregatedTransaction(txnName, transactions);
PlacesUtils.transactionManager.doTransaction(txn);
}
}
}),
/**
* Removes the set of selected ranges from history.
*
* @note history deletes are not undoable.
*/
_removeRowsFromHistory: function() {
let nodes = this._view.selectedNodes;
let URIs = [];
for (let i = 0; i < nodes.length; ++i) {
let node = nodes[i];
if (PlacesUtils.nodeIsURI(node)) {
let uri = NetUtil.newURI(node.uri);
// Avoid duplicates.
if (URIs.indexOf(uri) < 0) {
URIs.push(uri);
}
}
else if (PlacesUtils.nodeIsQuery(node) &&
PlacesUtils.asQuery(node).queryOptions.queryType ==
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
this._removeHistoryContainer(node);
}
}
// Do removal in chunks to give some breath to main-thread.
function* pagesChunkGenerator(aURIs) {
while (aURIs.length) {
let URIslice = aURIs.splice(0, REMOVE_PAGES_CHUNKLEN);
PlacesUtils.bhistory.removePages(URIslice, URIslice.length);
Services.tm.mainThread.dispatch(() => gen.next(),
Ci.nsIThread.DISPATCH_NORMAL);
yield undefined;
}
}
let gen = pagesChunkGenerator(URIs);
gen.next();
},
/**
* Removes history visits for an history container node.
* @param [in] aContainerNode
* The container node to remove.
*
* @note history deletes are not undoable.
*/
_removeHistoryContainer: function(aContainerNode) {
if (PlacesUtils.nodeIsHost(aContainerNode)) {
// Site container.
PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true);
}
else if (PlacesUtils.nodeIsDay(aContainerNode)) {
// Day container.
let query = aContainerNode.getQueries()[0];
let beginTime = query.beginTime;
let endTime = query.endTime;
NS_ASSERT(query && beginTime && endTime,
"A valid date container query should exist!");
// We want to exclude beginTime from the removal because
// removePagesByTimeframe includes both extremes, while date containers
// exclude the lower extreme. So, if we would not exclude it, we would
// end up removing more history than requested.
PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime);
}
},
/**
* Removes the selection
* @param aTxnName
* A name for the transaction if this is being performed
* as part of another operation.
*/
remove: Task.async(function* (aTxnName) {
if (!this._hasRemovableSelection())
return;
NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
var root = this._view.result.root;
if (PlacesUtils.nodeIsFolder(root)) {
if (PlacesUIUtils.useAsyncTransactions)
yield this._removeRowsFromBookmarks(aTxnName);
else
this._removeRowsFromBookmarks(aTxnName);
}
else if (PlacesUtils.nodeIsQuery(root)) {
var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) {
if (PlacesUIUtils.useAsyncTransactions)
yield this._removeRowsFromBookmarks(aTxnName);
else
this._removeRowsFromBookmarks(aTxnName);
}
else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
this._removeRowsFromHistory();
}
else {
NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
}
}
else
NS_ASSERT(false, "unexpected root");
}),
/**
* Fills a DataTransfer object with the content of the selection that can be
* dropped elsewhere.
* @param aEvent
* The dragstart event.
*/
setDataTransfer: function(aEvent) {
let dt = aEvent.dataTransfer;
let result = this._view.result;
let didSuppressNotifications = result.suppressNotifications;
if (!didSuppressNotifications)
result.suppressNotifications = true;
function addData(type, index, feedURI) {
let wrapNode = PlacesUtils.wrapNode(node, type, feedURI);
dt.mozSetDataAt(type, wrapNode, index);
}
function addURIData(index, feedURI) {
addData(PlacesUtils.TYPE_X_MOZ_URL, index, feedURI);
addData(PlacesUtils.TYPE_UNICODE, index, feedURI);
addData(PlacesUtils.TYPE_HTML, index, feedURI);
}
try {
let nodes = this._view.draggableSelection;
for (let i = 0; i < nodes.length; ++i) {
var node = nodes[i];
// This order is _important_! It controls how this and other
// applications select data to be inserted based on type.
addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
// Drop the feed uri for livemark containers
let livemarkInfo = this.getCachedLivemarkInfo(node);
if (livemarkInfo) {
addURIData(i, livemarkInfo.feedURI.spec);
}
else 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)
this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {});
[action, actionOwner] =
action.value.QueryInterface(Ci.nsISupportsString).data.split(",");
} catch (ex) {
// Paste from external sources don't have any associated action, just
// fallback to a copy action.
return "copy";
}
// For cuts also check who inited the action, since cuts across different
// instances should instead be handled as copies (The sources are not
// available for this instance).
if (action == "cut" && actionOwner != this.profileName)
action = "copy";
return action;
},
_releaseClipboardOwnership: function() {
if (this.cutNodes.length > 0) {
// This clears the logical clipboard, doesn't remove data.
this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
}
},
_clearClipboard: function() {
let xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
xferable.init(null);
// Empty transferables may cause crashes, so just add an unknown type.
const TYPE = "text/x-moz-place-empty";
xferable.addDataFlavor(TYPE);
xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0);
this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
},
_populateClipboard: function(aNodes, aAction) {
// This order is _important_! It controls how this and other applications
// select data to be inserted based on type.
let contents = [
{ type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
{ type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
{ type: PlacesUtils.TYPE_HTML, entries: [] },
{ type: PlacesUtils.TYPE_UNICODE, entries: [] },
];
// Avoid handling descendants of a copied node, the transactions take care
// of them automatically.
let copiedFolders = [];
aNodes.forEach(function (node) {
if (this._shouldSkipNode(node, copiedFolders))
return;
if (PlacesUtils.nodeIsFolder(node))
copiedFolders.push(node);
let livemarkInfo = this.getCachedLivemarkInfo(node);
let feedURI = livemarkInfo && livemarkInfo.feedURI.spec;
contents.forEach(function (content) {
content.entries.push(
PlacesUtils.wrapNode(node, content.type, feedURI)
);
});
}, this);
function addData(type, data) {
xferable.addDataFlavor(type);
xferable.setTransferData(type, PlacesUtils.toISupportsString(data),
data.length * 2);
}
let xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
xferable.init(null);
let hasData = false;
// This order matters here! It controls how this and other applications
// select data to be inserted based on type.
contents.forEach(function (content) {
if (content.entries.length > 0) {
hasData = true;
let glue =
content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
addData(content.type, content.entries.join(glue));
}
});
// Track the exected action in the xferable. This must be the last flavor
// since it's the least preferred one.
// Enqueue a unique instance identifier to distinguish operations across
// concurrent instances of the application.
addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName);
if (hasData) {
this.clipboard.setData(xferable,
this.cutNodes.length > 0 ? this : null,
Ci.nsIClipboard.kGlobalClipboard);
}
},
_cutNodes: [],
get cutNodes() {
return this._cutNodes;
},
set cutNodes(aNodes) {
let self = this;
function updateCutNodes(aValue) {
self._cutNodes.forEach(function (aNode) {
self._view.toggleCutNode(aNode, aValue);
});
}
updateCutNodes(false);
this._cutNodes = aNodes;
updateCutNodes(true);
return aNodes;
},
/**
* Copy Bookmarks and Folders to the clipboard
*/
copy: function() {
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() {
let result = this._view.result;
let didSuppressNotifications = result.suppressNotifications;
if (!didSuppressNotifications)
result.suppressNotifications = true;
try {
this._populateClipboard(this._view.selectedNodes, "cut");
this.cutNodes = this._view.selectedNodes;
}
finally {
if (!didSuppressNotifications)
result.suppressNotifications = false;
}
},
/**
* Paste Bookmarks and Folders from the clipboard
*/
paste: Task.async(function* () {
// No reason to proceed if there isn't a valid insertion point.
let ip = this._view.insertionPoint;
if (!ip)
throw Cr.NS_ERROR_NOT_AVAILABLE;
let action = this.clipboardAction;
let xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
xferable.init(null);
// This order matters here! It controls the preferred flavors for this
// paste operation.
[ PlacesUtils.TYPE_X_MOZ_PLACE,
PlacesUtils.TYPE_X_MOZ_URL,
PlacesUtils.TYPE_UNICODE,
].forEach(type => xferable.addDataFlavor(type));
this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
// Now get the clipboard contents, in the best available flavor.
let data = {}, type = {}, items = [];
try {
xferable.getAnyTransferData(type, data, {});
data = data.value.QueryInterface(Ci.nsISupportsString).data;
type = type.value;
items = PlacesUtils.unwrapNodes(data, type);
} catch (ex) {
// No supported data exists or nodes unwrap failed, just bail out.
return;
}
let itemsToSelect = [];
if (PlacesUIUtils.useAsyncTransactions) {
if (ip.isTag) {
let uris = items.filter(item => "uri" in item).map(item => NetUtil.newURI(item.uri));
yield PlacesTransactions.Tag({ uris: uris, tag: ip.tagName }).transact();
}
else {
yield PlacesTransactions.batch(function* () {
let insertionIndex = ip.index;
let parent = yield ip.promiseGuid();
for (let item of items) {
let doCopy = action == "copy";
// If this is not a copy, check for safety that we can move the
// source, otherwise report an error and fallback to a copy.
if (!doCopy &&
!PlacesControllerDragHelper.canMoveUnwrappedNode(item)) {
Components.utils.reportError("Tried to move an unmovable " +
"Places node, reverting to a copy operation.");
doCopy = true;
}
let guid = yield PlacesUIUtils.getTransactionForData(
item, type, parent, insertionIndex, doCopy).transact();
itemsToSelect.push(yield PlacesUtils.promiseItemId(guid));
// Adjust index to make sure items are pasted in the correct
// position. If index is DEFAULT_INDEX, items are just appended.
if (insertionIndex != PlacesUtils.bookmarks.DEFAULT_INDEX)
insertionIndex++;
}
});
}
}
else {
let transactions = [];
let insertionIndex = ip.index;
for (let i = 0; i < items.length; ++i) {
if (ip.isTag) {
// Pasting into a tag container means tagging the item, regardless of
// the requested action.
let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri),
[ip.itemId]);
transactions.push(tagTxn);
continue;
}
// Adjust index to make sure items are pasted in the correct position.
// If index is DEFAULT_INDEX, items are just appended.
if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX)
insertionIndex = ip.index + i;
// If this is not a copy, check for safety that we can move the source,
// otherwise report an error and fallback to a copy.
if (action != "copy" && !PlacesControllerDragHelper.canMoveUnwrappedNode(items[i])) {
Components.utils.reportError("Tried to move an unmovable Places " +
"node, reverting to a copy operation.");
action = "copy";
}
transactions.push(
PlacesUIUtils.makeTransaction(items[i], type, ip.itemId,
insertionIndex, action == "copy")
);
}
let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
for (let i = 0; i < transactions.length; ++i) {
itemsToSelect.push(
PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i)
);
}
}
// Cut/past operations are not repeatable, so clear the clipboard.
if (action == "cut") {
this._clearClipboard();
}
if (itemsToSelect.length > 0)
this._view.selectItems(itemsToSelect, false);
}),
/**
* Cache the livemark info for a node. This allows the controller and the
* views to treat the given node as a livemark.
* @param aNode
* a places result node.
* @param aLivemarkInfo
* a mozILivemarkInfo object.
*/
cacheLivemarkInfo: function(aNode, aLivemarkInfo) {
this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo);
},
/**
* Returns whether or not there's cached mozILivemarkInfo object for a node.
* @param aNode
* a places result node.
* @return true if there's a cached mozILivemarkInfo object for
* aNode, false otherwise.
*/
hasCachedLivemarkInfo: function(aNode) {
return this._cachedLivemarkInfoObjects.has(aNode);
},
/**
* 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(aNode) {
return this._cachedLivemarkInfoObjects.get(aNode, null);
}
};
/**
* 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 = {
/**
* DOM Element currently being dragged over
*/
currentDropTarget: null,
/**
* Determines if the mouse is currently being dragged over a child node of
* this menu. This is necessary so that the menu doesn't close while the
* mouse is dragging over one of its submenus
* @param node
* The container node
* @return true if the user is dragging over a node within the hierarchy of
* the container, false otherwise.
*/
draggingOverChildNode: function(node) {
let currentNode = this.currentDropTarget;
while (currentNode) {
if (currentNode == node)
return true;
currentNode = currentNode.parentNode;
}
return false;
},
/**
* @return The current active drag session. Returns null if there is none.
*/
getSession: function() {
return this.dragService.getCurrentSession();
},
/**
* Extract the first accepted flavor from a list of flavors.
* @param aFlavors
* The flavors list of type DOMStringList.
*/
getFirstValidFlavor: function(aFlavors) {
for (let i = 0; i < aFlavors.length; i++) {
if (PlacesUIUtils.SUPPORTED_FLAVORS.includes(aFlavors[i]))
return aFlavors[i];
}
// If no supported flavor is found, check if data includes text/plain
// contents. If so, request them as text/unicode, a conversion will happen
// automatically.
if (aFlavors.contains("text/plain")) {
return PlacesUtils.TYPE_UNICODE;
}
return null;
},
/**
* Determines whether or not the data currently being dragged can be dropped
* on a places view.
* @param ip
* The insertion point where the items should be dropped.
*/
canDrop: function(ip, dt) {
let dropCount = dt.mozItemCount;
// Check every dragged item.
for (let i = 0; i < dropCount; i++) {
let flavor = this.getFirstValidFlavor(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 dragged;
try {
dragged = PlacesUtils.unwrapNodes(data, flavor)[0];
}
catch (e) {
return false;
}
// Only bookmarks and urls can be dropped into tag containers.
if (ip.isTag &&
dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
(dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
(dragged.uri && dragged.uri.startsWith("place:")) ))
return false;
// The following loop disallows the dropping of a folder on itself or
// on any of its descendants.
if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
(dragged.uri && dragged.uri.startsWith("place:")) ) {
let parentId = ip.itemId;
while (parentId != PlacesUtils.placesRootId) {
if (dragged.concreteId == parentId || dragged.id == parentId)
return false;
parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
}
}
}
return true;
},
/**
* Determines if an unwrapped node can be moved.
*
* @param aUnwrappedNode
* A node unwrapped by PlacesUtils.unwrapNodes().
* @return True if the node can be moved, false otherwise.
*/
canMoveUnwrappedNode: function (aUnwrappedNode) {
return aUnwrappedNode.id > 0 &&
!PlacesUtils.isRootItem(aUnwrappedNode.id) &&
(!aUnwrappedNode.parent || !PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent)) &&
aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId;
},
/**
* Determines if a node can be moved.
*
* @param aNode
* A nsINavHistoryResultNode node.
* @param [optional] aDOMNode
* A XUL DOM node.
* @return True if the node can be moved, false otherwise.
*/
canMoveNode(aNode, aDOMNode) {
// Only bookmark items are movable.
if (aNode.itemId == -1)
return false;
let parentNode = aNode.parent;
if (!parentNode) {
// Normally parentless places nodes can not be moved,
// but simulated bookmarked URI nodes are special.
return !!aDOMNode &&
aDOMNode.hasAttribute("simulated-places-node") &&
PlacesUtils.nodeIsBookmark(aNode);
}
// Once tags and bookmarked are divorced, the tag-query check should be
// removed.
return !(PlacesUtils.nodeIsFolder(parentNode) &&
PlacesUIUtils.isContentsReadOnly(parentNode)) &&
!PlacesUtils.nodeIsTagQuery(parentNode);
},
/**
* Handles the drop of one or more items onto a view.
* @param insertionPoint
* The insertion point where the items should be dropped
*/
onDrop: Task.async(function* (insertionPoint, dt) {
let doCopy = ["copy", "link"].includes(dt.dropEffect);
let transactions = [];
let dropCount = dt.mozItemCount;
let movedCount = 0;
let parentGuid = PlacesUIUtils.useAsyncTransactions ?
(yield insertionPoint.promiseGuid()) : null;
let tagName = insertionPoint.tagName;
for (let i = 0; i < dropCount; ++i) {
let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
if (!flavor)
return;
let data = dt.mozGetDataAt(flavor, i);
let unwrapped;
if (flavor != TAB_DROP_TYPE) {
// There's only ever one in the D&D case.
unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
}
else if (data instanceof XULElement && data.localName == "tab" &&
data.ownerGlobal instanceof ChromeWindow) {
let uri = data.linkedBrowser.currentURI;
let spec = uri ? uri.spec : "about:blank";
unwrapped = { uri: spec,
title: data.label,
type: PlacesUtils.TYPE_X_MOZ_URL};
}
else
throw new Error("bogus data was passed as a tab");
let index = insertionPoint.index;
// Adjust insertion index to prevent reversal of dragged items. When you
// drag multiple elts upward: need to increment index or each successive
// elt will be inserted at the same index, each above the previous.
let dragginUp = insertionPoint.itemId == unwrapped.parent &&
index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
if (index != -1 && dragginUp)
index+= movedCount++;
// If dragging over a tag container we should tag the item.
if (insertionPoint.isTag) {
let uri = NetUtil.newURI(unwrapped.uri);
let tagItemId = insertionPoint.itemId;
if (PlacesUIUtils.useAsyncTransactions)
transactions.push(PlacesTransactions.Tag({ uri: uri, tag: tagName }));
else
transactions.push(new PlacesTagURITransaction(uri, [tagItemId]));
}
else {
// If this is not a copy, check for safety that we can move the source,
// otherwise report an error and fallback to a copy.
if (!doCopy && !PlacesControllerDragHelper.canMoveUnwrappedNode(unwrapped)) {
Components.utils.reportError("Tried to move an unmovable Places " +
"node, reverting to a copy operation.");
doCopy = true;
}
if (PlacesUIUtils.useAsyncTransactions) {
transactions.push(
PlacesUIUtils.getTransactionForData(unwrapped,
flavor,
parentGuid,
index,
doCopy));
}
else {
transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
flavor, insertionPoint.itemId,
index, doCopy));
}
}
}
if (PlacesUIUtils.useAsyncTransactions) {
yield PlacesTransactions.batch(transactions);
}
else {
let txn = new PlacesAggregatedTransaction("DropItems", transactions);
PlacesUtils.transactionManager.doTransaction(txn);
}
}),
/**
* Checks if we can insert into a container.
* @param aContainer
* The container were we are want to drop
*/
disallowInsertion: function(aContainer) {
NS_ASSERT(aContainer, "empty container");
// Allow dropping into Tag containers and editable folders.
return !PlacesUtils.nodeIsTagQuery(aContainer) &&
(!PlacesUtils.nodeIsFolder(aContainer) ||
PlacesUIUtils.isContentsReadOnly(aContainer));
}
};
XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
"@mozilla.org/widget/dragservice;1",
"nsIDragService");
function goUpdatePlacesCommands() {
// Get the controller for one of the places commands.
var placesController = doGetPlacesControllerForCommand("placesCmd_open");
function updatePlacesCommand(aCommand) {
goSetCommandEnabled(aCommand, placesController &&
placesController.isCommandEnabled(aCommand));
}
updatePlacesCommand("placesCmd_open");
updatePlacesCommand("placesCmd_open:window");
updatePlacesCommand("placesCmd_open:privatewindow");
updatePlacesCommand("placesCmd_open:tab");
updatePlacesCommand("placesCmd_new:folder");
updatePlacesCommand("placesCmd_new:bookmark");
updatePlacesCommand("placesCmd_new:separator");
updatePlacesCommand("placesCmd_show:info");
updatePlacesCommand("placesCmd_moveBookmarks");
updatePlacesCommand("placesCmd_reload");
updatePlacesCommand("placesCmd_sortBy:name");
updatePlacesCommand("placesCmd_cut");
updatePlacesCommand("placesCmd_copy");
updatePlacesCommand("placesCmd_paste");
updatePlacesCommand("placesCmd_delete");
}
function doGetPlacesControllerForCommand(aCommand)
{
// A context menu may be built for non-focusable views. Thus, we first try
// to look for a view associated with document.popupNode
let popupNode;
try {
popupNode = document.popupNode;
} catch (e) {
// The document went away (bug 797307).
return null;
}
if (popupNode) {
let view = PlacesUIUtils.getViewForNode(popupNode);
if (view && view._contextMenuShown)
return view.controllers.getControllerForCommand(aCommand);
}
// When we're not building a context menu, only focusable views
// are possible. Thus, we can safely use the command dispatcher.
let controller = top.document.commandDispatcher
.getControllerForCommand(aCommand);
if (controller)
return controller;
return null;
}
function goDoPlacesCommand(aCommand)
{
let controller = doGetPlacesControllerForCommand(aCommand);
if (controller && controller.isCommandEnabled(aCommand))
controller.doCommand(aCommand);
}