summaryrefslogtreecommitdiffstats
path: root/application/basilisk/base/content/browser-places.js
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-02 03:32:58 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-02 03:32:58 -0500
commite72ef92b5bdc43cd2584198e2e54e951b70299e8 (patch)
tree01ceb4a897c33eca9e7ccf2bc3aefbe530169fe5 /application/basilisk/base/content/browser-places.js
parent0d19b77d3eaa5b8d837bf52c19759e68e42a1c4c (diff)
downloadUXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar
UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar.gz
UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar.lz
UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar.xz
UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.zip
Add Basilisk
Diffstat (limited to 'application/basilisk/base/content/browser-places.js')
-rw-r--r--application/basilisk/base/content/browser-places.js2018
1 files changed, 2018 insertions, 0 deletions
diff --git a/application/basilisk/base/content/browser-places.js b/application/basilisk/base/content/browser-places.js
new file mode 100644
index 000000000..963d36c3a
--- /dev/null
+++ b/application/basilisk/base/content/browser-places.js
@@ -0,0 +1,2018 @@
+/* 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/. */
+
+var StarUI = {
+ _itemId: -1,
+ uri: null,
+ _batching: false,
+ _isNewBookmark: false,
+ _isComposing: false,
+ _autoCloseTimer: 0,
+ // The autoclose timer is diasbled if the user interacts with the
+ // popup, such as making a change through typing or clicking on
+ // the popup.
+ _autoCloseTimerEnabled: true,
+
+ _element(aID) {
+ return document.getElementById(aID);
+ },
+
+ // Edit-bookmark panel
+ get panel() {
+ delete this.panel;
+ var element = this._element("editBookmarkPanel");
+ // initially the panel is hidden
+ // to avoid impacting startup / new window performance
+ element.hidden = false;
+ element.addEventListener("keypress", this);
+ element.addEventListener("mousedown", this);
+ element.addEventListener("mouseout", this);
+ element.addEventListener("mousemove", this);
+ element.addEventListener("compositionstart", this);
+ element.addEventListener("compositionend", this);
+ element.addEventListener("input", this);
+ element.addEventListener("popuphidden", this);
+ element.addEventListener("popupshown", this);
+ return this.panel = element;
+ },
+
+ // Array of command elements to disable when the panel is opened.
+ get _blockedCommands() {
+ delete this._blockedCommands;
+ return this._blockedCommands =
+ ["cmd_close", "cmd_closeWindow"].map(id => this._element(id));
+ },
+
+ _blockCommands: function SU__blockCommands() {
+ this._blockedCommands.forEach(function(elt) {
+ // make sure not to permanently disable this item (see bug 409155)
+ if (elt.hasAttribute("wasDisabled"))
+ return;
+ if (elt.getAttribute("disabled") == "true") {
+ elt.setAttribute("wasDisabled", "true");
+ } else {
+ elt.setAttribute("wasDisabled", "false");
+ elt.setAttribute("disabled", "true");
+ }
+ });
+ },
+
+ _restoreCommandsState: function SU__restoreCommandsState() {
+ this._blockedCommands.forEach(function(elt) {
+ if (elt.getAttribute("wasDisabled") != "true")
+ elt.removeAttribute("disabled");
+ elt.removeAttribute("wasDisabled");
+ });
+ },
+
+ // nsIDOMEventListener
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "mousemove":
+ clearTimeout(this._autoCloseTimer);
+ // The autoclose timer is not disabled on generic mouseout
+ // because the user may not have actually interacted with the popup.
+ break;
+ case "popuphidden":
+ clearTimeout(this._autoCloseTimer);
+ if (aEvent.originalTarget == this.panel) {
+ if (!this._element("editBookmarkPanelContent").hidden)
+ this.quitEditMode();
+
+ if (this._anchorToolbarButton) {
+ this._anchorToolbarButton.removeAttribute("open");
+ this._anchorToolbarButton = null;
+ }
+ this._restoreCommandsState();
+ this._itemId = -1;
+ if (this._batching)
+ this.endBatch();
+
+ if (this._uriForRemoval) {
+ if (this._isNewBookmark) {
+ if (!PlacesUtils.useAsyncTransactions) {
+ PlacesUtils.transactionManager.undoTransaction();
+ break;
+ }
+ PlacesTransactions().undo().catch(Cu.reportError);
+ break;
+ }
+ // Remove all bookmarks for the bookmark's url, this also removes
+ // the tags for the url.
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval);
+ for (let itemId of itemIds) {
+ let txn = new PlacesRemoveItemTransaction(itemId);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ break;
+ }
+
+ PlacesTransactions.RemoveBookmarksForUrls([this._uriForRemoval])
+ .transact().catch(Cu.reportError);
+ }
+ }
+ break;
+ case "keypress":
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimerEnabled = false;
+
+ if (aEvent.defaultPrevented) {
+ // The event has already been consumed inside of the panel.
+ break;
+ }
+
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_ESCAPE:
+ this.panel.hidePopup();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ if (aEvent.target.classList.contains("expander-up") ||
+ aEvent.target.classList.contains("expander-down") ||
+ aEvent.target.id == "editBMPanel_newFolderButton" ||
+ aEvent.target.id == "editBookmarkPanelRemoveButton") {
+ // XXX Why is this necessary? The defaultPrevented check should
+ // be enough.
+ break;
+ }
+ this.panel.hidePopup();
+ break;
+ // This case is for catching character-generating keypresses
+ case 0:
+ let accessKey = document.getElementById("key_close");
+ if (eventMatchesKey(aEvent, accessKey)) {
+ this.panel.hidePopup();
+ }
+ break;
+ }
+ break;
+ case "compositionend":
+ // After composition is committed, "mouseout" or something can set
+ // auto close timer.
+ this._isComposing = false;
+ break;
+ case "compositionstart":
+ if (aEvent.defaultPrevented) {
+ // If the composition was canceled, nothing to do here.
+ break;
+ }
+ this._isComposing = true;
+ // Explicit fall-through, during composition, panel shouldn't be
+ // hidden automatically.
+ case "input":
+ // Might have edited some text without keyboard events nor composition
+ // events. Fall-through to cancel auto close in such case.
+ case "mousedown":
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimerEnabled = false;
+ break;
+ case "mouseout":
+ if (!this._autoCloseTimerEnabled) {
+ // Don't autoclose the popup if the user has made a selection
+ // or keypress and then subsequently mouseout.
+ break;
+ }
+ // Explicit fall-through
+ case "popupshown":
+ // Don't handle events for descendent elements.
+ if (aEvent.target != aEvent.currentTarget) {
+ break;
+ }
+ // auto-close if new and not interacted with
+ if (this._isNewBookmark && !this._isComposing) {
+ // 3500ms matches the timeout that Pocket uses in
+ // browser/extensions/pocket/content/panels/js/saved.js
+ let delay = 3500;
+ if (this._closePanelQuickForTesting) {
+ delay /= 10;
+ }
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimer = setTimeout(() => {
+ if (!this.panel.mozMatchesSelector(":hover")) {
+ this.panel.hidePopup();
+ }
+ }, delay);
+ this._autoCloseTimerEnabled = true;
+ }
+ break;
+ }
+ },
+
+ _overlayLoaded: false,
+ _overlayLoading: false,
+ showEditBookmarkPopup: Task.async(function* (aNode, aAnchorElement, aPosition, aIsNewBookmark) {
+ // Slow double-clicks (not true double-clicks) shouldn't
+ // cause the panel to flicker.
+ if (this.panel.state == "showing" ||
+ this.panel.state == "open") {
+ return;
+ }
+
+ this._isNewBookmark = aIsNewBookmark;
+ this._uriForRemoval = "";
+ // TODO: Deprecate this once async transactions are enabled and the legacy
+ // transactions code is gone (bug 1131491) - we don't want addons to to use
+ // the completeNodeLikeObjectForItemId, so it's better if they keep passing
+ // the item-id for now).
+ if (typeof(aNode) == "number") {
+ let itemId = aNode;
+ if (PlacesUIUtils.useAsyncTransactions) {
+ let guid = yield PlacesUtils.promiseItemGuid(itemId);
+ aNode = yield PlacesUIUtils.promiseNodeLike(guid);
+ } else {
+ aNode = { itemId };
+ yield PlacesUIUtils.completeNodeLikeObjectForItemId(aNode);
+ }
+ }
+
+ // Performance: load the overlay the first time the panel is opened
+ // (see bug 392443).
+ if (this._overlayLoading)
+ return;
+
+ if (this._overlayLoaded) {
+ this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
+ return;
+ }
+
+ this._overlayLoading = true;
+ document.loadOverlay(
+ "chrome://browser/content/places/editBookmarkOverlay.xul",
+ (function(aSubject, aTopic, aData) {
+ // Move the header (star, title, button) into the grid,
+ // so that it aligns nicely with the other items (bug 484022).
+ let header = this._element("editBookmarkPanelHeader");
+ let rows = this._element("editBookmarkPanelGrid").lastChild;
+ rows.insertBefore(header, rows.firstChild);
+ header.hidden = false;
+
+ this._overlayLoading = false;
+ this._overlayLoaded = true;
+ this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
+ }).bind(this)
+ );
+ }),
+
+ _doShowEditBookmarkPanel: Task.async(function* (aNode, aAnchorElement, aPosition) {
+ if (this.panel.state != "closed")
+ return;
+
+ this._blockCommands(); // un-done in the popuphidden handler
+
+ this._element("editBookmarkPanelTitle").value =
+ this._isNewBookmark ?
+ gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
+ gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle");
+
+ // No description; show the Done, Remove;
+ this._element("editBookmarkPanelDescription").textContent = "";
+ this._element("editBookmarkPanelBottomButtons").hidden = false;
+ this._element("editBookmarkPanelContent").hidden = false;
+
+ // The label of the remove button differs if the URI is bookmarked
+ // multiple times.
+ let bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
+ let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
+ let label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length);
+ this._element("editBookmarkPanelRemoveButton").label = label;
+
+ // unset the unstarred state, if set
+ this._element("editBookmarkPanelStarIcon").removeAttribute("unstarred");
+
+ this._itemId = aNode.itemId;
+ this.beginBatch();
+
+ if (aAnchorElement) {
+ // Set the open=true attribute if the anchor is a
+ // descendent of a toolbarbutton.
+ let parent = aAnchorElement.parentNode;
+ while (parent) {
+ if (parent.localName == "toolbarbutton") {
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ if (parent) {
+ this._anchorToolbarButton = parent;
+ parent.setAttribute("open", "true");
+ }
+ }
+ let panel = this.panel;
+ let target = panel;
+ if (target.parentNode) {
+ // By targeting the panel's parent and using a capturing listener, we
+ // can have our listener called before others waiting for the panel to
+ // be shown (which probably expect the panel to be fully initialized)
+ target = target.parentNode;
+ }
+ target.addEventListener("popupshown", function shownListener(event) {
+ if (event.target == panel) {
+ target.removeEventListener("popupshown", shownListener, true);
+
+ gEditItemOverlay.initPanel({ node: aNode
+ , hiddenRows: ["description", "location",
+ "loadInSidebar", "keyword"]
+ , focusedElement: "preferred"});
+ }
+ }, true);
+
+ this.panel.openPopup(aAnchorElement, aPosition);
+ }),
+
+ panelShown:
+ function SU_panelShown(aEvent) {
+ if (aEvent.target == this.panel) {
+ if (this._element("editBookmarkPanelContent").hidden) {
+ // Note this isn't actually used anymore, we should remove this
+ // once we decide not to bring back the page bookmarked notification
+ this.panel.focus();
+ }
+ }
+ },
+
+ quitEditMode: function SU_quitEditMode() {
+ this._element("editBookmarkPanelContent").hidden = true;
+ this._element("editBookmarkPanelBottomButtons").hidden = true;
+ gEditItemOverlay.uninitPanel(true);
+ },
+
+ removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
+ this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
+ this.panel.hidePopup();
+ },
+
+ // Matching the way it is used in the Library, editBookmarkOverlay implements
+ // an instant-apply UI, having no batched-Undo/Redo support.
+ // However, in this context (the Star UI) we have a Cancel button whose
+ // expected behavior is to undo all the operations done in the panel.
+ // Sometime in the future this needs to be reimplemented using a
+ // non-instant apply code path, but for the time being, we patch-around
+ // editBookmarkOverlay so that all of the actions done in the panel
+ // are treated by PlacesTransactions as a single batch. To do so,
+ // we start a PlacesTransactions batch when the star UI panel is shown, and
+ // we keep the batch ongoing until the panel is hidden.
+ _batchBlockingDeferred: null,
+ beginBatch() {
+ if (this._batching)
+ return;
+ if (PlacesUIUtils.useAsyncTransactions) {
+ this._batchBlockingDeferred = PromiseUtils.defer();
+ PlacesTransactions.batch(function* () {
+ yield this._batchBlockingDeferred.promise;
+ }.bind(this));
+ } else {
+ PlacesUtils.transactionManager.beginBatch(null);
+ }
+ this._batching = true;
+ },
+
+ endBatch() {
+ if (!this._batching)
+ return;
+
+ if (PlacesUIUtils.useAsyncTransactions) {
+ this._batchBlockingDeferred.resolve();
+ this._batchBlockingDeferred = null;
+ } else {
+ PlacesUtils.transactionManager.endBatch(false);
+ }
+ this._batching = false;
+ }
+};
+
+var PlacesCommandHook = {
+ /**
+ * Adds a bookmark to the page loaded in the given browser.
+ *
+ * @param aBrowser
+ * a <browser> element.
+ * @param [optional] aParent
+ * The folder in which to create a new bookmark if the page loaded in
+ * aBrowser isn't bookmarked yet, defaults to the unfiled root.
+ * @param [optional] aShowEditUI
+ * whether or not to show the edit-bookmark UI for the bookmark item
+ */
+ bookmarkPage: Task.async(function* (aBrowser, aParent, aShowEditUI) {
+ if (PlacesUIUtils.useAsyncTransactions) {
+ yield this._bookmarkPagePT(aBrowser, aParent, aShowEditUI);
+ return;
+ }
+
+ var uri = aBrowser.currentURI;
+ var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
+ let isNewBookmark = itemId == -1;
+ if (isNewBookmark) {
+ // Bug 1148838 - Make this code work for full page plugins.
+ var title;
+ var description;
+ var charset;
+
+ let docInfo = yield this._getPageDetails(aBrowser);
+
+ try {
+ title = docInfo.isErrorPage ? PlacesUtils.history.getPageTitle(uri)
+ : aBrowser.contentTitle;
+ title = title || uri.spec;
+ description = docInfo.description;
+ charset = aBrowser.characterSet;
+ } catch (e) { }
+
+ if (aShowEditUI && isNewBookmark) {
+ // If we bookmark the page here but open right into a cancelable
+ // state (i.e. new bookmark in Library), start batching here so
+ // all of the actions can be undone in a single undo step.
+ StarUI.beginBatch();
+ }
+
+ var parent = aParent !== undefined ?
+ aParent : PlacesUtils.unfiledBookmarksFolderId;
+ var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
+ var txn = new PlacesCreateBookmarkTransaction(uri, parent,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title, null, [descAnno]);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ itemId = txn.item.id;
+ // Set the character-set.
+ if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
+ PlacesUtils.setCharsetForURI(uri, charset);
+ }
+
+ // Revert the contents of the location bar
+ gURLBar.handleRevert();
+
+ // If it was not requested to open directly in "edit" mode, we are done.
+ if (!aShowEditUI)
+ return;
+
+ // Try to dock the panel to:
+ // 1. the bookmarks menu button
+ // 2. the identity icon
+ // 3. the content area
+ if (BookmarkingUI.anchor) {
+ StarUI.showEditBookmarkPopup(itemId, BookmarkingUI.anchor,
+ "bottomcenter topright", isNewBookmark);
+ return;
+ }
+
+ let identityIcon = document.getElementById("identity-icon");
+ if (isElementVisible(identityIcon)) {
+ StarUI.showEditBookmarkPopup(itemId, identityIcon,
+ "bottomcenter topright", isNewBookmark);
+ } else {
+ StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap", isNewBookmark);
+ }
+ }),
+
+ // TODO: Replace bookmarkPage code with this function once legacy
+ // transactions are removed.
+ _bookmarkPagePT: Task.async(function* (aBrowser, aParentId, aShowEditUI) {
+ let url = new URL(aBrowser.currentURI.spec);
+ let info = yield PlacesUtils.bookmarks.fetch({ url });
+ let isNewBookmark = !info;
+ if (!info) {
+ let parentGuid = aParentId !== undefined ?
+ yield PlacesUtils.promiseItemGuid(aParentId) :
+ PlacesUtils.bookmarks.unfiledGuid;
+ info = { url, parentGuid };
+ // Bug 1148838 - Make this code work for full page plugins.
+ let description = null;
+ let charset = null;
+
+ let docInfo = yield this._getPageDetails(aBrowser);
+
+ try {
+ info.title = docInfo.isErrorPage ?
+ (yield PlacesUtils.promisePlaceInfo(aBrowser.currentURI)).title :
+ aBrowser.contentTitle;
+ info.title = info.title || url.href;
+ description = docInfo.description;
+ charset = aBrowser.characterSet;
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ if (aShowEditUI && isNewBookmark) {
+ // If we bookmark the page here but open right into a cancelable
+ // state (i.e. new bookmark in Library), start batching here so
+ // all of the actions can be undone in a single undo step.
+ StarUI.beginBatch();
+ }
+
+ if (description) {
+ info.annotations = [{ name: PlacesUIUtils.DESCRIPTION_ANNO
+ , value: description }];
+ }
+
+ info.guid = yield PlacesTransactions.NewBookmark(info).transact();
+
+ // Set the character-set
+ if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
+ PlacesUtils.setCharsetForURI(makeURI(url.href), charset);
+ }
+
+ // Revert the contents of the location bar
+ gURLBar.handleRevert();
+
+ // If it was not requested to open directly in "edit" mode, we are done.
+ if (!aShowEditUI)
+ return;
+
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(info);
+
+ // Try to dock the panel to:
+ // 1. the bookmarks menu button
+ // 2. the identity icon
+ // 3. the content area
+ if (BookmarkingUI.anchor) {
+ StarUI.showEditBookmarkPopup(node, BookmarkingUI.anchor,
+ "bottomcenter topright", isNewBookmark);
+ return;
+ }
+
+ let identityIcon = document.getElementById("identity-icon");
+ if (isElementVisible(identityIcon)) {
+ StarUI.showEditBookmarkPopup(node, identityIcon,
+ "bottomcenter topright", isNewBookmark);
+ } else {
+ StarUI.showEditBookmarkPopup(node, aBrowser, "overlap", isNewBookmark);
+ }
+ }),
+
+ _getPageDetails(browser) {
+ return new Promise(resolve => {
+ let mm = browser.messageManager;
+ mm.addMessageListener("Bookmarks:GetPageDetails:Result", function listener(msg) {
+ mm.removeMessageListener("Bookmarks:GetPageDetails:Result", listener);
+ resolve(msg.data);
+ });
+
+ mm.sendAsyncMessage("Bookmarks:GetPageDetails", { })
+ });
+ },
+
+ /**
+ * Adds a bookmark to the page loaded in the current tab.
+ */
+ bookmarkCurrentPage: function PCH_bookmarkCurrentPage(aShowEditUI, aParent) {
+ this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI);
+ },
+
+ /**
+ * Adds a bookmark to the page targeted by a link.
+ * @param aParent
+ * The folder in which to create a new bookmark if aURL isn't
+ * bookmarked.
+ * @param aURL (string)
+ * the address of the link target
+ * @param aTitle
+ * The link text
+ * @param [optional] aDescription
+ * The linked page description, if available
+ */
+ bookmarkLink: Task.async(function* (aParentId, aURL, aTitle, aDescription = "") {
+ let node = yield PlacesUIUtils.fetchNodeLike({ url: aURL });
+ if (node) {
+ PlacesUIUtils.showBookmarkDialog({ action: "edit"
+ , node
+ }, window.top);
+ return;
+ }
+
+ let ip = new InsertionPoint(aParentId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "bookmark"
+ , uri: makeURI(aURL)
+ , title: aTitle
+ , description: aDescription
+ , defaultInsertionPoint: ip
+ , hiddenRows: [ "description"
+ , "location"
+ , "loadInSidebar"
+ , "keyword" ]
+ }, window.top);
+ }),
+
+ /**
+ * List of nsIURI objects characterizing the tabs currently open in the
+ * browser, modulo pinned tabs. The URIs will be in the order in which their
+ * corresponding tabs appeared and duplicates are discarded.
+ */
+ get uniqueCurrentPages() {
+ let uniquePages = {};
+ let URIs = [];
+
+ gBrowser.visibleTabs.forEach(tab => {
+ let browser = tab.linkedBrowser;
+ let uri = browser.currentURI;
+ let title = browser.contentTitle || tab.label;
+ let spec = uri.spec;
+ if (!tab.pinned && !(spec in uniquePages)) {
+ uniquePages[spec] = null;
+ URIs.push({ uri, title });
+ }
+ });
+ return URIs;
+ },
+
+ /**
+ * Adds a folder with bookmarks to all of the currently open tabs in this
+ * window.
+ */
+ bookmarkCurrentPages: function PCH_bookmarkCurrentPages() {
+ let pages = this.uniqueCurrentPages;
+ if (pages.length > 1) {
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "folder"
+ , URIList: pages
+ , hiddenRows: [ "description" ]
+ }, window);
+ }
+ },
+
+ /**
+ * Updates disabled state for the "Bookmark All Tabs" command.
+ */
+ updateBookmarkAllTabsCommand:
+ function PCH_updateBookmarkAllTabsCommand() {
+ // There's nothing to do in non-browser windows.
+ if (window.location.href != getBrowserURL())
+ return;
+
+ // Disable "Bookmark All Tabs" if there are less than two
+ // "unique current pages".
+ goSetCommandEnabled("Browser:BookmarkAllTabs",
+ this.uniqueCurrentPages.length >= 2);
+ },
+
+ /**
+ * Adds a Live Bookmark to a feed associated with the current page.
+ * @param url
+ * The nsIURI of the page the feed was attached to
+ * @title title
+ * The title of the feed. Optional.
+ * @subtitle subtitle
+ * A short description of the feed. Optional.
+ */
+ addLiveBookmark: Task.async(function *(url, feedTitle, feedSubtitle) {
+ let toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+
+ let feedURI = makeURI(url);
+ let title = feedTitle || gBrowser.contentTitle;
+ let description = feedSubtitle;
+ if (!description) {
+ description = (yield this._getPageDetails(gBrowser.selectedBrowser)).description;
+ }
+
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "livemark"
+ , feedURI
+ , siteURI: gBrowser.currentURI
+ , title
+ , description
+ , defaultInsertionPoint: toolbarIP
+ , hiddenRows: [ "feedLocation"
+ , "siteLocation"
+ , "description" ]
+ }, window);
+ }),
+
+ /**
+ * Opens the Places Organizer.
+ * @param aLeftPaneRoot
+ * The query to select in the organizer window - options
+ * are: History, AllBookmarks, BookmarksMenu, BookmarksToolbar,
+ * UnfiledBookmarks, Tags and Downloads.
+ */
+ showPlacesOrganizer: function PCH_showPlacesOrganizer(aLeftPaneRoot) {
+ var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
+ // Due to bug 528706, getMostRecentWindow can return closed windows.
+ if (!organizer || organizer.closed) {
+ // No currently open places window, so open one with the specified mode.
+ openDialog("chrome://browser/content/places/places.xul",
+ "", "chrome,toolbar=yes,dialog=no,resizable", aLeftPaneRoot);
+ } else {
+ organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
+ organizer.focus();
+ }
+ }
+};
+
+XPCOMUtils.defineLazyModuleGetter(this, "RecentlyClosedTabsAndWindowsMenuUtils",
+ "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm");
+
+// View for the history menu.
+function HistoryMenu(aPopupShowingEvent) {
+ // Workaround for Bug 610187. The sidebar does not include all the Places
+ // views definitions, and we don't need them there.
+ // Defining the prototype inheritance in the prototype itself would cause
+ // browser.js to halt on "PlacesMenu is not defined" error.
+ this.__proto__.__proto__ = PlacesMenu.prototype;
+ PlacesMenu.call(this, aPopupShowingEvent,
+ "place:sort=4&maxResults=15");
+}
+
+HistoryMenu.prototype = {
+ _getClosedTabCount() {
+ // SessionStore doesn't track the hidden window, so just return zero then.
+ if (window == Services.appShell.hiddenDOMWindow) {
+ return 0;
+ }
+
+ return SessionStore.getClosedTabCount(window);
+ },
+
+ toggleRecentlyClosedTabs: function HM_toggleRecentlyClosedTabs() {
+ // enable/disable the Recently Closed Tabs sub menu
+ var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0];
+
+ // no restorable tabs, so disable menu
+ if (this._getClosedTabCount() == 0)
+ undoMenu.setAttribute("disabled", true);
+ else
+ undoMenu.removeAttribute("disabled");
+ },
+
+ /**
+ * Populate when the history menu is opened
+ */
+ populateUndoSubmenu: function PHM_populateUndoSubmenu() {
+ var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0];
+ var undoPopup = undoMenu.firstChild;
+
+ // remove existing menu items
+ while (undoPopup.hasChildNodes())
+ undoPopup.removeChild(undoPopup.firstChild);
+
+ // no restorable tabs, so make sure menu is disabled, and return
+ if (this._getClosedTabCount() == 0) {
+ undoMenu.setAttribute("disabled", true);
+ return;
+ }
+
+ // enable menu
+ undoMenu.removeAttribute("disabled");
+
+ // populate menu
+ let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment(window, "menuitem");
+ undoPopup.appendChild(tabsFragment);
+ },
+
+ toggleRecentlyClosedWindows: function PHM_toggleRecentlyClosedWindows() {
+ // enable/disable the Recently Closed Windows sub menu
+ var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0];
+
+ // no restorable windows, so disable menu
+ if (SessionStore.getClosedWindowCount() == 0)
+ undoMenu.setAttribute("disabled", true);
+ else
+ undoMenu.removeAttribute("disabled");
+ },
+
+ /**
+ * Populate when the history menu is opened
+ */
+ populateUndoWindowSubmenu: function PHM_populateUndoWindowSubmenu() {
+ let undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0];
+ let undoPopup = undoMenu.firstChild;
+
+ // remove existing menu items
+ while (undoPopup.hasChildNodes())
+ undoPopup.removeChild(undoPopup.firstChild);
+
+ // no restorable windows, so make sure menu is disabled, and return
+ if (SessionStore.getClosedWindowCount() == 0) {
+ undoMenu.setAttribute("disabled", true);
+ return;
+ }
+
+ // enable menu
+ undoMenu.removeAttribute("disabled");
+
+ // populate menu
+ let windowsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment(window, "menuitem");
+ undoPopup.appendChild(windowsFragment);
+ },
+
+ toggleTabsFromOtherComputers: function PHM_toggleTabsFromOtherComputers() {
+ // Enable/disable the Tabs From Other Computers menu. Some of the menus handled
+ // by HistoryMenu do not have this menuitem.
+ let menuitem = this._rootElt.getElementsByClassName("syncTabsMenuItem")[0];
+ if (!menuitem)
+ return;
+
+ if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) {
+ menuitem.setAttribute("hidden", true);
+ return;
+ }
+
+ menuitem.setAttribute("hidden", false);
+ },
+
+ _onPopupShowing: function HM__onPopupShowing(aEvent) {
+ PlacesMenu.prototype._onPopupShowing.apply(this, arguments);
+
+ // Don't handle events for submenus.
+ if (aEvent.target != aEvent.currentTarget)
+ return;
+
+ this.toggleRecentlyClosedTabs();
+ this.toggleRecentlyClosedWindows();
+ this.toggleTabsFromOtherComputers();
+ },
+
+ _onCommand: function HM__onCommand(aEvent) {
+ let placesNode = aEvent.target._placesNode;
+ if (placesNode) {
+ if (!PrivateBrowsingUtils.isWindowPrivate(window))
+ PlacesUIUtils.markPageAsTyped(placesNode.uri);
+ openUILink(placesNode.uri, aEvent, { ignoreAlt: true });
+ }
+ }
+};
+
+/**
+ * Functions for handling events in the Bookmarks Toolbar and menu.
+ */
+var BookmarksEventHandler = {
+ /**
+ * Handler for click event for an item in the bookmarks toolbar or menu.
+ * Menus and submenus from the folder buttons bubble up to this handler.
+ * Left-click is handled in the onCommand function.
+ * When items are middle-clicked (or clicked with modifier), open in tabs.
+ * If the click came through a menu, close the menu.
+ * @param aEvent
+ * DOMEvent for the click
+ * @param aView
+ * The places view which aEvent should be associated with.
+ */
+ onClick: function BEH_onClick(aEvent, aView) {
+ // Only handle middle-click or left-click with modifiers.
+ let modifKey;
+#ifdef XP_MACOSX
+ modifKey = aEvent.metaKey || aEvent.shiftKey
+#else
+ modifKey = aEvent.ctrlKey || aEvent.shiftKey
+#endif
+
+ if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey))
+ return;
+
+ var target = aEvent.originalTarget;
+ // If this event bubbled up from a menu or menuitem, close the menus.
+ // Do this before opening tabs, to avoid hiding the open tabs confirm-dialog.
+ if (target.localName == "menu" || target.localName == "menuitem") {
+ for (let node = target.parentNode; node; node = node.parentNode) {
+ if (node.localName == "menupopup")
+ node.hidePopup();
+ else if (node.localName != "menu" &&
+ node.localName != "hbox" &&
+ node.localName != "vbox" )
+ break;
+ }
+ }
+
+ if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) {
+ // Don't open the root folder in tabs when the empty area on the toolbar
+ // is middle-clicked or when a non-bookmark item except for Open in Tabs)
+ // in a bookmarks menupopup is middle-clicked.
+ if (target.localName == "menu" || target.localName == "toolbarbutton")
+ PlacesUIUtils.openContainerNodeInTabs(target._placesNode, aEvent, aView);
+ } else if (aEvent.button == 1) {
+ // left-clicks with modifier are already served by onCommand
+ this.onCommand(aEvent, aView);
+ }
+ },
+
+ /**
+ * Handler for command event for an item in the bookmarks toolbar.
+ * Menus and submenus from the folder buttons bubble up to this handler.
+ * Opens the item.
+ * @param aEvent
+ * DOMEvent for the command
+ * @param aView
+ * The places view which aEvent should be associated with.
+ */
+ onCommand: function BEH_onCommand(aEvent, aView) {
+ var target = aEvent.originalTarget;
+ if (target._placesNode)
+ PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView);
+ },
+
+ fillInBHTooltip: function BEH_fillInBHTooltip(aDocument, aEvent) {
+ var node;
+ var cropped = false;
+ var targetURI;
+
+ if (aDocument.tooltipNode.localName == "treechildren") {
+ var tree = aDocument.tooltipNode.parentNode;
+ var tbo = tree.treeBoxObject;
+ var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
+ if (cell.row == -1)
+ return false;
+ node = tree.view.nodeForTreeIndex(cell.row);
+ cropped = tbo.isCellCropped(cell.row, cell.col);
+ } else {
+ // Check whether the tooltipNode is a Places node.
+ // In such a case use it, otherwise check for targetURI attribute.
+ var tooltipNode = aDocument.tooltipNode;
+ if (tooltipNode._placesNode)
+ node = tooltipNode._placesNode;
+ else {
+ // This is a static non-Places node.
+ targetURI = tooltipNode.getAttribute("targetURI");
+ }
+ }
+
+ if (!node && !targetURI)
+ return false;
+
+ // Show node.label as tooltip's title for non-Places nodes.
+ var title = node ? node.title : tooltipNode.label;
+
+ // Show URL only for Places URI-nodes or nodes with a targetURI attribute.
+ var url;
+ if (targetURI || PlacesUtils.nodeIsURI(node))
+ url = targetURI || node.uri;
+
+ // Show tooltip for containers only if their title is cropped.
+ if (!cropped && !url)
+ return false;
+
+ var tooltipTitle = aDocument.getElementById("bhtTitleText");
+ tooltipTitle.hidden = (!title || (title == url));
+ if (!tooltipTitle.hidden)
+ tooltipTitle.textContent = title;
+
+ var tooltipUrl = aDocument.getElementById("bhtUrlText");
+ tooltipUrl.hidden = !url;
+ if (!tooltipUrl.hidden)
+ tooltipUrl.value = url;
+
+ // Show tooltip.
+ return true;
+ }
+};
+
+// Handles special drag and drop functionality for Places menus that are not
+// part of a Places view (e.g. the bookmarks menu in the menubar).
+var PlacesMenuDNDHandler = {
+ _springLoadDelayMs: 350,
+ _closeDelayMs: 500,
+ _loadTimer: null,
+ _closeTimer: null,
+ _closingTimerNode: null,
+
+ /**
+ * Called when the user enters the <menu> element during a drag.
+ * @param event
+ * The DragEnter event that spawned the opening.
+ */
+ onDragEnter: function PMDH_onDragEnter(event) {
+ // Opening menus in a Places popup is handled by the view itself.
+ if (!this._isStaticContainer(event.target))
+ return;
+
+ // If we re-enter the same menu or anchor before the close timer runs out,
+ // we should ensure that we do not close:
+ if (this._closeTimer && this._closingTimerNode === event.currentTarget) {
+ this._closeTimer.cancel();
+ this._closingTimerNode = null;
+ this._closeTimer = null;
+ }
+
+ PlacesControllerDragHelper.currentDropTarget = event.target;
+ let popup = event.target.lastChild;
+ if (this._loadTimer || popup.state === "showing" || popup.state === "open")
+ return;
+
+ this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._loadTimer.initWithCallback(() => {
+ this._loadTimer = null;
+ popup.setAttribute("autoopened", "true");
+ popup.showPopup(popup);
+ }, this._springLoadDelayMs, Ci.nsITimer.TYPE_ONE_SHOT);
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Handles dragleave on the <menu> element.
+ */
+ onDragLeave: function PMDH_onDragLeave(event) {
+ // Handle menu-button separate targets.
+ if (event.relatedTarget === event.currentTarget ||
+ (event.relatedTarget &&
+ event.relatedTarget.parentNode === event.currentTarget))
+ return;
+
+ // Closing menus in a Places popup is handled by the view itself.
+ if (!this._isStaticContainer(event.target))
+ return;
+
+ PlacesControllerDragHelper.currentDropTarget = null;
+ let popup = event.target.lastChild;
+
+ if (this._loadTimer) {
+ this._loadTimer.cancel();
+ this._loadTimer = null;
+ }
+ this._closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._closingTimerNode = event.currentTarget;
+ this._closeTimer.initWithCallback(function() {
+ this._closeTimer = null;
+ this._closingTimerNode = null;
+ let node = PlacesControllerDragHelper.currentDropTarget;
+ let inHierarchy = false;
+ while (node && !inHierarchy) {
+ inHierarchy = node == event.target;
+ node = node.parentNode;
+ }
+ if (!inHierarchy && popup && popup.hasAttribute("autoopened")) {
+ popup.removeAttribute("autoopened");
+ popup.hidePopup();
+ }
+ }, this._closeDelayMs, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /**
+ * Determines if a XUL element represents a static container.
+ * @returns true if the element is a container element (menu or
+ *` menu-toolbarbutton), false otherwise.
+ */
+ _isStaticContainer: function PMDH__isContainer(node) {
+ let isMenu = node.localName == "menu" ||
+ (node.localName == "toolbarbutton" &&
+ (node.getAttribute("type") == "menu" ||
+ node.getAttribute("type") == "menu-button"));
+ let isStatic = !("_placesNode" in node) && node.lastChild &&
+ node.lastChild.hasAttribute("placespopup") &&
+ !node.parentNode.hasAttribute("placespopup");
+ return isMenu && isStatic;
+ },
+
+ /**
+ * Called when the user drags over the <menu> element.
+ * @param event
+ * The DragOver event.
+ */
+ onDragOver: function PMDH_onDragOver(event) {
+ let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+ if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer))
+ event.preventDefault();
+
+ event.stopPropagation();
+ },
+
+ /**
+ * Called when the user drops on the <menu> element.
+ * @param event
+ * The Drop event.
+ */
+ onDrop: function PMDH_onDrop(event) {
+ // Put the item at the end of bookmark menu.
+ let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+ PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
+ PlacesControllerDragHelper.currentDropTarget = null;
+ event.stopPropagation();
+ }
+};
+
+/**
+ * This object handles the initialization and uninitialization of the bookmarks
+ * toolbar.
+ */
+var PlacesToolbarHelper = {
+ _place: "place:folder=TOOLBAR",
+
+ get _viewElt() {
+ return document.getElementById("PlacesToolbar");
+ },
+
+ get _placeholder() {
+ return document.getElementById("bookmarks-toolbar-placeholder");
+ },
+
+ init: function PTH_init(forceToolbarOverflowCheck) {
+ let viewElt = this._viewElt;
+ if (!viewElt || viewElt._placesView)
+ return;
+
+ // CustomizableUI.addListener is idempotent, so we can safely
+ // call this multiple times.
+ CustomizableUI.addListener(this);
+
+ // If the bookmarks toolbar item is:
+ // - not in a toolbar, or;
+ // - the toolbar is collapsed, or;
+ // - the toolbar is hidden some other way:
+ // don't initialize. Also, there is no need to initialize the toolbar if
+ // customizing, because that will happen when the customization is done.
+ let toolbar = this._getParentToolbar(viewElt);
+ if (!toolbar || toolbar.collapsed || this._isCustomizing ||
+ getComputedStyle(toolbar, "").display == "none")
+ return;
+
+ new PlacesToolbar(this._place);
+ if (forceToolbarOverflowCheck) {
+ viewElt._placesView.updateOverflowStatus();
+ }
+ this._shouldWrap = false;
+ this._setupPlaceholder();
+ },
+
+ uninit: function PTH_uninit() {
+ CustomizableUI.removeListener(this);
+ },
+
+ customizeStart: function PTH_customizeStart() {
+ try {
+ let viewElt = this._viewElt;
+ if (viewElt && viewElt._placesView)
+ viewElt._placesView.uninit();
+ } finally {
+ this._isCustomizing = true;
+ }
+ this._shouldWrap = this._getShouldWrap();
+ },
+
+ customizeChange: function PTH_customizeChange() {
+ this._setupPlaceholder();
+ },
+
+ _setupPlaceholder: function PTH_setupPlaceholder() {
+ let placeholder = this._placeholder;
+ if (!placeholder) {
+ return;
+ }
+
+ let shouldWrapNow = this._getShouldWrap();
+ if (this._shouldWrap != shouldWrapNow) {
+ if (shouldWrapNow) {
+ placeholder.setAttribute("wrap", "true");
+ } else {
+ placeholder.removeAttribute("wrap");
+ }
+ this._shouldWrap = shouldWrapNow;
+ }
+ },
+
+ customizeDone: function PTH_customizeDone() {
+ this._isCustomizing = false;
+ this.init(true);
+ },
+
+ _getShouldWrap: function PTH_getShouldWrap() {
+ let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
+ let area = placement && placement.area;
+ let areaType = area && CustomizableUI.getAreaType(area);
+ return !area || CustomizableUI.TYPE_MENU_PANEL == areaType;
+ },
+
+ onPlaceholderCommand() {
+ let widgetGroup = CustomizableUI.getWidget("personal-bookmarks");
+ let widget = widgetGroup.forWindow(window);
+ if (widget.overflowed ||
+ widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
+ PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar");
+ }
+ },
+
+ _getParentToolbar(element) {
+ while (element) {
+ if (element.localName == "toolbar") {
+ return element;
+ }
+ element = element.parentNode;
+ }
+ return null;
+ },
+
+ onWidgetUnderflow(aNode, aContainer) {
+ // The view gets broken by being removed and reinserted by the overflowable
+ // toolbar, so we have to force an uninit and reinit.
+ let win = aNode.ownerGlobal;
+ if (aNode.id == "personal-bookmarks" && win == window) {
+ this._resetView();
+ }
+ },
+
+ onWidgetAdded(aWidgetId, aArea, aPosition) {
+ if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) {
+ // It's possible (with the "Add to Menu", "Add to Toolbar" context
+ // options) that the Places Toolbar Items have been moved without
+ // letting us prepare and handle it with with customizeStart and
+ // customizeDone. If that's the case, we need to reset the views
+ // since they're probably broken from the DOM reparenting.
+ this._resetView();
+ }
+ },
+
+ _resetView() {
+ if (this._viewElt) {
+ // It's possible that the placesView might not exist, and we need to
+ // do a full init. This could happen if the Bookmarks Toolbar Items are
+ // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar"
+ // context menu option, outside of customize mode.
+ if (this._viewElt._placesView) {
+ this._viewElt._placesView.uninit();
+ }
+ this.init(true);
+ }
+ },
+};
+
+/**
+ * Handles the bookmarks menu-button in the toolbar.
+ */
+
+var BookmarkingUI = {
+ BOOKMARK_BUTTON_ID: "bookmarks-menu-button",
+ BOOKMARK_BUTTON_SHORTCUT: "addBookmarkAsKb",
+ get button() {
+ delete this.button;
+ let widgetGroup = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID);
+ return this.button = widgetGroup.forWindow(window).node;
+ },
+
+ /* Can't make this a self-deleting getter because it's anonymous content
+ * and might lose/regain bindings at some point. */
+ get star() {
+ return document.getAnonymousElementByAttribute(this.button, "anonid",
+ "button");
+ },
+
+ get anchor() {
+ if (!this._shouldUpdateStarState()) {
+ return null;
+ }
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
+ .forWindow(window);
+ if (widget.overflowed)
+ return widget.anchor;
+
+ let star = this.star;
+ return star ? document.getAnonymousElementByAttribute(star, "class",
+ "toolbarbutton-icon")
+ : null;
+ },
+
+ get notifier() {
+ delete this.notifier;
+ return this.notifier = document.getElementById("bookmarked-notification-anchor");
+ },
+
+ get dropmarkerNotifier() {
+ delete this.dropmarkerNotifier;
+ return this.dropmarkerNotifier = document.getElementById("bookmarked-notification-dropmarker-anchor");
+ },
+
+ get broadcaster() {
+ delete this.broadcaster;
+ let broadcaster = document.getElementById("bookmarkThisPageBroadcaster");
+ return this.broadcaster = broadcaster;
+ },
+
+ STATUS_UPDATING: -1,
+ STATUS_UNSTARRED: 0,
+ STATUS_STARRED: 1,
+ get status() {
+ if (!this._shouldUpdateStarState()) {
+ return this.STATUS_UNSTARRED;
+ }
+ if (this._pendingStmt)
+ return this.STATUS_UPDATING;
+ return this.button.hasAttribute("starred") ? this.STATUS_STARRED
+ : this.STATUS_UNSTARRED;
+ },
+
+ get _starredTooltip() {
+ delete this._starredTooltip;
+ return this._starredTooltip =
+ this._getFormattedTooltip("starButtonOn.tooltip2");
+ },
+
+ get _unstarredTooltip() {
+ delete this._unstarredTooltip;
+ return this._unstarredTooltip =
+ this._getFormattedTooltip("starButtonOff.tooltip2");
+ },
+
+ _getFormattedTooltip(strId) {
+ let args = [];
+ let shortcut = document.getElementById(this.BOOKMARK_BUTTON_SHORTCUT);
+ if (shortcut)
+ args.push(ShortcutUtils.prettifyShortcut(shortcut));
+ return gNavigatorBundle.getFormattedString(strId, args);
+ },
+
+ /**
+ * The type of the area in which the button is currently located.
+ * When in the panel, we don't update the button's icon.
+ */
+ _currentAreaType: null,
+ _shouldUpdateStarState() {
+ return this._currentAreaType == CustomizableUI.TYPE_TOOLBAR;
+ },
+
+ /**
+ * The popup contents must be updated when the user customizes the UI, or
+ * changes the personal toolbar collapsed status. In such a case, any needed
+ * change should be handled in the popupshowing helper, for performance
+ * reasons.
+ */
+ _popupNeedsUpdate: true,
+ onToolbarVisibilityChange: function BUI_onToolbarVisibilityChange() {
+ this._popupNeedsUpdate = true;
+ },
+
+ onPopupShowing: function BUI_onPopupShowing(event) {
+ // Don't handle events for submenus.
+ if (event.target != event.currentTarget)
+ return;
+
+ // Ideally this code would never be reached, but if you click the outer
+ // button's border, some cpp code for the menu button's so-called XBL binding
+ // decides to open the popup even though the dropmarker is invisible.
+ if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
+ this._showSubview();
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
+ .forWindow(window);
+ if (widget.overflowed) {
+ // Don't open a popup in the overflow popup, rather just open the Library.
+ event.preventDefault();
+ widget.node.removeAttribute("closemenu");
+ PlacesCommandHook.showPlacesOrganizer("BookmarksMenu");
+ return;
+ }
+
+ this._initRecentBookmarks(document.getElementById("BMB_recentBookmarks"),
+ "subviewbutton");
+
+ if (!this._popupNeedsUpdate)
+ return;
+ this._popupNeedsUpdate = false;
+
+ let popup = event.target;
+ let getPlacesAnonymousElement =
+ aAnonId => document.getAnonymousElementByAttribute(popup.parentNode,
+ "placesanonid",
+ aAnonId);
+
+ let viewToolbarMenuitem = getPlacesAnonymousElement("view-toolbar");
+ if (viewToolbarMenuitem) {
+ // Update View bookmarks toolbar checkbox menuitem.
+ viewToolbarMenuitem.classList.add("subviewbutton");
+ let personalToolbar = document.getElementById("PersonalToolbar");
+ viewToolbarMenuitem.setAttribute("checked", !personalToolbar.collapsed);
+ }
+ },
+
+ attachPlacesView(event, node) {
+ // If the view is already there, bail out early.
+ if (node.parentNode._placesView)
+ return;
+
+ new PlacesMenu(event, "place:folder=BOOKMARKS_MENU", {
+ extraClasses: {
+ entry: "subviewbutton",
+ footer: "panel-subview-footer"
+ },
+ insertionPoint: ".panel-subview-footer"
+ });
+ },
+
+ RECENTLY_BOOKMARKED_PREF: "browser.bookmarks.showRecentlyBookmarked",
+
+ _initRecentBookmarks(aHeaderItem, aExtraCSSClass) {
+ this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass);
+
+ // Add observers and listeners and remove them again when the menupopup closes.
+
+ let bookmarksMenu = aHeaderItem.parentNode;
+ let placesContextMenu = document.getElementById("placesContext");
+
+ let prefObserver = () => {
+ this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass);
+ };
+
+ this._recentlyBookmarkedObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ Ci.nsISupportsWeakReference
+ ])
+ };
+ this._recentlyBookmarkedObserver.onItemRemoved = () => {
+ // Update the menu when a bookmark has been removed.
+ // The native menubar on Mac doesn't support live update, so this won't
+ // work there.
+ this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass);
+ };
+
+ let updatePlacesContextMenu = (shouldHidePrefUI = false) => {
+ let prefEnabled = !shouldHidePrefUI && Services.prefs.getBoolPref(this.RECENTLY_BOOKMARKED_PREF);
+ let showItem = document.getElementById("placesContext_showRecentlyBookmarked");
+ let hideItem = document.getElementById("placesContext_hideRecentlyBookmarked");
+ let separator = document.getElementById("placesContext_recentlyBookmarkedSeparator");
+ showItem.hidden = shouldHidePrefUI || prefEnabled;
+ hideItem.hidden = shouldHidePrefUI || !prefEnabled;
+ separator.hidden = shouldHidePrefUI;
+ if (!shouldHidePrefUI) {
+ // Move to the bottom of the menu.
+ separator.parentNode.appendChild(separator);
+ showItem.parentNode.appendChild(showItem);
+ hideItem.parentNode.appendChild(hideItem);
+ }
+ };
+
+ let onPlacesContextMenuShowing = event => {
+ if (event.target == event.currentTarget) {
+ let triggerPopup = event.target.triggerNode;
+ while (triggerPopup && triggerPopup.localName != "menupopup") {
+ triggerPopup = triggerPopup.parentNode;
+ }
+ let shouldHidePrefUI = triggerPopup != bookmarksMenu;
+ updatePlacesContextMenu(shouldHidePrefUI);
+ }
+ };
+
+ let onBookmarksMenuHidden = event => {
+ if (event.target == event.currentTarget) {
+ updatePlacesContextMenu(true);
+
+ Services.prefs.removeObserver(this.RECENTLY_BOOKMARKED_PREF, prefObserver);
+ PlacesUtils.bookmarks.removeObserver(this._recentlyBookmarkedObserver);
+ this._recentlyBookmarkedObserver = null;
+ if (placesContextMenu) {
+ placesContextMenu.removeEventListener("popupshowing", onPlacesContextMenuShowing);
+ }
+ bookmarksMenu.removeEventListener("popuphidden", onBookmarksMenuHidden);
+ }
+ };
+
+ Services.prefs.addObserver(this.RECENTLY_BOOKMARKED_PREF, prefObserver, false);
+ PlacesUtils.bookmarks.addObserver(this._recentlyBookmarkedObserver, true);
+
+ // The context menu doesn't exist in non-browser windows on Mac
+ if (placesContextMenu) {
+ placesContextMenu.addEventListener("popupshowing", onPlacesContextMenuShowing);
+ }
+
+ bookmarksMenu.addEventListener("popuphidden", onBookmarksMenuHidden);
+ },
+
+ _populateRecentBookmarks(aHeaderItem, aExtraCSSClass = "") {
+ while (aHeaderItem.nextSibling &&
+ aHeaderItem.nextSibling.localName == "menuitem") {
+ aHeaderItem.nextSibling.remove();
+ }
+
+ let shouldShow = Services.prefs.getBoolPref(this.RECENTLY_BOOKMARKED_PREF);
+ let separator = aHeaderItem.previousSibling;
+ aHeaderItem.hidden = !shouldShow;
+ separator.hidden = !shouldShow;
+
+ if (!shouldShow) {
+ return;
+ }
+
+ const kMaxResults = 5;
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.excludeQueries = true;
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ options.sortingMode = options.SORT_BY_DATEADDED_DESCENDING;
+ options.maxResults = kMaxResults;
+ let query = PlacesUtils.history.getNewQuery();
+
+ let sh = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ let loadingPrincipal = sh.serializeToString(document.nodePrincipal);
+
+ let fragment = document.createDocumentFragment();
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = node.uri;
+ let title = node.title;
+ let icon = node.icon;
+
+ let item =
+ document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "menuitem");
+ item.setAttribute("label", title || uri);
+ item.setAttribute("targetURI", uri);
+ item.setAttribute("simulated-places-node", true);
+ item.setAttribute("class", "menuitem-iconic menuitem-with-favicon bookmark-item " +
+ aExtraCSSClass);
+ if (icon) {
+ item.setAttribute("image", icon);
+ item.setAttribute("loadingprincipal", loadingPrincipal);
+ }
+ item._placesNode = node;
+ fragment.appendChild(item);
+ }
+ root.containerOpen = false;
+ aHeaderItem.parentNode.insertBefore(fragment, aHeaderItem.nextSibling);
+ },
+
+ showRecentlyBookmarked() {
+ Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, true);
+ },
+
+ hideRecentlyBookmarked() {
+ Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, false);
+ },
+
+ _updateCustomizationState: function BUI__updateCustomizationState() {
+ let placement = CustomizableUI.getPlacementOfWidget(this.BOOKMARK_BUTTON_ID);
+ this._currentAreaType = placement && CustomizableUI.getAreaType(placement.area);
+ },
+
+ _uninitView: function BUI__uninitView() {
+ // When an element with a placesView attached is removed and re-inserted,
+ // XBL reapplies the binding causing any kind of issues and possible leaks,
+ // so kill current view and let popupshowing generate a new one.
+ if (this.button._placesView)
+ this.button._placesView.uninit();
+ // Also uninit the main menubar placesView, since it would have the same
+ // issues.
+ let menubar = document.getElementById("bookmarksMenu");
+ if (menubar && menubar._placesView)
+ menubar._placesView.uninit();
+
+ // We have to do the same thing for the "special" views underneath the
+ // the bookmarks menu.
+ const kSpecialViewNodeIDs = ["BMB_bookmarksToolbar", "BMB_unsortedBookmarks"];
+ for (let viewNodeID of kSpecialViewNodeIDs) {
+ let elem = document.getElementById(viewNodeID);
+ if (elem && elem._placesView) {
+ elem._placesView.uninit();
+ }
+ }
+ },
+
+ onCustomizeStart: function BUI_customizeStart(aWindow) {
+ if (aWindow == window) {
+ this._uninitView();
+ this._isCustomizing = true;
+ }
+ },
+
+ onWidgetAdded: function BUI_widgetAdded(aWidgetId) {
+ if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetRemoved: function BUI_widgetRemoved(aWidgetId) {
+ if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetReset: function BUI_widgetReset(aNode, aContainer) {
+ if (aNode == this.button) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode, aContainer) {
+ if (aNode == this.button) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ _onWidgetWasMoved: function BUI_widgetWasMoved() {
+ let usedToUpdateStarState = this._shouldUpdateStarState();
+ this._updateCustomizationState();
+ if (!usedToUpdateStarState && this._shouldUpdateStarState()) {
+ this.updateStarState();
+ } else if (usedToUpdateStarState && !this._shouldUpdateStarState()) {
+ this._updateStar();
+ }
+ // If we're moved outside of customize mode, we need to uninit
+ // our view so it gets reconstructed.
+ if (!this._isCustomizing) {
+ this._uninitView();
+ }
+ },
+
+ onCustomizeEnd: function BUI_customizeEnd(aWindow) {
+ if (aWindow == window) {
+ this._isCustomizing = false;
+ this.onToolbarVisibilityChange();
+ }
+ },
+
+ init() {
+ CustomizableUI.addListener(this);
+ this._updateCustomizationState();
+ },
+
+ _hasBookmarksObserver: false,
+ _itemIds: [],
+ uninit: function BUI_uninit() {
+ this._updateBookmarkPageMenuItem(true);
+ CustomizableUI.removeListener(this);
+
+ this._uninitView();
+
+ if (this._hasBookmarksObserver) {
+ PlacesUtils.removeLazyBookmarkObserver(this);
+ }
+
+ if (this._pendingStmt) {
+ this._pendingStmt.cancel();
+ delete this._pendingStmt;
+ }
+ },
+
+ onLocationChange: function BUI_onLocationChange() {
+ if (this._uri && gBrowser.currentURI.equals(this._uri)) {
+ return;
+ }
+ this.updateStarState();
+ },
+
+ updateStarState: function BUI_updateStarState() {
+ // Reset tracked values.
+ this._uri = gBrowser.currentURI;
+ this._itemIds = [];
+
+ if (this._pendingStmt) {
+ this._pendingStmt.cancel();
+ delete this._pendingStmt;
+ }
+
+ this._pendingStmt = PlacesUtils.asyncGetBookmarkIds(this._uri, (aItemIds, aURI) => {
+ // Safety check that the bookmarked URI equals the tracked one.
+ if (!aURI.equals(this._uri)) {
+ Components.utils.reportError("BookmarkingUI did not receive current URI");
+ return;
+ }
+
+ // It's possible that onItemAdded gets called before the async statement
+ // calls back. For such an edge case, retain all unique entries from both
+ // arrays.
+ this._itemIds = this._itemIds.filter(
+ id => !aItemIds.includes(id)
+ ).concat(aItemIds);
+
+ this._updateStar();
+
+ // Start observing bookmarks if needed.
+ if (!this._hasBookmarksObserver) {
+ try {
+ PlacesUtils.addLazyBookmarkObserver(this);
+ this._hasBookmarksObserver = true;
+ } catch (ex) {
+ Components.utils.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex);
+ }
+ }
+
+ delete this._pendingStmt;
+ });
+ },
+
+ _updateStar: function BUI__updateStar() {
+ if (!this._shouldUpdateStarState()) {
+ if (this.broadcaster.hasAttribute("starred")) {
+ this.broadcaster.removeAttribute("starred");
+ this.broadcaster.removeAttribute("buttontooltiptext");
+ }
+ return;
+ }
+
+ if (this._itemIds.length > 0) {
+ this.broadcaster.setAttribute("starred", "true");
+ this.broadcaster.setAttribute("buttontooltiptext", this._starredTooltip);
+ if (this.button.getAttribute("overflowedItem") == "true") {
+ this.button.setAttribute("label", this._starButtonOverflowedStarredLabel);
+ }
+ } else {
+ this.broadcaster.removeAttribute("starred");
+ this.broadcaster.setAttribute("buttontooltiptext", this._unstarredTooltip);
+ if (this.button.getAttribute("overflowedItem") == "true") {
+ this.button.setAttribute("label", this._starButtonOverflowedLabel);
+ }
+ }
+ },
+
+ /**
+ * forceReset is passed when we're destroyed and the label should go back
+ * to the default (Bookmark This Page) for OS X.
+ */
+ _updateBookmarkPageMenuItem: function BUI__updateBookmarkPageMenuItem(forceReset) {
+ let isStarred = !forceReset && this._itemIds.length > 0;
+ let label = isStarred ? "editlabel" : "bookmarklabel";
+ if (this.broadcaster) {
+ this.broadcaster.setAttribute("label", this.broadcaster.getAttribute(label));
+ }
+ },
+
+ onMainMenuPopupShowing: function BUI_onMainMenuPopupShowing(event) {
+ // Don't handle events for submenus.
+ if (event.target != event.currentTarget)
+ return;
+
+ this._updateBookmarkPageMenuItem();
+ PlacesCommandHook.updateBookmarkAllTabsCommand();
+ this._initRecentBookmarks(document.getElementById("menu_recentBookmarks"));
+ },
+
+ _showBookmarkedNotification: function BUI_showBookmarkedNotification() {
+ function getCenteringTransformForRects(rectToPosition, referenceRect) {
+ let topDiff = referenceRect.top - rectToPosition.top;
+ let leftDiff = referenceRect.left - rectToPosition.left;
+ let heightDiff = referenceRect.height - rectToPosition.height;
+ let widthDiff = referenceRect.width - rectToPosition.width;
+ return [(leftDiff + .5 * widthDiff) + "px", (topDiff + .5 * heightDiff) + "px"];
+ }
+
+ if (this._notificationTimeout) {
+ clearTimeout(this._notificationTimeout);
+ }
+
+ if (this.notifier.style.transform == "") {
+ // Get all the relevant nodes and computed style objects
+ let dropmarker = document.getAnonymousElementByAttribute(this.button, "anonid", "dropmarker");
+ let dropmarkerIcon = document.getAnonymousElementByAttribute(dropmarker, "class", "dropmarker-icon");
+ let dropmarkerStyle = getComputedStyle(dropmarkerIcon);
+
+ // Check for RTL and get bounds
+ let isRTL = getComputedStyle(this.button).direction == "rtl";
+ let buttonRect = this.button.getBoundingClientRect();
+ let notifierRect = this.notifier.getBoundingClientRect();
+ let dropmarkerRect = dropmarkerIcon.getBoundingClientRect();
+ let dropmarkerNotifierRect = this.dropmarkerNotifier.getBoundingClientRect();
+
+ // Compute, but do not set, transform for star icon
+ let [translateX, translateY] = getCenteringTransformForRects(notifierRect, buttonRect);
+ let starIconTransform = "translate(" + translateX + ", " + translateY + ")";
+ if (isRTL) {
+ starIconTransform += " scaleX(-1)";
+ }
+
+ // Compute, but do not set, transform for dropmarker
+ [translateX, translateY] = getCenteringTransformForRects(dropmarkerNotifierRect, dropmarkerRect);
+ let dropmarkerTransform = "translate(" + translateX + ", " + translateY + ")";
+
+ // Do all layout invalidation in one go:
+ this.notifier.style.transform = starIconTransform;
+ this.dropmarkerNotifier.style.transform = dropmarkerTransform;
+
+ let dropmarkerAnimationNode = this.dropmarkerNotifier.firstChild;
+ dropmarkerAnimationNode.style.MozImageRegion = dropmarkerStyle.MozImageRegion;
+ dropmarkerAnimationNode.style.listStyleImage = dropmarkerStyle.listStyleImage;
+ }
+
+ let isInOverflowPanel = this.button.getAttribute("overflowedItem") == "true";
+ if (!isInOverflowPanel) {
+ this.notifier.setAttribute("notification", "finish");
+ this.button.setAttribute("notification", "finish");
+ this.dropmarkerNotifier.setAttribute("notification", "finish");
+ }
+
+ this._notificationTimeout = setTimeout( () => {
+ this.notifier.removeAttribute("notification");
+ this.dropmarkerNotifier.removeAttribute("notification");
+ this.button.removeAttribute("notification");
+
+ this.dropmarkerNotifier.style.transform = "";
+ this.notifier.style.transform = "";
+ }, 1000);
+ },
+
+ _showSubview() {
+ let view = document.getElementById("PanelUI-bookmarks");
+ view.addEventListener("ViewShowing", this);
+ view.addEventListener("ViewHiding", this);
+ let anchor = document.getElementById(this.BOOKMARK_BUTTON_ID);
+ anchor.setAttribute("closemenu", "none");
+ PanelUI.showSubView("PanelUI-bookmarks", anchor,
+ CustomizableUI.AREA_PANEL);
+ },
+
+ onCommand: function BUI_onCommand(aEvent) {
+ if (aEvent.target != aEvent.currentTarget) {
+ return;
+ }
+
+ // Handle special case when the button is in the panel.
+ let isBookmarked = this._itemIds.length > 0;
+
+ if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
+ this._showSubview();
+ return;
+ }
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
+ .forWindow(window);
+ if (widget.overflowed) {
+ // Close the overflow panel because the Edit Bookmark panel will appear.
+ widget.node.removeAttribute("closemenu");
+ }
+
+ // Ignore clicks on the star if we are updating its state.
+ if (!this._pendingStmt) {
+ if (!isBookmarked)
+ this._showBookmarkedNotification();
+ PlacesCommandHook.bookmarkCurrentPage(true);
+ }
+ },
+
+ onCurrentPageContextPopupShowing() {
+ this._updateBookmarkPageMenuItem();
+ },
+
+ handleEvent: function BUI_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "ViewShowing":
+ this.onPanelMenuViewShowing(aEvent);
+ break;
+ case "ViewHiding":
+ this.onPanelMenuViewHiding(aEvent);
+ break;
+ }
+ },
+
+ onPanelMenuViewShowing: function BUI_onViewShowing(aEvent) {
+ this._updateBookmarkPageMenuItem();
+ // Update checked status of the toolbar toggle.
+ let viewToolbar = document.getElementById("panelMenu_viewBookmarksToolbar");
+ let personalToolbar = document.getElementById("PersonalToolbar");
+ if (personalToolbar.collapsed)
+ viewToolbar.removeAttribute("checked");
+ else
+ viewToolbar.setAttribute("checked", "true");
+ // Get all statically placed buttons to supply them with keyboard shortcuts.
+ let staticButtons = viewToolbar.parentNode.getElementsByTagName("toolbarbutton");
+ for (let i = 0, l = staticButtons.length; i < l; ++i)
+ CustomizableUI.addShortcut(staticButtons[i]);
+ // Setup the Places view.
+ this._panelMenuView = new PlacesPanelMenuView("place:folder=BOOKMARKS_MENU",
+ "panelMenu_bookmarksMenu",
+ "panelMenu_bookmarksMenu", {
+ extraClasses: {
+ entry: "subviewbutton",
+ footer: "panel-subview-footer"
+ }
+ });
+ aEvent.target.removeEventListener("ViewShowing", this);
+ },
+
+ onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) {
+ this._panelMenuView.uninit();
+ delete this._panelMenuView;
+ aEvent.target.removeEventListener("ViewHiding", this);
+ },
+
+ onPanelMenuViewCommand: function BUI_onPanelMenuViewCommand(aEvent, aView) {
+ let target = aEvent.originalTarget;
+ if (!target._placesNode)
+ return;
+ if (PlacesUtils.nodeIsContainer(target._placesNode))
+ PlacesCommandHook.showPlacesOrganizer([ "BookmarksMenu", target._placesNode.itemId ]);
+ else
+ PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView);
+ PanelUI.hide();
+ },
+
+ // nsINavBookmarkObserver
+ onItemAdded: function BUI_onItemAdded(aItemId, aParentId, aIndex, aItemType,
+ aURI) {
+ if (aURI && aURI.equals(this._uri)) {
+ // If a new bookmark has been added to the tracked uri, register it.
+ if (!this._itemIds.includes(aItemId)) {
+ this._itemIds.push(aItemId);
+ // Only need to update the UI if it wasn't marked as starred before:
+ if (this._itemIds.length == 1) {
+ this._updateStar();
+ }
+ }
+ }
+ },
+
+ onItemRemoved: function BUI_onItemRemoved(aItemId) {
+ let index = this._itemIds.indexOf(aItemId);
+ // If one of the tracked bookmarks has been removed, unregister it.
+ if (index != -1) {
+ this._itemIds.splice(index, 1);
+ // Only need to update the UI if the page is no longer starred
+ if (this._itemIds.length == 0) {
+ this._updateStar();
+ }
+ }
+ },
+
+ onItemChanged: function BUI_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue) {
+ if (aProperty == "uri") {
+ let index = this._itemIds.indexOf(aItemId);
+ // If the changed bookmark was tracked, check if it is now pointing to
+ // a different uri and unregister it.
+ if (index != -1 && aNewValue != this._uri.spec) {
+ this._itemIds.splice(index, 1);
+ // Only need to update the UI if the page is no longer starred
+ if (this._itemIds.length == 0) {
+ this._updateStar();
+ }
+ } else if (index == -1 && aNewValue == this._uri.spec) {
+ // If another bookmark is now pointing to the tracked uri, register it.
+ this._itemIds.push(aItemId);
+ // Only need to update the UI if it wasn't marked as starred before:
+ if (this._itemIds.length == 1) {
+ this._updateStar();
+ }
+ }
+ }
+ },
+
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onBeforeItemRemoved() {},
+ onItemVisited() {},
+ onItemMoved() {},
+
+ // CustomizableUI events:
+ _starButtonLabel: null,
+ get _starButtonOverflowedLabel() {
+ delete this._starButtonOverflowedLabel;
+ return this._starButtonOverflowedLabel =
+ gNavigatorBundle.getString("starButtonOverflowed.label");
+ },
+ get _starButtonOverflowedStarredLabel() {
+ delete this._starButtonOverflowedStarredLabel;
+ return this._starButtonOverflowedStarredLabel =
+ gNavigatorBundle.getString("starButtonOverflowedStarred.label");
+ },
+ onWidgetOverflow(aNode, aContainer) {
+ let win = aNode.ownerGlobal;
+ if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window)
+ return;
+
+ let currentLabel = aNode.getAttribute("label");
+ if (!this._starButtonLabel)
+ this._starButtonLabel = currentLabel;
+
+ if (currentLabel == this._starButtonLabel) {
+ let desiredLabel = this._itemIds.length > 0 ? this._starButtonOverflowedStarredLabel
+ : this._starButtonOverflowedLabel;
+ aNode.setAttribute("label", desiredLabel);
+ }
+ },
+
+ onWidgetUnderflow(aNode, aContainer) {
+ let win = aNode.ownerGlobal;
+ if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window)
+ return;
+
+ // The view gets broken by being removed and reinserted. Uninit
+ // here so popupshowing will generate a new one:
+ this._uninitView();
+
+ if (aNode.getAttribute("label") != this._starButtonLabel)
+ aNode.setAttribute("label", this._starButtonLabel);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ ])
+};
+
+var AutoShowBookmarksToolbar = {
+ init() {
+ Services.obs.addObserver(this, "autoshow-bookmarks-toolbar", false);
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "autoshow-bookmarks-toolbar");
+ },
+
+ observe(subject, topic, data) {
+ let toolbar = document.getElementById("PersonalToolbar");
+ if (!toolbar.collapsed)
+ return;
+
+ let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
+ let area = placement && placement.area;
+ if (area != CustomizableUI.AREA_BOOKMARKS)
+ return;
+
+ setToolbarVisibility(toolbar, true);
+ }
+};