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

1997 lines
66 KiB
JavaScript

/* 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/. */
Components.utils.import("resource://gre/modules/AppConstants.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
/**
* The base view implements everything that's common to the toolbar and
* menu views.
*/
function PlacesViewBase(aPlace, aOptions) {
this.place = aPlace;
this.options = aOptions;
this._controller = new PlacesController(this);
this._viewElt.controllers.appendController(this._controller);
}
PlacesViewBase.prototype = {
// The xul element that holds the entire view.
_viewElt: null,
get viewElt() {
return this._viewElt;
},
get associatedElement() {
return this._viewElt;
},
get controllers() {
return this._viewElt.controllers;
},
// The xul element that represents the root container.
_rootElt: null,
// Set to true for views that are represented by native widgets (i.e.
// the native mac menu).
_nativeView: false,
QueryInterface: XPCOMUtils.generateQI(
[Components.interfaces.nsINavHistoryResultObserver,
Components.interfaces.nsISupportsWeakReference]),
_place: "",
get place() {
return this._place;
},
set place(val) {
this._place = val;
let history = PlacesUtils.history;
let queries = { }, options = { };
history.queryStringToQueries(val, queries, { }, options);
if (!queries.value.length)
queries.value = [history.getNewQuery()];
let result = history.executeQueries(queries.value, queries.value.length,
options.value);
result.addObserver(this, false);
return val;
},
_result: null,
get result() {
return this._result;
},
set result(val) {
if (this._result == val)
return val;
if (this._result) {
this._result.removeObserver(this);
this._resultNode.containerOpen = false;
}
if (this._rootElt.localName == "menupopup")
this._rootElt._built = false;
this._result = val;
if (val) {
this._resultNode = val.root;
this._rootElt._placesNode = this._resultNode;
this._domNodes = new Map();
this._domNodes.set(this._resultNode, this._rootElt);
// This calls _rebuild through invalidateContainer.
this._resultNode.containerOpen = true;
}
else {
this._resultNode = null;
delete this._domNodes;
}
return val;
},
_options: null,
get options() {
return this._options;
},
set options(val) {
if (!val)
val = {};
if (!("extraClasses" in val))
val.extraClasses = {};
this._options = val;
return val;
},
/**
* Gets the DOM node used for the given places node.
*
* @param aPlacesNode
* a places result node.
* @throws if there is no DOM node set for aPlacesNode.
*/
_getDOMNodeForPlacesNode:
function PVB__getDOMNodeForPlacesNode(aPlacesNode) {
let node = this._domNodes.get(aPlacesNode, null);
if (!node) {
throw new Error("No DOM node set for aPlacesNode.\nnode.type: " +
aPlacesNode.type + ". node.parent: " + aPlacesNode);
}
return node;
},
get controller() {
return this._controller;
},
get selType() {
return "single";
},
selectItems: function() { },
selectAll: function() { },
get selectedNode() {
if (this._contextMenuShown) {
let anchor = this._contextMenuShown.triggerNode;
if (!anchor)
return null;
if (anchor._placesNode)
return this._rootElt == anchor ? null : anchor._placesNode;
anchor = anchor.parentNode;
return this._rootElt == anchor ? null : (anchor._placesNode || null);
}
return null;
},
get hasSelection() {
return this.selectedNode != null;
},
get selectedNodes() {
let selectedNode = this.selectedNode;
return selectedNode ? [selectedNode] : [];
},
get removableSelectionRanges() {
// On static content the current selectedNode would be the selection's
// parent node. We don't want to allow removing a node when the
// selection is not explicit.
if (document.popupNode &&
(document.popupNode == "menupopup" || !document.popupNode._placesNode))
return [];
return [this.selectedNodes];
},
get draggableSelection() {
return [this._draggedElt];
},
get insertionPoint() {
// There is no insertion point for history queries, so bail out now and
// save a lot of work when updating commands.
let resultNode = this._resultNode;
if (PlacesUtils.nodeIsQuery(resultNode) &&
PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
return null;
// By default, the insertion point is at the top level, at the end.
let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
let container = this._resultNode;
let orientation = Ci.nsITreeView.DROP_BEFORE;
let tagName = null;
let selectedNode = this.selectedNode;
if (selectedNode) {
let popup = document.popupNode;
if (!popup._placesNode || popup._placesNode == this._resultNode ||
popup._placesNode.itemId == -1 || !selectedNode.parent) {
// If a static menuitem is selected, or if the root node is selected,
// the insertion point is inside the folder, at the end.
container = selectedNode;
orientation = Ci.nsITreeView.DROP_ON;
}
else {
// In all other cases the insertion point is before that node.
container = selectedNode.parent;
index = container.getChildIndex(selectedNode);
if (PlacesUtils.nodeIsTagQuery(container)) {
tagName = container.title;
// TODO (Bug 1160193): properly support dropping on a tag root.
if (!tagName)
return null;
}
}
}
if (PlacesControllerDragHelper.disallowInsertion(container))
return null;
return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
index, orientation, tagName);
},
buildContextMenu: function(aPopup) {
this._contextMenuShown = aPopup;
window.updateCommands("places");
return this.controller.buildContextMenu(aPopup);
},
destroyContextMenu: function(aPopup) {
this._contextMenuShown = null;
},
_cleanPopup: function(aPopup, aDelay) {
// Remove Places nodes from the popup.
let child = aPopup._startMarker;
while (child.nextSibling != aPopup._endMarker) {
let sibling = child.nextSibling;
if (sibling._placesNode && !aDelay) {
aPopup.removeChild(sibling);
}
else if (sibling._placesNode && aDelay) {
// HACK (bug 733419): the popups originating from the OS X native
// menubar don't live-update while open, thus we don't clean it
// until the next popupshowing, to avoid zombie menuitems.
if (!aPopup._delayedRemovals)
aPopup._delayedRemovals = [];
aPopup._delayedRemovals.push(sibling);
child = child.nextSibling;
}
else {
child = child.nextSibling;
}
}
},
_rebuildPopup: function(aPopup) {
let resultNode = aPopup._placesNode;
if (!resultNode.containerOpen)
return;
if (this.controller.hasCachedLivemarkInfo(resultNode)) {
this._setEmptyPopupStatus(aPopup, false);
aPopup._built = true;
this._populateLivemarkPopup(aPopup);
return;
}
this._cleanPopup(aPopup);
let cc = resultNode.childCount;
if (cc > 0) {
this._setEmptyPopupStatus(aPopup, false);
for (let i = 0; i < cc; ++i) {
let child = resultNode.getChild(i);
this._insertNewItemToPopup(child, aPopup, null);
}
}
else {
this._setEmptyPopupStatus(aPopup, true);
}
aPopup._built = true;
},
_removeChild: function(aChild) {
// If document.popupNode pointed to this child, null it out,
// otherwise controller's command-updating may rely on the removed
// item still being "selected".
if (document.popupNode == aChild)
document.popupNode = null;
aChild.parentNode.removeChild(aChild);
},
_setEmptyPopupStatus:
function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
if (!aPopup._emptyMenuitem) {
let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
aPopup._emptyMenuitem = document.createElement("menuitem");
aPopup._emptyMenuitem.setAttribute("label", label);
aPopup._emptyMenuitem.setAttribute("disabled", true);
aPopup._emptyMenuitem.className = "bookmark-item";
if (typeof this.options.extraClasses.entry == "string")
aPopup._emptyMenuitem.classList.add(this.options.extraClasses.entry);
}
if (aEmpty) {
aPopup.setAttribute("emptyplacesresult", "true");
// Don't add the menuitem if there is static content.
if (!aPopup._startMarker.previousSibling &&
!aPopup._endMarker.nextSibling)
aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
}
else {
aPopup.removeAttribute("emptyplacesresult");
try {
aPopup.removeChild(aPopup._emptyMenuitem);
} catch (ex) {}
}
},
_createMenuItemForPlacesNode:
function PVB__createMenuItemForPlacesNode(aPlacesNode) {
this._domNodes.delete(aPlacesNode);
let element;
let type = aPlacesNode.type;
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
element = document.createElement("menuseparator");
element.setAttribute("class", "small-separator");
}
else {
let itemId = aPlacesNode.itemId;
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
element = document.createElement("menuitem");
element.className = "menuitem-iconic bookmark-item menuitem-with-favicon";
element.setAttribute("scheme",
PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
}
else if (PlacesUtils.containerTypes.includes(type)) {
element = document.createElement("menu");
element.setAttribute("container", "true");
if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
element.setAttribute("query", "true");
if (PlacesUtils.nodeIsTagQuery(aPlacesNode))
element.setAttribute("tagContainer", "true");
else if (PlacesUtils.nodeIsDay(aPlacesNode))
element.setAttribute("dayContainer", "true");
else if (PlacesUtils.nodeIsHost(aPlacesNode))
element.setAttribute("hostContainer", "true");
}
else if (itemId != -1) {
PlacesUtils.livemarks.getLivemark({ id: itemId })
.then(aLivemark => {
element.setAttribute("livemark", "true");
if (AppConstants.platform === "macosx") {
// OS X native menubar doesn't track list-style-images since
// it doesn't have a frame (bug 733415). Thus enforce updating.
element.setAttribute("image", "");
element.removeAttribute("image");
}
this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
}, () => undefined);
}
let popup = document.createElement("menupopup");
popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
if (!this._nativeView) {
popup.setAttribute("placespopup", "true");
}
element.appendChild(popup);
element.className = "menu-iconic bookmark-item";
if (typeof this.options.extraClasses.entry == "string") {
element.classList.add(this.options.extraClasses.entry);
}
this._domNodes.set(aPlacesNode, popup);
}
else
throw "Unexpected node";
element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
let icon = aPlacesNode.icon;
if (icon)
element.setAttribute("image", icon);
}
element._placesNode = aPlacesNode;
if (!this._domNodes.has(aPlacesNode))
this._domNodes.set(aPlacesNode, element);
return element;
},
_insertNewItemToPopup:
function PVB__insertNewItemToPopup(aNewChild, aPopup, aBefore) {
let element = this._createMenuItemForPlacesNode(aNewChild);
let before = aBefore || aPopup._endMarker;
if (element.localName == "menuitem" || element.localName == "menu") {
if (typeof this.options.extraClasses.entry == "string")
element.classList.add(this.options.extraClasses.entry);
}
aPopup.insertBefore(element, before);
return element;
},
_setLivemarkSiteURIMenuItem:
function PVB__setLivemarkSiteURIMenuItem(aPopup) {
let livemarkInfo = this.controller.getCachedLivemarkInfo(aPopup._placesNode);
let siteUrl = livemarkInfo && livemarkInfo.siteURI ?
livemarkInfo.siteURI.spec : null;
if (!siteUrl && aPopup._siteURIMenuitem) {
aPopup.removeChild(aPopup._siteURIMenuitem);
aPopup._siteURIMenuitem = null;
aPopup.removeChild(aPopup._siteURIMenuseparator);
aPopup._siteURIMenuseparator = null;
}
else if (siteUrl && !aPopup._siteURIMenuitem) {
// Add "Open (Feed Name)" menuitem.
aPopup._siteURIMenuitem = document.createElement("menuitem");
aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem";
if (typeof this.options.extraClasses.entry == "string") {
aPopup._siteURIMenuitem.classList.add(this.options.extraClasses.entry);
}
aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl);
aPopup._siteURIMenuitem.setAttribute("oncommand",
"openUILink(this.getAttribute('targetURI'), event);");
// If a user middle-clicks this item we serve the oncommand event.
// We are using checkForMiddleClick because of Bug 246720.
// Note: stopPropagation is needed to avoid serving middle-click
// with BT_onClick that would open all items in tabs.
aPopup._siteURIMenuitem.setAttribute("onclick",
"checkForMiddleClick(this, event); event.stopPropagation();");
let label =
PlacesUIUtils.getFormattedString("menuOpenLivemarkOrigin.label",
[aPopup.parentNode.getAttribute("label")])
aPopup._siteURIMenuitem.setAttribute("label", label);
aPopup.insertBefore(aPopup._siteURIMenuitem, aPopup._startMarker);
aPopup._siteURIMenuseparator = document.createElement("menuseparator");
aPopup.insertBefore(aPopup._siteURIMenuseparator, aPopup._startMarker);
}
},
/**
* Add, update or remove the livemark status menuitem.
* @param aPopup
* The livemark container popup
* @param aStatus
* The livemark status
*/
_setLivemarkStatusMenuItem:
function PVB_setLivemarkStatusMenuItem(aPopup, aStatus) {
let statusMenuitem = aPopup._statusMenuitem;
if (!statusMenuitem) {
// Create the status menuitem and cache it in the popup object.
statusMenuitem = document.createElement("menuitem");
statusMenuitem.className = "livemarkstatus-menuitem";
if (typeof this.options.extraClasses.entry == "string") {
statusMenuitem.classList.add(this.options.extraClasses.entry);
}
statusMenuitem.setAttribute("disabled", true);
aPopup._statusMenuitem = statusMenuitem;
}
if (aStatus == Ci.mozILivemark.STATUS_LOADING ||
aStatus == Ci.mozILivemark.STATUS_FAILED) {
// Status has changed, update the cached status menuitem.
let stringId = aStatus == Ci.mozILivemark.STATUS_LOADING ?
"bookmarksLivemarkLoading" : "bookmarksLivemarkFailed";
statusMenuitem.setAttribute("label", PlacesUIUtils.getString(stringId));
if (aPopup._startMarker.nextSibling != statusMenuitem)
aPopup.insertBefore(statusMenuitem, aPopup._startMarker.nextSibling);
}
else if (aPopup._statusMenuitem.parentNode == aPopup) {
// The livemark has finished loading.
aPopup.removeChild(aPopup._statusMenuitem);
}
},
toggleCutNode: function(aPlacesNode, aValue) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// We may get the popup for menus, but we need the menu itself.
if (elt.localName == "menupopup")
elt = elt.parentNode;
if (aValue)
elt.setAttribute("cutting", "true");
else
elt.removeAttribute("cutting");
},
nodeURIChanged: function(aPlacesNode, aURIString) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// Here we need the <menu>.
if (elt.localName == "menupopup")
elt = elt.parentNode;
elt.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(aURIString));
},
nodeIconChanged: function(aPlacesNode) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// There's no UI representation for the root node, thus there's nothing to
// be done when the icon changes.
if (elt == this._rootElt)
return;
// Here we need the <menu>.
if (elt.localName == "menupopup")
elt = elt.parentNode;
let icon = aPlacesNode.icon;
if (!icon)
elt.removeAttribute("image");
else if (icon != elt.getAttribute("image"))
elt.setAttribute("image", icon);
},
nodeAnnotationChanged:
function PVB_nodeAnnotationChanged(aPlacesNode, aAnno) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// All livemarks have a feedURI, so use it as our indicator of a livemark
// being modified.
if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
let menu = elt.parentNode;
if (!menu.hasAttribute("livemark")) {
menu.setAttribute("livemark", "true");
if (AppConstants.platform === "macosx") {
// OS X native menubar doesn't track list-style-images since
// it doesn't have a frame (bug 733415). Thus enforce updating.
menu.setAttribute("image", "");
menu.removeAttribute("image");
}
}
PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
.then(aLivemark => {
// Controller will use this to build the meta data for the node.
this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
this.invalidateContainer(aPlacesNode);
}, () => undefined);
}
},
nodeTitleChanged:
function PVB_nodeTitleChanged(aPlacesNode, aNewTitle) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// There's no UI representation for the root node, thus there's
// nothing to be done when the title changes.
if (elt == this._rootElt)
return;
// Here we need the <menu>.
if (elt.localName == "menupopup")
elt = elt.parentNode;
if (!aNewTitle && elt.localName != "toolbarbutton") {
// Many users consider toolbars as shortcuts containers, so explicitly
// allow empty labels on toolbarbuttons. For any other element try to be
// smarter, guessing a title from the uri.
elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
}
else {
elt.setAttribute("label", aNewTitle);
}
},
nodeRemoved:
function PVB_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// Here we need the <menu>.
if (elt.localName == "menupopup")
elt = elt.parentNode;
if (parentElt._built) {
parentElt.removeChild(elt);
// Figure out if we need to show the "<Empty>" menu-item.
// TODO Bug 517701: This doesn't seem to handle the case of an empty
// root.
if (parentElt._startMarker.nextSibling == parentElt._endMarker)
this._setEmptyPopupStatus(parentElt, true);
}
},
nodeHistoryDetailsChanged:
function PVB_nodeHistoryDetailsChanged(aPlacesNode, aTime, aCount) {
if (aPlacesNode.parent &&
this.controller.hasCachedLivemarkInfo(aPlacesNode.parent)) {
// Find the node in the parent.
let popup = this._getDOMNodeForPlacesNode(aPlacesNode.parent);
for (let child = popup._startMarker.nextSibling;
child != popup._endMarker;
child = child.nextSibling) {
if (child._placesNode && child._placesNode.uri == aPlacesNode.uri) {
if (aCount)
child.setAttribute("visited", "true");
else
child.removeAttribute("visited");
break;
}
}
}
},
nodeTagsChanged: function() { },
nodeDateAddedChanged: function() { },
nodeLastModifiedChanged: function() { },
nodeKeywordChanged: function() { },
sortingChanged: function() { },
batching: function() { },
nodeInserted:
function PVB_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
if (!parentElt._built)
return;
let index = Array.prototype.indexOf.call(parentElt.childNodes, parentElt._startMarker) +
aIndex + 1;
this._insertNewItemToPopup(aPlacesNode, parentElt,
parentElt.childNodes[index]);
this._setEmptyPopupStatus(parentElt, false);
},
nodeMoved:
function PBV_nodeMoved(aPlacesNode,
aOldParentPlacesNode, aOldIndex,
aNewParentPlacesNode, aNewIndex) {
// Note: the current implementation of moveItem does not actually
// use this notification when the item in question is moved from one
// folder to another. Instead, it calls nodeRemoved and nodeInserted
// for the two folders. Thus, we can assume old-parent == new-parent.
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// Here we need the <menu>.
if (elt.localName == "menupopup")
elt = elt.parentNode;
// If our root node is a folder, it might be moved. There's nothing
// we need to do in that case.
if (elt == this._rootElt)
return;
let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
if (parentElt._built) {
// Move the node.
parentElt.removeChild(elt);
let index = Array.prototype.indexOf.call(parentElt.childNodes, parentElt._startMarker) +
aNewIndex + 1;
parentElt.insertBefore(elt, parentElt.childNodes[index]);
}
},
containerStateChanged:
function PVB_containerStateChanged(aPlacesNode, aOldState, aNewState) {
if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED ||
aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) {
this.invalidateContainer(aPlacesNode);
if (PlacesUtils.nodeIsFolder(aPlacesNode)) {
let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
if (queryOptions.excludeItems) {
return;
}
PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
.then(aLivemark => {
let shouldInvalidate =
!this.controller.hasCachedLivemarkInfo(aPlacesNode);
this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
aLivemark.registerForUpdates(aPlacesNode, this);
// Prioritize the current livemark.
aLivemark.reload();
PlacesUtils.livemarks.reloadLivemarks();
if (shouldInvalidate)
this.invalidateContainer(aPlacesNode);
}
else {
aLivemark.unregisterForUpdates(aPlacesNode);
}
}, () => undefined);
}
}
},
_populateLivemarkPopup: function(aPopup)
{
this._setLivemarkSiteURIMenuItem(aPopup);
// Show the loading status only if there are no entries yet.
if (aPopup._startMarker.nextSibling == aPopup._endMarker)
this._setLivemarkStatusMenuItem(aPopup, Ci.mozILivemark.STATUS_LOADING);
PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId })
.then(aLivemark => {
let placesNode = aPopup._placesNode;
if (!placesNode.containerOpen)
return;
if (aLivemark.status != Ci.mozILivemark.STATUS_LOADING)
this._setLivemarkStatusMenuItem(aPopup, aLivemark.status);
this._cleanPopup(aPopup,
this._nativeView && aPopup.parentNode.hasAttribute("open"));
let children = aLivemark.getNodesForContainer(placesNode);
for (let i = 0; i < children.length; i++) {
let child = children[i];
this.nodeInserted(placesNode, child, i);
if (child.accessCount)
this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
else
this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
}
}, Components.utils.reportError);
},
invalidateContainer: function(aPlacesNode) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
elt._built = false;
// If the menupopup is open we should live-update it.
if (elt.parentNode.open)
this._rebuildPopup(elt);
},
uninit: function() {
if (this._result) {
this._result.removeObserver(this);
this._resultNode.containerOpen = false;
this._resultNode = null;
this._result = null;
}
if (this._controller) {
this._controller.terminate();
// Removing the controller will fail if it is already no longer there.
// This can happen if the view element was removed/reinserted without
// our knowledge. There is no way to check for that having happened
// without the possibility of an exception. :-(
try {
this._viewElt.controllers.removeController(this._controller);
} catch (ex) {
} finally {
this._controller = null;
}
}
delete this._viewElt._placesView;
},
get isRTL() {
if ("_isRTL" in this)
return this._isRTL;
return this._isRTL = document.defaultView
.getComputedStyle(this.viewElt, "")
.direction == "rtl";
},
get ownerWindow() {
return window;
},
/**
* Adds an "Open All in Tabs" menuitem to the bottom of the popup.
* @param aPopup
* a Places popup.
*/
_mayAddCommandsItems: function(aPopup) {
// The command items are never added to the root popup.
if (aPopup == this._rootElt)
return;
let hasMultipleURIs = false;
// Check if the popup contains at least 2 menuitems with places nodes.
// We don't currently support opening multiple uri nodes when they are not
// populated by the result.
if (aPopup._placesNode.childCount > 0) {
let currentChild = aPopup.firstChild;
let numURINodes = 0;
while (currentChild) {
if (currentChild.localName == "menuitem" && currentChild._placesNode) {
if (++numURINodes == 2)
break;
}
currentChild = currentChild.nextSibling;
}
hasMultipleURIs = numURINodes > 1;
}
let isLiveMark = false;
if (this.controller.hasCachedLivemarkInfo(aPopup._placesNode)) {
hasMultipleURIs = true;
isLiveMark = true;
}
if (!hasMultipleURIs) {
aPopup.setAttribute("singleitempopup", "true");
} else {
aPopup.removeAttribute("singleitempopup");
}
if (!hasMultipleURIs) {
// We don't have to show any option.
if (aPopup._endOptOpenAllInTabs) {
aPopup.removeChild(aPopup._endOptOpenAllInTabs);
aPopup._endOptOpenAllInTabs = null;
aPopup.removeChild(aPopup._endOptSeparator);
aPopup._endOptSeparator = null;
}
}
else if (!aPopup._endOptOpenAllInTabs) {
// Create a separator before options.
aPopup._endOptSeparator = document.createElement("menuseparator");
aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator";
aPopup.appendChild(aPopup._endOptSeparator);
// Add the "Open All in Tabs" menuitem.
aPopup._endOptOpenAllInTabs = document.createElement("menuitem");
aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem";
if (typeof this.options.extraClasses.entry == "string")
aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.entry);
if (typeof this.options.extraClasses.footer == "string")
aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.footer);
if (isLiveMark) {
aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
"PlacesUIUtils.openLiveMarkNodesInTabs(this.parentNode._placesNode, event, " +
"PlacesUIUtils.getViewForNode(this));");
} else {
aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
"PlacesUIUtils.openContainerNodeInTabs(this.parentNode._placesNode, event, " +
"PlacesUIUtils.getViewForNode(this));");
}
aPopup._endOptOpenAllInTabs.setAttribute("onclick",
"checkForMiddleClick(this, event); event.stopPropagation();");
aPopup._endOptOpenAllInTabs.setAttribute("label",
gNavigatorBundle.getString("menuOpenAllInTabs.label"));
aPopup.appendChild(aPopup._endOptOpenAllInTabs);
}
},
_ensureMarkers: function(aPopup) {
if (aPopup._startMarker)
return;
// _startMarker is an hidden menuseparator that lives before places nodes.
aPopup._startMarker = document.createElement("menuseparator");
aPopup._startMarker.hidden = true;
aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild);
// _endMarker is a DOM node that lives after places nodes, specified with
// the 'insertionPoint' option or will be a hidden menuseparator.
let node = ("insertionPoint" in this.options) ?
aPopup.querySelector(this.options.insertionPoint) : null;
if (node) {
aPopup._endMarker = node;
} else {
aPopup._endMarker = document.createElement("menuseparator");
aPopup._endMarker.hidden = true;
}
aPopup.appendChild(aPopup._endMarker);
// Move the markers to the right position.
let firstNonStaticNodeFound = false;
for (let i = 0; i < aPopup.childNodes.length; i++) {
let child = aPopup.childNodes[i];
// Menus that have static content at the end, but are initially empty,
// use a special "builder" attribute to figure out where to start
// inserting places nodes.
if (child.getAttribute("builder") == "end") {
aPopup.insertBefore(aPopup._endMarker, child);
break;
}
if (child._placesNode && !child.hasAttribute("simulated-places-node") &&
!firstNonStaticNodeFound) {
firstNonStaticNodeFound = true;
aPopup.insertBefore(aPopup._startMarker, child);
}
}
if (!firstNonStaticNodeFound) {
aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker);
}
},
_onPopupShowing: function(aEvent) {
// Avoid handling popupshowing of inner views.
let popup = aEvent.originalTarget;
this._ensureMarkers(popup);
// Remove any delayed element, see _cleanPopup for details.
if ("_delayedRemovals" in popup) {
while (popup._delayedRemovals.length > 0) {
popup.removeChild(popup._delayedRemovals.shift());
}
}
if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
if (!popup._placesNode.containerOpen)
popup._placesNode.containerOpen = true;
if (!popup._built)
this._rebuildPopup(popup);
this._mayAddCommandsItems(popup);
}
},
_addEventListeners:
function PVB__addEventListeners(aObject, aEventNames, aCapturing) {
for (let i = 0; i < aEventNames.length; i++) {
aObject.addEventListener(aEventNames[i], this, aCapturing);
}
},
_removeEventListeners:
function PVB__removeEventListeners(aObject, aEventNames, aCapturing) {
for (let i = 0; i < aEventNames.length; i++) {
aObject.removeEventListener(aEventNames[i], this, aCapturing);
}
},
};
function PlacesToolbar(aPlace) {
let startTime = Date.now();
// Add some smart getters for our elements.
let thisView = this;
[
["_viewElt", "PlacesToolbar"],
["_rootElt", "PlacesToolbarItems"],
["_dropIndicator", "PlacesToolbarDropIndicator"],
["_chevron", "PlacesChevron"],
["_chevronPopup", "PlacesChevronPopup"]
].forEach(function (elementGlobal) {
let [name, id] = elementGlobal;
thisView.__defineGetter__(name, function () {
let element = document.getElementById(id);
if (!element)
return null;
delete thisView[name];
return thisView[name] = element;
});
});
this._viewElt._placesView = this;
this._addEventListeners(this._viewElt, this._cbEvents, false);
this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
this._addEventListeners(this._rootElt, ["overflow", "underflow"], true);
this._addEventListeners(window, ["resize", "unload"], false);
// If personal-bookmarks has been dragged to the tabs toolbar,
// we have to track addition and removals of tabs, to properly
// recalculate the available space for bookmarks.
// TODO (bug 734730): Use a performant mutation listener when available.
if (this._viewElt.parentNode.parentNode == document.getElementById("TabsToolbar")) {
this._addEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
}
PlacesViewBase.call(this, aPlace);
Services.telemetry.getHistogramById("FX_BOOKMARKS_TOOLBAR_INIT_MS")
.add(Date.now() - startTime);
}
PlacesToolbar.prototype = {
__proto__: PlacesViewBase.prototype,
_cbEvents: ["dragstart", "dragover", "dragexit", "dragend", "drop",
"mousemove", "mouseover", "mouseout"],
QueryInterface: function(aIID) {
if (aIID.equals(Ci.nsIDOMEventListener) ||
aIID.equals(Ci.nsITimerCallback))
return this;
return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
},
uninit: function() {
this._removeEventListeners(this._viewElt, this._cbEvents, false);
this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
true);
this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
this._removeEventListeners(window, ["resize", "unload"], false);
this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
if (this._chevron._placesView) {
this._chevron._placesView.uninit();
}
PlacesViewBase.prototype.uninit.apply(this, arguments);
},
_openedMenuButton: null,
_allowPopupShowing: true,
_rebuild: function() {
// Clear out references to existing nodes, since they will be removed
// and re-added.
if (this._overFolder.elt)
this._clearOverFolder();
this._openedMenuButton = null;
while (this._rootElt.hasChildNodes()) {
this._rootElt.removeChild(this._rootElt.firstChild);
}
let cc = this._resultNode.childCount;
for (let i = 0; i < cc; ++i) {
this._insertNewItem(this._resultNode.getChild(i), null);
}
if (this._chevronPopup.hasAttribute("type")) {
// Chevron has already been initialized, but since we are forcing
// a rebuild of the toolbar, it has to be rebuilt.
// Otherwise, it will be initialized when the toolbar overflows.
this._chevronPopup.place = this.place;
}
},
_insertNewItem:
function PT__insertNewItem(aChild, aBefore) {
this._domNodes.delete(aChild);
let type = aChild.type;
let button;
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
button = document.createElement("toolbarseparator");
}
else {
button = document.createElement("toolbarbutton");
button.className = "bookmark-item";
button.setAttribute("label", aChild.title || "");
let icon = aChild.icon;
if (icon)
button.setAttribute("image", icon);
if (PlacesUtils.containerTypes.includes(type)) {
button.setAttribute("type", "menu");
button.setAttribute("container", "true");
if (PlacesUtils.nodeIsQuery(aChild)) {
button.setAttribute("query", "true");
if (PlacesUtils.nodeIsTagQuery(aChild))
button.setAttribute("tagContainer", "true");
}
else if (PlacesUtils.nodeIsFolder(aChild)) {
PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
.then(aLivemark => {
button.setAttribute("livemark", "true");
this.controller.cacheLivemarkInfo(aChild, aLivemark);
}, () => undefined);
}
let popup = document.createElement("menupopup");
popup.setAttribute("placespopup", "true");
button.appendChild(popup);
popup._placesNode = PlacesUtils.asContainer(aChild);
popup.setAttribute("context", "placesContext");
this._domNodes.set(aChild, popup);
}
else if (PlacesUtils.nodeIsURI(aChild)) {
button.setAttribute("scheme",
PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
}
}
button._placesNode = aChild;
if (!this._domNodes.has(aChild))
this._domNodes.set(aChild, button);
if (aBefore)
this._rootElt.insertBefore(button, aBefore);
else
this._rootElt.appendChild(button);
},
_updateChevronPopupNodesVisibility:
function PT__updateChevronPopupNodesVisibility() {
for (let i = 0, node = this._chevronPopup._startMarker.nextSibling;
node != this._chevronPopup._endMarker;
i++, node = node.nextSibling) {
node.hidden = this._rootElt.childNodes[i].style.visibility != "hidden";
}
},
_onChevronPopupShowing:
function PT__onChevronPopupShowing(aEvent) {
// Handle popupshowing only for the chevron popup, not for nested ones.
if (aEvent.target != this._chevronPopup)
return;
if (!this._chevron._placesView)
this._chevron._placesView = new PlacesMenu(aEvent, this.place);
this._updateChevronPopupNodesVisibility();
},
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "unload":
this.uninit();
break;
case "resize":
// This handler updates nodes visibility in both the toolbar
// and the chevron popup when a window resize does not change
// the overflow status of the toolbar.
this.updateChevron();
break;
case "overflow":
if (!this._isOverflowStateEventRelevant(aEvent))
return;
this._onOverflow();
break;
case "underflow":
if (!this._isOverflowStateEventRelevant(aEvent))
return;
this._onUnderflow();
break;
case "TabOpen":
case "TabClose":
this.updateChevron();
break;
case "dragstart":
this._onDragStart(aEvent);
break;
case "dragover":
this._onDragOver(aEvent);
break;
case "dragexit":
this._onDragExit(aEvent);
break;
case "dragend":
this._onDragEnd(aEvent);
break;
case "drop":
this._onDrop(aEvent);
break;
case "mouseover":
this._onMouseOver(aEvent);
break;
case "mousemove":
this._onMouseMove(aEvent);
break;
case "mouseout":
this._onMouseOut(aEvent);
break;
case "popupshowing":
this._onPopupShowing(aEvent);
break;
case "popuphidden":
this._onPopupHidden(aEvent);
break;
default:
throw "Trying to handle unexpected event.";
}
},
updateOverflowStatus: function() {
if (this._rootElt.scrollLeftMin != this._rootElt.scrollLeftMax) {
this._onOverflow();
} else {
this._onUnderflow();
}
},
_isOverflowStateEventRelevant: function(aEvent) {
// Ignore events not aimed at ourselves, as well as purely vertical ones:
return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
},
_onOverflow: function() {
// Attach the popup binding to the chevron popup if it has not yet
// been initialized.
if (!this._chevronPopup.hasAttribute("type")) {
this._chevronPopup.setAttribute("place", this.place);
this._chevronPopup.setAttribute("type", "places");
}
this._chevron.collapsed = false;
this.updateChevron();
},
_onUnderflow: function() {
this.updateChevron();
this._chevron.collapsed = true;
},
updateChevron: function() {
// If the chevron is collapsed there's nothing to update.
if (this._chevron.collapsed)
return;
// Update the chevron on a timer. This will avoid repeated work when
// lot of changes happen in a small timeframe.
if (this._updateChevronTimer)
this._updateChevronTimer.cancel();
this._updateChevronTimer = this._setTimer(100);
},
_updateChevronTimerCallback: function() {
let scrollRect = this._rootElt.getBoundingClientRect();
let childOverflowed = false;
for (let i = 0; i < this._rootElt.childNodes.length; i++) {
let child = this._rootElt.childNodes[i];
// Once a child overflows, all the next ones will.
if (!childOverflowed) {
let childRect = child.getBoundingClientRect();
childOverflowed = this.isRTL ? (childRect.left < scrollRect.left)
: (childRect.right > scrollRect.right);
}
child.style.visibility = childOverflowed ? "hidden" : "visible";
}
// We rebuild the chevron on popupShowing, so if it is open
// we must update it.
if (this._chevron.open)
this._updateChevronPopupNodesVisibility();
},
nodeInserted:
function PT_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
if (parentElt == this._rootElt) {
let children = this._rootElt.childNodes;
this._insertNewItem(aPlacesNode,
aIndex < children.length ? children[aIndex] : null);
this.updateChevron();
return;
}
PlacesViewBase.prototype.nodeInserted.apply(this, arguments);
},
nodeRemoved:
function PT_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// Here we need the <menu>.
if (elt.localName == "menupopup")
elt = elt.parentNode;
if (parentElt == this._rootElt) {
this._removeChild(elt);
this.updateChevron();
return;
}
PlacesViewBase.prototype.nodeRemoved.apply(this, arguments);
},
nodeMoved:
function PT_nodeMoved(aPlacesNode,
aOldParentPlacesNode, aOldIndex,
aNewParentPlacesNode, aNewIndex) {
let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
if (parentElt == this._rootElt) {
// Container is on the toolbar.
// Move the element.
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// Here we need the <menu>.
if (elt.localName == "menupopup")
elt = elt.parentNode;
this._removeChild(elt);
this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
// The chevron view may get nodeMoved after the toolbar. In such a case,
// we should ensure (by manually swapping menuitems) that the actual nodes
// are in the final position before updateChevron tries to updates their
// visibility, or the chevron may go out of sync.
// Luckily updateChevron runs on a timer, so, by the time it updates
// nodes, the menu has already handled the notification.
this.updateChevron();
return;
}
PlacesViewBase.prototype.nodeMoved.apply(this, arguments);
},
nodeAnnotationChanged:
function PT_nodeAnnotationChanged(aPlacesNode, aAnno) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
if (elt == this._rootElt)
return;
// We're notified for the menupopup, not the containing toolbarbutton.
if (elt.localName == "menupopup")
elt = elt.parentNode;
if (elt.parentNode == this._rootElt) {
// Node is on the toolbar.
// All livemarks have a feedURI, so use it as our indicator.
if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
elt.setAttribute("livemark", true);
PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
.then(aLivemark => {
this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
this.invalidateContainer(aPlacesNode);
}, Components.utils.reportError);
}
}
else {
// Node is in a submenu.
PlacesViewBase.prototype.nodeAnnotationChanged.apply(this, arguments);
}
},
nodeTitleChanged: function(aPlacesNode, aNewTitle) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// There's no UI representation for the root node, thus there's
// nothing to be done when the title changes.
if (elt == this._rootElt)
return;
PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
// Here we need the <menu>.
if (elt.localName == "menupopup")
elt = elt.parentNode;
if (elt.parentNode == this._rootElt) {
// Node is on the toolbar
this.updateChevron();
}
},
invalidateContainer: function(aPlacesNode) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
if (elt == this._rootElt) {
// Container is the toolbar itself.
this._rebuild();
return;
}
PlacesViewBase.prototype.invalidateContainer.apply(this, arguments);
},
_overFolder: { elt: null,
openTimer: null,
hoverTime: 350,
closeTimer: null },
_clearOverFolder: function() {
// The mouse is no longer dragging over the stored menubutton.
// Close the menubutton, clear out drag styles, and clear all
// timers for opening/closing it.
if (this._overFolder.elt && this._overFolder.elt.lastChild) {
if (!this._overFolder.elt.lastChild.hasAttribute("dragover")) {
this._overFolder.elt.lastChild.hidePopup();
}
this._overFolder.elt.removeAttribute("dragover");
this._overFolder.elt = null;
}
if (this._overFolder.openTimer) {
this._overFolder.openTimer.cancel();
this._overFolder.openTimer = null;
}
if (this._overFolder.closeTimer) {
this._overFolder.closeTimer.cancel();
this._overFolder.closeTimer = null;
}
},
/**
* This function returns information about where to drop when dragging over
* the toolbar. The returned object has the following properties:
* - ip: the insertion point for the bookmarks service.
* - beforeIndex: child index to drop before, for the drop indicator.
* - folderElt: the folder to drop into, if applicable.
*/
_getDropPoint: function(aEvent) {
if (!PlacesUtils.nodeIsFolder(this._resultNode))
return null;
let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
let elt = aEvent.target;
if (elt._placesNode && elt != this._rootElt &&
elt.localName != "menupopup") {
let eltRect = elt.getBoundingClientRect();
let eltIndex = Array.prototype.indexOf.call(this._rootElt.childNodes, elt);
if (PlacesUtils.nodeIsFolder(elt._placesNode) &&
!PlacesUIUtils.isContentsReadOnly(elt._placesNode)) {
// This is a folder.
// If we are in the middle of it, drop inside it.
// Otherwise, drop before it, with regards to RTL mode.
let threshold = eltRect.width * 0.25;
if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold)
: (aEvent.clientX < eltRect.left + threshold)) {
// Drop before this folder.
dropPoint.ip =
new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
eltIndex, Ci.nsITreeView.DROP_BEFORE);
dropPoint.beforeIndex = eltIndex;
}
else if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
: (aEvent.clientX < eltRect.right - threshold)) {
// Drop inside this folder.
let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
elt._placesNode.title : null;
dropPoint.ip =
new InsertionPoint(PlacesUtils.getConcreteItemId(elt._placesNode),
-1, Ci.nsITreeView.DROP_ON,
tagName);
dropPoint.beforeIndex = eltIndex;
dropPoint.folderElt = elt;
}
else {
// Drop after this folder.
let beforeIndex =
(eltIndex == this._rootElt.childNodes.length - 1) ?
-1 : eltIndex + 1;
dropPoint.ip =
new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
beforeIndex, Ci.nsITreeView.DROP_BEFORE);
dropPoint.beforeIndex = beforeIndex;
}
}
else {
// This is a non-folder node or a read-only folder.
// Drop before it with regards to RTL mode.
let threshold = eltRect.width * 0.5;
if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
: (aEvent.clientX < eltRect.left + threshold)) {
// Drop before this bookmark.
dropPoint.ip =
new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
eltIndex, Ci.nsITreeView.DROP_BEFORE);
dropPoint.beforeIndex = eltIndex;
}
else {
// Drop after this bookmark.
let beforeIndex =
eltIndex == this._rootElt.childNodes.length - 1 ?
-1 : eltIndex + 1;
dropPoint.ip =
new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
beforeIndex, Ci.nsITreeView.DROP_BEFORE);
dropPoint.beforeIndex = beforeIndex;
}
}
}
else {
// We are most likely dragging on the empty area of the
// toolbar, we should drop after the last node.
dropPoint.ip =
new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-1, Ci.nsITreeView.DROP_BEFORE);
dropPoint.beforeIndex = -1;
}
return dropPoint;
},
_setTimer: function(aTime) {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
return timer;
},
notify: function(aTimer) {
if (aTimer == this._updateChevronTimer) {
this._updateChevronTimer = null;
this._updateChevronTimerCallback();
}
// * Timer to turn off indicator bar.
else if (aTimer == this._ibTimer) {
this._dropIndicator.collapsed = true;
this._ibTimer = null;
}
// * Timer to open a menubutton that's being dragged over.
else if (aTimer == this._overFolder.openTimer) {
// Set the autoopen attribute on the folder's menupopup so that
// the menu will automatically close when the mouse drags off of it.
this._overFolder.elt.lastChild.setAttribute("autoopened", "true");
this._overFolder.elt.open = true;
this._overFolder.openTimer = null;
}
// * Timer to close a menubutton that's been dragged off of.
else if (aTimer == this._overFolder.closeTimer) {
// Close the menubutton if we are not dragging over it or one of
// its children. The autoopened attribute will let the menu know to
// close later if the menu is still being dragged over.
let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget;
let inHierarchy = false;
while (currentPlacesNode) {
if (currentPlacesNode == this._rootElt) {
inHierarchy = true;
break;
}
currentPlacesNode = currentPlacesNode.parentNode;
}
// The _clearOverFolder() function will close the menu for
// _overFolder.elt. So null it out if we don't want to close it.
if (inHierarchy)
this._overFolder.elt = null;
// Clear out the folder and all associated timers.
this._clearOverFolder();
}
},
_onMouseOver: function(aEvent) {
let button = aEvent.target;
if (button.parentNode == this._rootElt && button._placesNode &&
PlacesUtils.nodeIsURI(button._placesNode))
window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri, null);
},
_onMouseOut: function(aEvent) {
window.XULBrowserWindow.setOverLink("", null);
},
_cleanupDragDetails: function() {
// Called on dragend and drop.
PlacesControllerDragHelper.currentDropTarget = null;
this._draggedElt = null;
if (this._ibTimer)
this._ibTimer.cancel();
this._dropIndicator.collapsed = true;
},
_onDragStart: function(aEvent) {
// Sub menus have their own d&d handlers.
let draggedElt = aEvent.target;
if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
return;
if (draggedElt.localName == "toolbarbutton" &&
draggedElt.getAttribute("type") == "menu") {
// If the drag gesture on a container is toward down we open instead
// of dragging.
let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY;
let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX;
if ((translateY) >= Math.abs(translateX/2)) {
// Don't start the drag.
aEvent.preventDefault();
// Open the menu.
draggedElt.open = true;
return;
}
// If the menu is open, close it.
if (draggedElt.open) {
draggedElt.lastChild.hidePopup();
draggedElt.open = false;
}
}
// Activate the view and cache the dragged element.
this._draggedElt = draggedElt._placesNode;
this._rootElt.focus();
this._controller.setDataTransfer(aEvent);
aEvent.stopPropagation();
},
_onDragOver: function(aEvent) {
// Cache the dataTransfer
PlacesControllerDragHelper.currentDropTarget = aEvent.target;
let dt = aEvent.dataTransfer;
let dropPoint = this._getDropPoint(aEvent);
if (!dropPoint || !dropPoint.ip ||
!PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) {
this._dropIndicator.collapsed = true;
aEvent.stopPropagation();
return;
}
if (this._ibTimer) {
this._ibTimer.cancel();
this._ibTimer = null;
}
if (dropPoint.folderElt || aEvent.originalTarget == this._chevron) {
// Dropping over a menubutton or chevron button.
// Set styles and timer to open relative menupopup.
let overElt = dropPoint.folderElt || this._chevron;
if (this._overFolder.elt != overElt) {
this._clearOverFolder();
this._overFolder.elt = overElt;
this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime);
}
if (!this._overFolder.elt.hasAttribute("dragover"))
this._overFolder.elt.setAttribute("dragover", "true");
this._dropIndicator.collapsed = true;
}
else {
// Dragging over a normal toolbarbutton,
// show indicator bar and move it to the appropriate drop point.
let ind = this._dropIndicator;
ind.parentNode.collapsed = false;
let halfInd = ind.clientWidth / 2;
let translateX;
if (this.isRTL) {
halfInd = Math.ceil(halfInd);
translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd;
if (this._rootElt.firstChild) {
if (dropPoint.beforeIndex == -1)
translateX += this._rootElt.lastChild.getBoundingClientRect().left;
else {
translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
.getBoundingClientRect().right;
}
}
}
else {
halfInd = Math.floor(halfInd);
translateX = 0 - this._rootElt.getBoundingClientRect().left +
halfInd;
if (this._rootElt.firstChild) {
if (dropPoint.beforeIndex == -1)
translateX += this._rootElt.lastChild.getBoundingClientRect().right;
else {
translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
.getBoundingClientRect().left;
}
}
}
ind.style.transform = "translate(" + Math.round(translateX) + "px)";
ind.style.marginInlineStart = (-ind.clientWidth) + "px";
ind.collapsed = false;
// Clear out old folder information.
this._clearOverFolder();
}
aEvent.preventDefault();
aEvent.stopPropagation();
},
_onDrop: function(aEvent) {
PlacesControllerDragHelper.currentDropTarget = aEvent.target;
let dropPoint = this._getDropPoint(aEvent);
if (dropPoint && dropPoint.ip) {
PlacesControllerDragHelper.onDrop(dropPoint.ip, aEvent.dataTransfer)
.then(null, Components.utils.reportError);
aEvent.preventDefault();
}
this._cleanupDragDetails();
aEvent.stopPropagation();
},
_onDragExit: function(aEvent) {
PlacesControllerDragHelper.currentDropTarget = null;
// Set timer to turn off indicator bar (if we turn it off
// here, dragenter might be called immediately after, creating
// flicker).
if (this._ibTimer)
this._ibTimer.cancel();
this._ibTimer = this._setTimer(10);
// If we hovered over a folder, close it now.
if (this._overFolder.elt)
this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime);
},
_onDragEnd: function(aEvent) {
this._cleanupDragDetails();
},
_onPopupShowing: function(aEvent) {
if (!this._allowPopupShowing) {
this._allowPopupShowing = true;
aEvent.preventDefault();
return;
}
let parent = aEvent.target.parentNode;
if (parent.localName == "toolbarbutton")
this._openedMenuButton = parent;
PlacesViewBase.prototype._onPopupShowing.apply(this, arguments);
},
_onPopupHidden: function(aEvent) {
let popup = aEvent.target;
let placesNode = popup._placesNode;
// Avoid handling popuphidden of inner views
if (placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
// UI performance: folder queries are cheap, keep the resultnode open
// so we don't rebuild its contents whenever the popup is reopened.
// Though, we want to always close feed containers so their expiration
// status will be checked at next opening.
if (!PlacesUtils.nodeIsFolder(placesNode) ||
this.controller.hasCachedLivemarkInfo(placesNode)) {
placesNode.containerOpen = false;
}
}
let parent = popup.parentNode;
if (parent.localName == "toolbarbutton") {
this._openedMenuButton = null;
// Clear the dragover attribute if present, if we are dragging into a
// folder in the hierachy of current opened popup we don't clear
// this attribute on clearOverFolder. See Notify for closeTimer.
if (parent.hasAttribute("dragover"))
parent.removeAttribute("dragover");
}
},
_onMouseMove: function(aEvent) {
// Used in dragStart to prevent dragging folders when dragging down.
this._cachedMouseMoveEvent = aEvent;
if (this._openedMenuButton == null ||
PlacesControllerDragHelper.getSession())
return;
let target = aEvent.originalTarget;
if (this._openedMenuButton != target &&
target.localName == "toolbarbutton" &&
target.type == "menu") {
this._openedMenuButton.open = false;
target.open = true;
}
}
};
/**
* View for Places menus. This object should be created during the first
* popupshowing that's dispatched on the menu.
*/
function PlacesMenu(aPopupShowingEvent, aPlace, aOptions) {
this._rootElt = aPopupShowingEvent.target; // <menupopup>
this._viewElt = this._rootElt.parentNode; // <menu>
this._viewElt._placesView = this;
this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
this._addEventListeners(window, ["unload"], false);
if (AppConstants.platform === "macosx") {
// Must walk up to support views in sub-menus, like Bookmarks Toolbar menu.
for (let elt = this._viewElt.parentNode; elt; elt = elt.parentNode) {
if (elt.localName == "menubar") {
this._nativeView = true;
break;
}
}
}
PlacesViewBase.call(this, aPlace, aOptions);
this._onPopupShowing(aPopupShowingEvent);
}
PlacesMenu.prototype = {
__proto__: PlacesViewBase.prototype,
QueryInterface: function(aIID) {
if (aIID.equals(Ci.nsIDOMEventListener))
return this;
return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
},
_removeChild: function(aChild) {
PlacesViewBase.prototype._removeChild.apply(this, arguments);
},
uninit: function() {
this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
true);
this._removeEventListeners(window, ["unload"], false);
PlacesViewBase.prototype.uninit.apply(this, arguments);
},
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "unload":
this.uninit();
break;
case "popupshowing":
this._onPopupShowing(aEvent);
break;
case "popuphidden":
this._onPopupHidden(aEvent);
break;
}
},
_onPopupHidden: function(aEvent) {
// Avoid handling popuphidden of inner views.
let popup = aEvent.originalTarget;
let placesNode = popup._placesNode;
if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this)
return;
// UI performance: folder queries are cheap, keep the resultnode open
// so we don't rebuild its contents whenever the popup is reopened.
// Though, we want to always close feed containers so their expiration
// status will be checked at next opening.
if (!PlacesUtils.nodeIsFolder(placesNode) ||
this.controller.hasCachedLivemarkInfo(placesNode))
placesNode.containerOpen = false;
// The autoopened attribute is set for folders which have been
// automatically opened when dragged over. Turn off this attribute
// when the folder closes because it is no longer applicable.
popup.removeAttribute("autoopened");
popup.removeAttribute("dragstart");
}
};
function PlacesPanelMenuView(aPlace, aViewId, aRootId, aOptions) {
this._viewElt = document.getElementById(aViewId);
this._rootElt = document.getElementById(aRootId);
this._viewElt._placesView = this;
this.options = aOptions;
PlacesViewBase.call(this, aPlace, aOptions);
}
PlacesPanelMenuView.prototype = {
__proto__: PlacesViewBase.prototype,
QueryInterface: function(aIID) {
return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
},
uninit: function() {
PlacesViewBase.prototype.uninit.apply(this, arguments);
},
_insertNewItem:
function PAMV__insertNewItem(aChild, aBefore) {
this._domNodes.delete(aChild);
let type = aChild.type;
let button;
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
button = document.createElement("toolbarseparator");
button.setAttribute("class", "small-separator");
}
else {
button = document.createElement("toolbarbutton");
button.className = "bookmark-item";
if (typeof this.options.extraClasses.entry == "string")
button.classList.add(this.options.extraClasses.entry);
button.setAttribute("label", aChild.title || "");
let icon = aChild.icon;
if (icon)
button.setAttribute("image", icon);
if (PlacesUtils.containerTypes.includes(type)) {
button.setAttribute("container", "true");
if (PlacesUtils.nodeIsQuery(aChild)) {
button.setAttribute("query", "true");
if (PlacesUtils.nodeIsTagQuery(aChild))
button.setAttribute("tagContainer", "true");
}
else if (PlacesUtils.nodeIsFolder(aChild)) {
PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
.then(aLivemark => {
button.setAttribute("livemark", "true");
this.controller.cacheLivemarkInfo(aChild, aLivemark);
}, () => undefined);
}
}
else if (PlacesUtils.nodeIsURI(aChild)) {
button.setAttribute("scheme",
PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
}
}
button._placesNode = aChild;
if (!this._domNodes.has(aChild))
this._domNodes.set(aChild, button);
this._rootElt.insertBefore(button, aBefore);
},
nodeInserted:
function PAMV_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
if (parentElt != this._rootElt)
return;
let children = this._rootElt.childNodes;
this._insertNewItem(aPlacesNode,
aIndex < children.length ? children[aIndex] : null);
},
nodeRemoved:
function PAMV_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
if (parentElt != this._rootElt)
return;
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
this._removeChild(elt);
},
nodeMoved:
function PAMV_nodeMoved(aPlacesNode,
aOldParentPlacesNode, aOldIndex,
aNewParentPlacesNode, aNewIndex) {
let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
if (parentElt != this._rootElt)
return;
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
this._removeChild(elt);
this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
},
nodeAnnotationChanged:
function PAMV_nodeAnnotationChanged(aPlacesNode, aAnno) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// There's no UI representation for the root node.
if (elt == this._rootElt)
return;
if (elt.parentNode != this._rootElt)
return;
// All livemarks have a feedURI, so use it as our indicator.
if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
elt.setAttribute("livemark", true);
PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
.then(aLivemark => {
this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
this.invalidateContainer(aPlacesNode);
}, Components.utils.reportError);
}
},
nodeTitleChanged: function(aPlacesNode, aNewTitle) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// There's no UI representation for the root node.
if (elt == this._rootElt)
return;
PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
},
invalidateContainer: function(aPlacesNode) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
if (elt != this._rootElt)
return;
// Container is the toolbar itself.
while (this._rootElt.hasChildNodes()) {
this._rootElt.removeChild(this._rootElt.firstChild);
}
for (let i = 0; i < this._resultNode.childCount; ++i) {
this._insertNewItem(this._resultNode.getChild(i), null);
}
}
};