From f9cab004186edb425a9b88ad649726605080a17c Mon Sep 17 00:00:00 2001 From: Thomas Groman Date: Mon, 20 Apr 2020 20:49:37 -0700 Subject: move browser to webbrowser/ --- webbrowser/components/places/PlacesUIUtils.jsm | 1373 ++++++++++++++ .../places/content/bookmarkProperties.js | 675 +++++++ .../places/content/bookmarkProperties.xul | 43 + .../components/places/content/bookmarksPanel.js | 25 + .../components/places/content/bookmarksPanel.xul | 55 + .../places/content/browserPlacesViews.js | 1754 ++++++++++++++++++ webbrowser/components/places/content/controller.js | 1895 ++++++++++++++++++++ .../places/content/downloadsViewOverlay.xul | 47 + .../places/content/editBookmarkOverlay.js | 1063 +++++++++++ .../places/content/editBookmarkOverlay.xul | 228 +++ .../components/places/content/history-panel.js | 91 + .../components/places/content/history-panel.xul | 95 + webbrowser/components/places/content/menu.xml | 488 +++++ .../components/places/content/moveBookmarks.js | 54 + .../components/places/content/moveBookmarks.xul | 53 + webbrowser/components/places/content/organizer.css | 7 + webbrowser/components/places/content/places.css | 16 + webbrowser/components/places/content/places.js | 1553 ++++++++++++++++ webbrowser/components/places/content/places.xul | 471 +++++ .../components/places/content/placesOverlay.xul | 247 +++ .../components/places/content/sidebarUtils.js | 108 ++ webbrowser/components/places/content/tree.xml | 789 ++++++++ webbrowser/components/places/content/treeView.js | 1770 ++++++++++++++++++ webbrowser/components/places/jar.mn | 34 + webbrowser/components/places/moz.build | 9 + 25 files changed, 12943 insertions(+) create mode 100644 webbrowser/components/places/PlacesUIUtils.jsm create mode 100644 webbrowser/components/places/content/bookmarkProperties.js create mode 100644 webbrowser/components/places/content/bookmarkProperties.xul create mode 100644 webbrowser/components/places/content/bookmarksPanel.js create mode 100644 webbrowser/components/places/content/bookmarksPanel.xul create mode 100644 webbrowser/components/places/content/browserPlacesViews.js create mode 100644 webbrowser/components/places/content/controller.js create mode 100644 webbrowser/components/places/content/downloadsViewOverlay.xul create mode 100644 webbrowser/components/places/content/editBookmarkOverlay.js create mode 100644 webbrowser/components/places/content/editBookmarkOverlay.xul create mode 100644 webbrowser/components/places/content/history-panel.js create mode 100644 webbrowser/components/places/content/history-panel.xul create mode 100644 webbrowser/components/places/content/menu.xml create mode 100644 webbrowser/components/places/content/moveBookmarks.js create mode 100644 webbrowser/components/places/content/moveBookmarks.xul create mode 100644 webbrowser/components/places/content/organizer.css create mode 100644 webbrowser/components/places/content/places.css create mode 100644 webbrowser/components/places/content/places.js create mode 100644 webbrowser/components/places/content/places.xul create mode 100644 webbrowser/components/places/content/placesOverlay.xul create mode 100644 webbrowser/components/places/content/sidebarUtils.js create mode 100644 webbrowser/components/places/content/tree.xml create mode 100644 webbrowser/components/places/content/treeView.js create mode 100644 webbrowser/components/places/jar.mn create mode 100644 webbrowser/components/places/moz.build (limited to 'webbrowser/components/places') diff --git a/webbrowser/components/places/PlacesUIUtils.jsm b/webbrowser/components/places/PlacesUIUtils.jsm new file mode 100644 index 0000000..e3a9e13 --- /dev/null +++ b/webbrowser/components/places/PlacesUIUtils.jsm @@ -0,0 +1,1373 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +this.EXPORTED_SYMBOLS = ["PlacesUIUtils"]; + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); + +XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() { + Cu.import("resource://gre/modules/PlacesUtils.jsm"); + return PlacesUtils; +}); + +this.PlacesUIUtils = { + ORGANIZER_LEFTPANE_VERSION: 7, + ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder", + ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery", + + LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar", + DESCRIPTION_ANNO: "bookmarkProperties/description", + + TYPE_TAB_DROP: "application/x-moz-tabbrowser-tab", + + /** + * Makes a URI from a spec, and do fixup + * @param aSpec + * The string spec of the URI + * @returns A URI object for the spec. + */ + createFixedURI: function PUIU_createFixedURI(aSpec) { + return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE); + }, + + getFormattedString: function PUIU_getFormattedString(key, params) { + return bundle.formatStringFromName(key, params, params.length); + }, + + /** + * Get a localized plural string for the specified key name and numeric value + * substituting parameters. + * + * @param aKey + * String, key for looking up the localized string in the bundle + * @param aNumber + * Number based on which the final localized form is looked up + * @param aParams + * Array whose items will substitute #1, #2,... #n parameters + * in the string. + * + * @see https://developer.mozilla.org/en/Localization_and_Plurals + * @return The localized plural string. + */ + getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) { + let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey)); + + // Replace #1 with aParams[0], #2 with aParams[1], and so on. + return str.replace(/\#(\d+)/g, function (matchedId, matchedNumber) { + let param = aParams[parseInt(matchedNumber, 10) - 1]; + return param !== undefined ? param : matchedId; + }); + }, + + getString: function PUIU_getString(key) { + return bundle.GetStringFromName(key); + }, + + get _copyableAnnotations() [ + this.DESCRIPTION_ANNO, + this.LOAD_IN_SIDEBAR_ANNO, + PlacesUtils.POST_DATA_ANNO, + PlacesUtils.READ_ONLY_ANNO, + ], + + /** + * Get a transaction for copying a uri item (either a bookmark or a history + * entry) from one container to another. + * + * @param aData + * JSON object of dropped or pasted item properties + * @param aContainer + * The container being copied into + * @param aIndex + * The index within the container the item is copied to + * @return A nsITransaction object that performs the copy. + * + * @note Since a copy creates a completely new item, only some internal + * annotations are synced from the old one. + * @see this._copyableAnnotations for the list of copyable annotations. + */ + _getURIItemCopyTransaction: + function PUIU__getURIItemCopyTransaction(aData, aContainer, aIndex) + { + let transactions = []; + if (aData.dateAdded) { + transactions.push( + new PlacesEditItemDateAddedTransaction(null, aData.dateAdded) + ); + } + if (aData.lastModified) { + transactions.push( + new PlacesEditItemLastModifiedTransaction(null, aData.lastModified) + ); + } + + let keyword = aData.keyword || null; + let annos = []; + if (aData.annos) { + annos = aData.annos.filter(function (aAnno) { + return this._copyableAnnotations.indexOf(aAnno.name) != -1; + }, this); + } + + return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri), + aContainer, aIndex, aData.title, + keyword, annos, transactions); + }, + + /** + * Gets a transaction for copying (recursively nesting to include children) + * a folder (or container) and its contents from one folder to another. + * + * @param aData + * Unwrapped dropped folder data - Obj containing folder and children + * @param aContainer + * The container we are copying into + * @param aIndex + * The index in the destination container to insert the new items + * @return A nsITransaction object that will perform the copy. + * + * @note Since a copy creates a completely new item, only some internal + * annotations are synced from the old one. + * @see this._copyableAnnotations for the list of copyable annotations. + */ + _getFolderCopyTransaction(aData, aContainer, aIndex) { + function getChildItemsTransactions(aRoot) { + let transactions = []; + let index = aIndex; + for (let i = 0; i < aRoot.childCount; ++i) { + let child = aRoot.getChild(i); + // Temporary hacks until we switch to PlacesTransactions.jsm. + let isLivemark = + PlacesUtils.annotations.itemHasAnnotation(child.itemId, + PlacesUtils.LMANNO_FEEDURI); + let [node] = PlacesUtils.unwrapNodes( + PlacesUtils.wrapNode(child, PlacesUtils.TYPE_X_MOZ_PLACE, isLivemark), + PlacesUtils.TYPE_X_MOZ_PLACE + ); + + // Make sure that items are given the correct index, this will be + // passed by the transaction manager to the backend for the insertion. + // Insertion behaves differently for DEFAULT_INDEX (append). + if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX) { + index = i; + } + + if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { + if (node.livemark && node.annos) { + transactions.push( + PlacesUIUtils._getLivemarkCopyTransaction(node, aContainer, index) + ); + } + else { + transactions.push( + PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index) + ); + } + } + else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) { + transactions.push(new PlacesCreateSeparatorTransaction(-1, index)); + } + else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) { + transactions.push( + PlacesUIUtils._getURIItemCopyTransaction(node, -1, index) + ); + } + else { + throw new Error("Unexpected item under a bookmarks folder"); + } + } + return transactions; + } + + if (aContainer == PlacesUtils.tagsFolderId) { // Copying into a tag folder. + let transactions = []; + if (!aData.livemark && aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { + let {root} = PlacesUtils.getFolderContents(aData.id, false, false); + let urls = PlacesUtils.getURLsForContainerNode(root); + root.containerOpen = false; + for (let { uri } of urls) { + transactions.push( + new PlacesTagURITransaction(NetUtil.newURI(uri), [aData.title]) + ); + } + } + return new PlacesAggregatedTransaction("addTags", transactions); + } + + if (aData.livemark && aData.annos) { // Copying a livemark. + return this._getLivemarkCopyTransaction(aData, aContainer, aIndex); + } + + let {root} = PlacesUtils.getFolderContents(aData.id, false, false); + let transactions = getChildItemsTransactions(root); + root.containerOpen = false; + + if (aData.dateAdded) { + transactions.push( + new PlacesEditItemDateAddedTransaction(null, aData.dateAdded) + ); + } + if (aData.lastModified) { + transactions.push( + new PlacesEditItemLastModifiedTransaction(null, aData.lastModified) + ); + } + + let annos = []; + if (aData.annos) { + annos = aData.annos.filter(function (aAnno) { + return this._copyableAnnotations.indexOf(aAnno.name) != -1; + }, this); + } + + return new PlacesCreateFolderTransaction(aData.title, aContainer, aIndex, + annos, transactions); + }, + + /** + * Gets a transaction for copying a live bookmark item from one container to + * another. + * + * @param aData + * Unwrapped live bookmarkmark data + * @param aContainer + * The container we are copying into + * @param aIndex + * The index in the destination container to insert the new items + * @return A nsITransaction object that will perform the copy. + * + * @note Since a copy creates a completely new item, only some internal + * annotations are synced from the old one. + * @see this._copyableAnnotations for the list of copyable annotations. + */ + _getLivemarkCopyTransaction: + function PUIU__getLivemarkCopyTransaction(aData, aContainer, aIndex) + { + if (!aData.livemark || !aData.annos) { + throw new Error("node is not a livemark"); + } + + let feedURI, siteURI; + let annos = []; + if (aData.annos) { + annos = aData.annos.filter(function (aAnno) { + if (aAnno.name == PlacesUtils.LMANNO_FEEDURI) { + feedURI = PlacesUtils._uri(aAnno.value); + } + else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) { + siteURI = PlacesUtils._uri(aAnno.value); + } + return this._copyableAnnotations.indexOf(aAnno.name) != -1 + }, this); + } + + return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title, + aContainer, aIndex, annos); + }, + + /** + * Test if a bookmark item = a live bookmark item. + * + * @param aItemId + * item identifier + * @return true if a live bookmark item, false otherwise. + * + * @note Maybe this should be removed later, see bug 1072833. + */ + _isLivemark: + function PUIU__isLivemark(aItemId) + { + // Since this check may be done on each dragover event, it's worth maintaining + // a cache. + let self = PUIU__isLivemark; + if (!("ids" in self)) { + const LIVEMARK_ANNO = PlacesUtils.LMANNO_FEEDURI; + + let idsVec = PlacesUtils.annotations.getItemsWithAnnotation(LIVEMARK_ANNO); + self.ids = new Set(idsVec); + + let obs = Object.freeze({ + QueryInterface: XPCOMUtils.generateQI(Ci.nsIAnnotationObserver), + + onItemAnnotationSet(itemId, annoName) { + if (annoName == LIVEMARK_ANNO) + self.ids.add(itemId); + }, + + onItemAnnotationRemoved(itemId, annoName) { + // If annoName is set to an empty string, the item is gone. + if (annoName == LIVEMARK_ANNO || annoName == "") + self.ids.delete(itemId); + }, + + onPageAnnotationSet() { }, + onPageAnnotationRemoved() { }, + }); + PlacesUtils.annotations.addObserver(obs); + PlacesUtils.registerShutdownFunction(() => { + PlacesUtils.annotations.removeObserver(obs); + }); + } + return self.ids.has(aItemId); + }, + + /** + * Constructs a Transaction for the drop or paste of a blob of data into + * a container. + * @param data + * The unwrapped data blob of dropped or pasted data. + * @param type + * The content type of the data + * @param container + * The container the data was dropped or pasted into + * @param index + * The index within the container the item was dropped or pasted at + * @param copy + * The drag action was copy, so don't move folders or links. + * @returns An object implementing nsITransaction that can perform + * the move/insert. + */ + makeTransaction: + function PUIU_makeTransaction(data, type, container, index, copy) + { + switch (data.type) { + case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: + if (copy) { + return this._getFolderCopyTransaction(data, container, index); + } + + // Otherwise move the item. + return new PlacesMoveItemTransaction(data.id, container, index); + break; + case PlacesUtils.TYPE_X_MOZ_PLACE: + if (copy || data.id == -1) { // Id is -1 if the place is not bookmarked. + return this._getURIItemCopyTransaction(data, container, index); + } + + // Otherwise move the item. + return new PlacesMoveItemTransaction(data.id, container, index); + break; + case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: + if (copy) { + // There is no data in a separator, so copying it just amounts to + // inserting a new separator. + return new PlacesCreateSeparatorTransaction(container, index); + } + + // Otherwise move the item. + return new PlacesMoveItemTransaction(data.id, container, index); + break; + default: + if (type == PlacesUtils.TYPE_X_MOZ_URL || + type == PlacesUtils.TYPE_UNICODE || + type == this.TYPE_TAB_DROP) { + let title = type != PlacesUtils.TYPE_UNICODE ? data.title + : data.uri; + return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri), + container, index, title); + } + } + return null; + }, + + /** + * Shows the bookmark dialog corresponding to the specified info. + * + * @param aInfo + * Describes the item to be edited/added in the dialog. + * See documentation at the top of bookmarkProperties.js + * @param aWindow + * Owner window for the new dialog. + * + * @see documentation at the top of bookmarkProperties.js + * @return true if any transaction has been performed, false otherwise. + */ + showBookmarkDialog: + function PUIU_showBookmarkDialog(aInfo, aParentWindow) { + // Preserve size attributes differently based on the fact the dialog has + // a folder picker or not, since it needs more horizontal space than the + // other controls. + let hasFolderPicker = !("hiddenRows" in aInfo) || + aInfo.hiddenRows.indexOf("folderPicker") == -1; + // Use a different chrome url to persist different sizes. + let dialogURL = hasFolderPicker ? + "chrome://browser/content/places/bookmarkProperties2.xul" : + "chrome://browser/content/places/bookmarkProperties.xul"; + + let features = "centerscreen,chrome,modal,resizable=yes"; + aParentWindow.openDialog(dialogURL, "", features, aInfo); + return ("performed" in aInfo && aInfo.performed); + }, + + _getTopBrowserWin: function PUIU__getTopBrowserWin() { + return RecentWindow.getMostRecentBrowserWindow(); + }, + + /** + * Returns the closet ancestor places view for the given DOM node + * @param aNode + * a DOM node + * @return the closet ancestor places view if exists, null otherwsie. + */ + getViewForNode: function PUIU_getViewForNode(aNode) { + let node = aNode; + + // The view for a of which its associated menupopup is a places + // view, is the menupopup. + if (node.localName == "menu" && !node._placesNode && + node.lastChild._placesView) + return node.lastChild._placesView; + + while (node instanceof Ci.nsIDOMElement) { + if (node._placesView) + return node._placesView; + if (node.localName == "tree" && node.getAttribute("type") == "places") + return node; + + node = node.parentNode; + } + + return null; + }, + + /** + * By calling this before visiting an URL, the visit will be associated to a + * TRANSITION_TYPED transition (if there is no a referrer). + * This is used when visiting pages from the history menu, history sidebar, + * url bar, url autocomplete results, and history searches from the places + * organizer. If this is not called visits will be marked as + * TRANSITION_LINK. + */ + markPageAsTyped: function PUIU_markPageAsTyped(aURL) { + PlacesUtils.history.markPageAsTyped(this.createFixedURI(aURL)); + }, + + /** + * By calling this before visiting an URL, the visit will be associated to a + * TRANSITION_BOOKMARK transition. + * This is used when visiting pages from the bookmarks menu, + * personal toolbar, and bookmarks from within the places organizer. + * If this is not called visits will be marked as TRANSITION_LINK. + */ + markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) { + PlacesUtils.history.markPageAsFollowedBookmark(this.createFixedURI(aURL)); + }, + + /** + * By calling this before visiting an URL, any visit in frames will be + * associated to a TRANSITION_FRAMED_LINK transition. + * This is actually used to distinguish user-initiated visits in frames + * so automatic visits can be correctly ignored. + */ + markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) { + PlacesUtils.history.markPageAsFollowedLink(this.createFixedURI(aURL)); + }, + + /** + * Allows opening of javascript/data URI only if the given node is + * bookmarked (see bug 224521). + * @param aURINode + * a URI node + * @param aWindow + * a window on which a potential error alert is shown on. + * @return true if it's safe to open the node in the browser, false otherwise. + * + */ + checkURLSecurity: function PUIU_checkURLSecurity(aURINode, aWindow) { + if (PlacesUtils.nodeIsBookmark(aURINode)) + return true; + + var uri = PlacesUtils._uri(aURINode.uri); + if (uri.schemeIs("javascript") || uri.schemeIs("data")) { + const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; + var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(BRANDING_BUNDLE_URI). + GetStringFromName("brandShortName"); + + var errorStr = this.getString("load-js-data-url-error"); + Services.prompt.alert(aWindow, brandShortName, errorStr); + return false; + } + return true; + }, + + /** + * Get the description associated with a document, as specified in a + * element. + * @param doc + * A DOM Document to get a description for + * @returns A description string if a META element was discovered with a + * "description" or "httpequiv" attribute, empty string otherwise. + */ + getDescriptionFromDocument: function PUIU_getDescriptionFromDocument(doc) { + var metaElements = doc.getElementsByTagName("META"); + for (var i = 0; i < metaElements.length; ++i) { + if (metaElements[i].name.toLowerCase() == "description" || + metaElements[i].httpEquiv.toLowerCase() == "description") { + return metaElements[i].content; + } + } + return ""; + }, + + /** + * Retrieve the description of an item + * @param aItemId + * item identifier + * @returns the description of the given item, or an empty string if it is + * not set. + */ + getItemDescription: function PUIU_getItemDescription(aItemId) { + if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO)) + return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO); + return ""; + }, + + /** + * Check whether or not the given node represents a removable entry (either in + * history or in bookmarks). + * + * @param aNode + * a node, except the root node of a query. + * @return true if the aNode represents a removable entry, false otherwise. + */ + canUserRemove: function (aNode) { + let parentNode = aNode.parent; + if (!parentNode) + throw new Error("canUserRemove doesn't accept root nodes"); + + // If it's not a bookmark, we can remove it unless it's a child of a + // livemark. + if (aNode.itemId == -1) { + // Rather than executing a db query, checking the existence of the feedURI + // annotation, detect livemark children by the fact that they are the only + // direct non-bookmark children of bookmark folders. + return !PlacesUtils.nodeIsFolder(parentNode); + } + + // Generally it's always possible to remove children of a query. + if (PlacesUtils.nodeIsQuery(parentNode)) + return true; + + // Otherwise it has to be a child of an editable folder. + return !this.isContentsReadOnly(parentNode); + }, + + /** + * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH + * TO GUIDS IS COMPLETE (BUG 1071511). + * + * Check whether or not the given node or item-id points to a folder which + * should not be modified by the user (i.e. its children should be unremovable + * and unmovable, new children should be disallowed, etc). + * These semantics are not inherited, meaning that read-only folder may + * contain editable items (for instance, the places root is read-only, but all + * of its direct children aren't). + * + * You should only pass folder item ids or folder nodes for aNodeOrItemId. + * While this is only enforced for the node case (if an item id of a separator + * or a bookmark is passed, false is returned), it's considered the caller's + * job to ensure that it checks a folder. + * Also note that folder-shortcuts should only be passed as result nodes. + * Otherwise they are just treated as bookmarks (i.e. false is returned). + * + * @param aNodeOrItemId + * any item id or result node. + * @throws if aNodeOrItemId is neither an item id nor a folder result node. + * @note livemark "folders" are considered read-only (but see bug 1072833). + * @return true if aItemId points to a read-only folder, false otherwise. + */ + isContentsReadOnly: function (aNodeOrItemId) { + let itemId; + if (typeof(aNodeOrItemId) == "number") { + itemId = aNodeOrItemId; + } + else if (PlacesUtils.nodeIsFolder(aNodeOrItemId)) { + itemId = PlacesUtils.getConcreteItemId(aNodeOrItemId); + } + else { + throw new Error("invalid value for aNodeOrItemId"); + } + + if (itemId == PlacesUtils.placesRootId || this._isLivemark(itemId)) + return true; + + // leftPaneFolderId, and as a result, allBookmarksFolderId, is a lazy getter + // performing at least a synchronous DB query (and on its very first call + // in a fresh profile, it also creates the entire structure). + // Therefore we don't want to this function, which is called very often by + // isCommandEnabled, to ever be the one that invokes it first, especially + // because isCommandEnabled may be called way before the left pane folder is + // even created (for example, if the user only uses the bookmarks menu or + // toolbar for managing bookmarks). To do so, we avoid comparing to those + // special folder if the lazy getter is still in place. This is safe merely + // because the only way to access the left pane contents goes through + // "resolving" the leftPaneFolderId getter. + if ("get" in Object.getOwnPropertyDescriptor(this, "leftPaneFolderId")) + return false; + + return itemId == this.leftPaneFolderId || + itemId == this.allBookmarksFolderId; + }, + + /** + * Gives the user a chance to cancel loading lots of tabs at once + */ + _confirmOpenInTabs: + function PUIU__confirmOpenInTabs(numTabsToOpen, aWindow) { + const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen"; + var reallyOpen = true; + + if (Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) { + if (numTabsToOpen >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) { + // default to true: if it were false, we wouldn't get this far + var warnOnOpen = { value: true }; + + var messageKey = "tabs.openWarningMultipleBranded"; + var openKey = "tabs.openButtonMultiple"; + const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; + var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(BRANDING_BUNDLE_URI). + GetStringFromName("brandShortName"); + + var buttonPressed = Services.prompt.confirmEx( + aWindow, + this.getString("tabs.openWarningTitle"), + this.getFormattedString(messageKey, [numTabsToOpen, brandShortName]), + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1), + this.getString(openKey), null, null, + this.getFormattedString("tabs.openWarningPromptMeBranded", + [brandShortName]), + warnOnOpen + ); + + reallyOpen = (buttonPressed == 0); + // don't set the pref unless they press OK and it's false + if (reallyOpen && !warnOnOpen.value) + Services.prefs.setBoolPref(WARN_ON_OPEN_PREF, false); + } + } + + return reallyOpen; + }, + + /** aItemsToOpen needs to be an array of objects of the form: + * {uri: string, isBookmark: boolean} + */ + _openTabset: function PUIU__openTabset(aItemsToOpen, aEvent, aWindow) { + if (!aItemsToOpen.length) + return; + + // Prefer the caller window if it's a browser window, otherwise use + // the top browser window. + var browserWindow = null; + browserWindow = + aWindow && aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser" ? + aWindow : this._getTopBrowserWin(); + + var urls = []; + let skipMarking = browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow); + for (let item of aItemsToOpen) { + urls.push(item.uri); + if (skipMarking) { + continue; + } + + if (item.isBookmark) + this.markPageAsFollowedBookmark(item.uri); + else + this.markPageAsTyped(item.uri); + } + + // whereToOpenLink doesn't return "window" when there's no browser window + // open (Bug 630255). + var where = browserWindow ? + browserWindow.whereToOpenLink(aEvent, false, true) : "window"; + if (where == "window") { + // There is no browser window open, thus open a new one. + var uriList = PlacesUtils.toISupportsString(urls.join("|")); + var args = Cc["@mozilla.org/supports-array;1"]. + createInstance(Ci.nsISupportsArray); + args.AppendElement(uriList); + browserWindow = Services.ww.openWindow(aWindow, + "chrome://browser/content/browser.xul", + null, "chrome,dialog=no,all", args); + return; + } + + var loadInBackground = where == "tabshifted" ? true : false; + // For consistency, we want all the bookmarks to open in new tabs, instead + // of having one of them replace the currently focused tab. Hence we call + // loadTabs with aReplace set to false. + browserWindow.gBrowser.loadTabs(urls, loadInBackground, false); + }, + + openLiveMarkNodesInTabs: + function PUIU_openLiveMarkNodesInTabs(aNode, aEvent, aView) { + let window = aView.ownerWindow; + + PlacesUtils.livemarks.getLivemark({id: aNode.itemId}) + .then(aLivemark => { + urlsToOpen = []; + + let nodes = aLivemark.getNodesForContainer(aNode); + for (let node of nodes) { + urlsToOpen.push({uri: node.uri, isBookmark: false}); + } + + if (this._confirmOpenInTabs(urlsToOpen.length, window)) { + this._openTabset(urlsToOpen, aEvent, window); + } + }, Cu.reportError); + }, + + openContainerNodeInTabs: + function PUIU_openContainerInTabs(aNode, aEvent, aView) { + let window = aView.ownerWindow; + + let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode); + if (this._confirmOpenInTabs(urlsToOpen.length, window)) { + this._openTabset(urlsToOpen, aEvent, window); + } + }, + + openURINodesInTabs: function PUIU_openURINodesInTabs(aNodes, aEvent, aView) { + let window = aView.ownerWindow; + + let urlsToOpen = []; + for (var i=0; i < aNodes.length; i++) { + // Skip over separators and folders. + if (PlacesUtils.nodeIsURI(aNodes[i])) + urlsToOpen.push({uri: aNodes[i].uri, isBookmark: PlacesUtils.nodeIsBookmark(aNodes[i])}); + } + this._openTabset(urlsToOpen, aEvent, window); + }, + + /** + * Loads the node's URL in the appropriate tab or window or as a web + * panel given the user's preference specified by modifier keys tracked by a + * DOM mouse/key event. + * @param aNode + * An uri result node. + * @param aEvent + * The DOM mouse/key event with modifier keys set that track the + * user's preferred destination window or tab. + * @param aView + * The controller associated with aNode. + */ + openNodeWithEvent: + function PUIU_openNodeWithEvent(aNode, aEvent, aView) { + let window = aView.ownerWindow; + this._openNodeIn(aNode, window.whereToOpenLink(aEvent, false, true), window); + }, + + /** + * Loads the node's URL in the appropriate tab or window or as a + * web panel. + * see also openUILinkIn + */ + openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) { + let window = aView.ownerWindow; + this._openNodeIn(aNode, aWhere, window, aPrivate); + }, + + _openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aWindow, aPrivate=false) { + if (aNode && PlacesUtils.nodeIsURI(aNode) && + this.checkURLSecurity(aNode, aWindow)) { + let isBookmark = PlacesUtils.nodeIsBookmark(aNode); + + if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) { + if (isBookmark) + this.markPageAsFollowedBookmark(aNode.uri); + else + this.markPageAsTyped(aNode.uri); + } + + // Check whether the node is a bookmark which should be opened as + // a web panel + if (aWhere == "current" && isBookmark) { + if (PlacesUtils.annotations + .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) { + let browserWin = this._getTopBrowserWin(); + if (browserWin) { + browserWin.openWebPanel(aNode.title, aNode.uri); + return; + } + } + } + aWindow.openUILinkIn(aNode.uri, aWhere, { + inBackground: Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground"), + private: aPrivate, + }); + } + }, + + /** + * Helper for guessing scheme from an url string. + * Used to avoid nsIURI overhead in frequently called UI functions. + * + * @param aUrlString the url to guess the scheme from. + * + * @return guessed scheme for this url string. + * + * @note this is not supposed be perfect, so use it only for UI purposes. + */ + guessUrlSchemeForUI: function PUIU_guessUrlSchemeForUI(aUrlString) { + return aUrlString.substr(0, aUrlString.indexOf(":")); + }, + + getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) { + var title; + if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) { + // if node title is empty, try to set the label using host and filename + // PlacesUtils._uri() will throw if aNode.uri is not a valid URI + try { + var uri = PlacesUtils._uri(aNode.uri); + var host = uri.host; + var fileName = uri.QueryInterface(Ci.nsIURL).fileName; + // if fileName is empty, use path to distinguish labels + if (aDoNotCutTitle) { + title = host + uri.path; + } else { + title = host + (fileName ? + (host ? "/" + this.ellipsis + "/" : "") + fileName : + uri.path); + } + } + catch (e) { + // Use (no title) for non-standard URIs (data:, javascript:, ...) + title = ""; + } + } + else + title = aNode.title; + + return title || this.getString("noTitle"); + }, + + get leftPaneQueries() { + // build the map + this.leftPaneFolderId; + return this.leftPaneQueries; + }, + + // Get the folder id for the organizer left-pane folder. + get leftPaneFolderId() { + let leftPaneRoot = -1; + let allBookmarksId; + + // Shortcuts to services. + let bs = PlacesUtils.bookmarks; + let as = PlacesUtils.annotations; + + // This is the list of the left pane queries. + let queries = { + "PlacesRoot": { title: "" }, + "History": { title: this.getString("OrganizerQueryHistory") }, + "Downloads": { title: this.getString("OrganizerQueryDownloads") }, + "Tags": { title: this.getString("OrganizerQueryTags") }, + "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") }, + "BookmarksToolbar": + { title: null, + concreteTitle: PlacesUtils.getString("BookmarksToolbarFolderTitle"), + concreteId: PlacesUtils.toolbarFolderId }, + "BookmarksMenu": + { title: null, + concreteTitle: PlacesUtils.getString("BookmarksMenuFolderTitle"), + concreteId: PlacesUtils.bookmarksMenuFolderId }, + "UnfiledBookmarks": + { title: null, + concreteTitle: PlacesUtils.getString("UnsortedBookmarksFolderTitle"), + concreteId: PlacesUtils.unfiledBookmarksFolderId }, + }; + // All queries but PlacesRoot. + const EXPECTED_QUERY_COUNT = 7; + + // Removes an item and associated annotations, ignoring eventual errors. + function safeRemoveItem(aItemId) { + try { + if (as.itemHasAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) && + !(as.getItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) in queries)) { + // Some extension annotated their roots with our query annotation, + // so we should not delete them. + return; + } + // removeItemAnnotation does not check if item exists, nor the anno, + // so this is safe to do. + as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO); + as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO); + // This will throw if the annotation is an orphan. + bs.removeItem(aItemId); + } + catch(e) { /* orphan anno */ } + } + + // Returns true if item really exists, false otherwise. + function itemExists(aItemId) { + try { + bs.getItemIndex(aItemId); + return true; + } + catch(e) { + return false; + } + } + + // Get all items marked as being the left pane folder. + let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO); + if (items.length > 1) { + // Something went wrong, we cannot have more than one left pane folder, + // remove all left pane folders and continue. We will create a new one. + items.forEach(safeRemoveItem); + } + else if (items.length == 1 && items[0] != -1) { + leftPaneRoot = items[0]; + // Check that organizer left pane root is valid. + let version = as.getItemAnnotation(leftPaneRoot, this.ORGANIZER_FOLDER_ANNO); + if (version != this.ORGANIZER_LEFTPANE_VERSION || + !itemExists(leftPaneRoot)) { + // Invalid root, we must rebuild the left pane. + safeRemoveItem(leftPaneRoot); + leftPaneRoot = -1; + } + } + + if (leftPaneRoot != -1) { + // A valid left pane folder has been found. + // Build the leftPaneQueries Map. This is used to quickly access them, + // associating a mnemonic name to the real item ids. + delete this.leftPaneQueries; + this.leftPaneQueries = {}; + + let items = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO); + // While looping through queries we will also check for their validity. + let queriesCount = 0; + for (let i = 0; i < items.length; i++) { + let queryName = as.getItemAnnotation(items[i], this.ORGANIZER_QUERY_ANNO); + + // Some extension did use our annotation to decorate their items + // with icons, so we should check only our elements, to avoid dataloss. + if (!(queryName in queries)) + continue; + + let query = queries[queryName]; + query.itemId = items[i]; + + if (!itemExists(query.itemId)) { + // Orphan annotation, bail out and create a new left pane root. + break; + } + + // Check that all queries have valid parents. + let parentId = bs.getFolderIdForItem(query.itemId); + if (items.indexOf(parentId) == -1 && parentId != leftPaneRoot) { + // The parent is not part of the left pane, bail out and create a new + // left pane root. + break; + } + + // Titles could have been corrupted or the user could have changed his + // locale. Check title and eventually fix it. + if (bs.getItemTitle(query.itemId) != query.title) + bs.setItemTitle(query.itemId, query.title); + if ("concreteId" in query) { + if (bs.getItemTitle(query.concreteId) != query.concreteTitle) + bs.setItemTitle(query.concreteId, query.concreteTitle); + } + + // Add the query to our cache. + this.leftPaneQueries[queryName] = query.itemId; + queriesCount++; + } + + if (queriesCount != EXPECTED_QUERY_COUNT) { + // Queries number is wrong, so the left pane must be corrupt. + // Note: we can't just remove the leftPaneRoot, because some query could + // have a bad parent, so we have to remove all items one by one. + items.forEach(safeRemoveItem); + safeRemoveItem(leftPaneRoot); + } + else { + // Everything is fine, return the current left pane folder. + delete this.leftPaneFolderId; + return this.leftPaneFolderId = leftPaneRoot; + } + } + + // Create a new left pane folder. + var callback = { + // Helper to create an organizer special query. + create_query: function CB_create_query(aQueryName, aParentId, aQueryUrl) { + let itemId = bs.insertBookmark(aParentId, + PlacesUtils._uri(aQueryUrl), + bs.DEFAULT_INDEX, + queries[aQueryName].title); + // Mark as special organizer query. + as.setItemAnnotation(itemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aQueryName, + 0, as.EXPIRE_NEVER); + // We should never backup this, since it changes between profiles. + as.setItemAnnotation(itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, + 0, as.EXPIRE_NEVER); + // Add to the queries map. + PlacesUIUtils.leftPaneQueries[aQueryName] = itemId; + return itemId; + }, + + // Helper to create an organizer special folder. + create_folder: function CB_create_folder(aFolderName, aParentId, aIsRoot) { + // Left Pane Root Folder. + let folderId = bs.createFolder(aParentId, + queries[aFolderName].title, + bs.DEFAULT_INDEX); + // We should never backup this, since it changes between profiles. + as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, + 0, as.EXPIRE_NEVER); + + if (aIsRoot) { + // Mark as special left pane root. + as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO, + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, + 0, as.EXPIRE_NEVER); + } + else { + // Mark as special organizer folder. + as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aFolderName, + 0, as.EXPIRE_NEVER); + PlacesUIUtils.leftPaneQueries[aFolderName] = folderId; + } + return folderId; + }, + + runBatched: function CB_runBatched(aUserData) { + delete PlacesUIUtils.leftPaneQueries; + PlacesUIUtils.leftPaneQueries = { }; + + // Left Pane Root Folder. + leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true); + + // History Query. + this.create_query("History", leftPaneRoot, + "place:type=" + + Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY + + "&sort=" + + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); + + // Downloads. + this.create_query("Downloads", leftPaneRoot, + "place:transition=" + + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + + "&sort=" + + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); + + // Tags Query. + this.create_query("Tags", leftPaneRoot, + "place:type=" + + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY + + "&sort=" + + Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING); + + // All Bookmarks Folder. + allBookmarksId = this.create_folder("AllBookmarks", leftPaneRoot, false); + + // All Bookmarks->Bookmarks Toolbar Query. + this.create_query("BookmarksToolbar", allBookmarksId, + "place:folder=TOOLBAR"); + + // All Bookmarks->Bookmarks Menu Query. + this.create_query("BookmarksMenu", allBookmarksId, + "place:folder=BOOKMARKS_MENU"); + + // All Bookmarks->Unfiled Bookmarks Query. + this.create_query("UnfiledBookmarks", allBookmarksId, + "place:folder=UNFILED_BOOKMARKS"); + } + }; + bs.runInBatchMode(callback, null); + // Maybe: PlacesUtils.bookmarks.runInBatchMode(callback, null); ? + + delete this.leftPaneFolderId; + return this.leftPaneFolderId = leftPaneRoot; + }, + + /** + * Get the folder id for the organizer left-pane folder. + */ + get allBookmarksFolderId() { + // ensure the left-pane root is initialized; + this.leftPaneFolderId; + delete this.allBookmarksFolderId; + return this.allBookmarksFolderId = this.leftPaneQueries["AllBookmarks"]; + }, + + /** + * If an item is a left-pane query, returns the name of the query + * or an empty string if not. + * + * @param aItemId id of a container + * @returns the name of the query, or empty string if not a left-pane query + */ + getLeftPaneQueryNameFromId: function PUIU_getLeftPaneQueryNameFromId(aItemId) { + var queryName = ""; + // If the let pane hasn't been built, use the annotation service + // directly, to avoid building the left pane too early. + if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) { + try { + queryName = PlacesUtils.annotations. + getItemAnnotation(aItemId, this.ORGANIZER_QUERY_ANNO); + } + catch (ex) { + // doesn't have the annotation + queryName = ""; + } + } + else { + // If the left pane has already been built, use the name->id map + // cached in PlacesUIUtils. + for (let [name, id] in Iterator(this.leftPaneQueries)) { + if (aItemId == id) + queryName = name; + } + } + return queryName; + }, + + /** + * Returns the passed URL with a #moz-resolution fragment + * for the specified dimensions and devicePixelRatio. + * + * @param aWindow + * A window from where we want to get the device + * pixel Ratio + * + * @param aURL + * The URL where we should add the fragment + * + * @param aWidth + * The target image width + * + * @param aHeight + * The target image height + * + * @return The URL with the fragment at the end + */ + getImageURLForResolution: + function PUIU_getImageURLForResolution(aWindow, aURL, aWidth, aHeight) { + return aURL; + } +}; + +XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF", + "@mozilla.org/rdf/rdf-service;1", + "nsIRDFService"); + +XPCOMUtils.defineLazyGetter(PlacesUIUtils, "localStore", function() { + return PlacesUIUtils.RDF.GetDataSource("rdf:local-store"); +}); + +XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() { + return Services.prefs.getComplexValue("intl.ellipsis", + Ci.nsIPrefLocalizedString).data; +}); + +XPCOMUtils.defineLazyServiceGetter(this, "URIFixup", + "@mozilla.org/docshell/urifixup;1", + "nsIURIFixup"); + +XPCOMUtils.defineLazyGetter(this, "bundle", function() { + const PLACES_STRING_BUNDLE_URI = + "chrome://browser/locale/places/places.properties"; + return Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(PLACES_STRING_BUNDLE_URI); +}); + +XPCOMUtils.defineLazyServiceGetter(this, "focusManager", + "@mozilla.org/focus-manager;1", + "nsIFocusManager"); + +/** + * This is a compatibility shim for old PUIU.ptm users. + * + * If you're looking for transactions and writing new code using them, directly + * use the transactions objects exported by the PlacesUtils.jsm module. + * + * This object will be removed once enough users are converted to the new API. + */ +XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ptm", function() { + // Ensure PlacesUtils is imported in scope. + PlacesUtils; + + return { + aggregateTransactions: function(aName, aTransactions) + new PlacesAggregatedTransaction(aName, aTransactions), + + createFolder: function(aName, aContainer, aIndex, aAnnotations, + aChildItemsTransactions) + new PlacesCreateFolderTransaction(aName, aContainer, aIndex, aAnnotations, + aChildItemsTransactions), + + createItem: function(aURI, aContainer, aIndex, aTitle, aKeyword, + aAnnotations, aChildTransactions) + new PlacesCreateBookmarkTransaction(aURI, aContainer, aIndex, aTitle, + aKeyword, aAnnotations, + aChildTransactions), + + createSeparator: function(aContainer, aIndex) + new PlacesCreateSeparatorTransaction(aContainer, aIndex), + + createLivemark: function(aFeedURI, aSiteURI, aName, aContainer, aIndex, + aAnnotations) + new PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName, aContainer, + aIndex, aAnnotations), + + moveItem: function(aItemId, aNewContainer, aNewIndex) + new PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex), + + removeItem: function(aItemId) + new PlacesRemoveItemTransaction(aItemId), + + editItemTitle: function(aItemId, aNewTitle) + new PlacesEditItemTitleTransaction(aItemId, aNewTitle), + + editBookmarkURI: function(aItemId, aNewURI) + new PlacesEditBookmarkURITransaction(aItemId, aNewURI), + + setItemAnnotation: function(aItemId, aAnnotationObject) + new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject), + + setPageAnnotation: function(aURI, aAnnotationObject) + new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject), + + editBookmarkKeyword: function(aItemId, aNewKeyword) + new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword), + + editBookmarkPostData: function(aItemId, aPostData) + new PlacesEditBookmarkPostDataTransaction(aItemId, aPostData), + + editLivemarkSiteURI: function(aLivemarkId, aSiteURI) + new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI), + + editLivemarkFeedURI: function(aLivemarkId, aFeedURI) + new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI), + + editItemDateAdded: function(aItemId, aNewDateAdded) + new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded), + + editItemLastModified: function(aItemId, aNewLastModified) + new PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified), + + sortFolderByName: function(aFolderId) + new PlacesSortFolderByNameTransaction(aFolderId), + + tagURI: function(aURI, aTags) + new PlacesTagURITransaction(aURI, aTags), + + untagURI: function(aURI, aTags) + new PlacesUntagURITransaction(aURI, aTags), + + /** + * Transaction for setting/unsetting Load-in-sidebar annotation. + * + * @param aBookmarkId + * id of the bookmark where to set Load-in-sidebar annotation. + * @param aLoadInSidebar + * boolean value. + * @returns nsITransaction object. + */ + setLoadInSidebar: function(aItemId, aLoadInSidebar) + { + let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO, + type: Ci.nsIAnnotationService.TYPE_INT32, + flags: 0, + value: aLoadInSidebar, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + return new PlacesSetItemAnnotationTransaction(aItemId, annoObj); + }, + + /** + * Transaction for editing the description of a bookmark or a folder. + * + * @param aItemId + * id of the item to edit. + * @param aDescription + * new description. + * @returns nsITransaction object. + */ + editItemDescription: function(aItemId, aDescription) + { + let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO, + type: Ci.nsIAnnotationService.TYPE_STRING, + flags: 0, + value: aDescription, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + return new PlacesSetItemAnnotationTransaction(aItemId, annoObj); + }, + + //////////////////////////////////////////////////////////////////////////// + //// nsITransactionManager forwarders. + + beginBatch: function() + PlacesUtils.transactionManager.beginBatch(null), + + endBatch: function() + PlacesUtils.transactionManager.endBatch(false), + + doTransaction: function(txn) + PlacesUtils.transactionManager.doTransaction(txn), + + undoTransaction: function() + PlacesUtils.transactionManager.undoTransaction(), + + redoTransaction: function() + PlacesUtils.transactionManager.redoTransaction(), + + get numberOfUndoItems() + PlacesUtils.transactionManager.numberOfUndoItems, + get numberOfRedoItems() + PlacesUtils.transactionManager.numberOfRedoItems, + get maxTransactionCount() + PlacesUtils.transactionManager.maxTransactionCount, + set maxTransactionCount(val) + PlacesUtils.transactionManager.maxTransactionCount = val, + + clear: function() + PlacesUtils.transactionManager.clear(), + + peekUndoStack: function() + PlacesUtils.transactionManager.peekUndoStack(), + + peekRedoStack: function() + PlacesUtils.transactionManager.peekRedoStack(), + + getUndoStack: function() + PlacesUtils.transactionManager.getUndoStack(), + + getRedoStack: function() + PlacesUtils.transactionManager.getRedoStack(), + + AddListener: function(aListener) + PlacesUtils.transactionManager.AddListener(aListener), + + RemoveListener: function(aListener) + PlacesUtils.transactionManager.RemoveListener(aListener) + } +}); diff --git a/webbrowser/components/places/content/bookmarkProperties.js b/webbrowser/components/places/content/bookmarkProperties.js new file mode 100644 index 0000000..e1d1077 --- /dev/null +++ b/webbrowser/components/places/content/bookmarkProperties.js @@ -0,0 +1,675 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/** + * The panel is initialized based on data given in the js object passed + * as window.arguments[0]. The object must have the following fields set: + * @ action (String). Possible values: + * - "add" - for adding a new item. + * @ type (String). Possible values: + * - "bookmark" + * @ loadBookmarkInSidebar - optional, the default state for the + * "Load this bookmark in the sidebar" field. + * - "folder" + * @ URIList (Array of nsIURI objects) - optional, list of uris to + * be bookmarked under the new folder. + * - "livemark" + * @ uri (nsIURI object) - optional, the default uri for the new item. + * The property is not used for the "folder with items" type. + * @ title (String) - optional, the default title for the new item. + * @ description (String) - optional, the default description for the new + * item. + * @ defaultInsertionPoint (InsertionPoint JS object) - optional, the + * default insertion point for the new item. + * @ keyword (String) - optional, the default keyword for the new item. + * @ postData (String) - optional, POST data to accompany the keyword. + * @ charSet (String) - optional, character-set to accompany the keyword. + * Notes: + * 1) If |uri| is set for a bookmark/livemark item and |title| isn't, + * the dialog will query the history tables for the title associated + * with the given uri. If the dialog is set to adding a folder with + * bookmark items under it (see URIList), a default static title is + * used ("[Folder Name]"). + * 2) The index field of the default insertion point is ignored if + * the folder picker is shown. + * - "edit" - for editing a bookmark item or a folder. + * @ type (String). Possible values: + * - "bookmark" + * @ itemId (Integer) - the id of the bookmark item. + * - "folder" (also applies to livemarks) + * @ itemId (Integer) - the id of the folder. + * @ hiddenRows (Strings array) - optional, list of rows to be hidden + * regardless of the item edited or added by the dialog. + * Possible values: + * - "title" + * - "location" + * - "description" + * - "keyword" + * - "tags" + * - "loadInSidebar" + * - "feedLocation" + * - "siteLocation" + * - "folderPicker" - hides both the tree and the menu. + * @ readOnly (Boolean) - optional, states if the panel should be read-only + * + * window.arguments[0].performed is set to true if any transaction has + * been performed by the dialog. + */ + +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const BOOKMARK_ITEM = 0; +const BOOKMARK_FOLDER = 1; +const LIVEMARK_CONTAINER = 2; + +const ACTION_EDIT = 0; +const ACTION_ADD = 1; + +var elementsHeight = new Map(); + +var BookmarkPropertiesPanel = { + + /** UI Text Strings */ + __strings: null, + get _strings() { + if (!this.__strings) { + this.__strings = document.getElementById("stringBundle"); + } + return this.__strings; + }, + + _action: null, + _itemType: null, + _itemId: -1, + _uri: null, + _loadInSidebar: false, + _title: "", + _description: "", + _URIs: [], + _keyword: "", + _postData: null, + _charSet: "", + _feedURI: null, + _siteURI: null, + + _defaultInsertionPoint: null, + _hiddenRows: [], + _batching: false, + _readOnly: false, + + /** + * This method returns the correct label for the dialog's "accept" + * button based on the variant of the dialog. + */ + _getAcceptLabel: function BPP__getAcceptLabel() { + if (this._action == ACTION_ADD) { + if (this._URIs.length) + return this._strings.getString("dialogAcceptLabelAddMulti"); + + if (this._itemType == LIVEMARK_CONTAINER) + return this._strings.getString("dialogAcceptLabelAddLivemark"); + + if (this._dummyItem || this._loadInSidebar) + return this._strings.getString("dialogAcceptLabelAddItem"); + + return this._strings.getString("dialogAcceptLabelSaveItem"); + } + return this._strings.getString("dialogAcceptLabelEdit"); + }, + + /** + * This method returns the correct title for the current variant + * of this dialog. + */ + _getDialogTitle: function BPP__getDialogTitle() { + if (this._action == ACTION_ADD) { + if (this._itemType == BOOKMARK_ITEM) + return this._strings.getString("dialogTitleAddBookmark"); + if (this._itemType == LIVEMARK_CONTAINER) + return this._strings.getString("dialogTitleAddLivemark"); + + // add folder + NS_ASSERT(this._itemType == BOOKMARK_FOLDER, "Unknown item type"); + if (this._URIs.length) + return this._strings.getString("dialogTitleAddMulti"); + + return this._strings.getString("dialogTitleAddFolder"); + } + if (this._action == ACTION_EDIT) { + return this._strings.getFormattedString("dialogTitleEdit", [this._title]); + } + return ""; + }, + + /** + * Determines the initial data for the item edited or added by this dialog + */ + _determineItemInfo: function BPP__determineItemInfo() { + var dialogInfo = window.arguments[0]; + this._action = dialogInfo.action == "add" ? ACTION_ADD : ACTION_EDIT; + this._hiddenRows = dialogInfo.hiddenRows ? dialogInfo.hiddenRows : []; + if (this._action == ACTION_ADD) { + NS_ASSERT("type" in dialogInfo, "missing type property for add action"); + + if ("title" in dialogInfo) + this._title = dialogInfo.title; + + if ("defaultInsertionPoint" in dialogInfo) { + this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint; + } + else + this._defaultInsertionPoint = + new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Ci.nsITreeView.DROP_ON); + + switch (dialogInfo.type) { + case "bookmark": + this._itemType = BOOKMARK_ITEM; + if ("uri" in dialogInfo) { + NS_ASSERT(dialogInfo.uri instanceof Ci.nsIURI, + "uri property should be a uri object"); + this._uri = dialogInfo.uri; + if (typeof(this._title) != "string") { + this._title = this._getURITitleFromHistory(this._uri) || + this._uri.spec; + } + } + else { + this._uri = PlacesUtils._uri("about:blank"); + this._title = this._strings.getString("newBookmarkDefault"); + this._dummyItem = true; + } + + if ("loadBookmarkInSidebar" in dialogInfo) + this._loadInSidebar = dialogInfo.loadBookmarkInSidebar; + + if ("keyword" in dialogInfo) { + this._keyword = dialogInfo.keyword; + this._isAddKeywordDialog = true; + if ("postData" in dialogInfo) + this._postData = dialogInfo.postData; + if ("charSet" in dialogInfo) + this._charSet = dialogInfo.charSet; + } + break; + + case "folder": + this._itemType = BOOKMARK_FOLDER; + if (!this._title) { + if ("URIList" in dialogInfo) { + this._title = this._strings.getString("bookmarkAllTabsDefault"); + this._URIs = dialogInfo.URIList; + } + else + this._title = this._strings.getString("newFolderDefault"); + this._dummyItem = true; + } + break; + + case "livemark": + this._itemType = LIVEMARK_CONTAINER; + if ("feedURI" in dialogInfo) + this._feedURI = dialogInfo.feedURI; + if ("siteURI" in dialogInfo) + this._siteURI = dialogInfo.siteURI; + + if (!this._title) { + if (this._feedURI) { + this._title = this._getURITitleFromHistory(this._feedURI) || + this._feedURI.spec; + } + else + this._title = this._strings.getString("newLivemarkDefault"); + } + } + + if ("description" in dialogInfo) + this._description = dialogInfo.description; + } + else { // edit + NS_ASSERT("itemId" in dialogInfo); + this._itemId = dialogInfo.itemId; + this._title = PlacesUtils.bookmarks.getItemTitle(this._itemId); + this._readOnly = !!dialogInfo.readOnly; + + switch (dialogInfo.type) { + case "bookmark": + this._itemType = BOOKMARK_ITEM; + + this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); + // keyword + this._keyword = PlacesUtils.bookmarks + .getKeywordForBookmark(this._itemId); + // Load In Sidebar + this._loadInSidebar = PlacesUtils.annotations + .itemHasAnnotation(this._itemId, + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); + break; + + case "folder": + this._itemType = BOOKMARK_FOLDER; + PlacesUtils.livemarks.getLivemark({ id: this._itemId }) + .then(aLivemark => { + this._itemType = LIVEMARK_CONTAINER; + this._feedURI = aLivemark.feedURI; + this._siteURI = aLivemark.siteURI; + this._fillEditProperties(); + + let acceptButton = document.documentElement.getButton("accept"); + acceptButton.disabled = !this._inputIsValid(); + + let newHeight = window.outerHeight + + this._element("descriptionField").boxObject.height; + window.resizeTo(window.outerWidth, newHeight); + }, () => undefined); + + break; + } + + // Description + if (PlacesUtils.annotations + .itemHasAnnotation(this._itemId, PlacesUIUtils.DESCRIPTION_ANNO)) { + this._description = PlacesUtils.annotations + .getItemAnnotation(this._itemId, + PlacesUIUtils.DESCRIPTION_ANNO); + } + } + }, + + /** + * This method returns the title string corresponding to a given URI. + * If none is available from the bookmark service (probably because + * the given URI doesn't appear in bookmarks or history), we synthesize + * a title from the first 100 characters of the URI. + * + * @param aURI + * nsIURI object for which we want the title + * + * @returns a title string + */ + _getURITitleFromHistory: function BPP__getURITitleFromHistory(aURI) { + NS_ASSERT(aURI instanceof Ci.nsIURI); + + // get the title from History + return PlacesUtils.history.getPageTitle(aURI); + }, + + /** + * This method should be called by the onload of the Bookmark Properties + * dialog to initialize the state of the panel. + */ + onDialogLoad: Task.async(function* BPP_onDialogLoad() { + this._determineItemInfo(); + + document.title = this._getDialogTitle(); + var acceptButton = document.documentElement.getButton("accept"); + acceptButton.label = this._getAcceptLabel(); + + // Do not use sizeToContent, otherwise, due to bug 90276, the dialog will + // grow at every opening. + // Since elements can be uncollapsed asynchronously, we must observe their + // mutations and resize the dialog using a cached element size. + this._height = window.outerHeight; + this._mutationObserver = new MutationObserver(mutations => { + for (let mutation of mutations) { + let target = mutation.target; + let id = target.id; + if (!/^editBMPanel_.*(Row|Checkbox)$/.test(id)) + continue; + + let collapsed = target.getAttribute("collapsed") === "true"; + let wasCollapsed = mutation.oldValue === "true"; + if (collapsed == wasCollapsed) + continue; + + if (collapsed) { + this._height -= elementsHeight.get(id); + elementsHeight.delete(id); + } else { + elementsHeight.set(id, target.boxObject.height); + this._height += elementsHeight.get(id); + } + window.resizeTo(window.outerWidth, this._height); + } + }); + + this._mutationObserver.observe(document, + { subtree: true, + attributeOldValue: true, + attributeFilter: ["collapsed"] }); + + // Some controls are flexible and we want to update their cached size when + // the dialog is resized. + window.addEventListener("resize", this); + + this._beginBatch(); + + switch (this._action) { + case ACTION_EDIT: + this._fillEditProperties(); + acceptButton.disabled = this._readOnly; + break; + case ACTION_ADD: + yield this._fillAddProperties(); + // if this is an uri related dialog disable accept button until + // the user fills an uri value. + if (this._itemType == BOOKMARK_ITEM) + acceptButton.disabled = !this._inputIsValid(); + break; + } + + if (!this._readOnly) { + // Listen on uri fields to enable accept button if input is valid + if (this._itemType == BOOKMARK_ITEM) { + this._element("locationField") + .addEventListener("input", this, false); + if (this._isAddKeywordDialog) { + this._element("keywordField") + .addEventListener("input", this, false); + } + } + else if (this._itemType == LIVEMARK_CONTAINER) { + this._element("feedLocationField") + .addEventListener("input", this, false); + this._element("siteLocationField") + .addEventListener("input", this, false); + } + } + + // Ensure the Name Picker textbox is focused on load + var namePickerElem = document.getElementById('editBMPanel_namePicker'); + namePickerElem.focus(); + namePickerElem.select(); + }), + + // nsIDOMEventListener + handleEvent: function BPP_handleEvent(aEvent) { + var target = aEvent.target; + switch (aEvent.type) { + case "input": + if (target.id == "editBMPanel_locationField" || + target.id == "editBMPanel_feedLocationField" || + target.id == "editBMPanel_siteLocationField" || + target.id == "editBMPanel_keywordField") { + // Check uri fields to enable accept button if input is valid + document.documentElement + .getButton("accept").disabled = !this._inputIsValid(); + } + break; + case "resize": + for (let [id, oldHeight] of elementsHeight) { + let newHeight = document.getElementById(id).boxObject.height; + this._height += - oldHeight + newHeight; + elementsHeight.set(id, newHeight); + } + break; + } + }, + + _beginBatch: function BPP__beginBatch() { + if (this._batching) + return; + + PlacesUtils.transactionManager.beginBatch(null); + this._batching = true; + }, + + _endBatch: function BPP__endBatch() { + if (!this._batching) + return; + + PlacesUtils.transactionManager.endBatch(false); + this._batching = false; + }, + + _fillEditProperties: function BPP__fillEditProperties() { + gEditItemOverlay.initPanel(this._itemId, + { hiddenRows: this._hiddenRows, + forceReadOnly: this._readOnly }); + }, + + _fillAddProperties: Task.async(function* BPP__fillAddProperties() { + yield this._createNewItem(); + // Edit the new item + gEditItemOverlay.initPanel(this._itemId, + { hiddenRows: this._hiddenRows }); + // Empty location field if the uri is about:blank, this way inserting a new + // url will be easier for the user, Accept button will be automatically + // disabled by the input listener until the user fills the field. + var locationField = this._element("locationField"); + if (locationField.value == "about:blank") + locationField.value = ""; + }), + + // nsISupports + QueryInterface: function BPP_QueryInterface(aIID) { + if (aIID.equals(Ci.nsIDOMEventListener) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_NOINTERFACE; + }, + + _element: function BPP__element(aID) { + return document.getElementById("editBMPanel_" + aID); + }, + + onDialogUnload: function BPP_onDialogUnload() { + // gEditItemOverlay does not exist anymore here, so don't rely on it. + this._mutationObserver.disconnect(); + delete this._mutationObserver; + + window.removeEventListener("resize", this); + + // Calling removeEventListener with arguments which do not identify any + // currently registered EventListener on the EventTarget has no effect. + this._element("locationField") + .removeEventListener("input", this, false); + this._element("feedLocationField") + .removeEventListener("input", this, false); + this._element("siteLocationField") + .removeEventListener("input", this, false); + }, + + onDialogAccept: function BPP_onDialogAccept() { + // We must blur current focused element to save its changes correctly + document.commandDispatcher.focusedElement.blur(); + // The order here is important! We have to uninit the panel first, otherwise + // late changes could force it to commit more transactions. + gEditItemOverlay.uninitPanel(true); + this._endBatch(); + window.arguments[0].performed = true; + }, + + onDialogCancel: function BPP_onDialogCancel() { + // The order here is important! We have to uninit the panel first, otherwise + // changes done as part of Undo may change the panel contents and by + // that force it to commit more transactions. + gEditItemOverlay.uninitPanel(true); + this._endBatch(); + PlacesUtils.transactionManager.undoTransaction(); + window.arguments[0].performed = false; + }, + + /** + * This method checks to see if the input fields are in a valid state. + * + * @returns true if the input is valid, false otherwise + */ + _inputIsValid: function BPP__inputIsValid() { + if (this._itemType == BOOKMARK_ITEM && + !this._containsValidURI("locationField")) + return false; + if (this._isAddKeywordDialog && !this._element("keywordField").value.length) + return false; + + return true; + }, + + /** + * Determines whether the XUL textbox with the given ID contains a + * string that can be converted into an nsIURI. + * + * @param aTextboxID + * the ID of the textbox element whose contents we'll test + * + * @returns true if the textbox contains a valid URI string, false otherwise + */ + _containsValidURI: function BPP__containsValidURI(aTextboxID) { + try { + var value = this._element(aTextboxID).value; + if (value) { + PlacesUIUtils.createFixedURI(value); + return true; + } + } catch (e) { } + return false; + }, + + /** + * [New Item Mode] Get the insertion point details for the new item, given + * dialog state and opening arguments. + * + * The container-identifier and insertion-index are returned separately in + * the form of [containerIdentifier, insertionIndex] + */ + _getInsertionPointDetails: function BPP__getInsertionPointDetails() { + var containerId = this._defaultInsertionPoint.itemId; + var indexInContainer = this._defaultInsertionPoint.index; + + return [containerId, indexInContainer]; + }, + + /** + * Returns a transaction for creating a new bookmark item representing the + * various fields and opening arguments of the dialog. + */ + _getCreateNewBookmarkTransaction: + function BPP__getCreateNewBookmarkTransaction(aContainer, aIndex) { + var annotations = []; + var childTransactions = []; + + if (this._description) { + let annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO, + type : Ci.nsIAnnotationService.TYPE_STRING, + flags : 0, + value : this._description, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + let editItemTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj); + childTransactions.push(editItemTxn); + } + + if (this._loadInSidebar) { + let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO, + value : true }; + let setLoadTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj); + childTransactions.push(setLoadTxn); + } + + if (this._postData) { + let postDataTxn = new PlacesEditBookmarkPostDataTransaction(-1, this._postData); + childTransactions.push(postDataTxn); + } + + //XXX TODO: this should be in a transaction! + if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window)) + PlacesUtils.setCharsetForURI(this._uri, this._charSet); + + let createTxn = new PlacesCreateBookmarkTransaction(this._uri, + aContainer, + aIndex, + this._title, + this._keyword, + annotations, + childTransactions); + + return new PlacesAggregatedTransaction(this._getDialogTitle(), + [createTxn]); + }, + + /** + * Returns a childItems-transactions array representing the URIList with + * which the dialog has been opened. + */ + _getTransactionsForURIList: function BPP__getTransactionsForURIList() { + var transactions = []; + for (var i = 0; i < this._URIs.length; ++i) { + var uri = this._URIs[i]; + var title = this._getURITitleFromHistory(uri); + var createTxn = new PlacesCreateBookmarkTransaction(uri, -1, + PlacesUtils.bookmarks.DEFAULT_INDEX, + title); + transactions.push(createTxn); + } + return transactions; + }, + + /** + * Returns a transaction for creating a new folder item representing the + * various fields and opening arguments of the dialog. + */ + _getCreateNewFolderTransaction: + function BPP__getCreateNewFolderTransaction(aContainer, aIndex) { + var annotations = []; + var childItemsTransactions; + if (this._URIs.length) + childItemsTransactions = this._getTransactionsForURIList(); + + if (this._description) + annotations.push(this._getDescriptionAnnotation(this._description)); + + return new PlacesCreateFolderTransaction(this._title, aContainer, + aIndex, annotations, + childItemsTransactions); + }, + + /** + * Returns a transaction for creating a new live-bookmark item representing + * the various fields and opening arguments of the dialog. + */ + _getCreateNewLivemarkTransaction: + function BPP__getCreateNewLivemarkTransaction(aContainer, aIndex) { + return new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI, + this._title, + aContainer, aIndex); + }, + + /** + * Dialog-accept code-path for creating a new item (any type) + */ + _createNewItem: Task.async(function* BPP__getCreateItemTransaction() { + var [container, index] = this._getInsertionPointDetails(); + var txn; + + switch (this._itemType) { + case BOOKMARK_FOLDER: + txn = this._getCreateNewFolderTransaction(container, index); + break; + case LIVEMARK_CONTAINER: + txn = this._getCreateNewLivemarkTransaction(container, index); + break; + default: // BOOKMARK_ITEM + txn = this._getCreateNewBookmarkTransaction(container, index); + } + + PlacesUtils.transactionManager.doTransaction(txn); + // This is a temporary hack until we use PlacesTransactions.jsm + if (txn._promise) { + yield txn._promise; + } + + let folderGuid = yield PlacesUtils.promiseItemGuid(container); + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: folderGuid, + index: index + }); + this._itemId = yield PlacesUtils.promiseItemId(bm.guid); + }) +}; diff --git a/webbrowser/components/places/content/bookmarkProperties.xul b/webbrowser/components/places/content/bookmarkProperties.xul new file mode 100644 index 0000000..2c04f8b --- /dev/null +++ b/webbrowser/components/places/content/bookmarkProperties.xul @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + %editBookmarkOverlayDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webbrowser/components/places/content/editBookmarkOverlay.js b/webbrowser/components/places/content/editBookmarkOverlay.js new file mode 100644 index 0000000..69d7d32 --- /dev/null +++ b/webbrowser/components/places/content/editBookmarkOverlay.js @@ -0,0 +1,1063 @@ +/* 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/. */ + +const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed"; +const MAX_FOLDER_ITEM_IN_MENU_LIST = 5; + +var gEditItemOverlay = { + _uri: null, + _itemId: -1, + _itemIds: [], + _uris: [], + _tags: [], + _allTags: [], + _keyword: null, + _multiEdit: false, + _itemType: -1, + _readOnly: false, + _hiddenRows: [], + _onPanelReady: false, + _observersAdded: false, + _staticFoldersListBuilt: false, + _initialized: false, + _titleOverride: "", + + // the first field which was edited after this panel was initialized for + // a certain item + _firstEditedField: "", + + get itemId() { + return this._itemId; + }, + + get uri() { + return this._uri; + }, + + get multiEdit() { + return this._multiEdit; + }, + + /** + * Determines the initial data for the item edited or added by this dialog + */ + _determineInfo: function EIO__determineInfo(aInfo) { + // hidden rows + if (aInfo && aInfo.hiddenRows) + this._hiddenRows = aInfo.hiddenRows; + else + this._hiddenRows.splice(0, this._hiddenRows.length); + // force-read-only + this._readOnly = aInfo && aInfo.forceReadOnly; + this._titleOverride = aInfo && aInfo.titleOverride ? aInfo.titleOverride + : ""; + this._onPanelReady = aInfo && aInfo.onPanelReady; + }, + + _showHideRows: function EIO__showHideRows() { + var isBookmark = this._itemId != -1 && + this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK; + var isQuery = false; + if (this._uri) + isQuery = this._uri.schemeIs("place"); + + this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1; + this._element("folderRow").collapsed = + this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly; + this._element("tagsRow").collapsed = !this._uri || + this._hiddenRows.indexOf("tags") != -1 || isQuery; + // Collapse the tag selector if the item does not accept tags. + if (!this._element("tagsSelectorRow").collapsed && + this._element("tagsRow").collapsed) + this.toggleTagsSelector(); + this._element("descriptionRow").collapsed = + this._hiddenRows.indexOf("description") != -1 || this._readOnly; + this._element("keywordRow").collapsed = !isBookmark || this._readOnly || + this._hiddenRows.indexOf("keyword") != -1 || isQuery; + this._element("locationRow").collapsed = !(this._uri && !isQuery) || + this._hiddenRows.indexOf("location") != -1; + this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery || + this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1; + this._element("feedLocationRow").collapsed = !this._isLivemark || + this._hiddenRows.indexOf("feedLocation") != -1; + this._element("siteLocationRow").collapsed = !this._isLivemark || + this._hiddenRows.indexOf("siteLocation") != -1; + this._element("selectionCount").hidden = !this._multiEdit; + }, + + /** + * Initialize the panel + * @param aFor + * Either a places-itemId (of a bookmark, folder or a live bookmark), + * an array of itemIds (used for bulk tagging), or a URI object (in + * which case, the panel would be initialized in read-only mode). + * @param [optional] aInfo + * JS object which stores additional info for the panel + * initialization. The following properties may bet set: + * * hiddenRows (Strings array): list of rows to be hidden regardless + * of the item edited. Possible values: "title", "location", + * "description", "keyword", "loadInSidebar", "feedLocation", + * "siteLocation", folderPicker" + * * forceReadOnly - set this flag to initialize the panel to its + * read-only (view) mode even if the given item is editable. + */ + initPanel: function EIO_initPanel(aFor, aInfo) { + // For sanity ensure that the implementer has uninited the panel before + // trying to init it again, or we could end up leaking due to observers. + if (this._initialized) + this.uninitPanel(false); + + var aItemIdList; + if (Array.isArray(aFor)) { + aItemIdList = aFor; + aFor = aItemIdList[0]; + } + else if (this._multiEdit) { + this._multiEdit = false; + this._tags = []; + this._uris = []; + this._allTags = []; + this._itemIds = []; + this._element("selectionCount").hidden = true; + } + + this._folderMenuList = this._element("folderMenuList"); + this._folderTree = this._element("folderTree"); + + this._determineInfo(aInfo); + if (aFor instanceof Ci.nsIURI) { + this._itemId = -1; + this._uri = aFor; + this._readOnly = true; + } + else { + this._itemId = aFor; + // We can't store information on invalid itemIds. + this._readOnly = this._readOnly || this._itemId == -1; + + var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); + this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId); + if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { + this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); + this._keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId); + this._initTextField("keywordField", this._keyword); + this._element("loadInSidebarCheckbox").checked = + PlacesUtils.annotations.itemHasAnnotation(this._itemId, + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); + } + else { + this._uri = null; + this._isLivemark = false; + PlacesUtils.livemarks.getLivemark({id: this._itemId }) + .then(aLivemark => { + this._isLivemark = true; + this._initTextField("feedLocationField", aLivemark.feedURI.spec, true); + this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true); + this._showHideRows(); + }, () => undefined); + } + + // folder picker + this._initFolderMenuList(containerId); + + // description field + this._initTextField("descriptionField", + PlacesUIUtils.getItemDescription(this._itemId)); + } + + if (this._itemId == -1 || + this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { + this._isLivemark = false; + + this._initTextField("locationField", this._uri.spec); + if (!aItemIdList) { + var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); + this._initTextField("tagsField", tags, false); + } + else { + this._multiEdit = true; + this._allTags = []; + this._itemIds = aItemIdList; + for (var i = 0; i < aItemIdList.length; i++) { + if (aItemIdList[i] instanceof Ci.nsIURI) { + this._uris[i] = aItemIdList[i]; + this._itemIds[i] = -1; + } + else + this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]); + this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); + } + this._allTags = this._getCommonTags(); + this._initTextField("tagsField", this._allTags.join(", "), false); + this._element("itemsCountText").value = + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", + this._itemIds.length, + [this._itemIds.length]); + } + + // tags selector + this._rebuildTagsSelectorList(); + } + + // name picker + this._initNamePicker(); + + this._showHideRows(); + + // observe changes + if (!this._observersAdded) { + // Single bookmarks observe any change. History entries and multiEdit + // observe only tags changes, through bookmarks. + if (this._itemId != -1 || this._uri || this._multiEdit) + PlacesUtils.bookmarks.addObserver(this, false); + + this._element("namePicker").addEventListener("blur", this); + this._element("locationField").addEventListener("blur", this); + this._element("tagsField").addEventListener("blur", this); + this._element("keywordField").addEventListener("blur", this); + this._element("descriptionField").addEventListener("blur", this); + window.addEventListener("unload", this, false); + this._observersAdded = true; + } + + let focusElement = () => { + this._initialized = true; + }; + + if (this._onPanelReady) { + this._onPanelReady(focusElement); + } else { + focusElement(); + } + }, + + /** + * Finds tags that are in common among this._tags entries that track tags + * for each selected uri. + * The tags arrays should be kept up-to-date for this to work properly. + * + * @return array of common tags for the selected uris. + */ + _getCommonTags: function() { + return this._tags[0].filter( + function (aTag) this._tags.every( + function (aTags) aTags.indexOf(aTag) != -1 + ), this + ); + }, + + _initTextField: function(aTextFieldId, aValue, aReadOnly) { + var field = this._element(aTextFieldId); + field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly; + + if (field.value != aValue) { + field.value = aValue; + this._editorTransactionManagerClear(field); + } + }, + + /** + * Appends a menu-item representing a bookmarks folder to a menu-popup. + * @param aMenupopup + * The popup to which the menu-item should be added. + * @param aFolderId + * The identifier of the bookmarks folder. + * @return the new menu item. + */ + _appendFolderItemToMenupopup: + function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) { + // First make sure the folders-separator is visible + this._element("foldersSeparator").hidden = false; + + var folderMenuItem = document.createElement("menuitem"); + var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId) + folderMenuItem.folderId = aFolderId; + folderMenuItem.setAttribute("label", folderTitle); + folderMenuItem.className = "menuitem-iconic folder-icon"; + aMenupopup.appendChild(folderMenuItem); + return folderMenuItem; + }, + + _initFolderMenuList: function EIO__initFolderMenuList(aSelectedFolder) { + // clean up first + var menupopup = this._folderMenuList.menupopup; + while (menupopup.childNodes.length > 6) + menupopup.removeChild(menupopup.lastChild); + + const bms = PlacesUtils.bookmarks; + const annos = PlacesUtils.annotations; + + // Build the static list + var unfiledItem = this._element("unfiledRootItem"); + if (!this._staticFoldersListBuilt) { + unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId); + unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId; + var bmMenuItem = this._element("bmRootItem"); + bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId); + bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId; + var toolbarItem = this._element("toolbarFolderItem"); + toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId); + toolbarItem.folderId = PlacesUtils.toolbarFolderId; + this._staticFoldersListBuilt = true; + } + + // List of recently used folders: + var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO); + + /** + * The value of the LAST_USED_ANNO annotation is the time (in the form of + * Date.getTime) at which the folder has been last used. + * + * First we build the annotated folders array, each item has both the + * folder identifier and the time at which it was last-used by this dialog + * set. Then we sort it descendingly based on the time field. + */ + this._recentFolders = []; + for (var i = 0; i < folderIds.length; i++) { + var lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO); + this._recentFolders.push({ folderId: folderIds[i], lastUsed: lastUsed }); + } + this._recentFolders.sort(function(a, b) { + if (b.lastUsed < a.lastUsed) + return -1; + if (b.lastUsed > a.lastUsed) + return 1; + return 0; + }); + + var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST, + this._recentFolders.length); + for (var i = 0; i < numberOfItems; i++) { + this._appendFolderItemToMenupopup(menupopup, + this._recentFolders[i].folderId); + } + + var defaultItem = this._getFolderMenuItem(aSelectedFolder); + this._folderMenuList.selectedItem = defaultItem; + + // Set a selectedIndex attribute to show special icons + this._folderMenuList.setAttribute("selectedIndex", + this._folderMenuList.selectedIndex); + + // Hide the folders-separator if no folder is annotated as recently-used + this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6); + this._folderMenuList.disabled = this._readOnly; + }, + + QueryInterface: function EIO_QueryInterface(aIID) { + if (aIID.equals(Ci.nsIDOMEventListener) || + aIID.equals(Ci.nsINavBookmarkObserver) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + _element: function EIO__element(aID) { + return document.getElementById("editBMPanel_" + aID); + }, + + _editorTransactionManagerClear: function EIO__editorTransactionManagerClear(aItem) { + // Clear the editor's undo stack + let transactionManager; + try { + transactionManager = aItem.editor.transactionManager; + } catch (e) { + // When retrieving the transaction manager, editor may be null resulting + // in a TypeError. Additionally, the transaction manager may not + // exist yet, which causes access to it to throw NS_ERROR_FAILURE. + // In either event, the transaction manager doesn't exist it, so we + // don't need to worry about clearing it. + if (!(e instanceof TypeError) && e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + } + if (transactionManager) { + transactionManager.clear(); + } + }, + + _getItemStaticTitle: function EIO__getItemStaticTitle() { + if (this._titleOverride) + return this._titleOverride; + + let title = ""; + if (this._itemId == -1) { + title = PlacesUtils.history.getPageTitle(this._uri); + } + else { + title = PlacesUtils.bookmarks.getItemTitle(this._itemId); + } + return title; + }, + + _initNamePicker: function EIO_initNamePicker() { + var namePicker = this._element("namePicker"); + namePicker.value = this._getItemStaticTitle(); + namePicker.readOnly = this._readOnly; + this._editorTransactionManagerClear(namePicker); + }, + + uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) { + if (aHideCollapsibleElements) { + // hide the folder tree if it was previously visible + var folderTreeRow = this._element("folderTreeRow"); + if (!folderTreeRow.collapsed) + this.toggleFolderTreeVisibility(); + + // hide the tag selector if it was previously visible + var tagsSelectorRow = this._element("tagsSelectorRow"); + if (!tagsSelectorRow.collapsed) + this.toggleTagsSelector(); + } + + if (this._observersAdded) { + if (this._itemId != -1 || this._uri || this._multiEdit) + PlacesUtils.bookmarks.removeObserver(this); + + this._element("namePicker").removeEventListener("blur", this); + this._element("locationField").removeEventListener("blur", this); + this._element("tagsField").removeEventListener("blur", this); + this._element("keywordField").removeEventListener("blur", this); + this._element("descriptionField").removeEventListener("blur", this); + + this._observersAdded = false; + } + + this._itemId = -1; + this._uri = null; + this._uris = []; + this._tags = []; + this._allTags = []; + this._itemIds = []; + this._multiEdit = false; + this._firstEditedField = ""; + this._initialized = false; + this._titleOverride = ""; + this._readOnly = false; + }, + + onTagsFieldBlur: function EIO_onTagsFieldBlur() { + if (this._updateTags()) // if anything has changed + this._mayUpdateFirstEditField("tagsField"); + }, + + _updateTags: function EIO__updateTags() { + if (this._multiEdit) + return this._updateMultipleTagsForItems(); + return this._updateSingleTagForItem(); + }, + + _updateSingleTagForItem: function EIO__updateSingleTagForItem() { + var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri); + var tags = this._getTagsArrayFromTagField(); + if (tags.length > 0 || currentTags.length > 0) { + var tagsToRemove = []; + var tagsToAdd = []; + var txns = []; + for (var i = 0; i < currentTags.length; i++) { + if (tags.indexOf(currentTags[i]) == -1) + tagsToRemove.push(currentTags[i]); + } + for (var i = 0; i < tags.length; i++) { + if (currentTags.indexOf(tags[i]) == -1) + tagsToAdd.push(tags[i]); + } + + if (tagsToRemove.length > 0) { + let untagTxn = new PlacesUntagURITransaction(this._uri, tagsToRemove); + txns.push(untagTxn); + } + if (tagsToAdd.length > 0) { + let tagTxn = new PlacesTagURITransaction(this._uri, tagsToAdd); + txns.push(tagTxn); + } + + if (txns.length > 0) { + let aggregate = new PlacesAggregatedTransaction("Update tags", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + + // Ensure the tagsField is in sync, clean it up from empty tags + var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); + this._initTextField("tagsField", tags, false); + return true; + } + } + return false; + }, + + /** + * Stores the first-edit field for this dialog, if the passed-in field + * is indeed the first edited field + * @param aNewField + * the id of the field that may be set (without the "editBMPanel_" + * prefix) + */ + _mayUpdateFirstEditField: function EIO__mayUpdateFirstEditField(aNewField) { + // * The first-edit-field behavior is not applied in the multi-edit case + // * if this._firstEditedField is already set, this is not the first field, + // so there's nothing to do + if (this._multiEdit || this._firstEditedField) + return; + + this._firstEditedField = aNewField; + + // set the pref + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField); + }, + + _updateMultipleTagsForItems: function EIO__updateMultipleTagsForItems() { + var tags = this._getTagsArrayFromTagField(); + if (tags.length > 0 || this._allTags.length > 0) { + var tagsToRemove = []; + var tagsToAdd = []; + var txns = []; + for (var i = 0; i < this._allTags.length; i++) { + if (tags.indexOf(this._allTags[i]) == -1) + tagsToRemove.push(this._allTags[i]); + } + for (var i = 0; i < this._tags.length; i++) { + tagsToAdd[i] = []; + for (var j = 0; j < tags.length; j++) { + if (this._tags[i].indexOf(tags[j]) == -1) + tagsToAdd[i].push(tags[j]); + } + } + + if (tagsToAdd.length > 0) { + for (let i = 0; i < this._uris.length; i++) { + if (tagsToAdd[i].length > 0) { + let tagTxn = new PlacesTagURITransaction(this._uris[i], + tagsToAdd[i]); + txns.push(tagTxn); + } + } + } + if (tagsToRemove.length > 0) { + for (let i = 0; i < this._uris.length; i++) { + let untagTxn = new PlacesUntagURITransaction(this._uris[i], + tagsToRemove); + txns.push(untagTxn); + } + } + + if (txns.length > 0) { + let aggregate = new PlacesAggregatedTransaction("Update tags", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + + this._allTags = tags; + this._tags = []; + for (let i = 0; i < this._uris.length; i++) { + this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); + } + + // Ensure the tagsField is in sync, clean it up from empty tags + this._initTextField("tagsField", tags, false); + return true; + } + } + return false; + }, + + onNamePickerBlur: function EIO_onNamePickerBlur() { + if (this._itemId == -1) + return; + + var namePicker = this._element("namePicker") + + // Here we update either the item title or its cached static title + var newTitle = namePicker.value; + if (!newTitle && + PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) == PlacesUtils.tagsFolderId) { + // We don't allow setting an empty title for a tag, restore the old one. + this._initNamePicker(); + } + else if (this._getItemStaticTitle() != newTitle) { + this._mayUpdateFirstEditField("namePicker"); + let txn = new PlacesEditItemTitleTransaction(this._itemId, newTitle); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + onDescriptionFieldBlur: function EIO_onDescriptionFieldBlur() { + var description = this._element("descriptionField").value; + if (description != PlacesUIUtils.getItemDescription(this._itemId)) { + var annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO, + type : Ci.nsIAnnotationService.TYPE_STRING, + flags : 0, + value : description, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + var txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + onLocationFieldBlur: function EIO_onLocationFieldBlur() { + var uri; + try { + uri = PlacesUIUtils.createFixedURI(this._element("locationField").value); + } + catch(ex) { return; } + + if (!this._uri.equals(uri)) { + var txn = new PlacesEditBookmarkURITransaction(this._itemId, uri); + PlacesUtils.transactionManager.doTransaction(txn); + this._uri = uri; + } + }, + + onKeywordFieldBlur: function EIO_onKeywordFieldBlur() { + let oldKeyword = this._keyword; + let keyword = this._keyword = this._element("keywordField").value; + if (keyword != oldKeyword) { + let txn = new PlacesEditBookmarkKeywordTransaction(this._itemId, + keyword, + null, + oldKeyword); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + onLoadInSidebarCheckboxCommand: + function EIO_onLoadInSidebarCheckboxCommand() { + let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO }; + if (this._element("loadInSidebarCheckbox").checked) + annoObj.value = true; + let txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); + PlacesUtils.transactionManager.doTransaction(txn); + }, + + toggleFolderTreeVisibility: function EIO_toggleFolderTreeVisibility() { + var expander = this._element("foldersExpander"); + var folderTreeRow = this._element("folderTreeRow"); + if (!folderTreeRow.collapsed) { + expander.className = "expander-down"; + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextdown")); + folderTreeRow.collapsed = true; + this._element("chooseFolderSeparator").hidden = + this._element("chooseFolderMenuItem").hidden = false; + } + else { + expander.className = "expander-up" + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextup")); + folderTreeRow.collapsed = false; + + // XXXmano: Ideally we would only do this once, but for some odd reason, + // the editable mode set on this tree, together with its collapsed state + // breaks the view. + const FOLDER_TREE_PLACE_URI = + "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" + + PlacesUIUtils.allBookmarksFolderId; + this._folderTree.place = FOLDER_TREE_PLACE_URI; + + this._element("chooseFolderSeparator").hidden = + this._element("chooseFolderMenuItem").hidden = true; + var currentFolder = this._getFolderIdFromMenuList(); + this._folderTree.selectItems([currentFolder]); + this._folderTree.focus(); + } + }, + + _getFolderIdFromMenuList: + function EIO__getFolderIdFromMenuList() { + var selectedItem = this._folderMenuList.selectedItem; + NS_ASSERT("folderId" in selectedItem, + "Invalid menuitem in the folders-menulist"); + return selectedItem.folderId; + }, + + /** + * Get the corresponding menu-item in the folder-menu-list for a bookmarks + * folder if such an item exists. Otherwise, this creates a menu-item for the + * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached, + * the new item replaces the last menu-item. + * @param aFolderId + * The identifier of the bookmarks folder. + */ + _getFolderMenuItem: + function EIO__getFolderMenuItem(aFolderId) { + var menupopup = this._folderMenuList.menupopup; + + for (let i = 0; i < menupopup.childNodes.length; i++) { + if ("folderId" in menupopup.childNodes[i] && + menupopup.childNodes[i].folderId == aFolderId) + return menupopup.childNodes[i]; + } + + // 3 special folders + separator + folder-items-count limit + if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST) + menupopup.removeChild(menupopup.lastChild); + + return this._appendFolderItemToMenupopup(menupopup, aFolderId); + }, + + onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) { + // Set a selectedIndex attribute to show special icons + this._folderMenuList.setAttribute("selectedIndex", + this._folderMenuList.selectedIndex); + + if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") { + // reset the selection back to where it was and expand the tree + // (this menu-item is hidden when the tree is already visible + var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); + var item = this._getFolderMenuItem(container); + this._folderMenuList.selectedItem = item; + // XXXmano HACK: setTimeout 100, otherwise focus goes back to the + // menulist right away + setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this); + return; + } + + // Move the item + var container = this._getFolderIdFromMenuList(); + if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) { + var txn = new PlacesMoveItemTransaction(this._itemId, + container, + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.transactionManager.doTransaction(txn); + + // Mark the containing folder as recently-used if it isn't in the + // static list + if (container != PlacesUtils.unfiledBookmarksFolderId && + container != PlacesUtils.toolbarFolderId && + container != PlacesUtils.bookmarksMenuFolderId) + this._markFolderAsRecentlyUsed(container); + } + + // Update folder-tree selection + var folderTreeRow = this._element("folderTreeRow"); + if (!folderTreeRow.collapsed) { + var selectedNode = this._folderTree.selectedNode; + if (!selectedNode || + PlacesUtils.getConcreteItemId(selectedNode) != container) + this._folderTree.selectItems([container]); + } + }, + + onFolderTreeSelect: function EIO_onFolderTreeSelect() { + var selectedNode = this._folderTree.selectedNode; + + // Disable the "New Folder" button if we cannot create a new folder + this._element("newFolderButton") + .disabled = !this._folderTree.insertionPoint || !selectedNode; + + if (!selectedNode) + return; + + var folderId = PlacesUtils.getConcreteItemId(selectedNode); + if (this._getFolderIdFromMenuList() == folderId) + return; + + var folderItem = this._getFolderMenuItem(folderId); + this._folderMenuList.selectedItem = folderItem; + folderItem.doCommand(); + }, + + _markFolderAsRecentlyUsed: + function EIO__markFolderAsRecentlyUsed(aFolderId) { + var txns = []; + + // Expire old unused recent folders + var anno = this._getLastUsedAnnotationObject(false); + while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) { + var folderId = this._recentFolders.pop().folderId; + let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno); + txns.push(annoTxn); + } + + // Mark folder as recently used + anno = this._getLastUsedAnnotationObject(true); + let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno); + txns.push(annoTxn); + + let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + }, + + /** + * Returns an object which could then be used to set/unset the + * LAST_USED_ANNO annotation for a folder. + * + * @param aLastUsed + * Whether to set or unset the LAST_USED_ANNO annotation. + * @returns an object representing the annotation which could then be used + * with the transaction manager. + */ + _getLastUsedAnnotationObject: + function EIO__getLastUsedAnnotationObject(aLastUsed) { + var anno = { name: LAST_USED_ANNO, + type: Ci.nsIAnnotationService.TYPE_INT32, + flags: 0, + value: aLastUsed ? new Date().getTime() : null, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + + return anno; + }, + + _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() { + var tagsSelector = this._element("tagsSelector"); + var tagsSelectorRow = this._element("tagsSelectorRow"); + if (tagsSelectorRow.collapsed) + return; + + // Save the current scroll position and restore it after the rebuild. + let firstIndex = tagsSelector.getIndexOfFirstVisibleRow(); + let selectedIndex = tagsSelector.selectedIndex; + let selectedTag = selectedIndex >= 0 ? tagsSelector.selectedItem.label + : null; + + while (tagsSelector.hasChildNodes()) + tagsSelector.removeChild(tagsSelector.lastChild); + + var tagsInField = this._getTagsArrayFromTagField(); + var allTags = PlacesUtils.tagging.allTags; + for (var i = 0; i < allTags.length; i++) { + var tag = allTags[i]; + var elt = document.createElement("listitem"); + elt.setAttribute("type", "checkbox"); + elt.setAttribute("label", tag); + if (tagsInField.indexOf(tag) != -1) + elt.setAttribute("checked", "true"); + tagsSelector.appendChild(elt); + if (selectedTag === tag) + selectedIndex = tagsSelector.getIndexOfItem(elt); + } + + // Restore position. + // The listbox allows to scroll only if the required offset doesn't + // overflow its capacity, thus need to adjust the index for removals. + firstIndex = + Math.min(firstIndex, + tagsSelector.itemCount - tagsSelector.getNumberOfVisibleRows()); + tagsSelector.scrollToIndex(firstIndex); + if (selectedIndex >= 0 && tagsSelector.itemCount > 0) { + selectedIndex = Math.min(selectedIndex, tagsSelector.itemCount - 1); + tagsSelector.selectedIndex = selectedIndex; + tagsSelector.ensureIndexIsVisible(selectedIndex); + } + }, + + toggleTagsSelector: function EIO_toggleTagsSelector() { + var tagsSelector = this._element("tagsSelector"); + var tagsSelectorRow = this._element("tagsSelectorRow"); + var expander = this._element("tagsSelectorExpander"); + if (tagsSelectorRow.collapsed) { + expander.className = "expander-up"; + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextup")); + tagsSelectorRow.collapsed = false; + this._rebuildTagsSelectorList(); + + // This is a no-op if we've added the listener. + tagsSelector.addEventListener("CheckboxStateChange", this, false); + } + else { + expander.className = "expander-down"; + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextdown")); + tagsSelectorRow.collapsed = true; + } + }, + + /** + * Splits "tagsField" element value, returning an array of valid tag strings. + * + * @return Array of tag strings found in the field value. + */ + _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() { + let tags = this._element("tagsField").value; + return tags.trim() + .split(/\s*,\s*/) // Split on commas and remove spaces. + .filter(function (tag) tag.length > 0); // Kill empty tags. + }, + + newFolder: function EIO_newFolder() { + var ip = this._folderTree.insertionPoint; + + // default to the bookmarks menu folder + if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) { + ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Ci.nsITreeView.DROP_ON); + } + + // XXXmano: add a separate "New Folder" string at some point... + var defaultLabel = this._element("newFolderButton").label; + var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index); + PlacesUtils.transactionManager.doTransaction(txn); + this._folderTree.focus(); + this._folderTree.selectItems([this._lastNewItem]); + this._folderTree.startEditing(this._folderTree.view.selection.currentIndex, + this._folderTree.columns.getFirstColumn()); + }, + + // nsIDOMEventListener + handleEvent: function EIO_nsIDOMEventListener(aEvent) { + switch (aEvent.type) { + case "CheckboxStateChange": + // Update the tags field when items are checked/unchecked in the listbox + var tags = this._getTagsArrayFromTagField(); + + if (aEvent.target.checked) { + if (tags.indexOf(aEvent.target.label) == -1) + tags.push(aEvent.target.label); + } + else { + var indexOfItem = tags.indexOf(aEvent.target.label); + if (indexOfItem != -1) + tags.splice(indexOfItem, 1); + } + this._element("tagsField").value = tags.join(", "); + this._updateTags(); + break; + case "blur": + let replaceFn = (str, firstLetter) => firstLetter.toUpperCase(); + let nodeName = aEvent.target.id.replace(/editBMPanel_(\w)/, replaceFn); + this["on" + nodeName + "Blur"](); + break; + case "unload": + this.uninitPanel(false); + break; + } + }, + + // nsINavBookmarkObserver + onItemChanged: function EIO_onItemChanged(aItemId, aProperty, + aIsAnnotationProperty, aValue, + aLastModified, aItemType) { + if (aProperty == "tags") { + // Tags case is special, since they should be updated if either: + // - the notification is for the edited bookmark + // - the notification is for the edited history entry + // - the notification is for one of edited uris + let shouldUpdateTagsField = this._itemId == aItemId; + if (this._itemId == -1 || this._multiEdit) { + // Check if the changed uri is part of the modified ones. + let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId); + let uris = this._multiEdit ? this._uris : [this._uri]; + uris.forEach(function (aURI, aIndex) { + if (aURI.equals(changedURI)) { + shouldUpdateTagsField = true; + if (this._multiEdit) { + this._tags[aIndex] = PlacesUtils.tagging.getTagsForURI(this._uris[aIndex]); + } + } + }, this); + } + + if (shouldUpdateTagsField) { + if (this._multiEdit) { + this._allTags = this._getCommonTags(); + this._initTextField("tagsField", this._allTags.join(", "), false); + } + else { + let tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); + this._initTextField("tagsField", tags, false); + } + } + + // Any tags change should be reflected in the tags selector. + this._rebuildTagsSelectorList(); + return; + } + + if (this._itemId != aItemId) { + if (aProperty == "title") { + // If the title of a folder which is listed within the folders + // menulist has been changed, we need to update the label of its + // representing element. + var menupopup = this._folderMenuList.menupopup; + for (let i = 0; i < menupopup.childNodes.length; i++) { + if ("folderId" in menupopup.childNodes[i] && + menupopup.childNodes[i].folderId == aItemId) { + menupopup.childNodes[i].label = aValue; + break; + } + } + } + + return; + } + + switch (aProperty) { + case "title": + var namePicker = this._element("namePicker"); + if (namePicker.value != aValue) { + namePicker.value = aValue; + this._editorTransactionManagerClear(namePicker); + } + break; + case "uri": + var locationField = this._element("locationField"); + if (locationField.value != aValue) { + this._uri = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newURI(aValue, null, null); + this._initTextField("locationField", this._uri.spec); + this._initNamePicker(); + this._initTextField("tagsField", + PlacesUtils.tagging + .getTagsForURI(this._uri).join(", "), + false); + this._rebuildTagsSelectorList(); + } + break; + case "keyword": + this._keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId); + this._initTextField("keywordField", this._keyword); + break; + case PlacesUIUtils.DESCRIPTION_ANNO: + this._initTextField("descriptionField", + PlacesUIUtils.getItemDescription(this._itemId)); + break; + case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO: + this._element("loadInSidebarCheckbox").checked = + PlacesUtils.annotations.itemHasAnnotation(this._itemId, + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); + break; + case PlacesUtils.LMANNO_FEEDURI: + let feedURISpec = + PlacesUtils.annotations.getItemAnnotation(this._itemId, + PlacesUtils.LMANNO_FEEDURI); + this._initTextField("feedLocationField", feedURISpec, true); + break; + case PlacesUtils.LMANNO_SITEURI: + let siteURISpec = ""; + try { + siteURISpec = + PlacesUtils.annotations.getItemAnnotation(this._itemId, + PlacesUtils.LMANNO_SITEURI); + } catch (ex) {} + this._initTextField("siteLocationField", siteURISpec, true); + break; + } + }, + + onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex, + aNewParent, aNewIndex, aItemType) { + if (aItemId != this._itemId || + aNewParent == this._getFolderIdFromMenuList()) + return; + + var folderItem = this._getFolderMenuItem(aNewParent); + + // just setting selectItem _does not_ trigger oncommand, so we don't + // recurse + this._folderMenuList.selectedItem = folderItem; + }, + + onItemAdded: function EIO_onItemAdded(aItemId, aParentId, aIndex, aItemType, + aURI) { + this._lastNewItem = aItemId; + }, + + onItemRemoved: function() { }, + onBeginUpdateBatch: function() { }, + onEndUpdateBatch: function() { }, + onItemVisited: function() { }, +}; diff --git a/webbrowser/components/places/content/editBookmarkOverlay.xul b/webbrowser/components/places/content/editBookmarkOverlay.xul new file mode 100644 index 0000000..196369d --- /dev/null +++ b/webbrowser/components/places/content/editBookmarkOverlay.xul @@ -0,0 +1,228 @@ + + + +%editBookmarkOverlayDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +