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