diff options
Diffstat (limited to 'browser/base/content/browser-places.js')
-rw-r--r-- | browser/base/content/browser-places.js | 1352 |
1 files changed, 1352 insertions, 0 deletions
diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js new file mode 100644 index 000000000..6fec2242f --- /dev/null +++ b/browser/base/content/browser-places.js @@ -0,0 +1,1352 @@ +# 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 <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: 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 <browser/> 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 <menu> 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 <menu> 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 <menu> 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 <menu> 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 + ]) +}; |