mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-12 06:08:24 +02:00
Updated time, access count and uri can be accessed using the node passed to the method. There is no need to access the other arguments, which contain the old values of the quantities changed. MozReview-Commit-ID: 3WEwAs8gQ0w --HG-- extra : rebase_source : 4f6f8bb9d9747cb9cfacf0a5ee5ed58dfca41c0d
1960 lines
67 KiB
JavaScript
1960 lines
67 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/. */
|
|
|
|
/* eslint-env mozilla/browser-window */
|
|
|
|
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);
|
|
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() { },
|
|
selectAll() { },
|
|
|
|
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 PVB_buildContextMenu(aPopup) {
|
|
this._contextMenuShown = aPopup;
|
|
window.updateCommands("places");
|
|
return this.controller.buildContextMenu(aPopup);
|
|
},
|
|
|
|
destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
|
|
this._contextMenuShown = null;
|
|
},
|
|
|
|
_cleanPopup: function PVB_cleanPopup(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 PVB__rebuildPopup(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 PVB__removeChild(aChild) {
|
|
// If document.popupNode pointed to this child, null it out,
|
|
// otherwise controller's command-updating may rely on the removed
|
|
// item still being "selected".
|
|
if (document.popupNode == aChild)
|
|
document.popupNode = null;
|
|
|
|
aChild.remove();
|
|
},
|
|
|
|
_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 PVB_toggleCutNode(aPlacesNode, aValue) {
|
|
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
|
|
|
|
// We may get the popup for menus, but we need the menu itself.
|
|
if (elt.localName == "menupopup")
|
|
elt = elt.parentNode;
|
|
if (aValue)
|
|
elt.setAttribute("cutting", "true");
|
|
else
|
|
elt.removeAttribute("cutting");
|
|
},
|
|
|
|
nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
|
|
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
|
|
|
|
// Here we need the <menu>.
|
|
if (elt.localName == "menupopup")
|
|
elt = elt.parentNode;
|
|
|
|
elt.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
|
|
},
|
|
|
|
nodeIconChanged: function PVB_nodeIconChanged(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;
|
|
}
|
|
// We must remove and reset the attribute to force an update.
|
|
elt.removeAttribute("image");
|
|
elt.setAttribute("image", aPlacesNode.icon);
|
|
},
|
|
|
|
nodeAnnotationChanged:
|
|
function PVB_nodeAnnotationChanged(aPlacesNode, aAnno) {
|
|
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 (aPlacesNode.accessCount)
|
|
child.setAttribute("visited", "true");
|
|
else
|
|
child.removeAttribute("visited");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
nodeTagsChanged() { },
|
|
nodeDateAddedChanged() { },
|
|
nodeLastModifiedChanged() { },
|
|
nodeKeywordChanged() { },
|
|
sortingChanged() { },
|
|
batching() { },
|
|
|
|
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 PVB__populateLivemarkPopup(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 PVB_invalidateContainer(aPlacesNode) {
|
|
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
|
|
elt._built = false;
|
|
|
|
// If the menupopup is open we should live-update it.
|
|
if (elt.parentNode.open)
|
|
this._rebuildPopup(elt);
|
|
},
|
|
|
|
uninit: function PVB_uninit() {
|
|
if (this._result) {
|
|
this._result.removeObserver(this);
|
|
this._resultNode.containerOpen = false;
|
|
this._resultNode = null;
|
|
this._result = null;
|
|
}
|
|
|
|
if (this._controller) {
|
|
this._controller.terminate();
|
|
// Removing the controller will fail if it is already no longer there.
|
|
// This can happen if the view element was removed/reinserted without
|
|
// our knowledge. There is no way to check for that having happened
|
|
// without the possibility of an exception. :-(
|
|
try {
|
|
this._viewElt.controllers.removeController(this._controller);
|
|
} catch (ex) {
|
|
} finally {
|
|
this._controller = null;
|
|
}
|
|
}
|
|
|
|
delete this._viewElt._placesView;
|
|
},
|
|
|
|
get isRTL() {
|
|
if ("_isRTL" in this)
|
|
return this._isRTL;
|
|
|
|
return this._isRTL = document.defaultView
|
|
.getComputedStyle(this.viewElt)
|
|
.direction == "rtl";
|
|
},
|
|
|
|
get ownerWindow() {
|
|
return window;
|
|
},
|
|
|
|
/**
|
|
* Adds an "Open All in Tabs" menuitem to the bottom of the popup.
|
|
* @param aPopup
|
|
* a Places popup.
|
|
*/
|
|
_mayAddCommandsItems: function PVB__mayAddCommandsItems(aPopup) {
|
|
// The command items are never added to the root popup.
|
|
if (aPopup == this._rootElt)
|
|
return;
|
|
|
|
let hasMultipleURIs = false;
|
|
|
|
// Check if the popup contains at least 2 menuitems with places nodes.
|
|
// We don't currently support opening multiple uri nodes when they are not
|
|
// populated by the result.
|
|
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 PVB__ensureMarkers(aPopup) {
|
|
if (aPopup._startMarker)
|
|
return;
|
|
|
|
// _startMarker is an hidden menuseparator that lives before places nodes.
|
|
aPopup._startMarker = document.createElement("menuseparator");
|
|
aPopup._startMarker.hidden = true;
|
|
aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild);
|
|
|
|
// _endMarker is a DOM node that lives after places nodes, specified with
|
|
// the 'insertionPoint' option or will be a hidden menuseparator.
|
|
let node = ("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 PVB__onPopupShowing(aEvent) {
|
|
// Avoid handling popupshowing of inner views.
|
|
let popup = aEvent.originalTarget;
|
|
|
|
this._ensureMarkers(popup);
|
|
|
|
// Remove any delayed element, see _cleanPopup for details.
|
|
if ("_delayedRemovals" in popup) {
|
|
while (popup._delayedRemovals.length > 0) {
|
|
popup.removeChild(popup._delayedRemovals.shift());
|
|
}
|
|
}
|
|
|
|
if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
|
|
if (!popup._placesNode.containerOpen)
|
|
popup._placesNode.containerOpen = true;
|
|
if (!popup._built)
|
|
this._rebuildPopup(popup);
|
|
|
|
this._mayAddCommandsItems(popup);
|
|
}
|
|
},
|
|
|
|
_addEventListeners:
|
|
function PVB__addEventListeners(aObject, aEventNames, aCapturing) {
|
|
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 PT_QueryInterface(aIID) {
|
|
if (aIID.equals(Ci.nsIDOMEventListener) ||
|
|
aIID.equals(Ci.nsITimerCallback))
|
|
return this;
|
|
|
|
return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
|
|
},
|
|
|
|
uninit: function PT_uninit() {
|
|
this._removeEventListeners(this._viewElt, this._cbEvents, false);
|
|
this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
|
|
true);
|
|
this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
|
|
this._removeEventListeners(window, ["resize", "unload"], false);
|
|
this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
|
|
|
|
if (this._chevron._placesView) {
|
|
this._chevron._placesView.uninit();
|
|
}
|
|
|
|
PlacesViewBase.prototype.uninit.apply(this, arguments);
|
|
},
|
|
|
|
_openedMenuButton: null,
|
|
_allowPopupShowing: true,
|
|
|
|
_rebuild: function PT__rebuild() {
|
|
// Clear out references to existing nodes, since they will be removed
|
|
// and re-added.
|
|
if (this._overFolder.elt)
|
|
this._clearOverFolder();
|
|
|
|
this._openedMenuButton = null;
|
|
while (this._rootElt.hasChildNodes()) {
|
|
this._rootElt.firstChild.remove();
|
|
}
|
|
|
|
let cc = this._resultNode.childCount;
|
|
for (let i = 0; i < cc; ++i) {
|
|
this._insertNewItem(this._resultNode.getChild(i), null);
|
|
}
|
|
|
|
if (this._chevronPopup.hasAttribute("type")) {
|
|
// 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 PT_handleEvent(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() {
|
|
if (this._rootElt.scrollLeftMin != this._rootElt.scrollLeftMax) {
|
|
this._onOverflow();
|
|
} else {
|
|
this._onUnderflow();
|
|
}
|
|
},
|
|
|
|
_isOverflowStateEventRelevant: function PT_isOverflowStateEventRelevant(aEvent) {
|
|
// Ignore events not aimed at ourselves, as well as purely vertical ones:
|
|
return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
|
|
},
|
|
|
|
_onOverflow: function PT_onOverflow() {
|
|
// Attach the popup binding to the chevron popup if it has not yet
|
|
// been initialized.
|
|
if (!this._chevronPopup.hasAttribute("type")) {
|
|
this._chevronPopup.setAttribute("place", this.place);
|
|
this._chevronPopup.setAttribute("type", "places");
|
|
}
|
|
this._chevron.collapsed = false;
|
|
this.updateChevron();
|
|
},
|
|
|
|
_onUnderflow: function PT_onUnderflow() {
|
|
this.updateChevron();
|
|
this._chevron.collapsed = true;
|
|
},
|
|
|
|
updateChevron: function PT_updateChevron() {
|
|
// If the chevron is collapsed there's nothing to update.
|
|
if (this._chevron.collapsed)
|
|
return;
|
|
|
|
// Update the chevron on a timer. This will avoid repeated work when
|
|
// lot of changes happen in a small timeframe.
|
|
if (this._updateChevronTimer)
|
|
this._updateChevronTimer.cancel();
|
|
|
|
this._updateChevronTimer = this._setTimer(100);
|
|
},
|
|
|
|
_updateChevronTimerCallback: function PT__updateChevronTimerCallback() {
|
|
let scrollRect = this._rootElt.getBoundingClientRect();
|
|
let childOverflowed = false;
|
|
for (let i = 0; i < this._rootElt.childNodes.length; i++) {
|
|
let child = this._rootElt.childNodes[i];
|
|
// Once a child overflows, all the next ones will.
|
|
if (!childOverflowed) {
|
|
let childRect = child.getBoundingClientRect();
|
|
childOverflowed = this.isRTL ? (childRect.left < scrollRect.left)
|
|
: (childRect.right > scrollRect.right);
|
|
|
|
}
|
|
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 PT_nodeTitleChanged(aPlacesNode, aNewTitle) {
|
|
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
|
|
|
|
// There's no UI representation for the root node, thus there's
|
|
// nothing to be done when the title changes.
|
|
if (elt == this._rootElt)
|
|
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 PT_invalidateContainer(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 PT__clearOverFolder() {
|
|
// 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 PT__getDropPoint(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 PT_setTimer(aTime) {
|
|
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
|
|
return timer;
|
|
},
|
|
|
|
notify: function PT_notify(aTimer) {
|
|
if (aTimer == this._updateChevronTimer) {
|
|
this._updateChevronTimer = null;
|
|
this._updateChevronTimerCallback();
|
|
} else if (aTimer == this._ibTimer) {
|
|
// * Timer to turn off indicator bar.
|
|
this._dropIndicator.collapsed = true;
|
|
this._ibTimer = null;
|
|
} else if (aTimer == this._overFolder.openTimer) {
|
|
// * Timer to open a menubutton that's being dragged over.
|
|
// Set the autoopen attribute on the folder's menupopup so that
|
|
// the menu will automatically close when the mouse drags off of it.
|
|
this._overFolder.elt.lastChild.setAttribute("autoopened", "true");
|
|
this._overFolder.elt.open = true;
|
|
this._overFolder.openTimer = null;
|
|
} else if (aTimer == this._overFolder.closeTimer) {
|
|
// * Timer to close a menubutton that's been dragged off of.
|
|
// Close the menubutton if we are not dragging over it or one of
|
|
// its children. The autoopened attribute will let the menu know to
|
|
// close later if the menu is still being dragged over.
|
|
let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget;
|
|
let inHierarchy = false;
|
|
while (currentPlacesNode) {
|
|
if (currentPlacesNode == this._rootElt) {
|
|
inHierarchy = true;
|
|
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 PT__onMouseOver(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 PT__onMouseOut(aEvent) {
|
|
window.XULBrowserWindow.setOverLink("", null);
|
|
},
|
|
|
|
_cleanupDragDetails: function PT__cleanupDragDetails() {
|
|
// Called on dragend and drop.
|
|
PlacesControllerDragHelper.currentDropTarget = null;
|
|
this._draggedElt = null;
|
|
if (this._ibTimer)
|
|
this._ibTimer.cancel();
|
|
|
|
this._dropIndicator.collapsed = true;
|
|
},
|
|
|
|
_onDragStart: function PT__onDragStart(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 PT__onDragOver(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 PT__onDrop(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 PT__onDragExit(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 PT_onDragEnd(aEvent) {
|
|
this._cleanupDragDetails();
|
|
},
|
|
|
|
_onPopupShowing: function PT__onPopupShowing(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 PT__onPopupHidden(aEvent) {
|
|
let popup = aEvent.target;
|
|
let placesNode = popup._placesNode;
|
|
// Avoid handling popuphidden of inner views
|
|
if (placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
|
|
// UI performance: folder queries are cheap, keep the resultnode open
|
|
// so we don't rebuild its contents whenever the popup is reopened.
|
|
// Though, we want to always close feed containers so their expiration
|
|
// status will be checked at next opening.
|
|
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 PT__onMouseMove(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 PM_QueryInterface(aIID) {
|
|
if (aIID.equals(Ci.nsIDOMEventListener))
|
|
return this;
|
|
|
|
return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
|
|
},
|
|
|
|
_removeChild: function PM_removeChild(aChild) {
|
|
PlacesViewBase.prototype._removeChild.apply(this, arguments);
|
|
},
|
|
|
|
uninit: function PM_uninit() {
|
|
this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
|
|
true);
|
|
this._removeEventListeners(window, ["unload"], false);
|
|
|
|
PlacesViewBase.prototype.uninit.apply(this, arguments);
|
|
},
|
|
|
|
handleEvent: function PM_handleEvent(aEvent) {
|
|
switch (aEvent.type) {
|
|
case "unload":
|
|
this.uninit();
|
|
break;
|
|
case "popupshowing":
|
|
this._onPopupShowing(aEvent);
|
|
break;
|
|
case "popuphidden":
|
|
this._onPopupHidden(aEvent);
|
|
break;
|
|
}
|
|
},
|
|
|
|
_onPopupHidden: function PM__onPopupHidden(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 PAMV_QueryInterface(aIID) {
|
|
return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
|
|
},
|
|
|
|
uninit: function PAMV_uninit() {
|
|
PlacesViewBase.prototype.uninit.apply(this, arguments);
|
|
},
|
|
|
|
_insertNewItem:
|
|
function PAMV__insertNewItem(aChild, aBefore) {
|
|
this._domNodes.delete(aChild);
|
|
|
|
let type = aChild.type;
|
|
let button;
|
|
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
|
|
button = document.createElement("toolbarseparator");
|
|
button.setAttribute("class", "small-separator");
|
|
} else {
|
|
button = document.createElement("toolbarbutton");
|
|
button.className = "bookmark-item";
|
|
if (typeof this.options.extraClasses.entry == "string")
|
|
button.classList.add(this.options.extraClasses.entry);
|
|
button.setAttribute("label", aChild.title || "");
|
|
let icon = aChild.icon;
|
|
if (icon)
|
|
button.setAttribute("image", icon);
|
|
|
|
if (PlacesUtils.containerTypes.includes(type)) {
|
|
button.setAttribute("container", "true");
|
|
|
|
if (PlacesUtils.nodeIsQuery(aChild)) {
|
|
button.setAttribute("query", "true");
|
|
if (PlacesUtils.nodeIsTagQuery(aChild))
|
|
button.setAttribute("tagContainer", "true");
|
|
} else if (PlacesUtils.nodeIsFolder(aChild)) {
|
|
PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
|
|
.then(aLivemark => {
|
|
button.setAttribute("livemark", "true");
|
|
this.controller.cacheLivemarkInfo(aChild, aLivemark);
|
|
}, () => undefined);
|
|
}
|
|
} else if (PlacesUtils.nodeIsURI(aChild)) {
|
|
button.setAttribute("scheme",
|
|
PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
|
|
}
|
|
}
|
|
|
|
button._placesNode = aChild;
|
|
if (!this._domNodes.has(aChild))
|
|
this._domNodes.set(aChild, button);
|
|
|
|
this._rootElt.insertBefore(button, aBefore);
|
|
},
|
|
|
|
nodeInserted:
|
|
function PAMV_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
|
|
let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
|
|
if (parentElt != this._rootElt)
|
|
return;
|
|
|
|
let children = this._rootElt.childNodes;
|
|
this._insertNewItem(aPlacesNode,
|
|
aIndex < children.length ? children[aIndex] : null);
|
|
},
|
|
|
|
nodeRemoved:
|
|
function PAMV_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
|
|
let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
|
|
if (parentElt != this._rootElt)
|
|
return;
|
|
|
|
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
|
|
this._removeChild(elt);
|
|
},
|
|
|
|
nodeMoved:
|
|
function PAMV_nodeMoved(aPlacesNode,
|
|
aOldParentPlacesNode, aOldIndex,
|
|
aNewParentPlacesNode, aNewIndex) {
|
|
let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
|
|
if (parentElt != this._rootElt)
|
|
return;
|
|
|
|
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
|
|
this._removeChild(elt);
|
|
this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
|
|
},
|
|
|
|
nodeAnnotationChanged:
|
|
function PAMV_nodeAnnotationChanged(aPlacesNode, aAnno) {
|
|
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
|
|
// There's no UI representation for the root node.
|
|
if (elt == this._rootElt)
|
|
return;
|
|
|
|
if (elt.parentNode != this._rootElt)
|
|
return;
|
|
|
|
// All livemarks have a feedURI, so use it as our indicator.
|
|
if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
|
|
elt.setAttribute("livemark", true);
|
|
|
|
PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
|
|
.then(aLivemark => {
|
|
this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
|
|
this.invalidateContainer(aPlacesNode);
|
|
}, Components.utils.reportError);
|
|
}
|
|
},
|
|
|
|
nodeTitleChanged: function PAMV_nodeTitleChanged(aPlacesNode, aNewTitle) {
|
|
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
|
|
|
|
// There's no UI representation for the root node.
|
|
if (elt == this._rootElt)
|
|
return;
|
|
|
|
PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
|
|
},
|
|
|
|
invalidateContainer: function PAMV_invalidateContainer(aPlacesNode) {
|
|
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
|
|
if (elt != this._rootElt)
|
|
return;
|
|
|
|
// Container is the toolbar itself.
|
|
while (this._rootElt.hasChildNodes()) {
|
|
this._rootElt.firstChild.remove();
|
|
}
|
|
|
|
for (let i = 0; i < this._resultNode.childCount; ++i) {
|
|
this._insertNewItem(this._resultNode.getChild(i), null);
|
|
}
|
|
}
|
|
};
|