diff options
Diffstat (limited to 'application/basilisk/components/places/content/browserPlacesViews.js')
-rw-r--r-- | application/basilisk/components/places/content/browserPlacesViews.js | 1996 |
1 files changed, 1996 insertions, 0 deletions
diff --git a/application/basilisk/components/places/content/browserPlacesViews.js b/application/basilisk/components/places/content/browserPlacesViews.js new file mode 100644 index 000000000..c6ee9b6ce --- /dev/null +++ b/application/basilisk/components/places/content/browserPlacesViews.js @@ -0,0 +1,1996 @@ +/* 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 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.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 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(aURIString)); + }, + + 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; + + 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 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.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 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: function() { + 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(); + } + + // * 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 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.removeChild(this._rootElt.firstChild); + } + + for (let i = 0; i < this._resultNode.childCount; ++i) { + this._insertNewItem(this._resultNode.getChild(i), null); + } + } +}; |