# 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/. //////////////////////////////////////////////////////////////////////////////// //// StarUI var StarUI = { _itemId: -1, uri: null, _batching: false, _element: function(aID) { return document.getElementById(aID); }, get showForNewBookmarks() { return Services.prefs.getBoolPref("browser.bookmarks.editDialog.showForNewBookmarks", false); }, // 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("popuphidden", this, false); element.addEventListener("keypress", this, false); 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(function(id) this._element(id), this); }, _blockCommands: function() { 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() { this._blockedCommands.forEach(function(elt) { if (elt.getAttribute("wasDisabled") != "true") { elt.removeAttribute("disabled"); } elt.removeAttribute("wasDisabled"); }); }, // nsIDOMEventListener handleEvent: function(aEvent) { switch (aEvent.type) { case "popuphidden": if (aEvent.originalTarget == this.panel) { if (!this._element("editBookmarkPanelContent").hidden) { this.quitEditMode(); } this._restoreCommandsState(); this._itemId = -1; if (this._batching) { PlacesUtils.transactionManager.endBatch(false); this._batching = false; } switch (this._actionOnHide) { case "cancel": { PlacesUtils.transactionManager.undoTransaction(); break; } case "remove": { // Remove all bookmarks for the bookmark's url, this also removes // the tags for the url. PlacesUtils.transactionManager.beginBatch(null); let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval); for (let i = 0; i < itemIds.length; i++) { let txn = new PlacesRemoveItemTransaction(itemIds[i]); PlacesUtils.transactionManager.doTransaction(txn); } PlacesUtils.transactionManager.endBatch(false); break; } } this._actionOnHide = ""; } break; case "keypress": if (aEvent.defaultPrevented) { // The event has already been consumed inside of the panel. break; } switch (aEvent.keyCode) { case KeyEvent.DOM_VK_ESCAPE: if (!this._element("editBookmarkPanelContent").hidden) { this.cancelButtonOnCommand(); } break; case KeyEvent.DOM_VK_RETURN: if (aEvent.target.className == "expander-up" || aEvent.target.className == "expander-down" || aEvent.target.id == "editBMPanel_newFolderButton") { //XXX Why is this necessary? The defaultPrevented check should // be enough. break; } this.panel.hidePopup(); break; } break; } }, _overlayLoaded: false, _overlayLoading: false, showEditBookmarkPopup: function(aItemId, aAnchorElement, aPosition) { // Performance: load the overlay the first time the panel is opened // (see bug 392443). if (this._overlayLoading) { return; } if (this._overlayLoaded) { this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition); return; } this._overlayLoading = true; document.loadOverlay( "chrome://browser/content/places/editBookmarkOverlay.xul", (function(aSubject, aTopic, aData) { //XXX We just caused localstore.rdf to be re-applied (bug 640158) retrieveToolbarIconsizesFromTheme(); // 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(aItemId, aAnchorElement, aPosition); }).bind(this) ); }, _doShowEditBookmarkPanel: function(aItemId, aAnchorElement, aPosition) { if (this.panel.state != "closed") { return; } this._blockCommands(); // un-done in the popuphiding handler // Set panel title: // if we are batching, i.e. the bookmark has been added now, // then show Page Bookmarked, else if the bookmark did already exist, // we are about editing it, then use Edit This Bookmark. this._element("editBookmarkPanelTitle").value = this._batching ? gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") : gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"); // No description; show the Done, Cancel; this._element("editBookmarkPanelDescription").textContent = ""; this._element("editBookmarkPanelBottomButtons").hidden = false; this._element("editBookmarkPanelContent").hidden = false; // The remove button is shown only if we're not already batching, i.e. // if the cancel button/ESC does not remove the bookmark. this._element("editBookmarkPanelRemoveButton").hidden = this._batching; // The label of the remove button differs if the URI is bookmarked // multiple times. var bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI); var forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label"); var 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 = aItemId !== undefined ? aItemId : this._itemId; this.beginBatch(); let onPanelReady = fn => { let target = this.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(event) { fn(); }, {"capture": true, "once": true}); }; gEditItemOverlay.initPanel(this._itemId, { onPanelReady, hiddenRows: ["description", "location", "loadInSidebar", "keyword"] }); this.panel.openPopup(aAnchorElement, aPosition); }, panelShown: function(aEvent) { if (aEvent.target == this.panel) { if (!this._element("editBookmarkPanelContent").hidden) { let fieldToFocus = "editBMPanel_" + gPrefService.getCharPref("browser.bookmarks.editDialog.firstEditField"); var elt = this._element(fieldToFocus); elt.focus(); elt.select(); } else { // 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() { this._element("editBookmarkPanelContent").hidden = true; this._element("editBookmarkPanelBottomButtons").hidden = true; gEditItemOverlay.uninitPanel(true); }, cancelButtonOnCommand: function() { this._actionOnHide = "cancel"; this.panel.hidePopup(); }, removeBookmarkButtonCommand: function() { this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); this._actionOnHide = "remove"; this.panel.hidePopup(); }, beginBatch: function() { if (!this._batching) { PlacesUtils.transactionManager.beginBatch(null); this._batching = true; } } } //////////////////////////////////////////////////////////////////////////////// //// PlacesCommandHook var PlacesCommandHook = { /** * Adds a bookmark to the page loaded in the given browser. * * @param aBrowser * a 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: function(aBrowser, aParent, aShowEditUI) { var uri = aBrowser.currentURI; var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri); if (itemId == -1) { // Copied over from addBookmarkForBrowser: // Bug 52536: We obtain the URL and title from the nsIWebNavigation // associated with a rather than from a DOMWindow. // This is because when a full page plugin is loaded, there is // no DOMWindow (?) but information about the loaded document // may still be obtained from the webNavigation. var webNav = aBrowser.webNavigation; var url = webNav.currentURI; var title; var description; var charset; try { let isErrorPage = /^about:(neterror|certerror|blocked)/ .test(webNav.document.documentURI); title = isErrorPage ? PlacesUtils.history.getPageTitle(url) : webNav.document.title; title = title || url.spec; description = PlacesUIUtils.getDescriptionFromDocument(webNav.document); charset = webNav.document.characterSet; } catch(e) {} if (aShowEditUI) { // If we bookmark the page here (i.e. page was not "starred" already) // but open right into the "edit" state, start batching here, so // "Cancel" in that state removes the bookmark. 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.isWindowPrivate(aBrowser.contentWindow)) { PlacesUtils.setCharsetForURI(uri, charset); } } // Revert the contents of the location bar if (gURLBar) { 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 page-proxy-favicon // 3. the content area if (BookmarkingUI.anchor) { StarUI.showEditBookmarkPopup(itemId, BookmarkingUI.anchor, "bottomcenter topright"); return; } let pageProxyFavicon = document.getElementById("page-proxy-favicon"); if (isElementVisible(pageProxyFavicon)) { StarUI.showEditBookmarkPopup(itemId, pageProxyFavicon, "bottomcenter topright"); } else { StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap"); } }, /** * Adds a bookmark to the page loaded in the current tab. */ bookmarkCurrentPage: function(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 */ bookmarkLink: function(aParent, aURL, aTitle) { var linkURI = makeURI(aURL); var itemId = PlacesUtils.getMostRecentBookmarkForURI(linkURI); if (itemId == -1) { PlacesUIUtils.showBookmarkDialog({ action: "add", type: "bookmark", uri: linkURI, title: aTitle, hiddenRows: [ "description", "location", "loadInSidebar", "keyword" ] }, window); } else { PlacesUIUtils.showBookmarkDialog({ action: "edit", type: "bookmark", itemId: itemId }, window); } }, /** * 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(function(tab) { let spec = tab.linkedBrowser.currentURI.spec; if (!tab.pinned && !(spec in uniquePages)) { uniquePages[spec] = null; URIs.push(tab.linkedBrowser.currentURI); } }); return URIs; }, /** * Adds a folder with bookmarks to all of the currently open tabs in this * window. */ bookmarkCurrentPages: function() { 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() { // 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: function(url, feedTitle, feedSubtitle) { let toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId, -1); let feedURI = makeURI(url); let title = feedTitle || gBrowser.contentTitle; let description = feedSubtitle; if (!description) { description = PlacesUIUtils.getDescriptionFromDocument(gBrowser.contentDocument); } PlacesUIUtils.showBookmarkDialog({ action: "add", type: "livemark", feedURI: feedURI, siteURI: gBrowser.currentURI, title: title, description: 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(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.selectLeftPaneQuery(aLeftPaneRoot); organizer.focus(); } } }; //////////////////////////////////////////////////////////////////////////////// //// HistoryMenu // 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; XPCOMUtils.defineLazyServiceGetter(this, "_ss", "@mozilla.org/browser/sessionstore;1", "nsISessionStore"); PlacesMenu.call(this, aPopupShowingEvent, "place:sort=4&maxResults=15"); } HistoryMenu.prototype = { toggleRestoreLastSession: function() { let restoreItem = this._rootElt.ownerDocument.getElementById("Browser:RestoreLastSession"); if (this._ss.canRestoreLastSession && !PrivateBrowsingUtils.isWindowPrivate(window)) { restoreItem.removeAttribute("disabled"); } else { restoreItem.setAttribute("disabled", true); } }, toggleRecentlyClosedTabs: function() { // enable/disable the Recently Closed Tabs sub menu var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0]; // no restorable tabs, so disable menu if (this._ss.getClosedTabCount(window) == 0) { undoMenu.setAttribute("disabled", true); } else { undoMenu.removeAttribute("disabled"); } }, /** * Re-open a closed tab and put it to the end of the tab strip. * Used for a middle click. * @param aEvent * The event when the user clicks the menu item */ _undoCloseMiddleClick: function(aEvent) { if (aEvent.button != 1) { return; } undoCloseTab(aEvent.originalTarget.value); gBrowser.moveTabToEnd(); }, /** * Populate when the history menu is opened */ populateUndoSubmenu: function() { 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._ss.getClosedTabCount(window) == 0) { undoMenu.setAttribute("disabled", true); return; } // enable menu undoMenu.removeAttribute("disabled"); // populate menu var undoItems = JSON.parse(this._ss.getClosedTabData(window)); for (var i = 0; i < undoItems.length; i++) { var m = document.createElement("menuitem"); m.setAttribute("label", undoItems[i].title); if (undoItems[i].image) { let iconURL = undoItems[i].image; // don't initiate a connection just to fetch a favicon (see bug 467828) if (/^https?:/.test(iconURL)) { iconURL = "moz-anno:favicon:" + iconURL; } m.setAttribute("image", iconURL); } m.setAttribute("class", "menuitem-iconic bookmark-item menuitem-with-favicon"); m.setAttribute("value", i); m.setAttribute("oncommand", "undoCloseTab(" + i + ");"); // Set the targetURI attribute so it will be shown in tooltip and trigger // onLinkHovered. SessionStore uses one-based indexes, so we need to // normalize them. let tabData = undoItems[i].state; let activeIndex = (tabData.index || tabData.entries.length) - 1; if (activeIndex >= 0 && tabData.entries[activeIndex]) { m.setAttribute("targetURI", tabData.entries[activeIndex].url); } m.addEventListener("click", this._undoCloseMiddleClick, false); if (i == 0) { m.setAttribute("key", "key_undoCloseTab"); } undoPopup.appendChild(m); } // "Restore All Tabs" var strings = gNavigatorBundle; undoPopup.appendChild(document.createElement("menuseparator")); m = undoPopup.appendChild(document.createElement("menuitem")); m.id = "menu_restoreAllTabs"; m.setAttribute("label", strings.getString("menuRestoreAllTabs.label")); m.addEventListener("command", function() { for (var i = 0; i < undoItems.length; i++) { undoCloseTab(); } }, false); }, toggleRecentlyClosedWindows: function() { // enable/disable the Recently Closed Windows sub menu var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0]; // no restorable windows, so disable menu if (this._ss.getClosedWindowCount() == 0) { undoMenu.setAttribute("disabled", true); } else { undoMenu.removeAttribute("disabled"); } }, /** * Populate when the history menu is opened */ populateUndoWindowSubmenu: function() { let undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0]; let undoPopup = undoMenu.firstChild; let menuLabelString = gNavigatorBundle.getString("menuUndoCloseWindowLabel"); let menuLabelStringSingleTab = gNavigatorBundle.getString("menuUndoCloseWindowSingleTabLabel"); // remove existing menu items while (undoPopup.hasChildNodes()) { undoPopup.removeChild(undoPopup.firstChild); } // no restorable windows, so make sure menu is disabled, and return if (this._ss.getClosedWindowCount() == 0) { undoMenu.setAttribute("disabled", true); return; } // enable menu undoMenu.removeAttribute("disabled"); // populate menu let undoItems = JSON.parse(this._ss.getClosedWindowData()); for (let i = 0; i < undoItems.length; i++) { let undoItem = undoItems[i]; let otherTabsCount = undoItem.tabs.length - 1; let label = (otherTabsCount == 0) ? menuLabelStringSingleTab : PluralForm.get(otherTabsCount, menuLabelString); let menuLabel = label.replace("#1", undoItem.title) .replace("#2", otherTabsCount); let m = document.createElement("menuitem"); m.setAttribute("label", menuLabel); let selectedTab = undoItem.tabs[undoItem.selected - 1]; if (selectedTab.image) { let iconURL = selectedTab.image; // don't initiate a connection just to fetch a favicon (see bug 467828) if (/^https?:/.test(iconURL)) { iconURL = "moz-anno:favicon:" + iconURL; } m.setAttribute("image", iconURL); } m.setAttribute("class", "menuitem-iconic bookmark-item menuitem-with-favicon"); m.setAttribute("oncommand", "undoCloseWindow(" + i + ");"); // Set the targetURI attribute so it will be shown in tooltip. // SessionStore uses one-based indexes, so we need to normalize them. let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1; if (activeIndex >= 0 && selectedTab.entries[activeIndex]) { m.setAttribute("targetURI", selectedTab.entries[activeIndex].url); } if (i == 0) { m.setAttribute("key", "key_undoCloseWindow"); } undoPopup.appendChild(m); } // "Open All in Windows" undoPopup.appendChild(document.createElement("menuseparator")); let m = undoPopup.appendChild(document.createElement("menuitem")); m.id = "menu_restoreAllWindows"; m.setAttribute("label", gNavigatorBundle.getString("menuRestoreAllWindows.label")); m.setAttribute("oncommand", "for (var i = 0; i < " + undoItems.length + "; i++) undoCloseWindow();"); }, toggleTabsFromOtherComputers: function() { // This is a no-op if MOZ_SERVICES_SYNC isn't defined #ifdef MOZ_SERVICES_SYNC // 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 Sync isn't configured yet, then don't show the menuitem. if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || Weave.Svc.Prefs.get("firstSync", "") == "notReady") { menuitem.setAttribute("hidden", true); return; } // The tabs engine might never be inited (if services.sync.registerEngines // is modified), so make sure we avoid undefined errors. let enabled = Weave.Service.isLoggedIn && Weave.Service.engineManager.get("tabs") && Weave.Service.engineManager.get("tabs").enabled; menuitem.setAttribute("disabled", !enabled); menuitem.setAttribute("hidden", false); #endif }, _onPopupShowing: function(aEvent) { PlacesMenu.prototype._onPopupShowing.apply(this, arguments); // Don't handle events for submenus. if (aEvent.target != aEvent.currentTarget) { return; } this.toggleRestoreLastSession(); this.toggleRecentlyClosedTabs(); this.toggleRecentlyClosedWindows(); this.toggleTabsFromOtherComputers(); }, _onCommand: function(aEvent) { let placesNode = aEvent.target._placesNode; if (placesNode) { if (!PrivateBrowsingUtils.isWindowPrivate(window)) { PlacesUIUtils.markPageAsTyped(placesNode.uri); } openUILink(placesNode.uri, aEvent, { ignoreAlt: true }); } } }; //////////////////////////////////////////////////////////////////////////////// //// BookmarksEventHandler /** * 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(aEvent, aView) { // Only handle middle-click or left-click with modifiers. var modifKey = aEvent.ctrlKey || aEvent.shiftKey; 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 (node = target.parentNode; node; node = node.parentNode) { if (node.localName == "menupopup") { node.hidePopup(); } else if (node.localName != "menu" && node.localName != "splitmenu" && 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(aEvent, aView) { var target = aEvent.originalTarget; if (target._placesNode) { PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView); } }, fillInBHTooltip: function(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; } }; //////////////////////////////////////////////////////////////////////////////// //// PlacesMenuDNDHandler // 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 = { _springLoadDelay: 350, // milliseconds _loadTimer: null, _closerTimer: null, /** * Called when the user enters the element during a drag. * @param event * The DragEnter event that spawned the opening. */ onDragEnter: function(event) { // Opening menus in a Places popup is handled by the view itself. if (!this._isStaticContainer(event.target)) { return; } 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._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT); event.preventDefault(); event.stopPropagation(); }, /** * Handles dragleave on the element. * @returns true if the element is a container element (menu or * menu-toolbarbutton), false otherwise. */ onDragLeave: function(event) { // Handle menu-button separate targets. if (event.relatedTarget === event.currentTarget || event.relatedTarget.parentNode === event.currentTarget) { return; } // Closing menus in a Places popup is handled by the view itself. if (!this._isStaticContainer(event.target)) { return; } 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._closeTimer.initWithCallback(function() { this._closeTimer = 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._springLoadDelay, 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(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 element. * @param event * The DragOver event. */ onDragOver: function(event) { let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, PlacesUtils.bookmarks.DEFAULT_INDEX, Ci.nsITreeView.DROP_ON); if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer)) { event.preventDefault(); } event.stopPropagation(); }, /** * Called when the user drops on the element. * @param event * The Drop event. */ onDrop: function(event) { // Put the item at the end of bookmark menu. let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, PlacesUtils.bookmarks.DEFAULT_INDEX, Ci.nsITreeView.DROP_ON); PlacesControllerDragHelper.onDrop(ip, event.dataTransfer); event.stopPropagation(); } }; //////////////////////////////////////////////////////////////////////////////// //// PlacesToolbarHelper /** * This object handles the initialization and uninitialization of the bookmarks * toolbar. */ var PlacesToolbarHelper = { _place: "place:folder=TOOLBAR", get _viewElt() { return document.getElementById("PlacesToolbar"); }, init: function() { let viewElt = this._viewElt; if (!viewElt || viewElt._placesView) { return; } // If the bookmarks toolbar item is hidden because the parent toolbar is // collapsed or hidden (i.e. in a popup), spare the initialization. Also, // there is no need to initialize the toolbar if customizing because // init() will be called when the customization is done. let toolbar = viewElt.parentNode.parentNode; if (toolbar.collapsed || getComputedStyle(toolbar, "").display == "none" || this._isCustomizing) { return; } new PlacesToolbar(this._place); }, customizeStart: function() { let viewElt = this._viewElt; if (viewElt && viewElt._placesView) { viewElt._placesView.uninit(); } this._isCustomizing = true; }, customizeDone: function() { this._isCustomizing = false; this.init(); } }; //////////////////////////////////////////////////////////////////////////////// //// BookmarkingUI /** * Handles the bookmarks star button in the URL bar, as well as the bookmark * menu button. */ var BookmarkingUI = { get button() { if (!this._button) { this._button = document.getElementById("bookmarks-menu-button"); } return this._button; }, get star() { if (!this._star) { this._star = document.getElementById("star-button"); } return this._star; }, get anchor() { if (this.star && isElementVisible(this.star)) { // Anchor to the icon, so the panel looks more natural. return this.star; } return null; }, STATUS_UPDATING: -1, STATUS_UNSTARRED: 0, STATUS_STARRED: 1, get status() { if (this._pendingStmt) { return this.STATUS_UPDATING; } return this.star && this.star.hasAttribute("starred") ? this.STATUS_STARRED : this.STATUS_UNSTARRED; }, get _starredTooltip() { delete this._starredTooltip; return this._starredTooltip = gNavigatorBundle.getString("starButtonOn.tooltip"); }, get _unstarredTooltip() { delete this._unstarredTooltip; return this._unstarredTooltip = gNavigatorBundle.getString("starButtonOff.tooltip"); }, /** * 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() { this._popupNeedsUpdate = true; }, onPopupShowing: function(event) { // Don't handle events for submenus. if (event.target != event.currentTarget) { return; } 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. let personalToolbar = document.getElementById("PersonalToolbar"); viewToolbarMenuitem.setAttribute("checked", !personalToolbar.collapsed); } let toolbarMenuitem = getPlacesAnonymousElement("toolbar-autohide"); if (toolbarMenuitem) { // If bookmarks items are visible, hide Bookmarks Toolbar menu and the // separator after it. toolbarMenuitem.collapsed = toolbarMenuitem.nextSibling.collapsed = isElementVisible(document.getElementById("personal-bookmarks")); } }, /** * Handles star styling based on page proxy state changes. */ onPageProxyStateChanged: function(aState) { if (!this.star) { return; } if (aState == "invalid") { this.star.setAttribute("disabled", "true"); this.star.removeAttribute("starred"); } else { this.star.removeAttribute("disabled"); } }, _updateToolbarStyle: function() { if (!this.button) { return; } let personalToolbar = document.getElementById("PersonalToolbar"); let onPersonalToolbar = this.button.parentNode == personalToolbar || this.button.parentNode.parentNode == personalToolbar; if (onPersonalToolbar) { this.button.classList.add("bookmark-item"); this.button.classList.remove("toolbarbutton-1"); } else { this.button.classList.remove("bookmark-item"); this.button.classList.add("toolbarbutton-1"); } }, _uninitView: function() { // 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 && 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(); } }, customizeStart: function() { this._uninitView(); }, customizeChange: function() { this._updateToolbarStyle(); }, customizeDone: function() { delete this._button; this.onToolbarVisibilityChange(); this._updateToolbarStyle(); }, _hasBookmarksObserver: false, uninit: function() { this._uninitView(); if (this._hasBookmarksObserver) { PlacesUtils.removeLazyBookmarkObserver(this); } if (this._pendingStmt) { this._pendingStmt.cancel(); delete this._pendingStmt; } }, onLocationChange: function() { if (this._uri && gBrowser.currentURI.equals(this._uri)) { return; } this.updateStarState(); }, updateStarState: function() { // Reset tracked values. this._uri = gBrowser.currentURI; this._itemIds = []; if (this._pendingStmt) { this._pendingStmt.cancel(); delete this._pendingStmt; } // We can load about:blank before the actual page, but there is no point in handling that page. if (isBlankPageURL(this._uri.spec)) { return; } 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( function(id) aItemIds.indexOf(id) == -1 ).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; }, this); }, _updateStar: function() { if (!this.star) { return; } if (this._itemIds.length > 0) { this.star.setAttribute("starred", "true"); this.star.setAttribute("tooltiptext", this._starredTooltip); } else { this.star.removeAttribute("starred"); this.star.setAttribute("tooltiptext", this._unstarredTooltip); } }, onCommand: function(aEvent) { if (aEvent.target != aEvent.currentTarget) { return; } // Ignore clicks on the star if we are updating its state. if (!this._pendingStmt) { PlacesCommandHook.bookmarkCurrentPage(this._itemIds.length > 0 || StarUI.showForNewBookmarks); } }, // nsINavBookmarkObserver onItemAdded: function(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.indexOf(aItemId) == -1) { this._itemIds.push(aItemId); this._updateStar(); } } }, onItemRemoved: function(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); this._updateStar(); } }, onItemChanged: function(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); 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); this._updateStar(); } } }, onBeginUpdateBatch: function() {}, onEndUpdateBatch: function() {}, onBeforeItemRemoved: function() {}, onItemVisited: function() {}, onItemMoved: function() {}, QueryInterface: XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver ]) };